@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 +174 -5
- package/dist/chunk-3PFMHU3C.js +442 -0
- package/dist/chunk-BQA7YSXT.js +462 -0
- package/dist/{chunk-2CSEFSLC.js → chunk-BVCDRUU4.js} +1 -1
- package/dist/{chunk-WTP4ZX22.js → chunk-CWGF5D52.js} +2 -2
- package/dist/{chunk-6FZL67GH.js → chunk-JFTKYEVN.js} +333 -45
- package/dist/{chunk-EUQ3OFCQ.js → chunk-Q4JYST7K.js} +3 -3
- package/dist/{chunk-IMYLU5C2.js → chunk-SAQT6VL6.js} +2 -2
- package/dist/{chunk-TGVURF54.js → chunk-USMJLDBD.js} +1 -1
- package/dist/cli/main.js +363 -166
- 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 +13 -3
- package/dist/workers/version-check.js +65 -0
- package/package.json +1 -1
- package/dist/chunk-UH7AJF6F.js +0 -204
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
|
|
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
|
+
};
|