@aliyunrds/ctxdb 0.0.5 → 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
  |---|---|---|---|
@@ -59,7 +59,7 @@ Implementation note: qoder/Claude consume the JSON `hookSpecificOutput.additiona
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. `uninstall` without `--agent` loops every supported agent; with `--agent <name>` it targets just that one. `teardown` is an alias for `uninstall`.
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
 
@@ -1,7 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- NotFoundError
4
- } from "./chunk-QSSNPN3M.js";
5
2
 
6
3
  // src/lib/kb.ts
7
4
  import { readFileSync, existsSync, statSync } from "fs";
@@ -12,16 +9,6 @@ var KB_COLLECTION = "/v1/knowledge/knowledge_bases";
12
9
  var DOCUMENTS = "/v1/knowledge/documents";
13
10
  var FILES = "/v1/knowledge/files";
14
11
  var DOCUMENT_DETAIL = "/v1/knowledge/documents/detail";
15
- async function tryNewThenLegacy(newPath, newCall, legacyCall) {
16
- try {
17
- return await newCall();
18
- } catch (err) {
19
- if (err instanceof NotFoundError && err.path === newPath) {
20
- return legacyCall();
21
- }
22
- throw err;
23
- }
24
- }
25
12
  var DEFAULT_POLL_INTERVAL_MS = 1500;
26
13
  var DEFAULT_INGEST_TIMEOUT_MS = 3e4;
27
14
  var DEFAULT_FILE_INGEST_TIMEOUT_MS = 6e4;
@@ -48,40 +35,20 @@ async function listKnowledgeBases(client) {
48
35
  }
49
36
  return [];
50
37
  }
51
- async function findKb(client, kbNameOrId) {
52
- for (const kb of await listKnowledgeBases(client)) {
53
- if (!kb || typeof kb !== "object") continue;
54
- if (kb.name === kbNameOrId || kb.id === kbNameOrId) return kb;
55
- }
56
- return null;
57
- }
58
38
  async function createKb(client, kbName, description = "") {
59
39
  return client.postJson(KB_COLLECTION, { name: kbName, description: description || "" });
60
40
  }
61
- async function findOrCreateKb(client, kbName, description = "") {
62
- const existing = await findKb(client, kbName);
63
- if (existing) return { kb: existing, created: false };
64
- const created = await createKb(client, kbName, description);
65
- return { kb: created, created: true };
66
- }
67
- async function uploadText(client, kbId, docName, text, mimeType = "text/plain", filePath) {
41
+ async function uploadText(client, kbName, docName, text, mimeType = "text/plain", filePath) {
68
42
  const body = {
69
- knowledge_base_id: kbId,
43
+ knowledge_base_name: kbName,
70
44
  name: docName,
71
45
  text,
72
46
  mime_type: mimeType
73
47
  };
74
48
  if (filePath !== void 0 && filePath !== "") body.file_path = filePath;
75
- return tryNewThenLegacy(
76
- DOCUMENTS,
77
- () => client.postJson(DOCUMENTS, body),
78
- () => {
79
- const { knowledge_base_id: _drop, ...legacyBody } = body;
80
- return client.postJson(`${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents`, legacyBody);
81
- }
82
- );
49
+ return client.postJson(DOCUMENTS, body);
83
50
  }
84
- async function uploadFile(client, kbId, localPath, options = {}) {
51
+ async function uploadFile(client, kbName, localPath, options = {}) {
85
52
  const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
86
53
  const expanded = expandHome(localPath);
87
54
  if (!existsSync(expanded)) throw new Error(`file not found: ${expanded}`);
@@ -97,30 +64,17 @@ async function uploadFile(client, kbId, localPath, options = {}) {
97
64
  const content = readFileSync(expanded);
98
65
  const mime = guessMime(expanded);
99
66
  const fields = {
100
- knowledge_base_id: kbId,
67
+ knowledge_base_name: kbName,
101
68
  name: docName
102
69
  };
103
70
  if (options.filePath !== void 0 && options.filePath !== "") {
104
71
  fields.file_path = options.filePath;
105
72
  }
106
- const uploadOpts = { timeoutMs: options.timeoutMs };
107
- return tryNewThenLegacy(
73
+ return client.postMultipart(
108
74
  FILES,
109
- () => client.postMultipart(
110
- FILES,
111
- fields,
112
- { file: { filename, content, mimeType: mime } },
113
- uploadOpts
114
- ),
115
- () => {
116
- const { knowledge_base_id: _drop, ...legacyFields } = fields;
117
- return client.postMultipart(
118
- `${KB_COLLECTION}/${encodeURIComponent(kbId)}/files`,
119
- legacyFields,
120
- { file: { filename, content, mimeType: mime } },
121
- uploadOpts
122
- );
123
- }
75
+ fields,
76
+ { file: { filename, content, mimeType: mime } },
77
+ { timeoutMs: options.timeoutMs }
124
78
  );
125
79
  }
126
80
  function guessMime(path) {
@@ -128,24 +82,14 @@ function guessMime(path) {
128
82
  if (ext in MIME_BY_EXT) return MIME_BY_EXT[ext];
129
83
  return "application/octet-stream";
130
84
  }
131
- async function getDocument(client, kbId, docId) {
132
- return tryNewThenLegacy(
133
- DOCUMENT_DETAIL,
134
- () => client.get(DOCUMENT_DETAIL, {
135
- knowledge_base_id: kbId,
136
- document_id: docId
137
- }),
138
- () => client.get(
139
- `${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents/${encodeURIComponent(docId)}`
140
- )
141
- );
85
+ async function getDocument(client, kbName, docId) {
86
+ return client.get(DOCUMENT_DETAIL, {
87
+ knowledge_base_name: kbName,
88
+ document_id: docId
89
+ });
142
90
  }
143
- async function listDocuments(client, kbId) {
144
- const resp = await tryNewThenLegacy(
145
- DOCUMENTS,
146
- () => client.get(DOCUMENTS, { knowledge_base_id: kbId }),
147
- () => client.get(`${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents`)
148
- );
91
+ async function listDocuments(client, kbName) {
92
+ const resp = await client.get(DOCUMENTS, { knowledge_base_name: kbName });
149
93
  if (Array.isArray(resp)) return resp;
150
94
  if (resp && typeof resp === "object") {
151
95
  const o = resp;
@@ -153,13 +97,13 @@ async function listDocuments(client, kbId) {
153
97
  }
154
98
  return [];
155
99
  }
156
- async function pollIngest(client, kbId, docId, options = {}) {
100
+ async function pollIngest(client, kbName, docId, options = {}) {
157
101
  const timeoutMs = options.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
158
102
  const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
159
103
  const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
160
104
  const now = options.now ?? (() => performance.now());
161
105
  const deadline = now() + timeoutMs;
162
- let doc = await getDocument(client, kbId, docId);
106
+ let doc = await getDocument(client, kbName, docId);
163
107
  let timedOut = false;
164
108
  while (ingestInFlight(doc)) {
165
109
  if (now() >= deadline) {
@@ -168,7 +112,7 @@ async function pollIngest(client, kbId, docId, options = {}) {
168
112
  }
169
113
  await sleep(intervalMs);
170
114
  try {
171
- doc = await getDocument(client, kbId, docId);
115
+ doc = await getDocument(client, kbName, docId);
172
116
  } catch {
173
117
  break;
174
118
  }
@@ -241,8 +185,7 @@ export {
241
185
  DEFAULT_INGEST_TIMEOUT_MS,
242
186
  DEFAULT_FILE_INGEST_TIMEOUT_MS,
243
187
  listKnowledgeBases,
244
- findKb,
245
- findOrCreateKb,
188
+ createKb,
246
189
  uploadText,
247
190
  uploadFile,
248
191
  getDocument,
@@ -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
  };