@aliyunrds/ctxdb 1.0.6-beta.0 → 1.0.7

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 CHANGED
@@ -217,7 +217,7 @@ For **qoder**, **qoderwork**, **qwenwork**, **codex**, and **claude**, hooks fir
217
217
  |---|---|---|
218
218
  | Session starts | `SessionStart` | Build a short cwd/git-signal query, recall relevant memories, and inject warm-up context as `additionalContext` |
219
219
  | User submits a prompt | `UserPromptSubmit` | Search `/v3/memories/search/`, format a `<recalled-memories>` block, inject as additional context. **By default only memory is recalled.** Set `recall_knowledge: true` in the selected agent config to also pull KB context into an `<external-knowledge>` block; graph synthesis and document evidence are labeled separately inside that wrapper. Otherwise the recommended path for KB is the agent calling `ctxdb kb search --agent <name>` explicitly when the user asks (see SKILL.md) |
220
- | Agent finishes its turn | `Stop` | Read transcript, detect KB-upload turns and skip capture (so uploaded content doesn't bleed into long-term memory), slice the current turn, sanitize the input, POST to memory store |
220
+ | Agent finishes its turn | `Stop` | Read transcript, detect KB create/update turns and skip capture (so document content doesn't bleed into long-term memory), slice the current turn, sanitize the input, POST to memory store |
221
221
 
222
222
  Implementation note: qoder/Claude consume the JSON `hookSpecificOutput.additionalContext` shape; Codex has been verified on this machine through `~/.codex/hooks.json`, `[features].hooks = true`, and hook audit logs, where non-empty stdout from `UserPromptSubmit`/`SessionStart` is treated as injected context. The same hook core is shared; setup passes `--agent=<name>` so each hook reads its own config section.
223
223
 
@@ -227,7 +227,7 @@ For **hermes**, `ctxdb setup --agent hermes` writes two shell hooks under `~/.he
227
227
 
228
228
  - `pre_llm_call` maps to recall on each user prompt and first-turn warmup. Hermes does not persist hook context into conversation history, so ctxdb re-injects the KB catalog on every user turn even when `kb_catalog_injection` keeps its default `"session_start"` value; this prevents the catalog from disappearing after turn one.
229
229
  - stdout uses Hermes' `{"context":"..."}` protocol.
230
- - `post_llm_call` captures `extra.user_message` + `extra.assistant_response` without a transcript file, while `extra.conversation_history` supplies current-turn tool calls for the B-3c KB-upload guard.
230
+ - `post_llm_call` captures `extra.user_message` + `extra.assistant_response` without a transcript file, while `extra.conversation_history` supplies current-turn tool calls for the B-3c KB create/update guard.
231
231
 
232
232
  ## CLI
233
233
 
@@ -235,7 +235,7 @@ For **hermes**, `ctxdb setup --agent hermes` writes two shell hooks under `~/.he
235
235
 
236
236
  - **Top-level**: `setup` / `status` / `ping` / `debug` / `uninstall` / `update` (`upgrade` alias)
237
237
  - **Memory**: `memory add|search|list|get|update|delete`
238
- - **KB**: `kb upload-text|upload-file|list|documents-list|document-get|search`
238
+ - **KB**: `kb upload-text|upload-file|update-text|update-file|list|documents-list|document-get|search`
239
239
 
240
240
  See `ctxdb --help`.
241
241
 
@@ -293,10 +293,10 @@ fact-extraction). Use it when the user explicitly asks for a verbatim
293
293
  memory ("记住 / 请记忆 / 原文记下"); without `--no-infer` the server may
294
294
  rewrite, merge, or skip details.
295
295
 
296
- ### KB upload completion mode
296
+ ### KB create/update completion mode
297
297
 
298
- **Breaking change:** `kb upload-text` and `kb upload-file` now return as soon
299
- as the service accepts the document. The returned document may still have
298
+ `kb upload-text`, `kb upload-file`, `kb update-text`, and `kb update-file`
299
+ return as soon as the service accepts the document. The returned document may still have
300
300
  `ingest_status: "processing"`; acceptance does not mean ingestion succeeded.
301
301
 
302
302
  - Add `--wait` when a script needs the previous behavior: poll until ingestion
@@ -309,23 +309,62 @@ Scripts that relied on implicit terminal polling must migrate to:
309
309
  ```sh
310
310
  ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --wait
311
311
  ctxdb kb upload-file <kb-name> <local-path> --wait
312
+ ctxdb kb update-text <document-id> --text="<body>" --wait
313
+ ctxdb kb update-file <document-id> <local-path> --wait
312
314
  ```
313
315
 
316
+ ### Explicit KB document updates
317
+
318
+ Create and update are separate operations. `upload-text` / `upload-file` use
319
+ create-only POST routes, so an existing logical document produces HTTP 409 and
320
+ is never silently overwritten. `update-text` / `update-file` use PUT and accept
321
+ exactly one locator family: the canonical document ID (preferred), or an
322
+ explicit knowledge-base name + logical document name + optional normalized
323
+ `--file-path`.
324
+
325
+ ```sh
326
+ # Preferred: no KB/name/path lookup or reconstruction
327
+ ctxdb kb update-text <document-id> --text="<new-body>"
328
+ ctxdb kb update-file <document-id> <local-path>
329
+
330
+ # Alternative when the exact document ID is unavailable
331
+ ctxdb kb update-text --kb-name=<kb-name> --doc-name=<logical-name> --text="<new-body>" [--file-path=<logical-parent>]
332
+ ctxdb kb update-file <local-path> --kb-name=<kb-name> [--doc-name=<existing-logical-name>] [--file-path=<logical-parent>]
333
+ ```
334
+
335
+ Do not combine a positional document ID with `--kb-name`, `--doc-name`, or
336
+ `--file-path`. ID mode derives the existing document's KB and logical metadata
337
+ on the server; a replacement file's basename remains only its physical upload
338
+ filename. Logical-key file mode defaults `--doc-name` to that basename, so pass
339
+ the existing logical name when the replacement basename differs.
340
+
341
+ Every successful update returns the canonical `document.id` and
342
+ `document.knowledge_base_id`. Changed content can replace the input ID, while
343
+ same-content updates may reuse it. Callers must retain the returned ID for the
344
+ next update; `--wait` also polls the returned ID/KB pair rather than caller input.
345
+
314
346
  ### KB file upload policy and large files
315
347
 
316
- `kb upload-file` does not maintain a client-side format allowlist or file-size
348
+ `kb upload-file` and `kb update-file` do not maintain a client-side format allowlist or file-size
317
349
  table. It validates only that the local path exists, is a readable regular
318
350
  file, then lets the service decide whether the format, per-type size, quota,
319
351
  and logical document identity are acceptable. Unknown extensions are sent as
320
352
  `application/octet-stream`.
321
353
 
322
354
  File bytes are streamed from disk with a known multipart size; the CLI does
323
- not read the complete file into a Buffer. It always attempts the ordinary
324
- upload endpoint first. If the service returns the dedicated
355
+ not read the complete file into a Buffer. Each command attempts its ordinary
356
+ direct endpoint first. If the service returns the dedicated
325
357
  `KNOWLEDGE_DIRECT_UPLOAD_REQUIRES_CHUNK` error (`errorCode=6001`) or an
326
- unstructured HTTP 413, the CLI resolves the KB ID and automatically uses the
327
- chunk init/part/complete protocol. `--doc-name` and `--file-path` have the same
328
- logical meaning in both modes.
358
+ unstructured HTTP 413, the CLI automatically uses the chunk init/part/complete
359
+ protocol. Create init uses POST and update init uses PUT; parts, complete, and
360
+ abort stay on POST. Document-ID update init sends the ID and physical filename
361
+ without resolving a KB. Logical-key update init resolves the KB and sends the
362
+ same KB/name/path identity as direct mode.
363
+
364
+ Chunk init and complete accept the release_pre bare response objects. Create
365
+ also accepts the previous `Box.data` init/complete shape during rolling
366
+ deployment; part and abort responses remain Box-wrapped. This response-shape
367
+ compatibility does not fall back from logical-key PUT to an older update contract.
329
368
 
330
369
  The first chunk-routing release intentionally sends init, each part, and
331
370
  complete only once. It does not automatically retry timeout, connection-loss,
@@ -4,12 +4,12 @@ import {
4
4
  isConnectionError,
5
5
  resetCircuit,
6
6
  tripCircuit
7
- } from "./chunk-B7JCQL4O.js";
7
+ } from "./chunk-X2BX6LR6.js";
8
8
  import {
9
9
  CtxdbError,
10
10
  debug,
11
11
  isDebug
12
- } from "./chunk-ARZX2RLR.js";
12
+ } from "./chunk-S5W4FQ7M.js";
13
13
 
