@aliyunrds/ctxdb 1.0.8-beta.1 → 1.0.8-beta.2

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,1064 +3,1071 @@ import {
3
3
  DISTRIBUTION_MANIFEST
4
4
  } from "./chunk-R67JELM7.js";
5
5
 
6
- // src/lib/agents.ts
6
+ // src/lib/logger.ts
7
+ import { appendFileSync, mkdirSync } from "fs";
7
8
  import { homedir } from "os";
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);
15
- }
16
- function isAgentSlug(v) {
17
- return v === "default" || isBuiltinAgent(v);
18
- }
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
- }
39
- }
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
- };
58
- }
59
- return { supported: true, detail: `${agent} is supported on ${platform}.` };
9
+ import { dirname, join } from "path";
10
+ function logPath() {
11
+ return join(homedir(), ".ctxdb", "logs", "ctxdb.log");
60
12
  }
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 };
13
+ var _enabled = null;
14
+ function setDebug(enabled) {
15
+ _enabled = enabled;
66
16
  }
67
- function existsSyncAdapter(path) {
68
- try {
69
- return existsSync(path);
70
- } catch {
71
- return false;
72
- }
17
+ function isDebug() {
18
+ return _enabled === true;
73
19
  }
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;
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}`;
83
35
  }
84
- }
85
- function isExecutableAdapter(path, platform) {
36
+ line += "\n";
37
+ const p = logPath();
86
38
  try {
87
- if (!statSync(path).isFile()) return false;
88
- if (platform !== "win32") accessSync(path, constants.X_OK);
89
- return true;
39
+ mkdirSync(dirname(p), { recursive: true });
40
+ appendFileSync(p, line, "utf-8");
90
41
  } catch {
91
- return false;
92
- }
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
- }
113
42
  }
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
43
  }
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 });
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 };
142
60
  }
61
+ } catch {
143
62
  }
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
- }
63
+ dir = dirname2(dir);
184
64
  }
185
- return { agent: agentFromEnv(env), fellBack: true };
65
+ return { name: "@aliyunrds/ctxdb", version: "0.0.0" };
186
66
  }
67
+ var PACKAGE_IDENTITY = findPackageIdentity();
68
+ var PACKAGE_NAME = PACKAGE_IDENTITY.name;
69
+ var PACKAGE_VERSION = PACKAGE_IDENTITY.version;
187
70
 
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);
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";
240
77
  }
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;
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;
92
+ }
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;
109
+ }
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;
146
+ }
147
+ };
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;
179
+ }
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));
267
192
  }
268
- } else {
269
- fs.rename(temp, target);
270
- tempExists = false;
193
+ const s = qs.toString();
194
+ if (s) url = `${url}?${s}`;
271
195
  }
272
- } finally {
273
- if (fd !== null) {
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)`);
202
+ }
203
+ const controller = new AbortController();
204
+ const timer = setTimeout(() => controller.abort(), effectiveTimeout);
205
+ try {
206
+ let resp;
274
207
  try {
275
- fs.close(fd);
276
- } catch {
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}`);
277
225
  }
278
- }
279
- if (tempExists && fs.exists(temp)) {
226
+ if (resp.status === 204) {
227
+ if (dbg) debug("http", `\u2190 ${method} ${path} 204 (${Date.now() - t0}ms)`);
228
+ return {};
229
+ }
230
+ let text;
280
231
  try {
281
- fs.unlink(temp);
282
- } catch {
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);
283
254
  }
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);
284
260
  }
285
261
  }
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`);
262
+ // ---------- Convenience verbs ----------
263
+ get(path, params) {
264
+ return this.doRequest("GET", path, { params });
322
265
  }
