@cairnkeep/cli 2.16.0 → 2.17.1
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 +34 -0
- package/README.md +1 -0
- package/docs/README.md +8 -0
- package/docs/agents.md +20 -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 +0 -1
- package/docs/learning/CURRICULUM-MAP.md +3 -1
- package/docs/learning/FEATURE-GUIDE.md +22 -3
- package/docs/learning/README.md +1 -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/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 +1 -1
- 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/operating.md +44 -0
- package/docs/privacy-and-data-flow.md +22 -0
- 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 +2 -2
- 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 +2 -0
- package/scripts/completion.sh +17 -6
- package/scripts/doctor.sh +4 -2
- package/scripts/terminal-prompts.mjs +5 -3
- package/scripts/test-completion.sh +8 -1
- package/scripts/test-doctor.sh +21 -0
- package/scripts/test-release-sbom.sh +4 -1
- package/scripts/test-setup-preflight.mjs +13 -0
- package/scripts/test-windows-native.mjs +30 -0
- package/scripts/windows-platform.mjs +8 -5
- package/templates/PROJECT-INSTRUCTIONS.md.template +0 -1
|
@@ -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
|
+
}
|