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.
Files changed (68) hide show
  1. package/assets/agent-rules.md +28 -1
  2. package/assets/setup-prompt.md +16 -1
  3. package/bin/cli.js +876 -4
  4. package/dist/auto-memory.d.ts +52 -0
  5. package/dist/auto-memory.d.ts.map +1 -0
  6. package/dist/auto-memory.js +142 -0
  7. package/dist/auto-memory.js.map +1 -0
  8. package/dist/register-tools.d.ts.map +1 -1
  9. package/dist/register-tools.js +2 -0
  10. package/dist/register-tools.js.map +1 -1
  11. package/dist/remote.d.ts +134 -0
  12. package/dist/remote.d.ts.map +1 -0
  13. package/dist/remote.js +242 -0
  14. package/dist/remote.js.map +1 -0
  15. package/dist/remote.test.d.ts +2 -0
  16. package/dist/remote.test.d.ts.map +1 -0
  17. package/dist/remote.test.js +107 -0
  18. package/dist/remote.test.js.map +1 -0
  19. package/dist/tools/context-status.d.ts.map +1 -1
  20. package/dist/tools/context-status.js +19 -0
  21. package/dist/tools/context-status.js.map +1 -1
  22. package/dist/tools/get-context.d.ts.map +1 -1
  23. package/dist/tools/get-context.js +44 -0
  24. package/dist/tools/get-context.js.map +1 -1
  25. package/dist/tools/publish-to-team.d.ts +11 -0
  26. package/dist/tools/publish-to-team.d.ts.map +1 -0
  27. package/dist/tools/publish-to-team.js +91 -0
  28. package/dist/tools/publish-to-team.js.map +1 -0
  29. package/dist/tools/publish-to-team.test.d.ts +2 -0
  30. package/dist/tools/publish-to-team.test.d.ts.map +1 -0
  31. package/dist/tools/publish-to-team.test.js +95 -0
  32. package/dist/tools/publish-to-team.test.js.map +1 -0
  33. package/dist/tools/recall.d.ts +1 -1
  34. package/dist/tools/recall.d.ts.map +1 -1
  35. package/dist/tools/recall.js +85 -1
  36. package/dist/tools/recall.js.map +1 -1
  37. package/dist/tools/save-context.d.ts +5 -1
  38. package/dist/tools/save-context.d.ts.map +1 -1
  39. package/dist/tools/save-context.js +163 -2
  40. package/dist/tools/save-context.js.map +1 -1
  41. package/dist/tools/session-start.d.ts.map +1 -1
  42. package/dist/tools/session-start.js +90 -86
  43. package/dist/tools/session-start.js.map +1 -1
  44. package/node_modules/@context-vault/core/dist/config.d.ts +3 -1
  45. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  46. package/node_modules/@context-vault/core/dist/config.js +48 -2
  47. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  48. package/node_modules/@context-vault/core/dist/main.d.ts +1 -1
  49. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  50. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  51. package/node_modules/@context-vault/core/dist/types.d.ts +7 -0
  52. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  53. package/node_modules/@context-vault/core/package.json +1 -1
  54. package/node_modules/@context-vault/core/src/config.ts +50 -3
  55. package/node_modules/@context-vault/core/src/main.ts +1 -0
  56. package/node_modules/@context-vault/core/src/types.ts +8 -0
  57. package/package.json +2 -2
  58. package/src/auto-memory.ts +169 -0
  59. package/src/register-tools.ts +2 -0
  60. package/src/remote.test.ts +123 -0
  61. package/src/remote.ts +325 -0
  62. package/src/tools/context-status.ts +19 -0
  63. package/src/tools/get-context.ts +44 -0
  64. package/src/tools/publish-to-team.test.ts +115 -0
  65. package/src/tools/publish-to-team.ts +112 -0
  66. package/src/tools/recall.ts +79 -1
  67. package/src/tools/save-context.ts +167 -1
  68. package/src/tools/session-start.ts +88 -100
@@ -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
- lines.push(`- "${h.title}" (${h.kind}, ${h.relevance})`);
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 resolvedMemoryPath = auto_memory_path?.trim()
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);