@aliyunrds/ctxdb 1.0.2 → 1.0.4

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
@@ -1,6 +1,6 @@
1
1
  # @aliyunrds/ctxdb
2
2
 
3
- Unified access layer for RDS ContextDatabase. One `ctxdb` CLI (memory + KB ops), one `setup` installer (`--agent <…>` for hooks/skills, bare for CLI-only), per-agent SKILL.md — supports multiple code agents from a single package.
3
+ Unified access layer for RDS ContextDatabase. One `ctxdb` CLI (memory + KB ops), one `setup` installer (`--agent <…>` for one hooks/skills integration, `--all` for every detected integration, bare for CLI-only), per-agent SKILL.md — supports multiple code agents from a single package.
4
4
 
5
5
  | Agent | Hooks | Skill install path | Skill style |
6
6
  |---|---|---|---|
@@ -15,6 +15,87 @@ Config and logs live under `~/.ctxdb/`. `~/.ctxdb/ctxdb.json` is one file, but r
15
15
 
16
16
  ## Install
17
17
 
18
+ ### Recommended: console-generated installer command
19
+
20
+ The customer console supplies the selected Agent and API key to one of two
21
+ standalone public files. Replace `<public-oss-url>` with the release-owned OSS
22
+ base URL:
23
+
24
+ ```sh
25
+ # macOS / Linux
26
+ curl -fsSL <public-oss-url>/install.sh | \
27
+ bash -s -- --agent codex --api-key 'ctxdb-...'
28
+ ```
29
+
30
+ ```powershell
31
+ # Windows PowerShell
32
+ & ([ScriptBlock]::Create((irm '<public-oss-url>/install.ps1'))) `
33
+ -Agent codex -ApiKey 'ctxdb-...'
34
+ ```
35
+
36
+ Windows Command Prompt uses the same `install.ps1`; there is no third CMD
37
+ artifact:
38
+
39
+ ```bat
40
+ powershell.exe -NoProfile -Command "& ([ScriptBlock]::Create((irm '<public-oss-url>/install.ps1'))) -Agent 'codex' -ApiKey 'ctxdb-...'"
41
+ ```
42
+
43
+ `--agent openclaw` / `-Agent openclaw` takes a dedicated plugin flow instead
44
+ of `ctxdb setup`: the installer requires an existing `openclaw` CLI, runs
45
+ `openclaw plugins install openclaw_memory_rds_ctxdb`, `openclaw plugins
46
+ enable openclaw_memory_rds_ctxdb`, and `openclaw ctxdb init --api-key <key>`,
47
+ then prints `openclaw gateway restart` for the user to run. It never installs
48
+ Node.js, npm packages, or the ctxdb CLI on this path.
49
+
50
+ An agent-readable manual, `bootstrap/install.md`, is published alongside the
51
+ scripts. A customer's coding agent can be pointed at its public URL to run the
52
+ same four-stage flow by hand under the same safety boundaries (used when the
53
+ scripts are unavailable or blocked by local policy).
54
+
55
+ The installer shows four stages: check runtime, install the CLI, configure the
56
+ Agent, and verify the connection. It requires Node.js 20 or newer and the npm
57
+ executable shipped beside that exact Node.js runtime. If Node.js is absent, it
58
+ can offer one reviewed Node.js 22 action after explicit confirmation. If an
59
+ existing Node.js is old, broken, or missing its sibling npm, it stops with
60
+ upgrade/repair guidance and does not install another private runtime.
61
+
62
+ The API key is passed directly to `ctxdb setup` and stored in
63
+ `~/.ctxdb/ctxdb.json`; installer-controlled output redacts the complete value.
64
+ The generated command itself contains the key, so treat it as a credential and
65
+ follow the local shell-history policy. The installer never retries with sudo,
66
+ silently elevates, or edits a shell/PowerShell profile. If the npm global prefix
67
+ is not writable, it uses a user-only prefix and prints one PATH follow-up.
68
+
69
+ Immediately before setup, the installer saves an opaque, private temporary copy
70
+ of `~/.ctxdb/ctxdb.json`. If setup fails, it prints the original setup
71
+ diagnostic, restores the previous config exactly, or removes the incomplete new
72
+ config when none existed before. Hooks, plugins, and skills may already have
73
+ changed; fix the reported cause and rerun the same command to repair them. A
74
+ later `status` failure does not roll back a setup that already succeeded.
75
+
76
+ Add `--dry-run` on macOS/Linux or `-DryRun` on Windows to preview the same four
77
+ stages without prompting, installing Node.js or ctxdb, writing a prefix probe,
78
+ or running setup/status:
79
+
80
+ ```sh
81
+ curl -fsSL <public-oss-url>/install.sh | \
82
+ bash -s -- --agent codex --api-key 'ctxdb-...' --dry-run
83
+ ```
84
+
85
+ ```powershell
86
+ & ([ScriptBlock]::Create((irm '<public-oss-url>/install.ps1'))) `
87
+ -Agent codex -ApiKey 'ctxdb-...' -DryRun
88
+ ```
89
+
90
+ Dry run still validates the required parameters and inspects the local runtime,
91
+ but it does not contact the npm registry or validate the API key, connectivity,
92
+ or Agent integration. Its output always redacts the supplied key.
93
+
94
+ For review-first installation, download the matching file, verify its published
95
+ SHA-256, inspect it, and run the local copy with the same parameters.
96
+
97
+ ### Manual setup and local configuration management
98
+
18
99
  ```sh
19
100
  npm install -g @aliyunrds/ctxdb
20
101
 
@@ -31,6 +112,9 @@ ctxdb setup --agent opencode --api-key ctxdb-...
31
112
  # Hermes (pre_llm_call recall/warmup + post_llm_call capture + skill; macOS/Linux)
32
113
  ctxdb setup --agent hermes --api-key ctxdb-...
33
114
 
115
+ # Detect and configure every supported Agent already used on this host.
116
+ ctxdb setup --all --api-key ctxdb-...
117
+
34
118
  # --base-url defaults to https://context-database.aliyuncs.com (public prod).
35
119
  # Pass --base-url <host> only if you target a different deployment
36
120
  # (e.g., a pre / staging host, or a self-hosted instance).
@@ -39,7 +123,42 @@ ctxdb setup --agent hermes --api-key ctxdb-...
39
123
  # sharing the same workspace (see "Configuration" below).
40
124
 
41
125
  # Verify
42
- ctxdb status --json
126
+ ctxdb status --agent codex --json
127
+ ctxdb status --all --json
128
+ ```
129
+
130
+ `setup --all` and `status --all` use one read-only detector and return Agents in
131
+ `SUPPORTED_AGENTS` order. Qoder/QoderWork/Codex/Claude require an existing
132
+ primary or supported variant home. OpenCode and Hermes are selected when their
133
+ home exists or their canonical executable is discoverable on PATH. Detection
134
+ does not create files, does not use an existing ctxdb profile as evidence, and
135
+ never selects `default` or OpenClaw. If nothing is detected, the command exits
136
+ non-zero and asks the user to install/start an Agent once or use explicit
137
+ `--agent <name>`.
138
+
139
+ When `ctxdb setup --all` is invoked without `--api-key`, the CLI snapshots each
140
+ selected Agent's effective configuration before any setup write. Credential
141
+ priority is `--api-key`, `CTXDB_API_KEY`, the Agent profile, then the `default`
142
+ profile. This means a key already configured on `agents.default` is reused for
143
+ detected Agents that do not have their own key. An Agent with no effective key
144
+ is reported failed without stopping later Agents. Supplying one explicit key
145
+ applies it to every selected Agent. Batch output includes per-Agent results and
146
+ one deduplicated uninstall-order hint, including after a partial failure.
147
+
148
+ The same two commands cover later management without a separate wizard:
149
+
150
+ ```sh
151
+ # Add another Agent
152
+ ctxdb setup --agent claude --api-key 'ctxdb-...'
153
+
154
+ # Repair hooks/plugin/skills using the Agent's stored configuration
155
+ ctxdb setup --agent codex
156
+
157
+ # Replace only this Agent's API key
158
+ ctxdb setup --agent codex --api-key 'ctxdb-new-...'
159
+
160
+ # Inspect current local/configuration/connectivity state
161
+ ctxdb status --agent codex --json
43
162
  ```
