@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.
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ fetchKbCatalogBlock,
3
4
  recallTurn
4
- } from "../chunk-7NOXAU2X.js";
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-QSSNPN3M.js";
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 = 3e4;
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
- if (!isComplete(cfg) || !cfg.warmupRecall) {
97
- debug("warmup", "skip (config incomplete or warmupRecall=false)");
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 result = await Promise.race([
106
- warmupRecall(cwd, cfg, client),
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 (!result.ok || !result.additionalContext) {
114
- debug("warmup", `no result: ${result.reason}`);
115
- return 0;
116
- }
117
- debug("warmup", `ok, memories=${result.memoryCount} kb=${result.knowledgeChunkCount}`);
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 (${result.memoryCount} memories, ${result.knowledgeChunkCount} kb chunks)
161
+ `ctxdb warmup: ok (${memoryCount} memories, ${kbChunkCount} kb chunks, kb_catalog=${kbCatalogLines} lines)
120
162
  `
121
163
  );
122
- if (agent === "codex") {
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
- main().then((code) => process.exit(code));
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
+ };
@@ -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-QSSNPN3M.js";
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: filtered,
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
- listKnowledgeBases
4
- } from "../chunk-HAUTENYD.js";
5
- import {
3
+ fetchKbCatalogBlock,
6
4
  recallTurn
7
- } from "../chunk-7NOXAU2X.js";
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-QSSNPN3M.js";
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
- const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
63
- const [result, kbBlock] = await Promise.all([
64
- recallTurn(prompt, cfg, client),
65
- fetchKbCatalogBlock(client, agent).catch(() => "")
66
- ]);
67
- if (!result.ok && !result.additionalContext && !kbBlock) {
68
- const reason = result.reason ?? "no_context";
69
- debug("recall", `no result: ${reason}`, result);
70
- if (result.reason && result.reason.startsWith("http_error:")) {
71
- process.stderr.write(`ctxdb recall: ${result.reason}
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
- if (agent === "codex") {
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
- main().then((code) => process.exit(code));
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>`