@aliyunrds/ctxdb 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 --agent <…>` installer, 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 hooks/skills, 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
  |---|---|---|---|
@@ -51,15 +51,15 @@ Implementation note: qoder/Claude consume the JSON `hookSpecificOutput.additiona
51
51
 
52
52
  ## CLI
53
53
 
54
- `ctxdb` ships 19 subcommands:
54
+ `ctxdb` ships 18 subcommands:
55
55
 
56
- - **Top-level**: `init` / `status` / `ping` / `setup` / `teardown`
56
+ - **Top-level**: `setup` / `status` / `ping` / `uninstall` / `upgrade`
57
57
  - **Memory**: `memory add|search|list|get|update|delete`
58
58
  - **KB**: `kb upload-text|upload-file|list|documents-list|document-get|search`
59
59
 
60
60
  See `ctxdb --help`.
61
61
 
62
- `setup` mandates `--agent <qoder|codex|claude>` because it mutates that agent's hooks/skills. Memory and KB commands also accept `--agent <name>` so agents use their own config section; when omitted, `CTXDB_AGENT` wins, otherwise `qoder` is used for backward compatibility. `teardown` loops every supported agent.
62
+ `setup` without `--agent` writes `agents.default` in `~/.ctxdb/ctxdb.json` (CLI-only, no hooks or skills); with `--agent <qoder|codex|claude>` it writes agent config and installs hooks + 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`.
63
63
 
64
64
  `memory add … --no-infer` stores the text verbatim (skips server-side LLM
65
65
  fact-extraction). Use it when the user explicitly asks for a verbatim
@@ -100,7 +100,7 @@ Lives at `~/.ctxdb/ctxdb.json` (co-located with logs at `~/.ctxdb/logs/`). Schem
100
100
  }
101
101
  ```
102
102
 
103
- Agent selection is driven by `--agent <qoder|codex|claude>` on every CLI invocation, falling back to `CTXDB_AGENT` env, then to `qoder`. There is no `default_agent` field — older installs that have one written get it dropped on the next save.
103
+ Agent selection is driven by `--agent <qoder|codex|claude|default>` on every CLI invocation, falling back to `CTXDB_AGENT` env, then to `default`. There is no `default_agent` field — older installs that have one written get it dropped on the next save.
104
104
 
105
105
  Field reference:
106
106
 
@@ -132,19 +132,19 @@ Two npm packages total. `@aliyunrds/ctxdb-shared` stays separate because OpenCla
132
132
 
133
133
  ## Uninstall
134
134
 
135
- > ⚠️ **Order matters.** npm 7+ removed the `preuninstall` / `postuninstall` lifecycle hooks — `npm uninstall -g` does **not** run any cleanup we ship. If you `npm uninstall` first you'll leave agent hook entries pointing at a now-missing binary, plus orphaned skills and `~/.ctxdb/` state. Always run **`ctxdb teardown` first, then `npm uninstall -g`**.
135
+ > ⚠️ **Order matters.** npm 7+ removed the `preuninstall` / `postuninstall` lifecycle hooks — `npm uninstall -g` does **not** run any cleanup we ship. If you `npm uninstall` first you'll leave agent hook entries pointing at a now-missing binary, plus orphaned skills and `~/.ctxdb/` state. Always run **`ctxdb uninstall` first, then `npm uninstall -g`**.
136
136
 
137
137
  ### Full removal (recommended)
138
138
 
139
139
  ```sh
140
140
  # 1. Strip hooks + skills + (optionally) config + logs while the binary still works.
141
- ctxdb teardown --purge-all # everything: hooks, skills, ~/.ctxdb/ctxdb.json, ~/.ctxdb/logs/
141
+ ctxdb uninstall --purge-all # everything: hooks, skills, ~/.ctxdb/ctxdb.json, ~/.ctxdb/logs/
142
142
 
143
143
  # 2. Then drop the npm binaries.
144
144
  npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared
145
145
  ```
146
146
 
147
- After step 1, `ctxdb teardown` prints the exact `npm uninstall` command to run next — copy it from your terminal.
147
+ After step 1, `ctxdb uninstall` prints the exact `npm uninstall` command to run next — copy it from your terminal.
148
148
 
149
149
  ### Partial removal
150
150
 
@@ -154,23 +154,23 @@ After step 1, `ctxdb teardown` prints the exact `npm uninstall` command to run n
154
154
  # preserved verbatim. If <X> was the last configured agent, ~/.ctxdb/ctxdb.json