44
163
 
45
164
  Setup first checks that the target agent home exists (`~/.qoder`, `~/.qoderwork`, `~/.codex`, or `~/.claude`). If the directory is missing, install or start that agent once before running `ctxdb setup --agent <name>`. OpenCode and Hermes are config-dir style: setup creates `~/.config/opencode` / `~/.hermes` when they are missing.
@@ -52,7 +171,7 @@ Hermes owns this security allowlist, so ctxdb only checks it and never writes ap
52
171
  hermes --accept-hooks chat
53
172
  ```
54
173
 
55
- This command opens an interactive chat. After Hermes starts, exit the chat and rerun `ctxdb setup --agent hermes`; the allowlist check should then pass.
174
+ This command opens an interactive chat. After Hermes starts, exit the chat and rerun `ctxdb setup --agent hermes`; the allowlist check should then pass. Finally, restart any running Hermes CLI or gateway process so it reloads `config.yaml` and registers the hooks. The public bootstrap installer preserves the hooks in this allowlist-pending state and reports these commands as required follow-up instead of rolling the integration back.
56
175
 
57
176
  The two ctxdb events are `pre_llm_call` and `post_llm_call`. `ctxdb status --agent hermes` also verifies their exact `(event, command)` pairs and reports the specific missing approval.
58
177
 
@@ -70,6 +189,16 @@ ctxdb update --agent codex
70
189
  ctxdb update --no-self-update
71
190
  ```
