Release v3.9.11
Published from npm package build Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, getActiveStreamingSessionsForProject } from './shared/hook-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
@@ -61,15 +61,18 @@ process.stdin.on('end', async () => {
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
// Load SDK session info from database
|
||||
const db = initializeDatabase();
|
||||
|
||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||
if (!sessions || sessions.length === 0) {
|
||||
debugLog('PostToolUse: No streaming session found', { project });
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
const sdkSessionId = sessionData.sdkSessionId;
|
||||
const sessionData = sessions[0];
|
||||
const sdkSessionId = sessionData.sdk_session_id;
|
||||
|
||||
// Convert tool response to string
|
||||
const toolResponseStr = typeof tool_response === 'string'
|
||||
@@ -135,6 +138,9 @@ process.stdin.on('end', async () => {
|
||||
}
|
||||
|
||||
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* Hook Helper Functions
|
||||
*
|
||||
*
|
||||
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
||||
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
||||
*/
|
||||
@@ -10,6 +10,9 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Database from 'better-sqlite3';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -235,3 +238,193 @@ export function debugLog(message, data = {}) {
|
||||
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATABASE HELPERS (inline SQL to avoid 'claude-mem' import issues)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the claude-mem data directory path
|
||||
*/
|
||||
function getDataDirectory() {
|
||||
return join(os.homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the database connection
|
||||
*/
|
||||
function getDatabase() {
|
||||
const dataDir = getDataDirectory();
|
||||
const dbPath = join(dataDir, 'claude-mem.db');
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Apply optimized SQLite settings
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
db.pragma('temp_store = memory');
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the streaming_sessions table exists
|
||||
*/
|
||||
function ensureStreamingSessionsTable(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
updated_at TEXT,
|
||||
updated_at_epoch INTEGER,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'failed'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indices if they don't exist
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id
|
||||
ON streaming_sessions(claude_session_id)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id
|
||||
ON streaming_sessions(sdk_session_id)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project_status
|
||||
ON streaming_sessions(project, status)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new streaming session record
|
||||
*/
|
||||
export function createStreamingSession(db, { claude_session_id, project, user_prompt, started_at }) {
|
||||
ensureStreamingSessionsTable(db);
|
||||
|
||||
const timestamp = started_at || new Date().toISOString();
|
||||
const epoch = new Date(timestamp).getTime();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO streaming_sessions (
|
||||
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
|
||||
) VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`);
|
||||
|
||||
const info = stmt.run(claude_session_id, project, user_prompt || null, timestamp, epoch);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(info.lastInsertRowid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a streaming session by internal ID
|
||||
*/
|
||||
export function updateStreamingSession(db, id, updates) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const epoch = Date.now();
|
||||
|
||||
const parts = [];
|
||||
const values = [];
|
||||
|
||||
if (updates.sdk_session_id !== undefined) {
|
||||
parts.push('sdk_session_id = ?');
|
||||
values.push(updates.sdk_session_id);
|
||||
}
|
||||
if (updates.title !== undefined) {
|
||||
parts.push('title = ?');
|
||||
values.push(updates.title);
|
||||
}
|
||||
if (updates.subtitle !== undefined) {
|
||||
parts.push('subtitle = ?');
|
||||
values.push(updates.subtitle);
|
||||
}
|
||||
if (updates.status !== undefined) {
|
||||
parts.push('status = ?');
|
||||
values.push(updates.status);
|
||||
}
|
||||
if (updates.completed_at !== undefined) {
|
||||
const completedTimestamp = typeof updates.completed_at === 'string'
|
||||
? updates.completed_at
|
||||
: new Date(updates.completed_at).toISOString();
|
||||
const completedEpoch = new Date(completedTimestamp).getTime();
|
||||
parts.push('completed_at = ?', 'completed_at_epoch = ?');
|
||||
values.push(completedTimestamp, completedEpoch);
|
||||
}
|
||||
|
||||
// Always update the updated_at timestamp
|
||||
parts.push('updated_at = ?', 'updated_at_epoch = ?');
|
||||
values.push(timestamp, epoch);
|
||||
|
||||
values.push(id);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE streaming_sessions
|
||||
SET ${parts.join(', ')}
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...values);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active streaming sessions for a project
|
||||
*/
|
||||
export function getActiveStreamingSessionsForProject(db, project) {
|
||||
ensureStreamingSessionsTable(db);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM streaming_sessions
|
||||
WHERE project = ? AND status = 'active'
|
||||
ORDER BY started_at_epoch DESC
|
||||
`);
|
||||
|
||||
return stmt.all(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a session as completed
|
||||
*/
|
||||
export function markStreamingSessionCompleted(db, id) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const epoch = Date.now();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE streaming_sessions
|
||||
SET status = ?,
|
||||
completed_at = ?,
|
||||
completed_at_epoch = ?,
|
||||
updated_at = ?,
|
||||
updated_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run('completed', timestamp, epoch, timestamp, epoch, id);
|
||||
|
||||
return db.prepare('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database with migrations and return connection
|
||||
*/
|
||||
export function initializeDatabase() {
|
||||
const db = getDatabase();
|
||||
ensureStreamingSessionsTable(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
+23
-10
@@ -12,8 +12,8 @@ import fs from 'fs';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, getActiveStreamingSessionsForProject, markStreamingSessionCompleted } from './shared/hook-helpers.js';
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
@@ -50,20 +50,31 @@ process.stdin.on('end', async () => {
|
||||
const { cwd } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
|
||||
// Immediately clear activity flag for UI indicator
|
||||
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||
try {
|
||||
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: false, timestamp: Date.now() }));
|
||||
} catch (error) {
|
||||
// Silent fail - non-critical
|
||||
}
|
||||
|
||||
// Return immediately with async mode
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
// Load SDK session info from database
|
||||
const db = initializeDatabase();
|
||||
|
||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
||||
if (!sessions || sessions.length === 0) {
|
||||
debugLog('Stop: No streaming session found', { project });
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
const sdkSessionId = sessionData.sdkSessionId;
|
||||
const claudeSessionId = sessionData.claudeSessionId;
|
||||
const sessionData = sessions[0];
|
||||
const sdkSessionId = sessionData.sdk_session_id;
|
||||
const claudeSessionId = sessionData.claude_session_id;
|
||||
|
||||
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
||||
|
||||
@@ -108,10 +119,12 @@ process.stdin.on('end', async () => {
|
||||
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
||||
}
|
||||
|
||||
// Clean up session file
|
||||
fs.unlinkSync(sessionFile);
|
||||
debugLog('Stop: Session ended and cleaned up', { project });
|
||||
// Mark session as completed in database
|
||||
markStreamingSessionCompleted(db, sessionData.id);
|
||||
debugLog('Stop: Session ended and marked complete', { project, sessionId: sessionData.id });
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('Stop: Error ending session', { error: error.message });
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
import { initializeDatabase, createStreamingSession, updateStreamingSession } from './shared/hook-helpers.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
@@ -60,6 +60,14 @@ process.stdin.on('end', async () => {
|
||||
|
||||
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
|
||||
|
||||
// Immediately signal activity start for UI indicator
|
||||
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
||||
try {
|
||||
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: true, project, timestamp: Date.now() }));
|
||||
} catch (error) {
|
||||
// Silent fail - non-critical
|
||||
}
|
||||
|
||||
// Generate title and subtitle non-blocking
|
||||
if (prompt && session_id && project) {
|
||||
import('child_process').then(({ spawn }) => {
|
||||
@@ -80,6 +88,22 @@ process.stdin.on('end', async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize database and create session record FIRST
|
||||
const db = initializeDatabase();
|
||||
|
||||
// Create session record immediately - this gives us a tracking ID
|
||||
const sessionRecord = createStreamingSession(db, {
|
||||
claude_session_id: session_id,
|
||||
project,
|
||||
user_prompt: prompt,
|
||||
started_at: timestamp
|
||||
});
|
||||
|
||||
debugLog('UserPromptSubmit: Created session record', {
|
||||
internalId: sessionRecord.id,
|
||||
claudeSessionId: session_id
|
||||
});
|
||||
|
||||
// Build system prompt using centralized config
|
||||
const systemPrompt = renderSystemPrompt({
|
||||
project,
|
||||
@@ -110,19 +134,19 @@ process.stdin.on('end', async () => {
|
||||
}
|
||||
|
||||
if (sdkSessionId) {
|
||||
// Save session info for other hooks
|
||||
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
fs.writeFileSync(sessionFile, JSON.stringify({
|
||||
sdkSessionId,
|
||||
claudeSessionId: session_id,
|
||||
project,
|
||||
startedAt: timestamp,
|
||||
date
|
||||
}, null, 2));
|
||||
// Update session record with SDK session ID
|
||||
updateStreamingSession(db, sessionRecord.id, {
|
||||
sdk_session_id: sdkSessionId
|
||||
});
|
||||
|
||||
debugLog('UserPromptSubmit: SDK session started', { sdkSessionId, sessionFile });
|
||||
debugLog('UserPromptSubmit: SDK session started', {
|
||||
internalId: sessionRecord.id,
|
||||
sdkSessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Close database connection
|
||||
db.close();
|
||||
} catch (error) {
|
||||
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user