@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CONTRIBUTING.md +12 -0
  3. package/README.md +85 -590
  4. package/docs/README.md +87 -0
  5. package/docs/agents.md +193 -0
  6. package/docs/context-intelligence.md +143 -0
  7. package/docs/context-packs.md +33 -0
  8. package/docs/domain-knowledge.md +29 -0
  9. package/docs/ecosystem.md +23 -0
  10. package/docs/learning/CURRICULUM-MAP.md +3 -1
  11. package/docs/learning/FEATURE-GUIDE.md +23 -4
  12. package/docs/learning/README.md +4 -0
  13. package/docs/learning/lessons/L00-why-cairnkeep.md +3 -1
  14. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  15. package/docs/learning/lessons/L02-installation.md +2 -2
  16. package/docs/learning/lessons/L03-first-project.md +9 -10
  17. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  18. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  19. package/docs/learning/lessons/L21-context-packs.md +1 -1
  20. package/docs/learning/lessons/L22-native-windows.md +2 -2
  21. package/docs/learning/lessons/L23-guided-setup.md +18 -5
  22. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  23. package/docs/learning/lessons/L25-playbooks.md +1 -1
  24. package/docs/learning/lessons/L26-context-intelligence.md +180 -0
  25. package/docs/learning/tracks/evidence-and-evaluation.md +1 -0
  26. package/docs/learning/tracks/operator.md +1 -0
  27. package/docs/learning/tracks/practitioner.md +1 -0
  28. package/docs/learning/video-scripts/V26-context-intelligence.md +144 -0
  29. package/docs/native-windows.md +5 -0
  30. package/docs/operating.md +65 -3
  31. package/docs/privacy-and-data-flow.md +22 -0
  32. package/docs/quickstart.md +12 -4
  33. package/docs/releasing.md +27 -19
  34. package/docs/storage.md +21 -0
  35. package/docs/work-evidence.md +14 -0
  36. package/examples/eval/bundled-fake.json +1 -1
  37. package/mcp-memory-server/dist/context-pack-cli.js +3 -1
  38. package/mcp-memory-server/dist/context-pack-retrieval.js +413 -0
  39. package/mcp-memory-server/dist/context-pack.js +171 -3
  40. package/mcp-memory-server/dist/domain-retrieval-provider.js +257 -0
  41. package/mcp-memory-server/dist/index.js +73 -387
  42. package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
  43. package/mcp-memory-server/dist/memory-extraction.js +87 -0
  44. package/mcp-memory-server/dist/memory-proposal-cli.js +110 -0
  45. package/mcp-memory-server/dist/memory-proposal-schema.js +78 -0
  46. package/mcp-memory-server/dist/memory-proposal-store.js +273 -0
  47. package/mcp-memory-server/dist/platform-security.js +32 -26
  48. package/mcp-memory-server/dist/retrieval-benchmark.js +199 -0
  49. package/mcp-memory-server/dist/reviewed-memory-store.js +281 -0
  50. package/mcp-memory-server/dist/work-evidence-schema.js +17 -0
  51. package/mcp-memory-server/dist/work-evidence-store.js +57 -1
  52. package/mcp-memory-server/package.json +8 -1
  53. package/package.json +1 -1
  54. package/schemas/context-usage-receipt.schema.json +41 -0
  55. package/schemas/memory-proposal.schema.json +65 -0
  56. package/schemas/retrieval-benchmark.schema.json +64 -0
  57. package/scripts/cairn-cli.mjs +3 -0
  58. package/scripts/completion.sh +17 -6
  59. package/scripts/doctor.sh +4 -2
  60. package/scripts/setup.mjs +92 -13
  61. package/scripts/terminal-prompts.mjs +172 -0
  62. package/scripts/test-completion.sh +8 -1
  63. package/scripts/test-docs-navigation.sh +63 -0
  64. package/scripts/test-doctor.sh +21 -0
  65. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  66. package/scripts/test-release-sbom.sh +44 -16
  67. package/scripts/test-setup-preflight.mjs +130 -0
  68. package/scripts/test-windows-native.mjs +30 -0
  69. package/scripts/verify-docs-parity.sh +28 -28
  70. package/scripts/windows-platform.mjs +8 -5