72
191
 
192
+ For npm-installed copies, eligible interactive `ctxdb` commands also schedule a detached version check at most once every 24 hours. The command never waits for npm: a newly discovered release can be shown only on a later invocation. A successful command may append this hint to stderr:
193
+
194
+ ```text
195
+ ctxdb: update available v1.0.2 → v1.1.0; run `ctxdb update`.
196
+ ```
197
+
198
+ The same release is mentioned at most once every 24 hours. Failed registry checks retry with a one-hour exponential backoff capped at six hours. Checks and hints are suppressed for JSON output, non-interactive stderr, CI, `ctxdb update` / `ctxdb upgrade`, and non-npm layouts. Set `CTXDB_DISABLE_UPDATE_CHECK=1` to opt out completely.
199
+
200
+ The notifier only reads/writes `~/.ctxdb/cache/version-check.json`; it never installs a package or changes agent setup. Updating remains an explicit `ctxdb update` action.
201
+
73
202
  The existing `--self-update` flag remains accepted for compatibility, but is no longer required. If the npm version check, global install, or setup pass fails, ctxdb prints the manual recovery commands:
74
203
 
75
204
  ```sh
@@ -99,7 +228,7 @@ For **hermes**, `ctxdb setup --agent hermes` writes two shell hooks under `~/.he
99
228
 
100
229
  ## CLI
101
230
 
102
- `ctxdb` ships 18 subcommands:
231
+ `ctxdb` ships grouped non-interactive commands:
103
232
 
104
233
  - **Top-level**: `setup` / `status` / `ping` / `uninstall` / `update` (`upgrade` alias)
105
234
  - **Memory**: `memory add|search|list|get|update|delete`