323
- let url;
324
- try {
325
- url = new URL(value);
326
- } catch {
327
- throw new Error(`credentials: ${field} must be a valid URL`);
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
+ });
328
274
  }
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`);
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
+ });
331
283
  }
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");
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
+ });
337
292
  }
338
- const raw = value;
339
- if (raw.kind !== "api-key") {
340
- throw new Error(`credentials: unsupported record kind ${String(raw.kind)}`);
293
+ delete(path, params) {
294
+ return this.doRequest("DELETE", path, { params });
341
295
  }
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");
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
+ });
346
310
  }
347
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
348
- throw new Error("credentials: api-key metadata must be an object");
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
+ });
349
327
  }
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");
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
+ });
354
337
  }
355
- if (meta.authorization_method !== "browser-loopback" && meta.authorization_method !== "device-code") {
356
- throw new Error("credentials: authorization_method is invalid");
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);
357
343
  }
358
- if (typeof meta.issued_at !== "string" || !Number.isFinite(Date.parse(meta.issued_at))) {
359
- throw new Error("credentials: issued_at is invalid");
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);
360
347
  }
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
- };
348
+ return fd;
373
349
  }
374
- function parseDocument(text) {
375
- let raw;
350
+ function maybeJson(text) {
376
351
  try {
377
- raw = JSON.parse(text);
352
+ return JSON.parse(text);
378
353
  } catch {
379
- throw new Error("credentials: credentials.json is not valid JSON");
380
- }
381
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
382
- throw new Error("credentials: document must be an object");
383
- }
384
- const document = raw;
385
- if (document.version !== DOCUMENT_VERSION) {
386
- throw new Error(`credentials: unsupported document version ${String(document.version)}`);
387
- }
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]}`);
354
+ return text;
395
355
  }
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
356
  }
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)`);
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 };
407
364
  }
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];
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
+ };
386
+ }
387
+ return { detail: String(parsed), responseBody: parsed };
416
388
  }
417
389
 
418
- // src/lib/config.ts
419
- import {
420
- resolveDebugPolicy
421
- } from "@aliyunrds/ctxdb-shared";
422
- function defaultConfigPath() {
423
- return join3(homedir3(), ".ctxdb", "ctxdb.json");
424
- }
425
- var DEFAULT_CONFIG_PATH = join3(homedir3(), ".ctxdb", "ctxdb.json");
426
- function configDir() {
427
- return join3(homedir3(), ".ctxdb");
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
+ var SUPPORTED_AGENT_CHOICES = SUPPORTED_AGENTS.join("|");
396
+ var AGENT_CHOICES = [...SUPPORTED_AGENTS, "default"].join("|");
397
+ function isBuiltinAgent(v) {
398
+ return typeof v === "string" && SUPPORTED_AGENTS.includes(v);
428
399
  }
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);
400
+ function isAgentSlug(v) {
401
+ return v === "default" || isBuiltinAgent(v);
437
402
  }
438
- function resolveConfigAgent(options = {}, preloadedRaw) {
439
- if (isAgentSlug(options.agent)) return options.agent;
440
- return agentFromEnv(options.env);
403
+ var isAgent = isAgentSlug;
404
+ function agentHomeDir(agent, home = homedir2()) {
405
+ switch (agent) {
406
+ case "qoder":
407
+ return join3(home, ".qoder");
408
+ case "qoderwork":
409
+ return join3(home, ".qoderwork");
410
+ case "qwenwork":
411
+ return join3(home, ".qwenwork");
412
+ case "codex":
413
+ return join3(home, ".codex");
414
+ case "claude":
415
+ return join3(home, ".claude");
416
+ case "opencode":
417
+ return join3(home, ".config", "opencode");
418
+ case "hermes":
419
+ return join3(home, ".hermes");
420
+ case "workbuddy":
421
+ return join3(home, ".workbuddy-ai");
422
+ }
441
423
  }
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;
424
+ var AGENT_VARIANT_HOMES = {
425
+ qoder: [".qoder-cn"],
426
+ qoderwork: [".qoderworkcn"],
427
+ qwenwork: [".qwenworkcn"]
428
+ };
429
+ function agentHomeDirs(agent, home = homedir2()) {
430
+ const primary = agentHomeDir(agent, home);
431
+ const variants = (AGENT_VARIANT_HOMES[agent] ?? []).map(
432
+ (name) => join3(home, name)
433
+ );
434
+ return [primary, ...variants];
446
435
  }
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;
436
+ function agentPlatformSupport(agent, platform = process.platform) {
437
+ if (agent === "hermes" && platform === "win32") {
438
+ return {
439
+ supported: false,
440
+ detail: "Hermes integration currently supports macOS/Linux only; Windows Hermes is not yet supported."
441
+ };
442
+ }
443
+ return { supported: true, detail: `${agent} is supported on ${platform}.` };
451
444
  }
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);
445
+ function inspectAgentHomes(agent, options = {}) {
446
+ const exists = options.exists ?? existsSyncAdapter;
447
+ const candidates = agentHomeDirs(agent, options.home);
448
+ const existing = candidates.filter(exists);
449
+ return { exists: existing.length > 0, existing, candidates };
456
450
  }
457
- function coerceKbCatalogInjection(v) {
458
- if (v === "session_start" || v === "user_prompt_submit" || v === "off") {
459
- return v;
451
+ function existsSyncAdapter(path) {
452
+ try {
453
+ return existsSync(path);
454
+ } catch {
455
+ return false;
460
456
  }
461
- return DEFAULT_KB_CATALOG_INJECTION;
462
457
  }
463
- function readRaw(path) {
464
- if (!existsSync4(path)) return {};
465
- try {
466
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
467
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
468
- return parsed;
469
- }
458
+ var EXECUTABLE_AGENT_NAMES = {
459
+ opencode: "opencode",
460
+ hermes: "hermes"
461
+ };
462
+ function isDirectoryAdapter(path) {
463
+ try {
464
+ return statSync(path).isDirectory();
470
465
  } catch {
466
+ return false;
471
467
  }
472
- return {};
473
468
  }
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
- }
469
+ function isExecutableAdapter(path, platform) {
470
+ try {
471
+ if (!statSync(path).isFile()) return false;
472
+ if (platform !== "win32") accessSync(path, constants.X_OK);
473
+ return true;
474
+ } catch {
475
+ return false;
481
476
  }
482
- return {};
483
- }
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);
487
477
  }
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
- });
478
+ function envValue(env, name, platform) {
479
+ if (env[name] !== void 0) return env[name];
480
+ if (platform !== "win32") return void 0;
481
+ const entry = Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase());
482
+ return entry?.[1];
509
483
  }
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;
484
+ function resolveAgentExecutable(command, options = {}) {
485
+ const platform = options.platform ?? process.platform;
486
+ const env = options.env ?? process.env;
487
+ const isExecutable = options.isExecutable ?? isExecutableAdapter;
488
+ const pathValue = envValue(env, "PATH", platform) ?? "";
489
+ const pathSeparator = platform === "win32" ? ";" : delimiter;
490
+ const pathEntries = pathValue.split(pathSeparator).map((entry) => entry.trim().replace(/^"(.*)"$/, "$1")).filter(Boolean);
491
+ const suffixes = platform === "win32" ? (envValue(env, "PATHEXT", platform) ?? ".COM;.EXE;.BAT;.CMD").split(";").map((suffix) => suffix.trim()).filter(Boolean) : [""];
492
+ for (const directory of pathEntries) {
493
+ for (const suffix of suffixes) {
494
+ const candidate = join3(directory, `${command}${suffix}`);
495
+ if (isExecutable(candidate, platform)) return candidate;
496
+ }
497
+ }
498
+ return void 0;
517
499
  }
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);
500
+ function displayHomePath(home, path) {
501
+ if (path === home) return "~";
502
+ return path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path;
525
503
  }
526
- function load(options = {}) {
527
- const path = options.path ?? defaultConfigPath();
504
+ function detectInstalledAgents(options = {}) {
505
+ const home = options.home ?? homedir2();
506
+ const platform = options.platform ?? process.platform;
528
507
  const env = options.env ?? process.env;
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 {
538
- }
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;
508
+ const isDirectory = options.isDirectory ?? isDirectoryAdapter;
509
+ const observations = [];
510
+ for (const agent of SUPPORTED_AGENTS) {
511
+ const evidence = [];
512
+ for (const candidate of agentHomeDirs(agent, home)) {
513
+ if (isDirectory(candidate)) {
514
+ evidence.push({ kind: "home", value: displayHomePath(home, candidate) });
515
+ }
546
516
  }
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
- }
517
+ const executableName = EXECUTABLE_AGENT_NAMES[agent];
518
+ if (executableName) {
519
+ const executable = resolveAgentExecutable(executableName, {
520
+ platform,
521
+ env,
522
+ isExecutable: options.isExecutable
523
+ });
524
+ if (executable) {
525
+ evidence.push({ kind: "executable", value: executableName });
557
526
  }
558
527
  }
528
+ if (evidence.length === 0) {
529
+ observations.push({
530
+ agent,
531
+ selected: false,
532
+ reason: "not-detected",
533
+ evidence
534
+ });
535
+ continue;
536
+ }
537
+ const support = agentPlatformSupport(agent, platform);
538
+ if (!support.supported) {
539
+ observations.push({
540
+ agent,
541
+ selected: false,
542
+ reason: "unsupported-platform",
543
+ evidence,
544
+ detail: support.detail
545
+ });
546
+ continue;
547
+ }
548
+ observations.push({ agent, selected: true, reason: "detected", evidence });
559
549
  }
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) {
571
550
  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
551
+ selected: observations.filter((item) => item.selected).map((item) => item.agent),
552
+ observations
586
553
  };
587
554
  }
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 {
612
- }
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 };
555
+ function agentFromEnv(env = process.env) {
556
+ return isAgentSlug(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "default";
618
557
  }
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
558
+ function agentFromArgvWithFallback(argv = process.argv.slice(2), env = process.env) {
559
+ for (let i = 0; i < argv.length; i++) {
560
+ const tok = argv[i];
561
+ if (tok === "--agent" && isAgentSlug(argv[i + 1])) {
562
+ return { agent: argv[i + 1], fellBack: false };
563
+ }
564
+ if (tok.startsWith("--agent=")) {
565
+ const raw = tok.slice("--agent=".length);
566
+ if (isAgentSlug(raw)) return { agent: raw, fellBack: false };
633
567
  }
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;
648
568
  }
649
- const profile = raw.agents[agent];
650
- return Boolean(
651
- profile && typeof profile === "object" && !Array.isArray(profile)
652
- );
569
+ return { agent: agentFromEnv(env), fellBack: true };
653
570
  }
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;
571
+
572
+ // src/lib/config.ts
573
+ import { readFileSync as readFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
574
+ import { homedir as homedir4 } from "os";
575
+ import { dirname as dirname5, join as join5 } from "path";
576
+
577
+ // src/lib/secure-file.ts
578
+ import {
579
+ chmodSync,
580
+ closeSync,
581
+ copyFileSync,
582
+ existsSync as existsSync2,
583
+ fsyncSync,
584
+ mkdirSync as mkdirSync2,
585
+ openSync,
586
+ renameSync,
587
+ unlinkSync,
588
+ writeFileSync
589
+ } from "fs";
590
+ import { randomBytes } from "crypto";
591
+ import { dirname as dirname3 } from "path";
592
+ var nodeSecureFileSystem = {
593
+ exists: existsSync2,
594
+ mkdir: (path, options) => {
595
+ mkdirSync2(path, options);
596
+ },
597
+ chmod: chmodSync,
598
+ open: openSync,
599
+ write: (fd, data) => {
600
+ writeFileSync(fd, data);
601
+ },
602
+ fsync: fsyncSync,
603
+ close: closeSync,
604
+ copy: copyFileSync,
605
+ rename: renameSync,
606
+ unlink: unlinkSync
607
+ };
608
+ function configBackupPath(target) {
609
+ return `${target}.bak`;
610
+ }
611
+ function secureAtomicWrite(target, content, options = {}) {
612
+ const fs = options.fs ?? nodeSecureFileSystem;
613
+ const platform = options.platform ?? process.platform;
614
+ const posix = platform !== "win32";
615
+ const dir = dirname3(target);
616
+ const backup = configBackupPath(target);
617
+ const suffix = options.tempSuffix ?? `${process.pid}.${randomBytes(6).toString("hex")}`;
618
+ const temp = `${target}.${suffix}.tmp`;
619
+ const hadTarget = fs.exists(target);
620
+ fs.mkdir(dir, { recursive: true, mode: 448 });
621
+ if (posix) {
622
+ fs.chmod(dir, 448);
623
+ if (hadTarget) fs.chmod(target, 384);
659
624
  }
660
- const agents = { ...raw.agents };
661
- const profile = agents[agent];
662
- if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
663
- return false;
625
+ let fd = null;
626
+ let tempExists = false;
627
+ try {
628
+ fd = fs.open(temp, "wx", 384);
629
+ tempExists = true;
630
+ fs.write(fd, Buffer.from(content, "utf-8"));
631
+ fs.fsync(fd);
632
+ fs.close(fd);
633
+ fd = null;
634
+ if (posix) fs.chmod(temp, 384);
635
+ if (hadTarget && options.backup !== false) {
636
+ if (platform === "win32") {
637
+ if (fs.exists(backup)) fs.unlink(backup);
638
+ fs.rename(target, backup);
639
+ try {
640
+ fs.rename(temp, target);
641
+ tempExists = false;
642
+ } catch (error) {
643
+ fs.rename(backup, target);
644
+ throw error;
645
+ }
646
+ } else {
647
+ fs.copy(target, backup);
648
+ fs.chmod(backup, 384);
649
+ fs.rename(temp, target);
650
+ tempExists = false;
651
+ }
652
+ } else {
653
+ fs.rename(temp, target);
654
+ tempExists = false;
655
+ }
656
+ } finally {
657
+ if (fd !== null) {
658
+ try {
659
+ fs.close(fd);
660
+ } catch {
661
+ }
662
+ }
663
+ if (tempExists && fs.exists(temp)) {
664
+ try {
665
+ fs.unlink(temp);
666
+ } catch {
667
+ }
668
+ }
664
669
  }
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");
677
670
  }
678
671
 
679
- // src/lib/logger.ts
680
- import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
681
- import { homedir as homedir4 } from "os";
672
+ // src/lib/secrets.ts
673
+ import { createHmac, randomBytes as randomBytes2 } from "crypto";
674
+ var PROCESS_FINGERPRINT_KEY = randomBytes2(32);
675
+
676
+ // src/credentials/local-credential-provider.ts
677
+ import {
678
+ closeSync as closeSync2,
679
+ existsSync as existsSync3,
680
+ mkdirSync as mkdirSync3,
681
+ openSync as openSync2,
682
+ readFileSync as readFileSync2,
683
+ statSync as statSync2,
684
+ unlinkSync as unlinkSync2,
685
+ writeFileSync as writeFileSync2
686
+ } from "fs";
687
+ import { homedir as homedir3 } from "os";
682
688
  import { dirname as dirname4, join as join4 } from "path";
683
- function logPath() {
684
- return join4(homedir4(), ".ctxdb", "logs", "ctxdb.log");
685
- }
686
- var _enabled = null;
687
- function setDebug(enabled) {
688
- _enabled = enabled;
689
+ import { setTimeout as delay } from "timers/promises";
690
+
691
+ // src/credentials/types.ts
692
+ var ACTIVE_CONTEXTDB_CREDENTIAL = "contextdb/active";
693
+
694
+ // src/credentials/local-credential-provider.ts
695
+ var DOCUMENT_VERSION = 1;
696
+ var STALE_LOCK_MS = 10 * 60 * 1e3;
697
+ function defaultCredentialsPath() {
698
+ return join4(homedir3(), ".ctxdb", "credentials.json");
689
699
  }
690
- function isDebug() {
691
- return _enabled === true;
700
+ function emptyDocument() {
701
+ return { version: DOCUMENT_VERSION, records: {} };
692
702
  }
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}`;
703
+ function normalizeOrigin(value, field) {
704
+ if (typeof value !== "string" || !value) {
705
+ throw new Error(`credentials: ${field} must be a non-empty URL`);
708
706
  }
709
- line += "\n";
710
- const p = logPath();
707
+ let url;
711
708
  try {
712
- mkdirSync3(dirname4(p), { recursive: true });
713
- appendFileSync(p, line, "utf-8");
709
+ url = new URL(value);
714
710
  } catch {
711
+ throw new Error(`credentials: ${field} must be a valid URL`);
715
712
  }
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
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
714
+ throw new Error(`credentials: ${field} must be an HTTP(S) origin`);
737
715
  }
