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