@cairnkeep/cli 2.15.4 → 2.17.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/CHANGELOG.md +44 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +85 -590
- package/docs/README.md +87 -0
- package/docs/agents.md +193 -0
- package/docs/context-intelligence.md +143 -0
- package/docs/context-packs.md +33 -0
- package/docs/domain-knowledge.md +29 -0
- package/docs/ecosystem.md +23 -0
- package/docs/learning/CURRICULUM-MAP.md +3 -1
- package/docs/learning/FEATURE-GUIDE.md +23 -4
- package/docs/learning/README.md +4 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +3 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +9 -10
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +18 -5
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +180 -0
- package/docs/learning/tracks/evidence-and-evaluation.md +1 -0
- package/docs/learning/tracks/operator.md +1 -0
- package/docs/learning/tracks/practitioner.md +1 -0
- package/docs/learning/video-scripts/V26-context-intelligence.md +144 -0
- package/docs/native-windows.md +5 -0
- package/docs/operating.md +65 -3
- package/docs/privacy-and-data-flow.md +22 -0
- package/docs/quickstart.md +12 -4
- package/docs/releasing.md +27 -19
- package/docs/storage.md +21 -0
- package/docs/work-evidence.md +14 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/context-pack-cli.js +3 -1
- package/mcp-memory-server/dist/context-pack-retrieval.js +413 -0
- package/mcp-memory-server/dist/context-pack.js +171 -3
- package/mcp-memory-server/dist/domain-retrieval-provider.js +257 -0
- package/mcp-memory-server/dist/index.js +73 -387
- package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
- package/mcp-memory-server/dist/memory-extraction.js +87 -0
- package/mcp-memory-server/dist/memory-proposal-cli.js +110 -0
- package/mcp-memory-server/dist/memory-proposal-schema.js +78 -0
- package/mcp-memory-server/dist/memory-proposal-store.js +273 -0
- package/mcp-memory-server/dist/platform-security.js +32 -26
- package/mcp-memory-server/dist/retrieval-benchmark.js +199 -0
- package/mcp-memory-server/dist/reviewed-memory-store.js +281 -0
- package/mcp-memory-server/dist/work-evidence-schema.js +17 -0
- package/mcp-memory-server/dist/work-evidence-store.js +57 -1
- package/mcp-memory-server/package.json +8 -1
- package/package.json +1 -1
- package/schemas/context-usage-receipt.schema.json +41 -0
- package/schemas/memory-proposal.schema.json +65 -0
- package/schemas/retrieval-benchmark.schema.json +64 -0
- package/scripts/cairn-cli.mjs +3 -0
- package/scripts/completion.sh +17 -6
- package/scripts/doctor.sh +4 -2
- package/scripts/setup.mjs +92 -13
- package/scripts/terminal-prompts.mjs +172 -0
- package/scripts/test-completion.sh +8 -1
- package/scripts/test-docs-navigation.sh +63 -0
- package/scripts/test-doctor.sh +21 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-release-sbom.sh +44 -16
- package/scripts/test-setup-preflight.mjs +130 -0
- package/scripts/test-windows-native.mjs +30 -0
- package/scripts/verify-docs-parity.sh +28 -28
- package/scripts/windows-platform.mjs +8 -5
|
@@ -14,13 +14,16 @@ import { z } from "zod";
|
|
|
14
14
|
import { artifactIdSchema, artifactKindSchema, artifactMediaTypeSchema, artifactNodeRefSchema, artifactProvenanceSchema, artifactSessionRefSchema, compactionSummaryInputContentSchema, diffContentSchema, generatedFileInputContentSchema, isArtifactHttpEnabled, isArtifactStoreEnabled, testOutputContentSchema, } from "./artifact-schema.js";
|
|
15
15
|
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
16
16
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
|
|
17
|
+
import { queryDomainKnowledge, resolveDomainRetrievalProvider } from "./domain-retrieval-provider.js";
|
|
17
18
|
import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
|
|
18
19
|
import { canonicalTagsSchema, isTypedMemoryNodesEnabled, memoryImportEnvelopeSchema, noteNodeSchema, nodeTypeSchema, } from "./node-schema.js";
|
|
19
|
-
import { attachNodeMetadata,
|
|
20
|
-
import { contextPackHttpEnabled, contextPacksEnabled, listVisibleContext, readVisibleContext, relatedVisibleContext, searchVisibleContext } from "./context-pack.js";
|
|
20
|
+
import { attachNodeMetadata, commitMemoryImport, createTypedNode, deleteTypedNode, getTypedNode, listTypedHistory, planMemoryImport, supersedeTypedNode, } from "./node-store.js";
|
|
21
|
+
import { contextPackHttpEnabled, contextPacksEnabled, listVisibleContext, readVisibleContext, relatedVisibleContext, searchVisibleContext, treeVisibleContext } from "./context-pack.js";
|
|
21
22
|
import { metadataForTool } from "./mcp-tool-catalog.js";
|
|
22
23
|
import { profileAllowsTool, resolveMcpToolProfile } from "./mcp-tool-profile.js";
|
|
23
24
|
import { isWorkEvidenceEnabled } from "./work-evidence-schema.js";
|
|
25
|
+
import { extractMemoryCandidates as extractMemoryCandidatesFromProvider } from "./memory-extraction.js";
|
|
26
|
+
import { applyReviewedMemory, invalidateReviewedMemory } from "./reviewed-memory-store.js";
|
|
24
27
|
const MINIMUM_NODE_MAJOR = 22;
|
|
25
28
|
const nodeMajor = Number.parseInt(process.versions.node, 10);
|
|
26
29
|
if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
@@ -223,33 +226,6 @@ function historyPrefix(baseKey) {
|
|
|
223
226
|
function historySnapshotKey(baseKey, timestamp) {
|
|
224
227
|
return `${historyPrefix(baseKey)}${timestamp}-${randomUUID()}`;
|
|
225
228
|
}
|
|
226
|
-
const reviewedRecordSchema = z.object({
|
|
227
|
-
schema_version: z.literal(1),
|
|
228
|
-
review_id: z.string(),
|
|
229
|
-
key: z.string(),
|
|
230
|
-
value_hash: z.string(),
|
|
231
|
-
state: z.enum(["active", "superseded", "invalidated"]),
|
|
232
|
-
applied_at: z.string(),
|
|
233
|
-
superseded_at: z.string().optional(),
|
|
234
|
-
superseded_by: z.string().optional(),
|
|
235
|
-
invalidated_at: z.string().optional(),
|
|
236
|
-
invalidation_reason: z.string().optional(),
|
|
237
|
-
});
|
|
238
|
-
function reviewedRecordKey(reviewId) {
|
|
239
|
-
return `${REVIEWED_NAMESPACE}/${reviewId}`;
|
|
240
|
-
}
|
|
241
|
-
function parseReviewedRecord(value) {
|
|
242
|
-
const parsed = reviewedRecordSchema.safeParse(value);
|
|
243
|
-
if (!parsed.success) {
|
|
244
|
-
throw new Error("Stored reviewed-memory provenance is invalid; refusing to continue.");
|
|
245
|
-
}
|
|
246
|
-
return parsed.data;
|
|
247
|
-
}
|
|
248
|
-
async function inImmediateScopeTransaction(agent, operation) {
|
|
249
|
-
const transaction = agent.getDatabase().transaction(operation);
|
|
250
|
-
const immediate = transaction.immediate;
|
|
251
|
-
return immediate();
|
|
252
|
-
}
|
|
253
229
|
function visibleEntries(entries, includeHistory) {
|
|
254
230
|
if (includeHistory) {
|
|
255
231
|
return entries;
|
|
@@ -340,113 +316,6 @@ function truncateOutput(value, maxLength = 12000) {
|
|
|
340
316
|
}
|
|
341
317
|
return `${value.slice(0, maxLength)}\n...[truncated ${value.length - maxLength} chars]`;
|
|
342
318
|
}
|
|
343
|
-
function stripMarkdownFences(value) {
|
|
344
|
-
return value
|
|
345
|
-
.trim()
|
|
346
|
-
.replace(/^```(?:json)?\s*/i, "")
|
|
347
|
-
.replace(/\s*```$/, "")
|
|
348
|
-
.trim();
|
|
349
|
-
}
|
|
350
|
-
function parseJsonResponse(value) {
|
|
351
|
-
const stripped = stripMarkdownFences(value);
|
|
352
|
-
try {
|
|
353
|
-
return JSON.parse(stripped);
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
const firstBrace = stripped.indexOf("{");
|
|
357
|
-
const lastBrace = stripped.lastIndexOf("}");
|
|
358
|
-
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
359
|
-
return JSON.parse(stripped.slice(firstBrace, lastBrace + 1));
|
|
360
|
-
}
|
|
361
|
-
throw new Error(`Failed to parse JSON response: ${truncateOutput(stripped, 1000)}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
function sanitizeExtractionCandidates(value, fallbackCategory) {
|
|
365
|
-
if (!Array.isArray(value)) {
|
|
366
|
-
return [];
|
|
367
|
-
}
|
|
368
|
-
return value
|
|
369
|
-
.map((item) => {
|
|
370
|
-
if (!item || typeof item !== "object") {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
const raw = item;
|
|
374
|
-
const key = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
375
|
-
const candidateValue = typeof raw.value === "string" ? raw.value.trim() : "";
|
|
376
|
-
if (!key || !candidateValue) {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
const category = typeof raw.category === "string" && raw.category.trim()
|
|
380
|
-
? raw.category.trim()
|
|
381
|
-
: fallbackCategory;
|
|
382
|
-
const importance = typeof raw.importance === "number"
|
|
383
|
-
? Math.max(0, Math.min(1, raw.importance))
|
|
384
|
-
: undefined;
|
|
385
|
-
return {
|
|
386
|
-
key,
|
|
387
|
-
value: candidateValue,
|
|
388
|
-
category,
|
|
389
|
-
importance,
|
|
390
|
-
};
|
|
391
|
-
})
|
|
392
|
-
.filter((candidate) => candidate !== null);
|
|
393
|
-
}
|
|
394
|
-
async function extractMemoryCandidates(content, modelOverride, category) {
|
|
395
|
-
const apiKey = process.env.CAIRN_LLM_API_KEY;
|
|
396
|
-
if (!apiKey) {
|
|
397
|
-
throw new Error("CAIRN_LLM_API_KEY is not set.");
|
|
398
|
-
}
|
|
399
|
-
const rawUrl = process.env.CAIRN_LLM_API_URL;
|
|
400
|
-
if (!rawUrl) {
|
|
401
|
-
throw new Error("CAIRN_LLM_API_URL is not set.");
|
|
402
|
-
}
|
|
403
|
-
const apiUrl = rawUrl.trim().replace(/\/+$/, "");
|
|
404
|
-
const model = (modelOverride ?? process.env.CAIRN_LLM_EXTRACTION_MODEL)?.trim();
|
|
405
|
-
if (!model) {
|
|
406
|
-
throw new Error("CAIRN_LLM_EXTRACTION_MODEL is not set.");
|
|
407
|
-
}
|
|
408
|
-
const systemPrompt = [
|
|
409
|
-
"You extract durable memory candidates from development notes.",
|
|
410
|
-
"Return ONLY valid JSON, no markdown fences.",
|
|
411
|
-
"Schema: {\"candidates\":[{\"key\":\"decisions/cache-rule\",\"value\":\"...\",\"category\":\"decision\",\"importance\":0.92}]}",
|
|
412
|
-
"Only include genuinely reusable knowledge.",
|
|
413
|
-
"Skip trivial status notes, temporary branch details, and duplicated points.",
|
|
414
|
-
"Prefer short kebab-case keys with a useful prefix such as decisions/, pitfalls/, patterns/, bugs/, constraints/, preferences/, conventions/.",
|
|
415
|
-
"Do not invent dates unless they are explicitly present in the source text.",
|
|
416
|
-
category ? `Bias extraction toward category: ${category}.` : "",
|
|
417
|
-
].filter(Boolean).join(" ");
|
|
418
|
-
const response = await fetch(`${apiUrl}/chat/completions`, {
|
|
419
|
-
method: "POST",
|
|
420
|
-
headers: {
|
|
421
|
-
Authorization: `Bearer ${apiKey}`,
|
|
422
|
-
"Content-Type": "application/json",
|
|
423
|
-
},
|
|
424
|
-
body: JSON.stringify({
|
|
425
|
-
model,
|
|
426
|
-
messages: [
|
|
427
|
-
{ role: "system", content: systemPrompt },
|
|
428
|
-
{ role: "user", content },
|
|
429
|
-
],
|
|
430
|
-
temperature: 0.1,
|
|
431
|
-
max_tokens: 1200,
|
|
432
|
-
}),
|
|
433
|
-
signal: AbortSignal.timeout(120000),
|
|
434
|
-
});
|
|
435
|
-
if (!response.ok) {
|
|
436
|
-
const text = await response.text();
|
|
437
|
-
throw new Error(`Extraction request failed with ${response.status}: ${text}`);
|
|
438
|
-
}
|
|
439
|
-
const payload = (await response.json());
|
|
440
|
-
const rawContent = payload.choices?.[0]?.message?.content;
|
|
441
|
-
if (!rawContent) {
|
|
442
|
-
throw new Error("Extraction model returned no content.");
|
|
443
|
-
}
|
|
444
|
-
const parsed = parseJsonResponse(rawContent);
|
|
445
|
-
return {
|
|
446
|
-
model,
|
|
447
|
-
candidates: sanitizeExtractionCandidates(parsed.candidates, category),
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
319
|
async function runCommand(command, args, timeoutMs, env = process.env) {
|
|
451
320
|
return new Promise((resolvePromise, reject) => {
|
|
452
321
|
const child = spawn(command, args, {
|
|
@@ -492,40 +361,6 @@ async function readStdin() {
|
|
|
492
361
|
function defaultAnythingLLMWorkspace(config) {
|
|
493
362
|
return config.anythingllm_workspaces?.find((workspace) => workspace !== "engineering-patterns");
|
|
494
363
|
}
|
|
495
|
-
async function callAnythingLLM(workspace, query) {
|
|
496
|
-
const apiKey = process.env.ANYTHINGLLM_API_KEY;
|
|
497
|
-
if (!apiKey) {
|
|
498
|
-
throw new Error("ANYTHINGLLM_API_KEY is not set.");
|
|
499
|
-
}
|
|
500
|
-
const baseUrl = process.env.ANYTHINGLLM_BASE_URL ?? "http://localhost:3001";
|
|
501
|
-
const response = await fetch(`${baseUrl}/api/v1/workspace/${encodeURIComponent(workspace)}/chat`, {
|
|
502
|
-
method: "POST",
|
|
503
|
-
headers: {
|
|
504
|
-
Authorization: `Bearer ${apiKey}`,
|
|
505
|
-
"Content-Type": "application/json",
|
|
506
|
-
},
|
|
507
|
-
body: JSON.stringify({
|
|
508
|
-
message: query,
|
|
509
|
-
mode: "query",
|
|
510
|
-
}),
|
|
511
|
-
signal: AbortSignal.timeout(120000),
|
|
512
|
-
});
|
|
513
|
-
if (!response.ok) {
|
|
514
|
-
const text = await response.text();
|
|
515
|
-
throw new Error(`AnythingLLM request failed with ${response.status}: ${text}`);
|
|
516
|
-
}
|
|
517
|
-
const payload = (await response.json());
|
|
518
|
-
const directText = [
|
|
519
|
-
payload.textResponse,
|
|
520
|
-
payload.response,
|
|
521
|
-
payload.message,
|
|
522
|
-
payload.text,
|
|
523
|
-
].find((value) => typeof value === "string");
|
|
524
|
-
if (typeof directText === "string") {
|
|
525
|
-
return directText;
|
|
526
|
-
}
|
|
527
|
-
return JSON.stringify(payload, null, 2);
|
|
528
|
-
}
|
|
529
364
|
function entryText(entry) {
|
|
530
365
|
const metadata = [entry.node_type, ...(entry.tags ?? [])].filter(Boolean).join("\n");
|
|
531
366
|
return [entry.key, entry.value, metadata].filter(Boolean).join("\n");
|
|
@@ -873,6 +708,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
873
708
|
const artifactToolsEnabled = isArtifactStoreEnabled() && (!context.remote || isArtifactHttpEnabled());
|
|
874
709
|
const contextPackToolsEnabled = contextPacksEnabled() && (!context.remote || contextPackHttpEnabled());
|
|
875
710
|
const workEvidenceToolsEnabled = isWorkEvidenceEnabled() && !context.remote;
|
|
711
|
+
const contextUsageToolsEnabled = process.env.CAIRN_CONTEXT_USAGE === "1" && workEvidenceToolsEnabled;
|
|
876
712
|
if (artifactToolsEnabled && context.remote && !context.projectId) {
|
|
877
713
|
throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
|
|
878
714
|
}
|
|
@@ -1317,7 +1153,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1317
1153
|
category: z.enum(["decision", "preference", "pattern", "pitfall", "constraint", "bug", "convention"]).optional(),
|
|
1318
1154
|
}),
|
|
1319
1155
|
}, async ({ scope, content, model, category }) => {
|
|
1320
|
-
const extracted = await
|
|
1156
|
+
const extracted = await extractMemoryCandidatesFromProvider(content, model, category);
|
|
1321
1157
|
const payload = {
|
|
1322
1158
|
scope,
|
|
1323
1159
|
model: extracted.model,
|
|
@@ -1424,102 +1260,17 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1424
1260
|
value: z.string(),
|
|
1425
1261
|
}),
|
|
1426
1262
|
}, async ({ scope, review_id, key, value }) => {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
const existing = parseReviewedRecord(stored);
|
|
1439
|
-
if (existing.key !== key || existing.value_hash !== valueHash) {
|
|
1440
|
-
throw new Error(`Review id ${review_id} was already used with different content.`);
|
|
1441
|
-
}
|
|
1442
|
-
if (existing.state !== "active") {
|
|
1443
|
-
throw new Error(`Review id ${review_id} is ${existing.state} and cannot be reapplied.`);
|
|
1444
|
-
}
|
|
1445
|
-
return {
|
|
1446
|
-
ok: true,
|
|
1447
|
-
scope,
|
|
1448
|
-
review_id,
|
|
1449
|
-
key,
|
|
1450
|
-
applied: false,
|
|
1451
|
-
idempotent: true,
|
|
1452
|
-
snapshot_key: null,
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
const appliedAt = new Date().toISOString();
|
|
1456
|
-
const provenance = await agent.kv.list(`${REVIEWED_NAMESPACE}/`);
|
|
1457
|
-
for (const entry of provenance) {
|
|
1458
|
-
const previousRecord = parseReviewedRecord(entry.value);
|
|
1459
|
-
if (previousRecord.state === "active" && previousRecord.key === key) {
|
|
1460
|
-
await agent.kv.set(entry.key, {
|
|
1461
|
-
...previousRecord,
|
|
1462
|
-
state: "superseded",
|
|
1463
|
-
superseded_at: appliedAt,
|
|
1464
|
-
superseded_by: review_id,
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
let snapshotKey = null;
|
|
1469
|
-
if (typedNodesEnabled) {
|
|
1470
|
-
const changed = await applyReviewedTypedNode({
|
|
1471
|
-
agent,
|
|
1472
|
-
scope,
|
|
1473
|
-
review_id,
|
|
1474
|
-
key,
|
|
1475
|
-
value,
|
|
1476
|
-
node_type: "memory",
|
|
1477
|
-
tags: [],
|
|
1478
|
-
in_transaction: true,
|
|
1479
|
-
});
|
|
1480
|
-
snapshotKey = changed.snapshot_key;
|
|
1481
|
-
}
|
|
1482
|
-
else {
|
|
1483
|
-
const previous = await agent.kv.get(key);
|
|
1484
|
-
const previousValue = previous === undefined ? undefined : normalizeValue(previous);
|
|
1485
|
-
if (previousValue !== undefined && previousValue !== value) {
|
|
1486
|
-
snapshotKey = historySnapshotKey(key, appliedAt);
|
|
1487
|
-
await agent.kv.set(snapshotKey, {
|
|
1488
|
-
value: previousValue,
|
|
1489
|
-
superseded_at: appliedAt,
|
|
1490
|
-
superseded_reason: `reviewed memory ${review_id}`,
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
|
-
await agent.kv.set(key, value);
|
|
1494
|
-
}
|
|
1495
|
-
await agent.kv.set(recordKey, {
|
|
1496
|
-
schema_version: 1,
|
|
1497
|
-
review_id,
|
|
1498
|
-
key,
|
|
1499
|
-
value_hash: valueHash,
|
|
1500
|
-
state: "active",
|
|
1501
|
-
applied_at: appliedAt,
|
|
1502
|
-
});
|
|
1503
|
-
return {
|
|
1504
|
-
ok: true,
|
|
1505
|
-
scope,
|
|
1506
|
-
review_id,
|
|
1507
|
-
key,
|
|
1508
|
-
applied: true,
|
|
1509
|
-
idempotent: false,
|
|
1510
|
-
snapshot_key: snapshotKey,
|
|
1511
|
-
};
|
|
1512
|
-
});
|
|
1513
|
-
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
1514
|
-
await linkActiveWorkEvidence(process.cwd(), { kind: "reviewed_memory", scope, review_id, key });
|
|
1515
|
-
return {
|
|
1516
|
-
content: [{ type: "text", text: asToolText(payload) }],
|
|
1517
|
-
structuredContent: payload,
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
finally {
|
|
1521
|
-
await agent.close();
|
|
1522
|
-
}
|
|
1263
|
+
const payload = await applyReviewedMemory(scope, review_id, key, value, {
|
|
1264
|
+
cwd: process.cwd(),
|
|
1265
|
+
projectId: context.projectId,
|
|
1266
|
+
typed: typedNodesEnabled,
|
|
1267
|
+
});
|
|
1268
|
+
const { linkActiveWorkEvidence } = await import("./work-evidence-store.js");
|
|
1269
|
+
await linkActiveWorkEvidence(process.cwd(), { kind: "reviewed_memory", scope, review_id, key });
|
|
1270
|
+
return {
|
|
1271
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1272
|
+
structuredContent: payload,
|
|
1273
|
+
};
|
|
1523
1274
|
});
|
|
1524
1275
|
registerTool("memory_invalidate_reviewed", {
|
|
1525
1276
|
description: "Invalidate a reviewed revision, removing it only when the live value still matches.",
|
|
@@ -1530,121 +1281,15 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1530
1281
|
reason: z.string().max(512).optional(),
|
|
1531
1282
|
}),
|
|
1532
1283
|
}, async ({ scope, review_id, key, reason }) => {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (stored === undefined) {
|
|
1543
|
-
const invalidatedAt = new Date().toISOString();
|
|
1544
|
-
await agent.kv.set(recordKey, {
|
|
1545
|
-
schema_version: 1,
|
|
1546
|
-
review_id,
|
|
1547
|
-
key,
|
|
1548
|
-
value_hash: "",
|
|
1549
|
-
state: "invalidated",
|
|
1550
|
-
applied_at: invalidatedAt,
|
|
1551
|
-
invalidated_at: invalidatedAt,
|
|
1552
|
-
...(reason ? { invalidation_reason: reason } : {}),
|
|
1553
|
-
});
|
|
1554
|
-
return {
|
|
1555
|
-
ok: true,
|
|
1556
|
-
scope,
|
|
1557
|
-
review_id,
|
|
1558
|
-
key,
|
|
1559
|
-
invalidated: true,
|
|
1560
|
-
idempotent: false,
|
|
1561
|
-
missing: true,
|
|
1562
|
-
removed: false,
|
|
1563
|
-
current_changed: false,
|
|
1564
|
-
snapshot_key: null,
|
|
1565
|
-
};
|
|
1566
|
-
}
|
|
1567
|
-
const record = parseReviewedRecord(stored);
|
|
1568
|
-
if (record.key !== key) {
|
|
1569
|
-
throw new Error(`Review id ${review_id} belongs to a different memory key.`);
|
|
1570
|
-
}
|
|
1571
|
-
if (record.state === "invalidated") {
|
|
1572
|
-
return {
|
|
1573
|
-
ok: true,
|
|
1574
|
-
scope,
|
|
1575
|
-
review_id,
|
|
1576
|
-
key: record.key,
|
|
1577
|
-
invalidated: false,
|
|
1578
|
-
idempotent: true,
|
|
1579
|
-
missing: record.value_hash === "",
|
|
1580
|
-
removed: false,
|
|
1581
|
-
current_changed: false,
|
|
1582
|
-
snapshot_key: null,
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
const invalidatedAt = new Date().toISOString();
|
|
1586
|
-
let currentChanged = false;
|
|
1587
|
-
let removed = false;
|
|
1588
|
-
let snapshotKey = null;
|
|
1589
|
-
if (record.state === "active") {
|
|
1590
|
-
const current = await agent.kv.get(record.key);
|
|
1591
|
-
if (current !== undefined) {
|
|
1592
|
-
const currentValue = normalizeValue(current);
|
|
1593
|
-
if (hashText(currentValue) === record.value_hash) {
|
|
1594
|
-
if (typedNodesEnabled) {
|
|
1595
|
-
const changed = await invalidateReviewedTypedNode({
|
|
1596
|
-
agent,
|
|
1597
|
-
scope,
|
|
1598
|
-
review_id,
|
|
1599
|
-
key: record.key,
|
|
1600
|
-
reason,
|
|
1601
|
-
in_transaction: true,
|
|
1602
|
-
});
|
|
1603
|
-
snapshotKey = changed.snapshot_key;
|
|
1604
|
-
}
|
|
1605
|
-
else {
|
|
1606
|
-
snapshotKey = historySnapshotKey(record.key, invalidatedAt);
|
|
1607
|
-
await agent.kv.set(snapshotKey, {
|
|
1608
|
-
value: currentValue,
|
|
1609
|
-
superseded_at: invalidatedAt,
|
|
1610
|
-
superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
|
|
1611
|
-
});
|
|
1612
|
-
await agent.kv.delete(record.key);
|
|
1613
|
-
}
|
|
1614
|
-
removed = true;
|
|
1615
|
-
}
|
|
1616
|
-
else {
|
|
1617
|
-
currentChanged = true;
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
await agent.kv.set(recordKey, {
|
|
1622
|
-
...record,
|
|
1623
|
-
state: "invalidated",
|
|
1624
|
-
invalidated_at: invalidatedAt,
|
|
1625
|
-
...(reason ? { invalidation_reason: reason } : {}),
|
|
1626
|
-
});
|
|
1627
|
-
return {
|
|
1628
|
-
ok: true,
|
|
1629
|
-
scope,
|
|
1630
|
-
review_id,
|
|
1631
|
-
key: record.key,
|
|
1632
|
-
invalidated: true,
|
|
1633
|
-
idempotent: false,
|
|
1634
|
-
missing: false,
|
|
1635
|
-
removed,
|
|
1636
|
-
current_changed: currentChanged,
|
|
1637
|
-
snapshot_key: snapshotKey,
|
|
1638
|
-
};
|
|
1639
|
-
});
|
|
1640
|
-
return {
|
|
1641
|
-
content: [{ type: "text", text: asToolText(payload) }],
|
|
1642
|
-
structuredContent: payload,
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
finally {
|
|
1646
|
-
await agent.close();
|
|
1647
|
-
}
|
|
1284
|
+
const payload = await invalidateReviewedMemory(scope, review_id, key, reason, {
|
|
1285
|
+
cwd: process.cwd(),
|
|
1286
|
+
projectId: context.projectId,
|
|
1287
|
+
typed: typedNodesEnabled,
|
|
1288
|
+
});
|
|
1289
|
+
return {
|
|
1290
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1291
|
+
structuredContent: payload,
|
|
1292
|
+
};
|
|
1648
1293
|
});
|
|
1649
1294
|
registerTool("memory_history", {
|
|
1650
1295
|
description: "Read prior versions of a memory entry from the hidden history namespace.",
|
|
@@ -1756,7 +1401,7 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1756
1401
|
});
|
|
1757
1402
|
}
|
|
1758
1403
|
registerTool("domain_knowledge_query", {
|
|
1759
|
-
description: "Query
|
|
1404
|
+
description: "Query the configured read-only domain retrieval provider for domain knowledge.",
|
|
1760
1405
|
inputSchema: z.object({
|
|
1761
1406
|
workspace: z.string().min(1).optional(),
|
|
1762
1407
|
query: z.string().min(1),
|
|
@@ -1769,7 +1414,11 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1769
1414
|
if (!workspaceSlug) {
|
|
1770
1415
|
throw new Error("No AnythingLLM workspace provided and no project workspace found in memory config.");
|
|
1771
1416
|
}
|
|
1772
|
-
const answer = await
|
|
1417
|
+
const answer = await queryDomainKnowledge({
|
|
1418
|
+
workspace: workspaceSlug,
|
|
1419
|
+
query,
|
|
1420
|
+
remote: context.remote,
|
|
1421
|
+
});
|
|
1773
1422
|
return {
|
|
1774
1423
|
content: [{ type: "text", text: answer }],
|
|
1775
1424
|
structuredContent: { workspace: workspaceSlug, answer },
|
|
@@ -1784,6 +1433,9 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1784
1433
|
timeout_seconds: z.number().int().min(30).max(3600).optional(),
|
|
1785
1434
|
}),
|
|
1786
1435
|
}, async ({ workspace, mode, confirm_replace, timeout_seconds }) => {
|
|
1436
|
+
if (resolveDomainRetrievalProvider() === "openviking") {
|
|
1437
|
+
throw new Error("domain_knowledge_sync is unavailable for the read-only OpenViking provider.");
|
|
1438
|
+
}
|
|
1787
1439
|
const syncMode = mode ?? "incremental";
|
|
1788
1440
|
const config = memoryConfig();
|
|
1789
1441
|
const workspaceSlug = workspace ?? defaultAnythingLLMWorkspace(config);
|
|
@@ -1912,14 +1564,28 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1912
1564
|
const payload = await listVisibleContext(contextPackProject);
|
|
1913
1565
|
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1914
1566
|
});
|
|
1567
|
+
registerTool("context_pack_tree", {
|
|
1568
|
+
description: "Browse a deterministic abstract or overview hierarchy of enabled context-pack files and approved skills.",
|
|
1569
|
+
inputSchema: z.object({
|
|
1570
|
+
pack: z.string().min(1).max(128).optional(),
|
|
1571
|
+
detail: z.enum(["abstract", "overview"]).optional(),
|
|
1572
|
+
}).strict(),
|
|
1573
|
+
}, async ({ pack, detail }) => {
|
|
1574
|
+
const payload = await treeVisibleContext({ ...contextPackProject, pack, detail });
|
|
1575
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1576
|
+
});
|
|
1915
1577
|
registerTool("context_pack_search", {
|
|
1916
1578
|
description: "Search documents and explicitly approved skills in context packs enabled for this project.",
|
|
1917
1579
|
inputSchema: z.object({
|
|
1918
1580
|
query: z.string().min(1).max(4096),
|
|
1919
1581
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1582
|
+
strategy: z.enum(["flat", "hierarchical"]).optional(),
|
|
1583
|
+
detail: z.enum(["abstract", "overview", "content"]).optional(),
|
|
1584
|
+
explain: z.boolean().optional(),
|
|
1585
|
+
include_refs: z.boolean().optional(),
|
|
1920
1586
|
}).strict(),
|
|
1921
|
-
}, async ({ query, limit }) => {
|
|
1922
|
-
const payload = await searchVisibleContext(query, { ...contextPackProject, limit });
|
|
1587
|
+
}, async ({ query, limit, strategy, detail, explain, include_refs }) => {
|
|
1588
|
+
const payload = await searchVisibleContext(query, { ...contextPackProject, limit, strategy, detail, explain, includeRefs: include_refs });
|
|
1923
1589
|
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1924
1590
|
});
|
|
1925
1591
|
registerTool("context_pack_read", {
|
|
@@ -1947,6 +1613,26 @@ function buildMemoryServer(context, capabilitySnapshot, withCapability, toolProf
|
|
|
1947
1613
|
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1948
1614
|
});
|
|
1949
1615
|
}
|
|
1616
|
+
if (contextUsageToolsEnabled) {
|
|
1617
|
+
registerTool("context_usage_record", {
|
|
1618
|
+
description: "Record a privacy-minimized context result receipt against work evidence.",
|
|
1619
|
+
inputSchema: z.object({
|
|
1620
|
+
evidence_id: z.string().min(4).max(40).regex(/^wev_[0-9a-f-]*$/i).optional(),
|
|
1621
|
+
task_digest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1622
|
+
result_digest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1623
|
+
outcome: z.enum(["used", "unused", "unknown"]),
|
|
1624
|
+
}).strict(),
|
|
1625
|
+
}, async ({ evidence_id, task_digest, result_digest, outcome }) => {
|
|
1626
|
+
const { appendContextUsageReceipt, linkActiveContextUsageReceipt } = await import("./work-evidence-store.js");
|
|
1627
|
+
const receipt = evidence_id
|
|
1628
|
+
? await appendContextUsageReceipt(process.cwd(), evidence_id, { task_digest, result_digest, outcome })
|
|
1629
|
+
: await linkActiveContextUsageReceipt(process.cwd(), { task_digest, result_digest, outcome });
|
|
1630
|
+
const payload = receipt
|
|
1631
|
+
? { schema_version: 1, recorded: true, receipt }
|
|
1632
|
+
: { schema_version: 1, recorded: false, receipt: null };
|
|
1633
|
+
return { content: [{ type: "text", text: asToolText(payload) }], structuredContent: payload };
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1950
1636
|
if (workEvidenceToolsEnabled) {
|
|
1951
1637
|
registerTool("work_evidence_list", {
|
|
1952
1638
|
description: "List bounded local Git-linked work-evidence records for this project.",
|
|
@@ -2031,7 +1717,7 @@ if (cliCommand === "extract") {
|
|
|
2031
1717
|
if (!content) {
|
|
2032
1718
|
throw new Error("No input provided on stdin.");
|
|
2033
1719
|
}
|
|
2034
|
-
const extracted = await
|
|
1720
|
+
const extracted = await extractMemoryCandidatesFromProvider(content, model, category);
|
|
2035
1721
|
output.write(`${JSON.stringify({
|
|
2036
1722
|
model: extracted.model,
|
|
2037
1723
|
count: extracted.candidates.length,
|
|
@@ -33,9 +33,11 @@ export const MCP_TOOL_CATALOG = {
|
|
|
33
33
|
context_explore: observation("Explore repository context", true),
|
|
34
34
|
route_check: observation("Check route", true),
|
|
35
35
|
context_pack_list: observation("List context packs"),
|
|
36
|
+
context_pack_tree: observation("Browse context pack hierarchy"),
|
|
36
37
|
context_pack_search: observation("Search context packs"),
|
|
37
38
|
context_pack_read: observation("Read context pack file"),
|
|
38
39
|
context_pack_related: observation("Traverse context pack links"),
|
|
40
|
+
context_usage_record: additive("Record context usage"),
|
|
39
41
|
work_evidence_list: observation("List work evidence"),
|
|
40
42
|
work_evidence_read: observation("Read work evidence"),
|
|
41
43
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
function truncateOutput(value, maxLength = 12000) {
|
|
2
|
+
return value.length <= maxLength
|
|
3
|
+
? value
|
|
4
|
+
: `${value.slice(0, maxLength)}\n...[truncated ${value.length - maxLength} chars]`;
|
|
5
|
+
}
|
|
6
|
+
function stripMarkdownFences(value) {
|
|
7
|
+
return value.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
8
|
+
}
|
|
9
|
+
function parseJsonResponse(value) {
|
|
10
|
+
const stripped = stripMarkdownFences(value);
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(stripped);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
const firstBrace = stripped.indexOf("{");
|
|
16
|
+
const lastBrace = stripped.lastIndexOf("}");
|
|
17
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
18
|
+
return JSON.parse(stripped.slice(firstBrace, lastBrace + 1));
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to parse JSON response: ${truncateOutput(stripped, 1000)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function sanitizeExtractionCandidates(value, fallbackCategory) {
|
|
24
|
+
if (!Array.isArray(value))
|
|
25
|
+
return [];
|
|
26
|
+
return value
|
|
27
|
+
.map((item) => {
|
|
28
|
+
if (!item || typeof item !== "object")
|
|
29
|
+
return null;
|
|
30
|
+
const raw = item;
|
|
31
|
+
const key = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
32
|
+
const candidateValue = typeof raw.value === "string" ? raw.value.trim() : "";
|
|
33
|
+
if (!key || !candidateValue)
|
|
34
|
+
return null;
|
|
35
|
+
const category = typeof raw.category === "string" && raw.category.trim()
|
|
36
|
+
? raw.category.trim()
|
|
37
|
+
: fallbackCategory;
|
|
38
|
+
const importance = typeof raw.importance === "number"
|
|
39
|
+
? Math.max(0, Math.min(1, raw.importance))
|
|
40
|
+
: undefined;
|
|
41
|
+
return { key, value: candidateValue, category, importance };
|
|
42
|
+
})
|
|
43
|
+
.filter((candidate) => candidate !== null);
|
|
44
|
+
}
|
|
45
|
+
export async function extractMemoryCandidates(content, modelOverride, category) {
|
|
46
|
+
const apiKey = process.env.CAIRN_LLM_API_KEY;
|
|
47
|
+
if (!apiKey)
|
|
48
|
+
throw new Error("CAIRN_LLM_API_KEY is not set.");
|
|
49
|
+
const rawUrl = process.env.CAIRN_LLM_API_URL;
|
|
50
|
+
if (!rawUrl)
|
|
51
|
+
throw new Error("CAIRN_LLM_API_URL is not set.");
|
|
52
|
+
const apiUrl = rawUrl.trim().replace(/\/+$/, "");
|
|
53
|
+
const model = (modelOverride ?? process.env.CAIRN_LLM_EXTRACTION_MODEL)?.trim();
|
|
54
|
+
if (!model)
|
|
55
|
+
throw new Error("CAIRN_LLM_EXTRACTION_MODEL is not set.");
|
|
56
|
+
const systemPrompt = [
|
|
57
|
+
"You extract durable memory candidates from development notes.",
|
|
58
|
+
"Return ONLY valid JSON, no markdown fences.",
|
|
59
|
+
"Schema: {\"candidates\":[{\"key\":\"decisions/cache-rule\",\"value\":\"...\",\"category\":\"decision\",\"importance\":0.92}]}",
|
|
60
|
+
"Only include genuinely reusable knowledge.",
|
|
61
|
+
"Skip trivial status notes, temporary branch details, and duplicated points.",
|
|
62
|
+
"Prefer short kebab-case keys with a useful prefix such as decisions/, pitfalls/, patterns/, bugs/, constraints/, preferences/, conventions/.",
|
|
63
|
+
"Do not invent dates unless they are explicitly present in the source text.",
|
|
64
|
+
category ? `Bias extraction toward category: ${category}.` : "",
|
|
65
|
+
].filter(Boolean).join(" ");
|
|
66
|
+
const response = await fetch(`${apiUrl}/chat/completions`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
model,
|
|
71
|
+
messages: [{ role: "system", content: systemPrompt }, { role: "user", content }],
|
|
72
|
+
temperature: 0.1,
|
|
73
|
+
max_tokens: 1200,
|
|
74
|
+
}),
|
|
75
|
+
signal: AbortSignal.timeout(120000),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
throw new Error(`Extraction request failed with ${response.status}: ${text}`);
|
|
80
|
+
}
|
|
81
|
+
const payload = (await response.json());
|
|
82
|
+
const rawContent = payload.choices?.[0]?.message?.content;
|
|
83
|
+
if (!rawContent)
|
|
84
|
+
throw new Error("Extraction model returned no content.");
|
|
85
|
+
const parsed = parseJsonResponse(rawContent);
|
|
86
|
+
return { model, candidates: sanitizeExtractionCandidates(parsed.candidates, category) };
|
|
87
|
+
}
|