refactor: remove deprecated hook templates and related utilities
- Deleted hook-prompt-renderer.js and hook-prompts.config.js as they are no longer needed. - Removed path-resolver.js, stop.js, and user-prompt-submit.js for streamlining the codebase. - Updated package.json scripts to point to new build and publish scripts. - Adjusted test imports to reflect new directory structure after removing unnecessary files.
This commit is contained in:
@@ -5,3 +5,4 @@ node_modules/
|
|||||||
.env.local
|
.env.local
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
.claude/
|
||||||
@@ -353,22 +353,24 @@ claude-mem changelog --historical 5
|
|||||||
|
|
||||||
### Memory System
|
### Memory System
|
||||||
|
|
||||||
**Rolling Memory** - Real-time conversation turn capture via hooks with immediate ChromaDB storage
|
**Hook Layer** - Real-time event capture via Claude Code streaming hooks with sub-50ms overhead
|
||||||
|
|
||||||
**TranscriptCompressor** - Intelligent chunking and compression of large conversations
|
**SDK Worker** - Agent SDK subprocess for async observation processing and session summarization
|
||||||
|
|
||||||
|
**Observation Queue** - SQLite-based queue for buffering tool observations between hooks and SDK
|
||||||
|
|
||||||
|
**Session Storage** - SQLite tables for tracking SDK sessions, observations, and summaries
|
||||||
|
|
||||||
**MCP Server** - 15+ ChromaDB tools for memory operations and semantic search
|
**MCP Server** - 15+ ChromaDB tools for memory operations and semantic search
|
||||||
|
|
||||||
**SQLite Backend** - Session tracking, metadata management, and diagnostics storage
|
|
||||||
|
|
||||||
### Hook Integration
|
### Hook Integration
|
||||||
|
|
||||||
Hooks communicate via JSON stdin/stdout and run with minimal overhead:
|
Hooks communicate via JSON stdin/stdout and run with minimal overhead:
|
||||||
|
|
||||||
1. **user-prompt-submit** - Stores user prompt immediately in ChromaDB
|
1. **new** (user-prompt-submit) - Creates SDK session and initializes Agent SDK worker for async processing
|
||||||
2. **post-tool-use** - Spawns Agent SDK subprocess for async compression
|
2. **save** (post-tool-use) - Queues tool observations for async SDK processing
|
||||||
3. **stop-streaming** - Generates session overview, deletes SDK transcript
|
3. **summary** (stop-streaming) - Triggers finalization, generates session overview, and cleanup
|
||||||
4. **session-start** - Loads project-specific context invisibly
|
4. **context** (session-start) - Loads project-specific session summaries automatically
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
@@ -376,14 +378,16 @@ Hooks communicate via JSON stdin/stdout and run with minimal overhead:
|
|||||||
src/
|
src/
|
||||||
├── bin/ # CLI entry point
|
├── bin/ # CLI entry point
|
||||||
├── commands/ # Command implementations
|
├── commands/ # Command implementations
|
||||||
├── core/ # Core compression logic
|
├── hooks/ # Hook implementations (new, save, summary, context)
|
||||||
├── services/ # SQLite, ChromaDB, path discovery
|
├── sdk/ # Agent SDK worker, prompts, and parser
|
||||||
├── shared/ # Configuration and utilities
|
├── services/ # SQLite database and path discovery
|
||||||
└── mcp-server.ts # MCP server implementation
|
├── shared/ # Configuration, paths, settings, and types
|
||||||
|
└── utils/ # Platform utilities
|
||||||
|
|
||||||
hook-templates/ # Hook source files
|
|
||||||
dist/ # Minified production bundle
|
dist/ # Minified production bundle
|
||||||
test/ # Unit and integration tests
|
tests/ # Database, SDK, and integration tests
|
||||||
|
docs/ # Documentation (BUILD.md, CHANGELOG.md)
|
||||||
|
scripts/ # Build and publish automation
|
||||||
```
|
```
|
||||||
|
|
||||||
## :wrench: Configuration
|
## :wrench: Configuration
|
||||||
|
|||||||
+19
-7
@@ -14,7 +14,7 @@ Build the project to create a bundled, minified executable:
|
|||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
# or
|
# or
|
||||||
node build.js
|
node scripts/build.js
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
@@ -38,7 +38,7 @@ To publish a new version to npm:
|
|||||||
```bash
|
```bash
|
||||||
npm run publish:npm
|
npm run publish:npm
|
||||||
# or
|
# or
|
||||||
node publish.js
|
node scripts/publish.js
|
||||||
```
|
```
|
||||||
|
|
||||||
The publish script will:
|
The publish script will:
|
||||||
@@ -99,12 +99,24 @@ claude-mem/
|
|||||||
├── src/ # TypeScript source
|
├── src/ # TypeScript source
|
||||||
│ ├── bin/cli.ts # CLI entry point
|
│ ├── bin/cli.ts # CLI entry point
|
||||||
│ ├── commands/ # Command implementations
|
│ ├── commands/ # Command implementations
|
||||||
│ ├── services/ # Core services
|
│ ├── hooks/ # Hook implementations
|
||||||
│ └── shared/ # Shared utilities
|
│ ├── sdk/ # Agent SDK worker
|
||||||
|
│ ├── services/ # SQLite and path services
|
||||||
|
│ ├── shared/ # Configuration and types
|
||||||
|
│ └── utils/ # Platform utilities
|
||||||
├── dist/ # Build output
|
├── dist/ # Build output
|
||||||
│ └── claude-mem.min.js # Bundled executable
|
│ └── claude-mem.min.js # Bundled executable
|
||||||
├── build.js # Build script
|
├── tests/ # Test files
|
||||||
├── publish.js # Publish script
|
│ ├── database-schema.test.ts
|
||||||
|
│ ├── sdk-prompts-parser.test.ts
|
||||||
|
│ ├── hooks-database-integration.test.ts
|
||||||
|
│ └── session-lifecycle.test.ts
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── BUILD.md # This file
|
||||||
|
│ └── CHANGELOG.md # Release notes
|
||||||
|
├── scripts/ # Build automation
|
||||||
|
│ ├── build.js # Build script
|
||||||
|
│ └── publish.js # Publish script
|
||||||
└── package.json # Package configuration
|
└── package.json # Package configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,4 +125,4 @@ claude-mem/
|
|||||||
- The build process embeds the version from `package.json` at build time
|
- The build process embeds the version from `package.json` at build time
|
||||||
- `prepublishOnly` script ensures build runs before npm publish
|
- `prepublishOnly` script ensures build runs before npm publish
|
||||||
- Dependencies are bundled except for external packages
|
- Dependencies are bundled except for external packages
|
||||||
- The published package includes: `dist/`, `hook-templates/`, `commands/`, `src/`
|
- The published package includes: `dist/`, `hook-templates/`, `commands/`, `src/`, `docs/`
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Post Tool Use Hook - Streaming SDK Version
|
|
||||||
*
|
|
||||||
* Feeds tool responses to the streaming SDK session for real-time processing.
|
|
||||||
* SDK decides what to store and calls bash commands directly.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
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, acquireSessionLock, releaseSessionLock, cleanupStaleLocks } from './shared/hook-helpers.js';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
|
||||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
|
||||||
process.stderr.write(logLine);
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail on log errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed: buildStreamingToolMessage function
|
|
||||||
// Now using centralized config from hook-prompt-renderer.js
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// GRACEFUL SHUTDOWN HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let db;
|
|
||||||
let lockAcquired = false;
|
|
||||||
let sdkSessionId = null;
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
if (lockAcquired && sdkSessionId && db) {
|
|
||||||
try {
|
|
||||||
releaseSessionLock(db, sdkSessionId);
|
|
||||||
debugLog('PostToolUse: Released session lock on shutdown', { sdkSessionId });
|
|
||||||
} catch (err) {
|
|
||||||
// Silent fail on cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
// Silent fail on cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
debugLog('PostToolUse: Received SIGTERM, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
debugLog('PostToolUse: Received SIGINT, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
let input = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
||||||
|
|
||||||
process.stdin.on('end', async () => {
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = input ? JSON.parse(input) : {};
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('PostToolUse: JSON parse error', { error: error.message });
|
|
||||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tool_name, tool_response, prompt, cwd, timestamp } = payload;
|
|
||||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
|
||||||
|
|
||||||
// Return immediately - process async in background (don't block next tool)
|
|
||||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Load SDK session info from database
|
|
||||||
db = initializeDatabase();
|
|
||||||
|
|
||||||
// Clean up any stale locks first
|
|
||||||
cleanupStaleLocks(db);
|
|
||||||
|
|
||||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
|
||||||
if (!sessions || sessions.length === 0) {
|
|
||||||
debugLog('PostToolUse: No streaming session found', { project });
|
|
||||||
db.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionData = sessions[0];
|
|
||||||
sdkSessionId = sessionData.sdk_session_id;
|
|
||||||
|
|
||||||
// Validate SDK session ID exists
|
|
||||||
if (!sdkSessionId) {
|
|
||||||
debugLog('PostToolUse: SDK session ID not yet available', { project });
|
|
||||||
db.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to acquire lock - if another hook has it, skip this tool
|
|
||||||
lockAcquired = acquireSessionLock(db, sdkSessionId, 'PostToolUse');
|
|
||||||
if (!lockAcquired) {
|
|
||||||
debugLog('PostToolUse: Session locked by another hook, skipping', { sdkSessionId });
|
|
||||||
db.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert tool response to string
|
|
||||||
const toolResponseStr = typeof tool_response === 'string'
|
|
||||||
? tool_response
|
|
||||||
: JSON.stringify(tool_response);
|
|
||||||
|
|
||||||
// Build message for SDK using centralized config
|
|
||||||
const message = renderToolMessage({
|
|
||||||
toolName: tool_name,
|
|
||||||
toolResponse: toolResponseStr,
|
|
||||||
userPrompt: prompt || '',
|
|
||||||
timestamp: timestamp || new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send to SDK and wait for processing to complete using centralized config
|
|
||||||
const response = query({
|
|
||||||
prompt: message,
|
|
||||||
options: {
|
|
||||||
model: HOOK_CONFIG.sdk.model,
|
|
||||||
resume: sdkSessionId,
|
|
||||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
|
||||||
maxTokens: HOOK_CONFIG.sdk.maxTokensTool,
|
|
||||||
cwd // Must match where transcript was created
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Consume the stream to let SDK fully process
|
|
||||||
for await (const msg of response) {
|
|
||||||
debugLog('PostToolUse: SDK message', { type: msg.type, subtype: msg.subtype });
|
|
||||||
|
|
||||||
// SDK messages are structured differently than we expected
|
|
||||||
// - type: 'assistant' contains the assistant's response with content blocks
|
|
||||||
// - Content blocks can be text or tool_use
|
|
||||||
// - type: 'user' contains tool results
|
|
||||||
// - type: 'result' is the final summary
|
|
||||||
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'text') {
|
|
||||||
debugLog('PostToolUse: SDK text', { text: block.text?.slice(0, 200) });
|
|
||||||
} else if (block.type === 'tool_use') {
|
|
||||||
debugLog('PostToolUse: SDK tool_use', {
|
|
||||||
tool: block.name,
|
|
||||||
input: JSON.stringify(block.input).slice(0, 200)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'user' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'tool_result') {
|
|
||||||
debugLog('PostToolUse: SDK tool_result', {
|
|
||||||
tool_use_id: block.tool_use_id,
|
|
||||||
content: typeof block.content === 'string' ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'result') {
|
|
||||||
debugLog('PostToolUse: SDK result', {
|
|
||||||
subtype: msg.subtype,
|
|
||||||
is_error: msg.is_error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('PostToolUse: Error sending to SDK', { error: error.message, stack: error.stack });
|
|
||||||
} finally {
|
|
||||||
// Always release lock and close database
|
|
||||||
if (lockAcquired && sdkSessionId && db) {
|
|
||||||
try {
|
|
||||||
releaseSessionLock(db, sdkSessionId);
|
|
||||||
debugLog('PostToolUse: Released session lock', { sdkSessionId });
|
|
||||||
} catch (err) {
|
|
||||||
debugLog('PostToolUse: Error releasing lock', { error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
debugLog('PostToolUse: Error closing database', { error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit cleanly after async processing completes
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session Start Hook (SDK Version)
|
|
||||||
*
|
|
||||||
* Calls the CLI to load relevant context from ChromaDB at session start.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createHookResponse, debugLog } from './shared/hook-helpers.js';
|
|
||||||
|
|
||||||
// Read stdin
|
|
||||||
let input = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
process.stdin.on('data', (chunk) => {
|
|
||||||
input += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.on('end', async () => {
|
|
||||||
const payload = input ? JSON.parse(input) : {};
|
|
||||||
|
|
||||||
debugLog('SessionStart hook invoked (SDK version)', { cwd: payload.cwd });
|
|
||||||
|
|
||||||
const { cwd, source } = payload;
|
|
||||||
|
|
||||||
// Run on startup or /clear
|
|
||||||
if (source !== 'startup' && source !== 'clear') {
|
|
||||||
const response = createHookResponse('SessionStart', true);
|
|
||||||
console.log(JSON.stringify(response));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call the CLI to load context
|
|
||||||
const { executeCliCommand } = await import('./shared/hook-helpers.js');
|
|
||||||
|
|
||||||
const result = await executeCliCommand('claude-mem', ['load-context', '--format', 'session-start']);
|
|
||||||
|
|
||||||
if (result.success && result.stdout) {
|
|
||||||
// Per Claude Code docs: for SessionStart, stdout with exit code 0 is added to context
|
|
||||||
// Use plain stdout instead of JSON to ensure it appears in Claude's context
|
|
||||||
console.log(result.stdout);
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
// Return without context - use JSON with suppressOutput to avoid empty context
|
|
||||||
const response = createHookResponse('SessionStart', true);
|
|
||||||
console.log(JSON.stringify(response));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Continue without context on error
|
|
||||||
const response = createHookResponse('SessionStart', true);
|
|
||||||
console.log(JSON.stringify(response));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared configuration loader utility for Claude Memory hooks
|
|
||||||
* Loads CLI command name from config.json with proper fallback handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { join, dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { readFileSync, existsSync } from 'fs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the CLI command name from the hooks config.json file
|
|
||||||
* @returns {string} The CLI command name (defaults to 'claude-mem')
|
|
||||||
*/
|
|
||||||
export function loadCliCommand() {
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const configPath = join(__dirname, '..', 'config.json');
|
|
||||||
|
|
||||||
if (existsSync(configPath)) {
|
|
||||||
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
||||||
return config.cliCommand || 'claude-mem';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'claude-mem';
|
|
||||||
}
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 'bun:sqlite';
|
|
||||||
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.query(`
|
|
||||||
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.query('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.query(`
|
|
||||||
UPDATE streaming_sessions
|
|
||||||
SET ${parts.join(', ')}
|
|
||||||
WHERE id = ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
stmt.run(...values);
|
|
||||||
|
|
||||||
return db.query('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.query(`
|
|
||||||
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.query(`
|
|
||||||
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.query('SELECT * FROM streaming_sessions WHERE id = ?').get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize database with migrations and return connection
|
|
||||||
*/
|
|
||||||
export function initializeDatabase() {
|
|
||||||
const db = getDatabase();
|
|
||||||
ensureStreamingSessionsTable(db);
|
|
||||||
ensureSessionLocksTable(db);
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SESSION LOCKING (prevents concurrent SDK resume)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the session_locks table exists
|
|
||||||
*/
|
|
||||||
function ensureSessionLocksTable(db) {
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS session_locks (
|
|
||||||
sdk_session_id TEXT PRIMARY KEY,
|
|
||||||
locked_by TEXT NOT NULL,
|
|
||||||
locked_at TEXT NOT NULL,
|
|
||||||
locked_at_epoch INTEGER NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to acquire a lock on an SDK session
|
|
||||||
* @returns {boolean} true if lock acquired, false if already locked
|
|
||||||
*/
|
|
||||||
export function acquireSessionLock(db, sdkSessionId, lockOwner) {
|
|
||||||
ensureSessionLocksTable(db);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const epoch = Date.now();
|
|
||||||
|
|
||||||
const stmt = db.query(`
|
|
||||||
INSERT INTO session_locks (sdk_session_id, locked_by, locked_at, locked_at_epoch)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
stmt.run(sdkSessionId, lockOwner, timestamp, epoch);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
// UNIQUE constraint violation = already locked
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release a lock on an SDK session
|
|
||||||
*/
|
|
||||||
export function releaseSessionLock(db, sdkSessionId) {
|
|
||||||
ensureSessionLocksTable(db);
|
|
||||||
|
|
||||||
const stmt = db.query(`
|
|
||||||
DELETE FROM session_locks
|
|
||||||
WHERE sdk_session_id = ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
stmt.run(sdkSessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up stale locks (older than 5 minutes)
|
|
||||||
*/
|
|
||||||
export function cleanupStaleLocks(db) {
|
|
||||||
ensureSessionLocksTable(db);
|
|
||||||
|
|
||||||
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
|
||||||
|
|
||||||
const stmt = db.query(`
|
|
||||||
DELETE FROM session_locks
|
|
||||||
WHERE locked_at_epoch < ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
stmt.run(fiveMinutesAgo);
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
// src/prompts/hook-prompts.config.ts
|
|
||||||
var HOOK_CONFIG = {
|
|
||||||
maxUserPromptLength: 200,
|
|
||||||
maxToolResponseLength: 20000,
|
|
||||||
sdk: {
|
|
||||||
model: "claude-sonnet-4-5",
|
|
||||||
allowedTools: ["Bash"],
|
|
||||||
maxTokensSystem: 8192,
|
|
||||||
maxTokensTool: 8192,
|
|
||||||
maxTokensEnd: 2048
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
|
||||||
|
|
||||||
# SESSION CONTEXT
|
|
||||||
- Project: {{project}}
|
|
||||||
- Session: {{sessionId}}
|
|
||||||
- Date: {{date}}
|
|
||||||
- User Request: "{{userPrompt}}"
|
|
||||||
|
|
||||||
# YOUR JOB
|
|
||||||
|
|
||||||
## FIRST: Generate Session Title
|
|
||||||
|
|
||||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
|
||||||
|
|
||||||
Use this bash command:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem update-session-metadata \\
|
|
||||||
--project "{{project}}" \\
|
|
||||||
--session "{{sessionId}}" \\
|
|
||||||
--title "Short title (3-6 words)" \\
|
|
||||||
--subtitle "One sentence description (max 20 words)"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Example for "Help me add dark mode to my app":
|
|
||||||
- Title: "Dark Mode Implementation"
|
|
||||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
|
||||||
|
|
||||||
## THEN: Process Tool Responses
|
|
||||||
|
|
||||||
You will receive a stream of tool responses. For each one:
|
|
||||||
|
|
||||||
1. ANALYZE: Does this contain information worth remembering?
|
|
||||||
2. DECIDE: Should I store this or skip it?
|
|
||||||
3. EXTRACT: What are the key semantic concepts?
|
|
||||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
|
||||||
5. STORE: Use bash to save the hierarchical memory
|
|
||||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
|
||||||
|
|
||||||
# WHAT TO STORE
|
|
||||||
|
|
||||||
Store these:
|
|
||||||
- File contents with logic, algorithms, or patterns
|
|
||||||
- Search results revealing project structure
|
|
||||||
- Build errors or test failures with context
|
|
||||||
- Code revealing architecture or design decisions
|
|
||||||
- Git diffs with significant changes
|
|
||||||
- Command outputs showing system state
|
|
||||||
|
|
||||||
Skip these:
|
|
||||||
- Simple status checks (git status with no changes)
|
|
||||||
- Trivial edits (one-line config changes)
|
|
||||||
- Repeated operations
|
|
||||||
- Binary data or noise
|
|
||||||
- Anything without semantic value
|
|
||||||
|
|
||||||
# HIERARCHICAL MEMORY FORMAT
|
|
||||||
|
|
||||||
Each memory has FOUR components:
|
|
||||||
|
|
||||||
## 1. TITLE (3-8 words)
|
|
||||||
A scannable headline that captures the core action or topic.
|
|
||||||
Examples:
|
|
||||||
- "SDK Transcript Cleanup Implementation"
|
|
||||||
- "Hook System Architecture Analysis"
|
|
||||||
- "ChromaDB Migration Planning"
|
|
||||||
|
|
||||||
## 2. SUBTITLE (max 24 words)
|
|
||||||
A concise, memorable summary that captures the essence of the change.
|
|
||||||
Examples:
|
|
||||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
|
||||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
|
||||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Clear and descriptive
|
|
||||||
- Focus on the outcome or benefit
|
|
||||||
- Use active voice when possible
|
|
||||||
- Keep it professional and informative
|
|
||||||
|
|
||||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
|
||||||
Individual, searchable statements that can be vector-embedded separately.
|
|
||||||
Each fact is ONE specific piece of information.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
|
||||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
|
||||||
- "Uses fs.unlink with graceful error handling for missing files"
|
|
||||||
- "Checks two transcript path formats for backward compatibility"
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Start with filename or component when relevant
|
|
||||||
- Be specific: include paths, function names, actual values
|
|
||||||
- Each fact stands alone (no pronouns like "it" or "this")
|
|
||||||
- 50-150 characters target
|
|
||||||
- Focus on searchable technical details
|
|
||||||
|
|
||||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
|
||||||
The full contextual story for deep dives:
|
|
||||||
|
|
||||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
|
||||||
|
|
||||||
This is the detailed explanation for when someone needs full context.
|
|
||||||
|
|
||||||
# STORAGE COMMAND FORMAT
|
|
||||||
|
|
||||||
Store using this EXACT bash command structure:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-memory \\
|
|
||||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
|
||||||
--title "Your Title Here" \\
|
|
||||||
--subtitle "Your concise subtitle here" \\
|
|
||||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
|
||||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
|
||||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
|
||||||
--project "{{project}}" \\
|
|
||||||
--session "{{sessionId}}" \\
|
|
||||||
--date "{{date}}"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
CRITICAL FORMATTING RULES:
|
|
||||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
|
||||||
- Use double quotes inside the JSON arrays: "item"
|
|
||||||
- Use double quotes around simple string values: --title "Title"
|
|
||||||
- Escape any quotes in the content properly
|
|
||||||
- Sequential numbering: 001, 002, 003, etc.
|
|
||||||
|
|
||||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
|
||||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
|
||||||
|
|
||||||
# EXAMPLE MEMORY
|
|
||||||
|
|
||||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
|
||||||
|
|
||||||
Your storage command:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-memory \\
|
|
||||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
|
||||||
--title "SDK Transcript Auto-Cleanup" \\
|
|
||||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
|
||||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
|
||||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
|
||||||
--files '["hooks/stop-streaming.js"]' \\
|
|
||||||
--project "claude-mem" \\
|
|
||||||
--session "abc123" \\
|
|
||||||
--date "2025-10-01"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
# STATE TRACKING
|
|
||||||
|
|
||||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
|
||||||
- Start at 001
|
|
||||||
- Increment for each stored memory
|
|
||||||
- Never repeat numbers
|
|
||||||
- Each session has separate numbering
|
|
||||||
|
|
||||||
# SESSION END
|
|
||||||
|
|
||||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
# IMPORTANT REMINDERS
|
|
||||||
|
|
||||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
|
||||||
- Use Bash tool to call claude-mem commands
|
|
||||||
- Keep subtitles clear and informative (max 24 words)
|
|
||||||
- Each fact is ONE specific thing (not multiple ideas)
|
|
||||||
- Be selective - quality over quantity
|
|
||||||
- Always increment memory numbers
|
|
||||||
- Facts should be searchable (specific file names, paths, functions)
|
|
||||||
|
|
||||||
Ready for tool responses.`;
|
|
||||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
|
||||||
|
|
||||||
Tool: {{toolName}}
|
|
||||||
User Context: "{{userPrompt}}"
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
{{toolResponse}}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Analyze and store if meaningful.`;
|
|
||||||
var END_MESSAGE = `# SESSION ENDING
|
|
||||||
|
|
||||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
|
||||||
|
|
||||||
Store it using Bash:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
|
||||||
var PROMPTS = {
|
|
||||||
system: SYSTEM_PROMPT,
|
|
||||||
tool: TOOL_MESSAGE,
|
|
||||||
end: END_MESSAGE
|
|
||||||
};
|
|
||||||
|
|
||||||
// src/prompts/hook-prompt-renderer.ts
|
|
||||||
function substituteVariables(template, variables) {
|
|
||||||
let result = template;
|
|
||||||
for (const [key, value] of Object.entries(variables)) {
|
|
||||||
const placeholder = `{{${key}}}`;
|
|
||||||
result = result.split(placeholder).join(value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
function truncate(text, maxLength) {
|
|
||||||
if (text.length <= maxLength)
|
|
||||||
return text;
|
|
||||||
return text.slice(0, maxLength) + (text.length > maxLength ? "..." : "");
|
|
||||||
}
|
|
||||||
function formatTime(timestamp) {
|
|
||||||
const timePart = timestamp.split("T")[1];
|
|
||||||
if (!timePart)
|
|
||||||
return "";
|
|
||||||
return timePart.slice(0, 8);
|
|
||||||
}
|
|
||||||
function renderSystemPrompt(variables) {
|
|
||||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
|
||||||
return substituteVariables(PROMPTS.system, {
|
|
||||||
project: variables.project,
|
|
||||||
sessionId: variables.sessionId,
|
|
||||||
date: variables.date,
|
|
||||||
userPrompt: userPromptTruncated
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function renderToolMessage(variables) {
|
|
||||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
|
||||||
const toolResponseTruncated = truncate(variables.toolResponse, HOOK_CONFIG.maxToolResponseLength);
|
|
||||||
const timeFormatted = formatTime(variables.timestamp);
|
|
||||||
return substituteVariables(PROMPTS.tool, {
|
|
||||||
toolName: variables.toolName,
|
|
||||||
toolResponse: toolResponseTruncated,
|
|
||||||
userPrompt: userPromptTruncated,
|
|
||||||
timestamp: variables.timestamp,
|
|
||||||
timeFormatted
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function renderEndMessage(variables) {
|
|
||||||
return substituteVariables(PROMPTS.end, {
|
|
||||||
project: variables.project,
|
|
||||||
sessionId: variables.sessionId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function renderPrompt(type, variables) {
|
|
||||||
switch (type) {
|
|
||||||
case "system":
|
|
||||||
return renderSystemPrompt(variables);
|
|
||||||
case "tool":
|
|
||||||
return renderToolMessage(variables);
|
|
||||||
case "end":
|
|
||||||
return renderEndMessage(variables);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown prompt type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export {
|
|
||||||
renderToolMessage,
|
|
||||||
renderSystemPrompt,
|
|
||||||
renderPrompt,
|
|
||||||
renderEndMessage,
|
|
||||||
PROMPTS,
|
|
||||||
HOOK_CONFIG
|
|
||||||
};
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
// src/prompts/hook-prompts.config.ts
|
|
||||||
var HOOK_CONFIG = {
|
|
||||||
maxUserPromptLength: 200,
|
|
||||||
maxToolResponseLength: 20000,
|
|
||||||
sdk: {
|
|
||||||
model: "claude-sonnet-4-5",
|
|
||||||
allowedTools: ["Bash"],
|
|
||||||
maxTokensSystem: 8192,
|
|
||||||
maxTokensTool: 8192,
|
|
||||||
maxTokensEnd: 2048
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
|
||||||
|
|
||||||
# SESSION CONTEXT
|
|
||||||
- Project: {{project}}
|
|
||||||
- Session: {{sessionId}}
|
|
||||||
- Date: {{date}}
|
|
||||||
- User Request: "{{userPrompt}}"
|
|
||||||
|
|
||||||
# YOUR JOB
|
|
||||||
|
|
||||||
## FIRST: Generate Session Title
|
|
||||||
|
|
||||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
|
||||||
|
|
||||||
Use this bash command:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem update-session-metadata \\
|
|
||||||
--project "{{project}}" \\
|
|
||||||
--session "{{sessionId}}" \\
|
|
||||||
--title "Short title (3-6 words)" \\
|
|
||||||
--subtitle "One sentence description (max 20 words)"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Example for "Help me add dark mode to my app":
|
|
||||||
- Title: "Dark Mode Implementation"
|
|
||||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
|
||||||
|
|
||||||
## THEN: Process Tool Responses
|
|
||||||
|
|
||||||
You will receive a stream of tool responses. For each one:
|
|
||||||
|
|
||||||
1. ANALYZE: Does this contain information worth remembering?
|
|
||||||
2. DECIDE: Should I store this or skip it?
|
|
||||||
3. EXTRACT: What are the key semantic concepts?
|
|
||||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
|
||||||
5. STORE: Use bash to save the hierarchical memory
|
|
||||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
|
||||||
|
|
||||||
# WHAT TO STORE
|
|
||||||
|
|
||||||
Store these:
|
|
||||||
- File contents with logic, algorithms, or patterns
|
|
||||||
- Search results revealing project structure
|
|
||||||
- Build errors or test failures with context
|
|
||||||
- Code revealing architecture or design decisions
|
|
||||||
- Git diffs with significant changes
|
|
||||||
- Command outputs showing system state
|
|
||||||
|
|
||||||
Skip these:
|
|
||||||
- Simple status checks (git status with no changes)
|
|
||||||
- Trivial edits (one-line config changes)
|
|
||||||
- Repeated operations
|
|
||||||
- Binary data or noise
|
|
||||||
- Anything without semantic value
|
|
||||||
|
|
||||||
# HIERARCHICAL MEMORY FORMAT
|
|
||||||
|
|
||||||
Each memory has FOUR components:
|
|
||||||
|
|
||||||
## 1. TITLE (3-8 words)
|
|
||||||
A scannable headline that captures the core action or topic.
|
|
||||||
Examples:
|
|
||||||
- "SDK Transcript Cleanup Implementation"
|
|
||||||
- "Hook System Architecture Analysis"
|
|
||||||
- "ChromaDB Migration Planning"
|
|
||||||
|
|
||||||
## 2. SUBTITLE (max 24 words)
|
|
||||||
A concise, memorable summary that captures the essence of the change.
|
|
||||||
Examples:
|
|
||||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
|
||||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
|
||||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Clear and descriptive
|
|
||||||
- Focus on the outcome or benefit
|
|
||||||
- Use active voice when possible
|
|
||||||
- Keep it professional and informative
|
|
||||||
|
|
||||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
|
||||||
Individual, searchable statements that can be vector-embedded separately.
|
|
||||||
Each fact is ONE specific piece of information.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
|
||||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
|
||||||
- "Uses fs.unlink with graceful error handling for missing files"
|
|
||||||
- "Checks two transcript path formats for backward compatibility"
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- Start with filename or component when relevant
|
|
||||||
- Be specific: include paths, function names, actual values
|
|
||||||
- Each fact stands alone (no pronouns like "it" or "this")
|
|
||||||
- 50-150 characters target
|
|
||||||
- Focus on searchable technical details
|
|
||||||
|
|
||||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
|
||||||
The full contextual story for deep dives:
|
|
||||||
|
|
||||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
|
||||||
|
|
||||||
This is the detailed explanation for when someone needs full context.
|
|
||||||
|
|
||||||
# STORAGE COMMAND FORMAT
|
|
||||||
|
|
||||||
Store using this EXACT bash command structure:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-memory \\
|
|
||||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
|
||||||
--title "Your Title Here" \\
|
|
||||||
--subtitle "Your concise subtitle here" \\
|
|
||||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
|
||||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
|
||||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
|
||||||
--project "{{project}}" \\
|
|
||||||
--session "{{sessionId}}" \\
|
|
||||||
--date "{{date}}"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
CRITICAL FORMATTING RULES:
|
|
||||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
|
||||||
- Use double quotes inside the JSON arrays: "item"
|
|
||||||
- Use double quotes around simple string values: --title "Title"
|
|
||||||
- Escape any quotes in the content properly
|
|
||||||
- Sequential numbering: 001, 002, 003, etc.
|
|
||||||
|
|
||||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
|
||||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
|
||||||
|
|
||||||
# EXAMPLE MEMORY
|
|
||||||
|
|
||||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
|
||||||
|
|
||||||
Your storage command:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-memory \\
|
|
||||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
|
||||||
--title "SDK Transcript Auto-Cleanup" \\
|
|
||||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
|
||||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
|
||||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
|
||||||
--files '["hooks/stop-streaming.js"]' \\
|
|
||||||
--project "claude-mem" \\
|
|
||||||
--session "abc123" \\
|
|
||||||
--date "2025-10-01"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
# STATE TRACKING
|
|
||||||
|
|
||||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
|
||||||
- Start at 001
|
|
||||||
- Increment for each stored memory
|
|
||||||
- Never repeat numbers
|
|
||||||
- Each session has separate numbering
|
|
||||||
|
|
||||||
# SESSION END
|
|
||||||
|
|
||||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
# IMPORTANT REMINDERS
|
|
||||||
|
|
||||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
|
||||||
- Use Bash tool to call claude-mem commands
|
|
||||||
- Keep subtitles clear and informative (max 24 words)
|
|
||||||
- Each fact is ONE specific thing (not multiple ideas)
|
|
||||||
- Be selective - quality over quantity
|
|
||||||
- Always increment memory numbers
|
|
||||||
- Facts should be searchable (specific file names, paths, functions)
|
|
||||||
|
|
||||||
Ready for tool responses.`;
|
|
||||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
|
||||||
|
|
||||||
Tool: {{toolName}}
|
|
||||||
User Context: "{{userPrompt}}"
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
{{toolResponse}}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Analyze and store if meaningful.`;
|
|
||||||
var END_MESSAGE = `# SESSION ENDING
|
|
||||||
|
|
||||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
|
||||||
|
|
||||||
Store it using Bash:
|
|
||||||
\`\`\`bash
|
|
||||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
|
||||||
var PROMPTS = {
|
|
||||||
system: SYSTEM_PROMPT,
|
|
||||||
tool: TOOL_MESSAGE,
|
|
||||||
end: END_MESSAGE
|
|
||||||
};
|
|
||||||
export {
|
|
||||||
TOOL_MESSAGE,
|
|
||||||
SYSTEM_PROMPT,
|
|
||||||
PROMPTS,
|
|
||||||
HOOK_CONFIG,
|
|
||||||
END_MESSAGE
|
|
||||||
};
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path resolver utility for Claude Memory hooks
|
|
||||||
* Provides proper path handling using environment variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { join, basename } from 'path';
|
|
||||||
import { homedir } from 'os';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the base data directory for claude-mem
|
|
||||||
* @returns {string} Data directory path
|
|
||||||
*/
|
|
||||||
export function getDataDir() {
|
|
||||||
return process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the settings file path
|
|
||||||
* @returns {string} Settings file path
|
|
||||||
*/
|
|
||||||
export function getSettingsPath() {
|
|
||||||
return join(getDataDir(), 'settings.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the archives directory path
|
|
||||||
* @returns {string} Archives directory path
|
|
||||||
*/
|
|
||||||
export function getArchivesDir() {
|
|
||||||
return process.env.CLAUDE_MEM_ARCHIVES_DIR || join(getDataDir(), 'archives');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the logs directory path
|
|
||||||
* @returns {string} Logs directory path
|
|
||||||
*/
|
|
||||||
export function getLogsDir() {
|
|
||||||
return process.env.CLAUDE_MEM_LOGS_DIR || join(getDataDir(), 'logs');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the compact flag file path
|
|
||||||
* @returns {string} Compact flag file path
|
|
||||||
*/
|
|
||||||
export function getCompactFlagPath() {
|
|
||||||
return join(getDataDir(), '.compact-running');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the claude-mem package root directory
|
|
||||||
* @returns {Promise<string>} Package root path
|
|
||||||
*/
|
|
||||||
export async function getPackageRoot() {
|
|
||||||
// Method 1: Check if we're running from development
|
|
||||||
const devPath = join(homedir(), 'Scripts', 'claude-mem-source');
|
|
||||||
const { existsSync } = await import('fs');
|
|
||||||
if (existsSync(join(devPath, 'package.json'))) {
|
|
||||||
return devPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Follow the binary symlink
|
|
||||||
try {
|
|
||||||
const { execSync } = await import('child_process');
|
|
||||||
const { realpathSync } = await import('fs');
|
|
||||||
const binPath = execSync('which claude-mem', { encoding: 'utf8' }).trim();
|
|
||||||
const realBinPath = realpathSync(binPath);
|
|
||||||
// Binary is typically at package_root/dist/claude-mem.min.js
|
|
||||||
return join(realBinPath, '../..');
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
throw new Error('Cannot locate claude-mem package root');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the project root directory
|
|
||||||
* Uses CLAUDE_PROJECT_DIR environment variable if available, otherwise falls back to cwd
|
|
||||||
* @returns {string} Project root path
|
|
||||||
*/
|
|
||||||
export function getProjectRoot() {
|
|
||||||
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives project name from CLAUDE_PROJECT_DIR or current working directory
|
|
||||||
* Priority: CLAUDE_PROJECT_DIR > cwd parameter > process.cwd()
|
|
||||||
* @param {string} [cwd] - Optional current working directory from hook payload
|
|
||||||
* @returns {string} Project name (basename of project directory)
|
|
||||||
*/
|
|
||||||
export function getProjectName(cwd) {
|
|
||||||
const projectRoot = process.env.CLAUDE_PROJECT_DIR || cwd || process.cwd();
|
|
||||||
return basename(projectRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all common paths used by hooks
|
|
||||||
* @returns {Object} Object containing all common paths
|
|
||||||
*/
|
|
||||||
export function getPaths() {
|
|
||||||
return {
|
|
||||||
dataDir: getDataDir(),
|
|
||||||
settingsPath: getSettingsPath(),
|
|
||||||
archivesDir: getArchivesDir(),
|
|
||||||
logsDir: getLogsDir(),
|
|
||||||
compactFlagPath: getCompactFlagPath()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop Hook - Simple Orchestrator
|
|
||||||
*
|
|
||||||
* Signals session end to SDK, which generates and stores the overview via CLI.
|
|
||||||
* Cleans up SDK transcript from UI.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
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, acquireSessionLock, releaseSessionLock, cleanupStaleLocks } from './shared/hook-helpers.js';
|
|
||||||
|
|
||||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
|
||||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
|
||||||
process.stderr.write(logLine);
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail on log errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// GRACEFUL SHUTDOWN HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let db;
|
|
||||||
let lockAcquired = false;
|
|
||||||
let sdkSessionId = null;
|
|
||||||
let sessionData = null;
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
if (lockAcquired && sdkSessionId && db) {
|
|
||||||
try {
|
|
||||||
releaseSessionLock(db, sdkSessionId);
|
|
||||||
debugLog('Stop: Released session lock on shutdown', { sdkSessionId });
|
|
||||||
} catch (err) {
|
|
||||||
// Silent fail on cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
// Silent fail on cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
debugLog('Stop: Received SIGTERM, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
debugLog('Stop: Received SIGINT, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let input = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
||||||
|
|
||||||
process.stdin.on('end', async () => {
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = input ? JSON.parse(input) : {};
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('Stop: JSON parse error', { error: error.message });
|
|
||||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { cwd } = payload;
|
|
||||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
|
||||||
|
|
||||||
// Return immediately with async mode
|
|
||||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Clear activity flag FIRST - even if hook fails, UI should update
|
|
||||||
const activityFlagPath = path.join(process.env.HOME || '', '.claude-mem', 'activity.flag');
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(activityFlagPath, JSON.stringify({ active: false, timestamp: Date.now() }));
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('Stop: Error clearing activity flag', { error: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load SDK session info from database
|
|
||||||
db = initializeDatabase();
|
|
||||||
|
|
||||||
// Clean up any stale locks first
|
|
||||||
cleanupStaleLocks(db);
|
|
||||||
|
|
||||||
const sessions = getActiveStreamingSessionsForProject(db, project);
|
|
||||||
if (!sessions || sessions.length === 0) {
|
|
||||||
debugLog('Stop: No streaming session found', { project });
|
|
||||||
db.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionData = sessions[0];
|
|
||||||
sdkSessionId = sessionData.sdk_session_id;
|
|
||||||
const claudeSessionId = sessionData.claude_session_id;
|
|
||||||
|
|
||||||
// Validate SDK session ID exists
|
|
||||||
if (!sdkSessionId) {
|
|
||||||
debugLog('Stop: SDK session ID not yet available', { project });
|
|
||||||
db.close();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to acquire lock - wait up to 10 seconds for PostToolUse to finish
|
|
||||||
let attempts = 0;
|
|
||||||
while (attempts < 20) {
|
|
||||||
lockAcquired = acquireSessionLock(db, sdkSessionId, 'Stop');
|
|
||||||
if (lockAcquired) break;
|
|
||||||
|
|
||||||
debugLog('Stop: Waiting for session lock', { attempt: attempts + 1, sdkSessionId });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lockAcquired) {
|
|
||||||
debugLog('Stop: Could not acquire session lock after 10 seconds', { sdkSessionId });
|
|
||||||
db.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
|
||||||
|
|
||||||
// Build end message - SDK will call `claude-mem store-overview` and `chroma_add_documents`
|
|
||||||
const message = renderEndMessage({
|
|
||||||
project,
|
|
||||||
sessionId: claudeSessionId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send end message and wait for SDK to complete
|
|
||||||
const response = query({
|
|
||||||
prompt: message,
|
|
||||||
options: {
|
|
||||||
model: HOOK_CONFIG.sdk.model,
|
|
||||||
resume: sdkSessionId,
|
|
||||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
|
||||||
maxTokens: HOOK_CONFIG.sdk.maxTokensEnd,
|
|
||||||
cwd // Must match where transcript was created
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Consume the response stream (wait for SDK to finish storing via CLI)
|
|
||||||
for await (const msg of response) {
|
|
||||||
if (msg.type === 'assistant' && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === 'tool_use') {
|
|
||||||
debugLog('Stop: SDK tool call', { tool: block.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog('Stop: SDK session ended', { sdkSessionId });
|
|
||||||
|
|
||||||
// Delete SDK memories transcript from Claude Code UI
|
|
||||||
const sanitizedCwd = cwd.replace(/\//g, '-');
|
|
||||||
const projectsDir = path.join(process.env.HOME, '.claude', 'projects', sanitizedCwd);
|
|
||||||
const memoriesTranscriptPath = path.join(projectsDir, `${sdkSessionId}.jsonl`);
|
|
||||||
|
|
||||||
if (fs.existsSync(memoriesTranscriptPath)) {
|
|
||||||
fs.unlinkSync(memoriesTranscriptPath);
|
|
||||||
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark session as completed in database
|
|
||||||
if (sessionData) {
|
|
||||||
markStreamingSessionCompleted(db, sessionData.id);
|
|
||||||
debugLog('Stop: Session ended and marked complete', { project, sessionId: sessionData.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('Stop: Error ending session', { error: error.message, stack: error.stack });
|
|
||||||
} finally {
|
|
||||||
// Always release lock and close database
|
|
||||||
if (lockAcquired && sdkSessionId && db) {
|
|
||||||
try {
|
|
||||||
releaseSessionLock(db, sdkSessionId);
|
|
||||||
debugLog('Stop: Released session lock', { sdkSessionId });
|
|
||||||
} catch (err) {
|
|
||||||
debugLog('Stop: Error releasing lock', { error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
debugLog('Stop: Error closing database', { error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit cleanly after async processing completes
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Prompt Submit Hook - Streaming SDK Version
|
|
||||||
*
|
|
||||||
* Starts a streaming SDK session that will process tool responses in real-time.
|
|
||||||
* Saves the SDK session ID for post-tool-use and stop hooks to resume.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
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 HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
|
||||||
|
|
||||||
function debugLog(message, data = {}) {
|
|
||||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
|
||||||
process.stderr.write(logLine);
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail on log errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed: buildStreamingSystemPrompt function
|
|
||||||
// Now using centralized config from hook-prompt-renderer.js
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// GRACEFUL SHUTDOWN HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let db;
|
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
// Silent fail on cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
debugLog('UserPromptSubmit: Received SIGTERM, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
debugLog('UserPromptSubmit: Received SIGINT, cleaning up');
|
|
||||||
cleanup();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
let input = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
||||||
|
|
||||||
process.stdin.on('end', async () => {
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = input ? JSON.parse(input) : {};
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('UserPromptSubmit: JSON parse error', { error: error.message });
|
|
||||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { prompt, cwd, session_id, timestamp } = payload;
|
|
||||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
|
||||||
const date = timestamp ? timestamp.split('T')[0] : new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
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 }) => {
|
|
||||||
const titleProcess = spawn('claude-mem', [
|
|
||||||
'generate-title',
|
|
||||||
'--save',
|
|
||||||
'--project', project,
|
|
||||||
'--session', session_id,
|
|
||||||
prompt
|
|
||||||
], {
|
|
||||||
stdio: 'ignore',
|
|
||||||
detached: true
|
|
||||||
});
|
|
||||||
titleProcess.unref();
|
|
||||||
}).catch(error => {
|
|
||||||
debugLog('UserPromptSubmit: Error spawning title generator', { error: error.message });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize database and create session record FIRST
|
|
||||||
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,
|
|
||||||
sessionId: session_id,
|
|
||||||
date,
|
|
||||||
userPrompt: prompt || ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start SDK session using centralized config
|
|
||||||
const response = query({
|
|
||||||
prompt: systemPrompt,
|
|
||||||
options: {
|
|
||||||
model: HOOK_CONFIG.sdk.model,
|
|
||||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
|
||||||
maxTokens: HOOK_CONFIG.sdk.maxTokensSystem,
|
|
||||||
cwd // SDK will save transcript in this directory
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for session ID from init message and consume entire stream
|
|
||||||
let sdkSessionId = null;
|
|
||||||
for await (const message of response) {
|
|
||||||
if (message.type === 'system' && message.subtype === 'init') {
|
|
||||||
sdkSessionId = message.session_id;
|
|
||||||
debugLog('UserPromptSubmit: Got SDK session ID', { sdkSessionId });
|
|
||||||
}
|
|
||||||
// Don't break - consume entire stream so transcript gets written
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sdkSessionId) {
|
|
||||||
// Update session record with SDK session ID
|
|
||||||
updateStreamingSession(db, sessionRecord.id, {
|
|
||||||
sdk_session_id: sdkSessionId
|
|
||||||
});
|
|
||||||
|
|
||||||
debugLog('UserPromptSubmit: SDK session started', {
|
|
||||||
internalId: sessionRecord.id,
|
|
||||||
sdkSessionId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message, stack: error.stack });
|
|
||||||
} finally {
|
|
||||||
// Always close database connection
|
|
||||||
if (db) {
|
|
||||||
try {
|
|
||||||
db.close();
|
|
||||||
} catch (err) {
|
|
||||||
debugLog('UserPromptSubmit: Error closing database', { error: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return success to Claude Code
|
|
||||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
+5
-4
@@ -36,10 +36,11 @@
|
|||||||
"claude-mem": "./dist/claude-mem.min.js"
|
"claude-mem": "./dist/claude-mem.min.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node scripts/build.js",
|
||||||
"publish:npm": "node publish.js",
|
"publish:npm": "node scripts/publish.js",
|
||||||
"dev": "bun run src/bin/cli.ts",
|
"dev": "bun run src/bin/cli.ts",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build",
|
||||||
|
"test": "bun test tests/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
||||||
@@ -56,8 +57,8 @@
|
|||||||
"hook-templates",
|
"hook-templates",
|
||||||
"commands",
|
"commands",
|
||||||
"src",
|
"src",
|
||||||
|
"docs",
|
||||||
".mcp.json",
|
".mcp.json",
|
||||||
"CHANGELOG.md",
|
|
||||||
"README_WINDOWS.md"
|
"README_WINDOWS.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Tests database schema and hook functions
|
* Tests database schema and hook functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DatabaseManager, migrations } from './src/services/sqlite/index.js';
|
import { DatabaseManager, migrations } from '../src/services/sqlite/index.js';
|
||||||
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
|
import { HooksDatabase } from '../src/services/sqlite/HooksDatabase.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
||||||
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
|
import { HooksDatabase } from '../src/services/sqlite/HooksDatabase.js';
|
||||||
import { DatabaseManager } from './src/services/sqlite/Database.js';
|
import { DatabaseManager } from '../src/services/sqlite/Database.js';
|
||||||
import { migrations } from './src/services/sqlite/migrations.js';
|
import { migrations } from '../src/services/sqlite/migrations.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
||||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from './src/sdk/prompts.js';
|
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from '../src/sdk/prompts.js';
|
||||||
import { parseObservations, parseSummary } from './src/sdk/parser.js';
|
import { parseObservations, parseSummary } from '../src/sdk/parser.js';
|
||||||
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
|
import { HooksDatabase } from '../src/services/sqlite/HooksDatabase.js';
|
||||||
import { DatabaseManager } from './src/services/sqlite/Database.js';
|
import { DatabaseManager } from '../src/services/sqlite/Database.js';
|
||||||
import { migrations } from './src/services/sqlite/migrations.js';
|
import { migrations } from '../src/services/sqlite/migrations.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
||||||
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
|
import { HooksDatabase } from '../src/services/sqlite/HooksDatabase.js';
|
||||||
import { DatabaseManager } from './src/services/sqlite/Database.js';
|
import { DatabaseManager } from '../src/services/sqlite/Database.js';
|
||||||
import { migrations } from './src/services/sqlite/migrations.js';
|
import { migrations } from '../src/services/sqlite/migrations.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user