context-vault 3.9.0 → 3.10.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/bin/cli.js +311 -0
- package/dist/remote.d.ts +52 -0
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +130 -0
- package/dist/remote.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +27 -1
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/recall.d.ts.map +1 -1
- package/dist/tools/recall.js +36 -1
- package/dist/tools/recall.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +46 -2
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/package.json +2 -2
- package/src/remote.ts +145 -0
- package/src/tools/get-context.ts +29 -1
- package/src/tools/recall.ts +35 -1
- package/src/tools/session-start.ts +47 -2
package/src/remote.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface TeamSearchResult extends RemoteSearchResult {
|
|
|
45
45
|
recall_members?: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface PublicSearchResult extends RemoteSearchResult {
|
|
49
|
+
source: 'public';
|
|
50
|
+
vault_slug: string;
|
|
51
|
+
recall_count: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
export interface PrivacyScanMatch {
|
|
49
55
|
type: string;
|
|
50
56
|
value: string;
|
|
@@ -242,6 +248,103 @@ export class RemoteClient {
|
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
async publicSearch(slug: string, params: Record<string, unknown>): Promise<PublicSearchResult[]> {
|
|
252
|
+
try {
|
|
253
|
+
const query = new URLSearchParams();
|
|
254
|
+
if (params.query) query.set('q', String(params.query));
|
|
255
|
+
if (Array.isArray(params.tags) && params.tags.length) query.set('tags', params.tags.join(','));
|
|
256
|
+
if (params.kind) query.set('kind', String(params.kind));
|
|
257
|
+
if (params.category) query.set('category', String(params.category));
|
|
258
|
+
if (params.limit) query.set('limit', String(params.limit));
|
|
259
|
+
if (params.since) query.set('since', String(params.since));
|
|
260
|
+
if (params.until) query.set('until', String(params.until));
|
|
261
|
+
|
|
262
|
+
const res = await this.fetch(`/api/public/${slug}/search?${query.toString()}`);
|
|
263
|
+
if (!res.ok) return [];
|
|
264
|
+
const data = await res.json() as Record<string, unknown>;
|
|
265
|
+
const entries = Array.isArray(data.entries) ? data.entries : Array.isArray(data) ? data : [];
|
|
266
|
+
return (entries as PublicSearchResult[]).map(e => ({ ...e, source: 'public' as const, vault_slug: slug }));
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async publicRecall(slug: string, params: {
|
|
273
|
+
signal: string;
|
|
274
|
+
signal_type: string;
|
|
275
|
+
bucket?: string;
|
|
276
|
+
max_hints?: number;
|
|
277
|
+
}): Promise<RemoteHint[]> {
|
|
278
|
+
try {
|
|
279
|
+
const res = await this.fetch(`/api/public/${slug}/search`, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
body: JSON.stringify(params),
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) return [];
|
|
284
|
+
const data = await res.json() as Record<string, unknown>;
|
|
285
|
+
return Array.isArray(data.hints) ? data.hints : Array.isArray(data) ? data as RemoteHint[] : [];
|
|
286
|
+
} catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async publicList(params?: { domain?: string; sort?: string }): Promise<Record<string, unknown>[]> {
|
|
292
|
+
try {
|
|
293
|
+
const query = new URLSearchParams();
|
|
294
|
+
if (params?.domain) query.set('domain', params.domain);
|
|
295
|
+
if (params?.sort) query.set('sort', params.sort);
|
|
296
|
+
|
|
297
|
+
const res = await this.fetch(`/api/public/vaults?${query.toString()}`);
|
|
298
|
+
if (!res.ok) return [];
|
|
299
|
+
const data = await res.json() as Record<string, unknown>;
|
|
300
|
+
return Array.isArray(data.vaults) ? data.vaults as Record<string, unknown>[] : Array.isArray(data) ? data as Record<string, unknown>[] : [];
|
|
301
|
+
} catch {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async publicCreate(params: {
|
|
307
|
+
name: string;
|
|
308
|
+
slug: string;
|
|
309
|
+
description?: string;
|
|
310
|
+
domain_tags?: string[];
|
|
311
|
+
visibility?: string;
|
|
312
|
+
}): Promise<{ ok: boolean; slug?: string; error?: string }> {
|
|
313
|
+
try {
|
|
314
|
+
const res = await this.fetch('/api/public/vaults', {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
body: JSON.stringify(params),
|
|
317
|
+
});
|
|
318
|
+
const data = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
319
|
+
if (res.ok) {
|
|
320
|
+
return { ok: true, slug: (data.slug as string) || params.slug };
|
|
321
|
+
}
|
|
322
|
+
return { ok: false, error: typeof data.error === 'string' ? data.error : `HTTP ${res.status}` };
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return { ok: false, error: (e as Error).message };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async publicSeed(slug: string, entry: Record<string, unknown>): Promise<PublishResult> {
|
|
329
|
+
try {
|
|
330
|
+
const res = await this.fetch(`/api/public/${slug}/entries`, {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
body: JSON.stringify(entry),
|
|
333
|
+
});
|
|
334
|
+
const data = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
335
|
+
if (res.ok) {
|
|
336
|
+
return { ok: true, id: data.id as string | undefined };
|
|
337
|
+
}
|
|
338
|
+
if (res.status === 422 && data.code === 'PRIVACY_SCAN_FAILED') {
|
|
339
|
+
const matches = Array.isArray(data.matches) ? data.matches as PrivacyScanMatch[] : [];
|
|
340
|
+
return { ok: false, error: 'Privacy scan failed', status: 422, privacyMatches: matches };
|
|
341
|
+
}
|
|
342
|
+
return { ok: false, error: typeof data.error === 'string' ? data.error : `HTTP ${res.status}`, status: res.status };
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return { ok: false, error: (e as Error).message };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
245
348
|
private fetch(path: string, opts: RequestInit = {}): Promise<Response> {
|
|
246
349
|
const controller = new AbortController();
|
|
247
350
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
@@ -323,3 +426,45 @@ export function mergeWithTeamResults<T extends { id: string; score?: number; rec
|
|
|
323
426
|
merged.sort((a, b) => ((b as any).score ?? 0) - ((a as any).score ?? 0));
|
|
324
427
|
return merged.slice(0, limit);
|
|
325
428
|
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Apply recall-driven ranking boost to public vault results.
|
|
432
|
+
* Uses freshness decay: entries not updated in 90+ days get 0.8 multiplier.
|
|
433
|
+
* Formula: score * log(1 + recall_count) * freshness_decay
|
|
434
|
+
*/
|
|
435
|
+
export function applyPublicRecallBoost<T extends { score?: number; recall_count?: number; updated_at?: string | null }>(
|
|
436
|
+
entries: T[]
|
|
437
|
+
): T[] {
|
|
438
|
+
const now = Date.now();
|
|
439
|
+
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
440
|
+
return entries.map(e => {
|
|
441
|
+
const baseScore = (e as any).score ?? 0;
|
|
442
|
+
const recallCount = (e as any).recall_count ?? 0;
|
|
443
|
+
if (recallCount === 0) return e;
|
|
444
|
+
const updatedAt = (e as any).updated_at ? new Date((e as any).updated_at).getTime() : now;
|
|
445
|
+
const freshnessDecay = (now - updatedAt > NINETY_DAYS_MS) ? 0.8 : 1.0;
|
|
446
|
+
const boostedScore = baseScore * Math.log(1 + recallCount) * freshnessDecay;
|
|
447
|
+
return { ...e, score: boostedScore };
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Merge existing results with public vault results.
|
|
453
|
+
* Public results get recall-driven ranking boost with freshness decay before merge.
|
|
454
|
+
*/
|
|
455
|
+
export function mergeWithPublicResults<T extends { id: string; score?: number; recall_count?: number }>(
|
|
456
|
+
existing: T[],
|
|
457
|
+
publicResults: T[],
|
|
458
|
+
limit: number
|
|
459
|
+
): T[] {
|
|
460
|
+
const existingIds = new Set(existing.map(r => r.id));
|
|
461
|
+
const uniquePublic = publicResults.filter(r => !existingIds.has(r.id));
|
|
462
|
+
const boostedPublic = applyPublicRecallBoost(uniquePublic);
|
|
463
|
+
const merged = [...existing, ...boostedPublic];
|
|
464
|
+
merged.sort((a, b) => ((b as any).score ?? 0) - ((a as any).score ?? 0));
|
|
465
|
+
return merged.slice(0, limit);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function getPublicVaults(config: { remote?: RemoteConfig & { publicVaults?: string[] } }): string[] {
|
|
469
|
+
return (config.remote as any)?.publicVaults || [];
|
|
470
|
+
}
|
package/src/tools/get-context.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveTemporalParams } from '../temporal.js';
|
|
|
10
10
|
import { collectLinkedEntries } from '../linking.js';
|
|
11
11
|
import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
|
|
12
12
|
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
13
|
-
import { getRemoteClient, getTeamId, mergeRemoteResults, mergeWithTeamResults } from '../remote.js';
|
|
13
|
+
import { getRemoteClient, getTeamId, getPublicVaults, mergeRemoteResults, mergeWithTeamResults, mergeWithPublicResults } from '../remote.js';
|
|
14
14
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
15
15
|
|
|
16
16
|
const STALE_DUPLICATE_DAYS = 7;
|
|
@@ -619,6 +619,34 @@ export async function handler(
|
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
// Public vault merge: query each configured public vault
|
|
623
|
+
const publicVaultSlugs = getPublicVaults(ctx.config);
|
|
624
|
+
if (remoteClient && publicVaultSlugs.length > 0 && (hasQuery || hasFilters)) {
|
|
625
|
+
const publicSearches = publicVaultSlugs.map(slug =>
|
|
626
|
+
remoteClient.publicSearch(slug, {
|
|
627
|
+
query: hasQuery ? query : undefined,
|
|
628
|
+
tags: effectiveTags.length ? effectiveTags : undefined,
|
|
629
|
+
kind: kindFilter || undefined,
|
|
630
|
+
category: scopedCategory || undefined,
|
|
631
|
+
limit: effectiveLimit,
|
|
632
|
+
since: effectiveSince || undefined,
|
|
633
|
+
until: effectiveUntil || undefined,
|
|
634
|
+
}).catch(e => {
|
|
635
|
+
console.warn(`[context-vault] Public vault "${slug}" search failed: ${(e as Error).message}`);
|
|
636
|
+
return [];
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
try {
|
|
640
|
+
const allPublicResults = await Promise.all(publicSearches);
|
|
641
|
+
const combined = allPublicResults.flat();
|
|
642
|
+
if (combined.length > 0) {
|
|
643
|
+
filtered = mergeWithPublicResults(filtered, combined as any[], effectiveLimit);
|
|
644
|
+
}
|
|
645
|
+
} catch (e) {
|
|
646
|
+
console.warn(`[context-vault] Public vault search failed: ${(e as Error).message}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
622
650
|
// Brief score boost: briefs rank slightly higher so consolidated snapshots
|
|
623
651
|
// surface above the individual entries they summarize.
|
|
624
652
|
for (const r of filtered) {
|
package/src/tools/recall.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { ok } from '../helpers.js';
|
|
3
3
|
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
4
4
|
import { getAutoMemory, findAutoMemoryOverlaps } from '../auto-memory.js';
|
|
5
|
-
import { getRemoteClient, getTeamId } from '../remote.js';
|
|
5
|
+
import { getRemoteClient, getTeamId, getPublicVaults } from '../remote.js';
|
|
6
6
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
7
7
|
|
|
8
8
|
const SEMANTIC_SIMILARITY_THRESHOLD = 0.6;
|
|
@@ -227,6 +227,40 @@ export async function handler(
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// Public vault recall: query each configured public vault
|
|
231
|
+
const publicVaultSlugs = getPublicVaults(ctx.config);
|
|
232
|
+
if (remoteClient && publicVaultSlugs.length > 0 && hints.length < limit) {
|
|
233
|
+
const publicRecalls = publicVaultSlugs.map(slug =>
|
|
234
|
+
remoteClient.publicRecall(slug, {
|
|
235
|
+
signal,
|
|
236
|
+
signal_type,
|
|
237
|
+
bucket,
|
|
238
|
+
max_hints: limit - hints.length,
|
|
239
|
+
}).catch(e => {
|
|
240
|
+
console.warn(`[context-vault] Public vault "${slug}" recall failed: ${(e as Error).message}`);
|
|
241
|
+
return [];
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
try {
|
|
245
|
+
const allPublicHints = await Promise.all(publicRecalls);
|
|
246
|
+
const existingIds = new Set(hints.map(h => h.id));
|
|
247
|
+
for (const publicHints of allPublicHints) {
|
|
248
|
+
for (const ph of publicHints) {
|
|
249
|
+
if (hints.length >= limit) break;
|
|
250
|
+
if (existingIds.has(ph.id)) continue;
|
|
251
|
+
if (sessionSet && !bypassDedup && sessionSet.has(ph.id)) {
|
|
252
|
+
suppressed++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
hints.push({ ...ph, tags: [...(ph.tags || []), '[public]'] });
|
|
256
|
+
if (sessionSet) sessionSet.add(ph.id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.warn(`[context-vault] Public vault recall failed: ${(e as Error).message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
230
264
|
let method: 'tag_match' | 'semantic' | 'none' = hints.length > 0 ? 'tag_match' : 'none';
|
|
231
265
|
|
|
232
266
|
// Semantic fallback: when fast-path returns 0 results and signal is not file-based
|
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
|
|
7
7
|
import { getAutoMemory } from '../auto-memory.js';
|
|
8
|
-
import { getRemoteClient, getTeamId } from '../remote.js';
|
|
8
|
+
import { getRemoteClient, getTeamId, getPublicVaults } from '../remote.js';
|
|
9
9
|
import type { AutoMemoryEntry, AutoMemoryResult } from '../auto-memory.js';
|
|
10
10
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
11
11
|
|
|
@@ -314,6 +314,50 @@ export async function handler(
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// Public vault entries: include public knowledge if publicVaults are configured
|
|
318
|
+
let publicCount = 0;
|
|
319
|
+
const publicVaultSlugs = getPublicVaults(ctx.config);
|
|
320
|
+
if (remoteClient && publicVaultSlugs.length > 0 && tokensUsed < tokenBudget) {
|
|
321
|
+
try {
|
|
322
|
+
const allPublicSeenIds = new Set([
|
|
323
|
+
...decisions.map((d: any) => d.id),
|
|
324
|
+
...deduped.map((d: any) => d.id),
|
|
325
|
+
...(lastSession ? [lastSession.id] : []),
|
|
326
|
+
]);
|
|
327
|
+
const publicSearches = publicVaultSlugs.map(slug =>
|
|
328
|
+
remoteClient.publicSearch(slug, {
|
|
329
|
+
tags: effectiveTags.length ? effectiveTags : undefined,
|
|
330
|
+
limit: 5,
|
|
331
|
+
since: sinceDate,
|
|
332
|
+
}).catch(() => [])
|
|
333
|
+
);
|
|
334
|
+
const allPublicResults = await Promise.all(publicSearches);
|
|
335
|
+
const flatPublic = allPublicResults.flat().filter((r: any) => !allPublicSeenIds.has(r.id));
|
|
336
|
+
if (flatPublic.length > 0) {
|
|
337
|
+
const header = '## Public Knowledge\n';
|
|
338
|
+
const headerTokens = estimateTokens(header);
|
|
339
|
+
if (tokensUsed + headerTokens <= tokenBudget) {
|
|
340
|
+
const entryLines: string[] = [];
|
|
341
|
+
tokensUsed += headerTokens;
|
|
342
|
+
for (const entry of flatPublic) {
|
|
343
|
+
const slug = (entry as any).vault_slug || 'public';
|
|
344
|
+
const line = formatEntry(entry) + ` \`[public:${slug}]\``;
|
|
345
|
+
const lineTokens = estimateTokens(line);
|
|
346
|
+
if (tokensUsed + lineTokens > tokenBudget) break;
|
|
347
|
+
entryLines.push(line);
|
|
348
|
+
tokensUsed += lineTokens;
|
|
349
|
+
publicCount++;
|
|
350
|
+
}
|
|
351
|
+
if (entryLines.length > 0) {
|
|
352
|
+
sections.push(header + entryLines.join('\n') + '\n');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.warn(`[context-vault] Public vault session_start failed: ${(e as Error).message}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
317
361
|
const totalEntries =
|
|
318
362
|
(lastSession ? 1 : 0) +
|
|
319
363
|
decisions.length +
|
|
@@ -321,7 +365,8 @@ export async function handler(
|
|
|
321
365
|
return true;
|
|
322
366
|
}).length +
|
|
323
367
|
remoteCount +
|
|
324
|
-
teamCount
|
|
368
|
+
teamCount +
|
|
369
|
+
publicCount;
|
|
325
370
|
|
|
326
371
|
if (indexWarning) {
|
|
327
372
|
sections.push(indexWarning);
|