738
- return { name: "@aliyunrds/ctxdb", version: "0.0.0" };
716
+ return url.toString().replace(/\/$/, "");
739
717
  }
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";
718
+ function parseRecord(value) {
719
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
720
+ throw new Error("credentials: record must be an object");
750
721
  }
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;
722
+ const raw = value;
723
+ if (raw.kind !== "api-key") {
724
+ throw new Error(`credentials: unsupported record kind ${String(raw.kind)}`);
765
725
  }
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;
726
+ const payload = raw.payload;
727
+ const metadata = raw.metadata;
728
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
729
+ throw new Error("credentials: api-key payload must be an object");
782
730
  }
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
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
732
+ throw new Error("credentials: api-key metadata must be an object");
801
733
  }
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;
734
+ const body = payload;
735
+ const meta = metadata;
736
+ if (typeof body.api_key !== "string" || !body.api_key) {
737
+ throw new Error("credentials: api_key must be a non-empty string");
819
738
  }
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");
739
+ if (meta.authorization_method !== "browser-loopback" && meta.authorization_method !== "device-code") {
740
+ throw new Error("credentials: authorization_method is invalid");
741
+ }
742
+ if (typeof meta.issued_at !== "string" || !Number.isFinite(Date.parse(meta.issued_at))) {
743
+ throw new Error("credentials: issued_at is invalid");
744
+ }
745
+ return {
746
+ kind: "api-key",
747
+ payload: {
748
+ apiKey: body.api_key,
749
+ baseUrl: normalizeOrigin(body.base_url, "base_url"),
750
+ loginServer: normalizeOrigin(body.login_server, "login_server")
751
+ },
752
+ metadata: {
753
+ authorizationMethod: meta.authorization_method,
754
+ issuedAt: meta.issued_at
833
755
  }
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;
756
+ };
757
+ }
758
+ function parseDocument(text) {
759
+ let raw;
760
+ try {
761
+ raw = JSON.parse(text);
762
+ } catch {
763
+ throw new Error("credentials: credentials.json is not valid JSON");
841
764
  }
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;
765
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
766
+ throw new Error("credentials: document must be an object");
852
767
  }
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}`;
768
+ const document = raw;
769
+ if (document.version !== DOCUMENT_VERSION) {
770
+ throw new Error(`credentials: unsupported document version ${String(document.version)}`);
771
+ }
772
+ if (!document.records || typeof document.records !== "object" || Array.isArray(document.records)) {
773
+ throw new Error("credentials: records must be an object");
774
+ }
775
+ const records = document.records;
776
+ const unknown = Object.keys(records).filter((key) => key !== ACTIVE_CONTEXTDB_CREDENTIAL);
777
+ if (unknown.length > 0) {
778
+ throw new Error(`credentials: unsupported record key ${unknown[0]}`);
779
+ }
780
+ const active = records[ACTIVE_CONTEXTDB_CREDENTIAL];
781
+ return {
782
+ version: DOCUMENT_VERSION,
783
+ records: active === void 0 ? {} : { [ACTIVE_CONTEXTDB_CREDENTIAL]: parseRecord(active) }
784
+ };
785
+ }
786
+ function assertOwnerOnly(path) {
787
+ if (process.platform === "win32" || !existsSync3(path)) return;
788
+ const mode = statSync2(path).mode & 511;
789
+ if ((mode & 63) !== 0) {
790
+ throw new Error(`credentials: ${path} must be owner-only (run chmod 600)`);
791
+ }
792
+ }
793
+ function readDocument(path) {
794
+ if (!existsSync3(path)) return emptyDocument();
795
+ assertOwnerOnly(path);
796
+ return parseDocument(readFileSync2(path, "utf8"));
797
+ }
798
+ function readActiveCredentialSync(path = defaultCredentialsPath()) {
799
+ return readDocument(path).records[ACTIVE_CONTEXTDB_CREDENTIAL];
800
+ }
801
+
802
+ // src/lib/config.ts
803
+ import {
804
+ resolveDebugPolicy
805
+ } from "@aliyunrds/ctxdb-shared";
806
+ function defaultConfigPath() {
807
+ return join5(homedir4(), ".ctxdb", "ctxdb.json");
808
+ }
809
+ var DEFAULT_CONFIG_PATH = join5(homedir4(), ".ctxdb", "ctxdb.json");
810
+ function configDir() {
811
+ return join5(homedir4(), ".ctxdb");
812
+ }
813
+ var DEFAULT_BASE_URL = "https://context-database.aliyuncs.com";
814
+ var DEFAULT_USER_ID = "default";
815
+ var DEFAULT_TOP_K = 5;
816
+ var DEFAULT_THRESHOLD = 0.4;
817
+ var DEFAULT_KNOWLEDGE_TOP_K = 6;
818
+ var DEFAULT_KB_CATALOG_INJECTION = "session_start";
819
+ function isComplete(cfg) {
820
+ return Boolean(cfg.apiKey && cfg.baseUrl);
821
+ }
822
+ function resolveConfigAgent(options = {}, preloadedRaw) {
823
+ if (isAgentSlug(options.agent)) return options.agent;
824
+ return agentFromEnv(options.env);
825
+ }
826
+ function coerceInt(v, fallback) {
827
+ if (v === null || v === void 0 || v === "") return fallback;
828
+ const n = typeof v === "number" ? v : Number(v);
829
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
830
+ }
831
+ function coerceFloat(v, fallback) {
832
+ if (v === null || v === void 0 || v === "") return fallback;
833
+ const n = typeof v === "number" ? v : Number(v);
834
+ return Number.isFinite(n) ? n : fallback;
835
+ }
836
+ function coerceBool(v, fallback) {
837
+ if (typeof v === "boolean") return v;
838
+ if (v === void 0 || v === null) return fallback;
839
+ return Boolean(v);
840
+ }
841
+ function coerceKbCatalogInjection(v) {
842
+ if (v === "session_start" || v === "user_prompt_submit" || v === "off") {
843
+ return v;
844
+ }
845
+ return DEFAULT_KB_CATALOG_INJECTION;
846
+ }
847
+ function readRaw(path) {
848
+ if (!existsSync4(path)) return {};
849
+ try {
850
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
851
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
852
+ return parsed;
868
853
  }
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)`);
854
+ } catch {
855
+ }
856
+ return {};
857
+ }
858
+ function agentRawFromFile(raw, agent) {
859
+ const agents = raw.agents;
860
+ if (agents && typeof agents === "object" && !Array.isArray(agents)) {
861
+ const candidate = agents[agent];
862
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
863
+ return candidate;
875
864
  }
876
- const controller = new AbortController();
877
- const timer = setTimeout(() => controller.abort(), effectiveTimeout);
865
+ }
866
+ return {};
867
+ }
868
+ function isV2Schema(raw) {
869
+ if (Object.keys(raw).length === 0) return true;
870
+ return raw.version === 2 && raw.agents !== null && typeof raw.agents === "object" && !Array.isArray(raw.agents);
871
+ }
872
+ function configFromDisk(raw) {
873
+ const debugConfigured = coerceBool(raw.debug, false);
874
+ return applyDebugPolicy({
875
+ apiKey: typeof raw.api_key === "string" && raw.api_key ? raw.api_key : null,
876
+ baseUrl: typeof raw.base_url === "string" && raw.base_url ? String(raw.base_url).replace(/\/+$/, "") : DEFAULT_BASE_URL,
877
+ userId: typeof raw.user_id === "string" && raw.user_id ? raw.user_id : DEFAULT_USER_ID,
878
+ agentId: typeof raw.agent_id === "string" && raw.agent_id ? raw.agent_id : null,
879
+ appId: typeof raw.app_id === "string" && raw.app_id ? raw.app_id : null,
880
+ autoCapture: coerceBool(raw.auto_capture, true),
881
+ autoRecall: coerceBool(raw.auto_recall, true),
882
+ warmupRecall: coerceBool(raw.warmup_recall, false),
883
+ recallKnowledge: coerceBool(raw.recall_knowledge, false),
884
+ topK: coerceInt(raw.top_k, DEFAULT_TOP_K),
885
+ threshold: coerceFloat(raw.threshold, DEFAULT_THRESHOLD),
886
+ knowledgeTopK: coerceInt(raw.knowledge_top_k, DEFAULT_KNOWLEDGE_TOP_K),
887
+ debug: debugConfigured,
888
+ debugConfigured,
889
+ debugForced: false,
890
+ debugReason: null,
891
+ kbCatalogInjection: coerceKbCatalogInjection(raw.kb_catalog_injection)
892
+ });
893
+ }
894
+ function applyDebugPolicy(cfg) {
895
+ const policy = resolveDebugPolicy(cfg.debugConfigured, cfg.baseUrl);
896
+ cfg.debug = policy.debug;
897
+ cfg.debugConfigured = policy.debugConfigured;
898
+ cfg.debugForced = policy.debugForced;
899
+ cfg.debugReason = policy.debugReason;
900
+ return cfg;
901
+ }
902
+ function applyEnv(cfg, env) {
903
+ if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
904
+ if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
905
+ if (env.CTXDB_USER_ID) cfg.userId = env.CTXDB_USER_ID;
906
+ if (env.CTXDB_AGENT_ID) cfg.agentId = env.CTXDB_AGENT_ID;
907
+ if (env.CTXDB_APP_ID) cfg.appId = env.CTXDB_APP_ID;
908
+ return applyDebugPolicy(cfg);
909
+ }
910
+ function load(options = {}) {
911
+ const path = options.path ?? defaultConfigPath();
912
+ const env = options.env ?? process.env;
913
+ const raw = readRaw(path);
914
+ const agent = resolveConfigAgent({ ...options, path, env }, raw);
915
+ if (!isV2Schema(raw)) {
878
916
  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);
917
+ process.stderr.write(
918
+ `[ctxdb] config schema invalid at ${path}, treating as first-time install
919
+ `
920
+ );
921
+ } catch {
922
+ }
923
+ const cfg2 = configFromDisk({});
924
+ const managed2 = !DISTRIBUTION_MANIFEST.capabilities.managedCredentials || options.managedCredentials === false ? void 0 : readActiveCredentialSync(
925
+ options.credentialsPath ?? join5(dirname5(path), "credentials.json")
926
+ );
927
+ if (managed2) {
928
+ cfg2.apiKey = managed2.payload.apiKey;
929
+ cfg2.baseUrl = managed2.payload.baseUrl;
930
+ }
931
+ return applyEnv(cfg2, env);
932
+ }
933
+ const agentRaw = agentRawFromFile(raw, agent);
934
+ if (agent !== "default") {
935
+ const defaultRaw = agentRawFromFile(raw, "default");
936
+ if (Object.keys(defaultRaw).length > 0) {
937
+ for (const [k, v] of Object.entries(defaultRaw)) {
938
+ if (!(k in agentRaw)) {
939
+ agentRaw[k] = v;
925
940
  }
926
- throw new CtxdbHttpError(resp.status, parsedError.detail, parsedError);
927
941
  }
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);
933
942
  }
