@exaudeus/memory-mcp 1.8.0 → 1.9.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.
- package/dist/formatters.d.ts +5 -1
- package/dist/formatters.js +9 -0
- package/dist/index.js +486 -118
- 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. Returns user identity, preferences, gotchas overview, stale entries. Call once at the beginning of a conversation. Example: brief(lobe: "android")',
|
|
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: 'Pre-task lookup. Describe what you are about to do and get relevant knowledge (semantic + keyword search). Example: recall(lobe: "android", context: "writing a Kotlin reducer for the messaging feature")',
|
|
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: '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: 'Get stored gotchas for a codebase area. Example: gotchas(lobe: "android", area: "auth")',
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: {
|
|
253
|
+
lobe: lobeProperty,
|
|
254
|
+
area: {
|
|
255
|
+
type: 'string',
|
|
256
|
+
description: 'Optional keyword filter for a specific area (e.g. "auth", "build", "navigation").',
|
|
262
257
|
},
|
|
263
|
-
|
|
258
|
+
},
|
|
259
|
+
required: [],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'conventions',
|
|
264
|
+
description: 'Get stored conventions for a codebase area. Example: conventions(lobe: "android", area: "testing")',
|
|
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 for a specific area (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: 'Store a gotcha — a pitfall, surprising behavior, or trap. Write naturally; title is auto-extracted. Example: gotcha(lobe: "android", 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: 'Store a convention — a pattern, rule, or standard the codebase follows. Example: convention(lobe: "android", 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
|
-
|
|
306
|
+
observation: {
|
|
293
307
|
type: 'string',
|
|
294
|
-
description: '
|
|
308
|
+
description: 'The convention — write naturally. First sentence becomes the title.',
|
|
295
309
|
},
|
|
296
|
-
|
|
297
|
-
type: 'string',
|
|
298
|
-
enum: ['brief', 'standard', 'full'],
|
|
299
|
-
description: 'brief = titles only, standard = summaries, full = complete content + metadata',
|
|
300
|
-
default: 'brief',
|
|
301
|
-
},
|
|
302
|
-
filter: {
|
|
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 a general observation — architecture decisions, dependency info, or any insight. Example: learn(lobe: "android", observation: "The messaging feature uses MVVM with a FlowCoordinator for navigation state")',
|
|
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: 'Store a user preference or working style rule. Stored with high trust. Example: prefer(rule: "Always suggest the simplest solution first")',
|
|
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. Lobe to scope this preference to. Omit for global preferences. 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: 'Fix or delete an entry. With correction: replaces content. Without: deletes. Example: fix(id: "gotcha-3f7a", correction: "updated text") or fix(id: "gotcha-3f7a")',
|
|
346
363
|
inputSchema: {
|
|
347
364
|
type: 'object',
|
|
348
365
|
properties: {
|
|
349
|
-
|
|
350
|
-
context: {
|
|
366
|
+
id: {
|
|
351
367
|
type: 'string',
|
|
352
|
-
description: '
|
|
368
|
+
description: 'Entry ID (e.g. gotcha-3f7a, arch-5c9b).',
|
|
353
369
|
},
|
|
354
|
-
|
|
355
|
-
type: '
|
|
356
|
-
description: '
|
|
357
|
-
default: 10,
|
|
358
|
-
},
|
|
359
|
-
minMatch: {
|
|
360
|
-
type: 'number',
|
|
361
|
-
description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
|
|
362
|
-
default: 0.2,
|
|
370
|
+
correction: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
description: 'New text. Omit 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
|
}
|
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) {
|