@tznthou/ccrecall 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +290 -0
- package/README_ZH.md +288 -0
- package/dist/api/routes.d.ts +13 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +428 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/api/server.d.ts +7 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +44 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli/daemon.d.ts +35 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +163 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/core/compression.d.ts +37 -0
- package/dist/core/compression.d.ts.map +1 -0
- package/dist/core/compression.js +129 -0
- package/dist/core/compression.js.map +1 -0
- package/dist/core/database.d.ts +219 -0
- package/dist/core/database.d.ts.map +1 -0
- package/dist/core/database.js +1513 -0
- package/dist/core/database.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/indexer.d.ts +15 -0
- package/dist/core/indexer.d.ts.map +1 -0
- package/dist/core/indexer.js +224 -0
- package/dist/core/indexer.js.map +1 -0
- package/dist/core/lint.d.ts +17 -0
- package/dist/core/lint.d.ts.map +1 -0
- package/dist/core/lint.js +51 -0
- package/dist/core/lint.js.map +1 -0
- package/dist/core/log-safe.d.ts +6 -0
- package/dist/core/log-safe.d.ts.map +1 -0
- package/dist/core/log-safe.js +11 -0
- package/dist/core/log-safe.js.map +1 -0
- package/dist/core/maintenance-coordinator.d.ts +26 -0
- package/dist/core/maintenance-coordinator.d.ts.map +1 -0
- package/dist/core/maintenance-coordinator.js +67 -0
- package/dist/core/maintenance-coordinator.js.map +1 -0
- package/dist/core/memory-service.d.ts +31 -0
- package/dist/core/memory-service.d.ts.map +1 -0
- package/dist/core/memory-service.js +47 -0
- package/dist/core/memory-service.js.map +1 -0
- package/dist/core/parser.d.ts +17 -0
- package/dist/core/parser.d.ts.map +1 -0
- package/dist/core/parser.js +205 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/scanner.d.ts +8 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +123 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/summarizer.d.ts +12 -0
- package/dist/core/summarizer.d.ts.map +1 -0
- package/dist/core/summarizer.js +430 -0
- package/dist/core/summarizer.js.map +1 -0
- package/dist/core/topic-extractor.d.ts +11 -0
- package/dist/core/topic-extractor.d.ts.map +1 -0
- package/dist/core/topic-extractor.js +51 -0
- package/dist/core/topic-extractor.js.map +1 -0
- package/dist/core/types.d.ts +329 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +12 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/watcher.d.ts +30 -0
- package/dist/core/watcher.d.ts.map +1 -0
- package/dist/core/watcher.js +146 -0
- package/dist/core/watcher.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +39 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +215 -0
- package/dist/mcp/tools.js.map +1 -0
- package/hooks/README.md +78 -0
- package/hooks/session-end.mjs +92 -0
- package/hooks/session-start.mjs +112 -0
- package/package.json +54 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { Database } from '../core/database.js';
|
|
8
|
+
import { registerTools } from './tools.js';
|
|
9
|
+
const DB_PATH = process.env.CCRECALL_DB_PATH ?? path.join(os.homedir(), '.ccrecall', 'ccrecall.db');
|
|
10
|
+
async function main() {
|
|
11
|
+
const db = new Database(DB_PATH);
|
|
12
|
+
const server = new McpServer({ name: 'ccrecall', version: '0.2.0' }, {
|
|
13
|
+
instructions: 'ccRecall is a local memory service for Claude Code. ' +
|
|
14
|
+
'Use recall_query to search past decisions, discoveries, and patterns ' +
|
|
15
|
+
'when the user references prior work or you need context from earlier sessions.',
|
|
16
|
+
});
|
|
17
|
+
registerTools(server, db);
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
console.error(`ccRecall MCP server running on stdio (db: ${DB_PATH})`);
|
|
21
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
22
|
+
process.on(signal, () => {
|
|
23
|
+
db.close();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
main().catch((err) => {
|
|
29
|
+
console.error('MCP server fatal:', err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":";AACA,sCAAsC;AACtC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,aAAa,CAAC,CAAA;AAEnG,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAA;IAChC,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EACtC;QACE,YAAY,EACV,sDAAsD;YACtD,uEAAuE;YACvE,gFAAgF;KACnF,CACF,CAAA;IAED,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAEzB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAA;IAC5C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC/B,OAAO,CAAC,KAAK,CAAC,6CAA6C,OAAO,GAAG,CAAC,CAAA;IAEtE,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,EAAE,CAAC,KAAK,EAAE,CAAA;YACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAA;IACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { Database } from '../core/database.js';
|
|
3
|
+
import { MemoryService } from '../core/memory-service.js';
|
|
4
|
+
import type { Memory, MemoryType, KnowledgeDepth } from '../core/types.js';
|
|
5
|
+
type McpTextResult = {
|
|
6
|
+
content: Array<{
|
|
7
|
+
type: 'text';
|
|
8
|
+
text: string;
|
|
9
|
+
}>;
|
|
10
|
+
isError?: true;
|
|
11
|
+
};
|
|
12
|
+
export declare function formatMemories(memories: Memory[], query: string): string;
|
|
13
|
+
export declare function recallQueryHandler(db: Database, memoryService: MemoryService, args: {
|
|
14
|
+
query: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}): McpTextResult;
|
|
17
|
+
interface TopicCluster {
|
|
18
|
+
topic: string;
|
|
19
|
+
depth: KnowledgeDepth;
|
|
20
|
+
mentionCount: number;
|
|
21
|
+
memories: Memory[];
|
|
22
|
+
}
|
|
23
|
+
export declare function formatContextResult(clusters: TopicCluster[], unmatchedKeywords: string[], fallbackMemories: Memory[] | null, keywords: string[]): string;
|
|
24
|
+
export declare function recallContextHandler(db: Database, memoryService: MemoryService, args: {
|
|
25
|
+
projectId: string;
|
|
26
|
+
keywords: string[];
|
|
27
|
+
memoryLimit?: number;
|
|
28
|
+
}): McpTextResult;
|
|
29
|
+
export declare function recallSaveHandler(db: Database, args: {
|
|
30
|
+
content: string;
|
|
31
|
+
type: MemoryType;
|
|
32
|
+
sessionId?: string | null;
|
|
33
|
+
messageId?: string | null;
|
|
34
|
+
confidence?: number;
|
|
35
|
+
projectId?: string | null;
|
|
36
|
+
}): McpTextResult;
|
|
37
|
+
export declare function registerTools(server: McpServer, db: Database): void;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAM1E,KAAK,aAAa,GAAG;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9C,OAAO,CAAC,EAAE,IAAI,CAAA;CACf,CAAA;AAqBD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,QAAQ,EACZ,aAAa,EAAE,aAAa,EAC5B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,aAAa,CAUf;AAaD,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,EAAE,EACxB,iBAAiB,EAAE,MAAM,EAAE,EAC3B,gBAAgB,EAAE,MAAM,EAAE,GAAG,IAAI,EACjC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,CAuBR;AAQD,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,QAAQ,EACZ,aAAa,EAAE,aAAa,EAC5B,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACpE,aAAa,CAwDf;AAED,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE;IACJ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,UAAU,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,GACA,aAAa,CAcf;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,GAAG,IAAI,CA8EnE"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { MemoryService } from '../core/memory-service.js';
|
|
4
|
+
import { deriveDepth } from '../core/types.js';
|
|
5
|
+
import { normalizeTopicKey } from '../core/topic-extractor.js';
|
|
6
|
+
const MEMORY_TYPES = ['decision', 'discovery', 'preference', 'pattern', 'feedback'];
|
|
7
|
+
function textResult(text) {
|
|
8
|
+
return { content: [{ type: 'text', text }] };
|
|
9
|
+
}
|
|
10
|
+
function textError(prefix, err) {
|
|
11
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12
|
+
return { content: [{ type: 'text', text: `${prefix}: ${msg}` }], isError: true };
|
|
13
|
+
}
|
|
14
|
+
const recallQueryInput = {
|
|
15
|
+
query: z.string().min(1).describe('FTS5 search query (keywords or phrase)'),
|
|
16
|
+
limit: z.number().int().positive().max(50).optional().describe('Max results (default 10, max 50)'),
|
|
17
|
+
};
|
|
18
|
+
function formatMemoryLine(m) {
|
|
19
|
+
const conf = m.confidence !== 1 ? ` (conf ${m.confidence.toFixed(2)})` : '';
|
|
20
|
+
return `- [${m.type}]${conf} ${m.content}`;
|
|
21
|
+
}
|
|
22
|
+
export function formatMemories(memories, query) {
|
|
23
|
+
if (memories.length === 0)
|
|
24
|
+
return `No memories found for: ${query}`;
|
|
25
|
+
return memories.map(formatMemoryLine).join('\n');
|
|
26
|
+
}
|
|
27
|
+
export function recallQueryHandler(db, memoryService, args) {
|
|
28
|
+
try {
|
|
29
|
+
const memories = db.queryMemories(args.query, args.limit ?? 10);
|
|
30
|
+
// Phase 4c: touch returned memories so access_count reflects real usage,
|
|
31
|
+
// extending their half-life under the decay formula. touch noops on [].
|
|
32
|
+
memoryService.touch(memories.map(m => m.id));
|
|
33
|
+
return textResult(formatMemories(memories, args.query));
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return textError('Error querying memories', err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const recallSaveInput = {
|
|
40
|
+
content: z.string().min(1).describe('Memory content — the fact or insight to remember'),
|
|
41
|
+
type: z.enum(MEMORY_TYPES).describe('Memory category'),
|
|
42
|
+
sessionId: z.string().nullable().optional().describe('Origin session ID (optional)'),
|
|
43
|
+
messageId: z.string().nullable().optional().describe('Origin message ID (optional)'),
|
|
44
|
+
confidence: z.number().min(0).max(1).optional().describe('Confidence 0-1 (default 1)'),
|
|
45
|
+
projectId: z.string().nullable().optional().describe('Project ID for scoped queries. Session-backed memories derive this from sessions.project_id automatically; manual memories should set this to avoid cross-project leakage.'),
|
|
46
|
+
};
|
|
47
|
+
export function formatContextResult(clusters, unmatchedKeywords, fallbackMemories, keywords) {
|
|
48
|
+
if (clusters.length === 0 && (!fallbackMemories || fallbackMemories.length === 0)) {
|
|
49
|
+
return `No relevant memories for: ${keywords.join(', ')}`;
|
|
50
|
+
}
|
|
51
|
+
const parts = [`# Relevant memories for: ${keywords.join(', ')}`, ''];
|
|
52
|
+
for (const c of clusters) {
|
|
53
|
+
parts.push(`## Topic: ${c.topic} (${c.depth}, ${c.mentionCount} mentions)`);
|
|
54
|
+
if (c.memories.length === 0) {
|
|
55
|
+
parts.push('(no memories linked yet)');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
for (const m of c.memories)
|
|
59
|
+
parts.push(formatMemoryLine(m));
|
|
60
|
+
}
|
|
61
|
+
parts.push('');
|
|
62
|
+
}
|
|
63
|
+
if (fallbackMemories && fallbackMemories.length > 0) {
|
|
64
|
+
parts.push('## FTS fallback (no topic match)');
|
|
65
|
+
for (const m of fallbackMemories)
|
|
66
|
+
parts.push(formatMemoryLine(m));
|
|
67
|
+
parts.push('');
|
|
68
|
+
}
|
|
69
|
+
if (unmatchedKeywords.length > 0) {
|
|
70
|
+
parts.push(`(No topic match for: ${unmatchedKeywords.join(', ')})`);
|
|
71
|
+
}
|
|
72
|
+
return parts.join('\n').trimEnd();
|
|
73
|
+
}
|
|
74
|
+
const recallContextInput = {
|
|
75
|
+
projectId: z.string().min(1).describe('Project ID (derived from cwd, e.g. "-Users-foo-my-project")'),
|
|
76
|
+
keywords: z.array(z.string().min(1)).min(1).describe('Candidate topic keywords (e.g. ["typescript", "mcp"])'),
|
|
77
|
+
memoryLimit: z.number().int().positive().max(20).optional().describe('Max memories per topic (default 5)'),
|
|
78
|
+
};
|
|
79
|
+
export function recallContextHandler(db, memoryService, args) {
|
|
80
|
+
try {
|
|
81
|
+
const memoryLimit = args.memoryLimit ?? 5;
|
|
82
|
+
const normalized = args.keywords
|
|
83
|
+
.map(k => ({ raw: k, key: normalizeTopicKey(k) }))
|
|
84
|
+
.filter((x) => x.key !== null);
|
|
85
|
+
const clusters = [];
|
|
86
|
+
const unmatched = [];
|
|
87
|
+
for (const { raw, key } of normalized) {
|
|
88
|
+
const topic = db.getTopic(key, args.projectId);
|
|
89
|
+
if (!topic) {
|
|
90
|
+
unmatched.push(raw);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const memories = db.getMemoriesByTopics(args.projectId, [key], memoryLimit);
|
|
94
|
+
clusters.push({
|
|
95
|
+
topic: key,
|
|
96
|
+
depth: deriveDepth(topic.mentionCount),
|
|
97
|
+
mentionCount: topic.mentionCount,
|
|
98
|
+
memories,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// FTS fallback if no topic matched — per-keyword union (queryMemories quotes every
|
|
102
|
+
// token into a phrase, so "foo OR bar" collapses to a phrase query, not a boolean OR)
|
|
103
|
+
let fallback = null;
|
|
104
|
+
if (clusters.length === 0 && args.keywords.length > 0) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const aggregated = [];
|
|
107
|
+
for (const kw of args.keywords) {
|
|
108
|
+
if (aggregated.length >= memoryLimit)
|
|
109
|
+
break;
|
|
110
|
+
const results = db.queryMemories(kw, memoryLimit, args.projectId);
|
|
111
|
+
for (const m of results) {
|
|
112
|
+
if (aggregated.length >= memoryLimit)
|
|
113
|
+
break;
|
|
114
|
+
if (!seen.has(m.id)) {
|
|
115
|
+
seen.add(m.id);
|
|
116
|
+
aggregated.push(m);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
fallback = aggregated;
|
|
121
|
+
}
|
|
122
|
+
// Phase 4c: gather all memory ids surfaced (clusters + fallback) and touch once.
|
|
123
|
+
// MemoryService.touch dedupes internally, so a memory appearing in multiple
|
|
124
|
+
// clusters still only bumps access_count by 1 per request.
|
|
125
|
+
const clusterIds = clusters.flatMap(c => c.memories.map(m => m.id));
|
|
126
|
+
const fallbackIds = fallback?.map(m => m.id) ?? [];
|
|
127
|
+
memoryService.touch([...clusterIds, ...fallbackIds]);
|
|
128
|
+
return textResult(formatContextResult(clusters, unmatched, fallback, args.keywords));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return textError('Error building context', err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function recallSaveHandler(db, args) {
|
|
135
|
+
try {
|
|
136
|
+
const id = db.saveMemory({
|
|
137
|
+
sessionId: args.sessionId ?? null,
|
|
138
|
+
messageId: args.messageId ?? null,
|
|
139
|
+
content: args.content,
|
|
140
|
+
type: args.type,
|
|
141
|
+
confidence: args.confidence ?? 1,
|
|
142
|
+
projectId: args.projectId ?? null,
|
|
143
|
+
});
|
|
144
|
+
return textResult(`Saved memory #${id} (type: ${args.type})`);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
return textError('Error saving memory', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function registerTools(server, db) {
|
|
151
|
+
const memoryService = new MemoryService(db);
|
|
152
|
+
server.registerTool('recall_query', {
|
|
153
|
+
title: 'Query ccRecall Memories',
|
|
154
|
+
description: [
|
|
155
|
+
'Search ccRecall memories by keyword (FTS5 full-text search).',
|
|
156
|
+
'',
|
|
157
|
+
'USE THIS WHEN:',
|
|
158
|
+
'- User references past work ("what did we decide", "previously", "last time")',
|
|
159
|
+
'- Continuing a topic from a prior Claude Code session',
|
|
160
|
+
'- You need context about past decisions, discoveries, patterns, or preferences',
|
|
161
|
+
'',
|
|
162
|
+
'Returns: formatted list of matching memories with type and confidence.',
|
|
163
|
+
].join('\n'),
|
|
164
|
+
inputSchema: recallQueryInput,
|
|
165
|
+
// Phase 4c touch mutates access_count / last_accessed, so this tool is no
|
|
166
|
+
// longer read-only or idempotent. Mislabelling would let MCP hosts retry
|
|
167
|
+
// or cache under the wrong assumption and silently skew recall ranking.
|
|
168
|
+
annotations: { readOnlyHint: false, idempotentHint: false },
|
|
169
|
+
}, async (args) => recallQueryHandler(db, memoryService, args));
|
|
170
|
+
server.registerTool('recall_context', {
|
|
171
|
+
title: 'Get Topic-Clustered Memories',
|
|
172
|
+
description: [
|
|
173
|
+
'Retrieve memories clustered by topic (uses knowledge_map, not plain FTS).',
|
|
174
|
+
'',
|
|
175
|
+
'USE THIS WHEN:',
|
|
176
|
+
'- Starting work on a topic and want to see what you already know about it',
|
|
177
|
+
'- User asks "what do we know about X" / "what\'s our take on Y"',
|
|
178
|
+
'- You want memories grouped by theme, with knowledge depth signals',
|
|
179
|
+
'',
|
|
180
|
+
'vs recall_query:',
|
|
181
|
+
'- recall_query: raw FTS search, returns flat list',
|
|
182
|
+
'- recall_context: topic-aware, groups results, shows mention count + depth',
|
|
183
|
+
'',
|
|
184
|
+
'Returns: markdown with memory clusters by topic, plus FTS fallback if no topic match.',
|
|
185
|
+
].join('\n'),
|
|
186
|
+
inputSchema: recallContextInput,
|
|
187
|
+
// Phase 4c touch mutates access_count / last_accessed per surfaced memory
|
|
188
|
+
// (with cross-cluster dedup), so this tool is no longer read-only or
|
|
189
|
+
// idempotent — same reasoning as recall_query above.
|
|
190
|
+
annotations: { readOnlyHint: false, idempotentHint: false },
|
|
191
|
+
}, async (args) => recallContextHandler(db, memoryService, args));
|
|
192
|
+
server.registerTool('recall_save', {
|
|
193
|
+
title: 'Save Memory to ccRecall',
|
|
194
|
+
description: [
|
|
195
|
+
'Save a new memory to ccRecall for recall in future sessions.',
|
|
196
|
+
'',
|
|
197
|
+
'USE THIS WHEN:',
|
|
198
|
+
'- User states a decision worth remembering across sessions',
|
|
199
|
+
'- You discover something non-obvious (bug cause, API quirk, design constraint)',
|
|
200
|
+
'- User explicitly says "remember this" or "save this"',
|
|
201
|
+
'',
|
|
202
|
+
'Types:',
|
|
203
|
+
'- decision: explicit choice with rationale',
|
|
204
|
+
'- discovery: non-obvious finding',
|
|
205
|
+
'- preference: user style or convention',
|
|
206
|
+
'- pattern: recurring workflow or code template',
|
|
207
|
+
'- feedback: user correction on past work',
|
|
208
|
+
'',
|
|
209
|
+
'Returns: memory ID and type confirmation.',
|
|
210
|
+
].join('\n'),
|
|
211
|
+
inputSchema: recallSaveInput,
|
|
212
|
+
annotations: { readOnlyHint: false, idempotentHint: false },
|
|
213
|
+
}, async (args) => recallSaveHandler(db, args));
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAGxB,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,CAAU,CAAA;AAO5F,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AACvD,CAAC;AAED,SAAS,SAAS,CAAC,MAAc,EAAE,GAAY;IAC7C,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC5D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,GAAG,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC3F,CAAC;AAED,MAAM,gBAAgB,GAAG;IACvB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IAC3E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kCAAkC,CAAC;CACnG,CAAA;AAED,SAAS,gBAAgB,CAAC,CAAS;IACjC,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IAC3E,OAAO,MAAM,CAAC,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAE,KAAa;IAC9D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,0BAA0B,KAAK,EAAE,CAAA;IACnE,OAAO,QAAQ,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,EAAY,EACZ,aAA4B,EAC5B,IAAuC;IAEvC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;QAC/D,yEAAyE;QACzE,wEAAwE;QACxE,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5C,OAAO,UAAU,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,SAAS,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAA;IAClD,CAAC;AACH,CAAC;AAED,MAAM,eAAe,GAAG;IACtB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,kDAAkD,CAAC;IACvF,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IACtD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;IACpF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;IACpF,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4BAA4B,CAAC;IACtF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAClD,4KAA4K,CAC7K;CACF,CAAA;AASD,MAAM,UAAU,mBAAmB,CACjC,QAAwB,EACxB,iBAA2B,EAC3B,gBAAiC,EACjC,QAAkB;IAElB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,gBAAgB,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QAClF,OAAO,6BAA6B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;IAC3D,CAAC;IACD,MAAM,KAAK,GAAa,CAAC,4BAA4B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAC/E,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,YAAY,YAAY,CAAC,CAAA;QAC3E,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACxC,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAA;QAC7D,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC;IACD,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;QAC9C,KAAK,MAAM,CAAC,IAAI,gBAAgB;YAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAA;QACjE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC;IACD,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,wBAAwB,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAA;AACnC,CAAC;AAED,MAAM,kBAAkB,GAAG;IACzB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,6DAA6D,CAAC;IACpG,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,uDAAuD,CAAC;IAC7G,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;CAC3G,CAAA;AAED,MAAM,UAAU,oBAAoB,CAClC,EAAY,EACZ,aAA4B,EAC5B,IAAqE;IAErE,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,CAAA;QACzC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ;aAC7B,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACjD,MAAM,CAAC,CAAC,CAAC,EAAqC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAA;QAEnE,MAAM,QAAQ,GAAmB,EAAE,CAAA;QACnC,MAAM,SAAS,GAAa,EAAE,CAAA;QAE9B,KAAK,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,UAAU,EAAE,CAAC;YACtC,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;YAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBACnB,SAAQ;YACV,CAAC;YACD,MAAM,QAAQ,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC,CAAA;YAC3E,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,GAAG;gBACV,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC;gBACtC,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,QAAQ;aACT,CAAC,CAAA;QACJ,CAAC;QAED,mFAAmF;QACnF,sFAAsF;QACtF,IAAI,QAAQ,GAAoB,IAAI,CAAA;QACpC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;YAC9B,MAAM,UAAU,GAAa,EAAE,CAAA;YAC/B,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC/B,IAAI,UAAU,CAAC,MAAM,IAAI,WAAW;oBAAE,MAAK;gBAC3C,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;gBACjE,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;oBACxB,IAAI,UAAU,CAAC,MAAM,IAAI,WAAW;wBAAE,MAAK;oBAC3C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;wBACpB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;wBACd,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;oBACpB,CAAC;gBACH,CAAC;YACH,CAAC;YACD,QAAQ,GAAG,UAAU,CAAA;QACvB,CAAC;QAED,iFAAiF;QACjF,4EAA4E;QAC5E,2DAA2D;QAC3D,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACnE,MAAM,WAAW,GAAG,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAA;QAClD,aAAa,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,EAAE,GAAG,WAAW,CAAC,CAAC,CAAA;QAEpD,OAAO,UAAU,CAAC,mBAAmB,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IACtF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,SAAS,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAA;IACjD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,EAAY,EACZ,IAOC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC;YACvB,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;YACjC,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;YACjC,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC;YAChC,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;SAClC,CAAC,CAAA;QACF,OAAO,UAAU,CAAC,iBAAiB,EAAE,WAAW,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;IAC/D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,SAAS,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAA;IAC9C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,EAAY;IAC3D,MAAM,aAAa,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC,CAAA;IAC3C,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE;YACX,8DAA8D;YAC9D,EAAE;YACF,gBAAgB;YAChB,+EAA+E;YAC/E,uDAAuD;YACvD,gFAAgF;YAChF,EAAE;YACF,wEAAwE;SACzE,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,WAAW,EAAE,gBAAgB;QAC7B,0EAA0E;QAC1E,yEAAyE;QACzE,wEAAwE;QACxE,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;KAC5D,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,CAC5D,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,8BAA8B;QACrC,WAAW,EAAE;YACX,2EAA2E;YAC3E,EAAE;YACF,gBAAgB;YAChB,2EAA2E;YAC3E,iEAAiE;YACjE,oEAAoE;YACpE,EAAE;YACF,kBAAkB;YAClB,mDAAmD;YACnD,4EAA4E;YAC5E,EAAE;YACF,uFAAuF;SACxF,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,WAAW,EAAE,kBAAkB;QAC/B,0EAA0E;QAC1E,qEAAqE;QACrE,qDAAqD;QACrD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;KAC5D,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,CAC9D,CAAA;IAED,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE;YACX,8DAA8D;YAC9D,EAAE;YACF,gBAAgB;YAChB,4DAA4D;YAC5D,gFAAgF;YAChF,uDAAuD;YACvD,EAAE;YACF,QAAQ;YACR,4CAA4C;YAC5C,kCAAkC;YAClC,wCAAwC;YACxC,gDAAgD;YAChD,0CAA0C;YAC1C,EAAE;YACF,2CAA2C;SAC5C,CAAC,IAAI,CAAC,IAAI,CAAC;QACZ,WAAW,EAAE,eAAe;QAC5B,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE;KAC5D,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,CAC5C,CAAA;AACH,CAAC"}
|
package/hooks/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ccRecall Claude Code Hooks
|
|
2
|
+
|
|
3
|
+
Hook scripts that let Claude Code's lifecycle events trigger ccRecall actions.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
The ccRecall HTTP service must be running on `127.0.0.1:7749` (or the port set via `CCRECALL_PORT`). Start it with:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm dev # from the ccRecall repo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If the service is not running, hooks log a warning to stderr and exit cleanly — they never block Claude Code.
|
|
14
|
+
|
|
15
|
+
## Available Hooks
|
|
16
|
+
|
|
17
|
+
| Script | Claude Code Event | Action |
|
|
18
|
+
|--------|------------------|--------|
|
|
19
|
+
| `session-start.mjs` | `SessionStart` | GET `/memory/query` with the project name, write matching memories to stdout (Claude prepends them to context) |
|
|
20
|
+
| `session-end.mjs` | `SessionEnd` | POST `/session/end` to harvest the just-ended session into a memory |
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add to `~/.claude/settings.json` (or your project's `.claude/settings.json`):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"hooks": {
|
|
29
|
+
"SessionStart": [
|
|
30
|
+
{
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node /Users/tznthou/Documents/ccRecall/hooks/session-start.mjs"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"SessionEnd": [
|
|
40
|
+
{
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "node /Users/tznthou/Documents/ccRecall/hooks/session-end.mjs"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Adjust the path to match your ccRecall clone location.
|
|
54
|
+
|
|
55
|
+
## Environment Variables
|
|
56
|
+
|
|
57
|
+
- `CCRECALL_PORT` — override the HTTP port (default `7749`)
|
|
58
|
+
|
|
59
|
+
## Verifying the Hook
|
|
60
|
+
|
|
61
|
+
Trigger manually:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
echo '{"session_id":"test-session","hook_event_name":"SessionEnd","reason":"other"}' \
|
|
65
|
+
| node /Users/tznthou/Documents/ccRecall/hooks/session-end.mjs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With ccRecall running, this POSTs to `/session/end`. The response (success or 404) is logged to stderr only on failure.
|
|
69
|
+
|
|
70
|
+
## Design Notes
|
|
71
|
+
|
|
72
|
+
- **Non-blocking**: hooks exit quickly; errors go to stderr and the script exits 0 — never blocks Claude Code
|
|
73
|
+
- **Skip on `resume`**:
|
|
74
|
+
- SessionEnd: `reason === 'resume'` means the session continues
|
|
75
|
+
- SessionStart: `source === 'resume'` means the context is already loaded
|
|
76
|
+
- **SessionStart query strategy**: uses the last path segment of `cwd` as a keyword against FTS5 — simple, no-prompt-yet heuristic; Phase 3+ will add smarter selection
|
|
77
|
+
- **SessionStart stdout = context injection**: only memories are written to stdout; all errors and diagnostics go to stderr to avoid polluting Claude's context
|
|
78
|
+
- **Timeouts**: SessionEnd 5s, SessionStart 2s (tighter because it sits on the pre-prompt critical path)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ccRecall SessionEnd hook — harvests the just-ended session into memories.
|
|
3
|
+
// Wired via ~/.claude/settings.json hooks.SessionEnd. Non-blocking.
|
|
4
|
+
import http from 'node:http'
|
|
5
|
+
|
|
6
|
+
const PORT = Number(process.env.CCRECALL_PORT ?? 7749)
|
|
7
|
+
const HOST = '127.0.0.1'
|
|
8
|
+
const TIMEOUT_MS = 5000
|
|
9
|
+
|
|
10
|
+
async function readStdin() {
|
|
11
|
+
const chunks = []
|
|
12
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
13
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Strip CR/LF and other control chars so attacker-controlled response bodies
|
|
17
|
+
// can't forge log entries when interpolated into console.error.
|
|
18
|
+
function sanitizeForLog(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return String(s).replace(/[\r\n\x00-\x1f\x7f]/g, ' ')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function postSessionEnd(sessionId) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const payload = JSON.stringify({ sessionId })
|
|
26
|
+
const req = http.request({
|
|
27
|
+
hostname: HOST,
|
|
28
|
+
port: PORT,
|
|
29
|
+
path: '/session/end',
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
34
|
+
},
|
|
35
|
+
timeout: TIMEOUT_MS,
|
|
36
|
+
}, (res) => {
|
|
37
|
+
let body = ''
|
|
38
|
+
res.on('data', (c) => { body += c })
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
if (res.statusCode !== 200) {
|
|
41
|
+
console.error(`[ccRecall] harvest failed ${res.statusCode}: ${sanitizeForLog(body.slice(0, 200))}`)
|
|
42
|
+
resolve()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(body)
|
|
47
|
+
if (Array.isArray(parsed.memoriesSaved) && parsed.memoriesSaved.length === 0) {
|
|
48
|
+
const reason = sanitizeForLog(String(parsed.reason ?? 'unknown'))
|
|
49
|
+
console.error(`[ccRecall] harvest yielded 0 memories (reason: ${reason}).`)
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// response wasn't JSON; already 200 so treat as success
|
|
53
|
+
}
|
|
54
|
+
resolve()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
req.on('error', (err) => {
|
|
58
|
+
console.error(`[ccRecall] harvest error: ${err.message} (is the service running on :${PORT}?)`)
|
|
59
|
+
resolve()
|
|
60
|
+
})
|
|
61
|
+
req.on('timeout', () => {
|
|
62
|
+
console.error(`[ccRecall] harvest timeout after ${TIMEOUT_MS}ms`)
|
|
63
|
+
req.destroy()
|
|
64
|
+
resolve()
|
|
65
|
+
})
|
|
66
|
+
req.write(payload)
|
|
67
|
+
req.end()
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
let input
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readStdin()
|
|
75
|
+
input = raw ? JSON.parse(raw) : {}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`[ccRecall] failed to parse hook input: ${err.message}`)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 'resume' means session is continuing, not ending — skip harvest.
|
|
82
|
+
if (input.reason === 'resume') return
|
|
83
|
+
|
|
84
|
+
const sessionId = input.session_id
|
|
85
|
+
if (!sessionId) return
|
|
86
|
+
|
|
87
|
+
await postSessionEnd(sessionId)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main().catch((err) => {
|
|
91
|
+
console.error(`[ccRecall] unexpected error: ${err.message}`)
|
|
92
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ccRecall SessionStart hook — injects relevant memories as context at session start.
|
|
3
|
+
// Wired via ~/.claude/settings.json hooks.SessionStart. Output on stdout is
|
|
4
|
+
// prepended to Claude's context; errors go to stderr only.
|
|
5
|
+
import http from 'node:http'
|
|
6
|
+
|
|
7
|
+
const PORT = Number(process.env.CCRECALL_PORT ?? 7749)
|
|
8
|
+
const HOST = '127.0.0.1'
|
|
9
|
+
const TIMEOUT_MS = 2000
|
|
10
|
+
const MEMORY_LIMIT = 5
|
|
11
|
+
|
|
12
|
+
async function readStdin() {
|
|
13
|
+
const chunks = []
|
|
14
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
15
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function queryMemories(query, projectId) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const params = new URLSearchParams({ q: query, limit: String(MEMORY_LIMIT) })
|
|
21
|
+
if (projectId) params.set('project', projectId)
|
|
22
|
+
const url = `/memory/query?${params.toString()}`
|
|
23
|
+
const req = http.request({
|
|
24
|
+
hostname: HOST,
|
|
25
|
+
port: PORT,
|
|
26
|
+
path: url,
|
|
27
|
+
method: 'GET',
|
|
28
|
+
timeout: TIMEOUT_MS,
|
|
29
|
+
}, (res) => {
|
|
30
|
+
let body = ''
|
|
31
|
+
res.on('data', (c) => { body += c })
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
if (res.statusCode !== 200) {
|
|
34
|
+
console.error(`[ccRecall] query failed ${res.statusCode}`)
|
|
35
|
+
resolve([])
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(body)
|
|
40
|
+
resolve(Array.isArray(parsed.memories) ? parsed.memories : [])
|
|
41
|
+
} catch {
|
|
42
|
+
resolve([])
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
req.on('error', (err) => {
|
|
47
|
+
console.error(`[ccRecall] query error: ${err.message} (is the service running on :${PORT}?)`)
|
|
48
|
+
resolve([])
|
|
49
|
+
})
|
|
50
|
+
req.on('timeout', () => {
|
|
51
|
+
console.error(`[ccRecall] query timeout after ${TIMEOUT_MS}ms`)
|
|
52
|
+
req.destroy()
|
|
53
|
+
resolve([])
|
|
54
|
+
})
|
|
55
|
+
req.end()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function projectNameFromCwd(cwd) {
|
|
60
|
+
if (!cwd || typeof cwd !== 'string') return ''
|
|
61
|
+
const parts = cwd.split('/').filter(Boolean)
|
|
62
|
+
return parts[parts.length - 1] ?? ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Matches Claude Code's project-directory encoding under ~/.claude/projects/
|
|
66
|
+
function projectIdFromCwd(cwd) {
|
|
67
|
+
if (!cwd || typeof cwd !== 'string') return ''
|
|
68
|
+
return cwd.replace(/\//g, '-')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatMemories(memories, query) {
|
|
72
|
+
if (memories.length === 0) return ''
|
|
73
|
+
const lines = memories.map((m) => {
|
|
74
|
+
const conf = m.confidence != null && m.confidence !== 1
|
|
75
|
+
? ` (conf ${Number(m.confidence).toFixed(2)})`
|
|
76
|
+
: ''
|
|
77
|
+
return `- ${m.content}${conf}`
|
|
78
|
+
})
|
|
79
|
+
return [
|
|
80
|
+
'[ccRecall memory recall]',
|
|
81
|
+
'',
|
|
82
|
+
...lines,
|
|
83
|
+
'',
|
|
84
|
+
`(matched via project keyword: "${query}")`,
|
|
85
|
+
].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
let input
|
|
90
|
+
try {
|
|
91
|
+
const raw = await readStdin()
|
|
92
|
+
input = raw ? JSON.parse(raw) : {}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[ccRecall] failed to parse hook input: ${err.message}`)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 'resume' continues an existing session; context is already there.
|
|
99
|
+
if (input.source === 'resume') return
|
|
100
|
+
|
|
101
|
+
const query = projectNameFromCwd(input.cwd)
|
|
102
|
+
if (!query) return
|
|
103
|
+
const projectId = projectIdFromCwd(input.cwd)
|
|
104
|
+
|
|
105
|
+
const memories = await queryMemories(query, projectId)
|
|
106
|
+
const text = formatMemories(memories, query)
|
|
107
|
+
if (text) process.stdout.write(text)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error(`[ccRecall] unexpected error: ${err.message}`)
|
|
112
|
+
})
|