@aliyunrds/ctxdb 0.0.5 → 0.0.8-beta.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/README.md +3 -3
- package/dist/{chunk-HAUTENYD.js → chunk-6S5RJYBC.js} +20 -77
- package/dist/{chunk-7NOXAU2X.js → chunk-C62I23HL.js} +41 -3
- package/dist/chunk-KEQMJ6IO.js +69 -0
- package/dist/{chunk-QSSNPN3M.js → chunk-U3T5O6NX.js} +57 -14
- package/dist/cli/main.js +428 -172
- package/dist/hooks/session-start.js +71 -32
- package/dist/hooks/stop.js +94 -21
- package/dist/hooks/user-prompt-submit.js +50 -45
- package/dist/setup/skills/contextdb-knowledge/SKILL.md +189 -0
- package/dist/setup/skills/contextdb-memory/SKILL.md +182 -0
- package/package.json +3 -4
- package/dist/setup/skills/cli-only/SKILL.md +0 -116
- package/dist/setup/skills/hooks-driven/SKILL.md +0 -144
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
fetchKbCatalogBlock,
|
|
3
4
|
recallTurn
|
|
4
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C62I23HL.js";
|
|
6
|
+
import "../chunk-6S5RJYBC.js";
|
|
7
|
+
import {
|
|
8
|
+
isCircuitOpen
|
|
9
|
+
} from "../chunk-KEQMJ6IO.js";
|
|
5
10
|
import {
|
|
6
11
|
HttpClient,
|
|
7
12
|
agentFromArgvWithFallback,
|
|
@@ -9,7 +14,10 @@ import {
|
|
|
9
14
|
isComplete,
|
|
10
15
|
load,
|
|
11
16
|
setDebug
|
|
12
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-U3T5O6NX.js";
|
|
18
|
+
|
|
19
|
+
// src/hooks/session-start.ts
|
|
20
|
+
import { pathToFileURL } from "url";
|
|
13
21
|
|
|
14
22
|
// src/lib/warmup-recall.ts
|
|
15
23
|
import { execSync } from "child_process";
|
|
@@ -63,7 +71,42 @@ async function warmupRecall(cwd, cfg, client) {
|
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
// src/hooks/session-start.ts
|
|
66
|
-
var HOOK_TIMEOUT_MS =
|
|
74
|
+
var HOOK_TIMEOUT_MS = 5e3;
|
|
75
|
+
function timeout(ms) {
|
|
76
|
+
return new Promise((resolve) => setTimeout(() => resolve(null), ms));
|
|
77
|
+
}
|
|
78
|
+
async function composeSessionStart(cfg, agent, client, cwd, timeoutMs = HOOK_TIMEOUT_MS) {
|
|
79
|
+
const kbInjectHere = cfg.kbCatalogInjection === "session_start";
|
|
80
|
+
const warmupPromise = cfg.warmupRecall ? Promise.race([warmupRecall(cwd, cfg, client), timeout(timeoutMs).then(() => null)]) : Promise.resolve(null);
|
|
81
|
+
const kbPromise = kbInjectHere ? Promise.race([fetchKbCatalogBlock(client, agent).catch(() => ""), timeout(timeoutMs).then(() => "")]) : Promise.resolve("");
|
|
82
|
+
const [result, kbBlock] = await Promise.all([warmupPromise, kbPromise]);
|
|
83
|
+
const warmupTimedOut = cfg.warmupRecall && result === null;
|
|
84
|
+
const warmupCtx = result && result.ok ? result.additionalContext || "" : "";
|
|
85
|
+
if (!warmupCtx && result && !result.ok) {
|
|
86
|
+
debug("warmup", `no result: ${result.reason}`);
|
|
87
|
+
}
|
|
88
|
+
let ctx = warmupCtx;
|
|
89
|
+
if (kbBlock) ctx = ctx ? `${ctx}
|
|
90
|
+
|
|
91
|
+
${kbBlock}` : kbBlock;
|
|
92
|
+
return {
|
|
93
|
+
ctx,
|
|
94
|
+
memoryCount: result?.ok ? result.memoryCount : 0,
|
|
95
|
+
kbChunkCount: result?.ok ? result.knowledgeChunkCount : 0,
|
|
96
|
+
kbCatalogLines: kbBlock ? kbBlock.split("\n").length : 0,
|
|
97
|
+
warmupTimedOut
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function formatSessionStartStdout(agent, ctx) {
|
|
101
|
+
if (agent === "codex") return ctx + "\n";
|
|
102
|
+
const out = {
|
|
103
|
+
hookSpecificOutput: {
|
|
104
|
+
hookEventName: "SessionStart",
|
|
105
|
+
additionalContext: ctx
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return JSON.stringify(out) + "\n";
|
|
109
|
+
}
|
|
67
110
|
async function readStdinJson() {
|
|
68
111
|
let raw = "";
|
|
69
112
|
for await (const chunk of process.stdin) raw += chunk;
|
|
@@ -75,9 +118,6 @@ async function readStdinJson() {
|
|
|
75
118
|
return {};
|
|
76
119
|
}
|
|
77
120
|
}
|
|
78
|
-
function timeout(ms) {
|
|
79
|
-
return new Promise((resolve) => setTimeout(() => resolve(null), ms));
|
|
80
|
-
}
|
|
81
121
|
async function main() {
|
|
82
122
|
try {
|
|
83
123
|
const event = await readStdinJson();
|
|
@@ -93,8 +133,13 @@ async function main() {
|
|
|
93
133
|
const cfg = load({ agent });
|
|
94
134
|
setDebug(cfg.debug);
|
|
95
135
|
debug("warmup", "start", { cwd, userId: cfg.userId });
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
const kbInjectHere = cfg.kbCatalogInjection === "session_start";
|
|
137
|
+
if (!isComplete(cfg) || !cfg.warmupRecall && !kbInjectHere) {
|
|
138
|
+
debug("warmup", "skip (config incomplete or both warmup+kb off)");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (isCircuitOpen(agent, cfg.baseUrl)) {
|
|
142
|
+
debug("warmup", "skip (circuit open)");
|
|
98
143
|
return 0;
|
|
99
144
|
}
|
|
100
145
|
const client = new HttpClient({
|
|
@@ -102,34 +147,21 @@ async function main() {
|
|
|
102
147
|
apiKey: cfg.apiKey,
|
|
103
148
|
timeoutMs: HOOK_TIMEOUT_MS
|
|
104
149
|
});
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
timeout(HOOK_TIMEOUT_MS).then(() => null)
|
|
108
|
-
]);
|
|
109
|
-
if (!result) {
|
|
150
|
+
const composed = await composeSessionStart(cfg, agent, client, cwd);
|
|
151
|
+
if (composed.warmupTimedOut) {
|
|
110
152
|
process.stderr.write("ctxdb warmup: timeout\n");
|
|
111
|
-
return 0;
|
|
112
153
|
}
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
if (!composed.ctx) return 0;
|
|
155
|
+
const { memoryCount, kbChunkCount, kbCatalogLines, ctx } = composed;
|
|
156
|
+
debug(
|
|
157
|
+
"warmup",
|
|
158
|
+
`ok, memories=${memoryCount} kb=${kbChunkCount} kb_catalog=${kbCatalogLines}`
|
|
159
|
+
);
|
|
118
160
|
process.stderr.write(
|
|
119
|
-
`ctxdb warmup: ok (${
|
|
161
|
+
`ctxdb warmup: ok (${memoryCount} memories, ${kbChunkCount} kb chunks, kb_catalog=${kbCatalogLines} lines)
|
|
120
162
|
`
|
|
121
163
|
);
|
|
122
|
-
|
|
123
|
-
process.stdout.write(result.additionalContext + "\n");
|
|
124
|
-
} else {
|
|
125
|
-
const out = {
|
|
126
|
-
hookSpecificOutput: {
|
|
127
|
-
hookEventName: "SessionStart",
|
|
128
|
-
additionalContext: result.additionalContext
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
132
|
-
}
|
|
164
|
+
process.stdout.write(formatSessionStartStdout(agent, ctx));
|
|
133
165
|
return 0;
|
|
134
166
|
} catch (err) {
|
|
135
167
|
process.stderr.write(`ctxdb warmup: unexpected error: ${err?.message ?? err}
|
|
@@ -137,4 +169,11 @@ async function main() {
|
|
|
137
169
|
return 0;
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
|
-
|
|
172
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
173
|
+
main().then((code) => process.exit(code));
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
HOOK_TIMEOUT_MS,
|
|
177
|
+
composeSessionStart,
|
|
178
|
+
formatSessionStartStdout
|
|
179
|
+
};
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
isCircuitOpen,
|
|
4
|
+
isConnectionError,
|
|
5
|
+
resetCircuit,
|
|
6
|
+
tripCircuit
|
|
7
|
+
} from "../chunk-KEQMJ6IO.js";
|
|
2
8
|
import {
|
|
3
9
|
CtxdbError,
|
|
4
10
|
HttpClient,
|
|
@@ -6,7 +12,7 @@ import {
|
|
|
6
12
|
debug,
|
|
7
13
|
load,
|
|
8
14
|
setDebug
|
|
9
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-U3T5O6NX.js";
|
|
10
16
|
|
|
11
17
|
// src/lib/capture-orchestrator.ts
|
|
12
18
|
import {
|
|
@@ -15,6 +21,26 @@ import {
|
|
|
15
21
|
selectTurnMessages
|
|
16
22
|
} from "@aliyunrds/ctxdb-shared";
|
|
17
23
|
|
|
24
|
+
// src/lib/git-context.ts
|
|
25
|
+
import { execSync } from "child_process";
|
|
26
|
+
function getGitContext() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = execSync("git rev-parse --show-toplevel --abbrev-ref HEAD", {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
timeout: 3e3,
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
32
|
+
}).trim();
|
|
33
|
+
const lines = raw.split("\n");
|
|
34
|
+
if (lines.length < 2) return null;
|
|
35
|
+
const project = lines[0].split(/[/\\]/).pop();
|
|
36
|
+
if (!project) return null;
|
|
37
|
+
const branch = lines[1];
|
|
38
|
+
return { project, branch };
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
// src/lib/transcript.ts
|
|
19
45
|
import { existsSync, readFileSync } from "fs";
|
|
20
46
|
var ANTHROPIC_MESSAGE_TYPES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
@@ -157,21 +183,10 @@ function toParsedMessages(rows) {
|
|
|
157
183
|
}
|
|
158
184
|
continue;
|
|
159
185
|
}
|
|
160
|
-
if (ptype === "reasoning")
|
|
161
|
-
const summary = p.summary;
|
|
162
|
-
if (!Array.isArray(summary)) continue;
|
|
163
|
-
for (const block of summary) {
|
|
164
|
-
if (!block || typeof block !== "object") continue;
|
|
165
|
-
const btype = block.type;
|
|
166
|
-
if (btype === "summary_text") {
|
|
167
|
-
const v = block.text;
|
|
168
|
-
if (typeof v === "string") push("assistant", v);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
186
|
+
if (ptype === "reasoning") continue;
|
|
173
187
|
continue;
|
|
174
188
|
}
|
|
189
|
+
if (row.isMeta === true) continue;
|
|
175
190
|
const msg = row.message;
|
|
176
191
|
if (!msg || typeof msg !== "object") continue;
|
|
177
192
|
const role = msg.role;
|
|
@@ -189,9 +204,6 @@ function toParsedMessages(rows) {
|
|
|
189
204
|
if (btype === "text") {
|
|
190
205
|
const v = block.text;
|
|
191
206
|
if (typeof v === "string") text = v;
|
|
192
|
-
} else if (btype === "thinking") {
|
|
193
|
-
const v = block.thinking;
|
|
194
|
-
if (typeof v === "string") text = v;
|
|
195
207
|
}
|
|
196
208
|
if (text) push(role, text);
|
|
197
209
|
}
|
|
@@ -199,15 +211,66 @@ function toParsedMessages(rows) {
|
|
|
199
211
|
}
|
|
200
212
|
return parsed;
|
|
201
213
|
}
|
|
214
|
+
var COMMAND_MESSAGE_RE = /<command-message>([\s\S]*?)<\/command-message>/;
|
|
215
|
+
var COMMAND_ARGS_RE = /<command-args>([\s\S]*?)<\/command-args>/;
|
|
216
|
+
function extractSkillSignals(rows) {
|
|
217
|
+
const signals = [];
|
|
218
|
+
const seen = /* @__PURE__ */ new Set();
|
|
219
|
+
const add = (s) => {
|
|
220
|
+
const key = `${s.trigger}:${s.skill}`;
|
|
221
|
+
if (seen.has(key)) return;
|
|
222
|
+
seen.add(key);
|
|
223
|
+
signals.push(s);
|
|
224
|
+
};
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
if (!row || typeof row !== "object") continue;
|
|
227
|
+
if (row.type === "user") {
|
|
228
|
+
const msg = row.message;
|
|
229
|
+
if (!msg || typeof msg !== "object") continue;
|
|
230
|
+
const content = msg.content;
|
|
231
|
+
if (typeof content === "string") {
|
|
232
|
+
const m = COMMAND_MESSAGE_RE.exec(content);
|
|
233
|
+
if (m) {
|
|
234
|
+
const argsMatch = COMMAND_ARGS_RE.exec(content);
|
|
235
|
+
add({
|
|
236
|
+
skill: m[1].trim(),
|
|
237
|
+
args: argsMatch?.[1]?.trim() || void 0,
|
|
238
|
+
trigger: "command"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (row.type === "assistant") {
|
|
244
|
+
const msg = row.message;
|
|
245
|
+
if (!msg || typeof msg !== "object") continue;
|
|
246
|
+
const content = msg.content;
|
|
247
|
+
if (!Array.isArray(content)) continue;
|
|
248
|
+
for (const block of content) {
|
|
249
|
+
if (block && typeof block === "object" && block.type === "tool_use" && block.name === "Skill") {
|
|
250
|
+
const input = block.input;
|
|
251
|
+
if (input && typeof input === "object") {
|
|
252
|
+
const skill = typeof input.skill === "string" ? input.skill : "";
|
|
253
|
+
const args = typeof input.args === "string" ? input.args : void 0;
|
|
254
|
+
if (skill) add({ skill, args, trigger: "agent" });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return signals;
|
|
261
|
+
}
|
|
202
262
|
|
|
203
263
|
// src/lib/capture-orchestrator.ts
|
|
204
|
-
async function captureTurn(transcriptPath, cfg, client) {
|
|
264
|
+
async function captureTurn(transcriptPath, cfg, client, agent = "default") {
|
|
205
265
|
if (!cfg.apiKey || !cfg.baseUrl) {
|
|
206
266
|
return { captured: false, reason: "config_incomplete", messageCount: 0 };
|
|
207
267
|
}
|
|
208
268
|
if (!cfg.autoCapture) {
|
|
209
269
|
return { captured: false, reason: "auto_capture_disabled", messageCount: 0 };
|
|
210
270
|
}
|
|
271
|
+
if (isCircuitOpen(agent, cfg.baseUrl)) {
|
|
272
|
+
return { captured: false, reason: `circuit_open: ${cfg.baseUrl}`, messageCount: 0 };
|
|
273
|
+
}
|
|
211
274
|
const rows = readTranscript(transcriptPath);
|
|
212
275
|
if (rows.length === 0) {
|
|
213
276
|
return { captured: false, reason: "empty_transcript", messageCount: 0 };
|
|
@@ -240,16 +303,26 @@ async function captureTurn(transcriptPath, cfg, client) {
|
|
|
240
303
|
if (filtered.length === 0) {
|
|
241
304
|
return { captured: false, reason: "all_filtered", messageCount: 0 };
|
|
242
305
|
}
|
|
306
|
+
const git = getGitContext();
|
|
307
|
+
const skills = extractSkillSignals(rows);
|
|
308
|
+
const bg = {};
|
|
309
|
+
if (git) bg.git = git;
|
|
310
|
+
if (skills.length > 0) bg.skills = skills.map(
|
|
311
|
+
({ skill, trigger, args }) => args ? { skill, trigger, args } : { skill, trigger }
|
|
312
|
+
);
|
|
313
|
+
const messages = bg.git || bg.skills ? [{ role: "background", content: JSON.stringify(bg) }, ...filtered] : filtered;
|
|
243
314
|
const payload = {
|
|
244
|
-
messages
|
|
315
|
+
messages,
|
|
245
316
|
user_id: cfg.userId,
|
|
246
317
|
async_mode: true
|
|
247
318
|
};
|
|
248
319
|
let resp;
|
|
249
320
|
try {
|
|
250
321
|
resp = await client.postJson("/v3/memories/add/", payload);
|
|
322
|
+
resetCircuit(agent);
|
|
251
323
|
} catch (err) {
|
|
252
324
|
const msg = err instanceof CtxdbError ? err.message : String(err);
|
|
325
|
+
if (isConnectionError(err)) tripCircuit(agent, cfg.baseUrl, msg);
|
|
253
326
|
return {
|
|
254
327
|
captured: false,
|
|
255
328
|
reason: `http_error: ${msg}`,
|
|
@@ -295,8 +368,8 @@ async function main() {
|
|
|
295
368
|
debug("capture", "skip (config incomplete)");
|
|
296
369
|
return 0;
|
|
297
370
|
}
|
|
298
|
-
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
299
|
-
const result = await captureTurn(transcriptPath, cfg, client);
|
|
371
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, timeoutMs: 5e3 });
|
|
372
|
+
const result = await captureTurn(transcriptPath, cfg, client, agent);
|
|
300
373
|
if (result.captured) {
|
|
301
374
|
debug("capture", `ok (${result.messageCount} msgs)`, result);
|
|
302
375
|
process.stderr.write(
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "../chunk-HAUTENYD.js";
|
|
5
|
-
import {
|
|
3
|
+
fetchKbCatalogBlock,
|
|
6
4
|
recallTurn
|
|
7
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C62I23HL.js";
|
|
6
|
+
import "../chunk-6S5RJYBC.js";
|
|
7
|
+
import {
|
|
8
|
+
isCircuitOpen
|
|
9
|
+
} from "../chunk-KEQMJ6IO.js";
|
|
8
10
|
import {
|
|
9
11
|
HttpClient,
|
|
10
12
|
agentFromArgvWithFallback,
|
|
@@ -12,23 +14,31 @@ import {
|
|
|
12
14
|
isComplete,
|
|
13
15
|
load,
|
|
14
16
|
setDebug
|
|
15
|
-
} from "../chunk-
|
|
16
|
-
|
|
17
|
-
// src/lib/kb-catalog.ts
|
|
18
|
-
async function fetchKbCatalogBlock(client, agent) {
|
|
19
|
-
const kbs = await listKnowledgeBases(client);
|
|
20
|
-
const active = kbs.filter((kb) => kb.status === "active");
|
|
21
|
-
if (active.length === 0) return "";
|
|
22
|
-
const lines = active.map((kb) => `\xB7 ${kb.name}`);
|
|
23
|
-
return [
|
|
24
|
-
"<available-knowledge-bases>",
|
|
25
|
-
`When you identify that relevant information may exist in the knowledge bases below, you MUST run \`ctxdb kb search "<query>" --kb=<name> --agent=${agent}\` with targeted keywords after initial analysis to supplement and correct your approach. Knowledge bases:`,
|
|
26
|
-
...lines,
|
|
27
|
-
"</available-knowledge-bases>"
|
|
28
|
-
].join("\n");
|
|
29
|
-
}
|
|
17
|
+
} from "../chunk-U3T5O6NX.js";
|
|
30
18
|
|
|
31
19
|
// src/hooks/user-prompt-submit.ts
|
|
20
|
+
import { pathToFileURL } from "url";
|
|
21
|
+
async function composeUserPromptSubmit(cfg, agent, client, prompt) {
|
|
22
|
+
const [recall, kbBlock] = await Promise.all([
|
|
23
|
+
recallTurn(prompt, cfg, client, agent),
|
|
24
|
+
cfg.kbCatalogInjection === "user_prompt_submit" ? fetchKbCatalogBlock(client, agent).catch(() => "") : Promise.resolve("")
|
|
25
|
+
]);
|
|
26
|
+
let ctx = recall.additionalContext || "";
|
|
27
|
+
if (kbBlock) ctx = ctx ? `${ctx}
|
|
28
|
+
|
|
29
|
+
${kbBlock}` : kbBlock;
|
|
30
|
+
return { ctx, recall, kbBlock };
|
|
31
|
+
}
|
|
32
|
+
function formatUserPromptSubmitStdout(agent, ctx) {
|
|
33
|
+
if (agent === "codex") return ctx + "\n";
|
|
34
|
+
const out = {
|
|
35
|
+
hookSpecificOutput: {
|
|
36
|
+
hookEventName: "UserPromptSubmit",
|
|
37
|
+
additionalContext: ctx
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return JSON.stringify(out) + "\n";
|
|
41
|
+
}
|
|
32
42
|
async function readStdinJson() {
|
|
33
43
|
let raw = "";
|
|
34
44
|
for await (const chunk of process.stdin) raw += chunk;
|
|
@@ -59,37 +69,26 @@ async function main() {
|
|
|
59
69
|
debug("recall", "skip (config incomplete or autoRecall=false)");
|
|
60
70
|
return 0;
|
|
61
71
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
if (isCircuitOpen(agent, cfg.baseUrl)) {
|
|
73
|
+
debug("recall", "skip (circuit open)");
|
|
74
|
+
process.stderr.write(`ctxdb recall: skip (circuit_open: ${cfg.baseUrl})
|
|
75
|
+
`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, timeoutMs: 5e3 });
|
|
79
|
+
const { ctx, recall, kbBlock } = await composeUserPromptSubmit(cfg, agent, client, prompt);
|
|
80
|
+
if (!recall.ok && !recall.additionalContext && !kbBlock) {
|
|
81
|
+
const reason = recall.reason ?? "no_context";
|
|
82
|
+
debug("recall", `no result: ${reason}`, recall);
|
|
83
|
+
if (recall.reason && recall.reason.startsWith("http_error:")) {
|
|
84
|
+
process.stderr.write(`ctxdb recall: ${recall.reason}
|
|
72
85
|
`);
|
|
73
86
|
}
|
|
74
87
|
return 0;
|
|
75
88
|
}
|
|
76
|
-
let ctx = result.additionalContext || "";
|
|
77
|
-
if (kbBlock) ctx = ctx ? `${ctx}
|
|
78
|
-
|
|
79
|
-
${kbBlock}` : kbBlock;
|
|
80
89
|
if (!ctx) return 0;
|
|
81
90
|
debug("recall", `ok, additionalContext length=${ctx.length}`);
|
|
82
|
-
|
|
83
|
-
process.stdout.write(ctx + "\n");
|
|
84
|
-
} else {
|
|
85
|
-
const out = {
|
|
86
|
-
hookSpecificOutput: {
|
|
87
|
-
hookEventName: "UserPromptSubmit",
|
|
88
|
-
additionalContext: ctx
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
92
|
-
}
|
|
91
|
+
process.stdout.write(formatUserPromptSubmitStdout(agent, ctx));
|
|
93
92
|
return 0;
|
|
94
93
|
} catch (err) {
|
|
95
94
|
process.stderr.write(`ctxdb recall: unexpected error: ${err?.message ?? err}
|
|
@@ -97,4 +96,10 @@ ${kbBlock}` : kbBlock;
|
|
|
97
96
|
return 0;
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
|
-
|
|
99
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
100
|
+
main().then((code) => process.exit(code));
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
composeUserPromptSubmit,
|
|
104
|
+
formatUserPromptSubmitStdout
|
|
105
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contextdb-knowledge
|
|
3
|
+
description: contextdb 知识库高频操作(查询 / 浏览 KB / 上传文档)和命令速查。当用户提到"查知识库 / 查 KB / 搜知识库 / 查 KB chunks / 上传到 KB / 灌进知识库 / 列出知识库 / 列出 KB / 看文档 / 建知识库 / 建 KB"等场景时使用。配套 contextdb-memory 处理长期记忆。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# contextdb Knowledge Base 操作
|
|
7
|
+
|
|
8
|
+
## 1. 什么时候用这个 skill
|
|
9
|
+
|
|
10
|
+
用户在做"查/灌/列知识库"的任何动作时进这个 skill。触发关键词:**查知识库 / 查 KB / 搜知识库 / 查 KB chunks / 上传到 KB / 灌进知识库 / 列出知识库 / 列出 KB / 看文档 / 建知识库**。
|
|
11
|
+
|
|
12
|
+
- 用户想**找已有 KB 里的内容** → §2 recipe 1
|
|
13
|
+
- 用户**不知道有哪些 KB** → §2 recipe 2
|
|
14
|
+
- 用户想**了解某个 KB 收了哪些文档** → §2 recipe 3
|
|
15
|
+
- 用户要**写入/上传内容** → §2 recipe 4/5
|
|
16
|
+
|
|
17
|
+
配套 skill:**contextdb-memory**(长期记忆操作)。两者用同一份 `~/.ctxdb/ctxdb.json` 配置和同一个 `--agent` 路由。
|
|
18
|
+
|
|
19
|
+
## 2. 高频 recipes
|
|
20
|
+
|
|
21
|
+
> 命令里 `--agent=qoder` 是必填参数。如果用户跑别的 agent(codex / claude),把 `qoder` 替换成对应名字。
|
|
22
|
+
|
|
23
|
+
### Recipe 1:在已知 KB 中查内容(最常见)
|
|
24
|
+
|
|
25
|
+
**场景**:用户问"X KB 里关于 Y 的内容"、"在知识库 Z 查一下 W"。
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
ctxdb kb search "<query>" --kb=<kb-name> --top-k=6 --agent=qoder
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
返回 `chunks` 数组,按 `score` 倒序判断相关性;默认 threshold 0.4,空结果时见 §5 注意事项 4。
|
|
32
|
+
|
|
33
|
+
**何时不适用**:用户没指定 KB → 先走 recipe 2 列出来确认。
|
|
34
|
+
|
|
35
|
+
### Recipe 2:列出所有 KB(不知道有哪些)
|
|
36
|
+
|
|
37
|
+
**场景**:用户问"有哪些知识库"、"列一下 KB",或本身在写 recipe 1 之前需要确认 KB 名。
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
ctxdb kb list --agent=qoder
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
返回 `knowledge_bases` 数组,每项含 `name` / `id` / `document_count`。跨多 KB 查 chunk 时把 `name` 拼起来:`--kb=a,b,c`。
|
|
44
|
+
|
|
45
|
+
**何时不适用**:hook 已注入 `<available-knowledge-bases>` 块时,直接读那个块更省一次调用(见 §4)。
|
|
46
|
+
|
|
47
|
+
### Recipe 3:列出某 KB 的文档清单
|
|
48
|
+
|
|
49
|
+
**场景**:用户问"X KB 收了哪些文档"、"X KB 里有几篇"。
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
ctxdb kb documents-list <kb-name> --agent=qoder
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
返回 `documents` 数组,每项含 `name` / `id` / `size`。
|
|
56
|
+
|
|
57
|
+
**何时不适用**:用户要的是文档**正文**而不是清单 → 走 recipe 1 用 `kb search`(见 §5 注意事项 1)。
|
|
58
|
+
|
|
59
|
+
### Recipe 4:灌一段文字进 KB
|
|
60
|
+
|
|
61
|
+
**场景**:用户说"把这段记进 X KB"、"上传这段文字到知识库 Y"。
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent=qoder
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
副作用操作。**用户没明示 KB 名前先 recipe 2 列出来确认**(见 §5 注意事项 3)。KB 不存在会直接报错,不自动创建。
|
|
68
|
+
|
|
69
|
+
**何时不适用**:内容是本地文件 → recipe 5。
|
|
70
|
+
|
|
71
|
+
### Recipe 5:上传本地文件到 KB
|
|
72
|
+
|
|
73
|
+
**场景**:用户说"把 X.pdf 上传到 Y KB"、"灌这个文件进知识库 Z"。
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
ctxdb kb upload-file <kb-name> <local-path> --agent=qoder
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
支持格式:PDF / DOCX / MD / TXT。默认等待 chunking 完成;想立即返回加 `--no-wait`(见 §6)。
|
|
80
|
+
|
|
81
|
+
**何时不适用**:要上传的内容是字符串而非本地文件 → recipe 4。
|
|
82
|
+
|
|
83
|
+
## 3. 命令速查
|
|
84
|
+
|
|
85
|
+
`--agent=<name>` 必填;详细解析链见 §6。其他 advanced flag 见 §6。
|
|
86
|
+
|
|
87
|
+
| 子命令 | minimal signature |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `kb search` | `ctxdb kb search "<query>" --agent=<name>` |
|
|
90
|
+
| `kb list` | `ctxdb kb list --agent=<name>` |
|
|
91
|
+
| `kb documents-list` | `ctxdb kb documents-list <kb-name> --agent=<name>` |
|
|
92
|
+
| `kb document-get` | `ctxdb kb document-get <kb-name> <doc-id> --agent=<name>` |
|
|
93
|
+
| `kb create` | `ctxdb kb create <kb-name> --agent=<name>` |
|
|
94
|
+
| `kb upload-text` | `ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent=<name>` |
|
|
95
|
+
| `kb upload-file` | `ctxdb kb upload-file <kb-name> <local-path> --agent=<name>` |
|
|
96
|
+
|
|
97
|
+
所有命令 JSON 写 stdout、错误写 stderr 非零退出码。
|
|
98
|
+
|
|
99
|
+
## 4. Hooks 感知
|
|
100
|
+
|
|
101
|
+
**`<available-knowledge-bases>` 块**:UPS / SessionStart hook 注入到 system context;存在时直接列出 workspace 内所有 KB 名(省一次 `kb list` 调用)。可直接喂给 `--kb=<name>`。
|
|
102
|
+
|
|
103
|
+
**`<recalled-memories>` 块**:是 contextdb-memory 的产物,但 `kb search --knowledge` 会复用同样的 retrieval 路径。两块独立。
|
|
104
|
+
|
|
105
|
+
**B-3c 守卫**:含 `kb upload-text` / `kb upload-file` 的对话轮次,其内容**不会**被 autoCapture 写入记忆(防文档原文混进记忆桶)。
|
|
106
|
+
|
|
107
|
+
**纯 CLI 模式**:对话里没有上面任何块 → 说明当前不在 hook 环境(agent 是裸 CLI 调用),所有信息只能主动调命令拿。
|
|
108
|
+
|
|
109
|
+
## 5. 注意事项
|
|
110
|
+
|
|
111
|
+
### 注意事项 1:`kb document-get` 只返 metadata,**不返正文**
|
|
112
|
+
|
|
113
|
+
- **现象**:想读文档内容,调 `kb document-get` 拿到的只有 `{id, name, size, upload_time, tags}`,没 `content` 字段
|
|
114
|
+
- **根因**:服务端当前只暴露元信息端点;获取正文需走 chunk 检索
|
|
115
|
+
- **正确做法**:用 `kb search "<关键词>" --kb=<name>` 拿 chunks,或 `kb documents-list <kb-name>` 后浏览返回的内嵌 chunk(如有)
|
|
116
|
+
|
|
117
|
+
### 注意事项 2:解析 JSON 用 `jq`,**不要 inline `python3 -c`**
|
|
118
|
+
|
|
119
|
+
- **现象**:在 shell 里写 `ctxdb kb list | python3 -c "import json,sys; ..."` 经常因为单/双引号嵌套 escape 失败
|
|
120
|
+
- **根因**:shell + python -c 的引号是两层独立 escape,组合时极容易漏
|
|
121
|
+
- **正确做法**:`ctxdb kb list --agent=qoder | jq '.knowledge_bases[].name'`;jq 表达式在单引号里不需要 escape
|
|
122
|
+
|
|
123
|
+
### 注意事项 3:`upload-*` / `create` 是副作用,没明示 KB 名前必须先确认
|
|
124
|
+
|
|
125
|
+
- **现象**:用户只说"把这段记进知识库"没指定 KB,agent 直接用了某个猜的 KB 名 → 写到错的桶
|
|
126
|
+
- **根因**:写命令不会跟 agent 二次确认;KB 不存在时报错,但**KB 名拼错却恰好命中另一个真实 KB**时会静默写错
|
|
127
|
+
- **正确做法**:用户没明示 → 必须先 `kb list` 拿候选清单,跟用户确认目标后才执行
|
|
128
|
+
|
|
129
|
+
### 注意事项 4:`kb search` 空结果时降阈值再试一次
|
|
130
|
+
|
|
131
|
+
- **现象**:query 明明跟内容相关,但 `chunks: []`
|
|
132
|
+
- **根因**:默认 `--threshold=0.4`,短 query / 关键词偏冷时打不到
|
|
133
|
+
- **正确做法**:降到 `--threshold=0.2` 或 `--threshold=0.3` 重试一次再判定真的没命中;仍空才放弃
|
|
134
|
+
|
|
135
|
+
### 注意事项 5:`upload-*` 不会自动建 KB
|
|
136
|
+
|
|
137
|
+
- **现象**:`kb upload-text foo bar --text=...` 报错 "kb 'foo' not found"
|
|
138
|
+
- **根因**:上传命令只写已有 KB,**不会**为了你 implicit 建一个
|
|
139
|
+
- **正确做法**:先 `ctxdb kb create foo --agent=qoder`,再 upload
|
|
140
|
+
|
|
141
|
+
## 6. 高级参数
|
|
142
|
+
|
|
143
|
+
### `kb search` 全部 flag
|
|
144
|
+
|
|
145
|
+
| 参数 | 说明 | 默认值 |
|
|
146
|
+
|------|------|--------|
|
|
147
|
+
| `--kb=<name1,name2>` | 限定搜索的 KB,逗号分隔 | 搜所有 |
|
|
148
|
+
| `--top-k=N` | chunk 数上限 | 6 |
|
|
149
|
+
| `--threshold=F` | 相关度阈值 | 0.4 |
|
|
150
|
+
| `--verbose` | 每个 chunk 加 `doc_name` / `kb_id` / `doc_id` / `tags` | false |
|
|
151
|
+
| `--raw` | 服务端原始响应(13+ 字段,含 tokenizer 细节) | false |
|
|
152
|
+
|
|
153
|
+
### `kb upload-file` / `kb upload-text` 全部 flag
|
|
154
|
+
|
|
155
|
+
| 参数 | 说明 |
|
|
156
|
+
|------|------|
|
|
157
|
+
| `--doc-name` | (`upload-file`)服务端文档名,默认取本地文件名 |
|
|
158
|
+
| `--file-path` | 服务端逻辑路径(归档/分类用) |
|
|
159
|
+
| `--no-wait` | 不等 chunking 完成立即返回 |
|
|
160
|
+
|
|
161
|
+
### `kb create` 全部 flag
|
|
162
|
+
|
|
163
|
+
| 参数 | 说明 |
|
|
164
|
+
|------|------|
|
|
165
|
+
| `--description` | 知识库描述 |
|
|
166
|
+
|
|
167
|
+
### `kb list` / `kb documents-list` / `kb document-get`
|
|
168
|
+
|
|
169
|
+
无额外 flag(除通用参数)。
|
|
170
|
+
|
|
171
|
+
### 通用参数
|
|
172
|
+
|
|
173
|
+
| 参数 | 说明 |
|
|
174
|
+
|------|------|
|
|
175
|
+
| `--agent=<name>` | **必填**。指定操作哪个 agent 的配置桶(`qoder` / `codex` / `claude` / `default`) |
|
|
176
|
+
|
|
177
|
+
`--agent` 解析顺序:
|
|
178
|
+
1. 命令行显式 `--agent=<name>` → 用它
|
|
179
|
+
2. 缺省 → 读 `CTXDB_AGENT` 环境变量
|
|
180
|
+
3. env 也没有 → 落到 `"default"` 桶
|
|
181
|
+
4. 若 `"default"` 桶未通过 `ctxdb setup --api-key=... --base-url=...` 配置过 → exit 2 "config incomplete for agent default"
|
|
182
|
+
|
|
183
|
+
## 7. 配置
|
|
184
|
+
|
|
185
|
+
- 配置文件:`~/.ctxdb/ctxdb.json`,分 `agents.<name>` 段(`qoder` / `codex` / `claude` / `default`),互不复用
|
|
186
|
+
- 配置命令:`ctxdb setup --agent <name> --api-key=<key> --base-url=<url> [--user-id=<id>]`
|
|
187
|
+
- 带 `--agent` 时还会装 hooks + skill;不带 `--agent` 时只写 `agents.default` 段(仅 CLI 用)
|
|
188
|
+
- 连接检查:`ctxdb ping --agent=<name>`
|
|
189
|
+
- 状态查看:`ctxdb status --agent=<name>`
|