@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.
- package/LICENSE +201 -0
- package/README.md +202 -0
- package/dist/chunk-464RHJDQ.js +136 -0
- package/dist/chunk-GDJVHVIT.js +35 -0
- package/dist/chunk-L4YJ7LDI.js +395 -0
- package/dist/cli/main.js +1665 -0
- package/dist/hooks/pre-tool-use.js +9 -0
- package/dist/hooks/session-start.js +142 -0
- package/dist/hooks/stop.js +320 -0
- package/dist/hooks/user-prompt-submit.js +77 -0
- package/dist/setup/skills/cli-only/SKILL.md +101 -0
- package/dist/setup/skills/hooks-driven/SKILL.md +103 -0
- package/package.json +45 -0
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,1665 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_BASE_URL,
|
|
4
|
+
DEFAULT_USER_ID,
|
|
5
|
+
HttpClient,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
SUPPORTED_AGENTS,
|
|
8
|
+
agentFromEnv,
|
|
9
|
+
agentHomeDir,
|
|
10
|
+
isAgent,
|
|
11
|
+
isComplete,
|
|
12
|
+
load,
|
|
13
|
+
removeAgent,
|
|
14
|
+
save
|
|
15
|
+
} from "../chunk-L4YJ7LDI.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/util.ts
|
|
18
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
19
|
+
// universal
|
|
20
|
+
"json",
|
|
21
|
+
// setup / init
|
|
22
|
+
"no-install-skill",
|
|
23
|
+
"no-validate",
|
|
24
|
+
"remove",
|
|
25
|
+
// teardown
|
|
26
|
+
"purge-config",
|
|
27
|
+
"purge-logs",
|
|
28
|
+
"purge-all",
|
|
29
|
+
// memory
|
|
30
|
+
"no-infer",
|
|
31
|
+
// memory add
|
|
32
|
+
"knowledge",
|
|
33
|
+
// memory search
|
|
34
|
+
"all",
|
|
35
|
+
// memory delete
|
|
36
|
+
// kb
|
|
37
|
+
"no-wait",
|
|
38
|
+
// kb upload-text / upload-file
|
|
39
|
+
"raw"
|
|
40
|
+
// kb search / memory search --knowledge (W3 compaction bypass)
|
|
41
|
+
]);
|
|
42
|
+
function parseArgs(argv, booleanFlags = BOOLEAN_FLAGS) {
|
|
43
|
+
const positional = [];
|
|
44
|
+
const flags = {};
|
|
45
|
+
let separatorSeen = false;
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const tok = argv[i];
|
|
48
|
+
if (separatorSeen) {
|
|
49
|
+
positional.push(tok);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (tok === "--") {
|
|
53
|
+
separatorSeen = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (tok.startsWith("--")) {
|
|
57
|
+
const eq = tok.indexOf("=");
|
|
58
|
+
if (eq > 0) {
|
|
59
|
+
const k = tok.slice(2, eq);
|
|
60
|
+
flags[k] = tok.slice(eq + 1);
|
|
61
|
+
} else {
|
|
62
|
+
const k = tok.slice(2);
|
|
63
|
+
if (booleanFlags.has(k)) {
|
|
64
|
+
flags[k] = true;
|
|
65
|
+
} else {
|
|
66
|
+
const next = argv[i + 1];
|
|
67
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
68
|
+
flags[k] = next;
|
|
69
|
+
i++;
|
|
70
|
+
} else {
|
|
71
|
+
flags[k] = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
positional.push(tok);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { positional, flags };
|
|
80
|
+
}
|
|
81
|
+
function summarize(v) {
|
|
82
|
+
if (typeof v === "string") return v;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(v);
|
|
85
|
+
} catch {
|
|
86
|
+
return String(v);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function printResult(value, json) {
|
|
90
|
+
if (json) {
|
|
91
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
92
|
+
} else {
|
|
93
|
+
process.stdout.write(summarize(value) + "\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function agentFromFlags(flags) {
|
|
97
|
+
const raw = flags.agent;
|
|
98
|
+
if (raw === void 0) return agentFromEnv();
|
|
99
|
+
if (isAgent(raw)) return raw;
|
|
100
|
+
fail(`unknown --agent: ${String(raw)} (expected one of ${SUPPORTED_AGENTS.join(" / ")})`, 2);
|
|
101
|
+
}
|
|
102
|
+
function buildContext(args) {
|
|
103
|
+
const agent = agentFromFlags(args?.flags ?? {});
|
|
104
|
+
const cfg = load({ agent });
|
|
105
|
+
if (!isComplete(cfg)) {
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`config incomplete for agent ${agent}: run \`ctxdb setup --agent ${agent}\` or set CTXDB_API_KEY / CTXDB_BASE_URL / CTXDB_USER_ID env vars
|
|
108
|
+
`
|
|
109
|
+
);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
const client = new HttpClient({
|
|
113
|
+
baseUrl: cfg.baseUrl,
|
|
114
|
+
apiKey: cfg.apiKey
|
|
115
|
+
});
|
|
116
|
+
return { cfg, client, agent };
|
|
117
|
+
}
|
|
118
|
+
function fail(message, code = 1) {
|
|
119
|
+
process.stderr.write(`${message}
|
|
120
|
+
`);
|
|
121
|
+
process.exit(code);
|
|
122
|
+
}
|
|
123
|
+
var PACKAGE_VERSION = "0.0.1";
|
|
124
|
+
|
|
125
|
+
// src/cli/top.ts
|
|
126
|
+
import { homedir } from "os";
|
|
127
|
+
import { join } from "path";
|
|
128
|
+
async function init(args) {
|
|
129
|
+
const agent = agentFromFlags(args.flags);
|
|
130
|
+
const cfg = load({ agent });
|
|
131
|
+
if (typeof args.flags["api-key"] === "string") cfg.apiKey = args.flags["api-key"];
|
|
132
|
+
if (typeof args.flags["base-url"] === "string") {
|
|
133
|
+
cfg.baseUrl = args.flags["base-url"].replace(/\/+$/, "");
|
|
134
|
+
} else if (!cfg.baseUrl) {
|
|
135
|
+
cfg.baseUrl = DEFAULT_BASE_URL;
|
|
136
|
+
}
|
|
137
|
+
const cliUserId = typeof args.flags["user-id"] === "string" ? args.flags["user-id"] : void 0;
|
|
138
|
+
cfg.userId = cliUserId ?? cfg.userId ?? DEFAULT_USER_ID;
|
|
139
|
+
save(cfg, void 0, { agent });
|
|
140
|
+
let validated = false;
|
|
141
|
+
let pingError = null;
|
|
142
|
+
if (!args.flags["no-validate"] && cfg.apiKey) {
|
|
143
|
+
try {
|
|
144
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
145
|
+
await client.get("/v1/ping/");
|
|
146
|
+
validated = true;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
pingError = err?.message ?? String(err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
printResult(
|
|
152
|
+
{
|
|
153
|
+
ok: true,
|
|
154
|
+
config_path: join(homedir(), ".ctxdb", "ctxdb.json"),
|
|
155
|
+
agent,
|
|
156
|
+
api_key_set: Boolean(cfg.apiKey),
|
|
157
|
+
base_url: cfg.baseUrl,
|
|
158
|
+
user_id: cfg.userId,
|
|
159
|
+
validated,
|
|
160
|
+
ping_error: pingError
|
|
161
|
+
},
|
|
162
|
+
!!args.flags.json
|
|
163
|
+
);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
async function status(args) {
|
|
167
|
+
const agent = agentFromFlags(args.flags);
|
|
168
|
+
const cfg = load({ agent });
|
|
169
|
+
const complete = isComplete(cfg);
|
|
170
|
+
let connected = false;
|
|
171
|
+
let pingError = null;
|
|
172
|
+
if (complete) {
|
|
173
|
+
try {
|
|
174
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
175
|
+
await client.get("/v1/ping/");
|
|
176
|
+
connected = true;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
pingError = err?.message ?? String(err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
printResult(
|
|
182
|
+
{
|
|
183
|
+
ok: complete,
|
|
184
|
+
agent,
|
|
185
|
+
base_url: cfg.baseUrl,
|
|
186
|
+
user_id: cfg.userId,
|
|
187
|
+
api_key_set: Boolean(cfg.apiKey),
|
|
188
|
+
auto_capture: cfg.autoCapture,
|
|
189
|
+
auto_recall: cfg.autoRecall,
|
|
190
|
+
top_k: cfg.topK,
|
|
191
|
+
threshold: cfg.threshold,
|
|
192
|
+
knowledge_top_k: cfg.knowledgeTopK,
|
|
193
|
+
connected,
|
|
194
|
+
ping_error: pingError,
|
|
195
|
+
version: PACKAGE_VERSION
|
|
196
|
+
},
|
|
197
|
+
!!args.flags.json
|
|
198
|
+
);
|
|
199
|
+
return complete && connected ? 0 : 1;
|
|
200
|
+
}
|
|
201
|
+
async function ping(args) {
|
|
202
|
+
const agent = agentFromFlags(args.flags);
|
|
203
|
+
const cfg = load({ agent });
|
|
204
|
+
if (!isComplete(cfg)) {
|
|
205
|
+
fail(
|
|
206
|
+
`config incomplete for agent ${agent} \u2014 run \`ctxdb setup --agent ${agent}\` or set CTXDB_API_KEY / CTXDB_BASE_URL / CTXDB_USER_ID`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
210
|
+
const resp = await client.get("/v1/ping/");
|
|
211
|
+
printResult(resp, !!args.flags.json);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/setup/installer.ts
|
|
216
|
+
import {
|
|
217
|
+
readFileSync,
|
|
218
|
+
writeFileSync,
|
|
219
|
+
existsSync,
|
|
220
|
+
mkdirSync,
|
|
221
|
+
copyFileSync,
|
|
222
|
+
chmodSync,
|
|
223
|
+
readdirSync,
|
|
224
|
+
rmSync,
|
|
225
|
+
rmdirSync,
|
|
226
|
+
unlinkSync,
|
|
227
|
+
statSync
|
|
228
|
+
} from "fs";
|
|
229
|
+
import { homedir as homedir2 } from "os";
|
|
230
|
+
import { join as join2, dirname } from "path";
|
|
231
|
+
import { fileURLToPath } from "url";
|
|
232
|
+
var UNINSTALL_NPM_HINT = "To also remove the npm binaries: `npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared`";
|
|
233
|
+
var UNINSTALL_ORDER_HINT = "When you eventually want to uninstall: run `ctxdb teardown --purge-all` FIRST, then `npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared`. (npm 7+ removed uninstall lifecycle hooks, so the order matters \u2014 otherwise agent settings.json hooks + ~/.ctxdb/ residue stay behind.)";
|
|
234
|
+
function skillInstallRoot(agent) {
|
|
235
|
+
switch (agent) {
|
|
236
|
+
case "qoder":
|
|
237
|
+
return join2(homedir2(), ".qoder", "skills");
|
|
238
|
+
case "codex":
|
|
239
|
+
return join2(homedir2(), ".codex", "skills");
|
|
240
|
+
case "claude":
|
|
241
|
+
return join2(homedir2(), ".claude", "skills");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
var SKILL_DIR_NAME = "ctxdb";
|
|
245
|
+
function skillResourceDir(agent) {
|
|
246
|
+
return agentSupportsHooks(agent) ? "hooks-driven" : "cli-only";
|
|
247
|
+
}
|
|
248
|
+
function skillInstallDir(agent) {
|
|
249
|
+
return join2(skillInstallRoot(agent), SKILL_DIR_NAME);
|
|
250
|
+
}
|
|
251
|
+
function agentSupportsHooks(agent) {
|
|
252
|
+
return agent === "qoder" || agent === "codex" || agent === "claude";
|
|
253
|
+
}
|
|
254
|
+
function hookConfigPath(agent) {
|
|
255
|
+
switch (agent) {
|
|
256
|
+
case "qoder":
|
|
257
|
+
return join2(homedir2(), ".qoder", "settings.json");
|
|
258
|
+
case "codex":
|
|
259
|
+
return join2(homedir2(), ".codex", "hooks.json");
|
|
260
|
+
case "claude":
|
|
261
|
+
return join2(homedir2(), ".claude", "settings.json");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
var LEGACY_QODER_SKILL_DIRS = [
|
|
265
|
+
"ctxdb-qoder",
|
|
266
|
+
"rds-ctxdb-qoder",
|
|
267
|
+
"qoder-ctxdb"
|
|
268
|
+
];
|
|
269
|
+
var HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionStart"];
|
|
270
|
+
async function runSetup(options) {
|
|
271
|
+
const steps = [];
|
|
272
|
+
if (!isAgent(options.agent)) {
|
|
273
|
+
steps.push({
|
|
274
|
+
step: "validate-agent",
|
|
275
|
+
ok: false,
|
|
276
|
+
detail: `unknown --agent: ${String(options.agent)} (expected one of ${SUPPORTED_AGENTS.join(" / ")})`
|
|
277
|
+
});
|
|
278
|
+
return { ok: false, steps };
|
|
279
|
+
}
|
|
280
|
+
const agent = options.agent;
|
|
281
|
+
const installSkill = options.installSkill !== false;
|
|
282
|
+
const validate = options.validate !== false;
|
|
283
|
+
const homeCheck = checkAgentHome(agent);
|
|
284
|
+
steps.push(homeCheck);
|
|
285
|
+
if (!homeCheck.ok) return { ok: false, steps };
|
|
286
|
+
const cfg = load({ agent });
|
|
287
|
+
if (options.apiKey) cfg.apiKey = options.apiKey;
|
|
288
|
+
if (options.baseUrl) cfg.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
289
|
+
else if (!cfg.baseUrl) cfg.baseUrl = DEFAULT_BASE_URL;
|
|
290
|
+
cfg.userId = options.userId ?? cfg.userId ?? DEFAULT_USER_ID;
|
|
291
|
+
save(cfg, void 0, { agent });
|
|
292
|
+
steps.push({
|
|
293
|
+
step: "write-config",
|
|
294
|
+
ok: true,
|
|
295
|
+
detail: `${agent}: ${cfg.baseUrl} (user_id=${cfg.userId})`
|
|
296
|
+
});
|
|
297
|
+
if (installSkill) {
|
|
298
|
+
try {
|
|
299
|
+
const srcDir = locateSkillDir(skillResourceDir(agent));
|
|
300
|
+
if (srcDir) {
|
|
301
|
+
const dest = skillInstallDir(agent);
|
|
302
|
+
copySkillDir(srcDir, dest, agent);
|
|
303
|
+
steps.push({ step: "install-skill", ok: true, detail: dest });
|
|
304
|
+
} else {
|
|
305
|
+
steps.push({
|
|
306
|
+
step: "install-skill",
|
|
307
|
+
ok: false,
|
|
308
|
+
detail: `skill source directory not found (${skillResourceDir(agent)})`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
steps.push({
|
|
313
|
+
step: "install-skill",
|
|
314
|
+
ok: false,
|
|
315
|
+
detail: err?.message ?? String(err)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (agentSupportsHooks(agent)) {
|
|
320
|
+
const hookPaths = locateHookPaths();
|
|
321
|
+
steps.push({
|
|
322
|
+
step: "locate-hooks",
|
|
323
|
+
ok: hookPaths !== null,
|
|
324
|
+
detail: hookPaths ? `${hookPaths.userPromptSubmit} + ${hookPaths.stop}` : "could not locate hook scripts in dist/ or src/"
|
|
325
|
+
});
|
|
326
|
+
if (!hookPaths) return { ok: false, steps };
|
|
327
|
+
try {
|
|
328
|
+
backupSettingsJson(agent);
|
|
329
|
+
steps.push({ step: "backup-settings", ok: true });
|
|
330
|
+
const removed = stripCtxdbHooks(agent);
|
|
331
|
+
if (removed > 0) {
|
|
332
|
+
steps.push({
|
|
333
|
+
step: "clean-legacy-hooks",
|
|
334
|
+
ok: true,
|
|
335
|
+
detail: `removed ${removed} existing entries`
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
appendHooks(agent, hookPaths);
|
|
339
|
+
steps.push({ step: "append-hooks", ok: true });
|
|
340
|
+
} catch (err) {
|
|
341
|
+
steps.push({
|
|
342
|
+
step: "settings-update",
|
|
343
|
+
ok: false,
|
|
344
|
+
detail: err?.message ?? String(err)
|
|
345
|
+
});
|
|
346
|
+
return { ok: false, steps };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
chmodSync(hookPaths.userPromptSubmit, 493);
|
|
350
|
+
chmodSync(hookPaths.stop, 493);
|
|
351
|
+
chmodSync(hookPaths.sessionStart, 493);
|
|
352
|
+
if (hookPaths.preToolUse && existsSync(hookPaths.preToolUse)) {
|
|
353
|
+
chmodSync(hookPaths.preToolUse, 493);
|
|
354
|
+
}
|
|
355
|
+
steps.push({ step: "chmod-hooks", ok: true });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
steps.push({
|
|
358
|
+
step: "chmod-hooks",
|
|
359
|
+
ok: false,
|
|
360
|
+
detail: err?.message ?? String(err)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (agent === "codex") {
|
|
364
|
+
steps.push(enableCodexHooksFeature());
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (validate) {
|
|
368
|
+
if (!cfg.apiKey || !cfg.baseUrl) {
|
|
369
|
+
steps.push({
|
|
370
|
+
step: "validate-ping",
|
|
371
|
+
ok: false,
|
|
372
|
+
detail: "skipped \u2014 config incomplete after install"
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
377
|
+
await client.get("/v1/ping/");
|
|
378
|
+
steps.push({ step: "validate-ping", ok: true });
|
|
379
|
+
} catch (err) {
|
|
380
|
+
steps.push({
|
|
381
|
+
step: "validate-ping",
|
|
382
|
+
ok: false,
|
|
383
|
+
detail: err?.message ?? String(err)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const ok = steps.every((s) => s.ok);
|
|
389
|
+
const hints = ok ? [UNINSTALL_ORDER_HINT] : void 0;
|
|
390
|
+
return { ok, steps, hints };
|
|
391
|
+
}
|
|
392
|
+
function runRemove(agent, options = {}) {
|
|
393
|
+
const steps = [];
|
|
394
|
+
if (!isAgent(agent)) {
|
|
395
|
+
steps.push({
|
|
396
|
+
step: "validate-agent",
|
|
397
|
+
ok: false,
|
|
398
|
+
detail: `unknown --agent: ${String(agent)} (expected one of ${SUPPORTED_AGENTS.join(" / ")})`
|
|
399
|
+
});
|
|
400
|
+
return { ok: false, steps };
|
|
401
|
+
}
|
|
402
|
+
if (agentSupportsHooks(agent)) {
|
|
403
|
+
const path = hookConfigPath(agent);
|
|
404
|
+
if (!existsSync(path)) {
|
|
405
|
+
steps.push({
|
|
406
|
+
step: "settings-missing",
|
|
407
|
+
ok: true,
|
|
408
|
+
detail: "nothing to remove"
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
try {
|
|
412
|
+
backupSettingsJson(agent);
|
|
413
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
414
|
+
const hooks = data.hooks ?? {};
|
|
415
|
+
let removedTotal = 0;
|
|
416
|
+
for (const ev of HOOK_EVENTS) {
|
|
417
|
+
const arr = hooks[ev];
|
|
418
|
+
if (!Array.isArray(arr)) continue;
|
|
419
|
+
const before = arr.length;
|
|
420
|
+
hooks[ev] = arr.filter((entry) => !entryIsCtxdb(entry));
|
|
421
|
+
removedTotal += before - hooks[ev].length;
|
|
422
|
+
}
|
|
423
|
+
data.hooks = hooks;
|
|
424
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
425
|
+
steps.push({
|
|
426
|
+
step: "remove-hooks",
|
|
427
|
+
ok: true,
|
|
428
|
+
detail: `removed ${removedTotal} entries`
|
|
429
|
+
});
|
|
430
|
+
} catch (err) {
|
|
431
|
+
steps.push({
|
|
432
|
+
step: "remove-hooks",
|
|
433
|
+
ok: false,
|
|
434
|
+
detail: err?.message ?? String(err)
|
|
435
|
+
});
|
|
436
|
+
return { ok: false, steps };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const dirs = agent === "qoder" ? [
|
|
440
|
+
skillInstallDir("qoder"),
|
|
441
|
+
...LEGACY_QODER_SKILL_DIRS.map(
|
|
442
|
+
(d) => join2(homedir2(), ".qoder", "skills", d)
|
|
443
|
+
)
|
|
444
|
+
] : [skillInstallDir(agent)];
|
|
445
|
+
for (const p of dirs) {
|
|
446
|
+
if (!existsSync(p)) continue;
|
|
447
|
+
try {
|
|
448
|
+
rmSync(p, { recursive: true, force: true });
|
|
449
|
+
steps.push({ step: "remove-skill-dir", ok: true, detail: p });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
steps.push({
|
|
452
|
+
step: "remove-skill-dir",
|
|
453
|
+
ok: false,
|
|
454
|
+
detail: `${p}: ${err?.message ?? String(err)}`
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
const dir = skillInstallDir(agent);
|
|
460
|
+
if (!existsSync(dir)) {
|
|
461
|
+
steps.push({
|
|
462
|
+
step: "skill-missing",
|
|
463
|
+
ok: true,
|
|
464
|
+
detail: `nothing to remove (${dir})`
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
try {
|
|
468
|
+
rmSync(dir, { recursive: true, force: true });
|
|
469
|
+
steps.push({ step: "remove-skill-dir", ok: true, detail: dir });
|
|
470
|
+
} catch (err) {
|
|
471
|
+
steps.push({
|
|
472
|
+
step: "remove-skill-dir",
|
|
473
|
+
ok: false,
|
|
474
|
+
detail: `${dir}: ${err?.message ?? String(err)}`
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const r = removeAgent(agent, void 0, {
|
|
481
|
+
keepEmptyShell: !!options.keepConfigShell
|
|
482
|
+
});
|
|
483
|
+
if (r.fileDeleted) {
|
|
484
|
+
steps.push({
|
|
485
|
+
step: "remove-config-section",
|
|
486
|
+
ok: true,
|
|
487
|
+
detail: `${agent} was last agent; deleted ~/.ctxdb/ctxdb.json`
|
|
488
|
+
});
|
|
489
|
+
} else if (r.removed) {
|
|
490
|
+
steps.push({
|
|
491
|
+
step: "remove-config-section",
|
|
492
|
+
ok: true,
|
|
493
|
+
detail: `removed agents.${agent}; remaining: ${r.remainingAgents.join(", ") || "(none)"}`
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
steps.push({
|
|
497
|
+
step: "remove-config-section",
|
|
498
|
+
ok: true,
|
|
499
|
+
detail: `nothing to remove (no agents.${agent} section)`
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
steps.push({
|
|
504
|
+
step: "remove-config-section",
|
|
505
|
+
ok: false,
|
|
506
|
+
detail: err?.message ?? String(err)
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return { ok: steps.every((s) => s.ok), steps };
|
|
510
|
+
}
|
|
511
|
+
function checkAgentHome(agent) {
|
|
512
|
+
const dir = agentHomeDir(agent);
|
|
513
|
+
if (!existsSync(dir)) {
|
|
514
|
+
return {
|
|
515
|
+
step: "check-agent-home",
|
|
516
|
+
ok: false,
|
|
517
|
+
detail: `${dir} not found; install or start ${agent} once before running ctxdb setup --agent ${agent}`
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return { step: "check-agent-home", ok: true, detail: dir };
|
|
521
|
+
}
|
|
522
|
+
function runTeardown(options = {}) {
|
|
523
|
+
const steps = [];
|
|
524
|
+
const purgeConfig = Boolean(options.purgeConfig || options.purgeAll);
|
|
525
|
+
const purgeLogs = Boolean(options.purgeLogs || options.purgeAll);
|
|
526
|
+
const purgeRoot = Boolean(options.purgeAll);
|
|
527
|
+
const keepConfigShell = !purgeConfig;
|
|
528
|
+
for (const agent of SUPPORTED_AGENTS) {
|
|
529
|
+
const sub = runRemove(agent, { keepConfigShell });
|
|
530
|
+
for (const s of sub.steps) {
|
|
531
|
+
steps.push({ ...s, step: `${agent}:${s.step}` });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (purgeConfig) {
|
|
535
|
+
const cfgPath = join2(homedir2(), ".ctxdb", "ctxdb.json");
|
|
536
|
+
if (existsSync(cfgPath)) {
|
|
537
|
+
try {
|
|
538
|
+
unlinkSync(cfgPath);
|
|
539
|
+
steps.push({ step: "purge-config", ok: true, detail: cfgPath });
|
|
540
|
+
} catch (err) {
|
|
541
|
+
steps.push({
|
|
542
|
+
step: "purge-config",
|
|
543
|
+
ok: false,
|
|
544
|
+
detail: `${cfgPath}: ${err?.message ?? String(err)}`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
steps.push({
|
|
549
|
+
step: "purge-config",
|
|
550
|
+
ok: true,
|
|
551
|
+
detail: `nothing to remove (${cfgPath})`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (purgeLogs) {
|
|
556
|
+
const logsDir = join2(homedir2(), ".ctxdb", "logs");
|
|
557
|
+
if (existsSync(logsDir)) {
|
|
558
|
+
try {
|
|
559
|
+
rmSync(logsDir, { recursive: true, force: true });
|
|
560
|
+
steps.push({ step: "purge-logs", ok: true, detail: logsDir });
|
|
561
|
+
} catch (err) {
|
|
562
|
+
steps.push({
|
|
563
|
+
step: "purge-logs",
|
|
564
|
+
ok: false,
|
|
565
|
+
detail: `${logsDir}: ${err?.message ?? String(err)}`
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
steps.push({
|
|
570
|
+
step: "purge-logs",
|
|
571
|
+
ok: true,
|
|
572
|
+
detail: `nothing to remove (${logsDir})`
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (purgeRoot) {
|
|
577
|
+
const root = join2(homedir2(), ".ctxdb");
|
|
578
|
+
if (existsSync(root)) {
|
|
579
|
+
try {
|
|
580
|
+
const remaining = readdirSync(root);
|
|
581
|
+
if (remaining.length === 0) {
|
|
582
|
+
rmdirSync(root);
|
|
583
|
+
steps.push({ step: "purge-root", ok: true, detail: root });
|
|
584
|
+
} else {
|
|
585
|
+
steps.push({
|
|
586
|
+
step: "purge-root",
|
|
587
|
+
ok: true,
|
|
588
|
+
detail: `kept (not empty): ${remaining.join(", ")}`
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
steps.push({
|
|
593
|
+
step: "purge-root",
|
|
594
|
+
ok: false,
|
|
595
|
+
detail: `${root}: ${err?.message ?? String(err)}`
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const ok = steps.every((s) => s.ok);
|
|
601
|
+
const hints = ok ? [UNINSTALL_NPM_HINT] : void 0;
|
|
602
|
+
return { ok, steps, hints };
|
|
603
|
+
}
|
|
604
|
+
var SETTINGS_BACKUP_KEEP = 5;
|
|
605
|
+
function backupSettingsJson(agent) {
|
|
606
|
+
const path = hookConfigPath(agent);
|
|
607
|
+
if (!path || !existsSync(path)) return;
|
|
608
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.]/g, "").replace(/Z$/, "");
|
|
609
|
+
const bak = `${path}.bak-ctxdb-${ts}`;
|
|
610
|
+
copyFileSync(path, bak);
|
|
611
|
+
rotateBackups(`${path}.bak-ctxdb-`, SETTINGS_BACKUP_KEEP);
|
|
612
|
+
}
|
|
613
|
+
function rotateBackups(prefix, keep) {
|
|
614
|
+
const dir = dirname(prefix);
|
|
615
|
+
const baseName = prefix.slice(dir.length + 1);
|
|
616
|
+
let entries;
|
|
617
|
+
try {
|
|
618
|
+
entries = readdirSync(dir);
|
|
619
|
+
} catch {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const candidates = [];
|
|
623
|
+
for (const name of entries) {
|
|
624
|
+
if (!name.startsWith(baseName)) continue;
|
|
625
|
+
const full = join2(dir, name);
|
|
626
|
+
try {
|
|
627
|
+
const st = statSync(full);
|
|
628
|
+
if (st.isFile()) candidates.push({ path: full, mtime: st.mtimeMs });
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (candidates.length <= keep) return;
|
|
633
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
634
|
+
for (const stale of candidates.slice(keep)) {
|
|
635
|
+
try {
|
|
636
|
+
unlinkSync(stale.path);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
try {
|
|
639
|
+
process.stderr.write(
|
|
640
|
+
`[ctxdb] failed to prune backup ${stale.path}: ${err?.message ?? err}
|
|
641
|
+
`
|
|
642
|
+
);
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function stripCtxdbHooks(agent) {
|
|
649
|
+
const path = hookConfigPath(agent);
|
|
650
|
+
if (!path || !existsSync(path)) return 0;
|
|
651
|
+
let data;
|
|
652
|
+
try {
|
|
653
|
+
data = JSON.parse(readFileSync(path, "utf-8"));
|
|
654
|
+
} catch {
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
const hooks = data?.hooks ?? {};
|
|
658
|
+
let removed = 0;
|
|
659
|
+
for (const ev of HOOK_EVENTS) {
|
|
660
|
+
const arr = hooks[ev];
|
|
661
|
+
if (!Array.isArray(arr)) continue;
|
|
662
|
+
const before = arr.length;
|
|
663
|
+
hooks[ev] = arr.filter((entry) => !entryIsCtxdb(entry));
|
|
664
|
+
removed += before - hooks[ev].length;
|
|
665
|
+
}
|
|
666
|
+
if (removed === 0) return 0;
|
|
667
|
+
data.hooks = hooks;
|
|
668
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
669
|
+
return removed;
|
|
670
|
+
}
|
|
671
|
+
function locateHookPaths() {
|
|
672
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
673
|
+
const pkgRoot = walkUpToPackageJson(here, "@aliyunrds/ctxdb");
|
|
674
|
+
if (!pkgRoot) return null;
|
|
675
|
+
const preferSrc = here.includes("/src/") || here.includes("\\src\\");
|
|
676
|
+
if (preferSrc) {
|
|
677
|
+
const src = hookPathsFromDir(join2(pkgRoot, "src", "hooks"), "ts");
|
|
678
|
+
if (src) return src;
|
|
679
|
+
}
|
|
680
|
+
const distDir = join2(pkgRoot, "dist", "hooks");
|
|
681
|
+
const dist = hookPathsFromDir(distDir, "js");
|
|
682
|
+
if (dist) return dist;
|
|
683
|
+
return hookPathsFromDir(join2(pkgRoot, "src", "hooks"), "ts");
|
|
684
|
+
}
|
|
685
|
+
function hookPathsFromDir(dir, ext) {
|
|
686
|
+
if (!existsSync(dir)) return null;
|
|
687
|
+
const ups = join2(dir, `user-prompt-submit.${ext}`);
|
|
688
|
+
const stp = join2(dir, `stop.${ext}`);
|
|
689
|
+
const ss = join2(dir, `session-start.${ext}`);
|
|
690
|
+
const ptu = join2(dir, `pre-tool-use.${ext}`);
|
|
691
|
+
if (existsSync(ups) && existsSync(stp) && existsSync(ss)) {
|
|
692
|
+
return {
|
|
693
|
+
userPromptSubmit: ups,
|
|
694
|
+
stop: stp,
|
|
695
|
+
sessionStart: ss,
|
|
696
|
+
preToolUse: existsSync(ptu) ? ptu : void 0
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
function locateSkillDir(variant) {
|
|
702
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
703
|
+
const pkgRoot = walkUpToPackageJson(here, "@aliyunrds/ctxdb");
|
|
704
|
+
if (!pkgRoot) return null;
|
|
705
|
+
const preferSrc = here.includes("/src/") || here.includes("\\src\\");
|
|
706
|
+
if (preferSrc) {
|
|
707
|
+
const srcSkill2 = join2(pkgRoot, "src", "setup", "skills", variant);
|
|
708
|
+
if (existsSync(srcSkill2)) return srcSkill2;
|
|
709
|
+
}
|
|
710
|
+
const distSkill = join2(pkgRoot, "dist", "setup", "skills", variant);
|
|
711
|
+
if (existsSync(distSkill)) return distSkill;
|
|
712
|
+
const srcSkill = join2(pkgRoot, "src", "setup", "skills", variant);
|
|
713
|
+
if (existsSync(srcSkill)) return srcSkill;
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
function walkUpToPackageJson(start, expectedName) {
|
|
717
|
+
let cur = start;
|
|
718
|
+
for (let i = 0; i < 8; i++) {
|
|
719
|
+
const p = join2(cur, "package.json");
|
|
720
|
+
if (existsSync(p)) {
|
|
721
|
+
try {
|
|
722
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
723
|
+
if (pkg.name === expectedName) return cur;
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const parent = dirname(cur);
|
|
728
|
+
if (parent === cur) break;
|
|
729
|
+
cur = parent;
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
function copySkillDir(src, dest, agent) {
|
|
734
|
+
mkdirSync(dest, { recursive: true });
|
|
735
|
+
for (const entry of readdirSync(src)) {
|
|
736
|
+
const srcPath = join2(src, entry);
|
|
737
|
+
const destPath = join2(dest, entry);
|
|
738
|
+
const st = statSync(srcPath);
|
|
739
|
+
if (st.isDirectory()) {
|
|
740
|
+
copySkillDir(srcPath, destPath, agent);
|
|
741
|
+
} else {
|
|
742
|
+
if (entry.endsWith(".md")) {
|
|
743
|
+
const text = readFileSync(srcPath, "utf-8").replace(/\{\{agent\}\}/g, agent);
|
|
744
|
+
writeFileSync(destPath, text, "utf-8");
|
|
745
|
+
} else {
|
|
746
|
+
copyFileSync(srcPath, destPath);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function appendHooks(agent, hookPaths) {
|
|
752
|
+
const path = hookConfigPath(agent);
|
|
753
|
+
if (!path) return;
|
|
754
|
+
let data = {};
|
|
755
|
+
if (existsSync(path)) {
|
|
756
|
+
try {
|
|
757
|
+
data = JSON.parse(readFileSync(path, "utf-8")) ?? {};
|
|
758
|
+
} catch {
|
|
759
|
+
data = {};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (!data.hooks || typeof data.hooks !== "object") data.hooks = {};
|
|
763
|
+
appendOne(data.hooks, "UserPromptSubmit", hookPaths.userPromptSubmit, agent);
|
|
764
|
+
appendOne(data.hooks, "Stop", hookPaths.stop, agent);
|
|
765
|
+
appendOne(data.hooks, "SessionStart", hookPaths.sessionStart, agent);
|
|
766
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
767
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
768
|
+
}
|
|
769
|
+
var ENTRY_MARKER_KEY = "_ctxdb";
|
|
770
|
+
var ENTRY_MARKER_VALUE = "@aliyunrds/ctxdb";
|
|
771
|
+
var ENTRY_AGENT_KEY = "_ctxdbAgent";
|
|
772
|
+
var LEGACY_MARKER_KEYS = ["_ctxdbQoder", "_ctxdbPackage"];
|
|
773
|
+
var LEGACY_MARKER_VALUES = ["@aliyunrds/ctxdb-qoder"];
|
|
774
|
+
var TOOL_SCOPED_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
775
|
+
function appendOne(hooks, event, command, agent) {
|
|
776
|
+
const commandWithAgent = `${command} --agent=${agent}`;
|
|
777
|
+
if (!Array.isArray(hooks[event])) hooks[event] = [];
|
|
778
|
+
const dup = hooks[event].some(
|
|
779
|
+
(entry2) => Array.isArray(entry2?.hooks) && entry2.hooks.some(
|
|
780
|
+
(h) => h?.type === "command" && typeof h.command === "string" && h.command === commandWithAgent
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
if (dup) return;
|
|
784
|
+
const entry = {
|
|
785
|
+
hooks: [{ type: "command", command: commandWithAgent, timeout: 60 }],
|
|
786
|
+
[ENTRY_MARKER_KEY]: ENTRY_MARKER_VALUE,
|
|
787
|
+
[ENTRY_AGENT_KEY]: agent
|
|
788
|
+
};
|
|
789
|
+
if (TOOL_SCOPED_EVENTS.has(event)) entry.matcher = "*";
|
|
790
|
+
hooks[event].push(entry);
|
|
791
|
+
}
|
|
792
|
+
function entryIsCtxdb(entry) {
|
|
793
|
+
if (!entry) return false;
|
|
794
|
+
if (entry[ENTRY_MARKER_KEY] === ENTRY_MARKER_VALUE) return true;
|
|
795
|
+
for (const k of LEGACY_MARKER_KEYS) {
|
|
796
|
+
if (entry[k] === ENTRY_MARKER_VALUE) return true;
|
|
797
|
+
}
|
|
798
|
+
for (const k of [ENTRY_MARKER_KEY, ...LEGACY_MARKER_KEYS]) {
|
|
799
|
+
if (typeof entry[k] === "string" && LEGACY_MARKER_VALUES.includes(entry[k])) {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
804
|
+
return entry.hooks.some(
|
|
805
|
+
(h) => h?.type === "command" && typeof h.command === "string" && (h.command.includes("@aliyunrds/ctxdb-qoder") || h.command.includes("@aliyunrds/ctxdb") || h.command.includes("ctxdb-qoder") || h.command.includes("rds-ctxdb") || h.command.includes("qoder-ctxdb"))
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
function codexConfigTomlPath() {
|
|
809
|
+
return join2(homedir2(), ".codex", "config.toml");
|
|
810
|
+
}
|
|
811
|
+
function inspectCodexHooksFeature(rawContent) {
|
|
812
|
+
const content = rawContent.replace(/^/, "");
|
|
813
|
+
for (const raw of content.split("\n")) {
|
|
814
|
+
const stripped = raw.replace(/#.*$/, "").trim();
|
|
815
|
+
if (!stripped) continue;
|
|
816
|
+
if (/^features\s*=\s*\{/.test(stripped)) {
|
|
817
|
+
return {
|
|
818
|
+
kind: "ambiguous",
|
|
819
|
+
reason: "[features] declared as inline table \u2014 please set hooks = true manually"
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (/^features\.hooks\s*=/.test(stripped)) {
|
|
823
|
+
const value = stripped.split("=", 2)[1]?.trim();
|
|
824
|
+
if (value === "true") return { kind: "already-enabled" };
|
|
825
|
+
return {
|
|
826
|
+
kind: "ambiguous",
|
|
827
|
+
reason: `features.hooks already set to '${value}' via dotted-key \u2014 please set it to true manually`
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const lines = content.split("\n");
|
|
832
|
+
let inFeatures = false;
|
|
833
|
+
let sawFeatures = false;
|
|
834
|
+
let featuresCount = 0;
|
|
835
|
+
for (const raw of lines) {
|
|
836
|
+
const stripped = raw.replace(/#.*$/, "").trim();
|
|
837
|
+
const headerMatch = stripped.match(/^\[([^\]]+)\]$/);
|
|
838
|
+
if (headerMatch) {
|
|
839
|
+
const name = headerMatch[1].trim();
|
|
840
|
+
if (name === "features") {
|
|
841
|
+
inFeatures = true;
|
|
842
|
+
sawFeatures = true;
|
|
843
|
+
featuresCount++;
|
|
844
|
+
} else {
|
|
845
|
+
inFeatures = false;
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (!inFeatures) continue;
|
|
850
|
+
const kv = stripped.match(/^hooks\s*=\s*(.+)$/);
|
|
851
|
+
if (kv) {
|
|
852
|
+
const value = kv[1].trim();
|
|
853
|
+
if (value === "true") return { kind: "already-enabled" };
|
|
854
|
+
if (value === "false") return { kind: "features-with-hooks-false" };
|
|
855
|
+
return {
|
|
856
|
+
kind: "ambiguous",
|
|
857
|
+
reason: `[features].hooks already set to '${value}' \u2014 please set it to true manually`
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (featuresCount > 1) {
|
|
862
|
+
return {
|
|
863
|
+
kind: "ambiguous",
|
|
864
|
+
reason: "multiple [features] sections detected \u2014 please set hooks = true manually"
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
if (sawFeatures) return { kind: "features-without-hooks" };
|
|
868
|
+
return { kind: "no-features-section" };
|
|
869
|
+
}
|
|
870
|
+
function applyCodexHooksFeature(rawContent, status2) {
|
|
871
|
+
switch (status2.kind) {
|
|
872
|
+
case "already-enabled":
|
|
873
|
+
case "ambiguous":
|
|
874
|
+
return rawContent;
|
|
875
|
+
case "no-config-file":
|
|
876
|
+
return "[features]\nhooks = true\n";
|
|
877
|
+
case "no-features-section": {
|
|
878
|
+
if (rawContent.length === 0) return "[features]\nhooks = true\n";
|
|
879
|
+
const trimmedTail = rawContent.replace(/\n+$/, "");
|
|
880
|
+
return `${trimmedTail}
|
|
881
|
+
|
|
882
|
+
[features]
|
|
883
|
+
hooks = true
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
case "features-without-hooks": {
|
|
887
|
+
const lines = rawContent.split("\n");
|
|
888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
889
|
+
const stripped = lines[i].replace(/#.*$/, "").trim();
|
|
890
|
+
if (stripped === "[features]") {
|
|
891
|
+
lines.splice(i + 1, 0, "hooks = true");
|
|
892
|
+
return lines.join("\n");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return rawContent;
|
|
896
|
+
}
|
|
897
|
+
case "features-with-hooks-false": {
|
|
898
|
+
const lines = rawContent.split("\n");
|
|
899
|
+
let inFeatures = false;
|
|
900
|
+
for (let i = 0; i < lines.length; i++) {
|
|
901
|
+
const stripped = lines[i].replace(/#.*$/, "").trim();
|
|
902
|
+
const headerMatch = stripped.match(/^\[([^\]]+)\]$/);
|
|
903
|
+
if (headerMatch) {
|
|
904
|
+
inFeatures = headerMatch[1].trim() === "features";
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (!inFeatures) continue;
|
|
908
|
+
if (/^hooks\s*=\s*false\b/.test(stripped)) {
|
|
909
|
+
lines[i] = lines[i].replace(/(hooks\s*=\s*)false\b/, "$1true");
|
|
910
|
+
return lines.join("\n");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return rawContent;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function enableCodexHooksFeature() {
|
|
918
|
+
const path = codexConfigTomlPath();
|
|
919
|
+
let content = "";
|
|
920
|
+
let createdNew = false;
|
|
921
|
+
if (existsSync(path)) {
|
|
922
|
+
try {
|
|
923
|
+
content = readFileSync(path, "utf-8");
|
|
924
|
+
} catch (err) {
|
|
925
|
+
return {
|
|
926
|
+
step: "codex-features-hooks",
|
|
927
|
+
ok: false,
|
|
928
|
+
detail: `read ${path} failed: ${err?.message ?? err}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
createdNew = true;
|
|
933
|
+
}
|
|
934
|
+
const status2 = createdNew ? { kind: "no-config-file" } : inspectCodexHooksFeature(content);
|
|
935
|
+
if (status2.kind === "already-enabled") {
|
|
936
|
+
return {
|
|
937
|
+
step: "codex-features-hooks",
|
|
938
|
+
ok: true,
|
|
939
|
+
detail: "already enabled in ~/.codex/config.toml"
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
if (status2.kind === "ambiguous") {
|
|
943
|
+
try {
|
|
944
|
+
process.stderr.write(
|
|
945
|
+
`[ctxdb] ~/.codex/config.toml: ${status2.reason}
|
|
946
|
+
[ctxdb] Codex won't fire any hooks until \`[features].hooks = true\` is set in that file.
|
|
947
|
+
`
|
|
948
|
+
);
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
step: "codex-features-hooks",
|
|
953
|
+
ok: true,
|
|
954
|
+
detail: `skipped: ${status2.reason}`
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const next = applyCodexHooksFeature(content, status2);
|
|
958
|
+
if (!createdNew) {
|
|
959
|
+
try {
|
|
960
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.]/g, "").replace(/Z$/, "");
|
|
961
|
+
copyFileSync(path, `${path}.bak-ctxdb-${ts}`);
|
|
962
|
+
rotateBackups(`${path}.bak-ctxdb-`, SETTINGS_BACKUP_KEEP);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
try {
|
|
965
|
+
process.stderr.write(
|
|
966
|
+
`[ctxdb] failed to back up ${path} before patching: ${err?.message ?? err}
|
|
967
|
+
`
|
|
968
|
+
);
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
975
|
+
writeFileSync(path, next, "utf-8");
|
|
976
|
+
return {
|
|
977
|
+
step: "codex-features-hooks",
|
|
978
|
+
ok: true,
|
|
979
|
+
detail: describeCodexFeatureMutation(status2, createdNew)
|
|
980
|
+
};
|
|
981
|
+
} catch (err) {
|
|
982
|
+
return {
|
|
983
|
+
step: "codex-features-hooks",
|
|
984
|
+
ok: false,
|
|
985
|
+
detail: `write ${path} failed: ${err?.message ?? err}`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function describeCodexFeatureMutation(status2, createdNew) {
|
|
990
|
+
if (createdNew) {
|
|
991
|
+
return "created ~/.codex/config.toml with [features].hooks = true";
|
|
992
|
+
}
|
|
993
|
+
switch (status2.kind) {
|
|
994
|
+
case "no-features-section":
|
|
995
|
+
return "appended [features] section with hooks = true";
|
|
996
|
+
case "features-without-hooks":
|
|
997
|
+
return "added hooks = true under existing [features] section";
|
|
998
|
+
case "features-with-hooks-false":
|
|
999
|
+
return "flipped hooks from false to true under [features]";
|
|
1000
|
+
default:
|
|
1001
|
+
return "updated ~/.codex/config.toml";
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/cli/setup-cli.ts
|
|
1006
|
+
function parseAgent(args) {
|
|
1007
|
+
const raw = args.flags.agent;
|
|
1008
|
+
if (typeof raw !== "string") return null;
|
|
1009
|
+
return SUPPORTED_AGENTS.includes(raw) ? raw : null;
|
|
1010
|
+
}
|
|
1011
|
+
function agentError() {
|
|
1012
|
+
process.stderr.write(
|
|
1013
|
+
`ctxdb setup: --agent <${SUPPORTED_AGENTS.join("|")}> is required
|
|
1014
|
+
e.g. ctxdb setup --agent qoder --api-key=<key>
|
|
1015
|
+
`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
async function setup(args) {
|
|
1019
|
+
const agent = parseAgent(args);
|
|
1020
|
+
if (!agent) {
|
|
1021
|
+
agentError();
|
|
1022
|
+
return 2;
|
|
1023
|
+
}
|
|
1024
|
+
const json = !!args.flags.json;
|
|
1025
|
+
if (args.flags.remove) {
|
|
1026
|
+
const result2 = runRemove(agent);
|
|
1027
|
+
printResult(result2, json);
|
|
1028
|
+
if (!json && result2.hints) {
|
|
1029
|
+
for (const h of result2.hints) process.stderr.write(`
|
|
1030
|
+
${h}
|
|
1031
|
+
`);
|
|
1032
|
+
}
|
|
1033
|
+
return result2.ok ? 0 : 1;
|
|
1034
|
+
}
|
|
1035
|
+
const result = await runSetup({
|
|
1036
|
+
agent,
|
|
1037
|
+
apiKey: typeof args.flags["api-key"] === "string" ? args.flags["api-key"] : void 0,
|
|
1038
|
+
baseUrl: typeof args.flags["base-url"] === "string" ? args.flags["base-url"] : void 0,
|
|
1039
|
+
userId: typeof args.flags["user-id"] === "string" ? args.flags["user-id"] : void 0,
|
|
1040
|
+
installSkill: !args.flags["no-install-skill"],
|
|
1041
|
+
validate: !args.flags["no-validate"]
|
|
1042
|
+
});
|
|
1043
|
+
printResult(result, json);
|
|
1044
|
+
if (!json && result.hints) {
|
|
1045
|
+
for (const h of result.hints) process.stderr.write(`
|
|
1046
|
+
${h}
|
|
1047
|
+
`);
|
|
1048
|
+
}
|
|
1049
|
+
return result.ok ? 0 : 1;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/cli/teardown.ts
|
|
1053
|
+
function teardown(args) {
|
|
1054
|
+
const result = runTeardown({
|
|
1055
|
+
purgeConfig: !!args.flags["purge-config"],
|
|
1056
|
+
purgeLogs: !!args.flags["purge-logs"],
|
|
1057
|
+
purgeAll: !!args.flags["purge-all"]
|
|
1058
|
+
});
|
|
1059
|
+
const json = !!args.flags.json;
|
|
1060
|
+
printResult(result, json);
|
|
1061
|
+
if (!json && result.hints) {
|
|
1062
|
+
for (const h of result.hints) process.stderr.write(`
|
|
1063
|
+
${h}
|
|
1064
|
+
`);
|
|
1065
|
+
}
|
|
1066
|
+
return result.ok ? 0 : 1;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/lib/kb.ts
|
|
1070
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
1071
|
+
import { basename, extname } from "path";
|
|
1072
|
+
import { homedir as homedir3 } from "os";
|
|
1073
|
+
import { resolve } from "path";
|
|
1074
|
+
var KB_COLLECTION = "/v1/knowledge/knowledge_bases";
|
|
1075
|
+
var DOCUMENTS = "/v1/knowledge/documents";
|
|
1076
|
+
var FILES = "/v1/knowledge/files";
|
|
1077
|
+
var DOCUMENT_DETAIL = "/v1/knowledge/documents/detail";
|
|
1078
|
+
async function tryNewThenLegacy(newPath, newCall, legacyCall) {
|
|
1079
|
+
try {
|
|
1080
|
+
return await newCall();
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
if (err instanceof NotFoundError && err.path === newPath) {
|
|
1083
|
+
return legacyCall();
|
|
1084
|
+
}
|
|
1085
|
+
throw err;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
1089
|
+
var DEFAULT_INGEST_TIMEOUT_MS = 3e4;
|
|
1090
|
+
var DEFAULT_FILE_INGEST_TIMEOUT_MS = 6e4;
|
|
1091
|
+
var MIME_BY_EXT = {
|
|
1092
|
+
".pdf": "application/pdf",
|
|
1093
|
+
".txt": "text/plain",
|
|
1094
|
+
".md": "text/markdown",
|
|
1095
|
+
".markdown": "text/markdown",
|
|
1096
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1097
|
+
".doc": "application/msword",
|
|
1098
|
+
".html": "text/html",
|
|
1099
|
+
".htm": "text/html",
|
|
1100
|
+
".json": "application/json",
|
|
1101
|
+
".csv": "text/csv",
|
|
1102
|
+
".xml": "application/xml",
|
|
1103
|
+
".rtf": "application/rtf"
|
|
1104
|
+
};
|
|
1105
|
+
async function listKnowledgeBases(client) {
|
|
1106
|
+
const resp = await client.get(KB_COLLECTION);
|
|
1107
|
+
if (Array.isArray(resp)) return resp;
|
|
1108
|
+
if (resp && typeof resp === "object") {
|
|
1109
|
+
const o = resp;
|
|
1110
|
+
return o.knowledge_bases ?? o.results ?? [];
|
|
1111
|
+
}
|
|
1112
|
+
return [];
|
|
1113
|
+
}
|
|
1114
|
+
async function findKb(client, kbNameOrId) {
|
|
1115
|
+
for (const kb of await listKnowledgeBases(client)) {
|
|
1116
|
+
if (!kb || typeof kb !== "object") continue;
|
|
1117
|
+
if (kb.name === kbNameOrId || kb.id === kbNameOrId) return kb;
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
async function createKb(client, kbName, description = "") {
|
|
1122
|
+
return client.postJson(KB_COLLECTION, { name: kbName, description: description || "" });
|
|
1123
|
+
}
|
|
1124
|
+
async function findOrCreateKb(client, kbName, description = "") {
|
|
1125
|
+
const existing = await findKb(client, kbName);
|
|
1126
|
+
if (existing) return { kb: existing, created: false };
|
|
1127
|
+
const created = await createKb(client, kbName, description);
|
|
1128
|
+
return { kb: created, created: true };
|
|
1129
|
+
}
|
|
1130
|
+
async function uploadText(client, kbId, docName, text, mimeType = "text/plain", filePath) {
|
|
1131
|
+
const body = {
|
|
1132
|
+
knowledge_base_id: kbId,
|
|
1133
|
+
name: docName,
|
|
1134
|
+
text,
|
|
1135
|
+
mime_type: mimeType
|
|
1136
|
+
};
|
|
1137
|
+
if (filePath !== void 0 && filePath !== "") body.file_path = filePath;
|
|
1138
|
+
return tryNewThenLegacy(
|
|
1139
|
+
DOCUMENTS,
|
|
1140
|
+
() => client.postJson(DOCUMENTS, body),
|
|
1141
|
+
() => {
|
|
1142
|
+
const { knowledge_base_id: _drop, ...legacyBody } = body;
|
|
1143
|
+
return client.postJson(`${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents`, legacyBody);
|
|
1144
|
+
}
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
async function uploadFile(client, kbId, localPath, options = {}) {
|
|
1148
|
+
const expanded = expandHome(localPath);
|
|
1149
|
+
if (!existsSync2(expanded)) throw new Error(`file not found: ${expanded}`);
|
|
1150
|
+
const stat = statSync2(expanded);
|
|
1151
|
+
if (!stat.isFile()) throw new Error(`not a file: ${expanded}`);
|
|
1152
|
+
const filename = basename(expanded);
|
|
1153
|
+
const docName = options.docName ?? filename;
|
|
1154
|
+
const content = readFileSync2(expanded);
|
|
1155
|
+
const mime = guessMime(expanded);
|
|
1156
|
+
const fields = {
|
|
1157
|
+
knowledge_base_id: kbId,
|
|
1158
|
+
name: docName
|
|
1159
|
+
};
|
|
1160
|
+
if (options.filePath !== void 0 && options.filePath !== "") {
|
|
1161
|
+
fields.file_path = options.filePath;
|
|
1162
|
+
}
|
|
1163
|
+
return tryNewThenLegacy(
|
|
1164
|
+
FILES,
|
|
1165
|
+
() => client.postMultipart(
|
|
1166
|
+
FILES,
|
|
1167
|
+
fields,
|
|
1168
|
+
{ file: { filename, content, mimeType: mime } }
|
|
1169
|
+
),
|
|
1170
|
+
() => {
|
|
1171
|
+
const { knowledge_base_id: _drop, ...legacyFields } = fields;
|
|
1172
|
+
return client.postMultipart(
|
|
1173
|
+
`${KB_COLLECTION}/${encodeURIComponent(kbId)}/files`,
|
|
1174
|
+
legacyFields,
|
|
1175
|
+
{ file: { filename, content, mimeType: mime } }
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
function guessMime(path) {
|
|
1181
|
+
const ext = extname(path).toLowerCase();
|
|
1182
|
+
if (ext in MIME_BY_EXT) return MIME_BY_EXT[ext];
|
|
1183
|
+
return "application/octet-stream";
|
|
1184
|
+
}
|
|
1185
|
+
async function getDocument(client, kbId, docId) {
|
|
1186
|
+
return tryNewThenLegacy(
|
|
1187
|
+
DOCUMENT_DETAIL,
|
|
1188
|
+
() => client.get(DOCUMENT_DETAIL, {
|
|
1189
|
+
knowledge_base_id: kbId,
|
|
1190
|
+
document_id: docId
|
|
1191
|
+
}),
|
|
1192
|
+
() => client.get(
|
|
1193
|
+
`${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents/${encodeURIComponent(docId)}`
|
|
1194
|
+
)
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
async function listDocuments(client, kbId) {
|
|
1198
|
+
const resp = await tryNewThenLegacy(
|
|
1199
|
+
DOCUMENTS,
|
|
1200
|
+
() => client.get(DOCUMENTS, { knowledge_base_id: kbId }),
|
|
1201
|
+
() => client.get(`${KB_COLLECTION}/${encodeURIComponent(kbId)}/documents`)
|
|
1202
|
+
);
|
|
1203
|
+
if (Array.isArray(resp)) return resp;
|
|
1204
|
+
if (resp && typeof resp === "object") {
|
|
1205
|
+
const o = resp;
|
|
1206
|
+
return o.documents ?? o.results ?? [];
|
|
1207
|
+
}
|
|
1208
|
+
return [];
|
|
1209
|
+
}
|
|
1210
|
+
async function pollIngest(client, kbId, docId, options = {}) {
|
|
1211
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
|
|
1212
|
+
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1213
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1214
|
+
const now = options.now ?? (() => performance.now());
|
|
1215
|
+
const deadline = now() + timeoutMs;
|
|
1216
|
+
let doc = await getDocument(client, kbId, docId);
|
|
1217
|
+
let timedOut = false;
|
|
1218
|
+
while (ingestInFlight(doc)) {
|
|
1219
|
+
if (now() >= deadline) {
|
|
1220
|
+
timedOut = true;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
await sleep(intervalMs);
|
|
1224
|
+
try {
|
|
1225
|
+
doc = await getDocument(client, kbId, docId);
|
|
1226
|
+
} catch {
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (doc && typeof doc === "object") {
|
|
1231
|
+
return { ...doc, _pollingTimedOut: timedOut };
|
|
1232
|
+
}
|
|
1233
|
+
return { _pollingTimedOut: timedOut };
|
|
1234
|
+
}
|
|
1235
|
+
function ingestInFlight(doc) {
|
|
1236
|
+
if (!doc || typeof doc !== "object") return false;
|
|
1237
|
+
const status2 = doc.ingest_status;
|
|
1238
|
+
return status2 === "processing" || status2 === "pending" || status2 === "in_progress";
|
|
1239
|
+
}
|
|
1240
|
+
function expandHome(p) {
|
|
1241
|
+
if (p.startsWith("~/") || p === "~") {
|
|
1242
|
+
return resolve(homedir3(), p.slice(2));
|
|
1243
|
+
}
|
|
1244
|
+
return resolve(p);
|
|
1245
|
+
}
|
|
1246
|
+
function compactChunk(raw) {
|
|
1247
|
+
const c = raw ?? {};
|
|
1248
|
+
const content = c.content_with_weight ?? c.content ?? c.chunk ?? c.text ?? "";
|
|
1249
|
+
const doc_name = c.docnm_kwd ?? c.doc_name ?? c.docnm ?? "";
|
|
1250
|
+
const kbRaw = c.kb_id ?? c.dataset_id;
|
|
1251
|
+
const kb_id = Array.isArray(kbRaw) ? typeof kbRaw[0] === "string" ? kbRaw[0] : "" : typeof kbRaw === "string" ? kbRaw : "";
|
|
1252
|
+
const docIdRaw = c.doc_id;
|
|
1253
|
+
const scoreRaw = c.similarity ?? c.rerank_score;
|
|
1254
|
+
const tagsRaw = c.tag_kwd;
|
|
1255
|
+
const out = {
|
|
1256
|
+
content,
|
|
1257
|
+
doc_name,
|
|
1258
|
+
kb_id,
|
|
1259
|
+
score: typeof scoreRaw === "number" ? scoreRaw : 0
|
|
1260
|
+
};
|
|
1261
|
+
if (typeof docIdRaw === "string" && docIdRaw.length > 0) {
|
|
1262
|
+
out.doc_id = docIdRaw;
|
|
1263
|
+
}
|
|
1264
|
+
if (Array.isArray(tagsRaw) && tagsRaw.length > 0) {
|
|
1265
|
+
out.tags = tagsRaw;
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
function compactKbQueryResponse(raw) {
|
|
1270
|
+
if (!raw || typeof raw !== "object") return { chunks: [] };
|
|
1271
|
+
const r = raw;
|
|
1272
|
+
const chunksIn = Array.isArray(r.chunks) ? r.chunks : [];
|
|
1273
|
+
const out = {
|
|
1274
|
+
chunks: chunksIn.map(compactChunk)
|
|
1275
|
+
};
|
|
1276
|
+
if (typeof r.total === "number") out.total = r.total;
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
function minimalChunk(raw) {
|
|
1280
|
+
const c = compactChunk(raw);
|
|
1281
|
+
return { content: c.content, score: c.score };
|
|
1282
|
+
}
|
|
1283
|
+
function minimalKbQueryResponse(raw) {
|
|
1284
|
+
if (!raw || typeof raw !== "object") return { chunks: [] };
|
|
1285
|
+
const r = raw;
|
|
1286
|
+
const chunksIn = Array.isArray(r.chunks) ? r.chunks : [];
|
|
1287
|
+
const out = {
|
|
1288
|
+
chunks: chunksIn.map(minimalChunk)
|
|
1289
|
+
};
|
|
1290
|
+
if (typeof r.total === "number") out.total = r.total;
|
|
1291
|
+
return out;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/cli/memory.ts
|
|
1295
|
+
async function memoryAdd(args) {
|
|
1296
|
+
const text = args.positional[0];
|
|
1297
|
+
if (!text) fail("usage: ctxdb memory add <text> [--user-id=...] [--metadata=K=V] [--no-infer]");
|
|
1298
|
+
const ctx = buildContext(args);
|
|
1299
|
+
const userId = args.flags["user-id"] ?? ctx.cfg.userId;
|
|
1300
|
+
const metadata = parseMetadataFlags(args.flags["metadata"]);
|
|
1301
|
+
const body = {
|
|
1302
|
+
messages: [{ role: "user", content: text }],
|
|
1303
|
+
user_id: userId,
|
|
1304
|
+
async_mode: false
|
|
1305
|
+
};
|
|
1306
|
+
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
1307
|
+
if (args.flags["no-infer"]) body.infer = false;
|
|
1308
|
+
const resp = await ctx.client.postJson("/v3/memories/add/", body);
|
|
1309
|
+
printResult(resp, !!args.flags.json);
|
|
1310
|
+
return 0;
|
|
1311
|
+
}
|
|
1312
|
+
async function memorySearch(args) {
|
|
1313
|
+
const query = args.positional[0];
|
|
1314
|
+
if (!query) fail("usage: ctxdb memory search <query> [--top-k=10] [--threshold=0.4] [--knowledge] [--verbose] [--raw]");
|
|
1315
|
+
const ctx = buildContext(args);
|
|
1316
|
+
const userId = args.flags["user-id"] ?? ctx.cfg.userId;
|
|
1317
|
+
const topK = numFlag(args.flags["top-k"], ctx.cfg.topK);
|
|
1318
|
+
const threshold = numFlag(args.flags["threshold"], ctx.cfg.threshold);
|
|
1319
|
+
const body = {
|
|
1320
|
+
query,
|
|
1321
|
+
user_id: userId,
|
|
1322
|
+
top_k: topK,
|
|
1323
|
+
threshold
|
|
1324
|
+
};
|
|
1325
|
+
if (args.flags.knowledge) {
|
|
1326
|
+
body.knowledge = { enable: true, top_k: ctx.cfg.knowledgeTopK };
|
|
1327
|
+
}
|
|
1328
|
+
const resp = await ctx.client.postJson("/v3/memories/search/", body);
|
|
1329
|
+
const out = projectKnowledgeChunks(resp, args.flags);
|
|
1330
|
+
printResult(out, !!args.flags.json);
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
1333
|
+
function projectKnowledgeChunks(resp, flags) {
|
|
1334
|
+
if (flags.raw) return resp;
|
|
1335
|
+
if (!resp || typeof resp !== "object") return resp;
|
|
1336
|
+
const r = resp;
|
|
1337
|
+
const k = r.knowledge;
|
|
1338
|
+
if (!k || typeof k !== "object") return resp;
|
|
1339
|
+
const chunks = k.chunks;
|
|
1340
|
+
if (!Array.isArray(chunks)) return resp;
|
|
1341
|
+
const project = flags.verbose ? compactChunk : minimalChunk;
|
|
1342
|
+
return {
|
|
1343
|
+
...r,
|
|
1344
|
+
knowledge: { ...k, chunks: chunks.map(project) }
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
async function memoryList(args) {
|
|
1348
|
+
const ctx = buildContext(args);
|
|
1349
|
+
const userId = args.flags["user-id"] ?? ctx.cfg.userId;
|
|
1350
|
+
const pageSize = numFlag(args.flags["page-size"], 100);
|
|
1351
|
+
const body = {
|
|
1352
|
+
filters: { user_id: userId },
|
|
1353
|
+
page_size: pageSize
|
|
1354
|
+
};
|
|
1355
|
+
const cat = args.flags["category"];
|
|
1356
|
+
if (typeof cat === "string") body.filters.category = cat;
|
|
1357
|
+
const resp = await ctx.client.postJson("/v3/memories/", body);
|
|
1358
|
+
printResult(resp, !!args.flags.json);
|
|
1359
|
+
return 0;
|
|
1360
|
+
}
|
|
1361
|
+
async function memoryGet(args) {
|
|
1362
|
+
const id = args.positional[0];
|
|
1363
|
+
if (!id) fail("usage: ctxdb memory get <memory-id>");
|
|
1364
|
+
const ctx = buildContext(args);
|
|
1365
|
+
const resp = await ctx.client.get(`/v1/memories/${encodeURIComponent(id)}/`);
|
|
1366
|
+
printResult(resp, !!args.flags.json);
|
|
1367
|
+
return 0;
|
|
1368
|
+
}
|
|
1369
|
+
async function memoryUpdate(args) {
|
|
1370
|
+
const id = args.positional[0];
|
|
1371
|
+
const text = args.flags["text"];
|
|
1372
|
+
if (!id || typeof text !== "string") {
|
|
1373
|
+
fail("usage: ctxdb memory update <memory-id> --text=<new-text>");
|
|
1374
|
+
}
|
|
1375
|
+
const ctx = buildContext(args);
|
|
1376
|
+
const resp = await ctx.client.putJson(
|
|
1377
|
+
`/v1/memories/${encodeURIComponent(id)}/`,
|
|
1378
|
+
{ text }
|
|
1379
|
+
);
|
|
1380
|
+
printResult(resp, !!args.flags.json);
|
|
1381
|
+
return 0;
|
|
1382
|
+
}
|
|
1383
|
+
async function memoryDelete(args) {
|
|
1384
|
+
const ctx = buildContext(args);
|
|
1385
|
+
const userId = args.flags["user-id"] ?? ctx.cfg.userId;
|
|
1386
|
+
if (args.flags.all) {
|
|
1387
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
1388
|
+
fail("memory delete --all requires --user-id=... or a userId in ctxdb config");
|
|
1389
|
+
}
|
|
1390
|
+
const resp2 = await ctx.client.delete("/v1/memories", { user_id: userId });
|
|
1391
|
+
printResult(resp2, !!args.flags.json);
|
|
1392
|
+
return 0;
|
|
1393
|
+
}
|
|
1394
|
+
const id = args.positional[0];
|
|
1395
|
+
if (!id) fail("usage: ctxdb memory delete <memory-id> | --all");
|
|
1396
|
+
const resp = await ctx.client.delete(
|
|
1397
|
+
`/v1/memories/${encodeURIComponent(id)}/`
|
|
1398
|
+
);
|
|
1399
|
+
printResult(resp, !!args.flags.json);
|
|
1400
|
+
return 0;
|
|
1401
|
+
}
|
|
1402
|
+
function numFlag(v, fallback) {
|
|
1403
|
+
if (typeof v !== "string") return fallback;
|
|
1404
|
+
const n = Number(v);
|
|
1405
|
+
return Number.isFinite(n) ? n : fallback;
|
|
1406
|
+
}
|
|
1407
|
+
function parseMetadataFlags(raw) {
|
|
1408
|
+
if (typeof raw !== "string") return {};
|
|
1409
|
+
const out = {};
|
|
1410
|
+
for (const pair of raw.split(",")) {
|
|
1411
|
+
const eq = pair.indexOf("=");
|
|
1412
|
+
if (eq <= 0) continue;
|
|
1413
|
+
out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
1414
|
+
}
|
|
1415
|
+
return out;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/cli/kb-cli.ts
|
|
1419
|
+
async function kbUploadText(args) {
|
|
1420
|
+
const [kbName, docName] = args.positional;
|
|
1421
|
+
const text = args.flags["text"];
|
|
1422
|
+
if (!kbName || !docName || typeof text !== "string") {
|
|
1423
|
+
fail(
|
|
1424
|
+
"usage: ctxdb kb upload-text <kb-name> <doc-name> --text=<body> [--kb-description=...] [--file-path=<server-logical-path>] [--no-wait]"
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
const ctx = buildContext(args);
|
|
1428
|
+
const description = args.flags["kb-description"] ?? "";
|
|
1429
|
+
const filePath = typeof args.flags["file-path"] === "string" ? args.flags["file-path"] : void 0;
|
|
1430
|
+
const { kb, created } = await findOrCreateKb(ctx.client, kbName, description);
|
|
1431
|
+
const doc = await uploadText(ctx.client, kb.id, docName, text, "text/plain", filePath);
|
|
1432
|
+
if (args.flags["no-wait"]) {
|
|
1433
|
+
printResult({ kb, kb_created: created, document: doc }, !!args.flags.json);
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
const final = await pollIngest(ctx.client, kb.id, doc.id, {
|
|
1437
|
+
timeoutMs: DEFAULT_INGEST_TIMEOUT_MS
|
|
1438
|
+
});
|
|
1439
|
+
printResult(
|
|
1440
|
+
{ kb, kb_created: created, document: final },
|
|
1441
|
+
!!args.flags.json
|
|
1442
|
+
);
|
|
1443
|
+
return 0;
|
|
1444
|
+
}
|
|
1445
|
+
async function kbUploadFile(args) {
|
|
1446
|
+
const [kbName, localPath] = args.positional;
|
|
1447
|
+
if (!kbName || !localPath) {
|
|
1448
|
+
fail(
|
|
1449
|
+
"usage: ctxdb kb upload-file <kb-name> <local-path> [--doc-name=...] [--file-path=<server-logical-path>] [--no-wait]"
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const ctx = buildContext(args);
|
|
1453
|
+
const { kb, created } = await findOrCreateKb(ctx.client, kbName);
|
|
1454
|
+
const doc = await uploadFile(ctx.client, kb.id, localPath, {
|
|
1455
|
+
docName: typeof args.flags["doc-name"] === "string" ? args.flags["doc-name"] : void 0,
|
|
1456
|
+
filePath: typeof args.flags["file-path"] === "string" ? args.flags["file-path"] : void 0
|
|
1457
|
+
});
|
|
1458
|
+
if (args.flags["no-wait"]) {
|
|
1459
|
+
printResult({ kb, kb_created: created, document: doc }, !!args.flags.json);
|
|
1460
|
+
return 0;
|
|
1461
|
+
}
|
|
1462
|
+
const final = await pollIngest(ctx.client, kb.id, doc.id, {
|
|
1463
|
+
timeoutMs: DEFAULT_FILE_INGEST_TIMEOUT_MS
|
|
1464
|
+
});
|
|
1465
|
+
printResult(
|
|
1466
|
+
{ kb, kb_created: created, document: final },
|
|
1467
|
+
!!args.flags.json
|
|
1468
|
+
);
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1471
|
+
async function kbList(args) {
|
|
1472
|
+
const ctx = buildContext(args);
|
|
1473
|
+
const kbs = await listKnowledgeBases(ctx.client);
|
|
1474
|
+
printResult({ knowledge_bases: kbs, count: kbs.length }, !!args.flags.json);
|
|
1475
|
+
return 0;
|
|
1476
|
+
}
|
|
1477
|
+
async function kbDocumentsList(args) {
|
|
1478
|
+
const kbNameOrId = args.positional[0];
|
|
1479
|
+
if (!kbNameOrId) fail("usage: ctxdb kb documents-list <kb-name-or-id>");
|
|
1480
|
+
const ctx = buildContext(args);
|
|
1481
|
+
const kb = await findKb(ctx.client, kbNameOrId);
|
|
1482
|
+
if (!kb) fail(`KB not found: ${kbNameOrId}`);
|
|
1483
|
+
const docs = await listDocuments(ctx.client, kb.id);
|
|
1484
|
+
printResult(
|
|
1485
|
+
{ kb, documents: docs, count: docs.length },
|
|
1486
|
+
!!args.flags.json
|
|
1487
|
+
);
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
async function kbDocumentGet(args) {
|
|
1491
|
+
const [kbNameOrId, docId] = args.positional;
|
|
1492
|
+
if (!kbNameOrId || !docId) {
|
|
1493
|
+
fail("usage: ctxdb kb document-get <kb-name-or-id> <doc-id>");
|
|
1494
|
+
}
|
|
1495
|
+
const ctx = buildContext(args);
|
|
1496
|
+
const kb = await findKb(ctx.client, kbNameOrId);
|
|
1497
|
+
if (!kb) fail(`KB not found: ${kbNameOrId}`);
|
|
1498
|
+
const doc = await getDocument(ctx.client, kb.id, docId);
|
|
1499
|
+
printResult({ kb, document: doc }, !!args.flags.json);
|
|
1500
|
+
return 0;
|
|
1501
|
+
}
|
|
1502
|
+
async function kbSearch(args) {
|
|
1503
|
+
const query = args.positional[0];
|
|
1504
|
+
if (!query) {
|
|
1505
|
+
fail(
|
|
1506
|
+
"usage: ctxdb kb search <query> [--kb=name1,name2] [--top-k=N] [--threshold=F] [--verbose] [--raw]"
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
const ctx = buildContext(args);
|
|
1510
|
+
const kbList2 = (typeof args.flags.kb === "string" ? args.flags.kb : "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1511
|
+
const datasetIds = [];
|
|
1512
|
+
for (const name of kbList2) {
|
|
1513
|
+
const kb = await findKb(ctx.client, name);
|
|
1514
|
+
if (!kb) fail(`KB not found: ${name}`);
|
|
1515
|
+
datasetIds.push(kb.id);
|
|
1516
|
+
}
|
|
1517
|
+
const body = {
|
|
1518
|
+
question: query,
|
|
1519
|
+
dataset_ids: datasetIds
|
|
1520
|
+
};
|
|
1521
|
+
if (typeof args.flags["top-k"] === "string") {
|
|
1522
|
+
const n = Number(args.flags["top-k"]);
|
|
1523
|
+
if (Number.isFinite(n)) body.top_k = n;
|
|
1524
|
+
}
|
|
1525
|
+
if (typeof args.flags["threshold"] === "string") {
|
|
1526
|
+
const f = Number(args.flags["threshold"]);
|
|
1527
|
+
if (Number.isFinite(f)) body.similarity_threshold = f;
|
|
1528
|
+
}
|
|
1529
|
+
const resp = await ctx.client.postJson("/v1/knowledge/query", body);
|
|
1530
|
+
const out = args.flags.raw ? resp : args.flags.verbose ? compactKbQueryResponse(resp) : minimalKbQueryResponse(resp);
|
|
1531
|
+
printResult(out, !!args.flags.json);
|
|
1532
|
+
return 0;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/cli/main.ts
|
|
1536
|
+
var HELP = `ctxdb v${PACKAGE_VERSION} \u2014 RDS ContextDatabase CLI (multi-agent)
|
|
1537
|
+
|
|
1538
|
+
USAGE
|
|
1539
|
+
ctxdb <command> [args] [--flags]
|
|
1540
|
+
|
|
1541
|
+
COMMANDS
|
|
1542
|
+
init [--agent <qoder|codex|claude>] [--api-key=K] [--base-url=URL] [--user-id=ID] [--no-validate]
|
|
1543
|
+
status [--agent <qoder|codex|claude>] [--json]
|
|
1544
|
+
ping [--agent <qoder|codex|claude>] [--json]
|
|
1545
|
+
setup --agent <qoder|codex|claude>
|
|
1546
|
+
[--api-key=K] [--base-url=URL] [--user-id=ID]
|
|
1547
|
+
[--no-install-skill] [--no-validate] [--json] [--remove]
|
|
1548
|
+
Qoder \u2192 writes agents.qoder config + ~/.qoder/settings.json hooks + skill
|
|
1549
|
+
Codex \u2192 writes agents.codex config + ~/.codex/hooks.json hooks + skill
|
|
1550
|
+
Claude \u2192 writes agents.claude config + ~/.claude/settings.json hooks + skill
|
|
1551
|
+
teardown [--purge-config] [--purge-logs] [--purge-all] [--json]
|
|
1552
|
+
Removes hooks + skills for every agent.
|
|
1553
|
+
--purge-config also deletes ~/.ctxdb/ctxdb.json.
|
|
1554
|
+
--purge-logs also deletes ~/.ctxdb/logs/.
|
|
1555
|
+
--purge-all deletes everything under ~/.ctxdb/
|
|
1556
|
+
(does not run npm uninstall).
|
|
1557
|
+
|
|
1558
|
+
memory add <text> [--agent=<name>] [--user-id=...] [--metadata=K1=V1,K2=V2] [--no-infer]
|
|
1559
|
+
memory search <query> [--agent=<name>] [--top-k=10] [--threshold=0.4] [--knowledge]
|
|
1560
|
+
[--verbose] [--raw]
|
|
1561
|
+
--knowledge enables KB chunks alongside memory.
|
|
1562
|
+
Default chunk view: {content, score} only.
|
|
1563
|
+
--verbose adds doc_name/kb_id/doc_id/tags.
|
|
1564
|
+
--raw ships server response verbatim.
|
|
1565
|
+
memory list [--agent=<name>] [--page-size=100] [--category=...]
|
|
1566
|
+
memory get <memory-id> [--agent=<name>]
|
|
1567
|
+
memory update <memory-id> --text=<new-text> [--agent=<name>]
|
|
1568
|
+
memory delete <memory-id> | --all [--agent=<name>]
|
|
1569
|
+
|
|
1570
|
+
kb upload-text <kb-name> <doc-name> --text=<body> [--agent=<name>]
|
|
1571
|
+
[--kb-description=...] [--no-wait]
|
|
1572
|
+
kb upload-file <kb-name> <file-path> [--agent=<name>] [--doc-name=...] [--no-wait]
|
|
1573
|
+
kb list [--agent=<name>]
|
|
1574
|
+
kb documents-list <kb-name-or-id> [--agent=<name>]
|
|
1575
|
+
kb document-get <kb-name-or-id> <doc-id> [--agent=<name>]
|
|
1576
|
+
kb search <query> [--agent=<name>] [--kb=name1,name2] [--top-k=N] [--threshold=F]
|
|
1577
|
+
[--verbose] [--raw]
|
|
1578
|
+
Standalone KB recall (independent of memory).
|
|
1579
|
+
Default: minimal {content, score} per chunk.
|
|
1580
|
+
--verbose: compact view adds doc_name/kb_id/
|
|
1581
|
+
doc_id?/tags? for citation.
|
|
1582
|
+
--raw: server response verbatim (curl parity).
|
|
1583
|
+
|
|
1584
|
+
ENV VARS (override selected ~/.ctxdb/ctxdb.json agent config):
|
|
1585
|
+
CTXDB_AGENT CTXDB_API_KEY CTXDB_BASE_URL CTXDB_USER_ID
|
|
1586
|
+
|
|
1587
|
+
CONFIG: ~/.ctxdb/ctxdb.json (agents.qoder / agents.codex / agents.claude)
|
|
1588
|
+
LOGS: ~/.ctxdb/logs/ctxdb.log
|
|
1589
|
+
`;
|
|
1590
|
+
var ROUTES = {
|
|
1591
|
+
init,
|
|
1592
|
+
status,
|
|
1593
|
+
ping,
|
|
1594
|
+
setup,
|
|
1595
|
+
teardown,
|
|
1596
|
+
memory: {
|
|
1597
|
+
add: memoryAdd,
|
|
1598
|
+
search: memorySearch,
|
|
1599
|
+
list: memoryList,
|
|
1600
|
+
get: memoryGet,
|
|
1601
|
+
update: memoryUpdate,
|
|
1602
|
+
delete: memoryDelete
|
|
1603
|
+
},
|
|
1604
|
+
kb: {
|
|
1605
|
+
"upload-text": kbUploadText,
|
|
1606
|
+
"upload-file": kbUploadFile,
|
|
1607
|
+
list: kbList,
|
|
1608
|
+
"documents-list": kbDocumentsList,
|
|
1609
|
+
"document-get": kbDocumentGet,
|
|
1610
|
+
search: kbSearch
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1614
|
+
if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
|
|
1615
|
+
process.stdout.write(HELP);
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
if (argv[0] === "-v" || argv[0] === "--version") {
|
|
1619
|
+
process.stdout.write(`ctxdb ${PACKAGE_VERSION}
|
|
1620
|
+
`);
|
|
1621
|
+
return 0;
|
|
1622
|
+
}
|
|
1623
|
+
const [first, ...rest] = argv;
|
|
1624
|
+
const route = ROUTES[first];
|
|
1625
|
+
if (!route) {
|
|
1626
|
+
process.stderr.write(`unknown command: ${first}
|
|
1627
|
+
`);
|
|
1628
|
+
process.stderr.write(HELP);
|
|
1629
|
+
return 2;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof route === "function") {
|
|
1632
|
+
return Promise.resolve(route(parseArgs(rest)));
|
|
1633
|
+
}
|
|
1634
|
+
if (rest.length === 0) {
|
|
1635
|
+
process.stderr.write(
|
|
1636
|
+
`usage: ctxdb ${first} <${Object.keys(route).join("|")}> ...
|
|
1637
|
+
`
|
|
1638
|
+
);
|
|
1639
|
+
return 2;
|
|
1640
|
+
}
|
|
1641
|
+
const [sub, ...subRest] = rest;
|
|
1642
|
+
const handler = route[sub];
|
|
1643
|
+
if (!handler) {
|
|
1644
|
+
process.stderr.write(`unknown subcommand: ${first} ${sub}
|
|
1645
|
+
`);
|
|
1646
|
+
return 2;
|
|
1647
|
+
}
|
|
1648
|
+
return Promise.resolve(handler(parseArgs(subRest)));
|
|
1649
|
+
}
|
|
1650
|
+
main().then(
|
|
1651
|
+
(code) => {
|
|
1652
|
+
if (typeof code === "number") process.exit(code);
|
|
1653
|
+
},
|
|
1654
|
+
(err) => {
|
|
1655
|
+
process.stderr.write(`error: ${err?.message ?? err}
|
|
1656
|
+
`);
|
|
1657
|
+
if (process.env.RDS_CTXDB_DEBUG === "1" && err?.stack) {
|
|
1658
|
+
process.stderr.write(err.stack + "\n");
|
|
1659
|
+
}
|
|
1660
|
+
process.exit(1);
|
|
1661
|
+
}
|
|
1662
|
+
);
|
|
1663
|
+
export {
|
|
1664
|
+
main
|
|
1665
|
+
};
|