@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 +51 -12
- package/dist/{chunk-6BB65W5W.js → chunk-DC5NWGDW.js} +2 -2
- package/dist/{chunk-VHNHVMCA.js → chunk-DPLMBHLU.js} +3 -3
- package/dist/{chunk-BHRWAU3E.js → chunk-NFTGG6WK.js} +2 -2
- package/dist/{chunk-L4GVGVOP.js → chunk-QVXDI77N.js} +235 -43
- package/dist/{chunk-7EIJB5LA.js → chunk-R2QS6ACP.js} +1 -1
- package/dist/{chunk-ARZX2RLR.js → chunk-S5W4FQ7M.js} +27 -10
- package/dist/{chunk-B7JCQL4O.js → chunk-X2BX6LR6.js} +1 -1
- package/dist/cli/main.js +121 -4
- package/dist/hooks/hermes-post-llm-call.js +3 -3
- package/dist/hooks/hermes-pre-llm-call.js +6 -6
- package/dist/hooks/session-start.js +5 -5
- package/dist/hooks/stop.js +3 -3
- package/dist/hooks/user-prompt-submit.js +5 -5
- package/dist/setup/skills/contextdb-knowledge/SKILL.md +46 -8
- package/dist/setup/skills/contextdb-memory/SKILL.md +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
296
|
+
### KB create/update completion mode
|
|
297
297
|
|
|
298
|
-
|
|
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`
|
|
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.
|
|
324
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
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-
|
|
7
|
+
} from "./chunk-X2BX6LR6.js";
|
|
8
8
|
import {
|
|
9
9
|
CtxdbError,
|
|
10
10
|
debug,
|
|
11
11
|
isDebug
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
4
|
+
} from "./chunk-QVXDI77N.js";
|
|
5
5
|
import {
|
|
6
6
|
isConnectionError,
|
|
7
7
|
resetCircuit,
|
|
8
8
|
tripCircuit
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-X2BX6LR6.js";
|
|
10
10
|
import {
|
|
11
11
|
CtxdbError
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
5
|
+
} from "./chunk-DPLMBHLU.js";
|
|
6
6
|
import {
|
|
7
7
|
debug
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
149
|
+
return body;
|
|
105
150
|
}
|
|
106
151
|
async function uploadFile(client, kbName, localPath, options = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
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.
|
|
163
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
};
|
|
@@ -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 =
|
|
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);
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
5
|
-
import "../chunk-
|
|
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-
|
|
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-
|
|
5
|
+
} from "../chunk-NFTGG6WK.js";
|
|
6
6
|
import {
|
|
7
7
|
composeUserPromptSubmit
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-R2QS6ACP.js";
|
|
9
9
|
import {
|
|
10
10
|
fetchKbCatalogBlock
|
|
11
|
-
} from "../chunk-
|
|
12
|
-
import "../chunk-
|
|
11
|
+
} from "../chunk-DPLMBHLU.js";
|
|
12
|
+
import "../chunk-QVXDI77N.js";
|
|
13
13
|
import {
|
|
14
14
|
isCircuitOpen
|
|
15
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-NFTGG6WK.js";
|
|
7
|
+
import "../chunk-DPLMBHLU.js";
|
|
8
|
+
import "../chunk-QVXDI77N.js";
|
|
9
9
|
import {
|
|
10
10
|
isCircuitOpen
|
|
11
|
-
} from "../chunk-
|
|
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-
|
|
19
|
+
} from "../chunk-S5W4FQ7M.js";
|
|
20
20
|
import {
|
|
21
21
|
shouldSkipHooks
|
|
22
22
|
} from "../chunk-UEKR2Z3S.js";
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
captureTurn
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
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-
|
|
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-
|
|
6
|
-
import "../chunk-
|
|
7
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-R2QS6ACP.js";
|
|
6
|
+
import "../chunk-DPLMBHLU.js";
|
|
7
|
+
import "../chunk-QVXDI77N.js";
|
|
8
8
|
import {
|
|
9
9
|
isCircuitOpen
|
|
10
|
-
} from "../chunk-
|
|
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-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
184
|
+
### 注意事项 3:创建和逻辑键更新没明示 KB 名前必须先确认
|
|
149
185
|
|
|
150
186
|
- **现象**:用户只说"把这段记进知识库"没指定 KB,agent 直接用了某个猜的 KB 名 → 写到错的桶
|
|
151
187
|
- **根因**:写命令不会跟 agent 二次确认;KB 不存在时报错,但**KB 名拼错却恰好命中另一个真实 KB**时会静默写错
|
|
152
|
-
-
|
|
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
|
|
215
|
+
### `kb upload-*` / `kb update-*` 全部 flag
|
|
179
216
|
|
|
180
217
|
| 参数 | 说明 |
|
|
181
218
|
|------|------|
|
|
182
|
-
| `--
|
|
183
|
-
| `--
|
|
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.
|
|
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.
|
|
35
|
+
"@aliyunrds/ctxdb-shared": "~0.0.7"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.15.0",
|