934
943
  }
935
- // ---------- Convenience verbs ----------
936
- get(path, params) {
937
- return this.doRequest("GET", path, { params });
938
- }
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
- });
947
- }
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
- });
956
- }
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
- });
965
- }
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
- });
944
+ const cfg = configFromDisk(agentRaw);
945
+ const managed = !DISTRIBUTION_MANIFEST.capabilities.managedCredentials || options.managedCredentials === false ? void 0 : readActiveCredentialSync(
946
+ options.credentialsPath ?? join5(dirname5(path), "credentials.json")
947
+ );
948
+ if (managed) {
949
+ cfg.apiKey = managed.payload.apiKey;
950
+ cfg.baseUrl = managed.payload.baseUrl;
983
951
  }
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
- });
952
+ return applyEnv(cfg, env);
953
+ }
954
+ function configToDisk(cfg) {
955
+ return {
956
+ api_key: cfg.apiKey,
957
+ base_url: cfg.baseUrl,
958
+ user_id: cfg.userId,
959
+ agent_id: cfg.agentId,
960
+ app_id: cfg.appId,
961
+ auto_capture: cfg.autoCapture,
962
+ auto_recall: cfg.autoRecall,
963
+ warmup_recall: cfg.warmupRecall,
964
+ recall_knowledge: cfg.recallKnowledge,
965
+ top_k: cfg.topK,
966
+ threshold: cfg.threshold,
967
+ knowledge_top_k: cfg.knowledgeTopK,
968
+ debug: cfg.debugConfigured,
969
+ kb_catalog_injection: cfg.kbCatalogInjection
970
+ };
971
+ }
972
+ function removeAgent(agent, path, options = {}) {
973
+ const target = path ?? defaultConfigPath();
974
+ if (!existsSync4(target)) {
975
+ return { removed: false, remainingAgents: [], fileDeleted: false };
1000
976
  }
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
- });
977
+ const raw = readRaw(target);
978
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
979
+ return { removed: false, remainingAgents: [], fileDeleted: false };
1010
980
  }
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);
981
+ const agents = { ...raw.agents };
982
+ if (!(agent in agents)) {
983
+ return {
984
+ removed: false,
985
+ remainingAgents: Object.keys(agents),
986
+ fileDeleted: false
987
+ };
1016
988
  }
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);
989
+ delete agents[agent];
990
+ const remaining = Object.keys(agents);
991
+ if (remaining.length === 0 && !options.keepEmptyShell) {
992
+ try {
993
+ unlinkSync3(target);
994
+ return { removed: true, remainingAgents: [], fileDeleted: true };
995
+ } catch {
996
+ }
1020
997
  }
