@cairnkeep/cli 2.3.1 → 2.4.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 +69 -0
- package/README.md +221 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +1 -1
- package/docs/design/meta-agent.md +439 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -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 +1 -1
- package/docs/operating.md +531 -4
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +4 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +48 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +37 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -11,8 +11,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { artifactIdSchema, artifactKindSchema, artifactMediaTypeSchema, artifactNodeRefSchema, artifactProvenanceSchema, artifactSessionRefSchema, compactionSummaryInputContentSchema, diffContentSchema, generatedFileInputContentSchema, isArtifactHttpEnabled, isArtifactStoreEnabled, testOutputContentSchema, } from "./artifact-schema.js";
|
|
15
|
+
import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
|
|
14
16
|
import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
|
|
15
17
|
import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
|
|
18
|
+
import { canonicalTagsSchema, isTypedMemoryNodesEnabled, memoryImportEnvelopeSchema, noteNodeSchema, nodeTypeSchema, } from "./node-schema.js";
|
|
19
|
+
import { attachNodeMetadata, applyReviewedTypedNode, commitMemoryImport, createTypedNode, deleteTypedNode, getTypedNode, invalidateReviewedTypedNode, listTypedHistory, planMemoryImport, supersedeTypedNode, } from "./node-store.js";
|
|
16
20
|
const MINIMUM_NODE_MAJOR = 22;
|
|
17
21
|
const nodeMajor = Number.parseInt(process.versions.node, 10);
|
|
18
22
|
if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
|
|
@@ -33,6 +37,24 @@ const REVIEWED_NAMESPACE = "__reviewed__";
|
|
|
33
37
|
const REVIEW_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
34
38
|
const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
35
39
|
const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
40
|
+
const NODE_ADDRESS_SPACE_SCHEMA = z.enum(["memory", "project-notes", "shared-notes"]);
|
|
41
|
+
const scopeMutationTails = new Map();
|
|
42
|
+
async function serializeScopeMutation(scopePath, operation) {
|
|
43
|
+
const previous = scopeMutationTails.get(scopePath) ?? Promise.resolve();
|
|
44
|
+
let release = () => { };
|
|
45
|
+
const turn = new Promise((resolveTurn) => { release = resolveTurn; });
|
|
46
|
+
const tail = previous.then(() => turn);
|
|
47
|
+
scopeMutationTails.set(scopePath, tail);
|
|
48
|
+
await previous;
|
|
49
|
+
try {
|
|
50
|
+
return await operation();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
release();
|
|
54
|
+
if (scopeMutationTails.get(scopePath) === tail)
|
|
55
|
+
scopeMutationTails.delete(scopePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
36
58
|
function expandHome(value) {
|
|
37
59
|
if (value === "~") {
|
|
38
60
|
return homedir();
|
|
@@ -127,6 +149,7 @@ function parseServerContext(headers) {
|
|
|
127
149
|
return {
|
|
128
150
|
projectId: rawProjectId || undefined,
|
|
129
151
|
memoryConfig,
|
|
152
|
+
remote: true,
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
function resolveScopePath(scope, options = {}) {
|
|
@@ -236,11 +259,17 @@ async function listEntries(scope, prefix = "", options = {}) {
|
|
|
236
259
|
}
|
|
237
260
|
try {
|
|
238
261
|
const entries = await agent.kv.list(prefix);
|
|
239
|
-
|
|
262
|
+
const visible = visibleEntries(entries.map(({ key, value }) => ({
|
|
240
263
|
scope,
|
|
241
264
|
key,
|
|
242
265
|
value: normalizeValue(value),
|
|
243
266
|
})), options.includeHistory ?? false);
|
|
267
|
+
if (!options.typed)
|
|
268
|
+
return visible;
|
|
269
|
+
const typed = [];
|
|
270
|
+
for (const entry of visible)
|
|
271
|
+
typed.push(await attachNodeMetadata(agent, scope, entry));
|
|
272
|
+
return typed;
|
|
244
273
|
}
|
|
245
274
|
finally {
|
|
246
275
|
await agent.close();
|
|
@@ -256,7 +285,8 @@ async function readKey(scope, key, options = {}) {
|
|
|
256
285
|
if (value === undefined) {
|
|
257
286
|
return [];
|
|
258
287
|
}
|
|
259
|
-
|
|
288
|
+
const entry = { scope, key, value: normalizeValue(value) };
|
|
289
|
+
return [options.typed ? await attachNodeMetadata(agent, scope, entry) : entry];
|
|
260
290
|
}
|
|
261
291
|
finally {
|
|
262
292
|
await agent.close();
|
|
@@ -264,10 +294,39 @@ async function readKey(scope, key, options = {}) {
|
|
|
264
294
|
}
|
|
265
295
|
function searchEntries(entries, query) {
|
|
266
296
|
const needle = query.toLowerCase();
|
|
267
|
-
return entries.filter(({ key, value }) =>
|
|
268
|
-
|
|
297
|
+
return entries.filter(({ key, value, node_type, tags }) => key.toLowerCase().includes(needle)
|
|
298
|
+
|| value.toLowerCase().includes(needle)
|
|
299
|
+
|| node_type?.toLowerCase().includes(needle)
|
|
300
|
+
|| tags?.some((tag) => tag.includes(needle)));
|
|
301
|
+
}
|
|
302
|
+
function filterEntries(entries, filters) {
|
|
303
|
+
return entries.filter((entry) => {
|
|
304
|
+
const tags = entry.tags ?? [];
|
|
305
|
+
if (filters.node_types && !filters.node_types.includes(entry.node_type ?? "memory"))
|
|
306
|
+
return false;
|
|
307
|
+
if (filters.tags_all && !filters.tags_all.every((tag) => tags.includes(tag)))
|
|
308
|
+
return false;
|
|
309
|
+
if (filters.tags_any && !filters.tags_any.some((tag) => tags.includes(tag)))
|
|
310
|
+
return false;
|
|
311
|
+
return true;
|
|
269
312
|
});
|
|
270
313
|
}
|
|
314
|
+
function memoryFiltersFromInput(input) {
|
|
315
|
+
return {
|
|
316
|
+
node_types: input.node_types === undefined ? undefined : z.array(nodeTypeSchema).min(1).parse(input.node_types),
|
|
317
|
+
tags_all: input.tags_all === undefined ? undefined : canonicalTagsSchema.pipe(z.array(z.string()).min(1)).parse(input.tags_all),
|
|
318
|
+
tags_any: input.tags_any === undefined ? undefined : canonicalTagsSchema.pipe(z.array(z.string()).min(1)).parse(input.tags_any),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function compareMemoryEntries(left, right) {
|
|
322
|
+
return left.scope.localeCompare(right.scope) || left.key.localeCompare(right.key);
|
|
323
|
+
}
|
|
324
|
+
function isExactTextHit(entry, query) {
|
|
325
|
+
const needle = query.toLowerCase();
|
|
326
|
+
return entry.key.toLowerCase().includes(needle)
|
|
327
|
+
|| entry.node_type?.toLowerCase() === needle
|
|
328
|
+
|| Boolean(entry.tags?.includes(needle));
|
|
329
|
+
}
|
|
271
330
|
function asToolText(value) {
|
|
272
331
|
return JSON.stringify(value, null, 2);
|
|
273
332
|
}
|
|
@@ -464,28 +523,32 @@ async function callAnythingLLM(workspace, query) {
|
|
|
464
523
|
return JSON.stringify(payload, null, 2);
|
|
465
524
|
}
|
|
466
525
|
function entryText(entry) {
|
|
467
|
-
|
|
526
|
+
const metadata = [entry.node_type, ...(entry.tags ?? [])].filter(Boolean).join("\n");
|
|
527
|
+
return [entry.key, entry.value, metadata].filter(Boolean).join("\n");
|
|
468
528
|
}
|
|
469
529
|
function embeddingCachePath(scope, projectId) {
|
|
470
530
|
return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope, { projectId }))}.json`);
|
|
471
531
|
}
|
|
472
|
-
async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId) {
|
|
532
|
+
async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId, options = {}) {
|
|
473
533
|
const scopes = getSearchScopes(scope, config);
|
|
474
534
|
const embeddingConfig = getEmbeddingConfig();
|
|
475
535
|
const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
|
|
476
536
|
scope: candidate,
|
|
477
|
-
entries: await listEntries(candidate, "", { projectId }),
|
|
537
|
+
entries: filterEntries(await listEntries(candidate, "", { projectId, typed: options.typed }), options.filters ?? {}),
|
|
478
538
|
})));
|
|
479
539
|
const allEntries = perScopeEntries.flatMap((group) => group.entries);
|
|
480
540
|
if (allEntries.length === 0) {
|
|
481
541
|
return { results: [], mode: embeddingConfig ? "semantic" : "substring" };
|
|
482
542
|
}
|
|
483
|
-
const substringFallback = () =>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
|
|
543
|
+
const substringFallback = () => {
|
|
544
|
+
const matches = searchEntries(allEntries, query);
|
|
545
|
+
if (options.typed)
|
|
546
|
+
matches.sort(compareMemoryEntries);
|
|
547
|
+
return {
|
|
548
|
+
results: matches.map((entry) => ({ ...entry, score: 1 })).slice(0, topK),
|
|
549
|
+
mode: "substring",
|
|
550
|
+
};
|
|
551
|
+
};
|
|
489
552
|
if (!embeddingConfig) {
|
|
490
553
|
return substringFallback();
|
|
491
554
|
}
|
|
@@ -522,15 +585,21 @@ async function semanticSearch(scope, query, topK, minScore, config = getMemoryCo
|
|
|
522
585
|
for (const cache of caches) {
|
|
523
586
|
cache.save();
|
|
524
587
|
}
|
|
525
|
-
const
|
|
588
|
+
const exact = options.typed
|
|
589
|
+
? allEntries.filter((entry) => isExactTextHit(entry, query)).sort(compareMemoryEntries).map((entry) => ({ ...entry, score: 1 }))
|
|
590
|
+
: [];
|
|
591
|
+
const semanticCandidates = options.typed
|
|
592
|
+
? allEntries.filter((entry) => !isExactTextHit(entry, query))
|
|
593
|
+
: allEntries;
|
|
594
|
+
const rankedSemantic = semanticCandidates
|
|
526
595
|
.map((entry) => {
|
|
527
596
|
const vector = vectors.get(entry);
|
|
528
597
|
const score = vector && queryVector ? cosineSimilarity(queryVector, vector) : 0;
|
|
529
598
|
return { ...entry, score };
|
|
530
599
|
})
|
|
531
600
|
.filter((entry) => entry.score >= minScore)
|
|
532
|
-
.sort((left, right) => right.score - left.score)
|
|
533
|
-
|
|
601
|
+
.sort((left, right) => right.score - left.score || (options.typed ? compareMemoryEntries(left, right) : 0));
|
|
602
|
+
const ranked = [...exact, ...rankedSemantic].slice(0, topK);
|
|
534
603
|
return { results: ranked, mode: "semantic", model: embeddingConfig.model };
|
|
535
604
|
}
|
|
536
605
|
catch {
|
|
@@ -753,10 +822,197 @@ function gitToplevel(cwd) {
|
|
|
753
822
|
// only allows one connected transport per server). All instances share the
|
|
754
823
|
// module-level helpers + AgentFS below. Enables a single long-lived process to
|
|
755
824
|
// serve many concurrent clients within one trusted server-side storage domain.
|
|
756
|
-
function
|
|
825
|
+
function buildMemoryServer(context, capabilitySnapshot, withCapability) {
|
|
757
826
|
const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
|
|
827
|
+
const capabilityEnabled = (id) => capabilitySnapshot?.capabilities.find((state) => state.id === id)?.enabled ?? true;
|
|
828
|
+
const registerCapabilityTool = (id, name, config, callback) => {
|
|
829
|
+
if (!capabilityEnabled(id))
|
|
830
|
+
return;
|
|
831
|
+
const handler = capabilitySnapshot && withCapability
|
|
832
|
+
? withCapability({
|
|
833
|
+
projectRoot: process.cwd(),
|
|
834
|
+
snapshot: capabilitySnapshot,
|
|
835
|
+
capabilityId: id,
|
|
836
|
+
classification: {
|
|
837
|
+
harness: "other",
|
|
838
|
+
source: "mcp",
|
|
839
|
+
transport: context.remote ? "http" : "stdio",
|
|
840
|
+
},
|
|
841
|
+
}, callback)
|
|
842
|
+
: callback;
|
|
843
|
+
server.registerTool(name, config, handler);
|
|
844
|
+
};
|
|
758
845
|
const memoryConfig = () => context.memoryConfig ?? getMemoryConfig();
|
|
759
846
|
const scopeOptions = { projectId: context.projectId };
|
|
847
|
+
const typedNodesEnabled = isTypedMemoryNodesEnabled();
|
|
848
|
+
const artifactToolsEnabled = isArtifactStoreEnabled() && (!context.remote || isArtifactHttpEnabled());
|
|
849
|
+
if (artifactToolsEnabled && context.remote && !context.projectId) {
|
|
850
|
+
throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
|
|
851
|
+
}
|
|
852
|
+
let fallbackArtifactSessionRef;
|
|
853
|
+
const resolveArtifactSessionRef = (sessionRef) => {
|
|
854
|
+
if (sessionRef)
|
|
855
|
+
return sessionRef;
|
|
856
|
+
fallbackArtifactSessionRef ??= `cairn:${randomUUID()}`;
|
|
857
|
+
return fallbackArtifactSessionRef;
|
|
858
|
+
};
|
|
859
|
+
const artifactProjectRoot = async () => {
|
|
860
|
+
if (!context.remote)
|
|
861
|
+
return process.cwd();
|
|
862
|
+
if (!context.projectId) {
|
|
863
|
+
throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
|
|
864
|
+
}
|
|
865
|
+
const { resolveRemoteArtifactProjectRoot } = await import("./artifact-store.js");
|
|
866
|
+
return resolveRemoteArtifactProjectRoot(getBaseDir(), context.projectId);
|
|
867
|
+
};
|
|
868
|
+
const noteTargetOptions = (scope, address_space) => {
|
|
869
|
+
if (scope !== "project")
|
|
870
|
+
throw new Error("INVALID_SCOPE: note address spaces require scope project.");
|
|
871
|
+
if (context.remote && address_space === "project-notes" && !context.projectId) {
|
|
872
|
+
throw new Error("INVALID_SCOPE: remote project notes require X-Cairn-Project.");
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
address_space,
|
|
876
|
+
...(context.projectId ? { projectId: context.projectId } : { projectRoot: process.cwd() }),
|
|
877
|
+
};
|
|
878
|
+
};
|
|
879
|
+
if (artifactToolsEnabled) {
|
|
880
|
+
const artifactIdentifierSchema = z.string().min(4).max(40).regex(/^art_[0-9a-f-]*$/i);
|
|
881
|
+
const artifactWriteToolSchema = z.object({
|
|
882
|
+
kind: artifactKindSchema,
|
|
883
|
+
session_ref: artifactSessionRefSchema.optional(),
|
|
884
|
+
node_ref: artifactNodeRefSchema.optional(),
|
|
885
|
+
media_type: artifactMediaTypeSchema,
|
|
886
|
+
provenance: artifactProvenanceSchema,
|
|
887
|
+
supersedes: artifactIdSchema.optional(),
|
|
888
|
+
content: z.union([
|
|
889
|
+
compactionSummaryInputContentSchema,
|
|
890
|
+
diffContentSchema,
|
|
891
|
+
testOutputContentSchema,
|
|
892
|
+
generatedFileInputContentSchema,
|
|
893
|
+
]),
|
|
894
|
+
}).strict();
|
|
895
|
+
server.registerTool("artifact_write", {
|
|
896
|
+
description: "Write one bounded inline artifact to the immutable project artifact store.",
|
|
897
|
+
inputSchema: artifactWriteToolSchema,
|
|
898
|
+
annotations: {
|
|
899
|
+
readOnlyHint: false,
|
|
900
|
+
idempotentHint: true,
|
|
901
|
+
},
|
|
902
|
+
}, async (input) => {
|
|
903
|
+
const projectRoot = await artifactProjectRoot();
|
|
904
|
+
const sessionRef = resolveArtifactSessionRef(input.session_ref);
|
|
905
|
+
const { putArtifact } = await import("./artifact-store.js");
|
|
906
|
+
const result = await putArtifact(projectRoot, { ...input, session_ref: sessionRef });
|
|
907
|
+
const artifact = result.artifact;
|
|
908
|
+
const payload = {
|
|
909
|
+
schema_version: artifact.schema_version,
|
|
910
|
+
artifact_id: artifact.artifact_id,
|
|
911
|
+
kind: artifact.kind,
|
|
912
|
+
session_ref: artifact.session_ref,
|
|
913
|
+
content_digest: artifact.content_digest,
|
|
914
|
+
logical_bytes: artifact.logical_bytes,
|
|
915
|
+
stored_bytes: artifact.stored_bytes,
|
|
916
|
+
status: result.idempotent ? "existing" : "created",
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
920
|
+
structuredContent: payload,
|
|
921
|
+
};
|
|
922
|
+
});
|
|
923
|
+
server.registerTool("artifact_read", {
|
|
924
|
+
description: "Read one artifact by exact ID or an unambiguous ID prefix.",
|
|
925
|
+
inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
|
|
926
|
+
annotations: {
|
|
927
|
+
readOnlyHint: true,
|
|
928
|
+
idempotentHint: true,
|
|
929
|
+
},
|
|
930
|
+
}, async ({ artifact_id }) => {
|
|
931
|
+
const projectRoot = await artifactProjectRoot();
|
|
932
|
+
const { readArtifact } = await import("./artifact-store.js");
|
|
933
|
+
const payload = await readArtifact(artifact_id, projectRoot);
|
|
934
|
+
return {
|
|
935
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
936
|
+
structuredContent: payload,
|
|
937
|
+
};
|
|
938
|
+
});
|
|
939
|
+
server.registerTool("artifact_list", {
|
|
940
|
+
description: "List artifact metadata newest first with exact filters and opaque pagination.",
|
|
941
|
+
inputSchema: z.object({
|
|
942
|
+
session_ref: artifactSessionRefSchema.optional(),
|
|
943
|
+
kind: artifactKindSchema.optional(),
|
|
944
|
+
node_ref: artifactNodeRefSchema.optional(),
|
|
945
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
946
|
+
cursor: z.string().min(1).max(256).optional(),
|
|
947
|
+
}).strict(),
|
|
948
|
+
annotations: {
|
|
949
|
+
readOnlyHint: true,
|
|
950
|
+
idempotentHint: true,
|
|
951
|
+
},
|
|
952
|
+
}, async (filters) => {
|
|
953
|
+
const projectRoot = await artifactProjectRoot();
|
|
954
|
+
const { listArtifacts } = await import("./artifact-store.js");
|
|
955
|
+
const listed = await listArtifacts(projectRoot, filters);
|
|
956
|
+
const payload = {
|
|
957
|
+
schema_version: listed.schema_version,
|
|
958
|
+
artifacts: listed.artifacts.map((artifact) => ({
|
|
959
|
+
schema_version: artifact.schema_version,
|
|
960
|
+
artifact_id: artifact.artifact_id,
|
|
961
|
+
kind: artifact.kind,
|
|
962
|
+
created_at: artifact.created_at,
|
|
963
|
+
session_ref: artifact.session_ref,
|
|
964
|
+
...(artifact.node_ref ? { node_ref: artifact.node_ref } : {}),
|
|
965
|
+
media_type: artifact.media_type,
|
|
966
|
+
logical_bytes: artifact.logical_bytes,
|
|
967
|
+
stored_bytes: artifact.stored_bytes,
|
|
968
|
+
content_digest: artifact.content_digest,
|
|
969
|
+
...(artifact.supersedes ? { supersedes: artifact.supersedes } : {}),
|
|
970
|
+
})),
|
|
971
|
+
logical_bytes: listed.logical_bytes,
|
|
972
|
+
...(listed.next_cursor ? { next_cursor: listed.next_cursor } : {}),
|
|
973
|
+
};
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
976
|
+
structuredContent: payload,
|
|
977
|
+
};
|
|
978
|
+
});
|
|
979
|
+
server.registerTool("artifact_delete", {
|
|
980
|
+
description: "Hard-delete one artifact by exact ID or an unambiguous ID prefix.",
|
|
981
|
+
inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
|
|
982
|
+
annotations: {
|
|
983
|
+
readOnlyHint: false,
|
|
984
|
+
destructiveHint: true,
|
|
985
|
+
idempotentHint: true,
|
|
986
|
+
},
|
|
987
|
+
}, async ({ artifact_id }) => {
|
|
988
|
+
const projectRoot = await artifactProjectRoot();
|
|
989
|
+
const { deleteArtifact } = await import("./artifact-store.js");
|
|
990
|
+
let payload;
|
|
991
|
+
try {
|
|
992
|
+
const deleted = await deleteArtifact(artifact_id, projectRoot);
|
|
993
|
+
payload = {
|
|
994
|
+
schema_version: deleted.schema_version,
|
|
995
|
+
artifact_id: deleted.artifact_id,
|
|
996
|
+
deleted: deleted.deleted,
|
|
997
|
+
missing: false,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
if (!(error instanceof Error) || error.message !== "Artifact not found.")
|
|
1002
|
+
throw error;
|
|
1003
|
+
payload = {
|
|
1004
|
+
schema_version: 1,
|
|
1005
|
+
artifact_id,
|
|
1006
|
+
deleted: false,
|
|
1007
|
+
missing: true,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
1012
|
+
structuredContent: payload,
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
760
1016
|
server.registerTool("memory_read", {
|
|
761
1017
|
description: "Read an exact key or search memory entries across AgentFS scopes.",
|
|
762
1018
|
// Plain object schema — a .refine() wrapper (ZodEffects) makes the SDK
|
|
@@ -765,20 +1021,35 @@ function createMemoryServer(context = {}) {
|
|
|
765
1021
|
scope: z.string(),
|
|
766
1022
|
key: z.string().optional(),
|
|
767
1023
|
query: z.string().optional(),
|
|
1024
|
+
...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
|
|
768
1025
|
}),
|
|
769
1026
|
annotations: {
|
|
770
1027
|
readOnlyHint: true,
|
|
771
1028
|
idempotentHint: true,
|
|
772
1029
|
},
|
|
773
|
-
}, async (
|
|
1030
|
+
}, async (toolInput) => {
|
|
1031
|
+
const { scope, key, query } = toolInput;
|
|
774
1032
|
if (Boolean(key) === Boolean(query)) {
|
|
775
1033
|
throw new Error("Provide exactly one of key or query.");
|
|
776
1034
|
}
|
|
1035
|
+
const addressSpace = "address_space" in toolInput && toolInput.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(toolInput.address_space) : undefined;
|
|
1036
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1037
|
+
const noteStore = await import("./note-store.js");
|
|
1038
|
+
const target = noteTargetOptions(scope, addressSpace);
|
|
1039
|
+
const noteResults = key
|
|
1040
|
+
? [noteStore.readAddressedNote({ ...target, key })].filter((entry) => entry !== null)
|
|
1041
|
+
: noteStore.searchAddressedNotes({ ...target, query: query ?? "" });
|
|
1042
|
+
const sortedNotes = noteResults.sort((left, right) => left.key.localeCompare(right.key));
|
|
1043
|
+
return {
|
|
1044
|
+
content: [{ type: "text", text: asToolText(sortedNotes) }],
|
|
1045
|
+
structuredContent: { results: sortedNotes },
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
777
1048
|
const config = memoryConfig();
|
|
778
1049
|
const scopes = getSearchScopes(scope, config);
|
|
779
1050
|
const results = key
|
|
780
|
-
? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, scopeOptions)))).flat()
|
|
781
|
-
: searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", scopeOptions)))).flat(), query ?? "");
|
|
1051
|
+
? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, { ...scopeOptions, typed: typedNodesEnabled })))).flat()
|
|
1052
|
+
: searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", { ...scopeOptions, typed: typedNodesEnabled })))).flat(), query ?? "");
|
|
782
1053
|
const sorted = results.sort((left, right) => {
|
|
783
1054
|
return `${left.scope}:${left.key}`.localeCompare(`${right.scope}:${right.key}`);
|
|
784
1055
|
});
|
|
@@ -787,16 +1058,40 @@ function createMemoryServer(context = {}) {
|
|
|
787
1058
|
structuredContent: { results: sorted },
|
|
788
1059
|
};
|
|
789
1060
|
});
|
|
790
|
-
|
|
1061
|
+
registerCapabilityTool("memory.write", "memory_write", {
|
|
791
1062
|
description: "Write a memory entry to a scoped AgentFS database and optionally promote it.",
|
|
792
1063
|
inputSchema: z.object({
|
|
793
1064
|
scope: z.string(),
|
|
794
1065
|
key: z.string().min(1),
|
|
795
1066
|
value: z.string(),
|
|
796
1067
|
promote_to: z.string().optional(),
|
|
1068
|
+
...(typedNodesEnabled ? {
|
|
1069
|
+
address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
|
|
1070
|
+
node_type: nodeTypeSchema.optional(),
|
|
1071
|
+
tags: canonicalTagsSchema.optional(),
|
|
1072
|
+
note: noteNodeSchema.optional(),
|
|
1073
|
+
} : {}),
|
|
797
1074
|
}),
|
|
798
|
-
}, async (
|
|
1075
|
+
}, async (input) => {
|
|
1076
|
+
const { scope, key, value, promote_to } = input;
|
|
1077
|
+
const nodeType = "node_type" in input && input.node_type !== undefined ? nodeTypeSchema.parse(input.node_type) : undefined;
|
|
1078
|
+
const tags = "tags" in input && input.tags !== undefined ? canonicalTagsSchema.parse(input.tags) : undefined;
|
|
1079
|
+
const note = "note" in input ? input.note : undefined;
|
|
1080
|
+
const metadataAware = "address_space" in input || nodeType !== undefined || tags !== undefined || note !== undefined;
|
|
799
1081
|
assertWritableMemoryKey(key);
|
|
1082
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1083
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1084
|
+
if (promote_to)
|
|
1085
|
+
throw new Error("INVALID_SCOPE: note writes do not support promote_to.");
|
|
1086
|
+
const noteStore = await import("./note-store.js");
|
|
1087
|
+
const payload = await noteStore.createAddressedNote({ ...noteTargetOptions(scope, addressSpace), key, value, note, node_type: nodeType, tags });
|
|
1088
|
+
return {
|
|
1089
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1090
|
+
structuredContent: payload,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (note !== undefined)
|
|
1094
|
+
throw new Error("UNSUPPORTED_TARGET: note payloads require a note address space.");
|
|
800
1095
|
const targets = promote_to && promote_to !== scope ? [scope, promote_to] : [scope];
|
|
801
1096
|
// Collision-safe: in the unified store, writes from different repos/machines
|
|
802
1097
|
// can share a key. If a different value already exists, preserve the old one
|
|
@@ -810,6 +1105,28 @@ function createMemoryServer(context = {}) {
|
|
|
810
1105
|
throw new Error(`Unable to open scope ${target}.`);
|
|
811
1106
|
}
|
|
812
1107
|
try {
|
|
1108
|
+
if (typedNodesEnabled) {
|
|
1109
|
+
const current = await getTypedNode(agent, target, key);
|
|
1110
|
+
if (metadataAware) {
|
|
1111
|
+
await createTypedNode({ agent, scope: target, key, value, node_type: nodeType, tags });
|
|
1112
|
+
}
|
|
1113
|
+
else if (!current) {
|
|
1114
|
+
await createTypedNode({ agent, scope: target, key, value });
|
|
1115
|
+
}
|
|
1116
|
+
else if (current.value !== value) {
|
|
1117
|
+
const changed = await supersedeTypedNode({
|
|
1118
|
+
agent,
|
|
1119
|
+
scope: target,
|
|
1120
|
+
key,
|
|
1121
|
+
value,
|
|
1122
|
+
reason: "collision-safe write in unified store",
|
|
1123
|
+
});
|
|
1124
|
+
if (changed.snapshot_key && changed.previous_value !== undefined) {
|
|
1125
|
+
collisions.push({ scope: target, snapshot_key: changed.snapshot_key, previous_value: changed.previous_value });
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
813
1130
|
const previous = await agent.kv.get(key);
|
|
814
1131
|
const previousNorm = previous === undefined ? undefined : normalizeValue(previous);
|
|
815
1132
|
if (previousNorm !== undefined && previousNorm !== value) {
|
|
@@ -839,17 +1156,37 @@ function createMemoryServer(context = {}) {
|
|
|
839
1156
|
inputSchema: z.object({
|
|
840
1157
|
scope: z.string(),
|
|
841
1158
|
prefix: z.string().optional(),
|
|
1159
|
+
...(typedNodesEnabled ? {
|
|
1160
|
+
address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
|
|
1161
|
+
node_types: z.array(nodeTypeSchema).min(1).optional(),
|
|
1162
|
+
tags_all: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
|
|
1163
|
+
tags_any: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
|
|
1164
|
+
} : {}),
|
|
842
1165
|
}),
|
|
843
1166
|
annotations: {
|
|
844
1167
|
readOnlyHint: true,
|
|
845
1168
|
idempotentHint: true,
|
|
846
1169
|
},
|
|
847
|
-
}, async (
|
|
848
|
-
const
|
|
1170
|
+
}, async (input) => {
|
|
1171
|
+
const { scope, prefix } = input;
|
|
1172
|
+
const filters = typedNodesEnabled ? memoryFiltersFromInput(input) : {};
|
|
1173
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1174
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1175
|
+
const noteStore = await import("./note-store.js");
|
|
1176
|
+
const entries = noteStore.listAddressedNotes({ ...noteTargetOptions(scope, addressSpace), prefix, ...filters });
|
|
1177
|
+
const keys = entries.map((entry) => entry.key);
|
|
1178
|
+
const nodes = entries.map(({ value: _value, ...entry }) => entry);
|
|
1179
|
+
return {
|
|
1180
|
+
content: [{ type: "text", text: asToolText(keys) }],
|
|
1181
|
+
structuredContent: { keys, nodes },
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
const entries = filterEntries(await listEntries(scope, prefix ?? "", { ...scopeOptions, typed: typedNodesEnabled }), filters);
|
|
849
1185
|
const keys = entries.map((entry) => entry.key).sort();
|
|
1186
|
+
const nodes = entries.map(({ value: _value, ...entry }) => entry).sort((left, right) => left.key.localeCompare(right.key));
|
|
850
1187
|
return {
|
|
851
1188
|
content: [{ type: "text", text: asToolText(keys) }],
|
|
852
|
-
structuredContent: { keys },
|
|
1189
|
+
structuredContent: typedNodesEnabled ? { keys, nodes } : { keys },
|
|
853
1190
|
};
|
|
854
1191
|
});
|
|
855
1192
|
server.registerTool("memory_delete", {
|
|
@@ -857,38 +1194,79 @@ function createMemoryServer(context = {}) {
|
|
|
857
1194
|
inputSchema: z.object({
|
|
858
1195
|
scope: z.string(),
|
|
859
1196
|
key: z.string().min(1),
|
|
1197
|
+
...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
|
|
860
1198
|
}),
|
|
861
|
-
}, async (
|
|
862
|
-
|
|
1199
|
+
}, async (input) => {
|
|
1200
|
+
const { scope, key } = input;
|
|
1201
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1202
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1203
|
+
const noteStore = await import("./note-store.js");
|
|
1204
|
+
const payload = await noteStore.deleteAddressedNote({ ...noteTargetOptions(scope, addressSpace), key });
|
|
1205
|
+
return {
|
|
1206
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1207
|
+
structuredContent: payload,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (typedNodesEnabled)
|
|
1211
|
+
assertWritableMemoryKey(key);
|
|
1212
|
+
else if (isReviewedKey(key)) {
|
|
863
1213
|
throw new Error(`Keys under ${REVIEWED_NAMESPACE}/ are reserved for reviewed-memory provenance.`);
|
|
864
1214
|
}
|
|
865
1215
|
const agent = await openScope(scope, false, scopeOptions);
|
|
1216
|
+
let typedResult;
|
|
866
1217
|
if (agent) {
|
|
867
1218
|
try {
|
|
868
|
-
|
|
1219
|
+
if (typedNodesEnabled)
|
|
1220
|
+
typedResult = await deleteTypedNode({ agent, scope, key });
|
|
1221
|
+
else
|
|
1222
|
+
await agent.kv.delete(key);
|
|
869
1223
|
}
|
|
870
1224
|
finally {
|
|
871
1225
|
await agent.close();
|
|
872
1226
|
}
|
|
873
1227
|
}
|
|
1228
|
+
const payload = typedNodesEnabled
|
|
1229
|
+
? { ok: true, scope, key, deleted: typedResult?.deleted ?? false, missing: typedResult?.missing ?? true, snapshot_key: typedResult?.snapshot_key ?? null, ...(typedResult?.final_snapshot ? { final_snapshot: typedResult.final_snapshot } : {}) }
|
|
1230
|
+
: { ok: true, scope, key };
|
|
874
1231
|
return {
|
|
875
|
-
content: [{ type: "text", text: asToolText(
|
|
876
|
-
structuredContent:
|
|
1232
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1233
|
+
structuredContent: payload,
|
|
877
1234
|
};
|
|
878
1235
|
});
|
|
879
|
-
|
|
1236
|
+
registerCapabilityTool("memory.search", "memory_search", {
|
|
880
1237
|
description: "Semantic search across AgentFS memory scopes using the configured embedding endpoint, ranked by cosine similarity. Falls back to substring matching when embeddings are unavailable. Use this to find memory by meaning rather than by exact key.",
|
|
881
1238
|
inputSchema: z.object({
|
|
882
1239
|
scope: z.string(),
|
|
883
1240
|
query: z.string().min(1),
|
|
884
1241
|
top_k: z.number().int().min(1).max(50).optional(),
|
|
885
1242
|
min_score: z.number().min(0).max(1).optional(),
|
|
1243
|
+
...(typedNodesEnabled ? {
|
|
1244
|
+
address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
|
|
1245
|
+
node_types: z.array(nodeTypeSchema).min(1).optional(),
|
|
1246
|
+
tags_all: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
|
|
1247
|
+
tags_any: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
|
|
1248
|
+
} : {}),
|
|
886
1249
|
}),
|
|
887
1250
|
annotations: {
|
|
888
1251
|
readOnlyHint: true,
|
|
889
1252
|
},
|
|
890
|
-
}, async (
|
|
891
|
-
const {
|
|
1253
|
+
}, async (input) => {
|
|
1254
|
+
const { scope, query, top_k, min_score } = input;
|
|
1255
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1256
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1257
|
+
const noteStore = await import("./note-store.js");
|
|
1258
|
+
const entries = noteStore.searchAddressedNotes({ ...noteTargetOptions(scope, addressSpace), query, ...memoryFiltersFromInput(input) }).slice(0, top_k ?? 8);
|
|
1259
|
+
const results = entries.map((entry) => ({ ...entry, score: 1 }));
|
|
1260
|
+
const payload = { mode: "exact", count: results.length, results };
|
|
1261
|
+
return {
|
|
1262
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1263
|
+
structuredContent: payload,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0, memoryConfig(), context.projectId, {
|
|
1267
|
+
typed: typedNodesEnabled,
|
|
1268
|
+
filters: typedNodesEnabled ? memoryFiltersFromInput(input) : {},
|
|
1269
|
+
});
|
|
892
1270
|
const payload = { mode, model, count: results.length, results };
|
|
893
1271
|
return {
|
|
894
1272
|
content: [{ type: "text", text: asToolText(payload) }],
|
|
@@ -923,14 +1301,51 @@ function createMemoryServer(context = {}) {
|
|
|
923
1301
|
key: z.string().min(1),
|
|
924
1302
|
value: z.string(),
|
|
925
1303
|
reason: z.string().optional(),
|
|
1304
|
+
...(typedNodesEnabled ? {
|
|
1305
|
+
address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
|
|
1306
|
+
node_type: nodeTypeSchema.optional(),
|
|
1307
|
+
tags: canonicalTagsSchema.optional(),
|
|
1308
|
+
note: noteNodeSchema.optional(),
|
|
1309
|
+
} : {}),
|
|
926
1310
|
}),
|
|
927
|
-
}, async (
|
|
1311
|
+
}, async (input) => {
|
|
1312
|
+
const { scope, key, value, reason } = input;
|
|
928
1313
|
assertWritableMemoryKey(key);
|
|
1314
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1315
|
+
const note = "note" in input ? input.note : undefined;
|
|
1316
|
+
const nodeType = "node_type" in input && input.node_type !== undefined ? nodeTypeSchema.parse(input.node_type) : undefined;
|
|
1317
|
+
const tags = "tags" in input && input.tags !== undefined ? canonicalTagsSchema.parse(input.tags) : undefined;
|
|
1318
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1319
|
+
const noteStore = await import("./note-store.js");
|
|
1320
|
+
const payload = await noteStore.supersedeAddressedNote({ ...noteTargetOptions(scope, addressSpace), key, value, reason, note, node_type: nodeType, tags });
|
|
1321
|
+
return {
|
|
1322
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1323
|
+
structuredContent: payload,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
if (note !== undefined)
|
|
1327
|
+
throw new Error("UNSUPPORTED_TARGET: note payloads require a note address space.");
|
|
929
1328
|
const agent = await openScope(scope, true, scopeOptions);
|
|
930
1329
|
if (!agent) {
|
|
931
1330
|
throw new Error(`Unable to open scope ${scope}.`);
|
|
932
1331
|
}
|
|
933
1332
|
try {
|
|
1333
|
+
if (typedNodesEnabled) {
|
|
1334
|
+
const changed = await supersedeTypedNode({
|
|
1335
|
+
agent,
|
|
1336
|
+
scope,
|
|
1337
|
+
key,
|
|
1338
|
+
value,
|
|
1339
|
+
node_type: nodeType,
|
|
1340
|
+
tags,
|
|
1341
|
+
reason,
|
|
1342
|
+
});
|
|
1343
|
+
const payload = { ...changed };
|
|
1344
|
+
return {
|
|
1345
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1346
|
+
structuredContent: payload,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
934
1349
|
const previous = await agent.kv.get(key);
|
|
935
1350
|
if (previous === undefined) {
|
|
936
1351
|
await agent.kv.set(key, value);
|
|
@@ -1015,18 +1430,33 @@ function createMemoryServer(context = {}) {
|
|
|
1015
1430
|
});
|
|
1016
1431
|
}
|
|
1017
1432
|
}
|
|
1018
|
-
const previous = await agent.kv.get(key);
|
|
1019
|
-
const previousValue = previous === undefined ? undefined : normalizeValue(previous);
|
|
1020
1433
|
let snapshotKey = null;
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1434
|
+
if (typedNodesEnabled) {
|
|
1435
|
+
const changed = await applyReviewedTypedNode({
|
|
1436
|
+
agent,
|
|
1437
|
+
scope,
|
|
1438
|
+
review_id,
|
|
1439
|
+
key,
|
|
1440
|
+
value,
|
|
1441
|
+
node_type: "memory",
|
|
1442
|
+
tags: [],
|
|
1443
|
+
in_transaction: true,
|
|
1027
1444
|
});
|
|
1445
|
+
snapshotKey = changed.snapshot_key;
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
const previous = await agent.kv.get(key);
|
|
1449
|
+
const previousValue = previous === undefined ? undefined : normalizeValue(previous);
|
|
1450
|
+
if (previousValue !== undefined && previousValue !== value) {
|
|
1451
|
+
snapshotKey = historySnapshotKey(key, appliedAt);
|
|
1452
|
+
await agent.kv.set(snapshotKey, {
|
|
1453
|
+
value: previousValue,
|
|
1454
|
+
superseded_at: appliedAt,
|
|
1455
|
+
superseded_reason: `reviewed memory ${review_id}`,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
await agent.kv.set(key, value);
|
|
1028
1459
|
}
|
|
1029
|
-
await agent.kv.set(key, value);
|
|
1030
1460
|
await agent.kv.set(recordKey, {
|
|
1031
1461
|
schema_version: 1,
|
|
1032
1462
|
review_id,
|
|
@@ -1124,13 +1554,26 @@ function createMemoryServer(context = {}) {
|
|
|
1124
1554
|
if (current !== undefined) {
|
|
1125
1555
|
const currentValue = normalizeValue(current);
|
|
1126
1556
|
if (hashText(currentValue) === record.value_hash) {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1557
|
+
if (typedNodesEnabled) {
|
|
1558
|
+
const changed = await invalidateReviewedTypedNode({
|
|
1559
|
+
agent,
|
|
1560
|
+
scope,
|
|
1561
|
+
review_id,
|
|
1562
|
+
key: record.key,
|
|
1563
|
+
reason,
|
|
1564
|
+
in_transaction: true,
|
|
1565
|
+
});
|
|
1566
|
+
snapshotKey = changed.snapshot_key;
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
snapshotKey = historySnapshotKey(record.key, invalidatedAt);
|
|
1570
|
+
await agent.kv.set(snapshotKey, {
|
|
1571
|
+
value: currentValue,
|
|
1572
|
+
superseded_at: invalidatedAt,
|
|
1573
|
+
superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
|
|
1574
|
+
});
|
|
1575
|
+
await agent.kv.delete(record.key);
|
|
1576
|
+
}
|
|
1134
1577
|
removed = true;
|
|
1135
1578
|
}
|
|
1136
1579
|
else {
|
|
@@ -1171,12 +1614,52 @@ function createMemoryServer(context = {}) {
|
|
|
1171
1614
|
inputSchema: z.object({
|
|
1172
1615
|
scope: z.string(),
|
|
1173
1616
|
key: z.string().min(1),
|
|
1617
|
+
...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
|
|
1174
1618
|
}),
|
|
1175
1619
|
annotations: {
|
|
1176
1620
|
readOnlyHint: true,
|
|
1177
1621
|
idempotentHint: true,
|
|
1178
1622
|
},
|
|
1179
|
-
}, async (
|
|
1623
|
+
}, async (input) => {
|
|
1624
|
+
const { scope, key } = input;
|
|
1625
|
+
const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
|
|
1626
|
+
if (addressSpace && addressSpace !== "memory") {
|
|
1627
|
+
const noteStore = await import("./note-store.js");
|
|
1628
|
+
const target = noteTargetOptions(scope, addressSpace);
|
|
1629
|
+
const currentNode = noteStore.readAddressedNote({ ...target, key });
|
|
1630
|
+
const history = noteStore.listAddressedNoteHistory({ ...target, key });
|
|
1631
|
+
const payload = { scope, key, current: currentNode?.value ?? null, history, current_node: currentNode, history_nodes: history };
|
|
1632
|
+
return {
|
|
1633
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1634
|
+
structuredContent: payload,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
if (typedNodesEnabled) {
|
|
1638
|
+
const agent = await openScope(scope, false, scopeOptions);
|
|
1639
|
+
let currentNode = null;
|
|
1640
|
+
let historyNodes = [];
|
|
1641
|
+
if (agent) {
|
|
1642
|
+
try {
|
|
1643
|
+
currentNode = await getTypedNode(agent, scope, key);
|
|
1644
|
+
historyNodes = await listTypedHistory(agent, key);
|
|
1645
|
+
}
|
|
1646
|
+
finally {
|
|
1647
|
+
await agent.close();
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const payload = {
|
|
1651
|
+
scope,
|
|
1652
|
+
key,
|
|
1653
|
+
current: currentNode?.value ?? null,
|
|
1654
|
+
history: historyNodes,
|
|
1655
|
+
current_node: currentNode,
|
|
1656
|
+
history_nodes: historyNodes,
|
|
1657
|
+
};
|
|
1658
|
+
return {
|
|
1659
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1660
|
+
structuredContent: payload,
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1180
1663
|
const current = await readKey(scope, key, scopeOptions);
|
|
1181
1664
|
const history = (await listEntries(scope, historyPrefix(key), {
|
|
1182
1665
|
includeHistory: true,
|
|
@@ -1194,6 +1677,59 @@ function createMemoryServer(context = {}) {
|
|
|
1194
1677
|
structuredContent: payload,
|
|
1195
1678
|
};
|
|
1196
1679
|
});
|
|
1680
|
+
if (typedNodesEnabled) {
|
|
1681
|
+
server.registerTool("memory_import", {
|
|
1682
|
+
description: "Validate, plan, and atomically import a bounded batch of typed memory nodes.",
|
|
1683
|
+
inputSchema: memoryImportEnvelopeSchema,
|
|
1684
|
+
annotations: {
|
|
1685
|
+
idempotentHint: true,
|
|
1686
|
+
},
|
|
1687
|
+
}, async (input) => {
|
|
1688
|
+
const envelope = memoryImportEnvelopeSchema.parse(input);
|
|
1689
|
+
if (envelope.address_space !== "memory") {
|
|
1690
|
+
const noteStore = await import("./note-store.js");
|
|
1691
|
+
const addressSpace = envelope.address_space;
|
|
1692
|
+
const plan = noteStore.planNoteImport({ input: envelope, ...noteTargetOptions(envelope.scope, addressSpace) });
|
|
1693
|
+
const payload = envelope.dry_run ? plan.result : await noteStore.commitNoteImport(plan);
|
|
1694
|
+
return {
|
|
1695
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1696
|
+
structuredContent: payload,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
// Resolve containment before any database is opened or directory is created.
|
|
1700
|
+
const scopePath = resolveScopePath(envelope.scope, scopeOptions);
|
|
1701
|
+
const existing = await openScope(envelope.scope, false, scopeOptions);
|
|
1702
|
+
let plan;
|
|
1703
|
+
try {
|
|
1704
|
+
plan = await planMemoryImport(existing, envelope);
|
|
1705
|
+
}
|
|
1706
|
+
finally {
|
|
1707
|
+
await existing?.close();
|
|
1708
|
+
}
|
|
1709
|
+
if (envelope.dry_run) {
|
|
1710
|
+
const payload = await commitMemoryImport(null, plan);
|
|
1711
|
+
return {
|
|
1712
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1713
|
+
structuredContent: payload,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
const payload = await serializeScopeMutation(scopePath, async () => {
|
|
1717
|
+
const agent = await openScope(envelope.scope, true, scopeOptions);
|
|
1718
|
+
if (!agent)
|
|
1719
|
+
throw new Error(`Unable to open scope ${envelope.scope}.`);
|
|
1720
|
+
try {
|
|
1721
|
+
return await commitMemoryImport(agent, plan);
|
|
1722
|
+
}
|
|
1723
|
+
finally {
|
|
1724
|
+
await agent.close();
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
return {
|
|
1728
|
+
content: [{ type: "text", text: asToolText(payload) }],
|
|
1729
|
+
structuredContent: payload,
|
|
1730
|
+
};
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1197
1733
|
server.registerTool("domain_knowledge_query", {
|
|
1198
1734
|
description: "Query an AnythingLLM workspace in query mode for domain knowledge.",
|
|
1199
1735
|
inputSchema: z.object({
|
|
@@ -1262,7 +1798,7 @@ function createMemoryServer(context = {}) {
|
|
|
1262
1798
|
},
|
|
1263
1799
|
};
|
|
1264
1800
|
});
|
|
1265
|
-
|
|
1801
|
+
registerCapabilityTool("context.explore", "context_explore", {
|
|
1266
1802
|
description: "Delegate a natural-language repo-exploration query to the external token_miser explore binary (FastContext-backed). Returns compact path:line-range citations. Requires CAIRN_EXPLORE_BINARY (absolute path to the token_miser binary) and a repo_root (per-call param or CAIRN_EXPLORE_REPO_ROOT env). Thin adapter — token_miser owns all exploration logic.",
|
|
1267
1803
|
inputSchema: z.object({
|
|
1268
1804
|
query: z.string().min(1),
|
|
@@ -1292,7 +1828,7 @@ function createMemoryServer(context = {}) {
|
|
|
1292
1828
|
structuredContent: payload,
|
|
1293
1829
|
};
|
|
1294
1830
|
});
|
|
1295
|
-
|
|
1831
|
+
registerCapabilityTool("route.check", "route_check", {
|
|
1296
1832
|
description: "Check reachability of the external token_miser routing/tiering proxy via its /health endpoint. Requires CAIRN_ROUTE_ENDPOINT (base URL of an already-running token_miser instance). Thin adapter — token_miser owns all routing/tiering logic; this tool neither hosts a proxy nor learns which tier serves a request.",
|
|
1297
1833
|
inputSchema: z.object({
|
|
1298
1834
|
timeout_seconds: z.number().int().min(1).max(60).optional(),
|
|
@@ -1345,6 +1881,15 @@ function createMemoryServer(context = {}) {
|
|
|
1345
1881
|
});
|
|
1346
1882
|
return server;
|
|
1347
1883
|
}
|
|
1884
|
+
function createMemoryServer(context = {}) {
|
|
1885
|
+
const contractEnabled = isCapabilityContractEnabled();
|
|
1886
|
+
if (!contractEnabled)
|
|
1887
|
+
return buildMemoryServer(context);
|
|
1888
|
+
return Promise.all([
|
|
1889
|
+
resolveCapabilityStatus({ projectRoot: process.cwd() }),
|
|
1890
|
+
import("./capability-adapter.js"),
|
|
1891
|
+
]).then(([snapshot, adapter]) => buildMemoryServer(context, snapshot, adapter.withCapability));
|
|
1892
|
+
}
|
|
1348
1893
|
// One-shot CLI: `node dist/index.js wakeup` prints project-scope memory for the
|
|
1349
1894
|
// SessionStart hook. Reads a file-snapshot copy of ./.agentfs/project.db so it
|
|
1350
1895
|
// never contends with a running cairn-memory MCP that holds the exclusive lock
|
|
@@ -1481,7 +2026,7 @@ if (httpPort > 0) {
|
|
|
1481
2026
|
onsessioninitialized: (id) => { sessions.set(id, transport); },
|
|
1482
2027
|
onsessionclosed: (id) => { sessions.delete(id); },
|
|
1483
2028
|
});
|
|
1484
|
-
const session = createMemoryServer(context);
|
|
2029
|
+
const session = await createMemoryServer(context);
|
|
1485
2030
|
await session.connect(transport);
|
|
1486
2031
|
return transport.handleRequest(request);
|
|
1487
2032
|
};
|
|
@@ -1540,7 +2085,7 @@ if (httpPort > 0) {
|
|
|
1540
2085
|
} process.exit(0); });
|
|
1541
2086
|
}
|
|
1542
2087
|
else {
|
|
1543
|
-
const server = createMemoryServer();
|
|
2088
|
+
const server = await createMemoryServer();
|
|
1544
2089
|
const transport = new StdioServerTransport();
|
|
1545
2090
|
process.on("SIGINT", async () => {
|
|
1546
2091
|
await server.close();
|