@@ -107,13 +236,53 @@ For **hermes**, `ctxdb setup --agent hermes` writes two shell hooks under `~/.he
107
236
 
108
237
  See `ctxdb --help`.
109
238
 
110
- `setup` without `--agent` writes `agents.default` in `~/.ctxdb/ctxdb.json` (CLI-only, no hooks or skills); with `--agent <qoder|qoderwork|codex|claude|opencode|hermes>` it writes agent config and installs hooks/plugins + skills for that harness. Memory and KB commands also accept `--agent <name>` so agents use their own config section; when omitted, `CTXDB_AGENT` env wins, otherwise `agents.default` is used. `uninstall` without `--agent` loops every supported agent; with `--agent <name>` it targets just that one. `teardown` is an alias for `uninstall`.
239
+ `setup` without `--agent` writes `agents.default` in `~/.ctxdb/ctxdb.json` (CLI-only, no hooks or skills); with `--agent <qoder|qoderwork|codex|claude|opencode|hermes>` it writes agent config and installs hooks/plugins + skills for that harness. Rerunning it repairs integration artifacts and may replace the selected Agent's API key. Memory and KB commands also accept `--agent <name>` so agents use their own config section; when omitted, `CTXDB_AGENT` env wins, otherwise `agents.default` is used. `uninstall` without `--agent` loops every supported agent; with `--agent <name>` it targets just that one. `teardown` is an alias for `uninstall`.
111
240
 
112
241
  `memory add … --no-infer` stores the text verbatim (skips server-side LLM
113
242
  fact-extraction). Use it when the user explicitly asks for a verbatim
114
243
  memory ("记住 / 请记忆 / 原文记下"); without `--no-infer` the server may
115
244
  rewrite, merge, or skip details.
116
245
 
246
+ ### KB upload completion mode
247
+
248
+ **Breaking change:** `kb upload-text` and `kb upload-file` now return as soon
249
+ as the service accepts the document. The returned document may still have
250
+ `ingest_status: "processing"`; acceptance does not mean ingestion succeeded.
251
+
252
+ - Add `--wait` when a script needs the previous behavior: poll until ingestion
253
+ succeeds, fails, or reaches the existing command timeout.
254
+ - Existing `--no-wait` calls remain valid and behave like the new default.
255
+ - `--wait` and `--no-wait` cannot be combined.
256
+
257
+ Scripts that relied on implicit terminal polling must migrate to:
258
+
259
+ ```sh
260
+ ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --wait
261
+ ctxdb kb upload-file <kb-name> <local-path> --wait
262
+ ```
263
+
264
+ ### KB file upload policy and large files
265
+
266
+ `kb upload-file` does not maintain a client-side format allowlist or file-size
267
+ table. It validates only that the local path exists, is a readable regular
268
+ file, then lets the service decide whether the format, per-type size, quota,
269
+ and logical document identity are acceptable. Unknown extensions are sent as
270
+ `application/octet-stream`.
271
+
272
+ File bytes are streamed from disk with a known multipart size; the CLI does
273
+ not read the complete file into a Buffer. It always attempts the ordinary
274
+ upload endpoint first. If the service returns the dedicated
275
+ `KNOWLEDGE_DIRECT_UPLOAD_REQUIRES_CHUNK` error (`errorCode=6001`) or an
276
+ unstructured HTTP 413, the CLI resolves the KB ID and automatically uses the
277
+ chunk init/part/complete protocol. `--doc-name` and `--file-path` have the same
278
+ logical meaning in both modes.
279
+
280
+ The first chunk-routing release intentionally sends init, each part, and
281
+ complete only once. It does not automatically retry timeout, connection-loss,
282
+ or response-loss failures because the service cannot yet prove that those
283
+ operations are idempotent. Failures before complete trigger a best-effort
284
+ abort; ambiguous complete failures are returned to the caller.
285
+
117
286
  `kb search <query> [--kb=name1,name2] [--top-k=N] [--threshold=F]` is the
118
287
  agent-driven KB recall path — calls `POST /v1/knowledge/query` directly
119
288
  (distinct from `memory search --knowledge`, which is a memory + KB hybrid
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CtxdbError
4
+ } from "./chunk-JFTKYEVN.js";
5
+
6
+ // src/lib/kb.ts
7
+ import {
8
+ accessSync,
9
+ constants as fsConstants,
10
+ existsSync,
11
+ openAsBlob,
12
+ statSync
13
+ } from "fs";
14
+ import { basename, extname } from "path";
15
+ import { homedir } from "os";
16
+ import { resolve } from "path";
17
+ import {
18
+ buildNormalizedGraphContext,
19
+ extractKnowledgeBaseIds,
20
+ extractKnowledgeChunkContent,
21
+ extractKnowledgeChunkSourceLabel,
22
+ extractKnowledgeChunkTags,
23
+ partitionKnowledgeChunks
24
+ } from "@aliyunrds/ctxdb-shared";
25
+ var KB_COLLECTION = "/v1/knowledge/knowledge_bases";
26
+ var KB_DETAIL = "/v1/knowledge/knowledge_bases/detail";
27
+ var DOCUMENTS = "/v1/knowledge/documents";
28
+ var FILES = "/v1/knowledge/files";
29
+ var DOCUMENT_DETAIL = "/v1/knowledge/documents/detail";
30
+ var CHUNK_INIT = "/v1/knowledge/files/init-upload";
31
+ var CHUNK_PART = "/v1/knowledge/files/chunks";
32
+ var CHUNK_COMPLETE = "/v1/knowledge/files/complete";
33
+ var CHUNK_ABORT = "/v1/knowledge/files/abort";
34
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
35
+ var DEFAULT_INGEST_TIMEOUT_MS = 3e4;
36
+ var DEFAULT_FILE_INGEST_TIMEOUT_MS = 6e4;
37
+ var DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 2 * 60 * 60 * 1e3;
38
+ var DIRECT_UPLOAD_REQUIRES_CHUNK_ERROR_CODE = 6001;
39
+ var MIME_BY_EXT = {
40
+ ".pdf": "application/pdf",
41
+ ".txt": "text/plain",
42
+ ".md": "text/markdown",
43
+ ".markdown": "text/markdown",
44
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
45
+ ".doc": "application/msword",
46
+ ".ppt": "application/vnd.ms-powerpoint",
47
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
48
+ ".xls": "application/vnd.ms-excel",
49
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
50
+ ".epub": "application/epub+zip",
51
+ ".html": "text/html",
52
+ ".htm": "text/html",
53
+ ".json": "application/json",
54
+ ".csv": "text/csv",
55
+ ".xml": "application/xml",
56
+ ".rtf": "application/rtf",
57
+ ".png": "image/png",
58
+ ".jpg": "image/jpeg",
59
+ ".jpeg": "image/jpeg",
60
+ ".gif": "image/gif",
61
+ ".bmp": "image/bmp",
62
+ ".webp": "image/webp",
63
+ ".tiff": "image/tiff",
64
+ ".tif": "image/tiff",
65
+ ".svg": "image/svg+xml",
66
+ ".mp3": "audio/mpeg",
67
+ ".wav": "audio/wav",
68
+ ".flac": "audio/flac",
69
+ ".ogg": "audio/ogg",
70
+ ".m4a": "audio/mp4",
71
+ ".aac": "audio/aac",
72
+ ".wma": "audio/x-ms-wma",
73
+ ".mp4": "video/mp4",
74
+ ".avi": "video/x-msvideo",
75
+ ".mov": "video/quicktime",
76
+ ".mkv": "video/x-matroska",
77
+ ".webm": "video/webm",
78
+ ".flv": "video/x-flv",
79
+ ".wmv": "video/x-ms-wmv",
80
+ ".3gp": "video/3gpp",
81
+ ".mpeg": "video/mpeg",
82
+ ".mpg": "video/mpeg"
83
+ };
84
+ async function listKnowledgeBases(client) {
85
+ const resp = await client.get(KB_COLLECTION);
86
+ if (Array.isArray(resp)) return resp;
87
+ if (resp && typeof resp === "object") {
88
+ const o = resp;
89
+ return o.knowledge_bases ?? o.results ?? [];
90
+ }
91
+ return [];
92
+ }
93
+ async function createKb(client, kbName, description = "") {
94
+ return client.postJson(KB_COLLECTION, { name: kbName, description: description || "" });
95
+ }
96
+ async function uploadText(client, kbName, docName, text, mimeType = "text/plain", filePath) {
97
+ const body = {
98
+ knowledge_base_name: kbName,
99
+ name: docName,
100
+ text,
101
+ mime_type: mimeType
102
+ };
103
+ if (filePath !== void 0 && filePath !== "") body.file_path = filePath;
104
+ return client.postJson(DOCUMENTS, body);
105
+ }
106
+ 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
+ }
118
+ const filename = basename(expanded);
119
+ const docName = options.docName ?? filename;
120
+ const mime = guessMime(expanded);
121
+ 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;
130
+ try {
131
+ return await client.postMultipart(
132
+ FILES,
133
+ fields,
134
+ { file: { filename, content, mimeType: mime } },
135
+ { timeoutMs }
136
+ );
137
+ } catch (error) {
138
+ if (!shouldFallbackToChunk(error)) throw error;
139
+ }
140
+ return uploadFileInChunks(client, {
141
+ kbName,
142
+ filename,
143
+ docName,
144
+ filePath: options.filePath,
145
+ mime,
146
+ content,
147
+ fileSize: stat.size,
148
+ timeoutMs
149
+ });
150
+ }
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");
156
+ const initParams = {
157
+ knowledge_base_id: knowledgeBaseId,
158
+ file_name: options.filename,
159
+ name: options.docName,
160
+ mime_type: options.mime
161
+ };
162
+ if (options.filePath !== void 0 && options.filePath !== "") {
163
+ initParams.file_path = options.filePath;
164
+ }
165
+ const initResponse = await client.postJson(
166
+ CHUNK_INIT,
167
+ {},
168
+ initParams,
169
+ { timeoutMs: options.timeoutMs }
170
+ );
171
+ const init = unwrapBox(initResponse, "chunk init");
172
+ const uploadTicket = readRequiredString(
173
+ init,
174
+ "upload_ticket",
175
+ "chunk init"
176
+ );
177
+ const documentId = readRequiredString(init, "document_id", "chunk init");
178
+ const chunkSize = readRequiredPositiveInteger(
179
+ init,
180
+ "chunk_size",
181
+ "chunk init"
182
+ );
183
+ let completeStarted = false;
184
+ try {
185
+ if (options.fileSize <= 0) {
186
+ throw new CtxdbError("chunk upload requires a non-empty file");
187
+ }
188
+ const parts = [];
189
+ let partNumber = 1;
190
+ for (let offset = 0; offset < options.fileSize; offset += chunkSize) {
191
+ const end = Math.min(offset + chunkSize, options.fileSize);
192
+ const partBlob = options.content.slice(offset, end, options.mime);
193
+ const partResponse = await client.postMultipart(
194
+ CHUNK_PART,
195
+ {},
196
+ {
197
+ data: {
198
+ filename: options.filename,
199
+ content: partBlob,
200
+ mimeType: options.mime
201
+ }
202
+ },
203
+ {
204
+ timeoutMs: options.timeoutMs,
205
+ headers: { "X-Upload-Ticket": uploadTicket },
206
+ params: { part_number: partNumber }
207
+ }
208
+ );
209
+ const part = unwrapBox(partResponse, `chunk part ${partNumber}`);
210
+ const returnedPartNumber = readRequiredPositiveInteger(
211
+ part,
212
+ "part_number",
213
+ `chunk part ${partNumber}`
214
+ );
215
+ if (returnedPartNumber !== partNumber) {
216
+ throw new CtxdbError(
217
+ `chunk part ${partNumber} returned mismatched part_number ${returnedPartNumber}`
218
+ );
219
+ }
220
+ parts.push({
221
+ part_number: returnedPartNumber,
222
+ etag: readRequiredString(
223
+ part,
224
+ "etag",
225
+ `chunk part ${partNumber}`
226
+ )
227
+ });
228
+ partNumber += 1;
229
+ }
230
+ completeStarted = true;
231
+ const completeResponse = await client.postJson(
232
+ CHUNK_COMPLETE,
233
+ { parts },
234
+ void 0,
235
+ {
236
+ timeoutMs: options.timeoutMs,
237
+ headers: { "X-Upload-Ticket": uploadTicket }
238
+ }
239
+ );
240
+ const completed = unwrapBox(completeResponse, "chunk complete");
241
+ const completedDocumentId = readRequiredString(
242
+ completed,
243
+ "document_id",
244
+ "chunk complete"
245
+ );
246
+ if (completedDocumentId !== documentId) {
247
+ throw new CtxdbError(
248
+ `chunk complete returned mismatched document_id ${completedDocumentId}`
249
+ );
250
+ }
251
+ return getDocument(client, options.kbName, documentId);
252
+ } catch (error) {
253
+ if (!completeStarted) {
254
+ try {
255
+ await client.postJson(
256
+ CHUNK_ABORT,
257
+ {},
258
+ void 0,
259
+ {
260
+ timeoutMs: options.timeoutMs,
261
+ headers: { "X-Upload-Ticket": uploadTicket }
262
+ }
263
+ );
264
+ } catch {
265
+ }
266
+ }
267
+ throw error;
268
+ }
269
+ }
270
+ function shouldFallbackToChunk(error) {
271
+ if (!error || typeof error !== "object") return false;
272
+ const candidate = error;
273
+ if (candidate.errorCode === DIRECT_UPLOAD_REQUIRES_CHUNK_ERROR_CODE) {
274
+ return true;
275
+ }
276
+ if (candidate.status !== 413 || candidate.errorCode !== void 0) {
277
+ return false;
278
+ }
279
+ if (typeof candidate.errorMessage === "string" && candidate.errorMessage.trim() !== "") {
280
+ return false;
281
+ }
282
+ const body = candidate.responseBody;
283
+ if (body && typeof body === "object" && !Array.isArray(body)) {
284
+ const fields = body;
285
+ if (["errorCode", "errorMessage", "detail", "message", "error"].some((field) => Object.hasOwn(fields, field))) {
286
+ return false;
287
+ }
288
+ }
289
+ return true;
290
+ }
291
+ function unwrapBox(response, operation) {
292
+ if (!response || typeof response !== "object" || Array.isArray(response)) {
293
+ throw new CtxdbError(`${operation} returned an invalid response`);
294
+ }
295
+ const box = response;
296
+ if (typeof box.errorCode === "number" && box.errorCode !== 0) {
297
+ const message = typeof box.errorMessage === "string" && box.errorMessage ? box.errorMessage : `errorCode=${box.errorCode}`;
298
+ throw new CtxdbError(`${operation} failed: ${message}`);
299
+ }
300
+ if (!box.data || typeof box.data !== "object" || Array.isArray(box.data)) {
301
+ throw new CtxdbError(`${operation} returned no data`);
302
+ }
303
+ return box.data;
304
+ }
305
+ function readRequiredString(value, field, source) {
306
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
307
+ throw new CtxdbError(`${source} returned an invalid response`);
308
+ }
309
+ const result = value[field];
310
+ if (typeof result !== "string" || result.trim() === "") {
311
+ throw new CtxdbError(`${source} returned invalid ${field}`);
312
+ }
313
+ return result;
314
+ }
315
+ function readRequiredPositiveInteger(value, field, source) {
316
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
317
+ throw new CtxdbError(`${source} returned an invalid response`);
318
+ }
319
+ const result = value[field];
320
+ if (typeof result !== "number" || !Number.isSafeInteger(result) || result <= 0) {
321
+ throw new CtxdbError(`${source} returned invalid ${field}`);
322
+ }
323
+ return result;
324
+ }
325
+ function guessMime(path) {
326
+ const ext = extname(path).toLowerCase();
327
+ if (ext in MIME_BY_EXT) return MIME_BY_EXT[ext];
328
+ return "application/octet-stream";
329
+ }
330
+ async function getDocument(client, kbName, docId) {
331
+ return client.get(DOCUMENT_DETAIL, {
332
+ knowledge_base_name: kbName,
333
+ document_id: docId
334
+ });
335
+ }
336
+ async function listDocuments(client, kbName) {
337
+ const resp = await client.get(DOCUMENTS, { knowledge_base_name: kbName });
338
+ if (Array.isArray(resp)) return resp;
339
+ if (resp && typeof resp === "object") {
340
+ const o = resp;
341
+ return o.documents ?? o.results ?? [];
342
+ }
343
+ return [];
344
+ }
345
+ async function pollIngest(client, kbName, docId, options = {}) {
346
+ const timeoutMs = options.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
347
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
348
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
349
+ const now = options.now ?? (() => performance.now());
350
+ const deadline = now() + timeoutMs;
351
+ let doc = await getDocument(client, kbName, docId);
352
+ let timedOut = false;
353
+ while (ingestInFlight(doc)) {
354
+ if (now() >= deadline) {
355
+ timedOut = true;
356
+ break;
357
+ }
358
+ await sleep(intervalMs);
359
+ try {
360
+ doc = await getDocument(client, kbName, docId);
361
+ } catch {
362
+ break;
363
+ }
364
+ }
365
+ if (doc && typeof doc === "object") {
366
+ return { ...doc, _pollingTimedOut: timedOut };
367
+ }
368
+ return { _pollingTimedOut: timedOut };
369
+ }
370
+ function ingestInFlight(doc) {
371
+ if (!doc || typeof doc !== "object") return false;
372
+ const status = doc.ingest_status;
373
+ return status === "processing" || status === "pending" || status === "in_progress";
374
+ }
375
+ function expandHome(p) {
376
+ if (p.startsWith("~/") || p === "~") {
377
+ return resolve(homedir(), p.slice(2));
378
+ }
379
+ return resolve(p);
380
+ }
381
+ function compactChunk(raw) {
382
+ const c = raw ?? {};
383
+ const content = extractKnowledgeChunkContent(raw);
384
+ const doc_name = extractKnowledgeChunkSourceLabel(raw);
385
+ const kb_id = extractKnowledgeBaseIds(raw)[0] ?? "";
386
+ const docIdRaw = c.doc_id;
387
+ const scoreRaw = c.similarity ?? c.rerank_score;
388
+ const tags = extractKnowledgeChunkTags(raw);
389
+ const out = {
390
+ content,
391
+ doc_name,
392
+ kb_id,
393
+ score: typeof scoreRaw === "number" ? scoreRaw : 0
394
+ };
395
+ if (typeof docIdRaw === "string" && docIdRaw.length > 0) {
396
+ out.doc_id = docIdRaw;
397
+ }
398
+ if (tags.length > 0) {
399
+ out.tags = tags;
400
+ }
401
+ return out;
402
+ }
403
+ function projectKbQueryResponse(raw, projectChunk, options = {}) {
404
+ if (!raw || typeof raw !== "object") return { chunks: [] };
405
+ const r = raw;
406
+ const chunksIn = Array.isArray(r.chunks) ? r.chunks : [];
407
+ const { graphChunks, documentChunks } = partitionKnowledgeChunks(chunksIn);
408
+ const graphContext = buildNormalizedGraphContext(graphChunks, {
409
+ verbose: options.verboseGraph
410
+ });
411
+ const out = {
412
+ chunks: documentChunks.map(projectChunk)
413
+ };
414
+ if (typeof r.total === "number") out.total = r.total;
415
+ if (graphContext) out.graph_context = graphContext;
416
+ return out;
417
+ }
418
+ function compactKbQueryResponse(raw) {
419
+ return projectKbQueryResponse(raw, compactChunk, { verboseGraph: true });
420
+ }
421
+ function minimalChunk(raw) {
422
+ const c = compactChunk(raw);
423
+ return { content: c.content, score: c.score };
424
+ }
425
+ function minimalKbQueryResponse(raw) {
426
+ return projectKbQueryResponse(raw, minimalChunk);
427
+ }
428
+
429
+ export {
430
+ DEFAULT_INGEST_TIMEOUT_MS,
431
+ DEFAULT_FILE_INGEST_TIMEOUT_MS,
432
+ DEFAULT_FILE_UPLOAD_TIMEOUT_MS,
433
+ listKnowledgeBases,
434
+ createKb,
435
+ uploadText,
436
+ uploadFile,
437
+ getDocument,
438
+ listDocuments,
439
+ pollIngest,
440
+ compactKbQueryResponse,
441
+ minimalKbQueryResponse
442
+ };