@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|