1021
- return fd;
998
+ const onDisk = { ...raw, version: 2, agents };
999
+ delete onDisk.default_agent;
1000
+ secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
1001
+ return { removed: true, remainingAgents: remaining, fileDeleted: false };
1022
1002
  }
1023
- function maybeJson(text) {
1024
- try {
1025
- return JSON.parse(text);
1026
- } catch {
1027
- return text;
1003
+ function save(cfg, path, options = {}) {
1004
+ const target = path ?? defaultConfigPath();
1005
+ const agent = resolveConfigAgent(options);
1006
+ const raw = readRaw(target);
1007
+ const validRaw = isV2Schema(raw) ? raw : {};
1008
+ const existingAgents = validRaw.agents && typeof validRaw.agents === "object" && !Array.isArray(validRaw.agents) ? { ...validRaw.agents } : {};
1009
+ const serialized = configToDisk(cfg);
1010
+ if (options.omitApiKey) delete serialized.api_key;
1011
+ const onDisk = {
1012
+ ...validRaw,
1013
+ version: 2,
1014
+ agents: {
1015
+ ...existingAgents,
1016
+ [agent]: serialized
1017
+ }
1018
+ };
1019
+ delete onDisk.default_agent;
1020
+ secureAtomicWrite(target, JSON.stringify(onDisk, null, 2) + "\n");
1021
+ }
1022
+ function configuredAgents(path) {
1023
+ const target = path ?? defaultConfigPath();
1024
+ const raw = readRaw(target);
1025
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object") return [];
1026
+ return Object.keys(raw.agents).filter(isAgent);
1027
+ }
1028
+ function hasConfiguredAgent(agent, path) {
1029
+ const raw = readRaw(path ?? defaultConfigPath());
1030
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
1031
+ return false;
1028
1032
  }
1033
+ const profile = raw.agents[agent];
1034
+ return Boolean(
1035
+ profile && typeof profile === "object" && !Array.isArray(profile)
1036
+ );
1029
1037
  }
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 };
1038
+ function updateConfiguredDebug(agent, configured, path) {
1039
+ const target = path ?? defaultConfigPath();
1040
+ const raw = readRaw(target);
1041
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
1042
+ return false;
1037
1043
  }
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
- };
1044
+ const agents = { ...raw.agents };
1045
+ const profile = agents[agent];
1046
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
1047
+ return false;
1059
1048
  }