14
14
  // src/lib/capture-orchestrator.ts
15
15
  import {
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  listKnowledgeBases
4
- } from "./chunk-L4GVGVOP.js";
4
+ } from "./chunk-QVXDI77N.js";
5
5
  import {
6
6
  isConnectionError,
7
7
  resetCircuit,
8
8
  tripCircuit
9
- } from "./chunk-B7JCQL4O.js";
9
+ } from "./chunk-X2BX6LR6.js";
10
10
  import {
11
11
  CtxdbError
12
- } from "./chunk-ARZX2RLR.js";
12
+ } from "./chunk-S5W4FQ7M.js";
13
13
 
14
14
  // src/lib/kb-catalog.ts
15
15
  function sanitizeKeyEntities(raw) {
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  fetchKbCatalogBlock,
4
4
  recallTurn
5
- } from "./chunk-VHNHVMCA.js";
5
+ } from "./chunk-DPLMBHLU.js";
6
6
  import {
7
7
  debug
8
- } from "./chunk-ARZX2RLR.js";
8
+ } from "./chunk-S5W4FQ7M.js";
9
9
 
10
10
  // src/lib/warmup-recall.ts
11
11
  import { execSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CtxdbError
4
- } from "./chunk-ARZX2RLR.js";
4
+ } from "./chunk-S5W4FQ7M.js";
5
5
 
6
6
  // src/lib/kb.ts
