context-vault 3.8.0 → 3.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/assets/agent-rules.md +28 -1
- package/assets/setup-prompt.md +16 -1
- package/bin/cli.js +876 -4
- package/dist/auto-memory.d.ts +52 -0
- package/dist/auto-memory.d.ts.map +1 -0
- package/dist/auto-memory.js +142 -0
- package/dist/auto-memory.js.map +1 -0
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +2 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/remote.d.ts +134 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +242 -0
- package/dist/remote.js.map +1 -0
- package/dist/remote.test.d.ts +2 -0
- package/dist/remote.test.d.ts.map +1 -0
- package/dist/remote.test.js +107 -0
- package/dist/remote.test.js.map +1 -0
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +19 -0
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +44 -0
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/publish-to-team.d.ts +11 -0
- package/dist/tools/publish-to-team.d.ts.map +1 -0
- package/dist/tools/publish-to-team.js +91 -0
- package/dist/tools/publish-to-team.js.map +1 -0
- package/dist/tools/publish-to-team.test.d.ts +2 -0
- package/dist/tools/publish-to-team.test.d.ts.map +1 -0
- package/dist/tools/publish-to-team.test.js +95 -0
- package/dist/tools/publish-to-team.test.js.map +1 -0
- package/dist/tools/recall.d.ts +1 -1
- package/dist/tools/recall.d.ts.map +1 -1
- package/dist/tools/recall.js +85 -1
- package/dist/tools/recall.js.map +1 -1
- package/dist/tools/save-context.d.ts +5 -1
- package/dist/tools/save-context.d.ts.map +1 -1
- package/dist/tools/save-context.js +163 -2
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +90 -86
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts +3 -1
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +48 -2
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +7 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/config.ts +50 -3
- package/node_modules/@context-vault/core/src/main.ts +1 -0
- package/node_modules/@context-vault/core/src/types.ts +8 -0
- package/package.json +2 -2
- package/src/auto-memory.ts +169 -0
- package/src/register-tools.ts +2 -0
- package/src/remote.test.ts +123 -0
- package/src/remote.ts +325 -0
- package/src/tools/context-status.ts +19 -0
- package/src/tools/get-context.ts +44 -0
- package/src/tools/publish-to-team.test.ts +115 -0
- package/src/tools/publish-to-team.ts +112 -0
- package/src/tools/recall.ts +79 -1
- package/src/tools/save-context.ts +167 -1
- package/src/tools/session-start.ts +88 -100
package/src/tools/recall.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ok } from '../helpers.js';
|
|
3
3
|
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
4
|
+
import { getAutoMemory, findAutoMemoryOverlaps } from '../auto-memory.js';
|
|
5
|
+
import { getRemoteClient, getTeamId } from '../remote.js';
|
|
4
6
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
5
7
|
|
|
6
8
|
const SEMANTIC_SIMILARITY_THRESHOLD = 0.6;
|
|
@@ -173,6 +175,58 @@ export async function handler(
|
|
|
173
175
|
if (sessionSet) sessionSet.add(row.id);
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
// Remote recall: merge hints from hosted API
|
|
179
|
+
const remoteClient = getRemoteClient(ctx.config);
|
|
180
|
+
if (remoteClient && hints.length < limit) {
|
|
181
|
+
try {
|
|
182
|
+
const remoteHints = await remoteClient.recall({
|
|
183
|
+
signal,
|
|
184
|
+
signal_type,
|
|
185
|
+
bucket,
|
|
186
|
+
max_hints: limit - hints.length,
|
|
187
|
+
});
|
|
188
|
+
const localIds = new Set(hints.map(h => h.id));
|
|
189
|
+
for (const rh of remoteHints) {
|
|
190
|
+
if (hints.length >= limit) break;
|
|
191
|
+
if (localIds.has(rh.id)) continue;
|
|
192
|
+
if (sessionSet && !bypassDedup && sessionSet.has(rh.id)) {
|
|
193
|
+
suppressed++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
hints.push(rh);
|
|
197
|
+
if (sessionSet) sessionSet.add(rh.id);
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.warn(`[context-vault] Remote recall failed: ${(e as Error).message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Team vault recall: include team results if teamId is configured
|
|
205
|
+
const teamId = getTeamId(ctx.config);
|
|
206
|
+
if (remoteClient && teamId && hints.length < limit) {
|
|
207
|
+
try {
|
|
208
|
+
const teamHints = await remoteClient.teamRecall(teamId, {
|
|
209
|
+
signal,
|
|
210
|
+
signal_type,
|
|
211
|
+
bucket,
|
|
212
|
+
max_hints: limit - hints.length,
|
|
213
|
+
});
|
|
214
|
+
const existingIds = new Set(hints.map(h => h.id));
|
|
215
|
+
for (const th of teamHints) {
|
|
216
|
+
if (hints.length >= limit) break;
|
|
217
|
+
if (existingIds.has(th.id)) continue;
|
|
218
|
+
if (sessionSet && !bypassDedup && sessionSet.has(th.id)) {
|
|
219
|
+
suppressed++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
hints.push({ ...th, tags: [...(th.tags || []), '[team]'] });
|
|
223
|
+
if (sessionSet) sessionSet.add(th.id);
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.warn(`[context-vault] Team recall failed: ${(e as Error).message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
176
230
|
let method: 'tag_match' | 'semantic' | 'none' = hints.length > 0 ? 'tag_match' : 'none';
|
|
177
231
|
|
|
178
232
|
// Semantic fallback: when fast-path returns 0 results and signal is not file-based
|
|
@@ -265,10 +319,33 @@ export async function handler(
|
|
|
265
319
|
recordCoRetrieval(ctx, hints.map((h) => h.id));
|
|
266
320
|
}
|
|
267
321
|
|
|
322
|
+
// Check for auto-memory overlap to avoid redundant surfacing
|
|
323
|
+
let autoMemoryOverlaps: Array<{ hint_id: string; memory_file: string; memory_name: string }> = [];
|
|
324
|
+
try {
|
|
325
|
+
const autoMemory = getAutoMemory();
|
|
326
|
+
if (autoMemory.detected && autoMemory.entries.length > 0) {
|
|
327
|
+
for (const h of hints) {
|
|
328
|
+
const searchText = [h.title, h.summary].filter(Boolean).join(' ');
|
|
329
|
+
const overlaps = findAutoMemoryOverlaps(autoMemory, searchText, 0.3);
|
|
330
|
+
if (overlaps.length > 0) {
|
|
331
|
+
autoMemoryOverlaps.push({
|
|
332
|
+
hint_id: h.id,
|
|
333
|
+
memory_file: overlaps[0].file,
|
|
334
|
+
memory_name: overlaps[0].name,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Non-fatal
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
// Format output
|
|
269
344
|
const lines = [`[Vault: ${hints.length} ${hints.length === 1 ? 'entry' : 'entries'} may be relevant]`];
|
|
270
345
|
for (const h of hints) {
|
|
271
|
-
|
|
346
|
+
const overlap = autoMemoryOverlaps.find(o => o.hint_id === h.id);
|
|
347
|
+
const overlapNote = overlap ? ` [also in auto-memory: ${overlap.memory_name}]` : '';
|
|
348
|
+
lines.push(`- "${h.title}" (${h.kind}, ${h.relevance})${overlapNote}`);
|
|
272
349
|
}
|
|
273
350
|
lines.push('Use get_context to retrieve full details.');
|
|
274
351
|
|
|
@@ -279,6 +356,7 @@ export async function handler(
|
|
|
279
356
|
signal_keywords: keywords,
|
|
280
357
|
suppressed,
|
|
281
358
|
hints,
|
|
359
|
+
auto_memory_overlaps: autoMemoryOverlaps.length > 0 ? autoMemoryOverlaps : undefined,
|
|
282
360
|
};
|
|
283
361
|
return result;
|
|
284
362
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { resolve, basename, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
|
|
5
|
+
import { captureAndIndex, updateEntryFile, writeEntry } from '@context-vault/core/capture';
|
|
6
6
|
import { indexEntry } from '@context-vault/core/index';
|
|
7
7
|
import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
|
|
8
8
|
import { normalizeKind, kindToPath } from '@context-vault/core/files';
|
|
@@ -11,6 +11,8 @@ import { shouldIndex } from '@context-vault/core/indexing';
|
|
|
11
11
|
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
|
|
12
12
|
import { maybeShowFeedbackPrompt } from '../telemetry.js';
|
|
13
13
|
import { validateRelatedTo } from '../linking.js';
|
|
14
|
+
import { getAutoMemory, findAutoMemoryOverlaps } from '../auto-memory.js';
|
|
15
|
+
import { getRemoteClient, getTeamId } from '../remote.js';
|
|
14
16
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
15
17
|
import {
|
|
16
18
|
MAX_BODY_LENGTH,
|
|
@@ -39,6 +41,17 @@ function isDualWriteEnabled(config: { dataDir: string }): boolean {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function isDeferredSyncEnabled(config: { dataDir: string }): boolean {
|
|
45
|
+
try {
|
|
46
|
+
const configPath = join(config.dataDir, 'config.json');
|
|
47
|
+
if (!existsSync(configPath)) return false;
|
|
48
|
+
const fc = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
49
|
+
return fc.dualWrite?.deferredSync === true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
function dualWriteLocal(entryFilePath: string, kind: string): void {
|
|
43
56
|
const cwd = process.cwd();
|
|
44
57
|
const home = homedir();
|
|
@@ -375,6 +388,12 @@ export const inputSchema = {
|
|
|
375
388
|
.describe(
|
|
376
389
|
'Whether to index this entry for search (generate embeddings + FTS). Default: auto-determined by indexing config. Set to false to store file + metadata only, skipping embedding generation. Set to true to force indexing regardless of config rules.'
|
|
377
390
|
),
|
|
391
|
+
visibility: z
|
|
392
|
+
.enum(['personal', 'team'])
|
|
393
|
+
.optional()
|
|
394
|
+
.describe(
|
|
395
|
+
'Where to save: "personal" (default) saves to local + personal remote. "team" publishes to the team vault instead via POST /api/vault/publish. Requires teamId in remote config or a prior `context-vault team join`.'
|
|
396
|
+
),
|
|
378
397
|
};
|
|
379
398
|
|
|
380
399
|
export async function handler(
|
|
@@ -398,6 +417,7 @@ export async function handler(
|
|
|
398
417
|
conflict_resolution,
|
|
399
418
|
encoding_context,
|
|
400
419
|
indexed,
|
|
420
|
+
visibility,
|
|
401
421
|
}: Record<string, any>,
|
|
402
422
|
ctx: LocalCtx,
|
|
403
423
|
{ ensureIndexed }: SharedCtx
|
|
@@ -511,6 +531,22 @@ export async function handler(
|
|
|
511
531
|
dualWriteLocal(entry.filePath, entry.kind);
|
|
512
532
|
}
|
|
513
533
|
|
|
534
|
+
// Remote sync for updates: fire-and-forget PUT to hosted API
|
|
535
|
+
const updateRemoteClient = getRemoteClient(config);
|
|
536
|
+
if (updateRemoteClient) {
|
|
537
|
+
updateRemoteClient.saveEntry({
|
|
538
|
+
id: entry.id,
|
|
539
|
+
kind: entry.kind,
|
|
540
|
+
title: entry.title,
|
|
541
|
+
body: entry.body,
|
|
542
|
+
tags: entry.tags,
|
|
543
|
+
meta: entry.meta,
|
|
544
|
+
source: entry.source,
|
|
545
|
+
}).catch((e: Error) => {
|
|
546
|
+
console.warn(`[context-vault] Remote sync (update) failed: ${e.message}`);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
514
550
|
const relPath = entry.filePath
|
|
515
551
|
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
516
552
|
: entry.filePath;
|
|
@@ -535,6 +571,68 @@ export async function handler(
|
|
|
535
571
|
return err(`Entity kind "${normalizedKind}" requires identity_key`, 'MISSING_IDENTITY_KEY');
|
|
536
572
|
}
|
|
537
573
|
|
|
574
|
+
// ── Deferred sync: file-only write, skip DB entirely ──────────────────
|
|
575
|
+
if (isDeferredSyncEnabled(config)) {
|
|
576
|
+
const category = categoryFor(normalizedKind);
|
|
577
|
+
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
578
|
+
const mergedMeta = { ...(meta || {}) };
|
|
579
|
+
if (folder) mergedMeta.folder = folder;
|
|
580
|
+
const parsedCtx = parseContextParam(encoding_context);
|
|
581
|
+
if (parsedCtx?.structured) {
|
|
582
|
+
mergedMeta.encoding_context = parsedCtx.structured;
|
|
583
|
+
} else if (parsedCtx?.text) {
|
|
584
|
+
mergedMeta.encoding_context = parsedCtx.text;
|
|
585
|
+
}
|
|
586
|
+
if (normalizedKind === 'decision') {
|
|
587
|
+
enrichDecisionMeta(mergedMeta, title, body);
|
|
588
|
+
}
|
|
589
|
+
const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
|
|
590
|
+
|
|
591
|
+
let entry;
|
|
592
|
+
try {
|
|
593
|
+
entry = writeEntry(ctx, {
|
|
594
|
+
kind: normalizedKind,
|
|
595
|
+
title,
|
|
596
|
+
body,
|
|
597
|
+
meta: finalMeta,
|
|
598
|
+
tags,
|
|
599
|
+
source,
|
|
600
|
+
folder,
|
|
601
|
+
identity_key,
|
|
602
|
+
expires_at,
|
|
603
|
+
supersedes,
|
|
604
|
+
related_to,
|
|
605
|
+
source_files,
|
|
606
|
+
tier: effectiveTier,
|
|
607
|
+
indexed: false,
|
|
608
|
+
});
|
|
609
|
+
} catch (e) {
|
|
610
|
+
return errWithHint(
|
|
611
|
+
e instanceof Error ? e.message : String(e),
|
|
612
|
+
'SAVE_FAILED',
|
|
613
|
+
'context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (isDualWriteEnabled(config) && entry.filePath) {
|
|
618
|
+
dualWriteLocal(entry.filePath, normalizedKind);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const relPath = entry.filePath
|
|
622
|
+
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
623
|
+
: entry.filePath;
|
|
624
|
+
const icon = kindIcon(normalizedKind);
|
|
625
|
+
const parts = [
|
|
626
|
+
`## ✓ Saved (deferred)`,
|
|
627
|
+
`${icon} **${title || '(untitled)'}**`,
|
|
628
|
+
`\`${normalizedKind}\` · **${effectiveTier}**${tags?.length ? ` · ${tags.join(', ')}` : ''}`,
|
|
629
|
+
`\`${entry.id}\` → ${relPath}`,
|
|
630
|
+
'',
|
|
631
|
+
'_Deferred sync: file written, not yet indexed. Run `vault sync` to index._',
|
|
632
|
+
];
|
|
633
|
+
return ok(parts.join('\n'));
|
|
634
|
+
}
|
|
635
|
+
|
|
538
636
|
// Start reindex in background but don't wait — similarity check
|
|
539
637
|
// may miss unindexed entries, but the save won't time out
|
|
540
638
|
await ensureIndexed({ blocking: false });
|
|
@@ -680,6 +778,56 @@ export async function handler(
|
|
|
680
778
|
dualWriteLocal(entry.filePath, normalizedKind);
|
|
681
779
|
}
|
|
682
780
|
|
|
781
|
+
// Remote sync: fire-and-forget POST to hosted API
|
|
782
|
+
const remoteClient = getRemoteClient(config);
|
|
783
|
+
if (visibility === 'team') {
|
|
784
|
+
const effectiveTeamId = getTeamId(config);
|
|
785
|
+
if (!remoteClient) {
|
|
786
|
+
console.warn('[context-vault] Team publish skipped: remote not configured');
|
|
787
|
+
} else if (!effectiveTeamId) {
|
|
788
|
+
console.warn('[context-vault] Team publish skipped: no teamId configured');
|
|
789
|
+
} else if (category === 'event') {
|
|
790
|
+
console.warn('[context-vault] Team publish skipped: events are private');
|
|
791
|
+
} else {
|
|
792
|
+
remoteClient.publishToTeam({
|
|
793
|
+
entryId: entry.id,
|
|
794
|
+
teamId: effectiveTeamId,
|
|
795
|
+
visibility: 'team',
|
|
796
|
+
entry: {
|
|
797
|
+
kind: normalizedKind,
|
|
798
|
+
title,
|
|
799
|
+
body,
|
|
800
|
+
tags,
|
|
801
|
+
meta: finalMeta,
|
|
802
|
+
source,
|
|
803
|
+
identity_key,
|
|
804
|
+
tier: effectiveTier,
|
|
805
|
+
category,
|
|
806
|
+
},
|
|
807
|
+
}).catch((e: Error) => {
|
|
808
|
+
console.warn(`[context-vault] Team publish failed: ${e.message}`);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
} else if (remoteClient) {
|
|
812
|
+
remoteClient.saveEntry({
|
|
813
|
+
id: entry.id,
|
|
814
|
+
kind: normalizedKind,
|
|
815
|
+
title,
|
|
816
|
+
body,
|
|
817
|
+
tags,
|
|
818
|
+
meta: finalMeta,
|
|
819
|
+
source,
|
|
820
|
+
identity_key,
|
|
821
|
+
expires_at,
|
|
822
|
+
supersedes,
|
|
823
|
+
related_to,
|
|
824
|
+
source_files,
|
|
825
|
+
tier: effectiveTier,
|
|
826
|
+
}).catch((e: Error) => {
|
|
827
|
+
console.warn(`[context-vault] Remote sync failed: ${e.message}`);
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
683
831
|
if (ctx.config?.dataDir) {
|
|
684
832
|
maybeShowFeedbackPrompt(ctx.config.dataDir);
|
|
685
833
|
}
|
|
@@ -750,6 +898,24 @@ export async function handler(
|
|
|
750
898
|
}
|
|
751
899
|
}
|
|
752
900
|
|
|
901
|
+
// Auto-memory overlap detection (advisory)
|
|
902
|
+
try {
|
|
903
|
+
const autoMemory = getAutoMemory();
|
|
904
|
+
if (autoMemory.detected && autoMemory.entries.length > 0) {
|
|
905
|
+
const searchText = [title, body].filter(Boolean).join(' ');
|
|
906
|
+
const overlaps = findAutoMemoryOverlaps(autoMemory, searchText, 0.3);
|
|
907
|
+
if (overlaps.length > 0) {
|
|
908
|
+
const top = overlaps[0];
|
|
909
|
+
parts.push('');
|
|
910
|
+
parts.push(`### Auto-Memory Overlap`);
|
|
911
|
+
parts.push(`Similar content found in auto-memory: **${top.name}** (\`${top.file}\`, ${top.type} type, ${(top.similarity * 100).toFixed(0)}% overlap)`);
|
|
912
|
+
parts.push(`_This knowledge already exists in your auto-memory. Consider whether vault storage adds cross-project value._`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
} catch {
|
|
916
|
+
// Non-fatal: auto-memory overlap check should not block save
|
|
917
|
+
}
|
|
918
|
+
|
|
753
919
|
const criticalLimit = config.thresholds?.totalEntries?.critical;
|
|
754
920
|
if (criticalLimit != null) {
|
|
755
921
|
try {
|
|
@@ -4,6 +4,9 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
|
|
7
|
+
import { getAutoMemory } from '../auto-memory.js';
|
|
8
|
+
import { getRemoteClient, getTeamId } from '../remote.js';
|
|
9
|
+
import type { AutoMemoryEntry, AutoMemoryResult } from '../auto-memory.js';
|
|
7
10
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
8
11
|
|
|
9
12
|
const DEFAULT_MAX_TOKENS = 4000;
|
|
@@ -12,99 +15,6 @@ const MAX_BODY_PER_ENTRY = 400;
|
|
|
12
15
|
const PRIORITY_KINDS = ['decision', 'insight', 'pattern'];
|
|
13
16
|
const SESSION_SUMMARY_KIND = 'session';
|
|
14
17
|
|
|
15
|
-
interface AutoMemoryEntry {
|
|
16
|
-
file: string;
|
|
17
|
-
name: string;
|
|
18
|
-
description: string;
|
|
19
|
-
type: string;
|
|
20
|
-
body: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface AutoMemoryResult {
|
|
24
|
-
detected: boolean;
|
|
25
|
-
path: string | null;
|
|
26
|
-
entries: AutoMemoryEntry[];
|
|
27
|
-
linesUsed: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Detect the Claude Code auto-memory directory for the current project.
|
|
32
|
-
* Convention: ~/.claude/projects/-<cwd-with-slashes-replaced-by-dashes>/memory/
|
|
33
|
-
*/
|
|
34
|
-
function detectAutoMemoryPath(): string | null {
|
|
35
|
-
try {
|
|
36
|
-
const cwd = process.cwd();
|
|
37
|
-
// Claude Code project key: absolute path with / replaced by -, leading - kept
|
|
38
|
-
const projectKey = cwd.replace(/\//g, '-');
|
|
39
|
-
const memoryDir = join(homedir(), '.claude', 'projects', projectKey, 'memory');
|
|
40
|
-
const memoryIndex = join(memoryDir, 'MEMORY.md');
|
|
41
|
-
if (existsSync(memoryIndex)) return memoryDir;
|
|
42
|
-
} catch {}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Parse YAML-ish frontmatter from a memory file.
|
|
48
|
-
* Returns { name, description, type } and the body after frontmatter.
|
|
49
|
-
*/
|
|
50
|
-
function parseMemoryFile(content: string): { name: string; description: string; type: string; body: string } {
|
|
51
|
-
const result = { name: '', description: '', type: '', body: content };
|
|
52
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
53
|
-
if (!fmMatch) return result;
|
|
54
|
-
|
|
55
|
-
const frontmatter = fmMatch[1];
|
|
56
|
-
result.body = fmMatch[2].trim();
|
|
57
|
-
|
|
58
|
-
for (const line of frontmatter.split('\n')) {
|
|
59
|
-
const kv = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
60
|
-
if (!kv) continue;
|
|
61
|
-
const [, key, val] = kv;
|
|
62
|
-
if (key === 'name') result.name = val.trim();
|
|
63
|
-
else if (key === 'description') result.description = val.trim();
|
|
64
|
-
else if (key === 'type') result.type = val.trim();
|
|
65
|
-
}
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Read and parse all auto-memory entries from a memory directory.
|
|
71
|
-
*/
|
|
72
|
-
function readAutoMemory(memoryDir: string): AutoMemoryResult {
|
|
73
|
-
const indexPath = join(memoryDir, 'MEMORY.md');
|
|
74
|
-
let linesUsed = 0;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const indexContent = readFileSync(indexPath, 'utf-8');
|
|
78
|
-
linesUsed = indexContent.split('\n').length;
|
|
79
|
-
} catch {
|
|
80
|
-
return { detected: true, path: memoryDir, entries: [], linesUsed: 0 };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const entries: AutoMemoryEntry[] = [];
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const files = readdirSync(memoryDir).filter(
|
|
87
|
-
(f) => f.endsWith('.md') && f !== 'MEMORY.md'
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
for (const file of files) {
|
|
91
|
-
try {
|
|
92
|
-
const content = readFileSync(join(memoryDir, file), 'utf-8');
|
|
93
|
-
const parsed = parseMemoryFile(content);
|
|
94
|
-
entries.push({
|
|
95
|
-
file,
|
|
96
|
-
name: parsed.name || file.replace('.md', ''),
|
|
97
|
-
description: parsed.description,
|
|
98
|
-
type: parsed.type,
|
|
99
|
-
body: parsed.body,
|
|
100
|
-
});
|
|
101
|
-
} catch {}
|
|
102
|
-
}
|
|
103
|
-
} catch {}
|
|
104
|
-
|
|
105
|
-
return { detected: true, path: memoryDir, entries, linesUsed };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
18
|
/**
|
|
109
19
|
* Build a search context string from auto-memory entries.
|
|
110
20
|
* Used to boost vault retrieval relevance.
|
|
@@ -241,12 +151,7 @@ export async function handler(
|
|
|
241
151
|
const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
|
|
242
152
|
|
|
243
153
|
// Auto-detect Claude Code auto-memory (explicit path overrides auto-detection)
|
|
244
|
-
const
|
|
245
|
-
? (existsSync(join(auto_memory_path.trim(), 'MEMORY.md')) ? auto_memory_path.trim() : null)
|
|
246
|
-
: detectAutoMemoryPath();
|
|
247
|
-
const autoMemory: AutoMemoryResult = resolvedMemoryPath
|
|
248
|
-
? readAutoMemory(resolvedMemoryPath)
|
|
249
|
-
: { detected: false, path: null, entries: [], linesUsed: 0 };
|
|
154
|
+
const autoMemory: AutoMemoryResult = getAutoMemory(auto_memory_path);
|
|
250
155
|
const autoMemoryContext = buildAutoMemoryContext(autoMemory.entries);
|
|
251
156
|
const topicsExtracted = autoMemory.entries.length > 0
|
|
252
157
|
? extractKeywords(autoMemoryContext).slice(0, 20)
|
|
@@ -328,12 +233,95 @@ export async function handler(
|
|
|
328
233
|
}
|
|
329
234
|
}
|
|
330
235
|
|
|
236
|
+
// Remote entries: pull recent from hosted API if configured
|
|
237
|
+
const remoteClient = getRemoteClient(ctx.config);
|
|
238
|
+
let remoteCount = 0;
|
|
239
|
+
if (remoteClient && tokensUsed < tokenBudget) {
|
|
240
|
+
try {
|
|
241
|
+
const seenIds = new Set([
|
|
242
|
+
...decisions.map((d: any) => d.id),
|
|
243
|
+
...deduped.map((d: any) => d.id),
|
|
244
|
+
...(lastSession ? [lastSession.id] : []),
|
|
245
|
+
]);
|
|
246
|
+
const remoteTags = effectiveTags.length ? effectiveTags : undefined;
|
|
247
|
+
const remoteResults = await remoteClient.search({
|
|
248
|
+
tags: remoteTags,
|
|
249
|
+
limit: 10,
|
|
250
|
+
since: sinceDate,
|
|
251
|
+
});
|
|
252
|
+
const uniqueRemote = remoteResults.filter((r: any) => !seenIds.has(r.id));
|
|
253
|
+
if (uniqueRemote.length > 0) {
|
|
254
|
+
const header = '## Remote Entries\n';
|
|
255
|
+
const headerTokens = estimateTokens(header);
|
|
256
|
+
if (tokensUsed + headerTokens <= tokenBudget) {
|
|
257
|
+
const entryLines: string[] = [];
|
|
258
|
+
tokensUsed += headerTokens;
|
|
259
|
+
for (const entry of uniqueRemote) {
|
|
260
|
+
const line = formatEntry(entry);
|
|
261
|
+
const lineTokens = estimateTokens(line);
|
|
262
|
+
if (tokensUsed + lineTokens > tokenBudget) break;
|
|
263
|
+
entryLines.push(line);
|
|
264
|
+
tokensUsed += lineTokens;
|
|
265
|
+
remoteCount++;
|
|
266
|
+
}
|
|
267
|
+
if (entryLines.length > 0) {
|
|
268
|
+
sections.push(header + entryLines.join('\n') + '\n');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.warn(`[context-vault] Remote session_start failed: ${(e as Error).message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Team vault entries: include team knowledge in brief if teamId is configured
|
|
278
|
+
let teamCount = 0;
|
|
279
|
+
const teamId = getTeamId(ctx.config);
|
|
280
|
+
if (remoteClient && teamId && tokensUsed < tokenBudget) {
|
|
281
|
+
try {
|
|
282
|
+
const allSeenIds = new Set([
|
|
283
|
+
...decisions.map((d: any) => d.id),
|
|
284
|
+
...deduped.map((d: any) => d.id),
|
|
285
|
+
...(lastSession ? [lastSession.id] : []),
|
|
286
|
+
]);
|
|
287
|
+
const teamResults = await remoteClient.teamSearch(teamId, {
|
|
288
|
+
tags: effectiveTags.length ? effectiveTags : undefined,
|
|
289
|
+
limit: 10,
|
|
290
|
+
since: sinceDate,
|
|
291
|
+
});
|
|
292
|
+
const uniqueTeam = teamResults.filter((r: any) => !allSeenIds.has(r.id));
|
|
293
|
+
if (uniqueTeam.length > 0) {
|
|
294
|
+
const header = '## Team Knowledge\n';
|
|
295
|
+
const headerTokens = estimateTokens(header);
|
|
296
|
+
if (tokensUsed + headerTokens <= tokenBudget) {
|
|
297
|
+
const entryLines: string[] = [];
|
|
298
|
+
tokensUsed += headerTokens;
|
|
299
|
+
for (const entry of uniqueTeam) {
|
|
300
|
+
const line = formatEntry(entry) + ' `[team]`';
|
|
301
|
+
const lineTokens = estimateTokens(line);
|
|
302
|
+
if (tokensUsed + lineTokens > tokenBudget) break;
|
|
303
|
+
entryLines.push(line);
|
|
304
|
+
tokensUsed += lineTokens;
|
|
305
|
+
teamCount++;
|
|
306
|
+
}
|
|
307
|
+
if (entryLines.length > 0) {
|
|
308
|
+
sections.push(header + entryLines.join('\n') + '\n');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.warn(`[context-vault] Team session_start failed: ${(e as Error).message}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
331
317
|
const totalEntries =
|
|
332
318
|
(lastSession ? 1 : 0) +
|
|
333
319
|
decisions.length +
|
|
334
320
|
deduped.filter((_d: any) => {
|
|
335
321
|
return true;
|
|
336
|
-
}).length
|
|
322
|
+
}).length +
|
|
323
|
+
remoteCount +
|
|
324
|
+
teamCount;
|
|
337
325
|
|
|
338
326
|
if (indexWarning) {
|
|
339
327
|
sections.push(indexWarning);
|