155
155
  # itself is unlinked so a future `ctxdb setup --agent <X>` runs through the
156
156
  # first-time install path cleanly.
157
- ctxdb setup --remove --agent qoder
158
- ctxdb setup --remove --agent codex
159
- ctxdb setup --remove --agent claude
157
+ ctxdb uninstall --agent qoder
158
+ ctxdb uninstall --agent codex
159
+ ctxdb uninstall --agent claude
160
160
 
161
161
  # Wholesale across all agents, but keep config + logs for re-setup later:
162
- ctxdb teardown # all agents, keeps ~/.ctxdb/{ctxdb.json,logs/}
163
- ctxdb teardown --purge-config # also deletes ~/.ctxdb/ctxdb.json
164
- ctxdb teardown --purge-logs # also deletes ~/.ctxdb/logs/
162
+ ctxdb uninstall # all agents, keeps ~/.ctxdb/{ctxdb.json,logs/}
163
+ ctxdb uninstall --purge-config # also deletes ~/.ctxdb/ctxdb.json
164
+ ctxdb uninstall --purge-logs # also deletes ~/.ctxdb/logs/
165
165
  ```
166
166
 
167
- ### What teardown does (and doesn't) touch
167
+ ### What uninstall does (and doesn't) touch
168
168
 
169
169
  - **Hook entries in `~/.qoder/settings.json`, `~/.codex/hooks.json`, and `~/.claude/settings.json`**: stripped precisely by marker (`_ctxdb = @aliyunrds/ctxdb`, plus legacy keys `_ctxdbQoder` / `_ctxdbPackage` and the legacy `@aliyunrds/ctxdb-qoder` value for installs predating the unified marker). Any hooks you added yourself stay. A timestamped `*.bak-ctxdb-<TS>` is written before each modification (rotation keeps the 5 most recent).
170
170
  - **Skill directories**: `~/.<agent>/skills/ctxdb/` for each set-up agent, plus legacy dirs (`ctxdb-qoder`, `rds-ctxdb-qoder`, `qoder-ctxdb`) under `~/.qoder/skills/` from older package names.
171
171
  - **`~/.ctxdb/` state**: only with `--purge-config` / `--purge-logs` / `--purge-all`. Defensive: `--purge-all` removes the `~/.ctxdb/` root only if it's empty after the named files are deleted (won't blanket-rm an unknown directory).
172
- - **`~/.codex/config.toml` `[features].hooks`**: **NOT** reverted. Setup adds `hooks = true` so Codex will fire ctxdb's hook entries; teardown leaves the flag alone because (a) the user may have wanted it on for non-ctxdb hooks, and (b) it's harmless when `~/.codex/hooks.json` is empty. If you want it off, edit the file by hand.
173
- - **`npm` itself**: never. `ctxdb teardown` does not call your host package manager — that's the second command above.
172
+ - **`~/.codex/config.toml` `[features].hooks`**: **NOT** reverted. Setup adds `hooks = true` so Codex will fire ctxdb's hook entries; uninstall leaves the flag alone because (a) the user may have wanted it on for non-ctxdb hooks, and (b) it's harmless when `~/.codex/hooks.json` is empty. If you want it off, edit the file by hand.
173
+ - **`npm` itself**: never. `ctxdb uninstall` does not call your host package manager — that's the second command above.
174
174
 
175
175
  ## Remote SSH (Qoder IDE)
176
176
 
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/kb.ts
4
+ import { readFileSync, existsSync, statSync } from "fs";
5
+ import { basename, extname } from "path";
6
+ import { homedir } from "os";
7
+ import { resolve } from "path";
8
+ var KB_COLLECTION = "/v1/knowledge/knowledge_bases";
9
+ var DOCUMENTS = "/v1/knowledge/documents";
10
+ var FILES = "/v1/knowledge/files";
11
+ var DOCUMENT_DETAIL = "/v1/knowledge/documents/detail";
12
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
13
+ var DEFAULT_INGEST_TIMEOUT_MS = 3e4;
14
+ var DEFAULT_FILE_INGEST_TIMEOUT_MS = 6e4;
15
+ var MIME_BY_EXT = {
16
+ ".pdf": "application/pdf",
17
+ ".txt": "text/plain",
18
+ ".md": "text/markdown",
19
+ ".markdown": "text/markdown",
20
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
21
+ ".doc": "application/msword",
22
+ ".html": "text/html",
23
+ ".htm": "text/html",
24
+ ".json": "application/json",
25
+ ".csv": "text/csv",
26
+ ".xml": "application/xml",
27
+ ".rtf": "application/rtf"
28
+ };
29
+ async function listKnowledgeBases(client) {
30
+ const resp = await client.get(KB_COLLECTION);
31
+ if (Array.isArray(resp)) return resp;
32
+ if (resp && typeof resp === "object") {
33
+ const o = resp;
34
+ return o.knowledge_bases ?? o.results ?? [];
35
+ }
36
+ return [];
37
+ }
38
+ async function createKb(client, kbName, description = "") {
39
+ return client.postJson(KB_COLLECTION, { name: kbName, description: description || "" });
40
+ }
41
+ async function uploadText(client, kbName, docName, text, mimeType = "text/plain", filePath) {
42
+ const body = {
43
+ knowledge_base_name: kbName,
44
+ name: docName,
45
+ text,
46
+ mime_type: mimeType
47
+ };
48
+ if (filePath !== void 0 && filePath !== "") body.file_path = filePath;
49
+ return client.postJson(DOCUMENTS, body);
50
+ }
51
+ async function uploadFile(client, kbName, localPath, options = {}) {
52
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
53
+ const expanded = expandHome(localPath);
54
+ if (!existsSync(expanded)) throw new Error(`file not found: ${expanded}`);
55
+ const stat = statSync(expanded);
56
+ if (!stat.isFile()) throw new Error(`not a file: ${expanded}`);
57
+ if (stat.size > MAX_UPLOAD_BYTES) {
58
+ throw new Error(
59
+ `file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB), maximum is 100 MB`
60
+ );
61
+ }
62
+ const filename = basename(expanded);
63
+ const docName = options.docName ?? filename;
64
+ const content = readFileSync(expanded);
65
+ const mime = guessMime(expanded);
66
+ const fields = {
67
+ knowledge_base_name: kbName,
68
+ name: docName
69
+ };
70
+ if (options.filePath !== void 0 && options.filePath !== "") {
71
+ fields.file_path = options.filePath;
72
+ }
73
+ return client.postMultipart(
74
+ FILES,
75
+ fields,
76
+ { file: { filename, content, mimeType: mime } },
77
+ { timeoutMs: options.timeoutMs }
78
+ );
79
+ }
80
+ function guessMime(path) {
81
+ const ext = extname(path).toLowerCase();
82
+ if (ext in MIME_BY_EXT) return MIME_BY_EXT[ext];
83
+ return "application/octet-stream";
84
+ }
85
+ async function getDocument(client, kbName, docId) {
86
+ return client.get(DOCUMENT_DETAIL, {
87
+ knowledge_base_name: kbName,
88
+ document_id: docId
89
+ });
90
+ }
91
+ async function listDocuments(client, kbName) {
92
+ const resp = await client.get(DOCUMENTS, { knowledge_base_name: kbName });
93
+ if (Array.isArray(resp)) return resp;
94
+ if (resp && typeof resp === "object") {
95
+ const o = resp;
96
+ return o.documents ?? o.results ?? [];
97
+ }
98
+ return [];
99
+ }
100
+ async function pollIngest(client, kbName, docId, options = {}) {
101
+ const timeoutMs = options.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
102
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
103
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
104
+ const now = options.now ?? (() => performance.now());
105
+ const deadline = now() + timeoutMs;
106
+ let doc = await getDocument(client, kbName, docId);
107
+ let timedOut = false;
108
+ while (ingestInFlight(doc)) {
109
+ if (now() >= deadline) {
110
+ timedOut = true;
111
+ break;
112
+ }
113
+ await sleep(intervalMs);
114
+ try {
115
+ doc = await getDocument(client, kbName, docId);
116
+ } catch {
117
+ break;
118
+ }
119
+ }
120
+ if (doc && typeof doc === "object") {
121
+ return { ...doc, _pollingTimedOut: timedOut };
122
+ }
123
+ return { _pollingTimedOut: timedOut };
124
+ }
125
+ function ingestInFlight(doc) {
126
+ if (!doc || typeof doc !== "object") return false;
127
+ const status = doc.ingest_status;
128
+ return status === "processing" || status === "pending" || status === "in_progress";
129
+ }
130
+ function expandHome(p) {
131
+ if (p.startsWith("~/") || p === "~") {
132
+ return resolve(homedir(), p.slice(2));
133
+ }
134
+ return resolve(p);
135
+ }
136
+ function compactChunk(raw) {
137
+ const c = raw ?? {};
138
+ const content = c.content_with_weight ?? c.content ?? c.chunk ?? c.text ?? "";
139
+ const doc_name = c.docnm_kwd ?? c.doc_name ?? c.docnm ?? "";
140
+ const kbRaw = c.kb_id ?? c.dataset_id;
141
+ const kb_id = Array.isArray(kbRaw) ? typeof kbRaw[0] === "string" ? kbRaw[0] : "" : typeof kbRaw === "string" ? kbRaw : "";
142
+ const docIdRaw = c.doc_id;
143
+ const scoreRaw = c.similarity ?? c.rerank_score;
144
+ const tagsRaw = c.tag_kwd;
145
+ const out = {
146
+ content,
147
+ doc_name,
148
+ kb_id,
149
+ score: typeof scoreRaw === "number" ? scoreRaw : 0
150
+ };
151
+ if (typeof docIdRaw === "string" && docIdRaw.length > 0) {
152
+ out.doc_id = docIdRaw;
153
+ }
154
+ if (Array.isArray(tagsRaw) && tagsRaw.length > 0) {
155
+ out.tags = tagsRaw;
156
+ }
157
+ return out;
158
+ }
159
+ function compactKbQueryResponse(raw) {
160
+ if (!raw || typeof raw !== "object") return { chunks: [] };
161
+ const r = raw;
162
+ const chunksIn = Array.isArray(r.chunks) ? r.chunks : [];
163
+ const out = {
164
+ chunks: chunksIn.map(compactChunk)
165
+ };
166
+ if (typeof r.total === "number") out.total = r.total;
167
+ return out;
168
+ }
169
+ function minimalChunk(raw) {
170
+ const c = compactChunk(raw);
171
+ return { content: c.content, score: c.score };
172
+ }
173
+ function minimalKbQueryResponse(raw) {
174
+ if (!raw || typeof raw !== "object") return { chunks: [] };
175
+ const r = raw;
176
+ const chunksIn = Array.isArray(r.chunks) ? r.chunks : [];
177
+ const out = {
178
+ chunks: chunksIn.map(minimalChunk)
179
+ };
180
+ if (typeof r.total === "number") out.total = r.total;
181
+ return out;
182
+ }
183
+
184
+ export {
185
+ DEFAULT_INGEST_TIMEOUT_MS,
186
+ DEFAULT_FILE_INGEST_TIMEOUT_MS,
187
+ listKnowledgeBases,
188
+ createKb,
189
+ uploadText,
190
+ uploadFile,
191
+ getDocument,
192
+ listDocuments,
193
+ pollIngest,
194
+ compactChunk,
195
+ compactKbQueryResponse,
196
+ minimalChunk,
197
+ minimalKbQueryResponse
198
+ };
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ isConnectionError,
4
+ resetCircuit,
5
+ tripCircuit
6
+ } from "./chunk-Q2EEP4CE.js";
2
7
  import {
3
8
  CtxdbError
4
- } from "./chunk-QSSNPN3M.js";
9
+ } from "./chunk-S45GOYUU.js";
5
10
 
6
11
  // src/lib/recall-orchestrator.ts
7
12
  import {
@@ -83,7 +88,7 @@ var EMPTY = {
83
88
  memoryCount: 0,
84
89
  knowledgeChunkCount: 0
85
90
  };
86
- async function recallTurn(prompt, cfg, client) {
91
+ async function recallTurn(prompt, cfg, client, agent = "default") {
87
92
  if (!prompt.trim()) return { ...EMPTY, reason: "empty_prompt" };
88
93
  if (!cfg.apiKey || !cfg.baseUrl) {
89
94
  return { ...EMPTY, reason: "config_incomplete" };
@@ -101,8 +106,10 @@ async function recallTurn(prompt, cfg, client) {
101
106
  let resp;
102
107
  try {
103
108
  resp = await client.postJson("/v3/memories/search/", body);
109
+ resetCircuit(agent);
104
110
  } catch (err) {
105
111
  const msg = err instanceof CtxdbError ? err.message : String(err);
112
+ if (isConnectionError(err)) tripCircuit(agent, cfg.baseUrl, msg);
106
113
  return { ...EMPTY, reason: `http_error: ${msg}` };
107
114
  }
108
115
  if (!resp || typeof resp !== "object") {
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ configDir
4
+ } from "./chunk-S45GOYUU.js";
5
+
6
+ // src/lib/circuit.ts
7
+ import { statSync, writeFileSync, unlinkSync, mkdirSync, readdirSync, readFileSync } from "fs";
8
+ import { join } from "path";
9
+ var CIRCUIT_DIR_NAME = "circuit";
10
+ var COOLDOWN_MS = 6e4;
11
+ function circuitDir() {
12
+ const dir = join(configDir(), CIRCUIT_DIR_NAME);
13
+ try {
14
+ mkdirSync(dir, { recursive: true });
15
+ } catch {
16
+ }
17
+ return dir;
18
+ }
19
+ function circuitPath(agent) {
20
+ return join(circuitDir(), `${agent}.circuit`);
21
+ }
22
+ function isCircuitOpen(agent, baseUrl) {
23
+ const own = checkFile(circuitPath(agent), baseUrl);
24
+ if (own) return true;
25
+ try {
26
+ for (const f of readdirSync(circuitDir())) {
27
+ if (!f.endsWith(".circuit") || f === `${agent}.circuit`) continue;
28
+ if (checkFile(join(circuitDir(), f), baseUrl)) return true;
29
+ }
30
+ } catch {
31
+ }
32
+ return false;
33
+ }
34
+ function checkFile(path, baseUrl) {
35
+ try {
36
+ const st = statSync(path);
37
+ if (Date.now() - st.mtimeMs > COOLDOWN_MS) return false;
38
+ const line = readFileSync(path, "utf-8").split("\n")[0] || "";
39
+ return line.startsWith(baseUrl);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ function tripCircuit(agent, baseUrl, reason) {
45
+ try {
46
+ writeFileSync(circuitPath(agent), `${baseUrl}|${reason}|${(/* @__PURE__ */ new Date()).toISOString()}
47
+ `);
48
+ } catch {
49
+ }
50
+ }
51
+ function resetCircuit(agent) {
52
+ try {
53
+ unlinkSync(circuitPath(agent));
54
+ } catch {
55
+ }
56
+ }
57
+ function isConnectionError(err) {
58
+ if (!err || typeof err !== "object") return false;
59
+ if (err.name === "CtxdbHttpError") return false;
60
+ const msg = err.message ?? "";
61
+ return /timeout|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|network error/i.test(msg);
62
+ }
63
+
64
+ export {
65
+ isCircuitOpen,
66
+ tripCircuit,
67
+ resetCircuit,
68
+ isConnectionError
69
+ };
@@ -16,7 +16,14 @@ function isDebug() {
16
16
  }
17
17
  function debug(tag, msg, data) {
18
18
  if (_enabled !== true) return;
19
- const ts = (/* @__PURE__ */ new Date()).toISOString();
19
+ const now = /* @__PURE__ */ new Date();
20
+ const pad = (n) => String(n).padStart(2, "0");
21
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
22
+ const tz = -now.getTimezoneOffset();
23
+ const tzSign = tz >= 0 ? "+" : "-";
24
+ const tzH = pad(Math.floor(Math.abs(tz) / 60));
25
+ const tzM = pad(Math.abs(tz) % 60);
26
+ const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${ms}${tzSign}${tzH}:${tzM}`;
20
27
  let line = `${ts} [${tag}] ${msg}`;
