@vishnuvmx/recall-memory 0.0.0-stage → 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -2
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +451 -0
  5. package/dist/core/agent.d.ts +21 -0
  6. package/dist/core/agent.js +140 -0
  7. package/dist/core/checkpoint.d.ts +42 -0
  8. package/dist/core/checkpoint.js +233 -0
  9. package/dist/core/config.d.ts +5 -0
  10. package/dist/core/config.js +36 -0
  11. package/dist/core/cost.d.ts +19 -0
  12. package/dist/core/cost.js +78 -0
  13. package/dist/core/doctor.d.ts +7 -0
  14. package/dist/core/doctor.js +69 -0
  15. package/dist/core/git.d.ts +27 -0
  16. package/dist/core/git.js +107 -0
  17. package/dist/core/index-builder.d.ts +11 -0
  18. package/dist/core/index-builder.js +84 -0
  19. package/dist/core/init.d.ts +13 -0
  20. package/dist/core/init.js +52 -0
  21. package/dist/core/landmarks.d.ts +19 -0
  22. package/dist/core/landmarks.js +323 -0
  23. package/dist/core/mapper.d.ts +28 -0
  24. package/dist/core/mapper.js +357 -0
  25. package/dist/core/memory.d.ts +44 -0
  26. package/dist/core/memory.js +199 -0
  27. package/dist/core/project.d.ts +12 -0
  28. package/dist/core/project.js +34 -0
  29. package/dist/core/retrieval.d.ts +27 -0
  30. package/dist/core/retrieval.js +180 -0
  31. package/dist/core/seed.d.ts +25 -0
  32. package/dist/core/seed.js +196 -0
  33. package/dist/core/service.d.ts +76 -0
  34. package/dist/core/service.js +228 -0
  35. package/dist/core/where.d.ts +14 -0
  36. package/dist/core/where.js +86 -0
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +11 -0
  39. package/dist/mcp/server.d.ts +65 -0
  40. package/dist/mcp/server.js +121 -0
  41. package/dist/schemas/index.d.ts +68 -0
  42. package/dist/schemas/index.js +43 -0
  43. package/dist/templates/index.d.ts +21 -0
  44. package/dist/templates/index.js +62 -0
  45. package/dist/utils/fs.d.ts +15 -0
  46. package/dist/utils/fs.js +73 -0
  47. package/dist/utils/managed.d.ts +5 -0
  48. package/dist/utils/managed.js +41 -0
  49. package/dist/utils/secrets.d.ts +3 -0
  50. package/dist/utils/secrets.js +23 -0
  51. package/package.json +65 -4
