@aliyunrds/ctxdb 1.0.7 → 1.0.8-beta.1

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.
@@ -1,877 +1,1069 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ DISTRIBUTION_MANIFEST
4
+ } from "./chunk-R67JELM7.js";
2
5
 
3
- // src/lib/logger.ts
4
- import { appendFileSync, mkdirSync } from "fs";
6
+ // src/lib/agents.ts
5
7
  import { homedir } from "os";
6
- import { dirname, join } from "path";
7
- function logPath() {
8
- return join(homedir(), ".ctxdb", "logs", "ctxdb.log");
8
+ import { delimiter, join, sep } from "path";
9
+ import { accessSync, constants, existsSync, statSync } from "fs";
10
+ var SUPPORTED_AGENTS = ["qoder", "qoderwork", "qwenwork", "codex", "claude", "opencode", "hermes", "workbuddy"];
11
+ var SUPPORTED_AGENT_CHOICES = SUPPORTED_AGENTS.join("|");
12
+ var AGENT_CHOICES = [...SUPPORTED_AGENTS, "default"].join("|");
13
+ function isBuiltinAgent(v) {
14
+ return typeof v === "string" && SUPPORTED_AGENTS.includes(v);
9
15
  }
10
- var _enabled = null;
11
- function setDebug(enabled) {
12
- _enabled = enabled;
16
+ function isAgentSlug(v) {
17
+ return v === "default" || isBuiltinAgent(v);
13
18
  }
14
- function isDebug() {
15
- return _enabled === true;
19
+ var isAgent = isAgentSlug;
20
+ function agentHomeDir(agent, home = homedir()) {
21
+ switch (agent) {
22
+ case "qoder":
23
+ return join(home, ".qoder");
24
+ case "qoderwork":
25
+ return join(home, ".qoderwork");
26
+ case "qwenwork":
27
+ return join(home, ".qwenwork");
28
+ case "codex":
29
+ return join(home, ".codex");
30
+ case "claude":
31
+ return join(home, ".claude");
32
+ case "opencode":
33
+ return join(home, ".config", "opencode");
34
+ case "hermes":
35
+ return join(home, ".hermes");
36
+ case "workbuddy":
37
+ return join(home, ".workbuddy");
38
+ }
16
39
  }
17
- function debug(tag, msg, data) {
18
- if (_enabled !== true) return;
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}`;
27
- let line = `${ts} [${tag}] ${msg}`;
28
- if (data !== void 0) {
29
- const s = typeof data === "string" ? data : JSON.stringify(data, null, 2);
30
- line += `
31
- ${s}`;
40
+ var AGENT_VARIANT_HOMES = {
41
+ qoder: [".qoder-cn"],
42
+ qoderwork: [".qoderworkcn"],
43
+ qwenwork: [".qwenworkcn"]
44
+ };
45
+ function agentHomeDirs(agent, home = homedir()) {
46
+ const primary = agentHomeDir(agent, home);
47
+ const variants = (AGENT_VARIANT_HOMES[agent] ?? []).map(
48
+ (name) => join(home, name)
49
+ );
50
+ return [primary, ...variants];
51
+ }
52
+ function agentPlatformSupport(agent, platform = process.platform) {
53
+ if (agent === "hermes" && platform === "win32") {
54
+ return {
55
+ supported: false,
56
+ detail: "Hermes integration currently supports macOS/Linux only; Windows Hermes is not yet supported."
57
+ };
32
58
  }
33
- line += "\n";
34
- const p = logPath();
59
+ return { supported: true, detail: `${agent} is supported on ${platform}.` };
60
+ }
61
+ function inspectAgentHomes(agent, options = {}) {
62
+ const exists = options.exists ?? existsSyncAdapter;
63
+ const candidates = agentHomeDirs(agent, options.home);
64
+ const existing = candidates.filter(exists);
65
+ return { exists: existing.length > 0, existing, candidates };
66
+ }
67
+ function existsSyncAdapter(path) {
35
68
  try {
36
- mkdirSync(dirname(p), { recursive: true });
37
- appendFileSync(p, line, "utf-8");
69
+ return existsSync(path);
38
70
  } catch {
71
+ return false;
39
72
  }
40
73
  }
41
-
42
- // src/lib/package-version.ts
43
- import { readFileSync } from "fs";
44
- import { dirname as dirname2, join as join2 } from "path";
45
- import { fileURLToPath } from "url";
46
- function findPackageVersion() {
47
- let dir = dirname2(fileURLToPath(import.meta.url));
48
- for (let i = 0; i < 5; i++) {
49
- try {
50
- const pkg = JSON.parse(readFileSync(join2(dir, "package.json"), "utf-8"));
51
- if (pkg.name === "@aliyunrds/ctxdb" && typeof pkg.version === "string") {
52
- return pkg.version;
53
- }
54
- } catch {
55
- }
56
- dir = dirname2(dir);
57
- }
58
- return "0.0.0";
59
- }
60
- var PACKAGE_VERSION = findPackageVersion();
61
-
62
- // src/lib/http-client.ts
63
- var DEFAULT_TIMEOUT_MS = 3e4;
64
- var CtxdbError = class extends Error {
65
- constructor(message) {
66
- super(message);
67
- this.name = "CtxdbError";
68
- }
69
- };
70
- var AuthError = class extends CtxdbError {
71
- status = 401;
72
- errorCode;
73
- errorMessage;
74
- data;
75
- responseBody;
76
- constructor(message = "Unauthorized (HTTP 401) \u2014 check api_key", fields = {}) {
77
- super(message);
78
- this.name = "AuthError";
79
- this.errorCode = fields.errorCode;
80
- this.errorMessage = fields.errorMessage;
81
- this.data = fields.data;
82
- this.responseBody = fields.responseBody;
83
- }
84
- };
85
- var NotFoundError = class extends CtxdbError {
86
- status = 404;
87
- path;
88
- errorCode;
89
- errorMessage;
90
- data;
91
- responseBody;
92
- constructor(path, fields = {}) {
93
- super(`Not found: ${path}`);
94
- this.name = "NotFoundError";
95
- this.path = path;
96
- this.errorCode = fields.errorCode;
97
- this.errorMessage = fields.errorMessage;
98
- this.data = fields.data;
99
- this.responseBody = fields.responseBody;
100
- }
101
- };
102
- var APIError = class extends CtxdbError {
103
- status = 400;
104
- path;
105
- detail;
106
- errorCode;
107
- errorMessage;
108
- data;
109
- responseBody;
110
- constructor(path, detail, fields = {}) {
111
- super(`API error at ${path}: ${detail}`);
112
- this.name = "APIError";
113
- this.path = path;
114
- this.detail = detail;
115
- this.errorCode = fields.errorCode;
116
- this.errorMessage = fields.errorMessage;
117
- this.data = fields.data;
118
- this.responseBody = fields.responseBody;
119
- }
74
+ var EXECUTABLE_AGENT_NAMES = {
75
+ opencode: "opencode",
76
+ hermes: "hermes"
120
77
  };
121
- var CtxdbHttpError = class extends CtxdbError {
122
- status;
123
- detail;
124
- errorCode;
125
- errorMessage;
126
- data;
127
- responseBody;
128
- constructor(status, detail, fields = {}) {
129
- super(`HTTP ${status}: ${detail}`);
130
- this.name = "CtxdbHttpError";
131
- this.status = status;
132
- this.detail = detail;
133
- this.errorCode = fields.errorCode;
134
- this.errorMessage = fields.errorMessage;
135
- this.data = fields.data;
136
- this.responseBody = fields.responseBody;
78
+ function isDirectoryAdapter(path) {
79
+ try {
80
+ return statSync(path).isDirectory();
81
+ } catch {
82
+ return false;
137
83
  }
138
- };
139
- var HttpClient = class {
140
- baseUrl;
141
- apiKey;
142
- timeoutMs;
143
- userAgent;
144
- extraHeaders;
145
- fetchImpl;
146
- constructor(opts) {
147
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
148
- this.apiKey = opts.apiKey;
149
- this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
150
- this.userAgent = opts.userAgent ?? `ctxdb-cli/${PACKAGE_VERSION}`;
151
- this.extraHeaders = { ...opts.extraHeaders ?? {} };
152
- const f = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
153
- this.fetchImpl = f;
84
+ }
85
+ function isExecutableAdapter(path, platform) {
86
+ try {
87
+ if (!statSync(path).isFile()) return false;
88
+ if (platform !== "win32") accessSync(path, constants.X_OK);
89
+ return true;
90
+ } catch {
91
+ return false;
154
92
  }
155
- buildHeaders(contentType, requestHeaders = {}) {
156
- const h = {
157
- Authorization: `Token ${this.apiKey}`,
158
- "User-Agent": this.userAgent,
159
- Connection: "close",
160
- ...this.extraHeaders,
161
- ...requestHeaders
162
- };
163
- if (contentType) h["Content-Type"] = contentType;
164
- return h;
93
+ }
94
+ function envValue(env, name, platform) {
95
+ if (env[name] !== void 0) return env[name];
96
+ if (platform !== "win32") return void 0;
97
+ const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
98
+ return entry?.[1];
99
+ }
100
+ function resolveAgentExecutable(command, options = {}) {
101
+ const platform = options.platform ?? process.platform;
102
+ const env = options.env ?? process.env;
103
+ const isExecutable = options.isExecutable ?? isExecutableAdapter;
104
+ const pathValue = envValue(env, "PATH", platform) ?? "";
105
+ const pathSeparator = platform === "win32" ? ";" : delimiter;
106
+ const pathEntries = pathValue.split(pathSeparator).map((entry) => entry.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
107
+ const suffixes = platform === "win32" ? (envValue(env, "PATHEXT", platform) ?? ".COM;.EXE;.BAT;.CMD").split(";").map((suffix) => suffix.trim()).filter(Boolean) : [""];
108
+ for (const directory of pathEntries) {
109
+ for (const suffix of suffixes) {
110
+ const candidate = join(directory, `${command}${suffix}`);
111
+ if (isExecutable(candidate, platform)) return candidate;
112
+ }
165
113
  }
166
- async doRequest(method, path, init = {}) {
167
- let url = `${this.baseUrl}${path}`;
168
- if (init.params) {
169
- const qs = new URLSearchParams();
170
- for (const [k, v] of Object.entries(init.params)) {
171
- if (v !== void 0 && v !== null) qs.append(k, String(v));
114
+ return void 0;
115
+ }
116
+ function displayHomePath(home, path) {
117
+ if (path === home) return "~";
118
+ return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path;
119
+ }
120
+ function detectInstalledAgents(options = {}) {
121
+ const home = options.home ?? homedir();
122
+ const platform = options.platform ?? process.platform;
123
+ const env = options.env ?? process.env;
124
+ const isDirectory = options.isDirectory ?? isDirectoryAdapter;
125
+ const observations = [];
126
+ for (const agent of SUPPORTED_AGENTS) {
127
+ const evidence = [];
128
+ for (const candidate of agentHomeDirs(agent, home)) {
129
+ if (isDirectory(candidate)) {
130
+ evidence.push({ kind: "home", value: displayHomePath(home, candidate) });
172
131
  }
173
- const s = qs.toString();
174
- if (s) url = `${url}?${s}`;
175
132
  }
176
- const effectiveTimeout = init.timeoutMs ?? this.timeoutMs;
177
- const dbg = isDebug();
178
- let t0 = 0;
179
- if (dbg) {
180
- t0 = Date.now();
181
- debug("http", `\u2192 ${method} ${url} (timeout=${effectiveTimeout}ms)`);
133
+ const executableName = EXECUTABLE_AGENT_NAMES[agent];
134
+ if (executableName) {
135
+ const executable = resolveAgentExecutable(executableName, {
136
+ platform,
137
+ env,
138
+ isExecutable: options.isExecutable
139
+ });
140
+ if (executable) {
141
+ evidence.push({ kind: "executable", value: executableName });
142
+ }
182
143
  }
183
- const controller = new AbortController();
184
- const timer = setTimeout(() => controller.abort(), effectiveTimeout);
185
- try {
186
- let resp;
187
- try {
188
- resp = await this.fetchImpl(url, {
189
- method,
190
- headers: this.buildHeaders(init.contentType, init.headers),
191
- body: init.body,
192
- signal: controller.signal
193
- });
194
- } catch (err) {
195
- if (err?.name === "AbortError") {
196
- if (dbg) debug("http", `\u2717 ${method} ${path} timeout after ${Date.now() - t0}ms`);
197
- throw new CtxdbError(`request timeout after ${effectiveTimeout}ms: ${url}`);
144
+ if (evidence.length === 0) {
145
+ observations.push({
146
+ agent,
147
+ selected: false,
148
+ reason: "not-detected",
149
+ evidence
150
+ });
151
+ continue;
152
+ }
153
+ const support = agentPlatformSupport(agent, platform);
154
+ if (!support.supported) {
155
+ observations.push({
156
+ agent,
157
+ selected: false,
158
+ reason: "unsupported-platform",
159
+ evidence,
160
+ detail: support.detail
161
+ });
162
+ continue;
163
+ }
164
+ observations.push({ agent, selected: true, reason: "detected", evidence });
165
+ }
166
+ return {
167
+ selected: observations.filter((item) => item.selected).map((item) => item.agent),
168
+ observations
169
+ };
170
+ }
171
+ function agentFromEnv(env = process.env) {
172
+ return isAgentSlug(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "default";
173
+ }
174
+ function agentFromArgvWithFallback(argv = process.argv.slice(2), env = process.env) {
175
+ for (let i = 0; i < argv.length; i++) {
176
+ const tok = argv[i];
177
+ if (tok === "--agent" && isAgentSlug(argv[i + 1])) {
178
+ return { agent: argv[i + 1], fellBack: false };
179
+ }
180
+ if (tok.startsWith("--agent=")) {
181
+ const raw = tok.slice("--agent=".length);
182
+ if (isAgentSlug(raw)) return { agent: raw, fellBack: false };
183
+ }
184
+ }
185
+ return { agent: agentFromEnv(env), fellBack: true };
186
+ }
187
+
188
+ // src/lib/config.ts
189
+ import { readFileSync as readFileSync2, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
190
+ import { homedir as homedir3 } from "os";
191
+ import { dirname as dirname3, join as join3 } from "path";
192
+
193
+ // src/lib/secure-file.ts
194
+ import {
195
+ chmodSync,
196
+ closeSync,
197
+ copyFileSync,
198
+ existsSync as existsSync2,
199
+ fsyncSync,
200
+ mkdirSync,
201
+ openSync,
202
+ renameSync,
203
+ unlinkSync,
204
+ writeFileSync
205
+ } from "fs";
206
+ import { randomBytes } from "crypto";
207
+ import { dirname } from "path";
208
+ var nodeSecureFileSystem = {
209
+ exists: existsSync2,
210
+ mkdir: (path, options) => {
211
+ mkdirSync(path, options);
212
+ },
213
+ chmod: chmodSync,
214
+ open: openSync,
215
+ write: (fd, data) => {
216
+ writeFileSync(fd, data);
217
+ },
218
+ fsync: fsyncSync,
219
+ close: closeSync,
220
+ copy: copyFileSync,
221
+ rename: renameSync,
222
+ unlink: unlinkSync
223
+ };
224
+ function configBackupPath(target) {
225
+ return `${target}.bak`;
226
+ }
227
+ function secureAtomicWrite(target, content, options = {}) {
228
+ const fs = options.fs ?? nodeSecureFileSystem;
229
+ const platform = options.platform ?? process.platform;
230
+ const posix = platform !== "win32";
231
+ const dir = dirname(target);
232
+ const backup = configBackupPath(target);
233
+ const suffix = options.tempSuffix ?? `${process.pid}.${randomBytes(6).toString("hex")}`;
234
+ const temp = `${target}.${suffix}.tmp`;
235
+ const hadTarget = fs.exists(target);
236
+ fs.mkdir(dir, { recursive: true, mode: 448 });
237
+ if (posix) {
238
+ fs.chmod(dir, 448);
239
+ if (hadTarget) fs.chmod(target, 384);
240
+ }
241
+ let fd = null;
242
+ let tempExists = false;
243
+ try {
244
+ fd = fs.open(temp, "wx", 384);
245
+ tempExists = true;
246
+ fs.write(fd, Buffer.from(content, "utf-8"));
247
+ fs.fsync(fd);
248
+ fs.close(fd);
249
+ fd = null;
250
+ if (posix) fs.chmod(temp, 384);
251
+ if (hadTarget && options.backup !== false) {
252
+ if (platform === "win32") {
253
+ if (fs.exists(backup)) fs.unlink(backup);
254
+ fs.rename(target, backup);
255
+ try {
256
+ fs.rename(temp, target);
257
+ tempExists = false;
258
+ } catch (error) {
259
+ fs.rename(backup, target);
260
+ throw error;
198
261
  }
199
- if (dbg) debug("http", `\u2717 ${method} ${path} network error after ${Date.now() - t0}ms: ${err?.message ?? err}`);
200
- throw new CtxdbError(`network error contacting ${url}: ${err?.message ?? err}`);
201
- }
202
- if (resp.status === 204) {
203
- if (dbg) debug("http", `\u2190 ${method} ${path} 204 (${Date.now() - t0}ms)`);
204
- return {};
262
+ } else {
263
+ fs.copy(target, backup);
264
+ fs.chmod(backup, 384);
265
+ fs.rename(temp, target);
266
+ tempExists = false;
205
267
  }
206
- let text;
268
+ } else {
269
+ fs.rename(temp, target);
270
+ tempExists = false;
271
+ }
272
+ } finally {
273
+ if (fd !== null) {
207
274
  try {
208
- text = await resp.text();
209
- } catch (err) {
210
- if (err?.name === "AbortError") {
211
- if (dbg) debug("http", `\u2717 ${method} ${path} body-read timeout after ${Date.now() - t0}ms`);
212
- throw new CtxdbError(`request timeout after ${effectiveTimeout}ms (body read): ${url}`);
213
- }
214
- if (dbg) debug("http", `\u2717 ${method} ${path} body-read error after ${Date.now() - t0}ms: ${err?.message ?? err}`);
215
- throw new CtxdbError(`network error reading body from ${url}: ${err?.message ?? err}`);
275
+ fs.close(fd);
276
+ } catch {
216
277
  }
217
- if (!resp.ok) {
218
- const parsedError = parseErrorResponse(text, `HTTP ${resp.status}`);
219
- if (dbg) debug("http", `\u2190 ${method} ${path} ${resp.status} (${Date.now() - t0}ms) ${parsedError.detail}`);
220
- if (resp.status === 401) {
221
- throw new AuthError(void 0, parsedError);
222
- }
223
- if (resp.status === 404) {
224
- throw new NotFoundError(path, parsedError);
225
- }
226
- if (resp.status === 400) {
227
- throw new APIError(path, parsedError.detail, parsedError);
228
- }
229
- throw new CtxdbHttpError(resp.status, parsedError.detail, parsedError);
278
+ }
279
+ if (tempExists && fs.exists(temp)) {
280
+ try {
281
+ fs.unlink(temp);
282
+ } catch {
230
283
  }
231
- if (dbg) debug("http", `\u2190 ${method} ${path} ${resp.status} (${Date.now() - t0}ms) body=${text.length}B`);
232
- if (!text) return {};
233
- return maybeJson(text);
234
- } finally {
235
- clearTimeout(timer);
236
284
  }
237
285
  }
238
- // ---------- Convenience verbs ----------
239
- get(path, params) {
240
- return this.doRequest("GET", path, { params });
286
+ }
287
+
288
+ // src/lib/secrets.ts
289
+ import { createHmac, randomBytes as randomBytes2 } from "crypto";
290
+ var PROCESS_FINGERPRINT_KEY = randomBytes2(32);
291
+
292
+ // src/credentials/local-credential-provider.ts
293
+ import {
294
+ closeSync as closeSync2,
295
+ existsSync as existsSync3,
296
+ mkdirSync as mkdirSync2,
297
+ openSync as openSync2,
298
+ readFileSync,
299
+ statSync as statSync2,
300
+ unlinkSync as unlinkSync2,
301
+ writeFileSync as writeFileSync2
302
+ } from "fs";
303
+ import { homedir as homedir2 } from "os";
304
+ import { dirname as dirname2, join as join2 } from "path";
305
+ import { setTimeout as delay } from "timers/promises";
306
+
307
+ // src/credentials/types.ts
308
+ var ACTIVE_CONTEXTDB_CREDENTIAL = "contextdb/active";
309
+
310
+ // src/credentials/local-credential-provider.ts
311
+ var DOCUMENT_VERSION = 1;
312
+ var STALE_LOCK_MS = 10 * 60 * 1e3;
313
+ function defaultCredentialsPath() {
314
+ return join2(homedir2(), ".ctxdb", "credentials.json");
315
+ }
316
+ function emptyDocument() {
317
+ return { version: DOCUMENT_VERSION, records: {} };
318
+ }
319
+ function normalizeOrigin(value, field) {
320
+ if (typeof value !== "string" || !value) {
321
+ throw new Error(`credentials: ${field} must be a non-empty URL`);
241
322
  }
242
- postJson(path, body, params, options = {}) {
243
- return this.doRequest("POST", path, {
244
- body: JSON.stringify(body ?? {}),
245
- contentType: "application/json",
246
- params,
247
- timeoutMs: options.timeoutMs,
248
- headers: options.headers
249
- });
323
+ let url;
324
+ try {
325
+ url = new URL(value);
326
+ } catch {
327
+ throw new Error(`credentials: ${field} must be a valid URL`);
250
328
  }
251
- putJson(path, body, params, options = {}) {
252
- return this.doRequest("PUT", path, {
253
- body: JSON.stringify(body),
254
- contentType: "application/json",
255
- params,
256
- timeoutMs: options.timeoutMs,
257
- headers: options.headers
258
- });
329
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
330
+ throw new Error(`credentials: ${field} must be an HTTP(S) origin`);
259
331
  }
260
- delete(path, params) {
261
- return this.doRequest("DELETE", path, { params });
332
+ return url.toString().replace(/\/$/, "");
333
+ }
334
+ function parseRecord(value) {
335
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
336
+ throw new Error("credentials: record must be an object");
262
337
  }
263
- /**
264
- * POST multipart/form-data using runtime-native FormData.
265
- *
266
- * `fields` are appended as text values; `files` are appended as Blob
267
- * with a filename. We intentionally do NOT pass a `Content-Type`
268
- * header — when fetch sees a FormData body it sets the multipart
269
- * boundary itself.
270
- */
271
- postMultipart(path, fields = {}, files = {}, options = {}) {
272
- const fd = buildMultipartForm(fields, files);
273
- return this.doRequest("POST", path, {
274
- body: fd,
275
- params: options.params,
276
- timeoutMs: options.timeoutMs,
277
- headers: options.headers
278
- });
338
+ const raw = value;
339
+ if (raw.kind !== "api-key") {
340
+ throw new Error(`credentials: unsupported record kind ${String(raw.kind)}`);
279
341
  }
280
- /** PUT multipart/form-data with the same runtime-owned boundary as POST. */
281
- putMultipart(path, fields = {}, files = {}, options = {}) {
282
- const fd = buildMultipartForm(fields, files);
283
- return this.doRequest("PUT", path, {
284
- body: fd,
285
- params: options.params,
286
- timeoutMs: options.timeoutMs,
287
- headers: options.headers
288
- });
342
+ const payload = raw.payload;
343
+ const metadata = raw.metadata;
344
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
345
+ throw new Error("credentials: api-key payload must be an object");
289
346
  }
290
- };
291
- function buildMultipartForm(fields, files) {
292
- const fd = new FormData();
293
- for (const [name, value] of Object.entries(fields)) {
294
- fd.append(name, value);
347
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
348
+ throw new Error("credentials: api-key metadata must be an object");
295
349
  }
296
- for (const [name, part] of Object.entries(files)) {
297
- const blob = part.content instanceof Blob ? part.content : new Blob([part.content], { type: part.mimeType });
298
- fd.append(name, blob, part.filename);
350
+ const body = payload;
351
+ const meta = metadata;
352
+ if (typeof body.api_key !== "string" || !body.api_key) {
353
+ throw new Error("credentials: api_key must be a non-empty string");
299
354
  }
300
- return fd;
355
+ if (meta.authorization_method !== "browser-loopback" && meta.authorization_method !== "device-code") {
356
+ throw new Error("credentials: authorization_method is invalid");
357
+ }
358
+ if (typeof meta.issued_at !== "string" || !Number.isFinite(Date.parse(meta.issued_at))) {
359
+ throw new Error("credentials: issued_at is invalid");
360
+ }
361
+ return {
362
+ kind: "api-key",
363
+ payload: {
364
+ apiKey: body.api_key,
365
+ baseUrl: normalizeOrigin(body.base_url, "base_url"),
366
+ loginServer: normalizeOrigin(body.login_server, "login_server")
367
+ },
368
+ metadata: {
369
+ authorizationMethod: meta.authorization_method,
370
+ issuedAt: meta.issued_at
371
+ }
372
+ };
301
373
  }
302
- function maybeJson(text) {
374
+ function parseDocument(text) {
375
+ let raw;
303
376
  try {
304
- return JSON.parse(text);
377
+ raw = JSON.parse(text);
305
378
  } catch {
306
- return text;
379
+ throw new Error("credentials: credentials.json is not valid JSON");
307
380
  }
308
- }
309
- function parseErrorResponse(text, fallback) {
310
- if (!text) return { detail: fallback };
311
- let parsed;
312
- try {
313
- parsed = JSON.parse(text);
314
- } catch {
315
- return { detail: text || fallback, responseBody: text || void 0 };
381
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
382
+ throw new Error("credentials: document must be an object");
316
383
  }
317
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
318
- const obj = parsed;
319
- let detail = "";
320
- for (const k of ["errorMessage", "detail", "message", "error"]) {
321
- const v = obj[k];
322
- if (typeof v === "string" && v) {
323
- detail = v;
324
- break;
325
- }
326
- }
327
- const rawCode = obj.errorCode;
328
- const numericCode = typeof rawCode === "number" ? rawCode : typeof rawCode === "string" && rawCode.trim() !== "" ? Number(rawCode) : void 0;
329
- const errorCode = typeof numericCode === "number" && Number.isFinite(numericCode) ? numericCode : void 0;
330
- const errorMessage = typeof obj.errorMessage === "string" && obj.errorMessage ? obj.errorMessage : void 0;
331
- return {
332
- detail: detail || JSON.stringify(parsed),
333
- errorCode,
334
- errorMessage,
335
- data: Object.hasOwn(obj, "data") ? obj.data : void 0,
336
- responseBody: parsed
337
- };
384
+ const document = raw;
385
+ if (document.version !== DOCUMENT_VERSION) {
386
+ throw new Error(`credentials: unsupported document version ${String(document.version)}`);
338
387
  }
339
- return { detail: String(parsed), responseBody: parsed };
388
+ if (!document.records || typeof document.records !== "object" || Array.isArray(document.records)) {
389
+ throw new Error("credentials: records must be an object");
390
+ }
391
+ const records = document.records;
392
+ const unknown = Object.keys(records).filter((key) => key !== ACTIVE_CONTEXTDB_CREDENTIAL);
393
+ if (unknown.length > 0) {
394
+ throw new Error(`credentials: unsupported record key ${unknown[0]}`);
395
+ }
396
+ const active = records[ACTIVE_CONTEXTDB_CREDENTIAL];
397
+ return {
398
+ version: DOCUMENT_VERSION,
399
+ records: active === void 0 ? {} : { [ACTIVE_CONTEXTDB_CREDENTIAL]: parseRecord(active) }
400
+ };
401
+ }
402
+ function assertOwnerOnly(path) {
403
+ if (process.platform === "win32" || !existsSync3(path)) return;
404
+ const mode = statSync2(path).mode & 511;
405
+ if ((mode & 63) !== 0) {
406
+ throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
407
+ }
408
+ }
409
+ function readDocument(path) {
410
+ if (!existsSync3(path)) return emptyDocument();
411
+ assertOwnerOnly(path);
412
+ return parseDocument(readFileSync(path, "utf8"));
413
+ }
414
+ function readActiveCredentialSync(path = defaultCredentialsPath()) {
415
+ return readDocument(path).records[ACTIVE_CONTEXTDB_CREDENTIAL];
340
416
  }
341
417
 
342
- // src/lib/agents.ts
343
- import { homedir as homedir2 } from "os";
344
- import { delimiter, join as join3, sep } from "path";
345
- import { accessSync, constants, existsSync, statSync } from "fs";
346
- var SUPPORTED_AGENTS = ["qoder", "qoderwork", "qwenwork", "codex", "claude", "opencode", "hermes"];
347
- function isBuiltinAgent(v) {
348
- return typeof v === "string" && SUPPORTED_AGENTS.includes(v);
418
+ // src/lib/config.ts
419
+ import {
420
+ resolveDebugPolicy
421
+ } from "@aliyunrds/ctxdb-shared";
422
+ function defaultConfigPath() {
423
+ return join3(homedir3(), ".ctxdb", "ctxdb.json");
349
424
  }
350
- function isAgentSlug(v) {
351
- return v === "default" || isBuiltinAgent(v);
425
+ var DEFAULT_CONFIG_PATH = join3(homedir3(), ".ctxdb", "ctxdb.json");
426
+ function configDir() {
427
+ return join3(homedir3(), ".ctxdb");
352
428
  }
353
- var isAgent = isAgentSlug;
354
- function agentHomeDir(agent, home = homedir2()) {
355
- switch (agent) {
356
- case "qoder":
357
- return join3(home, ".qoder");
358
- case "qoderwork":
359
- return join3(home, ".qoderwork");
360
- case "qwenwork":
361
- return join3(home, ".qwenwork");
362
- case "codex":
363
- return join3(home, ".codex");
364
- case "claude":
365
- return join3(home, ".claude");
366
- case "opencode":
367
- return join3(home, ".config", "opencode");
368
- case "hermes":
369
- return join3(home, ".hermes");
370
- }
429
+ var DEFAULT_BASE_URL = "https://context-database.aliyuncs.com";
430
+ var DEFAULT_USER_ID = "default";
431
+ var DEFAULT_TOP_K = 5;
432
+ var DEFAULT_THRESHOLD = 0.4;
433
+ var DEFAULT_KNOWLEDGE_TOP_K = 6;
434
+ var DEFAULT_KB_CATALOG_INJECTION = "session_start";
435
+ function isComplete(cfg) {
436
+ return Boolean(cfg.apiKey && cfg.baseUrl);
371
437
  }
372
- var AGENT_VARIANT_HOMES = {
373
- qoder: [".qoder-cn"],
374
- qoderwork: [".qoderworkcn"],
375
- qwenwork: [".qwenworkcn"]
376
- };
377
- function agentHomeDirs(agent, home = homedir2()) {
378
- const primary = agentHomeDir(agent, home);
379
- const variants = (AGENT_VARIANT_HOMES[agent] ?? []).map(
380
- (name) => join3(home, name)
381
- );
382
- return [primary, ...variants];
438
+ function resolveConfigAgent(options = {}, preloadedRaw) {
439
+ if (isAgentSlug(options.agent)) return options.agent;
440
+ return agentFromEnv(options.env);
383
441
  }
384
- function agentPlatformSupport(agent, platform = process.platform) {
385
- if (agent === "hermes" && platform === "win32") {
386
- return {
387
- supported: false,
388
- detail: "Hermes integration currently supports macOS/Linux only; Windows Hermes is not yet supported."
389
- };
390
- }
391
- return { supported: true, detail: `${agent} is supported on ${platform}.` };
442
+ function coerceInt(v, fallback) {
443
+ if (v === null || v === void 0 || v === "") return fallback;
444
+ const n = typeof v === "number" ? v : Number(v);
445
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
392
446
  }
393
- function inspectAgentHomes(agent, options = {}) {
394
- const exists = options.exists ?? existsSyncAdapter;
395
- const candidates = agentHomeDirs(agent, options.home);
396
- const existing = candidates.filter(exists);
397
- return { exists: existing.length > 0, existing, candidates };
447
+ function coerceFloat(v, fallback) {
448
+ if (v === null || v === void 0 || v === "") return fallback;
449
+ const n = typeof v === "number" ? v : Number(v);
450
+ return Number.isFinite(n) ? n : fallback;
398
451
  }
399
- function existsSyncAdapter(path) {
400
- try {
401
- return existsSync(path);
402
- } catch {
403
- return false;
404
- }
452
+ function coerceBool(v, fallback) {
453
+ if (typeof v === "boolean") return v;
454
+ if (v === void 0 || v === null) return fallback;
455
+ return Boolean(v);
405
456
  }
406
- var EXECUTABLE_AGENT_NAMES = {
407
- opencode: "opencode",
408
- hermes: "hermes"
409
- };
410
- function isDirectoryAdapter(path) {
411
- try {
412
- return statSync(path).isDirectory();
413
- } catch {
414
- return false;
457
+ function coerceKbCatalogInjection(v) {
458
+ if (v === "session_start" || v === "user_prompt_submit" || v === "off") {
459
+ return v;
415
460
  }
461
+ return DEFAULT_KB_CATALOG_INJECTION;
416
462
  }
417
- function isExecutableAdapter(path, platform) {
463
+ function readRaw(path) {
464
+ if (!existsSync4(path)) return {};
418
465
  try {
419
- if (!statSync(path).isFile()) return false;
420
- if (platform !== "win32") accessSync(path, constants.X_OK);
421
- return true;
466
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
467
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
468
+ return parsed;
469
+ }
422
470
  } catch {
423
- return false;
424
471
  }
472
+ return {};
425
473
  }
426
- function envValue(env, name, platform) {
427
- if (env[name] !== void 0) return env[name];
428
- if (platform !== "win32") return void 0;
429
- const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
430
- return entry?.[1];
431
- }
432
- function resolveAgentExecutable(command, options = {}) {
433
- const platform = options.platform ?? process.platform;
434
- const env = options.env ?? process.env;
435
- const isExecutable = options.isExecutable ?? isExecutableAdapter;
436
- const pathValue = envValue(env, "PATH", platform) ?? "";
437
- const pathSeparator = platform === "win32" ? ";" : delimiter;
438
- const pathEntries = pathValue.split(pathSeparator).map((entry) => entry.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
439
- const suffixes = platform === "win32" ? (envValue(env, "PATHEXT", platform) ?? ".COM;.EXE;.BAT;.CMD").split(";").map((suffix) => suffix.trim()).filter(Boolean) : [""];
440
- for (const directory of pathEntries) {
441
- for (const suffix of suffixes) {
442
- const candidate = join3(directory, `${command}${suffix}`);
443
- if (isExecutable(candidate, platform)) return candidate;
474
+ function agentRawFromFile(raw, agent) {
475
+ const agents = raw.agents;
476
+ if (agents && typeof agents === "object" && !Array.isArray(agents)) {
477
+ const candidate = agents[agent];
478
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
479
+ return candidate;
444
480
  }
445
481
  }
446
- return void 0;
482
+ return {};
447
483
  }
448
- function displayHomePath(home, path) {
449
- if (path === home) return "~";
450
- return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path;
484
+ function isV2Schema(raw) {
485
+ if (Object.keys(raw).length === 0) return true;
486
+ return raw.version === 2 && raw.agents !== null && typeof raw.agents === "object" && !Array.isArray(raw.agents);
451
487
  }
452
- function detectInstalledAgents(options = {}) {
453
- const home = options.home ?? homedir2();
454
- const platform = options.platform ?? process.platform;
488
+ function configFromDisk(raw) {
489
+ const debugConfigured = coerceBool(raw.debug, false);
490
+ return applyDebugPolicy({
491
+ apiKey: typeof raw.api_key === "string" && raw.api_key ? raw.api_key : null,
492
+ baseUrl: typeof raw.base_url === "string" && raw.base_url ? String(raw.base_url).replace(/\/+$/, "") : DEFAULT_BASE_URL,
493
+ userId: typeof raw.user_id === "string" && raw.user_id ? raw.user_id : DEFAULT_USER_ID,
494
+ agentId: typeof raw.agent_id === "string" && raw.agent_id ? raw.agent_id : null,
495
+ appId: typeof raw.app_id === "string" && raw.app_id ? raw.app_id : null,
496
+ autoCapture: coerceBool(raw.auto_capture, true),
497
+ autoRecall: coerceBool(raw.auto_recall, true),
498
+ warmupRecall: coerceBool(raw.warmup_recall, false),
499
+ recallKnowledge: coerceBool(raw.recall_knowledge, false),
500
+ topK: coerceInt(raw.top_k, DEFAULT_TOP_K),
501
+ threshold: coerceFloat(raw.threshold, DEFAULT_THRESHOLD),
502
+ knowledgeTopK: coerceInt(raw.knowledge_top_k, DEFAULT_KNOWLEDGE_TOP_K),
503
+ debug: debugConfigured,
504
+ debugConfigured,
505
+ debugForced: false,
506
+ debugReason: null,
507
+ kbCatalogInjection: coerceKbCatalogInjection(raw.kb_catalog_injection)
508
+ });
509
+ }
510
+ function applyDebugPolicy(cfg) {
511
+ const policy = resolveDebugPolicy(cfg.debugConfigured, cfg.baseUrl);
512
+ cfg.debug = policy.debug;
513
+ cfg.debugConfigured = policy.debugConfigured;
514
+ cfg.debugForced = policy.debugForced;
515
+ cfg.debugReason = policy.debugReason;
516
+ return cfg;
517
+ }
518
+ function applyEnv(cfg, env) {
519
+ if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
520
+ if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
521
+ if (env.CTXDB_USER_ID) cfg.userId = env.CTXDB_USER_ID;
522
+ if (env.CTXDB_AGENT_ID) cfg.agentId = env.CTXDB_AGENT_ID;
523
+ if (env.CTXDB_APP_ID) cfg.appId = env.CTXDB_APP_ID;
524
+ return applyDebugPolicy(cfg);
525
+ }
526
+ function load(options = {}) {
527
+ const path = options.path ?? defaultConfigPath();
455
528
  const env = options.env ?? process.env;
456
- const isDirectory = options.isDirectory ?? isDirectoryAdapter;
457
- const observations = [];
458
- for (const agent of SUPPORTED_AGENTS) {
459
- const evidence = [];
460
- for (const candidate of agentHomeDirs(agent, home)) {
461
- if (isDirectory(candidate)) {
462
- evidence.push({ kind: "home", value: displayHomePath(home, candidate) });
463
- }
464
- }
465
- const executableName = EXECUTABLE_AGENT_NAMES[agent];
466
- if (executableName) {
467
- const executable = resolveAgentExecutable(executableName, {
468
- platform,
469
- env,
470
- isExecutable: options.isExecutable
471
- });
472
- if (executable) {
473
- evidence.push({ kind: "executable", value: executableName });
474
- }
475
- }
476
- if (evidence.length === 0) {
477
- observations.push({
478
- agent,
479
- selected: false,
480
- reason: "not-detected",
481
- evidence
482
- });
483
- continue;
529
+ const raw = readRaw(path);
530
+ const agent = resolveConfigAgent({ ...options, path, env }, raw);
531
+ if (!isV2Schema(raw)) {
532
+ try {
533
+ process.stderr.write(
534
+ `[ctxdb] config schema invalid at ${path}, treating as first-time install
535
+ `
536
+ );
537
+ } catch {
484
538
  }
485
- const support = agentPlatformSupport(agent, platform);
486
- if (!support.supported) {
487
- observations.push({
488
- agent,
489
- selected: false,
490
- reason: "unsupported-platform",
491
- evidence,
492
- detail: support.detail
493
- });
494
- continue;
539
+ const cfg2 = configFromDisk({});
540
+ const managed2 = !DISTRIBUTION_MANIFEST.capabilities.managedCredentials || options.managedCredentials === false ? void 0 : readActiveCredentialSync(
541
+ options.credentialsPath ?? join3(dirname3(path), "credentials.json")
542
+ );
543
+ if (managed2) {
544
+ cfg2.apiKey = managed2.payload.apiKey;
545
+ cfg2.baseUrl = managed2.payload.baseUrl;
495
546
  }
496
- observations.push({ agent, selected: true, reason: "detected", evidence });
547
+ return applyEnv(cfg2, env);
497
548
  }
498
- return {
499
- selected: observations.filter((item) => item.selected).map((item) => item.agent),
500
- observations
501
- };
502
- }
503
- function agentFromEnv(env = process.env) {
504
- return isAgentSlug(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "default";
505
- }
506
- function agentFromArgvWithFallback(argv = process.argv.slice(2), env = process.env) {
507
- for (let i = 0; i < argv.length; i++) {
508
- const tok = argv[i];
509
- if (tok === "--agent" && isAgentSlug(argv[i + 1])) {
510
- return { agent: argv[i + 1], fellBack: false };
511
- }
512
- if (tok.startsWith("--agent=")) {
513
- const raw = tok.slice("--agent=".length);
514
- if (isAgentSlug(raw)) return { agent: raw, fellBack: false };
549
+ const agentRaw = agentRawFromFile(raw, agent);
550
+ if (agent !== "default") {
551
+ const defaultRaw = agentRawFromFile(raw, "default");
552
+ if (Object.keys(defaultRaw).length > 0) {
553
+ for (const [k, v] of Object.entries(defaultRaw)) {
554
+ if (!(k in agentRaw)) {
555
+ agentRaw[k] = v;
556
+ }
557
+ }
515
558
  }
516
559
  }
517
- return { agent: agentFromEnv(env), fellBack: true };
560
+ const cfg = configFromDisk(agentRaw);
561
+ const managed = !DISTRIBUTION_MANIFEST.capabilities.managedCredentials || options.managedCredentials === false ? void 0 : readActiveCredentialSync(
562
+ options.credentialsPath ?? join3(dirname3(path), "credentials.json")
563
+ );
564
+ if (managed) {
565
+ cfg.apiKey = managed.payload.apiKey;
566
+ cfg.baseUrl = managed.payload.baseUrl;
567
+ }
568
+ return applyEnv(cfg, env);
518
569
  }
519
-
520
- // src/lib/config.ts
521
- import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
522
- import { homedir as homedir3 } from "os";
523
- import { join as join4 } from "path";
524
-
525
- // src/lib/secure-file.ts
526
- import {
527
- chmodSync,
528
- closeSync,
529
- copyFileSync,
530
- existsSync as existsSync2,
531
- fsyncSync,
532
- mkdirSync as mkdirSync2,
533
- openSync,
534
- renameSync,
535
- unlinkSync,
536
- writeFileSync
537
- } from "fs";
538
- import { randomBytes } from "crypto";
539
- import { dirname as dirname3 } from "path";
540
- var nodeSecureFileSystem = {
541
- exists: existsSync2,
542
- mkdir: (path, options) => {
543
- mkdirSync2(path, options);
544
- },
545
- chmod: chmodSync,
546
- open: openSync,
547
- write: (fd, data) => {
548
- writeFileSync(fd, data);
549
- },
550
- fsync: fsyncSync,
551
- close: closeSync,
552
- copy: copyFileSync,
553
- rename: renameSync,
554
- unlink: unlinkSync
555
- };
556
- function configBackupPath(target) {
557
- return `${target}.bak`;
570
+ function configToDisk(cfg) {
571
+ return {
572
+ api_key: cfg.apiKey,
573
+ base_url: cfg.baseUrl,
574
+ user_id: cfg.userId,
575
+ agent_id: cfg.agentId,
576
+ app_id: cfg.appId,
577
+ auto_capture: cfg.autoCapture,
578
+ auto_recall: cfg.autoRecall,
579
+ warmup_recall: cfg.warmupRecall,
580
+ recall_knowledge: cfg.recallKnowledge,
581
+ top_k: cfg.topK,
582
+ threshold: cfg.threshold,
583
+ knowledge_top_k: cfg.knowledgeTopK,
584
+ debug: cfg.debugConfigured,
585
+ kb_catalog_injection: cfg.kbCatalogInjection
586
+ };
558
587
  }
559
- function secureAtomicWrite(target, content, options = {}) {
560
- const fs = options.fs ?? nodeSecureFileSystem;
561
- const platform = options.platform ?? process.platform;
562
- const posix = platform !== "win32";
563
- const dir = dirname3(target);
564
- const backup = configBackupPath(target);
565
- const suffix = options.tempSuffix ?? `${process.pid}.${randomBytes(6).toString("hex")}`;
566
- const temp = `${target}.${suffix}.tmp`;
567
- const hadTarget = fs.exists(target);
568
- fs.mkdir(dir, { recursive: true, mode: 448 });
569
- if (posix) {
570
- fs.chmod(dir, 448);
571
- if (hadTarget) fs.chmod(target, 384);
588
+ function removeAgent(agent, path, options = {}) {
589
+ const target = path ?? defaultConfigPath();
590
+ if (!existsSync4(target)) {
591
+ return { removed: false, remainingAgents: [], fileDeleted: false };
572
592
  }
573
- let fd = null;
574
- let tempExists = false;
575
- try {
576
- fd = fs.open(temp, "wx", 384);
577
- tempExists = true;
578
- fs.write(fd, Buffer.from(content, "utf-8"));
579
- fs.fsync(fd);
580
- fs.close(fd);
581
- fd = null;
582
- if (posix) fs.chmod(temp, 384);
583
- if (hadTarget && options.backup !== false) {
584
- if (platform === "win32") {
585
- if (fs.exists(backup)) fs.unlink(backup);
586
- fs.rename(target, backup);
587
- try {
588
- fs.rename(temp, target);
589
- tempExists = false;
590
- } catch (error) {
591
- fs.rename(backup, target);
592
- throw error;
593
- }
594
- } else {
595
- fs.copy(target, backup);
596
- fs.chmod(backup, 384);
597
- fs.rename(temp, target);
598
- tempExists = false;
599
- }
600
- } else {
601
- fs.rename(temp, target);
602
- tempExists = false;
603
- }
604
- } finally {
605
- if (fd !== null) {
606
- try {
607
- fs.close(fd);
608
- } catch {
609
- }
610
- }
611
- if (tempExists && fs.exists(temp)) {
612
- try {
613
- fs.unlink(temp);
614
- } catch {
615
- }
593
+ const raw = readRaw(target);
594
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
595
+ return { removed: false, remainingAgents: [], fileDeleted: false };
596
+ }
597
+ const agents = { ...raw.agents };
598
+ if (!(agent in agents)) {
599
+ return {
600
+ removed: false,
601
+ remainingAgents: Object.keys(agents),
602
+ fileDeleted: false
603
+ };
604
+ }
605
+ delete agents[agent];
606
+ const remaining = Object.keys(agents);
607
+ if (remaining.length === 0 && !options.keepEmptyShell) {
608
+ try {
609
+ unlinkSync3(target);
610
+ return { removed: true, remainingAgents: [], fileDeleted: true };
611
+ } catch {
616
612
  }
617
613
  }
614
+ const onDisk = { ...raw, version: 2, agents };
615
+ delete onDisk.default_agent;
616
+ secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
617
+ return { removed: true, remainingAgents: remaining, fileDeleted: false };
618
618
  }
619
-
620
- // src/lib/secrets.ts
621
- import { createHmac, randomBytes as randomBytes2 } from "crypto";
622
- var PROCESS_FINGERPRINT_KEY = randomBytes2(32);
623
-
624
- // src/lib/config.ts
625
- import {
626
- resolveDebugPolicy
627
- } from "@aliyunrds/ctxdb-shared";
628
- function defaultConfigPath() {
629
- return join4(homedir3(), ".ctxdb", "ctxdb.json");
619
+ function save(cfg, path, options = {}) {
620
+ const target = path ?? defaultConfigPath();
621
+ const agent = resolveConfigAgent(options);
622
+ const raw = readRaw(target);
623
+ const validRaw = isV2Schema(raw) ? raw : {};
624
+ const existingAgents = validRaw.agents && typeof validRaw.agents === "object" && !Array.isArray(validRaw.agents) ? { ...validRaw.agents } : {};
625
+ const serialized = configToDisk(cfg);
626
+ if (options.omitApiKey) delete serialized.api_key;
627
+ const onDisk = {
628
+ ...validRaw,
629
+ version: 2,
630
+ agents: {
631
+ ...existingAgents,
632
+ [agent]: serialized
633
+ }
634
+ };
635
+ delete onDisk.default_agent;
636
+ secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
630
637
  }
631
- var DEFAULT_CONFIG_PATH = join4(homedir3(), ".ctxdb", "ctxdb.json");
632
- function configDir() {
633
- return join4(homedir3(), ".ctxdb");
638
+ function configuredAgents(path) {
639
+ const target = path ?? defaultConfigPath();
640
+ const raw = readRaw(target);
641
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object") return [];
642
+ return Object.keys(raw.agents).filter(isAgent);
634
643
  }
635
- var DEFAULT_BASE_URL = "https://context-database.aliyuncs.com";
636
- var DEFAULT_USER_ID = "default";
637
- var DEFAULT_TOP_K = 5;
638
- var DEFAULT_THRESHOLD = 0.4;
639
- var DEFAULT_KNOWLEDGE_TOP_K = 6;
640
- var DEFAULT_KB_CATALOG_INJECTION = "session_start";
641
- function isComplete(cfg) {
642
- return Boolean(cfg.apiKey && cfg.baseUrl);
644
+ function hasConfiguredAgent(agent, path) {
645
+ const raw = readRaw(path ?? defaultConfigPath());
646
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
647
+ return false;
648
+ }
649
+ const profile = raw.agents[agent];
650
+ return Boolean(
651
+ profile && typeof profile === "object" && !Array.isArray(profile)
652
+ );
643
653
  }
644
- function resolveConfigAgent(options = {}, preloadedRaw) {
645
- if (isAgentSlug(options.agent)) return options.agent;
646
- return agentFromEnv(options.env);
654
+ function updateConfiguredDebug(agent, configured, path) {
655
+ const target = path ?? defaultConfigPath();
656
+ const raw = readRaw(target);
657
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
658
+ return false;
659
+ }
660
+ const agents = { ...raw.agents };
661
+ const profile = agents[agent];
662
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
663
+ return false;
664
+ }
665
+ agents[agent] = { ...profile, debug: configured };
666
+ secureAtomicWrite(
667
+ target,
668
+ JSON.stringify({ ...raw, agents }, null, 2) + "\n"
669
+ );
670
+ return true;
647
671
  }
648
- function coerceInt(v, fallback) {
649
- if (v === null || v === void 0 || v === "") return fallback;
650
- const n = typeof v === "number" ? v : Number(v);
651
- return Number.isFinite(n) ? Math.trunc(n) : fallback;
672
+ function writeInstalledPkgVersion(version, path) {
673
+ const target = path ?? defaultConfigPath();
674
+ const raw = readRaw(target);
675
+ const updated = { ...raw, installed_pkg_version: version };
676
+ secureAtomicWrite(target, JSON.stringify(updated, null, 2) + "\n");
652
677
  }
653
- function coerceFloat(v, fallback) {
654
- if (v === null || v === void 0 || v === "") return fallback;
655
- const n = typeof v === "number" ? v : Number(v);
656
- return Number.isFinite(n) ? n : fallback;
678
+
679
+ // src/lib/logger.ts
680
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
681
+ import { homedir as homedir4 } from "os";
682
+ import { dirname as dirname4, join as join4 } from "path";
683
+ function logPath() {
684
+ return join4(homedir4(), ".ctxdb", "logs", "ctxdb.log");
657
685
  }
658
- function coerceBool(v, fallback) {
659
- if (typeof v === "boolean") return v;
660
- if (v === void 0 || v === null) return fallback;
661
- return Boolean(v);
686
+ var _enabled = null;
687
+ function setDebug(enabled) {
688
+ _enabled = enabled;
662
689
  }
663
- function coerceKbCatalogInjection(v) {
664
- if (v === "session_start" || v === "user_prompt_submit" || v === "off") {
665
- return v;
666
- }
667
- return DEFAULT_KB_CATALOG_INJECTION;
690
+ function isDebug() {
691
+ return _enabled === true;
668
692
  }
669
- function readRaw(path) {
670
- if (!existsSync3(path)) return {};
693
+ function debug(tag, msg, data) {
694
+ if (_enabled !== true) return;
695
+ const now = /* @__PURE__ */ new Date();
696
+ const pad = (n) => String(n).padStart(2, "0");
697
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
698
+ const tz = -now.getTimezoneOffset();
699
+ const tzSign = tz >= 0 ? "+" : "-";
700
+ const tzH = pad(Math.floor(Math.abs(tz) / 60));
701
+ const tzM = pad(Math.abs(tz) % 60);
702
+ 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}`;
703
+ let line = `${ts} [${tag}] ${msg}`;
704
+ if (data !== void 0) {
705
+ const s = typeof data === "string" ? data : JSON.stringify(data, null, 2);
706
+ line += `
707
+ ${s}`;
708
+ }
709
+ line += "\n";
710
+ const p = logPath();
671
711
  try {
672
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
673
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
674
- return parsed;
675
- }
712
+ mkdirSync3(dirname4(p), { recursive: true });
713
+ appendFileSync(p, line, "utf-8");
676
714
  } catch {
677
715
  }
678
- return {};
679
716
  }
680
- function agentRawFromFile(raw, agent) {
681
- const agents = raw.agents;
682
- if (agents && typeof agents === "object" && !Array.isArray(agents)) {
683
- const candidate = agents[agent];
684
- if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
685
- return candidate;
717
+
718
+ // src/lib/package-version.ts
719
+ import { readFileSync as readFileSync3 } from "fs";
720
+ import { dirname as dirname5, join as join5 } from "path";
721
+ import { fileURLToPath } from "url";
722
+ var PACKAGE_NAMES = /* @__PURE__ */ new Set([
723
+ "@aliyunrds/ctxdb",
724
+ "@ali/ctxdb-internal"
725
+ ]);
726
+ function findPackageIdentity() {
727
+ let dir = dirname5(fileURLToPath(import.meta.url));
728
+ for (let i = 0; i < 5; i++) {
729
+ try {
730
+ const pkg = JSON.parse(readFileSync3(join5(dir, "package.json"), "utf-8"));
731
+ if (PACKAGE_NAMES.has(pkg.name) && typeof pkg.version === "string") {
732
+ return { name: pkg.name, version: pkg.version };
733
+ }
734
+ } catch {
686
735
  }
736
+ dir = dirname5(dir);
687
737
  }
688
- return {};
738
+ return { name: "@aliyunrds/ctxdb", version: "0.0.0" };
689
739
  }
690
- function isV2Schema(raw) {
691
- if (Object.keys(raw).length === 0) return true;
692
- return raw.version === 2 && raw.agents !== null && typeof raw.agents === "object" && !Array.isArray(raw.agents);
693
- }
694
- function configFromDisk(raw) {
695
- const debugConfigured = coerceBool(raw.debug, false);
696
- return applyDebugPolicy({
697
- apiKey: typeof raw.api_key === "string" && raw.api_key ? raw.api_key : null,
698
- baseUrl: typeof raw.base_url === "string" && raw.base_url ? String(raw.base_url).replace(/\/+$/, "") : DEFAULT_BASE_URL,
699
- userId: typeof raw.user_id === "string" && raw.user_id ? raw.user_id : DEFAULT_USER_ID,
700
- agentId: typeof raw.agent_id === "string" && raw.agent_id ? raw.agent_id : null,
701
- appId: typeof raw.app_id === "string" && raw.app_id ? raw.app_id : null,
702
- autoCapture: coerceBool(raw.auto_capture, true),
703
- autoRecall: coerceBool(raw.auto_recall, true),
704
- warmupRecall: coerceBool(raw.warmup_recall, false),
705
- recallKnowledge: coerceBool(raw.recall_knowledge, false),
706
- topK: coerceInt(raw.top_k, DEFAULT_TOP_K),
707
- threshold: coerceFloat(raw.threshold, DEFAULT_THRESHOLD),
708
- knowledgeTopK: coerceInt(raw.knowledge_top_k, DEFAULT_KNOWLEDGE_TOP_K),
709
- debug: debugConfigured,
710
- debugConfigured,
711
- debugForced: false,
712
- debugReason: null,
713
- kbCatalogInjection: coerceKbCatalogInjection(raw.kb_catalog_injection)
714
- });
715
- }
716
- function applyDebugPolicy(cfg) {
717
- const policy = resolveDebugPolicy(cfg.debugConfigured, cfg.baseUrl);
718
- cfg.debug = policy.debug;
719
- cfg.debugConfigured = policy.debugConfigured;
720
- cfg.debugForced = policy.debugForced;
721
- cfg.debugReason = policy.debugReason;
722
- return cfg;
723
- }
724
- function applyEnv(cfg, env) {
725
- if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
726
- if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
727
- if (env.CTXDB_USER_ID) cfg.userId = env.CTXDB_USER_ID;
728
- if (env.CTXDB_AGENT_ID) cfg.agentId = env.CTXDB_AGENT_ID;
729
- if (env.CTXDB_APP_ID) cfg.appId = env.CTXDB_APP_ID;
730
- return applyDebugPolicy(cfg);
731
- }
732
- function load(options = {}) {
733
- const path = options.path ?? defaultConfigPath();
734
- const env = options.env ?? process.env;
735
- const raw = readRaw(path);
736
- const agent = resolveConfigAgent({ ...options, path, env }, raw);
737
- if (!isV2Schema(raw)) {
738
- try {
739
- process.stderr.write(
740
- `[ctxdb] config schema invalid at ${path}, treating as first-time install
741
- `
742
- );
743
- } catch {
740
+ var PACKAGE_IDENTITY = findPackageIdentity();
741
+ var PACKAGE_NAME = PACKAGE_IDENTITY.name;
742
+ var PACKAGE_VERSION = PACKAGE_IDENTITY.version;
743
+
744
+ // src/lib/http-client.ts
745
+ var DEFAULT_TIMEOUT_MS = 3e4;
746
+ var CtxdbError = class extends Error {
747
+ constructor(message) {
748
+ super(message);
749
+ this.name = "CtxdbError";
750
+ }
751
+ };
752
+ var AuthError = class extends CtxdbError {
753
+ status = 401;
754
+ errorCode;
755
+ errorMessage;
756
+ data;
757
+ responseBody;
758
+ constructor(message = "Unauthorized (HTTP 401) \u2014 check api_key", fields = {}) {
759
+ super(message);
760
+ this.name = "AuthError";
761
+ this.errorCode = fields.errorCode;
762
+ this.errorMessage = fields.errorMessage;
763
+ this.data = fields.data;
764
+ this.responseBody = fields.responseBody;
765
+ }
766
+ };
767
+ var NotFoundError = class extends CtxdbError {
768
+ status = 404;
769
+ path;
770
+ errorCode;
771
+ errorMessage;
772
+ data;
773
+ responseBody;
774
+ constructor(path, fields = {}) {
775
+ super(`Not found: ${path}`);
776
+ this.name = "NotFoundError";
777
+ this.path = path;
778
+ this.errorCode = fields.errorCode;
779
+ this.errorMessage = fields.errorMessage;
780
+ this.data = fields.data;
781
+ this.responseBody = fields.responseBody;
782
+ }
783
+ };
784
+ var APIError = class extends CtxdbError {
785
+ status = 400;
786
+ path;
787
+ detail;
788
+ errorCode;
789
+ errorMessage;
790
+ data;
791
+ responseBody;
792
+ constructor(path, detail, fields = {}) {
793
+ super(`API error at ${path}: ${detail}`);
794
+ this.name = "APIError";
795
+ this.path = path;
796
+ this.detail = detail;
797
+ this.errorCode = fields.errorCode;
798
+ this.errorMessage = fields.errorMessage;
799
+ this.data = fields.data;
800
+ this.responseBody = fields.responseBody;
801
+ }
802
+ };
803
+ var CtxdbHttpError = class extends CtxdbError {
804
+ status;
805
+ detail;
806
+ errorCode;
807
+ errorMessage;
808
+ data;
809
+ responseBody;
810
+ constructor(status, detail, fields = {}) {
811
+ super(`HTTP ${status}: ${detail}`);
812
+ this.name = "CtxdbHttpError";
813
+ this.status = status;
814
+ this.detail = detail;
815
+ this.errorCode = fields.errorCode;
816
+ this.errorMessage = fields.errorMessage;
817
+ this.data = fields.data;
818
+ this.responseBody = fields.responseBody;
819
+ }
820
+ };
821
+ var HttpClient = class {
822
+ baseUrl;
823
+ apiKey;
824
+ timeoutMs;
825
+ userAgent;
826
+ extraHeaders;
827
+ fetchImpl;
828
+ authorizationProvider;
829
+ constructor(opts) {
830
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
831
+ if (!opts.apiKey && !opts.authorizationProvider) {
832
+ throw new Error("HttpClient requires apiKey or authorizationProvider");
744
833
  }
745
- return applyEnv(configFromDisk({}), env);
834
+ this.apiKey = opts.apiKey ?? "";
835
+ this.authorizationProvider = opts.authorizationProvider;
836
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
837
+ this.userAgent = opts.userAgent ?? `ctxdb-cli/${PACKAGE_VERSION}`;
838
+ this.extraHeaders = { ...opts.extraHeaders ?? {} };
839
+ const f = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
840
+ this.fetchImpl = f;
746
841
  }
747
- const agentRaw = agentRawFromFile(raw, agent);
748
- if (agent !== "default") {
749
- const defaultRaw = agentRawFromFile(raw, "default");
750
- if (Object.keys(defaultRaw).length > 0) {
751
- for (const [k, v] of Object.entries(defaultRaw)) {
752
- if (!(k in agentRaw)) {
753
- agentRaw[k] = v;
842
+ async buildHeaders(authorization, contentType, requestHeaders = {}) {
843
+ const h = {
844
+ Authorization: `${authorization.scheme} ${authorization.value}`,
845
+ "User-Agent": this.userAgent,
846
+ Connection: "close",
847
+ ...this.extraHeaders,
848
+ ...requestHeaders
849
+ };
850
+ if (contentType) h["Content-Type"] = contentType;
851
+ return h;
852
+ }
853
+ async doRequest(method, path, init = {}) {
854
+ const authorization = this.authorizationProvider ? await this.authorizationProvider.resolve() : {
855
+ scheme: "Token",
856
+ value: this.apiKey,
857
+ baseUrl: this.baseUrl,
858
+ expiresAt: null
859
+ };
860
+ let url = `${authorization.baseUrl.replace(/\/+$/, "")}${path}`;
861
+ if (init.params) {
862
+ const qs = new URLSearchParams();
863
+ for (const [k, v] of Object.entries(init.params)) {
864
+ if (v !== void 0 && v !== null) qs.append(k, String(v));
865
+ }
866
+ const s = qs.toString();
867
+ if (s) url = `${url}?${s}`;
868
+ }
869
+ const effectiveTimeout = init.timeoutMs ?? this.timeoutMs;
870
+ const dbg = isDebug();
871
+ let t0 = 0;
872
+ if (dbg) {
873
+ t0 = Date.now();
874
+ debug("http", `\u2192 ${method} ${url} (timeout=${effectiveTimeout}ms)`);
875
+ }
876
+ const controller = new AbortController();
877
+ const timer = setTimeout(() => controller.abort(), effectiveTimeout);
878
+ try {
879
+ let resp;
880
+ try {
881
+ resp = await this.fetchImpl(url, {
882
+ method,
883
+ headers: await this.buildHeaders(
884
+ authorization,
885
+ init.contentType,
886
+ init.headers
887
+ ),
888
+ body: init.body,
889
+ signal: controller.signal
890
+ });
891
+ } catch (err) {
892
+ if (err?.name === "AbortError") {
893
+ if (dbg) debug("http", `\u2717 ${method} ${path} timeout after ${Date.now() - t0}ms`);
894
+ throw new CtxdbError(`request timeout after ${effectiveTimeout}ms: ${url}`);
895
+ }
896
+ if (dbg) debug("http", `\u2717 ${method} ${path} network error after ${Date.now() - t0}ms: ${err?.message ?? err}`);
897
+ throw new CtxdbError(`network error contacting ${url}: ${err?.message ?? err}`);
898
+ }
899
+ if (resp.status === 204) {
900
+ if (dbg) debug("http", `\u2190 ${method} ${path} 204 (${Date.now() - t0}ms)`);
901
+ return {};
902
+ }
903
+ let text;
904
+ try {
905
+ text = await resp.text();
906
+ } catch (err) {
907
+ if (err?.name === "AbortError") {
908
+ if (dbg) debug("http", `\u2717 ${method} ${path} body-read timeout after ${Date.now() - t0}ms`);
909
+ throw new CtxdbError(`request timeout after ${effectiveTimeout}ms (body read): ${url}`);
910
+ }
911
+ if (dbg) debug("http", `\u2717 ${method} ${path} body-read error after ${Date.now() - t0}ms: ${err?.message ?? err}`);
912
+ throw new CtxdbError(`network error reading body from ${url}: ${err?.message ?? err}`);
913
+ }
914
+ if (!resp.ok) {
915
+ const parsedError = parseErrorResponse(text, `HTTP ${resp.status}`);
916
+ if (dbg) debug("http", `\u2190 ${method} ${path} ${resp.status} (${Date.now() - t0}ms) ${parsedError.detail}`);
917
+ if (resp.status === 401) {
918
+ throw new AuthError(void 0, parsedError);
919
+ }
920
+ if (resp.status === 404) {
921
+ throw new NotFoundError(path, parsedError);
922
+ }
923
+ if (resp.status === 400) {
924
+ throw new APIError(path, parsedError.detail, parsedError);
754
925
  }
926
+ throw new CtxdbHttpError(resp.status, parsedError.detail, parsedError);
755
927
  }
928
+ if (dbg) debug("http", `\u2190 ${method} ${path} ${resp.status} (${Date.now() - t0}ms) body=${text.length}B`);
929
+ if (!text) return {};
930
+ return maybeJson(text);
931
+ } finally {
932
+ clearTimeout(timer);
756
933
  }
757
934
  }
758
- return applyEnv(configFromDisk(agentRaw), env);
759
- }
760
- function configToDisk(cfg) {
761
- return {
762
- api_key: cfg.apiKey,
763
- base_url: cfg.baseUrl,
764
- user_id: cfg.userId,
765
- agent_id: cfg.agentId,
766
- app_id: cfg.appId,
767
- auto_capture: cfg.autoCapture,
768
- auto_recall: cfg.autoRecall,
769
- warmup_recall: cfg.warmupRecall,
770
- recall_knowledge: cfg.recallKnowledge,
771
- top_k: cfg.topK,
772
- threshold: cfg.threshold,
773
- knowledge_top_k: cfg.knowledgeTopK,
774
- debug: cfg.debugConfigured,
775
- kb_catalog_injection: cfg.kbCatalogInjection
776
- };
777
- }
778
- function removeAgent(agent, path, options = {}) {
779
- const target = path ?? defaultConfigPath();
780
- if (!existsSync3(target)) {
781
- return { removed: false, remainingAgents: [], fileDeleted: false };
935
+ // ---------- Convenience verbs ----------
936
+ get(path, params) {
937
+ return this.doRequest("GET", path, { params });
782
938
  }
783
- const raw = readRaw(target);
784
- if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
785
- return { removed: false, remainingAgents: [], fileDeleted: false };
939
+ postJson(path, body, params, options = {}) {
940
+ return this.doRequest("POST", path, {
941
+ body: JSON.stringify(body ?? {}),
942
+ contentType: "application/json",
943
+ params,
944
+ timeoutMs: options.timeoutMs,
945
+ headers: options.headers
946
+ });
786
947
  }
787
- const agents = { ...raw.agents };
788
- if (!(agent in agents)) {
789
- return {
790
- removed: false,
791
- remainingAgents: Object.keys(agents),
792
- fileDeleted: false
793
- };
948
+ putJson(path, body, params, options = {}) {
949
+ return this.doRequest("PUT", path, {
950
+ body: JSON.stringify(body),
951
+ contentType: "application/json",
952
+ params,
953
+ timeoutMs: options.timeoutMs,
954
+ headers: options.headers
955
+ });
794
956
  }
795
- delete agents[agent];
796
- const remaining = Object.keys(agents);
797
- if (remaining.length === 0 && !options.keepEmptyShell) {
798
- try {
799
- unlinkSync2(target);
800
- return { removed: true, remainingAgents: [], fileDeleted: true };
801
- } catch {
802
- }
957
+ patchJson(path, body, params, options = {}) {
958
+ return this.doRequest("PATCH", path, {
959
+ body: JSON.stringify(body),
960
+ contentType: "application/json",
961
+ params,
962
+ timeoutMs: options.timeoutMs,
963
+ headers: options.headers
964
+ });
803
965
  }
804
- const onDisk = { ...raw, version: 2, agents };
805
- delete onDisk.default_agent;
806
- secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
807
- return { removed: true, remainingAgents: remaining, fileDeleted: false };
808
- }
809
- function save(cfg, path, options = {}) {
810
- const target = path ?? defaultConfigPath();
811
- const agent = resolveConfigAgent(options);
812
- const raw = readRaw(target);
813
- const validRaw = isV2Schema(raw) ? raw : {};
814
- const existingAgents = validRaw.agents && typeof validRaw.agents === "object" && !Array.isArray(validRaw.agents) ? { ...validRaw.agents } : {};
815
- const onDisk = {
816
- ...validRaw,
817
- version: 2,
818
- agents: {
819
- ...existingAgents,
820
- [agent]: configToDisk(cfg)
821
- }
822
- };
823
- delete onDisk.default_agent;
824
- secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
825
- }
826
- function configuredAgents(path) {
827
- const target = path ?? defaultConfigPath();
828
- const raw = readRaw(target);
829
- if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object") return [];
830
- return Object.keys(raw.agents).filter(isAgent);
966
+ delete(path, params) {
967
+ return this.doRequest("DELETE", path, { params });
968
+ }
969
+ /**
970
+ * DELETE with a JSON body. ContextDB batch deletes use the
971
+ * `DELETE /v1/{resource}/batch` shape with an `{"ids": [...]}` body
972
+ * (server contract) the plain `delete()` above only supports query
973
+ * params.
974
+ */
975
+ deleteJson(path, body, params, options = {}) {
976
+ return this.doRequest("DELETE", path, {
977
+ body: JSON.stringify(body),
978
+ contentType: "application/json",
979
+ params,
980
+ timeoutMs: options.timeoutMs,
981
+ headers: options.headers
982
+ });
983
+ }
984
+ /**
985
+ * POST multipart/form-data using runtime-native FormData.
986
+ *
987
+ * `fields` are appended as text values; `files` are appended as Blob
988
+ * with a filename. We intentionally do NOT pass a `Content-Type`
989
+ * header when fetch sees a FormData body it sets the multipart
990
+ * boundary itself.
991
+ */
992
+ postMultipart(path, fields = {}, files = {}, options = {}) {
993
+ const fd = buildMultipartForm(fields, files);
994
+ return this.doRequest("POST", path, {
995
+ body: fd,
996
+ params: options.params,
997
+ timeoutMs: options.timeoutMs,
998
+ headers: options.headers
999
+ });
1000
+ }
1001
+ /** PUT multipart/form-data with the same runtime-owned boundary as POST. */
1002
+ putMultipart(path, fields = {}, files = {}, options = {}) {
1003
+ const fd = buildMultipartForm(fields, files);
1004
+ return this.doRequest("PUT", path, {
1005
+ body: fd,
1006
+ params: options.params,
1007
+ timeoutMs: options.timeoutMs,
1008
+ headers: options.headers
1009
+ });
1010
+ }
1011
+ };
1012
+ function buildMultipartForm(fields, files) {
1013
+ const fd = new FormData();
1014
+ for (const [name, value] of Object.entries(fields)) {
1015
+ fd.append(name, value);
1016
+ }
1017
+ for (const [name, part] of Object.entries(files)) {
1018
+ const blob = part.content instanceof Blob ? part.content : new Blob([part.content], { type: part.mimeType });
1019
+ fd.append(name, blob, part.filename);
1020
+ }
1021
+ return fd;
831
1022
  }
832
- function hasConfiguredAgent(agent, path) {
833
- const raw = readRaw(path ?? defaultConfigPath());
834
- if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
835
- return false;
1023
+ function maybeJson(text) {
1024
+ try {
1025
+ return JSON.parse(text);
1026
+ } catch {
1027
+ return text;
836
1028
  }
837
- const profile = raw.agents[agent];
838
- return Boolean(
839
- profile && typeof profile === "object" && !Array.isArray(profile)
840
- );
841
1029
  }
842
- function updateConfiguredDebug(agent, configured, path) {
843
- const target = path ?? defaultConfigPath();
844
- const raw = readRaw(target);
845
- if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
846
- return false;
1030
+ function parseErrorResponse(text, fallback) {
1031
+ if (!text) return { detail: fallback };
1032
+ let parsed;
1033
+ try {
1034
+ parsed = JSON.parse(text);
1035
+ } catch {
1036
+ return { detail: text || fallback, responseBody: text || void 0 };
847
1037
  }
848
- const agents = { ...raw.agents };
849
- const profile = agents[agent];
850
- if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
851
- return false;
1038
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1039
+ const obj = parsed;
1040
+ let detail = "";
1041
+ for (const k of ["errorMessage", "detail", "message", "error"]) {
1042
+ const v = obj[k];
1043
+ if (typeof v === "string" && v) {
1044
+ detail = v;
1045
+ break;
1046
+ }
1047
+ }
1048
+ const rawCode = obj.errorCode;
1049
+ const numericCode = typeof rawCode === "number" ? rawCode : typeof rawCode === "string" && rawCode.trim() !== "" ? Number(rawCode) : void 0;
1050
+ const errorCode = typeof numericCode === "number" && Number.isFinite(numericCode) ? numericCode : void 0;
1051
+ const errorMessage = typeof obj.errorMessage === "string" && obj.errorMessage ? obj.errorMessage : void 0;
1052
+ return {
1053
+ detail: detail || JSON.stringify(parsed),
1054
+ errorCode,
1055
+ errorMessage,
1056
+ data: Object.hasOwn(obj, "data") ? obj.data : void 0,
1057
+ responseBody: parsed
1058
+ };
852
1059
  }
853
- agents[agent] = { ...profile, debug: configured };
854
- secureAtomicWrite(
855
- target,
856
- JSON.stringify({ ...raw, agents }, null, 2) + "\n"
857
- );
858
- return true;
859
- }
860
- function writeInstalledPkgVersion(version, path) {
861
- const target = path ?? defaultConfigPath();
862
- const raw = readRaw(target);
863
- const updated = { ...raw, installed_pkg_version: version };
864
- secureAtomicWrite(target, JSON.stringify(updated, null, 2) + "\n");
1060
+ return { detail: String(parsed), responseBody: parsed };
865
1061
  }
866
1062
 
867
1063
  export {
868
- setDebug,
869
- isDebug,
870
- debug,
871
- PACKAGE_VERSION,
872
- CtxdbError,
873
- HttpClient,
874
1064
  SUPPORTED_AGENTS,
1065
+ SUPPORTED_AGENT_CHOICES,
1066
+ AGENT_CHOICES,
875
1067
  isBuiltinAgent,
876
1068
  isAgentSlug,
877
1069
  agentHomeDir,
@@ -891,5 +1083,12 @@ export {
891
1083
  configuredAgents,
892
1084
  hasConfiguredAgent,
893
1085
  updateConfiguredDebug,
894
- writeInstalledPkgVersion
1086
+ writeInstalledPkgVersion,
1087
+ setDebug,
1088
+ isDebug,
1089
+ debug,
1090
+ PACKAGE_NAME,
1091
+ PACKAGE_VERSION,
1092
+ CtxdbError,
1093
+ HttpClient
895
1094
  };