21
28
  if (data !== void 0) {
22
29
  const s = typeof data === "string" ? data : JSON.stringify(data, null, 2);
@@ -251,9 +258,13 @@ function extractErrorDetail(text, fallback) {
251
258
  import { homedir as homedir2 } from "os";
252
259
  import { join as join3 } from "path";
253
260
  var SUPPORTED_AGENTS = ["qoder", "codex", "claude"];
254
- function isAgent(v) {
261
+ function isBuiltinAgent(v) {
255
262
  return typeof v === "string" && SUPPORTED_AGENTS.includes(v);
256
263
  }
264
+ function isAgentSlug(v) {
265
+ return v === "default" || isBuiltinAgent(v);
266
+ }
267
+ var isAgent = isAgentSlug;
257
268
  function agentHomeDir(agent) {
258
269
  switch (agent) {
259
270
  case "qoder":
@@ -265,17 +276,17 @@ function agentHomeDir(agent) {
265
276
  }
266
277
  }
267
278
  function agentFromEnv(env = process.env) {
268
- return isAgent(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "qoder";
279
+ return isAgentSlug(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "default";
269
280
  }
270
281
  function agentFromArgvWithFallback(argv = process.argv.slice(2), env = process.env) {
271
282
  for (let i = 0; i < argv.length; i++) {
272
283
  const tok = argv[i];
273
- if (tok === "--agent" && isAgent(argv[i + 1])) {
284
+ if (tok === "--agent" && isAgentSlug(argv[i + 1])) {
274
285
  return { agent: argv[i + 1], fellBack: false };
275
286
  }
276
287
  if (tok.startsWith("--agent=")) {
277
288
  const raw = tok.slice("--agent=".length);
278
- if (isAgent(raw)) return { agent: raw, fellBack: false };
289
+ if (isAgentSlug(raw)) return { agent: raw, fellBack: false };
279
290
  }
280
291
  }
281
292
  return { agent: agentFromEnv(env), fellBack: true };
@@ -289,6 +300,9 @@ function defaultConfigPath() {
289
300
  return join4(homedir3(), ".ctxdb", "ctxdb.json");
290
301
  }
291
302
  var DEFAULT_CONFIG_PATH = join4(homedir3(), ".ctxdb", "ctxdb.json");
303
+ function configDir() {
304
+ return join4(homedir3(), ".ctxdb");
305
+ }
292
306
  var DEFAULT_BASE_URL = "https://context-database.aliyuncs.com";
293
307
  var DEFAULT_USER_ID = "default";
294
308
  var DEFAULT_TOP_K = 5;
@@ -297,8 +311,8 @@ var DEFAULT_KNOWLEDGE_TOP_K = 6;
297
311
  function isComplete(cfg) {
298
312
  return Boolean(cfg.apiKey && cfg.baseUrl);
299
313
  }
300
- function resolveConfigAgent(options = {}) {
301
- if (isAgent(options.agent)) return options.agent;
314
+ function resolveConfigAgent(options = {}, preloadedRaw) {
315
+ if (isAgentSlug(options.agent)) return options.agent;
302
316
  return agentFromEnv(options.env);
303
317
  }
304
318
  function coerceInt(v, fallback) {
@@ -365,8 +379,8 @@ function applyEnv(cfg, env) {
365
379
  function load(options = {}) {
366
380
  const path = options.path ?? defaultConfigPath();
367
381
  const env = options.env ?? process.env;
368
- const agent = resolveConfigAgent({ agent: options.agent, env });
369
382
  const raw = readRaw(path);
383
+ const agent = resolveConfigAgent({ ...options, path, env }, raw);
370
384
  if (!isV2Schema(raw)) {
371
385
  try {
372
386
  process.stderr.write(
@@ -377,7 +391,18 @@ function load(options = {}) {
377
391
  }
378
392
  return applyEnv(configFromDisk({}), env);
379
393
  }
380
- return applyEnv(configFromDisk(agentRawFromFile(raw, agent)), env);
394
+ const agentRaw = agentRawFromFile(raw, agent);
395
+ if (agent !== "default") {
396
+ const defaultRaw = agentRawFromFile(raw, "default");
397
+ if (Object.keys(defaultRaw).length > 0) {
398
+ for (const [k, v] of Object.entries(defaultRaw)) {
399
+ if (!(k in agentRaw)) {
400
+ agentRaw[k] = v;
401
+ }
402
+ }
403
+ }
404
+ }
405
+ return applyEnv(configFromDisk(agentRaw), env);
381
406
  }
382
407
  function configToDisk(cfg) {
383
408
  return {
@@ -446,23 +471,32 @@ function configuredAgents(path) {
446
471
  if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object") return [];
447
472
  return Object.keys(raw.agents).filter(isAgent);
448
473
  }
474
+ function writeInstalledPkgVersion(version, path) {
475
+ const target = path ?? defaultConfigPath();
476
+ const raw = readRaw(target);
477
+ const updated = { ...raw, installed_pkg_version: version };
478
+ mkdirSync2(dirname3(target), { recursive: true });
479
+ writeFileSync(target, JSON.stringify(updated, null, 2) + "\n", "utf-8");
480
+ }
449
481
 
450
482
  export {
451
483
  setDebug,
452
484
  debug,
453
485
  CtxdbError,
454
- NotFoundError,
455
486
  HttpClient,
456
487
  SUPPORTED_AGENTS,
457
- isAgent,
488
+ isBuiltinAgent,
489
+ isAgentSlug,
458
490
  agentHomeDir,
459
491
  agentFromEnv,
460
492
  agentFromArgvWithFallback,
493
+ configDir,
461
494
  DEFAULT_BASE_URL,
462
495
  DEFAULT_USER_ID,
463
496
  isComplete,
464
497
  load,
465
498
  removeAgent,
466
499
  save,
467
- configuredAgents
500
+ configuredAgents,
501
+ writeInstalledPkgVersion
468
502
  };