@@ -0,0 +1,228 @@
1
+ import { agentState } from './agent.js';
2
+ import { recordCheckpoint, pruneSessions } from './checkpoint.js';
3
+ import { loadConfig } from './config.js';
4
+ import { gitInfo, recallChanges, recallHistory, undoRecall } from './git.js';
5
+ import { indexState, rebuildIndex } from './index-builder.js';
6
+ import { generateProjectMap } from './mapper.js';
7
+ import { appendChangelog, createMemory, loadAll, rewriteMemory, updateMemory } from './memory.js';
8
+ import { requireInitialized } from './project.js';
9
+ import { search, tokenize } from './retrieval.js';
10
+ import { where } from './where.js';
11
+ import { PROFILE_BUDGETS } from '../schemas/index.js';
12
+ import fs from 'node:fs';
13
+ import { resolveInside } from '../utils/fs.js';
14
+ /** Create a memory, log it, and refresh the index. */
15
+ export function remember(start, input, action = 'remember') {
16
+ const p = requireInitialized(start);
17
+ const m = createMemory(p.recallDir, input);
18
+ appendChangelog(p.recallDir, action, m);
19
+ rebuildIndex(p.recallDir);
20
+ return m;
21
+ }
22
+ /** Agent-facing: always lands as `proposed` and is excluded from search until approved. */
23
+ export function propose(start, input) {
24
+ return remember(start, { ...input, status: 'proposed' }, 'propose');
25
+ }
26
+ const normTitle = (t) => t.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
27
+ /** Same normalized title, or high word overlap (Jaccard >= 0.7 over >= 2 words each). */
28
+ export function findDuplicate(candidates, title) {
29
+ const n = normTitle(title);
30
+ const exact = candidates.find((m) => normTitle(m.title) === n);
31
+ if (exact)
32
+ return exact;
33
+ const a = new Set(tokenize(title));
34
+ if (a.size < 2)
35
+ return undefined;
36
+ let best;
37
+ for (const m of candidates) {
38
+ const b = new Set(tokenize(m.title));
39
+ if (b.size < 2)
40
+ continue;
41
+ const inter = [...a].filter((t) => b.has(t)).length;
42
+ const j = inter / (a.size + b.size - inter);
43
+ if (j >= 0.7 && (!best || j > best.j))
44
+ best = { m, j };
45
+ }
46
+ return best?.m;
47
+ }
48
+ /**
49
+ * The write path for agents (CLI `remember` and MCP `recall_remember`).
50
+ * - `writes: auto` (default): active immediately, so memory is useful without human ceremony.
51
+ * - `writes: review` or RECALL_AGENT=1: lands as `proposed`.
52
+ * A near-duplicate title updates the existing memory instead of adding another file, which keeps
53
+ * retrieval results (and their token cost) from growing with repetition.
54
+ */
55
+ export function agentRemember(start, input) {
56
+ const p = requireInitialized(start);
57
+ let mode = 'auto';
58
+ try {
59
+ mode = loadConfig(p.recallDir).writes;
60
+ }
61
+ catch {
62
+ /* surfaced by `recall doctor` */
63
+ }
64
+ const status = input.status ?? (mode === 'review' || process.env.RECALL_AGENT === '1' ? 'proposed' : 'active');
65
+ const type = input.type ?? 'fact';
66
+ // In review mode never silently rewrite an approved memory; only merge into other proposals.
67
+ const candidates = loadAll(p.recallDir).memories.filter((m) => m.type === type && (status === 'active' ? m.status === 'active' || m.status === 'proposed' : m.status === 'proposed'));
68
+ const dup = findDuplicate(candidates, input.title);
69
+ const memory = dup
70
+ ? rewriteMemory(p.recallDir, dup.id, { ...input, type, status })
71
+ : createMemory(p.recallDir, { ...input, type, status });
72
+ appendChangelog(p.recallDir, dup ? 'update' : 'remember', memory);
73
+ rebuildIndex(p.recallDir);
74
+ return { memory, action: dup ? 'updated' : 'created' };
75
+ }
76
+ export function whereIs(start, query, limit) {
77
+ const p = requireInitialized(start);
78
+ return where(p.root, p.recallDir, query, limit);
79
+ }
80
+ export function approve(start, id) {
81
+ const p = requireInitialized(start);
82
+ const m = updateMemory(p.recallDir, id, { status: 'active' });
83
+ appendChangelog(p.recallDir, 'approve', m);
84
+ rebuildIndex(p.recallDir);
85
+ return m;
86
+ }
87
+ export function archive(start, id) {
88
+ const p = requireInitialized(start);
89
+ const m = updateMemory(p.recallDir, id, { status: 'archived' });
90
+ appendChangelog(p.recallDir, 'archive', m);
91
+ rebuildIndex(p.recallDir);
92
+ return m;
93
+ }
94
+ export function context(start, query, opts = {}) {
95
+ const p = requireInitialized(start);
96
+ return search(loadAll(p.recallDir).memories, query, opts);
97
+ }
98
+ function estimateTokens(chars) {
99
+ return Math.ceil(chars / 4);
100
+ }
101
+ function computeTokenFootprint(recallDir, profile = 'standard') {
102
+ const targets = ['MEMORY.md', 'INDEX.md', 'PROJECT_MAP.md', 'CURRENT_STATE.md'];
103
+ const files = {};
104
+ let totalChars = 0;
105
+ for (const f of targets) {
106
+ const p = resolveInside(recallDir, f);
107
+ if (fs.existsSync(p)) {
108
+ try {
109
+ const content = fs.readFileSync(p, 'utf8');
110
+ const tok = estimateTokens(content.length);
111
+ files[f] = tok;
112
+ totalChars += content.length;
113
+ }
114
+ catch {
115
+ files[f] = 0;
116
+ }
117
+ }
118
+ else {
119
+ files[f] = 0;
120
+ }
121
+ }
122
+ return {
123
+ totalTokens: estimateTokens(totalChars),
124
+ budgetTokens: PROFILE_BUDGETS[profile],
125
+ profile,
126
+ files,
127
+ };
128
+ }
129
+ export function status(start) {
130
+ const p = requireInitialized(start);
131
+ const loaded = loadAll(p.recallDir);
132
+ const count = (key) => {
133
+ const out = {};
134
+ for (const m of loaded.memories)
135
+ out[m[key]] = (out[m[key]] ?? 0) + 1;
136
+ return out;
137
+ };
138
+ let agentFile = 'AGENTS.md';
139
+ let agent = 'unknown';
140
+ let profile = 'standard';
141
+ try {
142
+ const cfg = loadConfig(p.recallDir);
143
+ agentFile = cfg.agentFile;
144
+ profile = cfg.profile ?? 'standard';
145
+ agent = agentState(p.root, agentFile);
146
+ }
147
+ catch {
148
+ /* surfaced by `recall doctor` */
149
+ }
150
+ const g = gitInfo(p.root);
151
+ return {
152
+ project: p,
153
+ byType: count('type'),
154
+ byStatus: count('status'),
155
+ problems: loaded.problems.length,
156
+ index: indexState(p.recallDir),
157
+ agentFile,
158
+ agent,
159
+ git: { ...g, changes: g.isRepo ? recallChanges(p.root) : null },
160
+ tokens: computeTokenFootprint(p.recallDir, profile),
161
+ profile,
162
+ };
163
+ }
164
+ export function getProposals(start) {
165
+ const p = requireInitialized(start);
166
+ return loadAll(p.recallDir).memories.filter((m) => m.status === 'proposed');
167
+ }
168
+ export function approveAll(start) {
169
+ const p = requireInitialized(start);
170
+ const proposals = getProposals(start);
171
+ const approved = [];
172
+ for (const m of proposals) {
173
+ const updated = updateMemory(p.recallDir, m.id, { status: 'active' });
174
+ appendChangelog(p.recallDir, 'approve', updated);
175
+ approved.push(updated);
176
+ }
177
+ if (approved.length > 0) {
178
+ rebuildIndex(p.recallDir);
179
+ }
180
+ return approved;
181
+ }
182
+ export function archiveAll(start) {
183
+ const p = requireInitialized(start);
184
+ const proposals = getProposals(start);
185
+ const archived = [];
186
+ for (const m of proposals) {
187
+ const updated = updateMemory(p.recallDir, m.id, { status: 'archived' });
188
+ appendChangelog(p.recallDir, 'archive', updated);
189
+ archived.push(updated);
190
+ }
191
+ if (archived.length > 0) {
192
+ rebuildIndex(p.recallDir);
193
+ }
194
+ return archived;
195
+ }
196
+ export function log(start, limit) {
197
+ const p = requireInitialized(start);
198
+ const g = gitInfo(p.root);
199
+ if (g.isRepo) {
200
+ const committed = recallHistory(p.root, limit);
201
+ if (committed) {
202
+ return { source: 'git', committed, uncommitted: recallChanges(p.root) ?? [], changelog: [] };
203
+ }
204
+ }
205
+ const file = resolveInside(p.recallDir, 'logs', 'CHANGELOG.md');
206
+ const lines = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.startsWith('- ')) : [];
207
+ return { source: 'changelog', committed: [], uncommitted: [], changelog: lines.slice(-limit).reverse() };
208
+ }
209
+ export function map(start, profile) {
210
+ const p = requireInitialized(start);
211
+ return generateProjectMap(p.root, p.recallDir, profile);
212
+ }
213
+ export function checkpoint(start, summary, opts) {
214
+ const p = requireInitialized(start);
215
+ return recordCheckpoint(p.root, p.recallDir, summary, opts);
216
+ }
217
+ export function prune(start, opts) {
218
+ const p = requireInitialized(start);
219
+ return pruneSessions(p.recallDir, opts);
220
+ }
221
+ export function undo(start, opts) {
222
+ const p = requireInitialized(start);
223
+ const res = undoRecall(p.root, '.recall', opts);
224
+ if (!opts?.dryRun && res.ok && fs.existsSync(p.recallDir)) {
225
+ rebuildIndex(p.recallDir);
226
+ }
227
+ return res;
228
+ }
@@ -0,0 +1,14 @@
1
+ import { type Landmark } from './landmarks.js';
2
+ export interface WhereHit {
3
+ file: string;
4
+ line: number;
5
+ name: string;
6
+ kind: Landmark['kind'] | 'file';
7
+ totalLines: number;
8
+ }
9
+ /**
10
+ * Symbol/file lookup over live landmarks (mtime-cached in `.recall/.cache`), so results are never
11
+ * stale and the agent pays for a handful of `file:line` rows instead of reading PROJECT_MAP.md.
12
+ */
13
+ export declare function where(root: string, recallDir: string, query: string, limit?: number): WhereHit[];
14
+ export declare function formatWhere(hits: WhereHit[]): string;
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { scanLandmarks } from './landmarks.js';
4
+ import { DEFAULT_IGNORED_DIRS, createGitignoreFilter } from './mapper.js';
5
+ import { toPosix } from '../utils/fs.js';
6
+ /**
7
+ * Symbol/file lookup over live landmarks (mtime-cached in `.recall/.cache`), so results are never
8
+ * stale and the agent pays for a handful of `file:line` rows instead of reading PROJECT_MAP.md.
9
+ */
10
+ export function where(root, recallDir, query, limit = 15) {
11
+ const q = query.trim().toLowerCase();
12
+ if (!q)
13
+ return [];
14
+ const files = scanLandmarks(root, 1, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, recallDir);
15
+ const ranked = [];
16
+ const seenFiles = new Set();
17
+ for (const f of files) {
18
+ seenFiles.add(f.relativePath);
19
+ const p = f.relativePath.toLowerCase();
20
+ if (p.includes(q)) {
21
+ const base = p.slice(p.lastIndexOf('/') + 1);
22
+ ranked.push({ hit: { file: f.relativePath, line: 1, name: f.relativePath, kind: 'file', totalLines: f.totalLines }, rank: base === q ? 0 : base.startsWith(q) ? 1 : 3 });
23
+ }
24
+ for (const lm of f.landmarks) {
25
+ const n = lm.name.toLowerCase();
26
+ const rank = n === q ? 0 : n.startsWith(q) ? 2 : n.includes(q) ? 4 : -1;
27
+ if (rank >= 0)
28
+ ranked.push({ hit: { file: f.relativePath, line: lm.line, name: lm.name, kind: lm.kind, totalLines: f.totalLines }, rank });
29
+ }
30
+ }
31
+ // Also check non-code project files (configs, package.json, Dockerfile, etc.)
32
+ const isGitignored = createGitignoreFilter(root);
33
+ function walk(currentDir, depth = 0) {
34
+ if (depth > 5)
35
+ return;
36
+ let entries = [];
37
+ try {
38
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
39
+ }
40
+ catch {
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (entry.name.startsWith('.') && entry.name !== '.gitignore' && entry.name !== '.env.example') {
45
+ if (entry.isDirectory())
46
+ continue;
47
+ }
48
+ const full = path.join(currentDir, entry.name);
49
+ const rel = toPosix(path.relative(root, full));
50
+ if (entry.isDirectory()) {
51
+ if (DEFAULT_IGNORED_DIRS.has(entry.name) || isGitignored(rel, true))
52
+ continue;
53
+ walk(full, depth + 1);
54
+ }
55
+ else if (entry.isFile()) {
56
+ if (seenFiles.has(rel) || isGitignored(rel, false))
57
+ continue;
58
+ const p = rel.toLowerCase();
59
+ if (p.includes(q)) {
60
+ let lineCount = 0;
61
+ try {
62
+ const stat = fs.statSync(full);
63
+ if (stat.size <= 2 * 1024 * 1024) {
64
+ lineCount = fs.readFileSync(full, 'utf8').split('\n').length;
65
+ }
66
+ }
67
+ catch {
68
+ // ignore unreadable
69
+ }
70
+ const base = p.slice(p.lastIndexOf('/') + 1);
71
+ ranked.push({ hit: { file: rel, line: 1, name: rel, kind: 'file', totalLines: lineCount }, rank: base === q ? 0 : base.startsWith(q) ? 1 : 3 });
72
+ }
73
+ }
74
+ }
75
+ }
76
+ walk(root);
77
+ ranked.sort((a, b) => a.rank - b.rank || a.hit.file.localeCompare(b.hit.file) || a.hit.line - b.hit.line);
78
+ return ranked.slice(0, limit).map((r) => r.hit);
79
+ }
80
+ export function formatWhere(hits) {
81
+ if (hits.length === 0)
82
+ return 'No match.';
83
+ return hits
84
+ .map((h) => (h.kind === 'file' ? `${h.file} (${h.totalLines} lines)` : `${h.file}:${h.line} ${h.name} (${h.kind})`))
85
+ .join('\n');
86
+ }
@@ -0,0 +1,11 @@
1
+ export { initProject } from './core/init.js';
2
+ export { search } from './core/retrieval.js';
3
+ export { loadAll, createMemory } from './core/memory.js';
4
+ export { findProject } from './core/project.js';
5
+ export { rebuildIndex } from './core/index-builder.js';
6
+ export { remember, propose, approve, approveAll, archive, archiveAll, getProposals, context, status, log, map, checkpoint, prune, agentRemember, whereIs } from './core/service.js';
7
+ export { seed } from './core/seed.js';
8
+ export { generateProjectMap } from './core/mapper.js';
9
+ export { recordCheckpoint, pruneSessions, pruneSessionLog } from './core/checkpoint.js';
10
+ export { costReport } from './core/cost.js';
11
+ export { where, formatWhere } from './core/where.js';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { initProject } from './core/init.js';
2
+ export { search } from './core/retrieval.js';
3
+ export { loadAll, createMemory } from './core/memory.js';
4
+ export { findProject } from './core/project.js';
5
+ export { rebuildIndex } from './core/index-builder.js';
6
+ export { remember, propose, approve, approveAll, archive, archiveAll, getProposals, context, status, log, map, checkpoint, prune, agentRemember, whereIs } from './core/service.js';
7
+ export { seed } from './core/seed.js';
8
+ export { generateProjectMap } from './core/mapper.js';
9
+ export { recordCheckpoint, pruneSessions, pruneSessionLog } from './core/checkpoint.js';
10
+ export { costReport } from './core/cost.js';
11
+ export { where, formatWhere } from './core/where.js';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Minimal MCP server over stdio (newline-delimited JSON-RPC 2.0), no SDK dependency.
3
+ *
4
+ * TOKEN BUDGET: every listed tool schema is sent to the model on every turn, so the list is kept to
5
+ * three terse tools (enforced by tests/budget.test.ts). Bookkeeping (checkpoints, map refresh, status)
6
+ * is NOT exposed to the model; those run from hooks/CLI. Legacy tool names stay callable, unlisted.
7
+ */
8
+ export declare const TOOLS: ({
9
+ name: string;
10
+ description: string;
11
+ inputSchema: {
12
+ type: string;
13
+ properties: {
14
+ query: {
15
+ type: string;
16
+ };
17
+ name?: undefined;
18
+ title?: undefined;
19
+ text?: undefined;
20
+ type?: undefined;
21
+ };
22
+ required: string[];
23
+ };
24
+ } | {
25
+ name: string;
26
+ description: string;
27
+ inputSchema: {
28
+ type: string;
29
+ properties: {
30
+ name: {
31
+ type: string;
32
+ };
33
+ query?: undefined;
34
+ title?: undefined;
35
+ text?: undefined;
36
+ type?: undefined;
37
+ };
38
+ required: string[];
39
+ };
40
+ } | {
41
+ name: string;
42
+ description: string;
43
+ inputSchema: {
44
+ type: string;
45
+ properties: {
46
+ title: {
47
+ type: string;
48
+ };
49
+ text: {
50
+ type: string;
51
+ };
52
+ type: {
53
+ type: string;
54
+ enum: string[];
55
+ };
56
+ query?: undefined;
57
+ name?: undefined;
58
+ };
59
+ required: string[];
60
+ };
61
+ })[];
62
+ type Json = Record<string, unknown>;
63
+ export declare function handleMessage(cwd: string, msg: Json): Json | null;
64
+ export declare function serveStdio(cwd: string): void;
65
+ export {};
@@ -0,0 +1,121 @@
1
+ import readline from 'node:readline';
2
+ import { agentRemember, checkpoint, context, map, propose, status, whereIs } from '../core/service.js';
3
+ import { formatHitsCompact } from '../core/retrieval.js';
4
+ import { formatWhere } from '../core/where.js';
5
+ import { MEMORY_TYPES } from '../schemas/index.js';
6
+ /**
7
+ * Minimal MCP server over stdio (newline-delimited JSON-RPC 2.0), no SDK dependency.
8
+ *
9
+ * TOKEN BUDGET: every listed tool schema is sent to the model on every turn, so the list is kept to
10
+ * three terse tools (enforced by tests/budget.test.ts). Bookkeeping (checkpoints, map refresh, status)
11
+ * is NOT exposed to the model; those run from hooks/CLI. Legacy tool names stay callable, unlisted.
12
+ */
13
+ export const TOOLS = [
14
+ {
15
+ name: 'recall_context',
16
+ description: 'Search project memory (decisions, conventions, lessons). Capped output.',
17
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
18
+ },
19
+ {
20
+ name: 'recall_where',
21
+ description: 'Find a symbol or file: returns file:line rows.',
22
+ inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
23
+ },
24
+ {
25
+ name: 'recall_remember',
26
+ description: 'Save a non-obvious decision, convention, or lesson. Similar titles update in place.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ title: { type: 'string' },
31
+ text: { type: 'string' },
32
+ type: { type: 'string', enum: ['decision', 'convention', 'lesson', 'bugfix', 'failed_approach'] },
33
+ },
34
+ required: ['title', 'text'],
35
+ },
36
+ },
37
+ ];
38
+ const asType = (v) => (MEMORY_TYPES.includes(v) ? v : 'fact');
39
+ const asTags = (v) => (Array.isArray(v) ? v.map(String) : []);
40
+ function callTool(cwd, name, args) {
41
+ if (name === 'recall_context') {
42
+ return formatHitsCompact(context(cwd, String(args.query ?? ''), { limit: Number(args.limit) || 5 }));
43
+ }
44
+ if (name === 'recall_where') {
45
+ return formatWhere(whereIs(cwd, String(args.name ?? args.query ?? '')));
46
+ }
47
+ if (name === 'recall_remember') {
48
+ const r = agentRemember(cwd, { title: String(args.title ?? ''), text: String(args.text ?? ''), type: asType(args.type), tags: asTags(args.tags) });
49
+ return `${r.action === 'updated' ? 'Updated' : 'Saved'} ${r.memory.id} (${r.memory.status}).`;
50
+ }
51
+ // ---- Legacy tools: callable for older clients, deliberately not listed ----
52
+ if (name === 'recall_propose') {
53
+ const m = propose(cwd, { title: String(args.title ?? ''), text: String(args.text ?? ''), type: asType(args.type), tags: asTags(args.tags) });
54
+ return `Proposed ${m.id} at ${m.file}. A human must run \`recall approve ${m.id}\`.`;
55
+ }
56
+ if (name === 'recall_checkpoint') {
57
+ const summary = args.summary ? String(args.summary) : undefined;
58
+ const nextSteps = args.nextSteps ? String(args.nextSteps) : undefined;
59
+ const res = checkpoint(cwd, summary, { nextSteps, auto: !args.summary });
60
+ return `Checkpoint recorded [${res.timestamp}]: ${res.summary}`;
61
+ }
62
+ if (name === 'recall_map') {
63
+ return `Project map refreshed at ${map(cwd)}.`;
64
+ }
65
+ if (name === 'recall_status') {
66
+ const s = status(cwd);
67
+ const types = Object.entries(s.byType).map(([k, v]) => `${k}: ${v}`).join(', ') || 'none';
68
+ const statuses = Object.entries(s.byStatus).map(([k, v]) => `${k}: ${v}`).join(', ') || 'none';
69
+ return `Recall Status:\n- Types: ${types}\n- Statuses: ${statuses}\n- Index: ${s.index}\n- Problems: ${s.problems}`;
70
+ }
71
+ throw new Error(`Unknown tool: ${name}`);
72
+ }
73
+ export function handleMessage(cwd, msg) {
74
+ const id = msg.id;
75
+ const reply = (result) => ({ jsonrpc: '2.0', id, result });
76
+ const fail = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
77
+ if (id === undefined)
78
+ return null; // notifications need no response
79
+ switch (msg.method) {
80
+ case 'initialize': {
81
+ const params = (msg.params ?? {});
82
+ return reply({
83
+ protocolVersion: typeof params.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
84
+ capabilities: { tools: {} },
85
+ serverInfo: { name: 'recall', version: '0.1.0' },
86
+ });
87
+ }
88
+ case 'ping':
89
+ return reply({});
90
+ case 'tools/list':
91
+ return reply({ tools: TOOLS });
92
+ case 'tools/call': {
93
+ const p = (msg.params ?? {});
94
+ try {
95
+ const text = callTool(cwd, String(p.name), (p.arguments ?? {}));
96
+ return reply({ content: [{ type: 'text', text }] });
97
+ }
98
+ catch (err) {
99
+ return reply({ content: [{ type: 'text', text: err.message }], isError: true });
100
+ }
101
+ }
102
+ default:
103
+ return fail(-32601, `Method not found: ${String(msg.method)}`);
104
+ }
105
+ }
106
+ export function serveStdio(cwd) {
107
+ const rl = readline.createInterface({ input: process.stdin });
108
+ rl.on('line', (line) => {
109
+ if (!line.trim())
110
+ return;
111
+ let out;
112
+ try {
113
+ out = handleMessage(cwd, JSON.parse(line));
114
+ }
115
+ catch {
116
+ out = { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } };
117
+ }
118
+ if (out)
119
+ process.stdout.write(`${JSON.stringify(out)}\n`);
120
+ });
121
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ export declare const MEMORY_TYPES: readonly ["fact", "decision", "convention", "lesson", "bugfix", "failed_approach", "task", "design_system"];
3
+ export declare const LIFECYCLE: readonly ["proposed", "active", "superseded", "archived"];
4
+ export declare const VERIFICATION: readonly ["unverified", "verified", "needs_review"];
5
+ export type MemoryType = (typeof MEMORY_TYPES)[number];
6
+ export type Lifecycle = (typeof LIFECYCLE)[number];
7
+ export type Verification = (typeof VERIFICATION)[number];
8
+ export declare const memoryMetaSchema: z.ZodObject<{
9
+ id: z.ZodString;
10
+ title: z.ZodString;
11
+ type: z.ZodEnum<["fact", "decision", "convention", "lesson", "bugfix", "failed_approach", "task", "design_system"]>;
12
+ created: z.ZodEffects<z.ZodString, string, unknown>;
13
+ updated: z.ZodEffects<z.ZodString, string, unknown>;
14
+ tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
15
+ status: z.ZodEnum<["proposed", "active", "superseded", "archived"]>;
16
+ verification: z.ZodEnum<["unverified", "verified", "needs_review"]>;
17
+ sources: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
18
+ }, "strip", z.ZodTypeAny, {
19
+ created: string;
20
+ updated: string;
21
+ type: "fact" | "decision" | "convention" | "lesson" | "bugfix" | "failed_approach" | "task" | "design_system";
22
+ status: "proposed" | "active" | "superseded" | "archived";
23
+ id: string;
24
+ title: string;
25
+ tags: string[];
26
+ verification: "unverified" | "verified" | "needs_review";
27
+ sources: string[];
28
+ }, {
29
+ type: "fact" | "decision" | "convention" | "lesson" | "bugfix" | "failed_approach" | "task" | "design_system";
30
+ status: "proposed" | "active" | "superseded" | "archived";
31
+ id: string;
32
+ title: string;
33
+ verification: "unverified" | "verified" | "needs_review";
34
+ created?: unknown;
35
+ updated?: unknown;
36
+ tags?: string[] | undefined;
37
+ sources?: string[] | undefined;
38
+ }>;
39
+ export type MemoryMeta = z.infer<typeof memoryMetaSchema>;
40
+ export declare const PROFILE_TIERS: readonly ["compact", "standard", "frontier"];
41
+ export type ProfileTier = (typeof PROFILE_TIERS)[number];
42
+ export declare const PROFILE_BUDGETS: Record<ProfileTier, number>;
43
+ export declare const WRITE_MODES: readonly ["auto", "review"];
44
+ export type WriteMode = (typeof WRITE_MODES)[number];
45
+ export declare const configSchema: z.ZodObject<{
46
+ version: z.ZodLiteral<1>;
47
+ agentFile: z.ZodDefault<z.ZodString>;
48
+ profile: z.ZodDefault<z.ZodEnum<["compact", "standard", "frontier"]>>;
49
+ retentionDays: z.ZodDefault<z.ZodNumber>;
50
+ maxLogEntries: z.ZodDefault<z.ZodNumber>;
51
+ /** `auto`: agent memory writes are active immediately. `review`: they land as `proposed` for human approval. */
52
+ writes: z.ZodDefault<z.ZodEnum<["auto", "review"]>>;
53
+ }, "strict", z.ZodTypeAny, {
54
+ version: 1;
55
+ agentFile: string;
56
+ profile: "compact" | "standard" | "frontier";
57
+ retentionDays: number;
58
+ maxLogEntries: number;
59
+ writes: "auto" | "review";
60
+ }, {
61
+ version: 1;
62
+ agentFile?: string | undefined;
63
+ profile?: "compact" | "standard" | "frontier" | undefined;
64
+ retentionDays?: number | undefined;
65
+ maxLogEntries?: number | undefined;
66
+ writes?: "auto" | "review" | undefined;
67
+ }>;
68
+ export type RecallConfig = z.infer<typeof configSchema>;
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export const MEMORY_TYPES = [
3
+ 'fact',
4
+ 'decision',
5
+ 'convention',
6
+ 'lesson',
7
+ 'bugfix',
8
+ 'failed_approach',
9
+ 'task',
10
+ 'design_system',
11
+ ];
12
+ export const LIFECYCLE = ['proposed', 'active', 'superseded', 'archived'];
13
+ export const VERIFICATION = ['unverified', 'verified', 'needs_review'];
14
+ const dateString = z.preprocess((v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v), z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'expected YYYY-MM-DD'));
15
+ export const memoryMetaSchema = z.object({
16
+ id: z.string().regex(/^mem_[a-z0-9_]+$/i, 'expected mem_<id>'),
17
+ title: z.string().trim().min(1),
18
+ type: z.enum(MEMORY_TYPES),
19
+ created: dateString,
20
+ updated: dateString,
21
+ tags: z.array(z.string()).default([]),
22
+ status: z.enum(LIFECYCLE),
23
+ verification: z.enum(VERIFICATION),
24
+ sources: z.array(z.string()).default([]),
25
+ });
26
+ export const PROFILE_TIERS = ['compact', 'standard', 'frontier'];
27
+ export const PROFILE_BUDGETS = {
28
+ compact: 3000,
29
+ standard: 12000,
30
+ frontier: 45000,
31
+ };
32
+ export const WRITE_MODES = ['auto', 'review'];
33
+ export const configSchema = z
34
+ .object({
35
+ version: z.literal(1),
36
+ agentFile: z.string().min(1).default('AGENTS.md'),
37
+ profile: z.enum(PROFILE_TIERS).default('standard'),
38
+ retentionDays: z.number().int().positive().default(14),
39
+ maxLogEntries: z.number().int().positive().default(50),
40
+ /** `auto`: agent memory writes are active immediately. `review`: they land as `proposed` for human approval. */
41
+ writes: z.enum(WRITE_MODES).default('auto'),
42
+ })
43
+ .strict();