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

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