@exaudeus/memory-mcp 1.8.0 → 1.9.1
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/dist/formatters.d.ts +5 -1
- package/dist/formatters.js +9 -0
- package/dist/index.js +523 -125
- package/dist/normalize.js +35 -15
- package/dist/text-analyzer.d.ts +12 -0
- package/dist/text-analyzer.js +28 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/formatters.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MemoryStats, StaleEntry, ConflictPair, BehaviorConfig } from './types.js';
|
|
1
|
+
import type { MemoryStats, StaleEntry, ConflictPair, BehaviorConfig, RelatedEntry } from './types.js';
|
|
2
2
|
import { type FilterGroup } from './text-analyzer.js';
|
|
3
3
|
import type { MarkdownMemoryStore } from './store.js';
|
|
4
4
|
/** Format the search mode indicator for context/recall responses.
|
|
@@ -6,6 +6,10 @@ import type { MarkdownMemoryStore } from './store.js';
|
|
|
6
6
|
*
|
|
7
7
|
* Shows whether semantic search is active and vector coverage. */
|
|
8
8
|
export declare function formatSearchMode(embedderAvailable: boolean, vectorCount: number, totalCount: number): string;
|
|
9
|
+
/** Format the loot-drop section — related entries shown after a storage operation.
|
|
10
|
+
* Transforms storage from "chore for the future" into "immediate value exchange."
|
|
11
|
+
* Pure function. */
|
|
12
|
+
export declare function formatLootDrop(related: readonly RelatedEntry[]): string;
|
|
9
13
|
/** Format the stale entries section for briefing/context responses */
|
|
10
14
|
export declare function formatStaleSection(staleDetails: readonly StaleEntry[]): string;
|
|
11
15
|
/** Format the conflict detection warning for query/context responses */
|
package/dist/formatters.js
CHANGED
|
@@ -20,6 +20,15 @@ export function formatSearchMode(embedderAvailable, vectorCount, totalCount) {
|
|
|
20
20
|
}
|
|
21
21
|
return `*Search: semantic + keyword (${vectorCount}/${totalCount} entries vectorized)*`;
|
|
22
22
|
}
|
|
23
|
+
/** Format the loot-drop section — related entries shown after a storage operation.
|
|
24
|
+
* Transforms storage from "chore for the future" into "immediate value exchange."
|
|
25
|
+
* Pure function. */
|
|
26
|
+
export function formatLootDrop(related) {
|
|
27
|
+
if (related.length === 0)
|
|
28
|
+
return '';
|
|
29
|
+
const lines = related.map(r => `- [${r.id}] "${r.title}" (confidence: ${r.confidence.toFixed(2)})`);
|
|
30
|
+
return `\n**Related knowledge:**\n${lines.join('\n')}`;
|
|
31
|
+
}
|
|
23
32
|
/** Format the stale entries section for briefing/context responses */
|
|
24
33
|
export function formatStaleSection(staleDetails) {
|
|
25
34
|
const lines = [
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Supports multiple workspaces simultaneously
|
|
5
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { readFile, writeFile } from 'fs/promises';
|
|
@@ -14,8 +14,8 @@ import { getLobeConfigs } from './config.js';
|
|
|
14
14
|
import { ConfigManager } from './config-manager.js';
|
|
15
15
|
import { normalizeArgs } from './normalize.js';
|
|
16
16
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
17
|
-
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections, formatSearchMode } from './formatters.js';
|
|
18
|
-
import { parseFilter } from './text-analyzer.js';
|
|
17
|
+
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections, formatSearchMode, formatLootDrop } from './formatters.js';
|
|
18
|
+
import { parseFilter, extractTitle } from './text-analyzer.js';
|
|
19
19
|
import { VOCABULARY_ECHO_LIMIT, WARN_SEPARATOR } from './thresholds.js';
|
|
20
20
|
import { matchRootsToLobeNames, buildLobeResolution } from './lobe-resolution.js';
|
|
21
21
|
let serverMode = { kind: 'running' };
|
|
@@ -143,7 +143,7 @@ function inferLobeFromPaths(paths) {
|
|
|
143
143
|
// Only return if unambiguous — exactly one lobe matched
|
|
144
144
|
return matchedLobes.size === 1 ? matchedLobes.values().next().value : undefined;
|
|
145
145
|
}
|
|
146
|
-
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
146
|
+
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
147
147
|
// --- Lobe resolution for read operations ---
|
|
148
148
|
// When the agent doesn't specify a lobe, we determine which lobe(s) to search
|
|
149
149
|
// via a degradation ladder (see lobe-resolution.ts for the pure logic):
|
|
@@ -208,170 +208,180 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
208
208
|
const currentLobeNames = configManager.getLobeNames();
|
|
209
209
|
const lobeProperty = buildLobeProperty(currentLobeNames);
|
|
210
210
|
return { tools: [
|
|
211
|
-
//
|
|
212
|
-
//
|
|
211
|
+
// ─── New v2 tool surface ─────────────────────────────────────────────
|
|
212
|
+
// 4 retrieval tools + 4 storage tools + 1 maintenance tool.
|
|
213
|
+
// Old tools (memory_store, memory_query, memory_correct, memory_context) are hidden
|
|
214
|
+
// from listing but their handlers remain active for backward compatibility.
|
|
215
|
+
// --- Retrieval ---
|
|
213
216
|
{
|
|
214
|
-
name: '
|
|
215
|
-
|
|
216
|
-
// "entries" (not "content") signals a collection; fighting the "content = string" prior
|
|
217
|
-
// is an architectural fix rather than patching the description after the fact.
|
|
218
|
-
description: 'memory_store(topic: "gotchas", entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}, {title: "Tuist version", fact: "Project requires Tuist 4.x"}], tags: ["build"]). Stores enduring knowledge — (1) Codebase facts (architecture, conventions, gotchas, modules): what IS true now, not past actions. Wrong: "Completed migration to StateFlow." Right: "All ViewModels use StateFlow." (2) User knowledge (user, preferences): who the person is, how they work. "user" and "preferences" are stored in the alwaysInclude lobe (shared across projects). One insight per object; use multiple objects instead of bundling. If content looks likely-ephemeral, the tool returns a review-required response; re-run with durabilityDecision: "store-anyway" only when deliberate.',
|
|
217
|
+
name: 'brief',
|
|
218
|
+
description: `Session start for a project. Returns your preferences (global), gotchas, and recent work for this project and branch. Also surfaces stale entries that need review. Call once at the beginning of a conversation. Lobe names: ${currentLobeNames.join(', ') || 'none configured'}. Example: brief(lobe: "${currentLobeNames[0] ?? 'my-project'}")`,
|
|
219
219
|
inputSchema: {
|
|
220
220
|
type: 'object',
|
|
221
221
|
properties: {
|
|
222
222
|
lobe: lobeProperty,
|
|
223
|
-
|
|
223
|
+
},
|
|
224
|
+
required: [],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'recall',
|
|
229
|
+
description: `Search stored knowledge by relevance within a project. BEFORE starting a task, describe what you are about to do in natural language and get relevant knowledge back (semantic + keyword search). Example: recall(lobe: "${currentLobeNames[0] ?? 'my-project'}", context: "how does auth token refresh work") or recall(lobe: "${currentLobeNames[0] ?? 'my-project'}", context: "refactoring the payment webhook handler")`,
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
lobe: lobeProperty,
|
|
234
|
+
context: {
|
|
224
235
|
type: 'string',
|
|
225
|
-
|
|
226
|
-
// doesn't restrict it — agents can pass any "modules/foo" value and it works.
|
|
227
|
-
// The description makes this explicit.
|
|
228
|
-
description: 'Predefined: user | preferences | architecture | conventions | gotchas | recent-work. Custom namespace: modules/<name> (e.g. modules/brainstorm, modules/game-design, modules/api-notes). Use modules/<name> for any domain that doesn\'t fit the built-in topics.',
|
|
229
|
-
enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
|
|
230
|
-
},
|
|
231
|
-
entries: {
|
|
232
|
-
type: 'array',
|
|
233
|
-
// Type annotation first — agents trained on code read type signatures before prose.
|
|
234
|
-
description: 'Array<{title: string, fact: string}> — not a string. One object per insight. title: short label (2-5 words). fact: codebase topics → present-tense state ("X uses Y", not "Completed X"); user/preferences → what the person expressed. Wrong: one object bundling two facts. Right: two objects.',
|
|
235
|
-
items: {
|
|
236
|
-
type: 'object',
|
|
237
|
-
properties: {
|
|
238
|
-
title: {
|
|
239
|
-
type: 'string',
|
|
240
|
-
description: 'Short label for this insight (2-5 words)',
|
|
241
|
-
},
|
|
242
|
-
fact: {
|
|
243
|
-
type: 'string',
|
|
244
|
-
description: 'The insight itself — one focused fact or observation',
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
required: ['title', 'fact'],
|
|
248
|
-
},
|
|
249
|
-
minItems: 1,
|
|
236
|
+
description: 'Describe what you are about to do, in natural language.',
|
|
250
237
|
},
|
|
251
|
-
|
|
252
|
-
type: '
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
default: [],
|
|
238
|
+
maxResults: {
|
|
239
|
+
type: 'number',
|
|
240
|
+
description: 'Max results (default: 10).',
|
|
241
|
+
default: 10,
|
|
256
242
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
},
|
|
244
|
+
required: ['context'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'gotchas',
|
|
249
|
+
description: `Pitfalls in a project, optionally scoped to an area. Check BEFORE making changes to avoid known traps. Example: gotchas(lobe: "${currentLobeNames[0] ?? 'my-project'}", area: "auth") or gotchas(lobe: "${currentLobeNames[0] ?? 'my-project'}") for all gotchas.`,
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
lobe: lobeProperty,
|
|
254
|
+
area: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Optional keyword filter (e.g. "auth", "build", "navigation").',
|
|
262
257
|
},
|
|
263
|
-
|
|
258
|
+
},
|
|
259
|
+
required: [],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'conventions',
|
|
264
|
+
description: `Coding patterns and standards in a project, optionally scoped to an area. Check BEFORE writing new code to follow established patterns. Example: conventions(lobe: "${currentLobeNames[0] ?? 'my-project'}", area: "testing") or conventions(lobe: "${currentLobeNames[0] ?? 'my-project'}") for all conventions.`,
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
lobe: lobeProperty,
|
|
269
|
+
area: {
|
|
264
270
|
type: 'string',
|
|
265
|
-
|
|
266
|
-
description: 'user (from human) > agent-confirmed > agent-inferred',
|
|
267
|
-
default: 'agent-inferred',
|
|
271
|
+
description: 'Optional keyword filter (e.g. "testing", "naming", "architecture").',
|
|
268
272
|
},
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
},
|
|
274
|
+
required: [],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
// --- Storage ---
|
|
278
|
+
{
|
|
279
|
+
name: 'gotcha',
|
|
280
|
+
description: `Flag a pitfall you hit — what you expected vs what actually happened. Gets priority in brief and recall results. Returns related knowledge you might want to review. Example: gotcha(lobe: "${currentLobeNames[0] ?? 'my-project'}", observation: "Gradle cache must be cleaned after Tuist changes or builds silently use stale artifacts")`,
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
lobe: lobeProperty,
|
|
285
|
+
observation: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
description: 'The gotcha. Write naturally — first sentence becomes the title.',
|
|
274
288
|
},
|
|
275
289
|
durabilityDecision: {
|
|
276
290
|
type: 'string',
|
|
277
291
|
enum: ['default', 'store-anyway'],
|
|
278
|
-
description: '
|
|
292
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
279
293
|
default: 'default',
|
|
280
294
|
},
|
|
281
295
|
},
|
|
282
|
-
required: ['
|
|
296
|
+
required: ['lobe', 'observation'],
|
|
283
297
|
},
|
|
284
298
|
},
|
|
285
299
|
{
|
|
286
|
-
name: '
|
|
287
|
-
description:
|
|
300
|
+
name: 'convention',
|
|
301
|
+
description: `Record a codebase pattern you observed — how the code is structured, naming rules, architectural decisions. For personal rules or preferences, use prefer() instead. Returns related knowledge. Example: convention(lobe: "${currentLobeNames[0] ?? 'my-project'}", observation: "All ViewModels use StateFlow for UI state. LiveData is banned.")`,
|
|
288
302
|
inputSchema: {
|
|
289
303
|
type: 'object',
|
|
290
304
|
properties: {
|
|
291
305
|
lobe: lobeProperty,
|
|
292
|
-
|
|
293
|
-
type: 'string',
|
|
294
|
-
description: 'Optional. Defaults to "*" (all topics). Options: * | user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>',
|
|
295
|
-
},
|
|
296
|
-
detail: {
|
|
306
|
+
observation: {
|
|
297
307
|
type: 'string',
|
|
298
|
-
|
|
299
|
-
description: 'brief = titles only, standard = summaries, full = complete content + metadata',
|
|
300
|
-
default: 'brief',
|
|
308
|
+
description: 'The convention. Write naturally — first sentence becomes the title.',
|
|
301
309
|
},
|
|
302
|
-
|
|
303
|
-
type: 'string',
|
|
304
|
-
description: 'Search terms. "A B" = AND, "A|B" = OR, "-A" = NOT, "#tag" = exact tag, "=term" = exact keyword (no stemming). Example: "#auth reducer -deprecated"',
|
|
305
|
-
},
|
|
306
|
-
branch: {
|
|
310
|
+
durabilityDecision: {
|
|
307
311
|
type: 'string',
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
type: 'boolean',
|
|
312
|
-
description: 'Set true on first memory call in a conversation to include identity/preferences from alwaysInclude lobes. Set false on subsequent calls to skip redundant global knowledge.',
|
|
313
|
-
default: true,
|
|
312
|
+
enum: ['default', 'store-anyway'],
|
|
313
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
314
|
+
default: 'default',
|
|
314
315
|
},
|
|
315
316
|
},
|
|
316
|
-
required: [],
|
|
317
|
+
required: ['lobe', 'observation'],
|
|
317
318
|
},
|
|
318
319
|
},
|
|
319
320
|
{
|
|
320
|
-
name: '
|
|
321
|
-
description:
|
|
321
|
+
name: 'learn',
|
|
322
|
+
description: `Store architecture, dependencies, decisions, progress, or any knowledge not covered by gotcha() or convention(). Use this as the catch-all for insights worth remembering. Returns related knowledge. Example: learn(lobe: "${currentLobeNames[0] ?? 'my-project'}", observation: "Payments module depends on auth for tokens only — no other cross-module dependency")`,
|
|
322
323
|
inputSchema: {
|
|
323
324
|
type: 'object',
|
|
324
325
|
properties: {
|
|
325
326
|
lobe: lobeProperty,
|
|
326
|
-
|
|
327
|
+
observation: {
|
|
327
328
|
type: 'string',
|
|
328
|
-
description: '
|
|
329
|
+
description: 'The observation. Write naturally — first sentence becomes the title.',
|
|
329
330
|
},
|
|
330
|
-
|
|
331
|
+
durabilityDecision: {
|
|
331
332
|
type: 'string',
|
|
332
|
-
|
|
333
|
+
enum: ['default', 'store-anyway'],
|
|
334
|
+
description: 'Use "store-anyway" only when re-storing after a review-required response.',
|
|
335
|
+
default: 'default',
|
|
333
336
|
},
|
|
334
|
-
|
|
337
|
+
},
|
|
338
|
+
required: ['lobe', 'observation'],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'prefer',
|
|
343
|
+
description: `When your user corrects you or states a working-style rule, record it here. Persists globally by default and is surfaced in every brief(). Optionally scope to a specific project. Example: prefer(rule: "Never use !! operator") or prefer(rule: "Use Anvil for DI", lobe: "${currentLobeNames[0] ?? 'my-project'}")`,
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
rule: {
|
|
335
348
|
type: 'string',
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
description: 'The preference or rule. Write naturally.',
|
|
350
|
+
},
|
|
351
|
+
lobe: {
|
|
352
|
+
...lobeProperty,
|
|
353
|
+
description: `Optional. Scope this preference to a specific lobe. Omit for global. Available: ${currentLobeNames.join(', ')}`,
|
|
338
354
|
},
|
|
339
355
|
},
|
|
340
|
-
required: ['
|
|
356
|
+
required: ['rule'],
|
|
341
357
|
},
|
|
342
358
|
},
|
|
359
|
+
// --- Maintenance ---
|
|
343
360
|
{
|
|
344
|
-
name: '
|
|
345
|
-
description:
|
|
361
|
+
name: 'fix',
|
|
362
|
+
description: `Correct or delete a memory entry. IDs appear in brief(), recall(), gotchas(), and conventions() results. Pass correction to update content; omit correction to delete the entry entirely. Example: fix(id: "gotcha-a1b2c3d4", correction: "Fixed in v2.3 — cache is auto-cleaned now") or fix(id: "gotcha-a1b2c3d4") to delete.`,
|
|
346
363
|
inputSchema: {
|
|
347
364
|
type: 'object',
|
|
348
365
|
properties: {
|
|
349
|
-
|
|
350
|
-
context: {
|
|
366
|
+
id: {
|
|
351
367
|
type: 'string',
|
|
352
|
-
description: '
|
|
353
|
-
},
|
|
354
|
-
maxResults: {
|
|
355
|
-
type: 'number',
|
|
356
|
-
description: 'Max results (default: 10)',
|
|
357
|
-
default: 10,
|
|
368
|
+
description: 'Entry ID (e.g. gotcha-3f7a, conv-5c9b, gen-a2d1, pref-8e4f).',
|
|
358
369
|
},
|
|
359
|
-
|
|
360
|
-
type: '
|
|
361
|
-
description: '
|
|
362
|
-
default: 0.2,
|
|
370
|
+
correction: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
description: 'New text to replace the entry content. Omit entirely to delete the entry.',
|
|
363
373
|
},
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
description:
|
|
367
|
-
default: true,
|
|
374
|
+
lobe: {
|
|
375
|
+
...lobeProperty,
|
|
376
|
+
description: `Optional. Searches all lobes if omitted. Available: ${currentLobeNames.join(', ')}`,
|
|
368
377
|
},
|
|
369
378
|
},
|
|
370
|
-
required: [],
|
|
379
|
+
required: ['id'],
|
|
371
380
|
},
|
|
372
381
|
},
|
|
373
|
-
//
|
|
374
|
-
//
|
|
382
|
+
// --- Legacy tools (still listed for backward compatibility) ---
|
|
383
|
+
// memory_store, memory_query, memory_correct, memory_context handlers remain active
|
|
384
|
+
// but are hidden from tool discovery. Agents use the new v2 tools above.
|
|
375
385
|
{
|
|
376
386
|
name: 'memory_bootstrap',
|
|
377
387
|
description: 'First-time setup: scan repo structure, README, and build system to seed initial knowledge. Run once per new codebase. If the lobe does not exist yet, provide "root" to auto-add it to memory-config.json and proceed without a manual restart.',
|
|
@@ -380,7 +390,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
380
390
|
properties: {
|
|
381
391
|
lobe: {
|
|
382
392
|
type: 'string',
|
|
383
|
-
// No enum restriction: agents pass new lobe names not yet in the config
|
|
384
393
|
description: `Memory lobe name. If the lobe doesn't exist yet, also pass "root" to auto-create it. Available lobes: ${currentLobeNames.join(', ')}`,
|
|
385
394
|
},
|
|
386
395
|
root: {
|
|
@@ -395,13 +404,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
395
404
|
required: [],
|
|
396
405
|
},
|
|
397
406
|
},
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// memory_reembed is hidden — utility for generating/regenerating embeddings.
|
|
402
|
-
// Surfaced via hint in memory_context when >50% of entries lack vectors.
|
|
407
|
+
// Hidden tools — handlers still active:
|
|
408
|
+
// memory_stats, memory_diagnose, memory_reembed, memory_list_lobes,
|
|
409
|
+
// memory_store, memory_query, memory_correct, memory_context
|
|
403
410
|
] };
|
|
404
411
|
});
|
|
412
|
+
// --- MCP Resource: memory://lobes ---
|
|
413
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
414
|
+
await configManager.ensureFresh();
|
|
415
|
+
return {
|
|
416
|
+
resources: [{
|
|
417
|
+
uri: 'memory://lobes',
|
|
418
|
+
name: 'Available memory lobes',
|
|
419
|
+
description: 'Lists all configured memory lobes with their health status and entry counts.',
|
|
420
|
+
mimeType: 'application/json',
|
|
421
|
+
}],
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
425
|
+
const uri = request.params.uri;
|
|
426
|
+
if (uri === 'memory://lobes') {
|
|
427
|
+
await configManager.ensureFresh();
|
|
428
|
+
const lobeInfo = await buildLobeInfo();
|
|
429
|
+
return {
|
|
430
|
+
contents: [{
|
|
431
|
+
uri: 'memory://lobes',
|
|
432
|
+
mimeType: 'application/json',
|
|
433
|
+
text: JSON.stringify({ lobes: lobeInfo }, null, 2),
|
|
434
|
+
}],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
438
|
+
});
|
|
405
439
|
// --- Tool handlers ---
|
|
406
440
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
407
441
|
const { name, arguments: rawArgs } = request.params;
|
|
@@ -449,6 +483,340 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
449
483
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
450
484
|
};
|
|
451
485
|
}
|
|
486
|
+
// ─── New v2 tool handlers ────────────────────────────────────────────
|
|
487
|
+
// Shared helper for gotcha/convention/learn — identical store logic, different topic.
|
|
488
|
+
case 'gotcha':
|
|
489
|
+
case 'convention':
|
|
490
|
+
case 'learn': {
|
|
491
|
+
const topicMap = {
|
|
492
|
+
gotcha: 'gotchas', convention: 'conventions', learn: 'general',
|
|
493
|
+
};
|
|
494
|
+
const topic = topicMap[name];
|
|
495
|
+
const { lobe: rawLobe, observation, durabilityDecision } = z.object({
|
|
496
|
+
lobe: z.string().min(1),
|
|
497
|
+
observation: z.string().min(1),
|
|
498
|
+
durabilityDecision: z.enum(['default', 'store-anyway']).default('default'),
|
|
499
|
+
}).parse(args);
|
|
500
|
+
process.stderr.write(`[memory-mcp] tool=${name} lobe=${rawLobe}\n`);
|
|
501
|
+
const ctx = resolveToolContext(rawLobe);
|
|
502
|
+
if (!ctx.ok)
|
|
503
|
+
return contextError(ctx);
|
|
504
|
+
const { title, content } = extractTitle(observation);
|
|
505
|
+
const result = await ctx.store.store(topic, title, content, [], 'agent-inferred', [], [], durabilityDecision);
|
|
506
|
+
if (result.kind === 'review-required') {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: `[${ctx.label}] Review required: ${result.warning}\n\nIf intentional, re-run: ${name}(lobe: "${rawLobe}", observation: "...", durabilityDecision: "store-anyway")` }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (result.kind === 'rejected') {
|
|
513
|
+
return {
|
|
514
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed: ${result.warning}` }],
|
|
515
|
+
isError: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const lootDrop = result.relatedEntries ? formatLootDrop(result.relatedEntries) : '';
|
|
519
|
+
const label = name === 'learn' ? '' : `${name} `;
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: 'text', text: `[${ctx.label}] Stored ${label}${result.id}: "${title}" (confidence: ${result.confidence})${lootDrop}` }],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
case 'brief': {
|
|
525
|
+
// Session-start briefing — user identity, preferences, gotchas overview, stale entries.
|
|
526
|
+
// Delegates to the same briefing logic as memory_context(context: undefined).
|
|
527
|
+
const { lobe: rawLobe } = z.object({
|
|
528
|
+
lobe: z.string().optional(),
|
|
529
|
+
}).parse(args ?? {});
|
|
530
|
+
process.stderr.write(`[memory-mcp] tool=brief lobe=${rawLobe ?? 'auto'}\n`);
|
|
531
|
+
// Surface previous crash report
|
|
532
|
+
const previousCrash = await readLatestCrash();
|
|
533
|
+
const crashSection = previousCrash
|
|
534
|
+
? `## Previous Crash Detected\n${formatCrashSummary(previousCrash)}\nRun **memory_diagnose** for full details.\n`
|
|
535
|
+
: '';
|
|
536
|
+
if (previousCrash)
|
|
537
|
+
await clearLatestCrash();
|
|
538
|
+
// Surface degraded lobes warning
|
|
539
|
+
const allBriefingLobes = rawLobe
|
|
540
|
+
? [rawLobe, ...configManager.getAlwaysIncludeLobes().filter(n => n !== rawLobe)]
|
|
541
|
+
: configManager.getLobeNames();
|
|
542
|
+
const degradedLobeNames = allBriefingLobes.filter(n => configManager.getLobeHealth(n)?.status === 'degraded');
|
|
543
|
+
const degradedSection = degradedLobeNames.length > 0
|
|
544
|
+
? `## Degraded Lobes: ${degradedLobeNames.join(', ')}\nRun **memory_diagnose** for details.`
|
|
545
|
+
: '';
|
|
546
|
+
const sections = [];
|
|
547
|
+
if (crashSection)
|
|
548
|
+
sections.push(crashSection);
|
|
549
|
+
if (degradedSection)
|
|
550
|
+
sections.push(degradedSection);
|
|
551
|
+
// Collect briefing across all lobes (or specified lobe + alwaysInclude)
|
|
552
|
+
const briefingLobeNames = allBriefingLobes;
|
|
553
|
+
const allStale = [];
|
|
554
|
+
let totalEntries = 0;
|
|
555
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
556
|
+
for (const lobeName of briefingLobeNames) {
|
|
557
|
+
const health = configManager.getLobeHealth(lobeName);
|
|
558
|
+
if (health?.status === 'degraded')
|
|
559
|
+
continue;
|
|
560
|
+
const store = configManager.getStore(lobeName);
|
|
561
|
+
if (!store)
|
|
562
|
+
continue;
|
|
563
|
+
const budget = alwaysIncludeSet.has(lobeName) ? 300 : 100;
|
|
564
|
+
const lobeBriefing = await store.briefing(budget);
|
|
565
|
+
if (lobeBriefing.entryCount > 0)
|
|
566
|
+
sections.push(lobeBriefing.briefing);
|
|
567
|
+
if (lobeBriefing.staleDetails)
|
|
568
|
+
allStale.push(...lobeBriefing.staleDetails);
|
|
569
|
+
totalEntries += lobeBriefing.entryCount;
|
|
570
|
+
}
|
|
571
|
+
if (allStale.length > 0)
|
|
572
|
+
sections.push(formatStaleSection(allStale));
|
|
573
|
+
if (sections.length === 0) {
|
|
574
|
+
sections.push('No knowledge stored yet. As you work, use **gotcha**, **convention**, or **learn** to store observations. Try **memory_bootstrap** to seed initial knowledge.');
|
|
575
|
+
}
|
|
576
|
+
const briefLobes = briefingLobeNames.filter(n => configManager.getLobeHealth(n)?.status !== 'degraded');
|
|
577
|
+
sections.push(`---\n*${totalEntries} entries across ${briefLobes.length} ${briefLobes.length === 1 ? 'lobe' : 'lobes'}. Use **recall(context: "what you are doing")** for task-specific knowledge.*`);
|
|
578
|
+
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
579
|
+
}
|
|
580
|
+
case 'recall': {
|
|
581
|
+
// Pre-task lookup — semantic + keyword search on the target lobe.
|
|
582
|
+
// Pure task-relevant search — no global preferences (that's what brief is for).
|
|
583
|
+
const { lobe: rawLobe, context, maxResults } = z.object({
|
|
584
|
+
lobe: z.string().optional(),
|
|
585
|
+
context: z.string().min(1),
|
|
586
|
+
maxResults: z.number().optional(),
|
|
587
|
+
}).parse(args);
|
|
588
|
+
process.stderr.write(`[memory-mcp] tool=recall lobe=${rawLobe ?? 'auto'} context="${context.slice(0, 50)}"\n`);
|
|
589
|
+
const max = maxResults ?? 10;
|
|
590
|
+
const allLobeResults = [];
|
|
591
|
+
const ctxEntryLobeMap = new Map();
|
|
592
|
+
let label;
|
|
593
|
+
let primaryStore;
|
|
594
|
+
if (rawLobe) {
|
|
595
|
+
const ctx = resolveToolContext(rawLobe);
|
|
596
|
+
if (!ctx.ok)
|
|
597
|
+
return contextError(ctx);
|
|
598
|
+
label = ctx.label;
|
|
599
|
+
primaryStore = ctx.store;
|
|
600
|
+
const lobeResults = await ctx.store.contextSearch(context, max);
|
|
601
|
+
allLobeResults.push(...lobeResults);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
// Search all non-global lobes (recall is task-specific, not identity)
|
|
605
|
+
const resolution = await resolveLobesForRead(false);
|
|
606
|
+
switch (resolution.kind) {
|
|
607
|
+
case 'resolved': {
|
|
608
|
+
label = resolution.label;
|
|
609
|
+
for (const lobeName of resolution.lobes) {
|
|
610
|
+
const store = configManager.getStore(lobeName);
|
|
611
|
+
if (!store)
|
|
612
|
+
continue;
|
|
613
|
+
if (!primaryStore)
|
|
614
|
+
primaryStore = store;
|
|
615
|
+
const lobeResults = await store.contextSearch(context, max);
|
|
616
|
+
if (resolution.lobes.length > 1) {
|
|
617
|
+
for (const r of lobeResults)
|
|
618
|
+
ctxEntryLobeMap.set(r.entry.id, lobeName);
|
|
619
|
+
}
|
|
620
|
+
allLobeResults.push(...lobeResults);
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'global-only': {
|
|
625
|
+
// No project lobes matched — ask agent to specify
|
|
626
|
+
return {
|
|
627
|
+
content: [{ type: 'text', text: `Cannot determine which lobe to search. Specify the lobe: recall(lobe: "...", context: "${context}")\nAvailable: ${configManager.getLobeNames().join(', ')}` }],
|
|
628
|
+
isError: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Dedupe, sort, slice
|
|
634
|
+
const seenIds = new Set();
|
|
635
|
+
const results = allLobeResults
|
|
636
|
+
.sort((a, b) => b.score - a.score)
|
|
637
|
+
.filter(r => {
|
|
638
|
+
if (seenIds.has(r.entry.id))
|
|
639
|
+
return false;
|
|
640
|
+
seenIds.add(r.entry.id);
|
|
641
|
+
return true;
|
|
642
|
+
})
|
|
643
|
+
.slice(0, max);
|
|
644
|
+
if (results.length === 0) {
|
|
645
|
+
const modeHint = primaryStore
|
|
646
|
+
? `\n${formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount)}`
|
|
647
|
+
: '';
|
|
648
|
+
return {
|
|
649
|
+
content: [{
|
|
650
|
+
type: 'text',
|
|
651
|
+
text: `No relevant knowledge found for: "${context}"\n\nProceed without prior context. Use **gotcha**, **convention**, or **learn** to store observations as you work.${modeHint}`,
|
|
652
|
+
}],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// Format results grouped by topic
|
|
656
|
+
const sections = [`## Recall: "${context}"\n`];
|
|
657
|
+
const byTopic = new Map();
|
|
658
|
+
for (const r of results) {
|
|
659
|
+
const list = byTopic.get(r.entry.topic) ?? [];
|
|
660
|
+
list.push(r);
|
|
661
|
+
byTopic.set(r.entry.topic, list);
|
|
662
|
+
}
|
|
663
|
+
const topicOrder = ['gotchas', 'conventions', 'architecture', 'general'];
|
|
664
|
+
const orderedTopics = [
|
|
665
|
+
...topicOrder.filter(t => byTopic.has(t)),
|
|
666
|
+
...Array.from(byTopic.keys()).filter(t => !topicOrder.includes(t)).sort(),
|
|
667
|
+
];
|
|
668
|
+
const showLobeLabels = ctxEntryLobeMap.size > 0;
|
|
669
|
+
for (const topic of orderedTopics) {
|
|
670
|
+
const topicResults = byTopic.get(topic);
|
|
671
|
+
const heading = topic === 'gotchas' ? 'Gotchas'
|
|
672
|
+
: topic === 'conventions' ? 'Conventions'
|
|
673
|
+
: topic === 'general' ? 'General'
|
|
674
|
+
: topic.startsWith('modules/') ? `Module: ${topic.split('/')[1]}`
|
|
675
|
+
: topic.charAt(0).toUpperCase() + topic.slice(1);
|
|
676
|
+
sections.push(`### ${heading}`);
|
|
677
|
+
for (const r of topicResults) {
|
|
678
|
+
const marker = topic === 'gotchas' ? '[!] ' : '';
|
|
679
|
+
const lobeLabel = showLobeLabels ? ` [${ctxEntryLobeMap.get(r.entry.id) ?? '?'}]` : '';
|
|
680
|
+
sections.push(`- **${marker}${r.entry.title}**${lobeLabel}: ${r.entry.content}`);
|
|
681
|
+
}
|
|
682
|
+
sections.push('');
|
|
683
|
+
}
|
|
684
|
+
// Mode indicator
|
|
685
|
+
if (primaryStore) {
|
|
686
|
+
sections.push(formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount));
|
|
687
|
+
}
|
|
688
|
+
return { content: [{ type: 'text', text: sections.join('\n') }] };
|
|
689
|
+
}
|
|
690
|
+
case 'gotchas': {
|
|
691
|
+
// Topic-filtered query for gotchas.
|
|
692
|
+
const { lobe: rawLobe, area } = z.object({
|
|
693
|
+
lobe: z.string().optional(),
|
|
694
|
+
area: z.string().optional(),
|
|
695
|
+
}).parse(args ?? {});
|
|
696
|
+
process.stderr.write(`[memory-mcp] tool=gotchas lobe=${rawLobe ?? 'auto'} area=${area ?? '*'}\n`);
|
|
697
|
+
const ctx = resolveToolContext(rawLobe);
|
|
698
|
+
if (!ctx.ok)
|
|
699
|
+
return contextError(ctx);
|
|
700
|
+
const result = await ctx.store.query('gotchas', 'standard', area);
|
|
701
|
+
if (result.entries.length === 0) {
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: 'text', text: `[${ctx.label}] No gotchas found${area ? ` for "${area}"` : ''}. Use **gotcha(lobe, observation)** to store one.` }],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const lines = result.entries.map(e => `- **[!] ${e.title}** (${e.id}): ${e.content}`);
|
|
707
|
+
return {
|
|
708
|
+
content: [{ type: 'text', text: `## [${ctx.label}] Gotchas${area ? ` — ${area}` : ''}\n\n${lines.join('\n')}` }],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
case 'conventions': {
|
|
712
|
+
// Topic-filtered query for conventions.
|
|
713
|
+
const { lobe: rawLobe, area } = z.object({
|
|
714
|
+
lobe: z.string().optional(),
|
|
715
|
+
area: z.string().optional(),
|
|
716
|
+
}).parse(args ?? {});
|
|
717
|
+
process.stderr.write(`[memory-mcp] tool=conventions lobe=${rawLobe ?? 'auto'} area=${area ?? '*'}\n`);
|
|
718
|
+
const ctx = resolveToolContext(rawLobe);
|
|
719
|
+
if (!ctx.ok)
|
|
720
|
+
return contextError(ctx);
|
|
721
|
+
const result = await ctx.store.query('conventions', 'standard', area);
|
|
722
|
+
if (result.entries.length === 0) {
|
|
723
|
+
return {
|
|
724
|
+
content: [{ type: 'text', text: `[${ctx.label}] No conventions found${area ? ` for "${area}"` : ''}. Use **convention(lobe, observation)** to store one.` }],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const lines = result.entries.map(e => `- **${e.title}** (${e.id}): ${e.content}`);
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: 'text', text: `## [${ctx.label}] Conventions${area ? ` — ${area}` : ''}\n\n${lines.join('\n')}` }],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
case 'prefer': {
|
|
733
|
+
// Store a user preference. Routes to alwaysInclude lobe (or specified lobe).
|
|
734
|
+
const { rule, lobe: rawLobe } = z.object({
|
|
735
|
+
rule: z.string().min(1),
|
|
736
|
+
lobe: z.string().optional(),
|
|
737
|
+
}).parse(args);
|
|
738
|
+
process.stderr.write(`[memory-mcp] tool=prefer lobe=${rawLobe ?? 'global'}\n`);
|
|
739
|
+
// Route to alwaysInclude lobe when no lobe specified
|
|
740
|
+
let effectiveLobe = rawLobe;
|
|
741
|
+
if (!effectiveLobe) {
|
|
742
|
+
const alwaysIncludeLobes = configManager.getAlwaysIncludeLobes();
|
|
743
|
+
effectiveLobe = alwaysIncludeLobes.length > 0 ? alwaysIncludeLobes[0] : undefined;
|
|
744
|
+
}
|
|
745
|
+
const ctx = resolveToolContext(effectiveLobe);
|
|
746
|
+
if (!ctx.ok) {
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: 'text', text: `No global lobe configured for preferences. Specify a lobe: prefer(rule: "...", lobe: "...").\nAvailable: ${configManager.getLobeNames().join(', ')}` }],
|
|
749
|
+
isError: true,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const { title, content } = extractTitle(rule);
|
|
753
|
+
const result = await ctx.store.store('preferences', title, content, [], 'user');
|
|
754
|
+
if (result.kind !== 'stored') {
|
|
755
|
+
return {
|
|
756
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed to store preference: ${result.kind === 'review-required' ? result.warning : result.kind === 'rejected' ? result.warning : 'unknown error'}` }],
|
|
757
|
+
isError: true,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
content: [{ type: 'text', text: `[${ctx.label}] Stored preference ${result.id}: "${title}" (trust: user)` }],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
case 'fix': {
|
|
765
|
+
// Fix or delete an entry. Search all stores if no lobe specified.
|
|
766
|
+
const { id, correction, lobe: rawLobe } = z.object({
|
|
767
|
+
id: z.string().min(1),
|
|
768
|
+
correction: z.string().optional(),
|
|
769
|
+
lobe: z.string().optional(),
|
|
770
|
+
}).parse(args);
|
|
771
|
+
process.stderr.write(`[memory-mcp] tool=fix id=${id} action=${correction ? 'replace' : 'delete'}\n`);
|
|
772
|
+
const action = correction !== undefined && correction.length > 0 ? 'replace' : 'delete';
|
|
773
|
+
// Resolve lobe — search all stores if not specified
|
|
774
|
+
let effectiveFixLobe = rawLobe;
|
|
775
|
+
let foundInLobe;
|
|
776
|
+
if (!effectiveFixLobe) {
|
|
777
|
+
for (const lobeName of configManager.getLobeNames()) {
|
|
778
|
+
const store = configManager.getStore(lobeName);
|
|
779
|
+
if (!store)
|
|
780
|
+
continue;
|
|
781
|
+
try {
|
|
782
|
+
if (await store.hasEntry(id)) {
|
|
783
|
+
effectiveFixLobe = lobeName;
|
|
784
|
+
foundInLobe = lobeName;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// Skip degraded lobes
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!effectiveFixLobe) {
|
|
794
|
+
return {
|
|
795
|
+
content: [{ type: 'text', text: `Entry "${id}" not found in any lobe. Available: ${configManager.getLobeNames().join(', ')}` }],
|
|
796
|
+
isError: true,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const ctx = resolveToolContext(effectiveFixLobe);
|
|
800
|
+
if (!ctx.ok)
|
|
801
|
+
return contextError(ctx);
|
|
802
|
+
const result = await ctx.store.correct(id, correction ?? '', action);
|
|
803
|
+
if (!result.corrected) {
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed: ${result.error}` }],
|
|
806
|
+
isError: true,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const lobeNote = foundInLobe && !rawLobe ? ` (found in lobe: ${foundInLobe})` : '';
|
|
810
|
+
if (action === 'delete') {
|
|
811
|
+
return {
|
|
812
|
+
content: [{ type: 'text', text: `Deleted entry ${id}${lobeNote}.` }],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
content: [{ type: 'text', text: `Fixed entry ${id}${lobeNote} (confidence: ${result.newConfidence}, trust: ${result.trust}).` }],
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
// ─── Legacy tool handlers (hidden from listing) ──────────────────────
|
|
452
820
|
case 'memory_store': {
|
|
453
821
|
const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags, durabilityDecision } = z.object({
|
|
454
822
|
lobe: z.string().optional(),
|
|
@@ -470,7 +838,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
470
838
|
const topic = parseTopicScope(rawTopic);
|
|
471
839
|
if (!topic) {
|
|
472
840
|
return {
|
|
473
|
-
content: [{ type: 'text', text: `Invalid topic: "${rawTopic}". Valid: user | preferences | architecture | conventions | gotchas | recent-work | modules/<name>` }],
|
|
841
|
+
content: [{ type: 'text', text: `Invalid topic: "${rawTopic}". Valid: user | preferences | architecture | conventions | gotchas | general | recent-work | modules/<name>` }],
|
|
474
842
|
isError: true,
|
|
475
843
|
};
|
|
476
844
|
}
|
|
@@ -1179,20 +1547,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1179
1547
|
}
|
|
1180
1548
|
catch (error) {
|
|
1181
1549
|
const message = error instanceof Error ? error.message : String(error);
|
|
1182
|
-
// Provide helpful hints for
|
|
1550
|
+
// Provide helpful, human-readable hints for Zod validation errors.
|
|
1551
|
+
// Raw Zod JSON is cryptic for agents — translate to actionable guidance.
|
|
1552
|
+
const lobeNames = configManager.getLobeNames();
|
|
1553
|
+
const lobeList = lobeNames.join(', ');
|
|
1554
|
+
let friendlyMessage = message;
|
|
1555
|
+
// Detect Zod validation errors (they contain path arrays in JSON)
|
|
1556
|
+
if (message.includes('"path"') && message.includes('"message"')) {
|
|
1557
|
+
// Build a friendlyMessage from the Zod issues
|
|
1558
|
+
try {
|
|
1559
|
+
const issues = JSON.parse(message);
|
|
1560
|
+
const fields = issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
|
|
1561
|
+
friendlyMessage = `Invalid arguments — ${fields}`;
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// If parsing fails, keep original message
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
// Tool-specific hints based on which field failed
|
|
1183
1568
|
let hint = '';
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1569
|
+
const v2ToolHints = {
|
|
1570
|
+
brief: `brief takes an optional lobe. Example: {"lobe": "${lobeNames[0] ?? 'default'}"}. Available lobes: ${lobeList}`,
|
|
1571
|
+
recall: `recall requires "context" (describe what you are working on). Example: {"context": "implementing auth flow", "lobe": "${lobeNames[0] ?? 'default'}"}. Available lobes: ${lobeList}`,
|
|
1572
|
+
gotcha: `gotcha requires "lobe" and "observation". Example: {"lobe": "${lobeNames[0] ?? 'default'}", "observation": "Gradle cache breaks after Tuist changes"}`,
|
|
1573
|
+
convention: `convention requires "lobe" and "observation". Example: {"lobe": "${lobeNames[0] ?? 'default'}", "observation": "All ViewModels use StateFlow"}`,
|
|
1574
|
+
learn: `learn requires "lobe" and "observation". Example: {"lobe": "${lobeNames[0] ?? 'default'}", "observation": "Messaging uses MVVM with FlowCoordinator"}`,
|
|
1575
|
+
prefer: `prefer requires "rule". Example: {"rule": "Always suggest the simplest solution first"}`,
|
|
1576
|
+
fix: `fix requires "id". Optional: "correction" (new text; omit to delete). Example: {"id": "gotcha-3f7a", "correction": "updated text"}`,
|
|
1577
|
+
gotchas: `gotchas takes optional "lobe" and "area". Example: {"lobe": "${lobeNames[0] ?? 'default'}", "area": "auth"}. Available lobes: ${lobeList}`,
|
|
1578
|
+
conventions: `conventions takes optional "lobe" and "area". Example: {"lobe": "${lobeNames[0] ?? 'default'}", "area": "testing"}. Available lobes: ${lobeList}`,
|
|
1579
|
+
};
|
|
1580
|
+
if (name in v2ToolHints) {
|
|
1581
|
+
hint = `\n\nUsage: ${v2ToolHints[name]}`;
|
|
1582
|
+
}
|
|
1583
|
+
else if (friendlyMessage.includes('"lobe"') || friendlyMessage.includes('lobe:')) {
|
|
1584
|
+
hint = `\n\nHint: lobe is required. Available: ${lobeList}`;
|
|
1187
1585
|
}
|
|
1188
1586
|
else if (message.includes('"topic"') || message.includes('"entries"')) {
|
|
1189
|
-
hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|recent-work|modules/<name>) and entries (Array<{title, fact}>).
|
|
1587
|
+
hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|general|recent-work|modules/<name>) and entries (Array<{title, fact}>).';
|
|
1190
1588
|
}
|
|
1191
1589
|
else if (message.includes('"scope"')) {
|
|
1192
|
-
hint = '\n\nHint: memory_query requires:
|
|
1590
|
+
hint = '\n\nHint: memory_query requires: scope (architecture|conventions|gotchas|*). Example: memory_query(scope: "*")';
|
|
1193
1591
|
}
|
|
1194
1592
|
return {
|
|
1195
|
-
content: [{ type: 'text', text:
|
|
1593
|
+
content: [{ type: 'text', text: `${friendlyMessage}${hint}` }],
|
|
1196
1594
|
isError: true,
|
|
1197
1595
|
};
|
|
1198
1596
|
}
|
package/dist/normalize.js
CHANGED
|
@@ -3,17 +3,10 @@
|
|
|
3
3
|
// Agents frequently guess wrong param names. This module resolves common aliases
|
|
4
4
|
// and applies defaults to avoid wasted round-trips from validation errors.
|
|
5
5
|
// Pure functions — no side effects, no state.
|
|
6
|
-
/**
|
|
7
|
-
const
|
|
6
|
+
/** Global param aliases — apply to all tools regardless of name */
|
|
7
|
+
const GLOBAL_ALIASES = {
|
|
8
8
|
// memory_store aliases
|
|
9
9
|
refs: 'references',
|
|
10
|
-
// memory_query aliases
|
|
11
|
-
query: 'filter',
|
|
12
|
-
search: 'filter',
|
|
13
|
-
keyword: 'filter',
|
|
14
|
-
// memory_context aliases
|
|
15
|
-
description: 'context',
|
|
16
|
-
task: 'context',
|
|
17
10
|
// tag aliases
|
|
18
11
|
tag: 'tags',
|
|
19
12
|
labels: 'tags',
|
|
@@ -22,6 +15,28 @@ const PARAM_ALIASES = {
|
|
|
22
15
|
workspace: 'lobe',
|
|
23
16
|
repo: 'lobe',
|
|
24
17
|
};
|
|
18
|
+
/** Tool-specific param aliases — only apply when the tool name matches.
|
|
19
|
+
* Prevents `query` → `filter` rewriting from breaking `recall(query: "...")`. */
|
|
20
|
+
const TOOL_ALIASES = {
|
|
21
|
+
// Legacy tools: "query" means "filter"
|
|
22
|
+
memory_query: { query: 'filter', search: 'filter', keyword: 'filter' },
|
|
23
|
+
memory_store: { scope: 'topic' },
|
|
24
|
+
// Legacy tools: "description"/"task" mean "context"
|
|
25
|
+
memory_context: { description: 'context', task: 'context' },
|
|
26
|
+
// v2 retrieval tools: "query"/"search"/"description"/"task" mean "context"
|
|
27
|
+
recall: { query: 'context', search: 'context', description: 'context', task: 'context' },
|
|
28
|
+
// v2 storage tools: "content"/"note"/"fact" mean "observation"
|
|
29
|
+
gotcha: { content: 'observation', note: 'observation', fact: 'observation', message: 'observation' },
|
|
30
|
+
convention: { content: 'observation', note: 'observation', fact: 'observation', message: 'observation' },
|
|
31
|
+
learn: { content: 'observation', note: 'observation', fact: 'observation', message: 'observation' },
|
|
32
|
+
// v2 prefer: "preference"/"pref" mean "rule"
|
|
33
|
+
prefer: { preference: 'rule', pref: 'rule' },
|
|
34
|
+
// v2 fix: "text"/"content"/"replacement" mean "correction"
|
|
35
|
+
fix: { text: 'correction', content: 'correction', replacement: 'correction' },
|
|
36
|
+
// v2 retrieval: "filter"/"keyword" mean "area"
|
|
37
|
+
gotchas: { filter: 'area', keyword: 'area', query: 'area', search: 'area' },
|
|
38
|
+
conventions: { filter: 'area', keyword: 'area', query: 'area', search: 'area' },
|
|
39
|
+
};
|
|
25
40
|
/** Wildcard scope aliases — agents guess many variations instead of "*" */
|
|
26
41
|
const SCOPE_WILDCARDS = new Set([
|
|
27
42
|
'all', 'everything', 'any', '*', 'global', 'project', 'repo',
|
|
@@ -30,17 +45,22 @@ const SCOPE_WILDCARDS = new Set([
|
|
|
30
45
|
/** Normalize args before Zod validation: resolve aliases, default workspace, fix wildcards */
|
|
31
46
|
export function normalizeArgs(toolName, raw, lobeNames) {
|
|
32
47
|
const args = { ...(raw ?? {}) };
|
|
33
|
-
// 1. Resolve param aliases
|
|
34
|
-
for (const [alias, canonical] of Object.entries(
|
|
48
|
+
// 1. Resolve global param aliases
|
|
49
|
+
for (const [alias, canonical] of Object.entries(GLOBAL_ALIASES)) {
|
|
35
50
|
if (alias in args && !(canonical in args)) {
|
|
36
51
|
args[canonical] = args[alias];
|
|
37
52
|
delete args[alias];
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
|
-
// 2.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
// 2. Resolve tool-specific param aliases
|
|
56
|
+
const toolSpecific = TOOL_ALIASES[toolName];
|
|
57
|
+
if (toolSpecific) {
|
|
58
|
+
for (const [alias, canonical] of Object.entries(toolSpecific)) {
|
|
59
|
+
if (alias in args && !(canonical in args)) {
|
|
60
|
+
args[canonical] = args[alias];
|
|
61
|
+
delete args[alias];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
44
64
|
}
|
|
45
65
|
// 3. Default lobe to the only available one when omitted
|
|
46
66
|
if (!('lobe' in args) || args['lobe'] === undefined || args['lobe'] === '') {
|
package/dist/text-analyzer.d.ts
CHANGED
|
@@ -60,3 +60,15 @@ export declare function matchesFilter(allKeywords: Set<string>, filter: string,
|
|
|
60
60
|
* Title matches get 2x weight over content-only matches.
|
|
61
61
|
* Tag and exact matches count as full-weight hits (same as title). */
|
|
62
62
|
export declare function computeRelevanceScore(titleKeywords: Set<string>, contentKeywords: Set<string>, confidence: number, filter: string, tags?: readonly string[]): number;
|
|
63
|
+
/** Extract a title and content from a single observation string.
|
|
64
|
+
* Title: first sentence (terminated by . ! ? or newline), capped at MAX_TITLE_LENGTH.
|
|
65
|
+
* Content: the full observation text (title is a derived label, not subtracted).
|
|
66
|
+
*
|
|
67
|
+
* Abbreviation-safe: requires 2+ word characters before the period to avoid
|
|
68
|
+
* splitting on "e.g.", "U.S.", "i.e.", etc.
|
|
69
|
+
*
|
|
70
|
+
* Pure function — no I/O, no side effects. */
|
|
71
|
+
export declare function extractTitle(observation: string): {
|
|
72
|
+
readonly title: string;
|
|
73
|
+
readonly content: string;
|
|
74
|
+
};
|
package/dist/text-analyzer.js
CHANGED
|
@@ -275,3 +275,31 @@ export function computeRelevanceScore(titleKeywords, contentKeywords, confidence
|
|
|
275
275
|
}
|
|
276
276
|
return bestScore * confidence;
|
|
277
277
|
}
|
|
278
|
+
/** Maximum title length before truncation */
|
|
279
|
+
const MAX_TITLE_LENGTH = 80;
|
|
280
|
+
/** Extract a title and content from a single observation string.
|
|
281
|
+
* Title: first sentence (terminated by . ! ? or newline), capped at MAX_TITLE_LENGTH.
|
|
282
|
+
* Content: the full observation text (title is a derived label, not subtracted).
|
|
283
|
+
*
|
|
284
|
+
* Abbreviation-safe: requires 2+ word characters before the period to avoid
|
|
285
|
+
* splitting on "e.g.", "U.S.", "i.e.", etc.
|
|
286
|
+
*
|
|
287
|
+
* Pure function — no I/O, no side effects. */
|
|
288
|
+
export function extractTitle(observation) {
|
|
289
|
+
const trimmed = observation.trim();
|
|
290
|
+
if (trimmed.length === 0)
|
|
291
|
+
return { title: '', content: '' };
|
|
292
|
+
// Find first sentence boundary.
|
|
293
|
+
// The lookbehind (?<=\w{2}) ensures we don't split on abbreviations like "e.g." or "U.S."
|
|
294
|
+
// where a period follows a single character. Newlines always end a sentence.
|
|
295
|
+
const sentenceMatch = trimmed.match(/(?<=\w{2})[.!?](?:\s|$)|\n/);
|
|
296
|
+
const sentenceEnd = sentenceMatch?.index ?? -1;
|
|
297
|
+
const firstSentence = sentenceEnd >= 0
|
|
298
|
+
? trimmed.slice(0, sentenceEnd + 1).trim()
|
|
299
|
+
: trimmed;
|
|
300
|
+
// Truncate if too long
|
|
301
|
+
const title = firstSentence.length <= MAX_TITLE_LENGTH
|
|
302
|
+
? firstSentence
|
|
303
|
+
: firstSentence.slice(0, MAX_TITLE_LENGTH - 3).trimEnd() + '...';
|
|
304
|
+
return { title, content: trimmed };
|
|
305
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type EphemeralSeverity = 'high' | 'medium' | 'low';
|
|
|
7
7
|
/** Parse a raw string into a TrustLevel, returning null for invalid input */
|
|
8
8
|
export declare function parseTrustLevel(raw: string): TrustLevel | null;
|
|
9
9
|
/** Predefined topic scopes for organizing knowledge */
|
|
10
|
-
export type TopicScope = 'user' | 'preferences' | 'architecture' | 'conventions' | 'gotchas' | 'recent-work' | `modules/${string}`;
|
|
10
|
+
export type TopicScope = 'user' | 'preferences' | 'architecture' | 'conventions' | 'gotchas' | 'general' | 'recent-work' | `modules/${string}`;
|
|
11
11
|
/** Validated tag: lowercase alphanumeric slug (letters, digits, hyphens).
|
|
12
12
|
* Branded type prevents accidentally passing raw strings where validated tags are expected. */
|
|
13
13
|
export type Tag = string & {
|
package/dist/types.js
CHANGED
|
@@ -9,7 +9,7 @@ const TRUST_LEVELS = ['user', 'agent-confirmed', 'agent-inferred'];
|
|
|
9
9
|
export function parseTrustLevel(raw) {
|
|
10
10
|
return TRUST_LEVELS.includes(raw) ? raw : null;
|
|
11
11
|
}
|
|
12
|
-
const FIXED_TOPICS = ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'];
|
|
12
|
+
const FIXED_TOPICS = ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'general', 'recent-work'];
|
|
13
13
|
/** Construct an EmbeddingVector from a Float32Array. Boundary validation only —
|
|
14
14
|
* callers (OllamaEmbedder, FakeEmbedder, vector deserialization) validate dimensions. */
|
|
15
15
|
export function asEmbeddingVector(raw) {
|