7
7
  import {
@@ -94,6 +94,51 @@ async function createKb(client, kbName, description = "") {
94
94
  return client.postJson(KB_COLLECTION, { name: kbName, description: description || "" });
95
95
  }
96
96
  async function uploadText(client, kbName, docName, text, mimeType = "text/plain", filePath) {
97
+ return client.postJson(
98
+ DOCUMENTS,
99
+ buildTextDocumentBody(kbName, docName, text, mimeType, filePath)
100
+ );
101
+ }
102
+ async function updateText(client, locator, text) {
103
+ const body = {
104
+ ...buildUpdateLocatorFields(locator),
105
+ text
106
+ };
107
+ const response = await client.putJson(DOCUMENTS, body);
108
+ return readCanonicalUpdateDocument(response, "text update");
109
+ }
110
+ function buildUpdateLocatorFields(locator) {
111
+ if (!locator || typeof locator !== "object") {
112
+ throw new CtxdbError("update requires exactly one document locator");
113
+ }
114
+ const candidate = locator;
115
+ const hasDocumentId = Object.hasOwn(candidate, "documentId");
116
+ const hasLogicalField = ["kbName", "docName", "filePath"].some((field) => Object.hasOwn(candidate, field));
117
+ if (hasDocumentId === hasLogicalField) {
118
+ throw new CtxdbError("update requires exactly one document locator");
119
+ }
120
+ if (hasDocumentId) {
121
+ if (typeof candidate.documentId !== "string" || candidate.documentId.trim() === "") {
122
+ throw new CtxdbError("update requires a non-empty document ID");
123
+ }
124
+ return { document_id: candidate.documentId };
125
+ }
126
+ if (typeof candidate.kbName !== "string" || candidate.kbName.trim() === "" || typeof candidate.docName !== "string" || candidate.docName.trim() === "") {
127
+ throw new CtxdbError("logical update requires KB name and document name");
128
+ }
129
+ if (candidate.filePath !== void 0 && (typeof candidate.filePath !== "string" || candidate.filePath.trim() === "")) {
130
+ throw new CtxdbError("logical update file path must be a non-empty string");
131
+ }
132
+ const fields = {
133
+ knowledge_base_name: candidate.kbName,
134
+ name: candidate.docName
135
+ };
136
+ if (typeof candidate.filePath === "string") {
137
+ fields.file_path = candidate.filePath;
138
+ }
139
+ return fields;
140
+ }
141
+ function buildTextDocumentBody(kbName, docName, text, mimeType, filePath) {
97
142
  const body = {
98
143
  knowledge_base_name: kbName,
99
144
  name: docName,
@@ -101,74 +146,133 @@ async function uploadText(client, kbName, docName, text, mimeType = "text/plain"
101
146
  mime_type: mimeType
102
147
  };
103
148
  if (filePath !== void 0 && filePath !== "") body.file_path = filePath;
104
- return client.postJson(DOCUMENTS, body);
149
+ return body;
105
150
  }
106
151
  async function uploadFile(client, kbName, localPath, options = {}) {
107
- const expanded = expandHome(localPath);
108
- if (!existsSync(expanded)) throw new Error(`file not found: ${expanded}`);
109
- const stat = statSync(expanded);
110
- if (!stat.isFile()) throw new Error(`not a file: ${expanded}`);
111
- try {
112
- accessSync(expanded, fsConstants.R_OK);
113
- } catch (error) {
114
- throw new Error(
115
- `file is not readable: ${expanded}${error?.message ? ` (${error.message})` : ""}`
116
- );
117
- }
152
+ return writeFile(
153
+ client,
154
+ {
155
+ mode: "create",
156
+ kbName,
157
+ docName: options.docName,
158
+ filePath: options.filePath
159
+ },
160
+ localPath,
161
+ options.timeoutMs
162
+ );
163
+ }
164
+ async function updateFile(client, locator, localPath, options = {}) {
165
+ buildUpdateLocatorFields(locator);
166
+ return writeFile(
167
+ client,
168
+ { mode: "update", locator },
169
+ localPath,
170
+ options.timeoutMs
171
+ );
172
+ }
173
+ async function writeFile(client, target, localPath, requestedTimeoutMs) {
174
+ const { expanded, stat } = inspectLocalFile(localPath);
118
175
  const filename = basename(expanded);
119
- const docName = options.docName ?? filename;
120
176
  const mime = guessMime(expanded);
121
177
  const content = await openAsBlob(expanded, { type: mime });
122
- const fields = {
123
- knowledge_base_name: kbName,
124
- name: docName
125
- };
126
- if (options.filePath !== void 0 && options.filePath !== "") {
127
- fields.file_path = options.filePath;
128
- }
129
- const timeoutMs = options.timeoutMs ?? DEFAULT_FILE_UPLOAD_TIMEOUT_MS;
178
+ const fields = target.mode === "create" ? {
179
+ knowledge_base_name: target.kbName,
180
+ name: target.docName ?? filename,
181
+ ...target.filePath ? { file_path: target.filePath } : {}
182
+ } : buildUpdateLocatorFields(target.locator);
183
+ const timeoutMs = requestedTimeoutMs ?? DEFAULT_FILE_UPLOAD_TIMEOUT_MS;
130
184
  try {
131
- return await client.postMultipart(
185
+ const response = target.mode === "create" ? await client.postMultipart(
186
+ FILES,
187
+ fields,
188
+ { file: { filename, content, mimeType: mime } },
189
+ { timeoutMs }
190
+ ) : await client.putMultipart(
132
191
  FILES,
133
192
  fields,
134
193
  { file: { filename, content, mimeType: mime } },
135
194
  { timeoutMs }
136
195
  );
196
+ return target.mode === "update" ? readCanonicalUpdateDocument(response, "file update") : response;
137
197
  } catch (error) {
138
198
  if (!shouldFallbackToChunk(error)) throw error;
139
199
  }
140
- return uploadFileInChunks(client, {
141
- kbName,
200
+ return writeFileInChunks(client, {
201
+ target,
142
202
  filename,
143
- docName,
144
- filePath: options.filePath,
145
203
  mime,
146
204
  content,
147
205
  fileSize: stat.size,
148
206
  timeoutMs
149
207
  });
150
208
  }
151
- async function uploadFileInChunks(client, options) {
152
- const kb = await client.get(KB_DETAIL, {
153
- knowledge_base_name: options.kbName
154
- });
155
- const knowledgeBaseId = readRequiredString(kb, "id", "knowledge base detail");
209
+ function validateLocalFile(localPath) {
210
+ inspectLocalFile(localPath);
211
+ }
212
+ function inspectLocalFile(localPath) {
213
+ const expanded = expandHome(localPath);
214
+ if (!existsSync(expanded)) throw new Error(`file not found: ${expanded}`);
215
+ const stat = statSync(expanded);
216
+ if (!stat.isFile()) throw new Error(`not a file: ${expanded}`);
217
+ try {
218
+ accessSync(expanded, fsConstants.R_OK);
219
+ } catch (error) {
220
+ throw new Error(
221
+ `file is not readable: ${expanded}${error?.message ? ` (${error.message})` : ""}`
222
+ );
223
+ }
224
+ return { expanded, stat };
225
+ }
226
+ async function writeFileInChunks(client, options) {
227
+ let expectedKnowledgeBaseId;
156
228
  const initParams = {
157
- knowledge_base_id: knowledgeBaseId,
158
229
  file_name: options.filename,
159
- name: options.docName,
160
230
  mime_type: options.mime
161
231
  };
162
- if (options.filePath !== void 0 && options.filePath !== "") {
163
- initParams.file_path = options.filePath;
232
+ if (options.target.mode === "create") {
233
+ expectedKnowledgeBaseId = await resolveKnowledgeBaseId(
234
+ client,
235
+ options.target.kbName
236
+ );
237
+ initParams.knowledge_base_id = expectedKnowledgeBaseId;
238
+ initParams.name = options.target.docName ?? options.filename;
239
+ if (options.target.filePath) initParams.file_path = options.target.filePath;
240
+ } else if ("documentId" in options.target.locator) {
241
+ initParams.document_id = options.target.locator.documentId;
242
+ } else {
243
+ expectedKnowledgeBaseId = await resolveKnowledgeBaseId(
244
+ client,
245
+ options.target.locator.kbName
246
+ );
247
+ initParams.knowledge_base_id = expectedKnowledgeBaseId;
248
+ initParams.name = options.target.locator.docName;
249
+ if (options.target.locator.filePath) {
250
+ initParams.file_path = options.target.locator.filePath;
251
+ }
164
252
  }
165
- const initResponse = await client.postJson(
253
+ const initResponse = options.target.mode === "create" ? await client.postJson(
254
+ CHUNK_INIT,
255
+ {},
256
+ initParams,
257
+ { timeoutMs: options.timeoutMs }
258
+ ) : await client.putJson(
166
259
  CHUNK_INIT,
167
260
  {},
168
261
  initParams,
169
262
  { timeoutMs: options.timeoutMs }
170
263
  );
171
- const init = unwrapBox(initResponse, "chunk init");
264
+ const normalizedInit = unwrapBareOrBox(initResponse, "chunk init");
265
+ const init = normalizedInit.value;
266
+ const initKnowledgeBaseId = readOptionalString(init, "knowledge_base_id");
267
+ if (!initKnowledgeBaseId) {
268
+ if (!(options.target.mode === "create" && normalizedInit.boxed)) {
269
+ throw new CtxdbError("chunk init returned invalid knowledge_base_id");
270
+ }
271
+ } else if (expectedKnowledgeBaseId && initKnowledgeBaseId !== expectedKnowledgeBaseId) {
272
+ throw new CtxdbError(
273
+ `chunk init returned mismatched knowledge_base_id ${initKnowledgeBaseId}`
274
+ );
275
+ }
172
276
  const uploadTicket = readRequiredString(
173
277
  init,
174
278
  "upload_ticket",
@@ -237,19 +341,56 @@ async function uploadFileInChunks(client, options) {
237
341
  headers: { "X-Upload-Ticket": uploadTicket }
238
342
  }
239
343
  );
240
- const completed = unwrapBox(completeResponse, "chunk complete");
344
+ const normalizedComplete = unwrapBareOrBox(
345
+ completeResponse,
346
+ "chunk complete"
347
+ );
348
+ const completed = normalizedComplete.value;
349
+ const completedKnowledgeBaseId = readOptionalString(
350
+ completed,
351
+ "knowledge_base_id"
352
+ );
353
+ if (!completedKnowledgeBaseId) {
354
+ if (!(options.target.mode === "create" && normalizedComplete.boxed)) {
355
+ throw new CtxdbError(
356
+ "chunk complete returned invalid knowledge_base_id"
357
+ );
358
+ }
359
+ } else if (initKnowledgeBaseId && completedKnowledgeBaseId !== initKnowledgeBaseId) {
360
+ throw new CtxdbError(
361
+ `chunk complete returned mismatched knowledge_base_id ${completedKnowledgeBaseId}`
362
+ );
363
+ }
241
364
  const completedDocumentId = readRequiredString(
242
365
  completed,
243
366
  "document_id",
244
367
  "chunk complete"
245
368
  );
246
- const reusedSucceededDocument = completedDocumentId !== documentId && completed.status === "succeeded";
369
+ const completedStatus = readOptionalString(completed, "status");
370
+ if (!completedStatus && !normalizedComplete.boxed) {
371
+ throw new CtxdbError("chunk complete returned invalid status");
372
+ }
373
+ const reusedSucceededDocument = completedDocumentId !== documentId && completedStatus === "succeeded";
247
374
  if (completedDocumentId !== documentId && !reusedSucceededDocument) {
248
375
  throw new CtxdbError(
249
376
  `chunk complete returned mismatched document_id ${completedDocumentId}`
250
377
  );
251
378
  }
252
- return getDocument(client, options.kbName, completedDocumentId);
379
+ if (options.target.mode === "update") {
380
+ const canonicalKnowledgeBaseId = completedKnowledgeBaseId ?? initKnowledgeBaseId;
381
+ if (!canonicalKnowledgeBaseId) {
382
+ throw new CtxdbError(
383
+ "chunk update returned invalid knowledge_base_id"
384
+ );
385
+ }
386
+ const detail = await getDocumentByKnowledgeBaseId(
387
+ client,
388
+ canonicalKnowledgeBaseId,
389
+ completedDocumentId
390
+ );
391
+ return readCanonicalUpdateDocument(detail, "chunk update detail");
392
+ }
393
+ return getDocument(client, options.target.kbName, completedDocumentId);
253
394
  } catch (error) {
254
395
  if (!completeStarted) {
255
396
  try {
@@ -268,6 +409,10 @@ async function uploadFileInChunks(client, options) {
268
409
  throw error;
269
410
  }
270
411
  }
412
+ async function resolveKnowledgeBaseId(client, kbName) {
413
+ const kb = await client.get(KB_DETAIL, { knowledge_base_name: kbName });
414
+ return readRequiredString(kb, "id", "knowledge base detail");
415
+ }
271
416
  function shouldFallbackToChunk(error) {
272
417
  if (!error || typeof error !== "object") return false;
273
418
  const candidate = error;
@@ -303,6 +448,31 @@ function unwrapBox(response, operation) {
303
448
  }
304
449
  return box.data;
305
450
  }
451
+ function unwrapBareOrBox(response, operation) {
452
+ if (!response || typeof response !== "object" || Array.isArray(response)) {
453
+ throw new CtxdbError(`${operation} returned an invalid response`);
454
+ }
455
+ const object = response;
456
+ if (Object.hasOwn(object, "data")) {
457
+ return { value: unwrapBox(response, operation), boxed: true };
458
+ }
459
+ return { value: object, boxed: false };
460
+ }
461
+ function readCanonicalUpdateDocument(response, operation) {
462
+ if (!response || typeof response !== "object" || Array.isArray(response)) {
463
+ throw new CtxdbError(`${operation} returned an invalid response`);
464
+ }
465
+ readRequiredString(response, "id", operation);
466
+ readRequiredString(response, "knowledge_base_id", operation);
467
+ return response;
468
+ }
469
+ function readOptionalString(value, field) {
470
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
471
+ return void 0;
472
+ }
473
+ const result = value[field];
474
+ return typeof result === "string" && result.trim() !== "" ? result : void 0;
475
+ }
306
476
  function readRequiredString(value, field, source) {
307
477
  if (!value || typeof value !== "object" || Array.isArray(value)) {
308
478
  throw new CtxdbError(`${source} returned an invalid response`);
@@ -334,6 +504,12 @@ async function getDocument(client, kbName, docId) {
334
504
  document_id: docId
335
505
  });
336
506
  }
507
+ async function getDocumentByKnowledgeBaseId(client, knowledgeBaseId, docId) {
508
+ return client.get(DOCUMENT_DETAIL, {
509
+ knowledge_base_id: knowledgeBaseId,
510
+ document_id: docId
511
+ });
512
+ }
337
513
  async function listDocuments(client, kbName) {
338
514
  const resp = await client.get(DOCUMENTS, { knowledge_base_name: kbName });
339
515
  if (Array.isArray(resp)) return resp;
@@ -344,12 +520,24 @@ async function listDocuments(client, kbName) {
344
520
  return [];
345
521
  }
346
522
  async function pollIngest(client, kbName, docId, options = {}) {
523
+ return pollIngestWith(
524
+ () => getDocument(client, kbName, docId),
525
+ options
526
+ );
527
+ }
528
+ async function pollIngestByKnowledgeBaseId(client, knowledgeBaseId, docId, options = {}) {
529
+ return pollIngestWith(
530
+ () => getDocumentByKnowledgeBaseId(client, knowledgeBaseId, docId),
531
+ options
532
+ );
533
+ }
534
+ async function pollIngestWith(getLatest, options) {
347
535
  const timeoutMs = options.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
348
536
  const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
349
537
  const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
350
538
  const now = options.now ?? (() => performance.now());
351
539
  const deadline = now() + timeoutMs;
352
- let doc = await getDocument(client, kbName, docId);
540
+ let doc = await getLatest();
353
541
  let timedOut = false;
354
542
  while (ingestInFlight(doc)) {
355
543
  if (now() >= deadline) {
@@ -358,7 +546,7 @@ async function pollIngest(client, kbName, docId, options = {}) {
358
546
  }
359
547
  await sleep(intervalMs);
360
548
  try {
361
- doc = await getDocument(client, kbName, docId);
549
+ doc = await getLatest();
362
550
  } catch {
363
551
  break;
364
552
  }
@@ -434,10 +622,14 @@ export {
434
622
  listKnowledgeBases,
435
623
  createKb,
436
624
  uploadText,
625
+ updateText,
437
626
  uploadFile,
627
+ updateFile,
628
+ validateLocalFile,
438
629
  getDocument,
439
630
  listDocuments,
440
631
  pollIngest,
632
+ pollIngestByKnowledgeBaseId,
441
633
  compactKbQueryResponse,
442
634
  minimalKbQueryResponse
443
635
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  fetchKbCatalogBlock,
4
4
  recallTurn
5
- } from "./chunk-VHNHVMCA.js";
5
+ } from "./chunk-DPLMBHLU.js";
6
6
 
7
7
  // src/lib/user-prompt-submit-compose.ts
8
8
  async function composeUserPromptSubmit(cfg, agent, client, prompt, sessionId = null) {
@@ -248,10 +248,13 @@ var HttpClient = class {
248
248
  headers: options.headers
249
249
  });
250
250
  }
251
- putJson(path, body) {
251
+ putJson(path, body, params, options = {}) {
252
252
  return this.doRequest("PUT", path, {
253
253
  body: JSON.stringify(body),
254
- contentType: "application/json"
254
+ contentType: "application/json",
255
+ params,
256
+ timeoutMs: options.timeoutMs,
257
+ headers: options.headers
255
258
  });
256
259
  }
257
260
  delete(path, params) {
@@ -266,14 +269,7 @@ var HttpClient = class {
266
269
  * boundary itself.
267
270
  */
268
271
  postMultipart(path, fields = {}, files = {}, options = {}) {
269
- const fd = new FormData();
270
- for (const [name, value] of Object.entries(fields)) {
271
- fd.append(name, value);
272
- }
273
- for (const [name, part] of Object.entries(files)) {
274
- const blob = part.content instanceof Blob ? part.content : new Blob([part.content], { type: part.mimeType });
275
- fd.append(name, blob, part.filename);
276
- }
272
+ const fd = buildMultipartForm(fields, files);
277
273
  return this.doRequest("POST", path, {
278
274
  body: fd,
279
275
  params: options.params,
@@ -281,7 +277,28 @@ var HttpClient = class {
281
277
  headers: options.headers
282
278
  });
283
279
  }
280
+ /** PUT multipart/form-data with the same runtime-owned boundary as POST. */
281
+ putMultipart(path, fields = {}, files = {}, options = {}) {
282
+ const fd = buildMultipartForm(fields, files);
283
+ return this.doRequest("PUT", path, {
284
+ body: fd,
285
+ params: options.params,
286
+ timeoutMs: options.timeoutMs,
287
+ headers: options.headers
288
+ });
289
+ }
284
290
  };
291
+ function buildMultipartForm(fields, files) {
292
+ const fd = new FormData();
293
+ for (const [name, value] of Object.entries(fields)) {
294
+ fd.append(name, value);
295
+ }
296
+ for (const [name, part] of Object.entries(files)) {
297
+ const blob = part.content instanceof Blob ? part.content : new Blob([part.content], { type: part.mimeType });
298
+ fd.append(name, blob, part.filename);
299
+ }
300
+ return fd;
301
+ }
285
302
  function maybeJson(text) {
286
303
  try {
287
304
  return JSON.parse(text);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  configDir
4
- } from "./chunk-ARZX2RLR.js";
4
+ } from "./chunk-S5W4FQ7M.js";
5
5
 
6
6
  // src/lib/circuit.ts
7
7
  import { statSync, writeFileSync, unlinkSync, mkdirSync, readdirSync, readFileSync } from "fs";
package/dist/cli/main.js CHANGED
@@ -10,9 +10,13 @@ import {
10
10
  listKnowledgeBases,
11
11
  minimalKbQueryResponse,
12
12
  pollIngest,
13
+ pollIngestByKnowledgeBaseId,
14
+ updateFile,
15
+ updateText,
13
16
  uploadFile,
14
- uploadText
15
- } from "../chunk-L4GVGVOP.js";
17
+ uploadText,
18
+ validateLocalFile
19
+ } from "../chunk-QVXDI77N.js";
16
20
  import {
17
21
  DEFAULT_BASE_URL,
18
22
  DEFAULT_USER_ID,
@@ -35,7 +39,7 @@ import {
35
39
  save,
36
40
  updateConfiguredDebug,
37
41
  writeInstalledPkgVersion
38
- } from "../chunk-ARZX2RLR.js";
42
+ } from "../chunk-S5W4FQ7M.js";
39
43
  import {
40
44
  beginUpdateNotification,
41
45
  completeUpdateNotification,
@@ -2612,8 +2616,61 @@ function parseMetadataFlags(raw) {
2612
2616
  }
2613
2617
 
2614
2618
  // src/cli/kb-cli.ts
2619
+ import { basename } from "path";
2615
2620
  var UPLOAD_TEXT_USAGE = "usage: ctxdb kb upload-text <kb-name> <doc-name> --text=<body> [--file-path=<server-logical-path>] [--wait|--no-wait]";
2616
2621
  var UPLOAD_FILE_USAGE = "usage: ctxdb kb upload-file <kb-name> <local-path> [--doc-name=...] [--file-path=<server-logical-path>] [--wait|--no-wait]";
2622
+ var UPDATE_TEXT_USAGE = "usage: ctxdb kb update-text <document-id> --text=<body> [--wait|--no-wait]\n or: ctxdb kb update-text --kb-name=<kb-name> --doc-name=<logical-name> --text=<body> [--file-path=<server-logical-path>] [--wait|--no-wait]";
2623
+ var UPDATE_FILE_USAGE = "usage: ctxdb kb update-file <document-id> <local-path> [--wait|--no-wait]\n or: ctxdb kb update-file <local-path> --kb-name=<kb-name> [--doc-name=<logical-name>] [--file-path=<server-logical-path>] [--wait|--no-wait]";
2624
+ var UPDATE_LOCATOR_FLAGS = ["kb-name", "doc-name", "file-path"];
2625
+ function nonEmptyFlag(args, name) {
2626
+ const value = args.flags[name];
2627
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
2628
+ }
2629
+ function hasAnyLocatorFlag(args) {
2630
+ return UPDATE_LOCATOR_FLAGS.some((name) => Object.hasOwn(args.flags, name));
2631
+ }
2632
+ function parseTextUpdateLocator(args) {
2633
+ if (args.positional.length === 1 && !hasAnyLocatorFlag(args)) {
2634
+ const documentId = args.positional[0];
2635
+ if (documentId) return { documentId };
2636
+ }
2637
+ if (args.positional.length === 0) {
2638
+ const kbName = nonEmptyFlag(args, "kb-name");
2639
+ const docName = nonEmptyFlag(args, "doc-name");
2640
+ const filePath = nonEmptyFlag(args, "file-path");
2641
+ const filePathValid = !Object.hasOwn(args.flags, "file-path") || !!filePath;
2642
+ if (kbName && docName && filePathValid) {
2643
+ return { kbName, docName, ...filePath ? { filePath } : {} };
2644
+ }
2645
+ }
2646
+ fail(UPDATE_TEXT_USAGE, 2);
2647
+ }
2648
+ function parseFileUpdate(args) {
2649
+ if (args.positional.length === 2 && !hasAnyLocatorFlag(args)) {
2650
+ const [documentId, localPath] = args.positional;
2651
+ if (documentId && localPath) return { locator: { documentId }, localPath };
2652
+ }
2653
+ if (args.positional.length === 1) {
2654
+ const [localPath] = args.positional;
2655
+ const kbName = nonEmptyFlag(args, "kb-name");
2656
+ const docNameFlag = nonEmptyFlag(args, "doc-name");
2657
+ const filePath = nonEmptyFlag(args, "file-path");
2658
+ const optionalFlagsValid = ["doc-name", "file-path"].every(
2659
+ (name) => !Object.hasOwn(args.flags, name) || !!nonEmptyFlag(args, name)
2660
+ );
2661
+ if (localPath && kbName && optionalFlagsValid) {
2662
+ return {
2663
+ locator: {
2664
+ kbName,
2665
+ docName: docNameFlag ?? basename(localPath),
2666
+ ...filePath ? { filePath } : {}
2667
+ },
2668
+ localPath
2669
+ };
2670
+ }
2671
+ }
2672
+ fail(UPDATE_FILE_USAGE, 2);
2673
+ }
2617
2674
  function shouldWaitForIngest(args, usage) {
2618
2675
  const waitRequested = args.flags.wait !== void 0;
2619
2676
  const noWaitRequested = args.flags["no-wait"] !== void 0;
@@ -2665,6 +2722,57 @@ async function kbUploadFile(args) {
2665
2722
  printResult({ document: final }, !!args.flags.json);
2666
2723
  return 0;
2667
2724
  }
2725
+ async function kbUpdateText(args) {
2726
+ const shouldWait = shouldWaitForIngest(args, UPDATE_TEXT_USAGE);
2727
+ const text = args.flags.text;
2728
+ if (typeof text !== "string" || text.length === 0) {
2729
+ fail(UPDATE_TEXT_USAGE, 2);
2730
+ }
2731
+ const locator = parseTextUpdateLocator(args);
2732
+ const ctx = buildContext(args);
2733
+ const doc = await updateText(ctx.client, locator, text);
2734
+ if (!shouldWait) {
2735
+ printResult({ document: doc }, !!args.flags.json);
2736
+ return 0;
2737
+ }
2738
+ const final = await pollIngestByKnowledgeBaseId(
2739
+ ctx.client,
2740
+ doc.knowledge_base_id,
2741
+ doc.id,
2742
+ {
2743
+ timeoutMs: DEFAULT_INGEST_TIMEOUT_MS
2744
+ }
2745
+ );
2746
+ printResult({ document: final }, !!args.flags.json);
2747
+ return 0;
2748
+ }
2749
+ async function kbUpdateFile(args) {
2750
+ const shouldWait = shouldWaitForIngest(args, UPDATE_FILE_USAGE);
2751
+ const { locator, localPath } = parseFileUpdate(args);
2752
+ try {
2753
+ validateLocalFile(localPath);
2754
+ } catch (error) {
2755
+ fail(error instanceof Error ? error.message : String(error), 2);
2756
+ }
2757
+ const ctx = buildContext(args);
2758
+ const doc = await updateFile(ctx.client, locator, localPath, {
2759
+ timeoutMs: DEFAULT_FILE_UPLOAD_TIMEOUT_MS
2760
+ });
2761
+ if (!shouldWait) {
2762
+ printResult({ document: doc }, !!args.flags.json);
2763
+ return 0;
2764
+ }
2765
+ const final = await pollIngestByKnowledgeBaseId(
2766
+ ctx.client,
2767
+ doc.knowledge_base_id,
2768
+ doc.id,
2769
+ {
2770
+ timeoutMs: DEFAULT_FILE_INGEST_TIMEOUT_MS
2771
+ }
2772
+ );
2773
+ printResult({ document: final }, !!args.flags.json);
2774
+ return 0;
2775
+ }
2668
2776
  async function kbCreate(args) {
2669
2777
  const kbName = args.positional[0];
2670
2778
  if (!kbName) fail("usage: ctxdb kb create <kb-name> [--description=<desc>]");
@@ -2944,9 +3052,16 @@ COMMANDS
2944
3052
  [--file-path=<server-logical-path>] [--wait|--no-wait]
2945
3053
  kb upload-file <kb-name> <local-path> [--agent=<name>] [--doc-name=...]
2946
3054
  [--file-path=<server-logical-path>] [--wait|--no-wait]
2947
- Uploads return immediately by default.
3055
+ kb update-text <document-id> --text=<body> [--agent=<name>] [--wait|--no-wait]
3056
+ or: update-text --kb-name=<kb-name> --doc-name=<logical-name> --text=<body>
3057
+ [--file-path=<server-logical-path>] [--wait|--no-wait]
3058
+ kb update-file <document-id> <local-path> [--agent=<name>] [--wait|--no-wait]
3059
+ or: update-file <local-path> --kb-name=<kb-name> [--doc-name=<logical-name>]
3060
+ [--file-path=<server-logical-path>] [--wait|--no-wait]
3061
+ Uploads return immediately by default; updates do too.
2948
3062
  --wait polls for terminal ingest status.
2949
3063
  --no-wait is a compatibility alias for the default.
3064
+ Keep the returned ID for the next update.
2950
3065
  kb list [--agent=<name>]
2951
3066
  kb documents-list <kb-name> [--agent=<name>]
2952
3067
  kb document-get <kb-name> <doc-id> [--agent=<name>]
@@ -3012,6 +3127,8 @@ var ROUTES = {
3012
3127
  create: kbCreate,
3013
3128
  "upload-text": kbUploadText,
3014
3129
  "upload-file": kbUploadFile,
3130
+ "update-text": kbUpdateText,
3131
+ "update-file": kbUpdateFile,
3015
3132
  list: kbList,
3016
3133
  "documents-list": kbDocumentsList,
3017
3134
  "document-get": kbDocumentGet,
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  captureParsedMessages
4
- } from "../chunk-6BB65W5W.js";
5
- import "../chunk-B7JCQL4O.js";
4
+ } from "../chunk-DC5NWGDW.js";
5
+ import "../chunk-X2BX6LR6.js";
6
6
  import {
7
7
  HttpClient,
8
8
  agentFromArgvWithFallback,
9
9
  debug,
10
10
  load,
11
11
  setDebug
12
- } from "../chunk-ARZX2RLR.js";
12
+ } from "../chunk-S5W4FQ7M.js";
13
13
  import {
14
14
  shouldSkipHooks
15
15
  } from "../chunk-UEKR2Z3S.js";
@@ -2,17 +2,17 @@
2
2
  import {
3
3
  HOOK_TIMEOUT_MS,
4
4
  composeSessionStart
5
- } from "../chunk-BHRWAU3E.js";
5
+ } from "../chunk-NFTGG6WK.js";
6
6
  import {
7
7
  composeUserPromptSubmit
8
- } from "../chunk-7EIJB5LA.js";
8
+ } from "../chunk-R2QS6ACP.js";
9
9
  import {
10
10
  fetchKbCatalogBlock
11
- } from "../chunk-VHNHVMCA.js";
12
- import "../chunk-L4GVGVOP.js";
11
+ } from "../chunk-DPLMBHLU.js";
12
+ import "../chunk-QVXDI77N.js";
13
13
  import {
14
14
  isCircuitOpen
15
- } from "../chunk-B7JCQL4O.js";
15
+ } from "../chunk-X2BX6LR6.js";
16
16
  import {
17
17
  HttpClient,
18
18
  agentFromArgvWithFallback,
@@ -20,7 +20,7 @@ import {
20
20
  isComplete,
21
21
  load,
22
22
  setDebug
23
- } from "../chunk-ARZX2RLR.js";
23
+ } from "../chunk-S5W4FQ7M.js";
24
24
  import {
25
25
  shouldSkipHooks
26
26
  } from "../chunk-UEKR2Z3S.js";
@@ -3,12 +3,12 @@ import {
3
3
  HOOK_TIMEOUT_MS,
4
4
  composeSessionStart,
5
5
  formatSessionStartStdout
6
- } from "../chunk-BHRWAU3E.js";
7
- import "../chunk-VHNHVMCA.js";
8
- import "../chunk-L4GVGVOP.js";
6
+ } from "../chunk-NFTGG6WK.js";
7
+ import "../chunk-DPLMBHLU.js";
8
+ import "../chunk-QVXDI77N.js";
9
9
  import {
10
10
  isCircuitOpen
11
- } from "../chunk-B7JCQL4O.js";
11
+ } from "../chunk-X2BX6LR6.js";
12
12
  import {
13
13
  HttpClient,
14
14
  agentFromArgvWithFallback,
@@ -16,7 +16,7 @@ import {
16
16
  isComplete,
17
17
  load,
18
18
  setDebug
19
- } from "../chunk-ARZX2RLR.js";
19
+ } from "../chunk-S5W4FQ7M.js";
20
20
  import {
21
21
  shouldSkipHooks
22
22
  } from "../chunk-UEKR2Z3S.js";
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  captureTurn
4
- } from "../chunk-6BB65W5W.js";
5
- import "../chunk-B7JCQL4O.js";
4
+ } from "../chunk-DC5NWGDW.js";
5
+ import "../chunk-X2BX6LR6.js";
6
6
  import {
7
7
  HttpClient,
8
8
  agentFromArgvWithFallback,
9
9
  debug,
10
10
  load,
11
11
  setDebug
12
- } from "../chunk-ARZX2RLR.js";
12
+ } from "../chunk-S5W4FQ7M.js";
13
13
  import {
14
14
  shouldSkipHooks
15
15
  } from "../chunk-UEKR2Z3S.js";
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  composeUserPromptSubmit,
4
4
  formatUserPromptSubmitStdout
5
- } from "../chunk-7EIJB5LA.js";
6
- import "../chunk-VHNHVMCA.js";
7
- import "../chunk-L4GVGVOP.js";
5
+ } from "../chunk-R2QS6ACP.js";
6
+ import "../chunk-DPLMBHLU.js";
7
+ import "../chunk-QVXDI77N.js";
8
8
  import {
9
9
  isCircuitOpen
10
- } from "../chunk-B7JCQL4O.js";
10
+ } from "../chunk-X2BX6LR6.js";
11
11
  import {
12
12
  HttpClient,
13
13
  agentFromArgvWithFallback,
@@ -15,7 +15,7 @@ import {
15
15
  isComplete,
16
16
  load,
17
17
  setDebug
18
- } from "../chunk-ARZX2RLR.js";
18
+ } from "../chunk-S5W4FQ7M.js";
19
19
  import {
20
20
  shouldSkipHooks
21
21
  } from "../chunk-UEKR2Z3S.js";
@@ -12,7 +12,8 @@ description: contextdb 知识库高频操作(查询 / 浏览 KB / 上传或更
12
12
  - 用户想**找已有 KB 里的内容** → §2 recipe 1
13
13
  - 用户**不知道有哪些 KB** → §2 recipe 2
14
14
  - 用户想**了解某个 KB 收了哪些文档** → §2 recipe 3
15
- - 用户要**写入/上传/更新内容** → §2 recipe 4/5
15
+ - 用户要**新建/上传内容** → §2 recipe 4/5
16
+ - 用户要**更新已有文档** → §2 recipe 6/7
16
17
 
17
18
  配套 skill:**contextdb-memory**(长期记忆操作)。两者用同一份 `~/.ctxdb/ctxdb.json` 配置和同一个 `--agent` 路由。
18
19
 
@@ -101,7 +102,40 @@ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
101
102
 
102
103
  **何时不适用**:要上传的内容是字符串而非本地文件 → recipe 4。
103
104
 
104
- > **同名文档更新**:同一 KB、同一 `--file-path` 下,以相同文档名再次上传,服务端按更新(替换)语义处理;继续调用对应的 `kb upload-text` / `kb upload-file` 命令即可(`upload-file` 必要时用 `--doc-name` 保持文档名不变)。
105
+ ### Recipe 6:更新已有文本文档
106
+
107
+ **场景**:用户明确要替换某个已存在 KB 文本文档的内容。
108
+
109
+ ```sh
110
+ ctxdb kb update-text <document-id> --text="<new-body>" --agent={{agent}}
111
+
112
+ # 只有拿不到精确 document ID、但已知完整逻辑键时才用:
113
+ ctxdb kb update-text --kb-name=<kb-name> --doc-name=<logical-name> --text="<new-body>" [--file-path=<logical-parent-path>] --agent={{agent}}
114
+ ```
115
+
116
+ 已有精确 `document-id` 时优先用第一种,不需要再向用户索取 KB,也不要同时传
117
+ `--kb-name` / `--doc-name` / `--file-path`。服务端会从文档 ID 校验权限并解析所属 KB。
118
+ 命令返回的 `document.id` 是后续更新的新权威 ID:内容变化时通常会换 ID,相同内容也可能
119
+ 复用原 ID,Agent 都必须以本次返回值为准。只有拿不到 ID、但已确认完整 **KB + 文档名 +
120
+ `file_path`** 逻辑键时才用第二种;目标信息不完整时先核对或向用户确认,不能模糊猜测。
121
+ 重复调用 `upload-text` 是创建冲突,不会自动转成更新。
122
+
123
+ ### Recipe 7:更新已有文件文档
124
+
125
+ **场景**:用户用新的本地文件内容替换某个已存在 KB 文件文档。
126
+
127
+ ```sh
128
+ ctxdb kb update-file <document-id> <local-path> --agent={{agent}}
129
+
130
+ # 只有拿不到精确 document ID、但已知完整逻辑键时才用:
131
+ ctxdb kb update-file <local-path> --kb-name=<kb-name> [--doc-name=<existing-logical-name>] [--file-path=<logical-parent-path>] --agent={{agent}}
132
+ ```
133
+
134
+ ID 模式只用 `document-id` 定位远端文档,本地文件名只是上传的物理文件名;替换文件改了
135
+ 文件名也不需要补远端名称或 KB。逻辑键模式下,远端逻辑文档名默认取本地文件名;若替换
136
+ 文件名不同,必须用 `--doc-name` 继续传已有逻辑名。两种定位方式不能混用。分片更新只在
137
+ init 使用 PUT,part/complete 保持原协议;网络结果不明确时不自动重试或删除旧文档。
138
+ 成功后同样必须保存返回的 `document.id`,不要继续沿用输入 ID。
105
139
 
106
140
  ## 3. 命令速查
107
141
 
@@ -116,6 +150,8 @@ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
116
150
  | `kb create` | `ctxdb kb create <kb-name> --agent=<name>` |
117
151
  | `kb upload-text` | `ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent=<name>` |
118
152
  | `kb upload-file` | `ctxdb kb upload-file <kb-name> <local-path> --agent=<name>` |
153
+ | `kb update-text` | `ctxdb kb update-text <document-id> --text="<body>" --agent=<name>` |
154
+ | `kb update-file` | `ctxdb kb update-file <document-id> <local-path> --agent=<name>` |
119
155
 
120
156
  所有命令 JSON 写 stdout、错误写 stderr 非零退出码。
121
157
 
@@ -127,7 +163,7 @@ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
127
163
 
128
164
  **`<external-knowledge>` 块**:开启 KB 自动召回时,内部按 `## 图谱合成上下文` / `## 文档证据` 分区。图谱区只用于关系理解和检索导航,文档区才是回答与引用证据;图谱区被独立限制长度,不会挤掉已选中的文档证据。
129
165
 
130
- **B-3c 守卫**:含 `kb upload-text` / `kb upload-file` 的对话轮次,其内容**不会**被 autoCapture 写入记忆(防文档原文混进记忆桶)。
166
+ **B-3c 守卫**:含 `kb upload-text` / `kb upload-file` / `kb update-text` / `kb update-file` 的对话轮次,其内容**不会**被 autoCapture 写入记忆(防文档原文混进记忆桶)。
131
167
 
132
168
  **纯 CLI 模式**:对话里没有上面任何块 → 说明当前不在 hook 环境(agent 是裸 CLI 调用),所有信息只能主动调命令拿。
133
169
 
@@ -145,11 +181,12 @@ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
145
181
  - **根因**:shell + python -c 的引号是两层独立 escape,组合时极容易漏
146
182
  - **正确做法**:`ctxdb kb list --agent={{agent}} | jq '.knowledge_bases[].name'`;jq 表达式在单引号里不需要 escape
147
183
 
148
- ### 注意事项 3:`upload-*` / `create` 是副作用,没明示 KB 名前必须先确认
184
+ ### 注意事项 3:创建和逻辑键更新没明示 KB 名前必须先确认
149
185
 
150
186
  - **现象**:用户只说"把这段记进知识库"没指定 KB,agent 直接用了某个猜的 KB 名 → 写到错的桶
151
187
  - **根因**:写命令不会跟 agent 二次确认;KB 不存在时报错,但**KB 名拼错却恰好命中另一个真实 KB**时会静默写错
152
- - **正确做法**:用户没明示 必须先 `kb list` 拿候选清单,跟用户确认目标后才执行
188
+ - **正确做法**:`upload-*` / `create` / 逻辑键 `update-*` 在用户没明示 KB 时,必须先
189
+ `kb list` 拿候选清单并确认;精确 document-ID 更新已经绑定目标,不再额外要求 KB
153
190
 
154
191
  ### 注意事项 4:`kb search` 空结果时降阈值再试一次
155
192
 
@@ -175,12 +212,13 @@ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
175
212
  | `--verbose` | 每个 chunk 加 `doc_name` / `kb_id` / `doc_id` / `tags` | false |
176
213
  | `--raw` | 服务端原始响应(13+ 字段,图结果仍混在 `chunks`,含 tokenizer 细节) | false |
177
214
 
178
- ### `kb upload-file` / `kb upload-text` 全部 flag
215
+ ### `kb upload-*` / `kb update-*` 全部 flag
179
216
 
180
217
  | 参数 | 说明 |
181
218
  |------|------|
182
- | `--doc-name` | (`upload-file`)服务端文档名,默认取本地文件名 |
183
- | `--file-path` | KB 内的逻辑父目录,不包含文档名/文件名 |
219
+ | `--kb-name` | 仅逻辑键更新使用;不能与位置参数中的 document ID 混用 |
220
+ | `--doc-name` | `upload-file` 的逻辑文档名,或逻辑键更新的精确文档名;ID 更新不使用 |
221
+ | `--file-path` | 创建/逻辑键更新时 KB 内的逻辑父目录;ID 更新不使用 |
184
222
  | `--wait` | 等到 ingest 成功、失败或达到原有超时;用于迁移旧的默认等待行为 |
185
223
  | `--no-wait` | 兼容参数;与新的“受理后立即返回”默认行为相同 |
186
224
 
@@ -95,7 +95,7 @@ ctxdb memory delete <memory-id> --agent={{agent}}
95
95
 
96
96
  **autoCapture(Stop / post_llm_call / plugin)**:每轮对话结束后服务端异步提炼事实写记忆,**不用 agent 主动调** `memory add`。`memory add` 用于用户**明确强调**的内容(强加重要性)。
97
97
 
98
- **B-3c 守卫**:含 `kb upload-text` / `kb upload-file` 的轮次,**autoCapture 跳过**这一轮(防文档原文进记忆桶)。
98
+ **B-3c 守卫**:含 `kb upload-text` / `kb upload-file` / `kb update-text` / `kb update-file` 的轮次,**autoCapture 跳过**这一轮(防新建或更新的文档原文进记忆桶)。
99
99
 
100
100
  **纯 CLI 模式**:对话里没 `<recalled-memories>` 块 → 不在 hook 环境(裸 CLI),所有 recall / capture 只能主动 `ctxdb memory search` / `ctxdb memory add`。
101
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliyunrds/ctxdb",
3
- "version": "1.0.6-beta.0",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "Unified access layer for RDS ContextDatabase: `ctxdb` CLI (memory + KB ops), one-shot `setup --agent <qoder|qoderwork|codex|claude|opencode|hermes>` installer, per-agent config, hooks/plugins, and SKILL.md.",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "semver": "^7.8.5",
34
34
  "yaml": "^2.9.0",
35
- "@aliyunrds/ctxdb-shared": "~0.0.6"
35
+ "@aliyunrds/ctxdb-shared": "~0.0.7"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.15.0",