@@ -7,6 +7,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
7
7
  import { promisify } from "node:util";
8
8
  import { z } from "zod";
9
9
  import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText } from "./embeddings.js";
10
+ import { doctorProgressiveContextCache, loadProgressiveContext, publicTree, selectHierarchicalFiles, } from "./context-pack-retrieval.js";
10
11
  import { indexOkfBundle, validateOkfBundle } from "./okf.js";
11
12
  import { atomicReplace, hardenPrivatePath, privatePathIsSafe } from "./platform-security.js";
12
13
  const execFileAsync = promisify(execFile);
@@ -547,7 +548,15 @@ async function acquirePointerLock(options) {
547
548
  catch (error) {
548
549
  if (error.code !== "EEXIST")
549
550
  throw error;
550
- const info = lstatSync(lock);
551
+ let info;
552
+ try {
553
+ info = lstatSync(lock);
554
+ }
555
+ catch (inspectionError) {
556
+ if (inspectionError.code === "ENOENT")
557
+ continue;
558
+ throw inspectionError;
559
+ }
551
560
  if (!info.isDirectory() || info.isSymbolicLink())
552
561
  throw new Error("Context pack project pointer lock is unsafe.");
553
562
  await delay(10);
@@ -782,7 +791,7 @@ export async function listVisibleContext(options) {
782
791
  }
783
792
  return { schema_version: 1, packs: [...grouped.values()] };
784
793
  }
785
- export async function searchVisibleContext(query, options) {
794
+ async function flatSearchVisibleContext(query, options) {
786
795
  const normalized = query.trim().toLocaleLowerCase("en");
787
796
  if (!normalized)
788
797
  throw new Error("Context pack search query must not be empty.");
@@ -829,6 +838,137 @@ export async function searchVisibleContext(query, options) {
829
838
  results.sort((a, b) => b.score - a.score || a.pack_digest.localeCompare(b.pack_digest, "en") || a.path.localeCompare(b.path, "en") || a.offset - b.offset);
830
839
  return { schema_version: 1, query, search_mode: "substring", results: results.slice(0, options.limit ?? 20) };
831
840
  }
841
+ function attachContextResultRefs(payload) {
842
+ const results = payload.results.map((result) => {
843
+ if (typeof result.file_digest !== "string")
844
+ throw new Error("Context search result is missing file provenance.");
845
+ return {
846
+ ...result,
847
+ chunk_digest: sha256("cairn-context-chunk-v1\0", result.pack_digest, "\0", result.path, "\0", result.file_digest, "\0", String(result.offset), "\0", result.text),
848
+ };
849
+ });
850
+ const resultDigest = sha256("cairn-context-results-v1\n", results.map(({ chunk_digest }) => chunk_digest).join("\n"));
851
+ return { ...payload, result_digest: resultDigest, results };
852
+ }
853
+ async function progressiveVisibleContext(options) {
854
+ const visible = await visiblePackFiles(options);
855
+ const inputs = await Promise.all(visible.map(async ({ pack, file }) => ({
856
+ pack_id: pack.manifest.id,
857
+ version: pack.manifest.version,
858
+ pack_digest: pack.digest,
859
+ pack_title: pack.manifest.title,
860
+ pack_description: pack.manifest.description,
861
+ path: file.path,
862
+ kind: file.kind,
863
+ title: file.title,
864
+ description: file.description,
865
+ keywords: file.keywords,
866
+ file_digest: file.sha256,
867
+ text: await readFile(join(pack.root, file.path), "utf8"),
868
+ })));
869
+ return { packs: await loadProgressiveContext(inputs, join(packBaseDir(), "cache", "context")), visible };
870
+ }
871
+ function packMatches(selector, pack) {
872
+ return pack.pack_digest === selector || pack.pack_digest.startsWith(selector) || pack.pack_id === selector || `${pack.pack_id}@${pack.version}` === selector;
873
+ }
874
+ export async function treeVisibleContext(options) {
875
+ const detail = options.detail ?? "abstract";
876
+ const { packs } = await progressiveVisibleContext(options);
877
+ const selected = options.pack ? packs.filter((pack) => packMatches(options.pack, pack)) : packs;
878
+ if (options.pack && selected.length !== 1)
879
+ throw new Error(selected.length ? "Context pack selector is ambiguous." : "Context pack is not enabled.");
880
+ return { schema_version: 1, detail, packs: selected.map((pack) => publicTree(pack, detail)) };
881
+ }
882
+ function progressiveFileKey(file) {
883
+ return `${file.pack_digest}:${file.path}`;
884
+ }
885
+ export async function searchVisibleContext(query, options) {
886
+ const strategy = options.strategy ?? "flat";
887
+ const detail = options.detail ?? "content";
888
+ if (strategy === "flat" && detail === "content" && !options.explain) {
889
+ const payload = await flatSearchVisibleContext(query, options);
890
+ return options.includeRefs ? attachContextResultRefs(payload) : payload;
891
+ }
892
+ const normalized = query.trim().toLocaleLowerCase("en");
893
+ if (!normalized)
894
+ throw new Error("Context pack search query must not be empty.");
895
+ const { packs, visible } = await progressiveVisibleContext(options);
896
+ const progressiveFiles = packs.flatMap((pack) => pack.files);
897
+ if (strategy === "flat") {
898
+ // Summary detail is one result per file. Retrieve all matching chunks
899
+ // before deduplication so a chatty first file cannot consume the public
900
+ // result limit and hide later matching files.
901
+ const flat = await flatSearchVisibleContext(query, { ...options, limit: Number.MAX_SAFE_INTEGER });
902
+ const summaries = new Map(progressiveFiles.map((file) => [progressiveFileKey(file), file]));
903
+ const seen = new Set();
904
+ const allResults = flat.results.flatMap((result) => {
905
+ const key = progressiveFileKey(result);
906
+ if (detail !== "content" && seen.has(key))
907
+ return [];
908
+ seen.add(key);
909
+ const summary = summaries.get(key);
910
+ return [{ ...result, offset: detail === "content" ? result.offset : 0, text: detail === "content" ? result.text : summary?.[detail] ?? result.text }];
911
+ });
912
+ const results = allResults.slice(0, options.limit ?? 20);
913
+ const trace = options.explain ? {
914
+ exact_leaf_bypass: false,
915
+ considered_files: progressiveFiles.length,
916
+ selected_files: new Set(results.map(progressiveFileKey)).size,
917
+ events: results.slice(0, 64).map((result) => ({ phase: "file", pack_digest: result.pack_digest, path: result.path, score: result.score, reason: "flat-result" })),
918
+ truncated: allResults.length > results.length || results.length > 64,
919
+ } : undefined;
920
+ const payload = { ...flat, strategy, detail, results, ...(trace ? { trace } : {}) };
921
+ return options.includeRefs ? attachContextResultRefs(payload) : payload;
922
+ }
923
+ const selection = selectHierarchicalFiles(query, packs);
924
+ const visibleByKey = new Map(visible.map((entry) => [`${entry.pack.digest}:${entry.file.path}`, entry]));
925
+ const selectedScores = new Map(selection.events.filter((event) => event.phase === "file" && event.path && event.pack_digest)
926
+ .map((event) => [`${event.pack_digest}:${event.path}`, event.score ?? 0]));
927
+ const results = [];
928
+ for (const file of selection.files) {
929
+ const source = visibleByKey.get(progressiveFileKey(file));
930
+ if (!source)
931
+ continue;
932
+ const base = { ...provenance(source.pack, source.file), ...await okfProvenance(source.pack, source.file) };
933
+ const summaryScore = selectedScores.get(progressiveFileKey(file)) ?? 0;
934
+ if (detail !== "content") {
935
+ results.push({ ...base, offset: 0, text: file[detail], score: summaryScore });
936
+ continue;
937
+ }
938
+ const fileChunks = chunks(file.text);
939
+ let matched = false;
940
+ for (const chunk of fileChunks) {
941
+ const searchable = `${file.title}\n${file.description}\n${file.keywords.join(" ")}\n${chunk.text}`.toLocaleLowerCase("en");
942
+ const occurrences = searchable.split(normalized).length - 1;
943
+ if (!occurrences && !selection.exactLeafBypass)
944
+ continue;
945
+ matched = true;
946
+ results.push({ ...base, offset: chunk.offset, text: chunk.text, score: selection.exactLeafBypass ? 100 : occurrences });
947
+ }
948
+ if (!matched && fileChunks[0])
949
+ results.push({ ...base, offset: fileChunks[0].offset, text: fileChunks[0].text, score: summaryScore });
950
+ }
951
+ results.sort((left, right) => right.score - left.score || left.pack_digest.localeCompare(right.pack_digest, "en") || left.path.localeCompare(right.path, "en") || left.offset - right.offset);
952
+ const limited = results.slice(0, options.limit ?? 20);
953
+ const trace = options.explain ? {
954
+ exact_leaf_bypass: selection.exactLeafBypass,
955
+ considered_packs: packs.length,
956
+ considered_files: progressiveFiles.length,
957
+ selected_files: selection.files.length,
958
+ events: selection.events,
959
+ truncated: selection.events.length >= 64 || results.length > limited.length,
960
+ } : undefined;
961
+ const payload = {
962
+ schema_version: 1,
963
+ query,
964
+ search_mode: "substring",
965
+ strategy,
966
+ detail,
967
+ results: limited,
968
+ ...(trace ? { trace } : {}),
969
+ };
970
+ return options.includeRefs ? attachContextResultRefs(payload) : payload;
971
+ }
832
972
  export async function readVisibleContext(packSelector, path, options) {
833
973
  const normalized = normalizePackPath(path);
834
974
  const visible = await visiblePackFiles(options);
@@ -873,7 +1013,8 @@ export async function relatedVisibleContext(packSelector, path, options) {
873
1013
  }
874
1014
  return { schema_version: 1, ...provenance(pack, file), ...await okfProvenance(pack, file), direction, results, diagnostics: index.diagnostics.filter(({ path: diagnosticPath }) => diagnosticPath === file.path) };
875
1015
  }
876
- export async function doctorContextPacks() {
1016
+ export async function doctorContextPacks(options = {}) {
1017
+ const progressiveCachePacks = [];
877
1018
  const issues = [];
878
1019
  const remnants = [];
879
1020
  let objects = 0;
@@ -883,6 +1024,23 @@ export async function doctorContextPacks() {
883
1024
  objects = installed.length;
884
1025
  for (const item of installed) {
885
1026
  const pack = await validateContextPack(objectRoot(item.digest));
1027
+ progressiveCachePacks.push({
1028
+ pack_digest: pack.digest,
1029
+ files: await Promise.all(pack.manifest.files.map(async (file) => ({
1030
+ pack_id: pack.manifest.id,
1031
+ version: pack.manifest.version,
1032
+ pack_digest: pack.digest,
1033
+ pack_title: pack.manifest.title,
1034
+ pack_description: pack.manifest.description,
1035
+ path: file.path,
1036
+ kind: file.kind,
1037
+ file_digest: file.sha256,
1038
+ title: file.title,
1039
+ description: file.description,
1040
+ keywords: file.keywords,
1041
+ text: await readFile(join(pack.root, file.path), "utf8"),
1042
+ }))),
1043
+ });
886
1044
  if (pack.manifest.source_format?.name === "okf") {
887
1045
  const graph = await indexOkfBundle(pack.root, pack.manifest.files.map(({ path }) => path), pack.manifest.source_format.version);
888
1046
  expectedGraphs.set(pack.digest, JSON.stringify(graph));
@@ -984,6 +1142,16 @@ export async function doctorContextPacks() {
984
1142
  issues.push(`Stale context pack skill approval: ${path} -> ${approval.path}`);
985
1143
  }
986
1144
  }
1145
+ const progressiveCache = await doctorProgressiveContextCache(join(packBaseDir(), "cache", "context"), progressiveCachePacks, { repair: options.repair });
1146
+ issues.push(...progressiveCache.issues);
1147
+ remnants.push(...progressiveCache.temporary_remnants);
1148
+ if (options.repair) {
1149
+ const contextCacheDirectory = join(packBaseDir(), "cache", "context");
1150
+ for (let index = remnants.length - 1; index >= 0; index -= 1) {
1151
+ if (remnants[index] === contextCacheDirectory || remnants[index].startsWith(`${contextCacheDirectory}${sep}`))
1152
+ remnants.splice(index, 1);
1153
+ }
1154
+ }
987
1155
  return { ok: issues.length === 0 && remnants.length === 0, objects, projects: pointers.length, issues, temporary_remnants: remnants };
988
1156
  }
989
1157
  export async function inspectContextPackUpdate(selector, options) {
@@ -0,0 +1,257 @@
1
+ import { isIP } from "node:net";
2
+ const OPENVIKING_RESPONSE_LIMIT = 1024 * 1024;
3
+ const OPENVIKING_DEFAULT_TIMEOUT_MS = 30_000;
4
+ const OPENVIKING_MAX_TIMEOUT_MS = 120_000;
5
+ export function resolveDomainRetrievalProvider(env = process.env) {
6
+ const configured = env.CAIRN_DOMAIN_RETRIEVAL_PROVIDER?.trim() || "anythingllm";
7
+ if (configured === "anythingllm" || configured === "openviking")
8
+ return configured;
9
+ throw new Error("CAIRN_DOMAIN_RETRIEVAL_PROVIDER must be either 'anythingllm' or 'openviking'.");
10
+ }
11
+ function isLoopbackHostname(hostname) {
12
+ const normalized = hostname.startsWith("[") && hostname.endsWith("]")
13
+ ? hostname.slice(1, -1)
14
+ : hostname;
15
+ if (normalized === "localhost" || normalized === "::1")
16
+ return true;
17
+ if (isIP(normalized) !== 4)
18
+ return false;
19
+ return normalized.split(".")[0] === "127";
20
+ }
21
+ function openVikingEndpoint(env) {
22
+ const configured = env.CAIRN_OPENVIKING_BASE_URL?.trim();
23
+ if (!configured) {
24
+ throw new Error("OpenViking base URL is not set; configure CAIRN_OPENVIKING_BASE_URL.");
25
+ }
26
+ let url;
27
+ try {
28
+ url = new URL(configured);
29
+ }
30
+ catch {
31
+ throw new Error("OpenViking base URL must be a valid HTTP(S) URL.");
32
+ }
33
+ if (url.username || url.password) {
34
+ throw new Error("OpenViking base URL must not contain embedded credentials.");
35
+ }
36
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
37
+ throw new Error("OpenViking base URL must use HTTP or HTTPS.");
38
+ }
39
+ if (url.protocol === "http:" && !isLoopbackHostname(url.hostname)) {
40
+ throw new Error("OpenViking base URL must use HTTPS unless it targets a loopback host.");
41
+ }
42
+ if (url.search || url.hash) {
43
+ throw new Error("OpenViking base URL must not contain a query string or fragment.");
44
+ }
45
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/api/v1/search/find`;
46
+ return url;
47
+ }
48
+ function openVikingTimeout(env) {
49
+ const raw = env.CAIRN_OPENVIKING_TIMEOUT_MS?.trim();
50
+ if (!raw)
51
+ return OPENVIKING_DEFAULT_TIMEOUT_MS;
52
+ const timeout = Number(raw);
53
+ if (!Number.isInteger(timeout) || timeout < 100 || timeout > OPENVIKING_MAX_TIMEOUT_MS) {
54
+ throw new Error(`CAIRN_OPENVIKING_TIMEOUT_MS must be an integer from 100 to ${OPENVIKING_MAX_TIMEOUT_MS}.`);
55
+ }
56
+ return timeout;
57
+ }
58
+ function openVikingTargetUri(workspace) {
59
+ if (!workspace.startsWith("viking://")) {
60
+ return `viking://resources/${encodeURIComponent(workspace)}`;
61
+ }
62
+ if (workspace !== "viking://resources"
63
+ && !/^viking:\/\/resources\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$/.test(workspace)) {
64
+ throw new Error("OpenViking workspace URI must stay below viking://resources.");
65
+ }
66
+ const path = workspace.slice("viking://resources".length);
67
+ let decoded;
68
+ try {
69
+ decoded = decodeURIComponent(path);
70
+ }
71
+ catch {
72
+ throw new Error("OpenViking workspace URI contains invalid percent encoding.");
73
+ }
74
+ if (decoded.split("/").some((segment) => segment === "." || segment === "..")) {
75
+ throw new Error("OpenViking workspace URI must not contain traversal segments.");
76
+ }
77
+ return workspace;
78
+ }
79
+ async function boundedResponseText(response) {
80
+ const advertisedLength = Number(response.headers.get("content-length"));
81
+ if (Number.isFinite(advertisedLength) && advertisedLength > OPENVIKING_RESPONSE_LIMIT) {
82
+ throw new Error("OpenViking response is too large.");
83
+ }
84
+ if (!response.body)
85
+ return "";
86
+ const reader = response.body.getReader();
87
+ const chunks = [];
88
+ let length = 0;
89
+ while (true) {
90
+ const { done, value } = await reader.read();
91
+ if (done)
92
+ break;
93
+ length += value.byteLength;
94
+ if (length > OPENVIKING_RESPONSE_LIMIT) {
95
+ await reader.cancel();
96
+ throw new Error("OpenViking response is too large.");
97
+ }
98
+ chunks.push(value);
99
+ }
100
+ const joined = new Uint8Array(length);
101
+ let offset = 0;
102
+ for (const chunk of chunks) {
103
+ joined.set(chunk, offset);
104
+ offset += chunk.byteLength;
105
+ }
106
+ return new TextDecoder("utf-8", { fatal: true }).decode(joined);
107
+ }
108
+ async function callAnythingLLM(workspace, query, env) {
109
+ const apiKey = env.ANYTHINGLLM_API_KEY;
110
+ if (!apiKey) {
111
+ throw new Error("ANYTHINGLLM_API_KEY is not set.");
112
+ }
113
+ const baseUrl = env.ANYTHINGLLM_BASE_URL ?? "http://localhost:3001";
114
+ const response = await fetch(`${baseUrl}/api/v1/workspace/${encodeURIComponent(workspace)}/chat`, {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${apiKey}`,
118
+ "Content-Type": "application/json",
119
+ },
120
+ body: JSON.stringify({
121
+ message: query,
122
+ mode: "query",
123
+ }),
124
+ signal: AbortSignal.timeout(120000),
125
+ });
126
+ if (!response.ok) {
127
+ const text = await response.text();
128
+ throw new Error(`AnythingLLM request failed with ${response.status}: ${text}`);
129
+ }
130
+ const payload = (await response.json());
131
+ const directText = [
132
+ payload.textResponse,
133
+ payload.response,
134
+ payload.message,
135
+ payload.text,
136
+ ].find((value) => typeof value === "string");
137
+ if (typeof directText === "string") {
138
+ return directText;
139
+ }
140
+ return JSON.stringify(payload, null, 2);
141
+ }
142
+ function parseOpenVikingResult(text) {
143
+ let payload;
144
+ try {
145
+ payload = JSON.parse(text);
146
+ }
147
+ catch {
148
+ throw new Error("OpenViking response is not valid JSON.");
149
+ }
150
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
151
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
152
+ }
153
+ const envelope = payload;
154
+ const result = envelope.result;
155
+ if (envelope.status !== "ok"
156
+ || !result
157
+ || typeof result !== "object"
158
+ || Array.isArray(result)) {
159
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
160
+ }
161
+ const candidate = result;
162
+ if (!Array.isArray(candidate.resources) || !Number.isInteger(candidate.total) || Number(candidate.total) < 0) {
163
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
164
+ }
165
+ const resources = [];
166
+ for (const resource of candidate.resources) {
167
+ if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
168
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
169
+ }
170
+ const item = resource;
171
+ if (typeof item.uri !== "string"
172
+ || (item.uri !== "viking://resources" && !item.uri.startsWith("viking://resources/"))
173
+ || typeof item.abstract !== "string"
174
+ || (item.context_type !== undefined && item.context_type !== "resource")
175
+ || (item.level !== undefined && (!Number.isInteger(item.level) || Number(item.level) < 0 || Number(item.level) > 2))
176
+ || (item.score !== undefined && (typeof item.score !== "number" || !Number.isFinite(item.score)))
177
+ || (item.category !== undefined && typeof item.category !== "string")
178
+ || (item.match_reason !== undefined && typeof item.match_reason !== "string")
179
+ || (item.overview !== undefined && item.overview !== null && typeof item.overview !== "string")) {
180
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
181
+ }
182
+ resources.push({
183
+ ...(item.context_type === "resource" ? { context_type: item.context_type } : {}),
184
+ uri: item.uri,
185
+ ...(item.level !== undefined ? { level: Number(item.level) } : {}),
186
+ ...(item.score !== undefined ? { score: Number(item.score) } : {}),
187
+ ...(typeof item.category === "string" ? { category: item.category } : {}),
188
+ ...(typeof item.match_reason === "string" ? { match_reason: item.match_reason } : {}),
189
+ abstract: item.abstract,
190
+ ...(item.overview === null || typeof item.overview === "string" ? { overview: item.overview } : {}),
191
+ });
192
+ }
193
+ if (Number(candidate.total) < resources.length) {
194
+ throw new Error("OpenViking response does not contain a useful retrieval payload.");
195
+ }
196
+ return { resources, total: Number(candidate.total) };
197
+ }
198
+ async function callOpenViking(workspace, query, remote, env) {
199
+ if (env.CAIRN_OPENVIKING !== "1") {
200
+ throw new Error("OpenViking retrieval requires CAIRN_OPENVIKING=1.");
201
+ }
202
+ if (remote && env.CAIRN_OPENVIKING_MCP_HTTP !== "1") {
203
+ throw new Error("Remote OpenViking retrieval requires CAIRN_OPENVIKING_MCP_HTTP=1.");
204
+ }
205
+ const endpoint = openVikingEndpoint(env);
206
+ const timeout = openVikingTimeout(env);
207
+ const apiKey = (env.CAIRN_OPENVIKING_API_KEY ?? env.OPENVIKING_API_KEY)?.trim();
208
+ let response;
209
+ try {
210
+ response = await fetch(endpoint, {
211
+ method: "POST",
212
+ headers: {
213
+ ...(apiKey ? { "X-API-Key": apiKey } : {}),
214
+ "Content-Type": "application/json",
215
+ },
216
+ body: JSON.stringify({
217
+ query,
218
+ target_uri: openVikingTargetUri(workspace),
219
+ context_type: ["resource"],
220
+ limit: 10,
221
+ }),
222
+ redirect: "manual",
223
+ signal: AbortSignal.timeout(timeout),
224
+ });
225
+ }
226
+ catch (error) {
227
+ if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
228
+ throw new Error("OpenViking request timed out.");
229
+ }
230
+ throw new Error(`OpenViking request failed: ${error instanceof Error ? error.message : String(error)}`);
231
+ }
232
+ if (response.status >= 300 && response.status < 400) {
233
+ throw new Error(`OpenViking request refused redirect status ${response.status}.`);
234
+ }
235
+ if (!response.ok) {
236
+ throw new Error(`OpenViking request failed with ${response.status}.`);
237
+ }
238
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().includes("application/json")) {
239
+ throw new Error("OpenViking response is not JSON.");
240
+ }
241
+ let text;
242
+ try {
243
+ text = await boundedResponseText(response);
244
+ }
245
+ catch (error) {
246
+ if (error instanceof TypeError)
247
+ throw new Error("OpenViking response is not valid UTF-8.");
248
+ throw error;
249
+ }
250
+ return JSON.stringify(parseOpenVikingResult(text), null, 2);
251
+ }
252
+ export async function queryDomainKnowledge({ workspace, query, remote = false, env = process.env, }) {
253
+ const provider = resolveDomainRetrievalProvider(env);
254
+ if (provider === "anythingllm")
255
+ return callAnythingLLM(workspace, query, env);
256
+ return callOpenViking(workspace, query, remote, env);
257
+ }