claude-mem-lite 3.96.0 → 3.97.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 (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
package/tool-schemas.mjs CHANGED
@@ -9,48 +9,51 @@ export const OBS_TYPE_ENUM = z.enum([...OBS_TYPES]);
9
9
 
10
10
  // LLM-friendly coercion: accept string numbers and normalize to proper types
11
11
  const coerceInt = z.preprocess(
12
- (v) => (typeof v === 'string' && /^-?\d+$/.test(v.trim())) ? parseInt(v.trim(), 10) : v,
13
- z.number().int()
12
+ (v) => (typeof v === 'string' && /^-?\d+$/.test(v.trim()) ? parseInt(v.trim(), 10) : v),
13
+ z.number().int(),
14
14
  );
15
15
 
16
16
  // LLM-friendly coercion: accept "true"/"false"/"True"/"TRUE" strings as boolean
17
17
  const coerceBool = z.preprocess(
18
- (v) => typeof v === 'string' ? ({ true: true, false: false })[v.toLowerCase()] ?? v : v,
19
- z.boolean()
18
+ (v) => (typeof v === 'string' ? ({ true: true, false: false }[v.toLowerCase()] ?? v) : v),
19
+ z.boolean(),
20
20
  );
21
21
 
22
22
  // Coerce ids: accept single number, string "123", comma-separated "1,2,3", or array
23
- const coerceIntArray = z.preprocess(
24
- (v) => {
25
- if (Array.isArray(v)) return v.map(x => typeof x === 'string' ? parseInt(x, 10) : x);
26
- if (typeof v === 'number') return [v];
27
- if (typeof v === 'string') return v.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
28
- return v;
29
- },
30
- z.array(z.number().int())
31
- );
23
+ const coerceIntArray = z.preprocess((v) => {
24
+ if (Array.isArray(v)) return v.map((x) => (typeof x === 'string' ? parseInt(x, 10) : x));
25
+ if (typeof v === 'number') return [v];
26
+ if (typeof v === 'string')
27
+ return v
28
+ .split(',')
29
+ .map((s) => parseInt(s.trim(), 10))
30
+ .filter((n) => !isNaN(n));
31
+ return v;
32
+ }, z.array(z.number().int()));
32
33
 
33
34
  // Coerce string arrays: accept array, comma-separated string, JSON-array string, or bare string.
34
35
  // MCP bridges sometimes JSON-stringify complex args — bare `z.array(z.string())` rejects those
35
36
  // with "expected array, received string" and the caller loses the field silently. Parity with
36
37
  // coerceIntArray: tolerate the same shapes so files/fields survive client serialization quirks.
37
- const coerceStringArray = z.preprocess(
38
- (v) => {
39
- if (Array.isArray(v)) return v.map(x => typeof x === 'string' ? x : String(x));
40
- if (typeof v === 'string') {
41
- const s = v.trim();
42
- if (s.startsWith('[') && s.endsWith(']')) {
43
- try {
44
- const parsed = JSON.parse(s);
45
- if (Array.isArray(parsed)) return parsed.map(x => typeof x === 'string' ? x : String(x));
46
- } catch { /* fall through to comma-split */ }
38
+ const coerceStringArray = z.preprocess((v) => {
39
+ if (Array.isArray(v)) return v.map((x) => (typeof x === 'string' ? x : String(x)));
40
+ if (typeof v === 'string') {
41
+ const s = v.trim();
42
+ if (s.startsWith('[') && s.endsWith(']')) {
43
+ try {
44
+ const parsed = JSON.parse(s);
45
+ if (Array.isArray(parsed)) return parsed.map((x) => (typeof x === 'string' ? x : String(x)));
46
+ } catch {
47
+ /* fall through to comma-split */
47
48
  }
48
- return s.split(',').map(x => x.trim()).filter(x => x.length > 0);
49
49
  }
50
- return v;
51
- },
52
- z.array(z.string())
53
- );
50
+ return s
51
+ .split(',')
52
+ .map((x) => x.trim())
53
+ .filter((x) => x.length > 0);
54
+ }
55
+ return v;
56
+ }, z.array(z.string()));
54
57
 
55
58
  // Coerce mixed ID tokens (#N / P#N / S#N / D#N / bare N) for mem_get. Accepts:
56
59
  // - native arrays: [1, "P#2", "#3"]
@@ -61,45 +64,93 @@ const coerceStringArray = z.preprocess(
61
64
  // at the handler. Closes the CLI↔MCP gap noted in #8127.
62
65
  const coerceMixedIdTokens = z.preprocess(
63
66
  (v) => {
64
- const norm = (x) => typeof x === 'string' ? x.trim() : String(x);
65
- if (Array.isArray(v)) return v.map(norm).filter(s => s.length > 0);
67
+ const norm = (x) => (typeof x === 'string' ? x.trim() : String(x));
68
+ if (Array.isArray(v)) return v.map(norm).filter((s) => s.length > 0);
66
69
  if (typeof v === 'number') return [String(v)];
67
70
  if (typeof v === 'string') {
68
71
  const s = v.trim();
69
72
  if (s.startsWith('[') && s.endsWith(']')) {
70
73
  try {
71
74
  const parsed = JSON.parse(s);
72
- if (Array.isArray(parsed)) return parsed.map(norm).filter(x => x.length > 0);
73
- } catch { /* fall through to comma-split */ }
75
+ if (Array.isArray(parsed)) return parsed.map(norm).filter((x) => x.length > 0);
76
+ } catch {
77
+ /* fall through to comma-split */
78
+ }
74
79
  }
75
- return s.split(',').map(x => x.trim()).filter(Boolean);
80
+ return s
81
+ .split(',')
82
+ .map((x) => x.trim())
83
+ .filter(Boolean);
76
84
  }
77
85
  return v;
78
86
  },
79
87
  // D#N (deferred_work) requires the `#` — bare "D92" is prose, not a token.
80
88
  // Round-trip rule: `defer list` renders "(D#92)", so mem_get must accept it
81
89
  // back (tests/schema-roundtrip.test.mjs). Delete/timeline keep rejecting D#.
82
- z.array(z.string().regex(/^(?:[Dd]#|[EePpSs]?#?)\d+$/, 'Expected N, #N, P#N, S#N, E#N, or D#N')).min(1).max(20)
90
+ z
91
+ .array(z.string().regex(/^(?:[Dd]#|[EePpSs]?#?)\d+$/, 'Expected N, #N, P#N, S#N, E#N, or D#N'))
92
+ .min(1)
93
+ .max(20),
83
94
  );
84
95
 
85
96
  export const memSearchSchema = {
86
97
  query: z.string().optional().describe('Search query (FTS5 syntax supported)'),
87
- type: z.enum(['observations', 'sessions', 'prompts', 'events']).optional().describe('Limit to one source table (default: all). events = the canonical bugfix/feature/decision/lesson history (auto-captured event-typed memories)'),
98
+ type: z
99
+ .enum(['observations', 'sessions', 'prompts', 'events'])
100
+ .optional()
101
+ .describe(
102
+ 'Limit to one source table (default: all). events = the canonical bugfix/feature/decision/lesson history (auto-captured event-typed memories)',
103
+ ),
88
104
  obs_type: OBS_TYPE_ENUM.optional().describe('Filter observation type'),
89
105
  project: z.string().optional().describe('Filter by project name'),
90
106
  date_from: z.string().optional().describe('Start date (ISO 8601 or YYYY-MM-DD)'),
91
- date_to: z.string().optional().describe('End date (ISO 8601 or YYYY-MM-DD). Date-only format is inclusive (covers full day)'),
92
- date_since: z.string().optional().describe('Relative lower bound from now: 7d/24h/90m/2w/30s. Use for "recent" queries instead of computing a date_from; ignored when date_from is set'),
93
- importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Minimum importance (1=routine, 2=notable, 3=critical)'),
107
+ date_to: z
108
+ .string()
109
+ .optional()
110
+ .describe('End date (ISO 8601 or YYYY-MM-DD). Date-only format is inclusive (covers full day)'),
111
+ date_since: z
112
+ .string()
113
+ .optional()
114
+ .describe(
115
+ 'Relative lower bound from now: 7d/24h/90m/2w/30s. Use for "recent" queries instead of computing a date_from; ignored when date_from is set',
116
+ ),
117
+ importance: coerceInt
118
+ .pipe(z.number().int().min(1).max(3))
119
+ .optional()
120
+ .describe('Minimum importance (1=routine, 2=notable, 3=critical)'),
94
121
  branch: z.string().optional().describe('Filter by git branch name'),
95
- tier: z.enum(['working', 'active', 'archive']).optional().describe('Filter by memory tier (working=current session, active=within decay window, archive=old/compressed)'),
122
+ tier: z
123
+ .enum(['working', 'active', 'archive'])
124
+ .optional()
125
+ .describe(
126
+ 'Filter by memory tier (working=current session, active=within decay window, archive=old/compressed)',
127
+ ),
96
128
  limit: coerceInt.pipe(z.number().int().min(1).max(100)).optional().describe('Max results (default 20)'),
97
129
  offset: coerceInt.pipe(z.number().int().min(0)).optional().describe('Offset for pagination'),
98
- sort: z.enum(['relevance', 'time', 'importance']).optional().describe('Sort order: relevance (default, BM25), time (newest first), importance (highest first)'),
99
- include_noise: coerceBool.optional().describe('Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default as they have ~3% access rate'),
100
- or: coerceBool.optional().describe('Force OR semantics between query terms from the start (default: AND with automatic OR-fallback when AND returns 0). Aligns with CLI --or.'),
101
- deep: coerceBool.optional().describe('Tri-state LLM multi-query/HyDE deep search (observations-only). true=force; false=never; omit=AUTO (default ON for mem_search): a normal search that returns weak/few results auto-escalates with ONE Haiku call (query rewritten to keyword/concept/HyDE variants, RRF-fused). Set CLAUDE_MEM_AUTO_DEEP=0 to disable AUTO. Passive recall stays single-query.'),
102
- rerank: coerceBool.optional().describe('Opt-in: LLM-rerank the deep-search candidates for ranking precision (one extra Haiku call, ~1.4s). Requires deep=true (no effect on AUTO/normal). Reserve for hard, ranking-sensitive queries where the right memory is likely retrieved but mis-ranked — skip for routine search. Default off.'),
130
+ sort: z
131
+ .enum(['relevance', 'time', 'importance'])
132
+ .optional()
133
+ .describe('Sort order: relevance (default, BM25), time (newest first), importance (highest first)'),
134
+ include_noise: coerceBool
135
+ .optional()
136
+ .describe(
137
+ 'Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default as they have ~3% access rate',
138
+ ),
139
+ or: coerceBool
140
+ .optional()
141
+ .describe(
142
+ 'Force OR semantics between query terms from the start (default: AND with automatic OR-fallback when AND returns 0). Aligns with CLI --or.',
143
+ ),
144
+ deep: coerceBool
145
+ .optional()
146
+ .describe(
147
+ 'Tri-state LLM multi-query/HyDE deep search (observations-only). true=force; false=never; omit=AUTO (default ON for mem_search): a normal search that returns weak/few results auto-escalates with ONE Haiku call (query rewritten to keyword/concept/HyDE variants, RRF-fused). Set CLAUDE_MEM_AUTO_DEEP=0 to disable AUTO. Passive recall stays single-query.',
148
+ ),
149
+ rerank: coerceBool
150
+ .optional()
151
+ .describe(
152
+ 'Opt-in: LLM-rerank the deep-search candidates for ranking precision (one extra Haiku call, ~1.4s). Requires deep=true (no effect on AUTO/normal). Reserve for hard, ranking-sensitive queries where the right memory is likely retrieved but mis-ranked — skip for routine search. Default off.',
153
+ ),
103
154
  // ── CLI-flag aliases (v3.61.0) ──────────────────────────────────────────────
104
155
  // A property the schema doesn't declare is STRIPPED by the validator, so a caller
105
156
  // using the CLI vocabulary (`--source` / `--from` / `--to` / `--since`) previously
@@ -107,7 +158,12 @@ export const memSearchSchema = {
107
158
  // result set that reads as filtered. Declaring the aliases makes the filter apply;
108
159
  // the canonical name wins when both are supplied. Mirror of v3.59.0, which taught
109
160
  // the CLI to accept MCP field names.
110
- source: z.enum(['observations', 'sessions', 'prompts', 'events']).optional().describe('Alias for `type` (CLI `search --source`). Note: CLI `--type` is the OBSERVATION type — that is `obs_type` here.'),
161
+ source: z
162
+ .enum(['observations', 'sessions', 'prompts', 'events'])
163
+ .optional()
164
+ .describe(
165
+ 'Alias for `type` (CLI `search --source`). Note: CLI `--type` is the OBSERVATION type — that is `obs_type` here.',
166
+ ),
111
167
  from: z.string().optional().describe('Alias for `date_from` (CLI `search --from`)'),
112
168
  to: z.string().optional().describe('Alias for `date_to` (CLI `search --to`)'),
113
169
  since: z.string().optional().describe('Alias for `date_since` (CLI `search --since`)'),
@@ -116,8 +172,15 @@ export const memSearchSchema = {
116
172
  export const memRecentSchema = {
117
173
  limit: coerceInt.pipe(z.number().int().min(1).max(100)).optional().describe('Max results (default 10)'),
118
174
  project: z.string().optional().describe('Filter by project (default: inferred from CWD)'),
119
- obs_type: OBS_TYPE_ENUM.optional().describe('Filter observation type (e.g. bugfix, decision) — CLI `recent --type` parity'),
120
- date_since: z.string().optional().describe('Relative lower bound from now: 7d/24h/90m/2w/30s. Only items newer than the window (pair with a high limit for "everything since X")'),
175
+ obs_type: OBS_TYPE_ENUM.optional().describe(
176
+ 'Filter observation type (e.g. bugfix, decision) — CLI `recent --type` parity',
177
+ ),
178
+ date_since: z
179
+ .string()
180
+ .optional()
181
+ .describe(
182
+ 'Relative lower bound from now: 7d/24h/90m/2w/30s. Only items newer than the window (pair with a high limit for "everything since X")',
183
+ ),
121
184
  // CLI-flag aliases — see the note on memSearchSchema. `recent --type` IS the
122
185
  // observation type here (unlike `search --type`, which the CLI spells `--source`),
123
186
  // so `type` maps to obs_type on this tool.
@@ -138,17 +201,27 @@ const coerceAnchor = z.preprocess(
138
201
  }
139
202
  return v;
140
203
  },
141
- z.union([
142
- z.number().int(),
143
- z.string().regex(/^[EePpSs]?#?\d+$/, 'Expected N, #N, P#N, S#N, or E#N'),
144
- ])
204
+ z.union([z.number().int(), z.string().regex(/^[EePpSs]?#?\d+$/, 'Expected N, #N, P#N, S#N, or E#N')]),
145
205
  );
146
206
 
147
207
  export const memTimelineSchema = {
148
- anchor: coerceAnchor.optional().describe('Anchor as observation ID (int) or prefixed token string: "#123", "P#123" (prompt → nearest obs), "S#123" (session → nearest obs), "E#123" (event → nearest obs). Takes precedence over query.'),
149
- query: z.string().optional().describe('FTS5 query to auto-find anchor. Ignored when anchor is also given; use one or the other.'),
150
- before: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items before anchor (default 5)'),
151
- after: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items after anchor (default 5)'),
208
+ anchor: coerceAnchor
209
+ .optional()
210
+ .describe(
211
+ 'Anchor as observation ID (int) or prefixed token string: "#123", "P#123" (prompt → nearest obs), "S#123" (session → nearest obs), "E#123" (event → nearest obs). Takes precedence over query.',
212
+ ),
213
+ query: z
214
+ .string()
215
+ .optional()
216
+ .describe('FTS5 query to auto-find anchor. Ignored when anchor is also given; use one or the other.'),
217
+ before: coerceInt
218
+ .pipe(z.number().int().min(0).max(50))
219
+ .optional()
220
+ .describe('Items before anchor (default 5)'),
221
+ after: coerceInt
222
+ .pipe(z.number().int().min(0).max(50))
223
+ .optional()
224
+ .describe('Items after anchor (default 5)'),
152
225
  project: z.string().optional().describe('Filter by project'),
153
226
  };
154
227
 
@@ -156,9 +229,20 @@ export const memGetSchema = {
156
229
  // Accepts mixed tokens so pasted search results work verbatim: [1], [1, "P#2"], "1,P#2,S#3",
157
230
  // or the JSON-stringified form ["1","P#2"]. Each token's prefix routes to its source bucket
158
231
  // in server.mjs via lib/id-routing.bucketIdTokens. An explicit `source` override still wins.
159
- ids: coerceMixedIdTokens.describe('Mixed observation/prompt/session/event/deferred IDs — accepts N, #N, P#N, S#N, E#N, D#N; comma-strings and JSON arrays also coerced. D#N reads a deferred_work item with FULL detail (defer list is title-only)'),
160
- source: z.enum(['obs', 'session', 'prompt', 'event']).optional().describe('Force all IDs to this source (overrides per-token prefixes). Omit to let P#/S#/E#/# prefixes route individually. D#N tokens are exempt — they always read deferred_work.'),
161
- fields: coerceStringArray.optional().describe('Specific fields to return (default: all; validated against obs schema — session/prompt sources ignore this filter)'),
232
+ ids: coerceMixedIdTokens.describe(
233
+ 'Mixed observation/prompt/session/event/deferred IDs — accepts N, #N, P#N, S#N, E#N, D#N; comma-strings and JSON arrays also coerced. D#N reads a deferred_work item with FULL detail (defer list is title-only)',
234
+ ),
235
+ source: z
236
+ .enum(['obs', 'session', 'prompt', 'event'])
237
+ .optional()
238
+ .describe(
239
+ 'Force all IDs to this source (overrides per-token prefixes). Omit to let P#/S#/E#/# prefixes route individually. D#N tokens are exempt — they always read deferred_work.',
240
+ ),
241
+ fields: coerceStringArray
242
+ .optional()
243
+ .describe(
244
+ 'Specific fields to return (default: all; validated against obs schema — session/prompt sources ignore this filter)',
245
+ ),
162
246
  };
163
247
 
164
248
  export const memDeleteSchema = {
@@ -173,7 +257,7 @@ export const memDeleteSchema = {
173
257
  const coerceDeferredTokens = z.preprocess(
174
258
  (v) => {
175
259
  if (Array.isArray(v)) {
176
- return v.map(x => {
260
+ return v.map((x) => {
177
261
  if (typeof x === 'number') return x;
178
262
  if (typeof x === 'string') {
179
263
  const s = x.trim();
@@ -185,10 +269,15 @@ const coerceDeferredTokens = z.preprocess(
185
269
  }
186
270
  return v;
187
271
  },
188
- z.array(z.union([
189
- z.number().int().positive(),
190
- z.string().regex(/^D#\d+$/, 'expected D#N (raw id) or positive integer (ordinal)'),
191
- ])).min(1).max(20)
272
+ z
273
+ .array(
274
+ z.union([
275
+ z.number().int().positive(),
276
+ z.string().regex(/^D#\d+$/, 'expected D#N (raw id) or positive integer (ordinal)'),
277
+ ]),
278
+ )
279
+ .min(1)
280
+ .max(20),
192
281
  );
193
282
 
194
283
  // Coerce supersedes input — positive observation ids, plus `E#<n>` for an EVENT row
@@ -198,13 +287,19 @@ const coerceDeferredTokens = z.preprocess(
198
287
  // a number here would lose the only thing that says which table it names.
199
288
  // Empty/other shapes reject.
200
289
  const coerceSupersedes = z.preprocess(
201
- (v) => (Array.isArray(v)
202
- ? v.map(x => (typeof x === 'string' && /^\d+$/.test(x.trim()) ? parseInt(x.trim(), 10) : x))
203
- : v),
204
- z.array(z.union([
205
- z.number().int().positive(),
206
- z.string().regex(/^[Ee]#?\d+$/, 'expected a positive observation id or E#<n> for an event'),
207
- ])).min(1).max(20)
290
+ (v) =>
291
+ Array.isArray(v)
292
+ ? v.map((x) => (typeof x === 'string' && /^\d+$/.test(x.trim()) ? parseInt(x.trim(), 10) : x))
293
+ : v,
294
+ z
295
+ .array(
296
+ z.union([
297
+ z.number().int().positive(),
298
+ z.string().regex(/^[Ee]#?\d+$/, 'expected a positive observation id or E#<n> for an event'),
299
+ ]),
300
+ )
301
+ .min(1)
302
+ .max(20),
208
303
  );
209
304
 
210
305
  export const memSaveSchema = {
@@ -219,50 +314,122 @@ export const memSaveSchema = {
219
314
  // every `--type bugfix` filter, so it must not be a silent coercion.
220
315
  obs_type: OBS_TYPE_ENUM.optional().describe('Alias for `type` (parity with mem_search/mem_recent)'),
221
316
  project: z.string().optional().describe('Project name (default: inferred from CWD)'),
222
- importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
223
- files: coerceStringArray.optional().describe('File paths associated with this observation. Stored in the `files_modified` column and rendered as `files` — passing a path here does not assert the file was edited; a file you only read belongs here too'),
224
- lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway, ≤500 chars (for bugfix: root cause & fix; for decision: rationale)'),
225
- closes_deferred: coerceDeferredTokens.optional().describe('Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert — a single invalid id rolls back the whole save.'),
226
- supersedes: coerceSupersedes.optional().describe('Ids (same project) that this save overturns: a bare number for an observation, or E#<n> for an event — the same prefix events are shown with in the injected lessons block, so you can retire one by typing back what you read. They are marked superseded (dropped from live search); observations are also linked to the new row via superseded_by, events are not, because that column can only reference another event. Use ONLY when this genuinely replaces a prior conclusion; do NOT use for merely-related or updated-but-still-valid memories.'),
317
+ importance: coerceInt
318
+ .pipe(z.number().int().min(1).max(3))
319
+ .optional()
320
+ .describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
321
+ files: coerceStringArray
322
+ .optional()
323
+ .describe(
324
+ 'File paths associated with this observation. Stored in the `files_modified` column and rendered as `files` — passing a path here does not assert the file was edited; a file you only read belongs here too',
325
+ ),
326
+ lesson_learned: z
327
+ .string()
328
+ .max(500)
329
+ .optional()
330
+ .describe('Key lesson or takeaway, ≤500 chars (for bugfix: root cause & fix; for decision: rationale)'),
331
+ closes_deferred: coerceDeferredTokens
332
+ .optional()
333
+ .describe(
334
+ 'Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert — a single invalid id rolls back the whole save.',
335
+ ),
336
+ supersedes: coerceSupersedes
337
+ .optional()
338
+ .describe(
339
+ 'Ids (same project) that this save overturns: a bare number for an observation, or E#<n> for an event — the same prefix events are shown with in the injected lessons block, so you can retire one by typing back what you read. They are marked superseded (dropped from live search); observations are also linked to the new row via superseded_by, events are not, because that column can only reference another event. Use ONLY when this genuinely replaces a prior conclusion; do NOT use for merely-related or updated-but-still-valid memories.',
340
+ ),
227
341
  };
228
342
 
229
343
  export const memStatsSchema = {
230
344
  project: z.string().optional().describe('Filter by project'),
231
345
  days: coerceInt.pipe(z.number().int().min(1).max(365)).optional().describe('Look back N days (default 30)'),
232
- quality: coerceBool.optional().describe('Return quality dashboard (lesson rate, LOW_SIGNAL rate, per-type hit/lesson %, top-accessed lessons, R-2 watchdog) instead of default stats. Aligns with CLI --quality.'),
346
+ quality: coerceBool
347
+ .optional()
348
+ .describe(
349
+ 'Return quality dashboard (lesson rate, LOW_SIGNAL rate, per-type hit/lesson %, top-accessed lessons, R-2 watchdog) instead of default stats. Aligns with CLI --quality.',
350
+ ),
233
351
  };
234
352
 
235
353
  export const memCompressSchema = {
236
354
  preview: coerceBool.optional().describe('true=count candidates, false=execute compression (default: true)'),
237
- age_days: coerceInt.pipe(z.number().int().min(30).max(365)).optional().describe('Min age in days (default: 30, minimum: 30)'),
355
+ age_days: coerceInt
356
+ .pipe(z.number().int().min(30).max(365))
357
+ .optional()
358
+ .describe('Min age in days (default: 30, minimum: 30)'),
238
359
  project: z.string().optional().describe('Filter by project'),
239
360
  };
240
361
 
241
362
  export const memOptimizeSchema = {
242
- action: z.enum(['preview', 'run', 'run_all']).optional().default('preview')
363
+ action: z
364
+ .enum(['preview', 'run', 'run_all'])
365
+ .optional()
366
+ .default('preview')
243
367
  .describe('preview=scan candidates, run=execute with limits, run_all=bypass gates'),
244
- tasks: z.array(z.enum(['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'])).optional()
368
+ tasks: z
369
+ .array(z.enum(['re-enrich', 'normalize', 'cluster-merge', 'smart-compress']))
370
+ .optional()
245
371
  .describe('Which optimization tasks to run (default: all)'),
246
- max_items: coerceInt.pipe(z.number().int().min(1).max(100)).optional().default(15)
372
+ max_items: coerceInt
373
+ .pipe(z.number().int().min(1).max(100))
374
+ .optional()
375
+ .default(15)
247
376
  .describe('Maximum LLM calls across all tasks (default: 15)'),
248
- scope: z.enum(['narrow', 'wide']).optional().default('narrow')
249
- .describe("Re-enrich scope: narrow=narrative-only candidates (default); wide=R-7 backfill (bugfix/refactor/feature/decision with narrative but lesson_learned='none'). CLI parity: --scope wide."),
250
- project: z.string().optional().describe('Filter all 4 tasks to a single project. Default: scan all projects.'),
251
- detail: coerceBool.optional().describe('preview action only — include cluster contents + re-enrich/compress sample arrays alongside aggregate counts.'),
377
+ scope: z
378
+ .enum(['narrow', 'wide'])
379
+ .optional()
380
+ .default('narrow')
381
+ .describe(
382
+ "Re-enrich scope: narrow=narrative-only candidates (default); wide=R-7 backfill (bugfix/refactor/feature/decision with narrative but lesson_learned='none'). CLI parity: --scope wide.",
383
+ ),
384
+ project: z
385
+ .string()
386
+ .optional()
387
+ .describe('Filter all 4 tasks to a single project. Default: scan all projects.'),
388
+ detail: coerceBool
389
+ .optional()
390
+ .describe(
391
+ 'preview action only — include cluster contents + re-enrich/compress sample arrays alongside aggregate counts.',
392
+ ),
252
393
  };
253
394
 
254
395
  export const memMaintainSchema = {
255
396
  action: z.enum(['scan', 'execute']).describe('scan=analyze candidates, execute=apply changes'),
256
- operations: z.array(z.enum(['dedup', 'decay', 'cleanup', 'boost', 'demote_pinned', 'purge_stale', 'rebuild_vectors', 'vacuum'])).optional()
257
- .describe('Operations: dedup=find/merge duplicate observations, decay=reduce importance of old low-value obs, cleanup=remove orphaned records, boost=promote frequently-accessed obs, demote_pinned=floor importance for obs injected>=8 times but never cited — to 1 with no lesson_learned, to 2 with one (v3.76.1: a lesson-bearing row keeps eligibility on every importance>=2 injection face) (clears pinned noise the decay op cannot reach; in the default set since v3.76.0 and ordered after boost, since boost would otherwise raise the row straight back — set CLAUDE_MEM_SKIP_DEMOTE_PINNED=1 to drop it from the DEFAULT set only), purge_stale=DELETE pending-purge obs older than retain_days (requires confirm=true; first call previews), rebuild_vectors=rebuild TF-IDF vocabulary and all observation vectors, vacuum=reclaim freelist dead space (whole-DB)'),
258
- merge_ids: z.preprocess(
259
- (v) => Array.isArray(v) ? v.map(g => Array.isArray(g) ? g.map(x => typeof x === 'string' ? parseInt(x, 10) : x) : g) : v,
260
- z.array(z.array(z.number().int()).min(2))
261
- ).optional().describe('For dedup: [[keepId, removeId1, removeId2], ...] — first ID in each group is kept'),
262
- retain_days: coerceInt.pipe(z.number().int().min(7).max(365)).optional()
397
+ operations: z
398
+ .array(
399
+ z.enum([
400
+ 'dedup',
401
+ 'decay',
402
+ 'cleanup',
403
+ 'boost',
404
+ 'demote_pinned',
405
+ 'purge_stale',
406
+ 'rebuild_vectors',
407
+ 'vacuum',
408
+ ]),
409
+ )
410
+ .optional()
411
+ .describe(
412
+ 'Operations: dedup=find/merge duplicate observations, decay=reduce importance of old low-value obs, cleanup=remove orphaned records, boost=promote frequently-accessed obs, demote_pinned=floor importance for obs injected>=8 times but never cited — to 1 with no lesson_learned, to 2 with one (v3.76.1: a lesson-bearing row keeps eligibility on every importance>=2 injection face) (clears pinned noise the decay op cannot reach; in the default set since v3.76.0 and ordered after boost, since boost would otherwise raise the row straight back — set CLAUDE_MEM_SKIP_DEMOTE_PINNED=1 to drop it from the DEFAULT set only), purge_stale=DELETE pending-purge obs older than retain_days (requires confirm=true; first call previews), rebuild_vectors=rebuild TF-IDF vocabulary and all observation vectors, vacuum=reclaim freelist dead space (whole-DB)',
413
+ ),
414
+ merge_ids: z
415
+ .preprocess(
416
+ (v) =>
417
+ Array.isArray(v)
418
+ ? v.map((g) => (Array.isArray(g) ? g.map((x) => (typeof x === 'string' ? parseInt(x, 10) : x)) : g))
419
+ : v,
420
+ z.array(z.array(z.number().int()).min(2)),
421
+ )
422
+ .optional()
423
+ .describe('For dedup: [[keepId, removeId1, removeId2], ...] — first ID in each group is kept'),
424
+ retain_days: coerceInt
425
+ .pipe(z.number().int().min(7).max(365))
426
+ .optional()
263
427
  .describe('For purge_stale: keep observations newer than N days (default 30)'),
264
- confirm: coerceBool.optional()
265
- .describe('Required for destructive ops in `execute` mode (currently: purge_stale). Omit/false → dry-run preview; true → actually delete.'),
428
+ confirm: coerceBool
429
+ .optional()
430
+ .describe(
431
+ 'Required for destructive ops in `execute` mode (currently: purge_stale). Omit/false → dry-run preview; true → actually delete.',
432
+ ),
266
433
  project: z.string().optional().describe('Filter by project'),
267
434
  };
268
435
 
@@ -270,10 +437,18 @@ export const memUpdateSchema = {
270
437
  id: coerceInt.pipe(z.number().int().positive()).describe('Observation ID to update'),
271
438
  // CLI parity (cmdUpdate): empty/whitespace title would render as `(untitled)`
272
439
  // in every listing — reject here like the CLI does, instead of persisting it.
273
- title: z.string().refine(s => s.trim() !== '', 'title cannot be empty').optional().describe('New title'),
440
+ title: z
441
+ .string()
442
+ .refine((s) => s.trim() !== '', 'title cannot be empty')
443
+ .optional()
444
+ .describe('New title'),
274
445
  // Reject empty content fields (parity with `title` above + cmdUpdate): an explicit
275
446
  // '' would blank narrative/lesson/concepts irrecoverably (mem_update takes no snapshot).
276
- narrative: z.string().refine(s => s.trim() !== '', 'narrative cannot be empty').optional().describe('New narrative/content'),
447
+ narrative: z
448
+ .string()
449
+ .refine((s) => s.trim() !== '', 'narrative cannot be empty')
450
+ .optional()
451
+ .describe('New narrative/content'),
277
452
  type: OBS_TYPE_ENUM.optional().describe('New observation type'),
278
453
  // Same alias as memSaveSchema, for the same reason and found by the same review: without
279
454
  // it `mem_update({id, importance: 3, obs_type: 'bugfix'})` reported
@@ -283,8 +458,17 @@ export const memUpdateSchema = {
283
458
  importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('New importance (1-3)'),
284
459
  // 500-char cap mirrors memSaveSchema + cmdUpdate — update was the one path
285
460
  // that let overlong lessons leak into the DB via MCP.
286
- lesson_learned: z.string().max(500).refine(s => s.trim() !== '', 'lesson_learned cannot be empty').optional().describe('Add or update lesson learned'),
287
- concepts: z.string().refine(s => s.trim() !== '', 'concepts cannot be empty').optional().describe('Space-separated concept tags'),
461
+ lesson_learned: z
462
+ .string()
463
+ .max(500)
464
+ .refine((s) => s.trim() !== '', 'lesson_learned cannot be empty')
465
+ .optional()
466
+ .describe('Add or update lesson learned'),
467
+ concepts: z
468
+ .string()
469
+ .refine((s) => s.trim() !== '', 'concepts cannot be empty')
470
+ .optional()
471
+ .describe('Space-separated concept tags'),
288
472
  };
289
473
 
290
474
  export const memExportSchema = {
@@ -305,13 +489,22 @@ export const memExportSchema = {
305
489
  // The default stays 200 because an MCP result is model context — a bare exploratory call
306
490
  // must not dump a whole store into the transcript — but a capped result now announces
307
491
  // itself as PARTIAL and names the limit that would return all of it.
308
- limit: coerceInt.pipe(z.number().int().min(1)).optional().describe('Max observations to export (default: 200 — a capped result is flagged PARTIAL and names the total; pass that total for a complete backup, no upper bound)'),
492
+ limit: coerceInt
493
+ .pipe(z.number().int().min(1))
494
+ .optional()
495
+ .describe(
496
+ 'Max observations to export (default: 200 — a capped result is flagged PARTIAL and names the total; pass that total for a complete backup, no upper bound)',
497
+ ),
309
498
  };
310
499
 
311
500
  export const memRecallSchema = {
312
501
  file: z.string().min(1).describe('File path or filename to recall observations for'),
313
502
  limit: coerceInt.pipe(z.number().int().min(1).max(50)).optional().describe('Max results (default 10)'),
314
- include_noise: coerceBool.optional().describe('Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default for parity with mem_search'),
503
+ include_noise: coerceBool
504
+ .optional()
505
+ .describe(
506
+ 'Include hook-llm fallback titles ("Modified X", "Worked on X", raw error logs) — hidden by default for parity with mem_search',
507
+ ),
315
508
  };
316
509
 
317
510
  export const memFtsCheckSchema = {
@@ -319,12 +512,17 @@ export const memFtsCheckSchema = {
319
512
  };
320
513
 
321
514
  export const memRegistrySchema = {
322
- action: z.enum(['list', 'stats', 'search', 'import', 'remove', 'reindex', 'import_url', 'enrich']).describe('Registry operation'),
515
+ action: z
516
+ .enum(['list', 'stats', 'search', 'import', 'remove', 'reindex', 'import_url', 'enrich'])
517
+ .describe('Registry operation'),
323
518
  query: z.string().optional().describe('Search query — keywords describing what you need (for search)'),
324
519
  type: z.enum(['skill', 'agent']).optional().describe('Filter by resource type (for list/search)'),
325
520
  name: z.string().optional().describe('Resource name (for import/remove)'),
326
521
  resource_type: z.enum(['skill', 'agent']).optional().describe('Resource type (for import/remove)'),
327
- source: z.enum(['preinstalled', 'user', 'github']).optional().describe('Source (for import, default: user)'),
522
+ source: z
523
+ .enum(['preinstalled', 'user', 'github'])
524
+ .optional()
525
+ .describe('Source (for import, default: user)'),
328
526
  repo_url: z.string().optional().describe('GitHub repository URL (for import)'),
329
527
  local_path: z.string().optional().describe('Local file path (for import)'),
330
528
  invocation_name: z.string().optional().describe('Invocation name like "plugin:skill" (for import)'),
@@ -337,8 +535,14 @@ export const memRegistrySchema = {
337
535
  use_cases: z.string().optional().describe('Usage scenarios (for import)'),
338
536
  url: z.string().optional().describe('GitHub repository URL (for import_url action)'),
339
537
  enrich: coerceBool.optional().describe('Auto-enrich imported resources (for import_url action)'),
340
- category: z.string().optional().describe("Filter by category (e.g., 'testing', 'code-quality', 'debugging')"),
341
- quality: z.enum(['installed', 'verified', 'community']).optional().describe('Filter by quality tier (default: all)'),
538
+ category: z
539
+ .string()
540
+ .optional()
541
+ .describe("Filter by category (e.g., 'testing', 'code-quality', 'debugging')"),
542
+ quality: z
543
+ .enum(['installed', 'verified', 'community'])
544
+ .optional()
545
+ .describe('Filter by quality tier (default: all)'),
342
546
  };
343
547
 
344
548
  export const memUseSchema = {
@@ -349,12 +553,18 @@ export const memUseSchema = {
349
553
  export const memBrowseSchema = {
350
554
  project: z.string().optional().describe('Filter by project (default: inferred from CWD)'),
351
555
  tier: z.enum(['working', 'active', 'archive']).optional().describe('Show only this tier'),
352
- limit: coerceInt.pipe(z.number().int().min(1).max(100)).optional().describe('Max entries per tier (default 5, or 20 when filtering by tier)'),
556
+ limit: coerceInt
557
+ .pipe(z.number().int().min(1).max(100))
558
+ .optional()
559
+ .describe('Max entries per tier (default 5, or 20 when filtering by tier)'),
353
560
  };
354
561
 
355
562
  export const memDeferSchema = {
356
563
  title: z.string().min(1).max(200).describe('One-line subject of the deferred item'),
357
- priority: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('1=low, 2=normal, 3=urgent (default: 2)'),
564
+ priority: coerceInt
565
+ .pipe(z.number().int().min(1).max(3))
566
+ .optional()
567
+ .describe('1=low, 2=normal, 3=urgent (default: 2)'),
358
568
  detail: z.string().max(2000).optional().describe('Optional longer description / constraint / why deferred'),
359
569
  files: coerceStringArray.optional().describe('Optional file paths this deferred item concerns'),
360
570
  project: z.string().optional().describe('Project name (default: inferred from CWD)'),
@@ -366,10 +576,12 @@ export const memDeferListSchema = {
366
576
  };
367
577
 
368
578
  export const memDeferDropSchema = {
369
- id: z.union([
370
- coerceInt.pipe(z.number().int().positive()),
371
- z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
372
- ]).describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
579
+ id: z
580
+ .union([
581
+ coerceInt.pipe(z.number().int().positive()),
582
+ z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
583
+ ])
584
+ .describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
373
585
  reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
374
586
  project: z.string().optional().describe('Project name (default: inferred from CWD)'),
375
587
  };
@@ -418,7 +630,9 @@ export const tools = [
418
630
  ' - Looking for prior art on a module/feature before refactoring\n' +
419
631
  ' - A normal search missed — weak results auto-escalate to deep (set deep=false to opt out)\n' +
420
632
  '\n' +
421
- 'Equivalent CLI: ' + CLI_INVOKE + ' search "<query>" [--type bugfix] [--deep]',
633
+ 'Equivalent CLI: ' +
634
+ CLI_INVOKE +
635
+ ' search "<query>" [--type bugfix] [--deep]',
422
636
  inputSchema: memSearchSchema,
423
637
  },
424
638
  {
@@ -436,7 +650,9 @@ export const tools = [
436
650
  ' - User asks "what did we do yesterday / last" with no topic keyword\n' +
437
651
  ' - Verifying that a just-made change was captured as an observation\n' +
438
652
  '\n' +
439
- 'Equivalent CLI: ' + CLI_INVOKE + ' recent [N]',
653
+ 'Equivalent CLI: ' +
654
+ CLI_INVOKE +
655
+ ' recent [N]',
440
656
  inputSchema: memRecentSchema,
441
657
  },
442
658
  {
@@ -456,7 +672,9 @@ export const tools = [
456
672
  ' - A search hit is interesting and you want its chronological neighbours\n' +
457
673
  ' - Replaying a session narrative around a known observation ID\n' +
458
674
  '\n' +
459
- 'Equivalent CLI: ' + CLI_INVOKE + ' timeline --anchor <ID> [--before N --after N]',
675
+ 'Equivalent CLI: ' +
676
+ CLI_INVOKE +
677
+ ' timeline --anchor <ID> [--before N --after N]',
460
678
  inputSchema: memTimelineSchema,
461
679
  },
462
680
  {
@@ -476,7 +694,9 @@ export const tools = [
476
694
  '\n' +
477
695
  'On miss, response includes "Try: …" hint listing other sources the ID lives in.\n' +
478
696
  '\n' +
479
- 'Equivalent CLI: ' + CLI_INVOKE + ' get <id>[,<id>,...] — accepts P#/S#/# prefix.',
697
+ 'Equivalent CLI: ' +
698
+ CLI_INVOKE +
699
+ ' get <id>[,<id>,...] — accepts P#/S#/# prefix.',
480
700
  inputSchema: memGetSchema,
481
701
  },
482
702
  {
@@ -494,7 +714,9 @@ export const tools = [
494
714
  ' - Cleaning up an observation saved from a test run or incorrect save\n' +
495
715
  ' - Always run once with confirm=false, then again with confirm=true\n' +
496
716
  '\n' +
497
- 'Equivalent CLI: ' + CLI_INVOKE + ' delete <id>[,<id>,...] [--confirm]',
717
+ 'Equivalent CLI: ' +
718
+ CLI_INVOKE +
719
+ ' delete <id>[,<id>,...] [--confirm]',
498
720
  inputSchema: memDeleteSchema,
499
721
  hidden: true,
500
722
  },
@@ -513,7 +735,9 @@ export const tools = [
513
735
  ' - After a non-obvious architecture/tradeoff decision — set type="decision", lesson_learned="<constraint + why>"\n' +
514
736
  ' - User explicitly asks "remember this" or "save a note that ..."\n' +
515
737
  '\n' +
516
- 'Equivalent CLI: ' + CLI_INVOKE + ' save --type bugfix --lesson "..." "<content>"',
738
+ 'Equivalent CLI: ' +
739
+ CLI_INVOKE +
740
+ ' save --type bugfix --lesson "..." "<content>"',
517
741
  inputSchema: memSaveSchema,
518
742
  },
519
743
  {
@@ -531,7 +755,9 @@ export const tools = [
531
755
  ' - Diagnosing why search feels sparse or noisy at a macro level\n' +
532
756
  ' - Auditing a project before major compression/maintenance\n' +
533
757
  '\n' +
534
- 'Equivalent CLI: ' + CLI_INVOKE + ' stats [--project X] [--days 30]',
758
+ 'Equivalent CLI: ' +
759
+ CLI_INVOKE +
760
+ ' stats [--project X] [--days 30]',
535
761
  inputSchema: memStatsSchema,
536
762
  hidden: true,
537
763
  },
@@ -550,7 +776,9 @@ export const tools = [
550
776
  ' - After a major project phase completes and old per-file observations are noise\n' +
551
777
  ' - Stats show thousands of low-importance rows dragging search quality\n' +
552
778
  '\n' +
553
- 'Equivalent CLI: ' + CLI_INVOKE + ' compress [--execute] [--age-days 90] (preview is default)',
779
+ 'Equivalent CLI: ' +
780
+ CLI_INVOKE +
781
+ ' compress [--execute] [--age-days 90] (preview is default)',
554
782
  inputSchema: memCompressSchema,
555
783
  hidden: true,
556
784
  },
@@ -569,7 +797,9 @@ export const tools = [
569
797
  ' - After bulk imports or a long offline period\n' +
570
798
  ' - User asks for periodic maintenance / cleanup\n' +
571
799
  '\n' +
572
- 'Equivalent CLI: ' + CLI_INVOKE + ' maintain scan --ops dedup,decay',
800
+ 'Equivalent CLI: ' +
801
+ CLI_INVOKE +
802
+ ' maintain scan --ops dedup,decay',
573
803
  inputSchema: memMaintainSchema,
574
804
  hidden: true,
575
805
  },
@@ -588,7 +818,9 @@ export const tools = [
588
818
  ' - stats show many degraded (title-only, no lesson) observations\n' +
589
819
  ' - Start with action="preview" to see candidates before spending tokens\n' +
590
820
  '\n' +
591
- 'Equivalent CLI: ' + CLI_INVOKE + ' optimize [--run|--run-all] [--task re-enrich,normalize,cluster-merge,smart-compress] [--max N] (preview is default)',
821
+ 'Equivalent CLI: ' +
822
+ CLI_INVOKE +
823
+ ' optimize [--run|--run-all] [--task re-enrich,normalize,cluster-merge,smart-compress] [--max N] (preview is default)',
592
824
  inputSchema: memOptimizeSchema,
593
825
  hidden: true,
594
826
  },
@@ -607,7 +839,9 @@ export const tools = [
607
839
  ' - Looking for a tool by capability → action="search" with keywords\n' +
608
840
  ' - User explicitly asks to import a GitHub repo → action="import_url"\n' +
609
841
  '\n' +
610
- 'Equivalent CLI: ' + CLI_INVOKE + ' registry <list|search|import|...> [args]',
842
+ 'Equivalent CLI: ' +
843
+ CLI_INVOKE +
844
+ ' registry <list|search|import|...> [args]',
611
845
  inputSchema: memRegistrySchema,
612
846
  hidden: true,
613
847
  },
@@ -615,7 +849,7 @@ export const tools = [
615
849
  name: 'mem_use',
616
850
  description:
617
851
  'Load and activate a skill or agent from the registry by EXACT name or invocation_name.\n' +
618
- 'A name that matches nothing returns closest-match names to pick from — never another resource\'s content.\n' +
852
+ "A name that matches nothing returns closest-match names to pick from — never another resource's content.\n" +
619
853
  '\n' +
620
854
  'DO NOT use when:\n' +
621
855
  ' - You have not confirmed the skill exists (run mem_registry action="list" first)\n' +
@@ -646,7 +880,9 @@ export const tools = [
646
880
  ' - You later discover additional context worth appending to lesson_learned\n' +
647
881
  ' - Reclassifying an observation after its true type becomes clear\n' +
648
882
  '\n' +
649
- 'Equivalent CLI: ' + CLI_INVOKE + ' update <id> [--title ...] [--lesson ...]',
883
+ 'Equivalent CLI: ' +
884
+ CLI_INVOKE +
885
+ ' update <id> [--title ...] [--lesson ...]',
650
886
  inputSchema: memUpdateSchema,
651
887
  hidden: true,
652
888
  },
@@ -663,9 +899,11 @@ export const tools = [
663
899
  'USE when:\n' +
664
900
  ' - Backing up memory before a migration or reinstall\n' +
665
901
  ' - Moving observations between machines or projects\n' +
666
- ' - User asks for a JSON snapshot of a project\'s memories\n' +
902
+ " - User asks for a JSON snapshot of a project's memories\n" +
667
903
  '\n' +
668
- 'Equivalent CLI: ' + CLI_INVOKE + ' export [--format jsonl] [--project X] [--limit 500]',
904
+ 'Equivalent CLI: ' +
905
+ CLI_INVOKE +
906
+ ' export [--format jsonl] [--project X] [--limit 500]',
669
907
  inputSchema: memExportSchema,
670
908
  hidden: true,
671
909
  },
@@ -684,7 +922,9 @@ export const tools = [
684
922
  ' - User asks "what do we know about <file>"\n' +
685
923
  ' - Investigating a recurring issue in a file you have not touched recently\n' +
686
924
  '\n' +
687
- 'Equivalent CLI: ' + CLI_INVOKE + ' recall "<file>" [--limit 10]',
925
+ 'Equivalent CLI: ' +
926
+ CLI_INVOKE +
927
+ ' recall "<file>" [--limit 10]',
688
928
  inputSchema: memRecallSchema,
689
929
  },
690
930
  {
@@ -702,7 +942,9 @@ export const tools = [
702
942
  ' - After a crash, power loss, or manual DB edit\n' +
703
943
  ' - doctor / stats flags FTS integrity problems\n' +
704
944
  '\n' +
705
- 'Equivalent CLI: ' + CLI_INVOKE + ' fts-check <check|rebuild>',
945
+ 'Equivalent CLI: ' +
946
+ CLI_INVOKE +
947
+ ' fts-check <check|rebuild>',
706
948
  inputSchema: memFtsCheckSchema,
707
949
  hidden: true,
708
950
  },
@@ -721,7 +963,9 @@ export const tools = [
721
963
  ' - Triaging what to compress or clean up before running maintenance\n' +
722
964
  ' - Scanning for interesting anchors to follow up with mem_timeline\n' +
723
965
  '\n' +
724
- 'Equivalent CLI: ' + CLI_INVOKE + ' browse [--tier active] [--project X]',
966
+ 'Equivalent CLI: ' +
967
+ CLI_INVOKE +
968
+ ' browse [--tier active] [--project X]',
725
969
  inputSchema: memBrowseSchema,
726
970
  hidden: true,
727
971
  },
@@ -738,9 +982,11 @@ export const tools = [
738
982
  'USE when:\n' +
739
983
  ' - User says "下次/next session/留给下个会话/defer to next round"\n' +
740
984
  ' - Wrap-up phase enumerates follow-up items for the next session\n' +
741
- ' - Bug surfaces but root cause is out of this session\'s scope\n' +
985
+ " - Bug surfaces but root cause is out of this session's scope\n" +
742
986
  '\n' +
743
- 'Equivalent CLI: ' + CLI_INVOKE + ' defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
987
+ 'Equivalent CLI: ' +
988
+ CLI_INVOKE +
989
+ ' defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
744
990
  inputSchema: memDeferSchema,
745
991
  },
746
992
  {
@@ -757,7 +1003,9 @@ export const tools = [
757
1003
  ' - About to refer to "item N" and need to confirm what N points to\n' +
758
1004
  ' - Auditing carry-forward state across multiple sessions\n' +
759
1005
  '\n' +
760
- 'Equivalent CLI: ' + CLI_INVOKE + ' defer list [--project X] [--limit 10]',
1006
+ 'Equivalent CLI: ' +
1007
+ CLI_INVOKE +
1008
+ ' defer list [--project X] [--limit 10]',
761
1009
  inputSchema: memDeferListSchema,
762
1010
  },
763
1011
  {
@@ -775,7 +1023,9 @@ export const tools = [
775
1023
  ' - Scope changed and the work is no longer needed\n' +
776
1024
  ' - User explicitly says "drop the deferred X, never mind"\n' +
777
1025
  '\n' +
778
- 'Equivalent CLI: ' + CLI_INVOKE + ' defer drop <D#N|ordinal> --reason "..."',
1026
+ 'Equivalent CLI: ' +
1027
+ CLI_INVOKE +
1028
+ ' defer drop <D#N|ordinal> --reason "..."',
779
1029
  inputSchema: memDeferDropSchema,
780
1030
  },
781
1031
  ];