@aliyunrds/ctxdb 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/http-client.ts
4
+ var DEFAULT_TIMEOUT_MS = 3e4;
5
+ var CtxdbError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "CtxdbError";
9
+ }
10
+ };
11
+ var AuthError = class extends CtxdbError {
12
+ constructor(message = "Unauthorized (HTTP 401) \u2014 check api_key") {
13
+ super(message);
14
+ this.name = "AuthError";
15
+ }
16
+ };
17
+ var NotFoundError = class extends CtxdbError {
18
+ path;
19
+ constructor(path) {
20
+ super(`Not found: ${path}`);
21
+ this.name = "NotFoundError";
22
+ this.path = path;
23
+ }
24
+ };
25
+ var APIError = class extends CtxdbError {
26
+ path;
27
+ detail;
28
+ constructor(path, detail) {
29
+ super(`API error at ${path}: ${detail}`);
30
+ this.name = "APIError";
31
+ this.path = path;
32
+ this.detail = detail;
33
+ }
34
+ };
35
+ var CtxdbHttpError = class extends CtxdbError {
36
+ status;
37
+ detail;
38
+ constructor(status, detail) {
39
+ super(`HTTP ${status}: ${detail}`);
40
+ this.name = "CtxdbHttpError";
41
+ this.status = status;
42
+ this.detail = detail;
43
+ }
44
+ };
45
+ var HttpClient = class {
46
+ baseUrl;
47
+ apiKey;
48
+ timeoutMs;
49
+ userAgent;
50
+ extraHeaders;
51
+ fetchImpl;
52
+ constructor(opts) {
53
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
54
+ this.apiKey = opts.apiKey;
55
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
56
+ this.userAgent = opts.userAgent ?? "ctxdb-cli/0.0.1";
57
+ this.extraHeaders = { ...opts.extraHeaders ?? {} };
58
+ const f = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
59
+ this.fetchImpl = f;
60
+ }
61
+ buildHeaders(contentType) {
62
+ const h = {
63
+ Authorization: `Token ${this.apiKey}`,
64
+ "User-Agent": this.userAgent,
65
+ ...this.extraHeaders
66
+ };
67
+ if (contentType) h["Content-Type"] = contentType;
68
+ return h;
69
+ }
70
+ async doRequest(method, path, init = {}) {
71
+ let url = `${this.baseUrl}${path}`;
72
+ if (init.params) {
73
+ const qs = new URLSearchParams();
74
+ for (const [k, v] of Object.entries(init.params)) {
75
+ if (v !== void 0 && v !== null) qs.append(k, String(v));
76
+ }
77
+ const s = qs.toString();
78
+ if (s) url = `${url}?${s}`;
79
+ }
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
82
+ try {
83
+ let resp;
84
+ try {
85
+ resp = await this.fetchImpl(url, {
86
+ method,
87
+ headers: this.buildHeaders(init.contentType),
88
+ body: init.body,
89
+ signal: controller.signal
90
+ });
91
+ } catch (err) {
92
+ if (err?.name === "AbortError") {
93
+ throw new CtxdbError(`request timeout after ${this.timeoutMs}ms: ${url}`);
94
+ }
95
+ throw new CtxdbError(`network error contacting ${url}: ${err?.message ?? err}`);
96
+ }
97
+ if (resp.status === 204) return {};
98
+ let text;
99
+ try {
100
+ text = await resp.text();
101
+ } catch (err) {
102
+ if (err?.name === "AbortError") {
103
+ throw new CtxdbError(`request timeout after ${this.timeoutMs}ms (body read): ${url}`);
104
+ }
105
+ throw new CtxdbError(`network error reading body from ${url}: ${err?.message ?? err}`);
106
+ }
107
+ if (!resp.ok) {
108
+ const detail = extractErrorDetail(text, `HTTP ${resp.status}`);
109
+ if (resp.status === 401) throw new AuthError();
110
+ if (resp.status === 404) throw new NotFoundError(path);
111
+ if (resp.status === 400) throw new APIError(path, detail);
112
+ throw new CtxdbHttpError(resp.status, detail);
113
+ }
114
+ if (!text) return {};
115
+ return maybeJson(text);
116
+ } finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+ // ---------- Convenience verbs ----------
121
+ get(path, params) {
122
+ return this.doRequest("GET", path, { params });
123
+ }
124
+ postJson(path, body, params) {
125
+ return this.doRequest("POST", path, {
126
+ body: JSON.stringify(body ?? {}),
127
+ contentType: "application/json",
128
+ params
129
+ });
130
+ }
131
+ putJson(path, body) {
132
+ return this.doRequest("PUT", path, {
133
+ body: JSON.stringify(body),
134
+ contentType: "application/json"
135
+ });
136
+ }
137
+ delete(path, params) {
138
+ return this.doRequest("DELETE", path, { params });
139
+ }
140
+ /**
141
+ * POST multipart/form-data using runtime-native FormData.
142
+ *
143
+ * `fields` are appended as text values; `files` are appended as Blob
144
+ * with a filename. We intentionally do NOT pass a `Content-Type`
145
+ * header — when fetch sees a FormData body it sets the multipart
146
+ * boundary itself.
147
+ */
148
+ postMultipart(path, fields = {}, files = {}) {
149
+ const fd = new FormData();
150
+ for (const [name, value] of Object.entries(fields)) {
151
+ fd.append(name, value);
152
+ }
153
+ for (const [name, part] of Object.entries(files)) {
154
+ const blob = part.content instanceof Blob ? part.content : new Blob([part.content], { type: part.mimeType });
155
+ fd.append(name, blob, part.filename);
156
+ }
157
+ return this.doRequest("POST", path, { body: fd });
158
+ }
159
+ };
160
+ function maybeJson(text) {
161
+ try {
162
+ return JSON.parse(text);
163
+ } catch {
164
+ return text;
165
+ }
166
+ }
167
+ function extractErrorDetail(text, fallback) {
168
+ if (!text) return fallback;
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(text);
172
+ } catch {
173
+ return text || fallback;
174
+ }
175
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
176
+ const obj = parsed;
177
+ for (const k of ["detail", "message", "error"]) {
178
+ const v = obj[k];
179
+ if (typeof v === "string" && v) return v;
180
+ }
181
+ return JSON.stringify(parsed);
182
+ }
183
+ return String(parsed);
184
+ }
185
+
186
+ // src/lib/agents.ts
187
+ import { homedir } from "os";
188
+ import { join } from "path";
189
+ var SUPPORTED_AGENTS = ["qoder", "codex", "claude"];
190
+ function isAgent(v) {
191
+ return typeof v === "string" && SUPPORTED_AGENTS.includes(v);
192
+ }
193
+ function agentHomeDir(agent) {
194
+ switch (agent) {
195
+ case "qoder":
196
+ return join(homedir(), ".qoder");
197
+ case "codex":
198
+ return join(homedir(), ".codex");
199
+ case "claude":
200
+ return join(homedir(), ".claude");
201
+ }
202
+ }
203
+ function agentFromEnv(env = process.env) {
204
+ return isAgent(env.CTXDB_AGENT) ? env.CTXDB_AGENT : "qoder";
205
+ }
206
+ function agentFromArgvWithFallback(argv = process.argv.slice(2), env = process.env) {
207
+ for (let i = 0; i < argv.length; i++) {
208
+ const tok = argv[i];
209
+ if (tok === "--agent" && isAgent(argv[i + 1])) {
210
+ return { agent: argv[i + 1], fellBack: false };
211
+ }
212
+ if (tok.startsWith("--agent=")) {
213
+ const raw = tok.slice("--agent=".length);
214
+ if (isAgent(raw)) return { agent: raw, fellBack: false };
215
+ }
216
+ }
217
+ return { agent: agentFromEnv(env), fellBack: true };
218
+ }
219
+
220
+ // src/lib/config.ts
221
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
222
+ import { homedir as homedir2 } from "os";
223
+ import { dirname, join as join2 } from "path";
224
+ function defaultConfigPath() {
225
+ return join2(homedir2(), ".ctxdb", "ctxdb.json");
226
+ }
227
+ var DEFAULT_CONFIG_PATH = join2(homedir2(), ".ctxdb", "ctxdb.json");
228
+ var DEFAULT_BASE_URL = "https://context-database.aliyuncs.com";
229
+ var DEFAULT_USER_ID = "default";
230
+ var DEFAULT_TOP_K = 5;
231
+ var DEFAULT_THRESHOLD = 0.4;
232
+ var DEFAULT_KNOWLEDGE_TOP_K = 6;
233
+ function isComplete(cfg) {
234
+ return Boolean(cfg.apiKey && cfg.baseUrl);
235
+ }
236
+ function resolveConfigAgent(options = {}) {
237
+ if (isAgent(options.agent)) return options.agent;
238
+ return agentFromEnv(options.env);
239
+ }
240
+ function coerceInt(v, fallback) {
241
+ if (v === null || v === void 0 || v === "") return fallback;
242
+ const n = typeof v === "number" ? v : Number(v);
243
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
244
+ }
245
+ function coerceFloat(v, fallback) {
246
+ if (v === null || v === void 0 || v === "") return fallback;
247
+ const n = typeof v === "number" ? v : Number(v);
248
+ return Number.isFinite(n) ? n : fallback;
249
+ }
250
+ function coerceBool(v, fallback) {
251
+ if (typeof v === "boolean") return v;
252
+ if (v === void 0 || v === null) return fallback;
253
+ return Boolean(v);
254
+ }
255
+ function readRaw(path) {
256
+ if (!existsSync(path)) return {};
257
+ try {
258
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
259
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
260
+ return parsed;
261
+ }
262
+ } catch {
263
+ }
264
+ return {};
265
+ }
266
+ function agentRawFromFile(raw, agent) {
267
+ const agents = raw.agents;
268
+ if (agents && typeof agents === "object" && !Array.isArray(agents)) {
269
+ const candidate = agents[agent];
270
+ if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
271
+ return candidate;
272
+ }
273
+ }
274
+ return {};
275
+ }
276
+ function isV2Schema(raw) {
277
+ if (Object.keys(raw).length === 0) return true;
278
+ return raw.version === 2 && raw.agents !== null && typeof raw.agents === "object" && !Array.isArray(raw.agents);
279
+ }
280
+ function configFromDisk(raw) {
281
+ return {
282
+ apiKey: typeof raw.api_key === "string" && raw.api_key ? raw.api_key : null,
283
+ baseUrl: typeof raw.base_url === "string" && raw.base_url ? String(raw.base_url).replace(/\/+$/, "") : DEFAULT_BASE_URL,
284
+ userId: typeof raw.user_id === "string" && raw.user_id ? raw.user_id : DEFAULT_USER_ID,
285
+ autoCapture: coerceBool(raw.auto_capture, true),
286
+ autoRecall: coerceBool(raw.auto_recall, true),
287
+ warmupRecall: coerceBool(raw.warmup_recall, true),
288
+ recallKnowledge: coerceBool(raw.recall_knowledge, false),
289
+ topK: coerceInt(raw.top_k, DEFAULT_TOP_K),
290
+ threshold: coerceFloat(raw.threshold, DEFAULT_THRESHOLD),
291
+ knowledgeTopK: coerceInt(raw.knowledge_top_k, DEFAULT_KNOWLEDGE_TOP_K),
292
+ debug: coerceBool(raw.debug, false)
293
+ };
294
+ }
295
+ function applyEnv(cfg, env) {
296
+ if (env.CTXDB_API_KEY) cfg.apiKey = env.CTXDB_API_KEY;
297
+ if (env.CTXDB_BASE_URL) cfg.baseUrl = env.CTXDB_BASE_URL.replace(/\/+$/, "");
298
+ if (env.CTXDB_USER_ID) cfg.userId = env.CTXDB_USER_ID;
299
+ return cfg;
300
+ }
301
+ function load(options = {}) {
302
+ const path = options.path ?? defaultConfigPath();
303
+ const env = options.env ?? process.env;
304
+ const agent = resolveConfigAgent({ agent: options.agent, env });
305
+ const raw = readRaw(path);
306
+ if (!isV2Schema(raw)) {
307
+ try {
308
+ process.stderr.write(
309
+ `[ctxdb] config schema invalid at ${path}, treating as first-time install
310
+ `
311
+ );
312
+ } catch {
313
+ }
314
+ return applyEnv(configFromDisk({}), env);
315
+ }
316
+ return applyEnv(configFromDisk(agentRawFromFile(raw, agent)), env);
317
+ }
318
+ function configToDisk(cfg) {
319
+ return {
320
+ api_key: cfg.apiKey,
321
+ base_url: cfg.baseUrl,
322
+ user_id: cfg.userId,
323
+ auto_capture: cfg.autoCapture,
324
+ auto_recall: cfg.autoRecall,
325
+ warmup_recall: cfg.warmupRecall,
326
+ recall_knowledge: cfg.recallKnowledge,
327
+ top_k: cfg.topK,
328
+ threshold: cfg.threshold,
329
+ knowledge_top_k: cfg.knowledgeTopK,
330
+ debug: cfg.debug
331
+ };
332
+ }
333
+ function removeAgent(agent, path, options = {}) {
334
+ const target = path ?? defaultConfigPath();
335
+ if (!existsSync(target)) {
336
+ return { removed: false, remainingAgents: [], fileDeleted: false };
337
+ }
338
+ const raw = readRaw(target);
339
+ if (!isV2Schema(raw) || !raw.agents || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
340
+ return { removed: false, remainingAgents: [], fileDeleted: false };
341
+ }
342
+ const agents = { ...raw.agents };
343
+ if (!(agent in agents)) {
344
+ return {
345
+ removed: false,
346
+ remainingAgents: Object.keys(agents),
347
+ fileDeleted: false
348
+ };
349
+ }
350
+ delete agents[agent];
351
+ const remaining = Object.keys(agents);
352
+ if (remaining.length === 0 && !options.keepEmptyShell) {
353
+ try {
354
+ unlinkSync(target);
355
+ return { removed: true, remainingAgents: [], fileDeleted: true };
356
+ } catch {
357
+ }
358
+ }
359
+ const onDisk = { version: 2, agents };
360
+ writeFileSync(target, JSON.stringify(onDisk, null, 2) + "\n", "utf-8");
361
+ return { removed: true, remainingAgents: remaining, fileDeleted: false };
362
+ }
363
+ function save(cfg, path, options = {}) {
364
+ const target = path ?? defaultConfigPath();
365
+ const agent = resolveConfigAgent(options);
366
+ const raw = readRaw(target);
367
+ const validRaw = isV2Schema(raw) ? raw : {};
368
+ const existingAgents = validRaw.agents && typeof validRaw.agents === "object" && !Array.isArray(validRaw.agents) ? { ...validRaw.agents } : {};
369
+ const onDisk = {
370
+ version: 2,
371
+ agents: {
372
+ ...existingAgents,
373
+ [agent]: configToDisk(cfg)
374
+ }
375
+ };
376
+ mkdirSync(dirname(target), { recursive: true });
377
+ writeFileSync(target, JSON.stringify(onDisk, null, 2) + "\n", "utf-8");
378
+ }
379
+
380
+ export {
381
+ CtxdbError,
382
+ NotFoundError,
383
+ HttpClient,
384
+ SUPPORTED_AGENTS,
385
+ isAgent,
386
+ agentHomeDir,
387
+ agentFromEnv,
388
+ agentFromArgvWithFallback,
389
+ DEFAULT_BASE_URL,
390
+ DEFAULT_USER_ID,
391
+ isComplete,
392
+ load,
393
+ removeAgent,
394
+ save
395
+ };