1060
- return { detail: String(parsed), responseBody: parsed };
1049
+ agents[agent] = { ...profile, debug: configured };
1050
+ secureAtomicWrite(
1051
+ target,
1052
+ JSON.stringify({ ...raw, agents }, null, 2) + "\n"
1053
+ );
1054
+ return true;
1055
+ }
1056
+ function writeInstalledPkgVersion(version, path) {
1057
+ const target = path ?? defaultConfigPath();
1058
+ const raw = readRaw(target);
1059
+ const updated = { ...raw, installed_pkg_version: version };
1060
+ secureAtomicWrite(target, JSON.stringify(updated, null, 2) + "\n");
1061
1061
  }
1062
1062
 
1063
1063
  export {
1064
+ setDebug,
1065
+ isDebug,
1066
+ debug,
1067
+ PACKAGE_NAME,
1068
+ PACKAGE_VERSION,
1069
+ CtxdbError,
1070
+ HttpClient,
1064
1071
  SUPPORTED_AGENTS,
1065
1072
  SUPPORTED_AGENT_CHOICES,
1066
1073
  AGENT_CHOICES,
@@ -1083,12 +1090,5 @@ export {
1083
1090
  configuredAgents,
1084
1091
  hasConfiguredAgent,
1085
1092
  updateConfiguredDebug,
1086
- writeInstalledPkgVersion,
1087
- setDebug,
1088
- isDebug,
1089
- debug,
1090
- PACKAGE_NAME,
1091
- PACKAGE_VERSION,
1092
- CtxdbError,
1093
- HttpClient
1093
+ writeInstalledPkgVersion
1094
1094
  };