Files
claude-mem/hook-templates/shared/hook-helpers.js
T
Alex Newman 5244a12422 Release v3.9.11
Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
2025-10-03 20:55:36 -04:00

431 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Hook Helper Functions
*
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
*/
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));
/**
* Creates a standardized hook response using the HookTemplates system
* @param {string} hookType - Type of hook ('PreCompact' or 'SessionStart')
* @param {boolean} success - Whether the operation was successful
* @param {Object} options - Additional options
* @returns {Object} Formatted hook response
*/
export function createHookResponse(hookType, success, options = {}) {
if (hookType === 'PreCompact') {
if (success) {
return {
continue: true,
suppressOutput: true
};
} else {
return {
continue: false,
stopReason: options.reason || 'Pre-compact operation failed',
suppressOutput: true
};
}
}
if (hookType === 'SessionStart') {
if (success && options.context) {
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: options.context
}
};
} else {
return {
continue: true,
suppressOutput: true
};
}
}
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
return {
continue: true,
suppressOutput: true
};
}
if (hookType === 'Stop') {
return {
continue: true,
suppressOutput: true
};
}
// Generic response for unknown hook types
return {
continue: success,
suppressOutput: true,
...(options.reason && !success ? { stopReason: options.reason } : {})
};
}
/**
* Formats a session start context message using standardized templates
* @param {Object} contextData - Context information
* @returns {string} Formatted context message
*/
export function formatSessionStartContext(contextData) {
const {
projectName = 'unknown project',
memoryCount = 0,
lastSessionTime,
recentComponents = [],
recentDecisions = []
} = contextData;
const timeInfo = lastSessionTime ? ` (last worked: ${lastSessionTime})` : '';
const contextParts = [];
contextParts.push(`🧠 Loaded ${memoryCount} memories from previous sessions for ${projectName}${timeInfo}`);
if (recentComponents.length > 0) {
contextParts.push(`\n🎯 Recent components: ${recentComponents.slice(0, 3).join(', ')}`);
}
if (recentDecisions.length > 0) {
contextParts.push(`\n🔄 Recent decisions: ${recentDecisions.slice(0, 2).join(', ')}`);
}
if (memoryCount > 0) {
contextParts.push('\n💡 Use search_nodes("keywords") to find related work or open_nodes(["entity_name"]) to load specific components');
}
return contextParts.join('');
}
/**
* Executes a CLI command and returns the result
* @param {string} command - CLI command to execute
* @param {Array} args - Command arguments
* @param {Object} options - Spawn options
* @returns {Promise<{stdout: string, stderr: string, success: boolean}>}
*/
export async function executeCliCommand(command, args = [], options = {}) {
return new Promise((resolve) => {
const { input, ...spawnOptions } = options;
const process = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
...spawnOptions
});
let stdout = '';
let stderr = '';
if (process.stdout) {
process.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (process.stderr) {
process.stderr.on('data', (data) => {
stderr += data.toString();
});
}
if (input && process.stdin) {
process.stdin.write(input);
process.stdin.end();
} else if (process.stdin) {
process.stdin.end();
}
process.on('close', (code) => {
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
success: code === 0
});
});
process.on('error', (error) => {
resolve({
stdout: '',
stderr: error.message,
success: false
});
});
});
}
/**
* Parses context data from CLI output
* @param {string} output - Raw CLI output
* @returns {Object} Parsed context data
*/
export function parseContextData(output) {
if (!output || !output.trim()) {
return {
memoryCount: 0,
recentComponents: [],
recentDecisions: []
};
}
// Try to parse as JSON first (if CLI outputs structured data)
try {
const parsed = JSON.parse(output);
return {
memoryCount: parsed.memoryCount || 0,
recentComponents: parsed.recentComponents || [],
recentDecisions: parsed.recentDecisions || [],
lastSessionTime: parsed.lastSessionTime
};
} catch (e) {
// If not JSON, treat as plain text context
const lines = output.split('\n').filter(line => line.trim());
return {
memoryCount: lines.length,
recentComponents: [],
recentDecisions: [],
rawContext: output
};
}
}
/**
* Validates hook payload structure
* @param {Object} payload - Hook payload to validate
* @param {string} expectedHookType - Expected hook event name
* @returns {{valid: boolean, error?: string}} Validation result
*/
export function validateHookPayload(payload, expectedHookType) {
if (!payload || typeof payload !== 'object') {
return { valid: false, error: 'Payload must be a valid object' };
}
if (!payload.session_id || typeof payload.session_id !== 'string') {
return { valid: false, error: 'Missing or invalid session_id' };
}
if (!payload.transcript_path || typeof payload.transcript_path !== 'string') {
return { valid: false, error: 'Missing or invalid transcript_path' };
}
if (expectedHookType && payload.hook_event_name !== expectedHookType) {
return { valid: false, error: `Expected hook_event_name to be ${expectedHookType}` };
}
return { valid: true };
}
/**
* Logs debug information if debug mode is enabled
* @param {string} message - Debug message
* @param {Object} data - Additional data to log
*/
export function debugLog(message, data = {}) {
if (process.env.DEBUG === 'true' || process.env.CLAUDE_MEM_DEBUG === 'true') {
const timestamp = new Date().toISOString();
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;
}