codetrap 0.1.1 → 0.1.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.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +121 -38
- package/docs/installation.md +18 -10
- package/package.json +4 -1
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
- package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
- package/plugins/codetrap-agent/hooks.json +11 -0
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
- package/scripts/release-preflight.ts +55 -0
- package/skills/codetrap-add/SKILL.md +4 -1
- package/skills/codetrap-check/SKILL.md +24 -4
- package/skills/codetrap-search/SKILL.md +32 -12
- package/src/commands/command-result.ts +29 -0
- package/src/commands/router.ts +6 -400
- package/src/commands/workflow.ts +466 -0
- package/src/db/embedding-queries.ts +33 -0
- package/src/db/queries.ts +119 -20
- package/src/db/repository.ts +39 -2
- package/src/db/schema.ts +35 -0
- package/src/domain/trap.ts +31 -2
- package/src/index.ts +13 -1
- package/src/lib/config.ts +102 -0
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +76 -0
- package/src/lib/embedding-health.ts +49 -0
- package/src/lib/format.ts +5 -1
- package/src/lib/output-json.ts +116 -0
- package/src/lib/scope-context.ts +116 -0
- package/src/lib/scope-migration.ts +360 -0
- package/src/lib/scope.ts +6 -4
- package/src/lib/search-normalizer.ts +6 -0
- package/src/lib/search-policy.ts +276 -0
- package/src/lib/search-result-card.ts +1 -0
- package/src/lib/search-service.ts +36 -98
- package/src/lib/store.ts +96 -107
- package/src/lib/trap-archive.ts +9 -42
- package/src/lib/trap-codec.ts +113 -0
- package/src/lib/trap-json-fields.ts +12 -0
- package/src/lib/trap-mutation-result.ts +36 -0
- package/src/lib/trap-operations.ts +27 -6
- package/src/lib/trap-scope-match.ts +112 -0
- package/src/lib/trap-search-document.ts +8 -1
- package/src/lib/trap-transfer.ts +88 -0
- package/src/mcp/server.ts +75 -57
- package/src/mcp/tools.ts +32 -5
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { TrapStore } from "../lib/store";
|
|
3
|
+
import { formatTrapShort, formatTrapDetails, formatTrapActionCard } from "../lib/format";
|
|
4
|
+
import type { Trap } from "../domain/trap";
|
|
5
|
+
import { SEARCH_MODES, type SearchMode } from "../lib/constants";
|
|
6
|
+
import {
|
|
7
|
+
formatScopeMigrationText,
|
|
8
|
+
runScopeMigration,
|
|
9
|
+
type ScopeMigrationCommand,
|
|
10
|
+
} from "../lib/scope-migration";
|
|
11
|
+
import { TrapOperations } from "../lib/trap-operations";
|
|
12
|
+
import { buildDoctorReport, formatDoctorText } from "../lib/doctor";
|
|
13
|
+
import { searchDefaultsFromConfig } from "../lib/config";
|
|
14
|
+
import {
|
|
15
|
+
toCliSearchJson,
|
|
16
|
+
toListJson,
|
|
17
|
+
toStatsJson,
|
|
18
|
+
toTrapDetailsJson,
|
|
19
|
+
} from "../lib/output-json";
|
|
20
|
+
import {
|
|
21
|
+
errorResult,
|
|
22
|
+
jsonResult,
|
|
23
|
+
textResult,
|
|
24
|
+
type CommandResult,
|
|
25
|
+
} from "./command-result";
|
|
26
|
+
import { mutationJsonPayload } from "../lib/trap-mutation-result";
|
|
27
|
+
|
|
28
|
+
type ParsedArgs = {
|
|
29
|
+
opts: Record<string, string>;
|
|
30
|
+
positionals: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function executeCommand(strip: string[], store: TrapStore): Promise<CommandResult> {
|
|
34
|
+
const sub = strip[0];
|
|
35
|
+
const args = strip.slice(1);
|
|
36
|
+
const operations = new TrapOperations(store);
|
|
37
|
+
|
|
38
|
+
switch (sub) {
|
|
39
|
+
case "add":
|
|
40
|
+
return cmdAdd(args, operations);
|
|
41
|
+
case "search":
|
|
42
|
+
return cmdSearch(args, operations);
|
|
43
|
+
case "list":
|
|
44
|
+
return cmdList(args, operations);
|
|
45
|
+
case "show":
|
|
46
|
+
return cmdShow(args, operations);
|
|
47
|
+
case "edit":
|
|
48
|
+
return cmdEdit(args, operations);
|
|
49
|
+
case "delete":
|
|
50
|
+
case "rm":
|
|
51
|
+
return cmdDelete(args, operations);
|
|
52
|
+
case "add_trap_evidence":
|
|
53
|
+
case "add-evidence":
|
|
54
|
+
return cmdAddTrapEvidence(args, operations);
|
|
55
|
+
case "archive_trap":
|
|
56
|
+
case "archive":
|
|
57
|
+
return cmdArchiveTrap(args, operations);
|
|
58
|
+
case "supersede_trap":
|
|
59
|
+
case "supersede":
|
|
60
|
+
return cmdSupersedeTrap(args, operations);
|
|
61
|
+
case "init":
|
|
62
|
+
return cmdInit(args, store);
|
|
63
|
+
case "export":
|
|
64
|
+
return cmdExport(args, operations);
|
|
65
|
+
case "import":
|
|
66
|
+
return cmdImport(args, operations);
|
|
67
|
+
case "stats":
|
|
68
|
+
return cmdStats(args, operations);
|
|
69
|
+
case "doctor":
|
|
70
|
+
return cmdDoctor(args, store, operations);
|
|
71
|
+
case "repair-scope":
|
|
72
|
+
return cmdScopeMigration("repair-scope", args, operations);
|
|
73
|
+
case "migrate-project":
|
|
74
|
+
return cmdScopeMigration("migrate-project", args, operations);
|
|
75
|
+
case "embed":
|
|
76
|
+
return cmdEmbed(args, store);
|
|
77
|
+
default:
|
|
78
|
+
return errorResult([
|
|
79
|
+
`Unknown command: ${sub}`,
|
|
80
|
+
"Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, doctor, repair-scope, migrate-project, embed",
|
|
81
|
+
].join("\n"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseArgs(args: string[]): ParsedArgs {
|
|
86
|
+
const opts: Record<string, string> = {};
|
|
87
|
+
const positionals: string[] = [];
|
|
88
|
+
for (let i = 0; i < args.length; i++) {
|
|
89
|
+
if (args[i].startsWith("--")) {
|
|
90
|
+
const key = args[i].slice(2);
|
|
91
|
+
const val = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
|
|
92
|
+
opts[key] = val;
|
|
93
|
+
} else {
|
|
94
|
+
positionals.push(args[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { opts, positionals };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cmdInit(_args: string[], store: TrapStore): CommandResult {
|
|
101
|
+
if (store.hasProject()) {
|
|
102
|
+
return textResult(`Already in a project: ${store.getProjectRoot()}`);
|
|
103
|
+
}
|
|
104
|
+
return textResult("Project initialized.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cmdAdd(args: string[], operations: TrapOperations): CommandResult {
|
|
108
|
+
const { opts, positionals } = parseArgs(args);
|
|
109
|
+
if (opts.json !== undefined) {
|
|
110
|
+
if (!opts.json || opts.json === "true") {
|
|
111
|
+
return errorResult("Error: --json requires a JSON string argument");
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = operations.addTrap(JSON.parse(opts.json));
|
|
115
|
+
return opts["output-json"] !== undefined
|
|
116
|
+
? jsonResult(result)
|
|
117
|
+
: textResult(`Trap #${result.id} added to ${result.scope} scope.`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return errorFrom(error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (positionals.length > 0) {
|
|
124
|
+
return textResult([
|
|
125
|
+
"Use --json mode for structured input.",
|
|
126
|
+
`Quick add: codetrap add --json '{"title":"${positionals.join(" ")}","category":"other","scope":"global","context":"...","mistake":"...","fix":"..."}'`,
|
|
127
|
+
].join("\n"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return textResult([
|
|
131
|
+
"Interactive mode not yet implemented. Use --json for now.",
|
|
132
|
+
'Example: codetrap add --json \'{"title":"...","category":"convention","scope":"project","context":"...","mistake":"...","fix":"..."}\'',
|
|
133
|
+
].join("\n"));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function cmdSearch(args: string[], operations: TrapOperations): Promise<CommandResult> {
|
|
137
|
+
const { opts, positionals } = parseArgs(args);
|
|
138
|
+
const query = readQuery(positionals);
|
|
139
|
+
if (!query) {
|
|
140
|
+
return errorResult("Usage: codetrap search <query> [--category X] [--limit N] [--mode fts|semantic|hybrid] [--status active|superseded|archived|all] [--path file] [--module name] [--owner name] [--json]");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const mode = opts.mode ? parseSearchMode(opts.mode) : undefined;
|
|
145
|
+
const defaults = searchDefaultsFromConfig();
|
|
146
|
+
const cards = await operations.searchTrapCards({
|
|
147
|
+
query,
|
|
148
|
+
category: opts.category,
|
|
149
|
+
scope: opts.scope ?? defaults.scope,
|
|
150
|
+
limit: opts.limit ? parseOptionalInt(opts.limit) : defaults.limit,
|
|
151
|
+
mode: mode ?? defaults.mode,
|
|
152
|
+
status: opts.status,
|
|
153
|
+
path: opts.path,
|
|
154
|
+
module: opts.module,
|
|
155
|
+
owner: opts.owner,
|
|
156
|
+
rerank: opts["no-rerank"] !== undefined ? false : defaults.rerank,
|
|
157
|
+
includeRankingSignals: opts["ranking-signals"] !== undefined,
|
|
158
|
+
});
|
|
159
|
+
if (opts.json !== undefined) return jsonResult(toCliSearchJson(cards));
|
|
160
|
+
return textResult(cards.length > 0 ? cards.map(formatTrapActionCard).join("\n\n") : "No traps found.");
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return errorFrom(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cmdList(args: string[], operations: TrapOperations): CommandResult {
|
|
167
|
+
const { opts } = parseArgs(args);
|
|
168
|
+
try {
|
|
169
|
+
const groups = operations.listTraps({
|
|
170
|
+
category: opts.category,
|
|
171
|
+
scope: opts.scope,
|
|
172
|
+
status: opts.status,
|
|
173
|
+
path: opts.path,
|
|
174
|
+
module: opts.module,
|
|
175
|
+
owner: opts.owner,
|
|
176
|
+
limit: parseOptionalInt(opts.limit, 50),
|
|
177
|
+
});
|
|
178
|
+
if (opts.json !== undefined) return jsonResult(toListJson(groups));
|
|
179
|
+
|
|
180
|
+
const lines = groups.flatMap((group) =>
|
|
181
|
+
group.traps.map((trap) => formatTrapShort(trap, group.scope))
|
|
182
|
+
);
|
|
183
|
+
return textResult(lines.length > 0 ? lines.join("\n") : "No traps found.");
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return errorFrom(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function cmdShow(args: string[], operations: TrapOperations): CommandResult {
|
|
190
|
+
const { opts, positionals } = parseArgs(args);
|
|
191
|
+
const id = parseId(positionals[0], "Usage: codetrap show <id> [--scope project|global] [--json]");
|
|
192
|
+
if (typeof id !== "number") return id;
|
|
193
|
+
|
|
194
|
+
const result = operations.getTrapDetails(id, opts.scope);
|
|
195
|
+
if (!result) return errorResult(`Trap #${id} not found.`);
|
|
196
|
+
|
|
197
|
+
operations.hitTrap(id, result.scope);
|
|
198
|
+
return opts.json !== undefined
|
|
199
|
+
? jsonResult(toTrapDetailsJson(result))
|
|
200
|
+
: textResult(formatTrapDetails(result));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdEdit(args: string[], operations: TrapOperations): CommandResult {
|
|
204
|
+
const { opts, positionals } = parseArgs(args);
|
|
205
|
+
const id = parseId(positionals[0], "Usage: codetrap edit <id> --json '{\"title\":\"new title\"}' [--scope project|global]");
|
|
206
|
+
if (typeof id !== "number") return id;
|
|
207
|
+
|
|
208
|
+
if (!opts.json) {
|
|
209
|
+
return errorResult([
|
|
210
|
+
"Error: edit requires --json for now.",
|
|
211
|
+
"Example: codetrap edit 1 --json '{\"title\":\"new title\"}' [--scope project|global]",
|
|
212
|
+
].join("\n"));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = operations.updateTrap(id, JSON.parse(opts.json), opts.scope);
|
|
217
|
+
if (!result.success) return errorResult(`Trap #${id} not found or no fields changed.`);
|
|
218
|
+
return opts["output-json"] !== undefined
|
|
219
|
+
? jsonResult({ id, ...result })
|
|
220
|
+
: textResult(`Trap #${id} updated in ${result.scope} scope.`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return errorFrom(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function cmdDelete(args: string[], operations: TrapOperations): CommandResult {
|
|
227
|
+
const { opts, positionals } = parseArgs(args);
|
|
228
|
+
const id = parseId(positionals[0], "Usage: codetrap delete <id> [--scope project|global] [--json]");
|
|
229
|
+
if (typeof id !== "number") return id;
|
|
230
|
+
|
|
231
|
+
const result = operations.deleteTrap(id, opts.scope);
|
|
232
|
+
if (opts.json !== undefined) {
|
|
233
|
+
return mutationJsonResult({ id, ...result }, `Trap #${id} not found.`);
|
|
234
|
+
}
|
|
235
|
+
return result.success
|
|
236
|
+
? textResult(`Trap #${id} deleted from ${result.scope} scope.`)
|
|
237
|
+
: errorResult(`Trap #${id} not found.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function cmdAddTrapEvidence(args: string[], operations: TrapOperations): CommandResult {
|
|
241
|
+
const { opts, positionals } = parseArgs(args);
|
|
242
|
+
const id = parseId(
|
|
243
|
+
positionals[0],
|
|
244
|
+
"Usage: codetrap add_trap_evidence <id> --source_type manual|conversation|commit|issue|test_failure [--scope project|global] [--source_ref X] [--related_files a,b] [--note X]"
|
|
245
|
+
);
|
|
246
|
+
if (typeof id !== "number") return id;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const input = opts.json ? JSON.parse(opts.json) : {
|
|
250
|
+
source_type: opts.source_type ?? opts["source-type"],
|
|
251
|
+
source_ref: opts.source_ref ?? opts["source-ref"],
|
|
252
|
+
observed_at: opts.observed_at ?? opts["observed-at"],
|
|
253
|
+
related_files: parseCsv(opts.related_files ?? opts["related-files"]),
|
|
254
|
+
note: opts.note,
|
|
255
|
+
};
|
|
256
|
+
const result = operations.addTrapEvidence(id, input, opts.scope);
|
|
257
|
+
if (opts["output-json"] !== undefined) {
|
|
258
|
+
return mutationJsonResult({ id, ...result }, `Trap #${id} not found.`);
|
|
259
|
+
}
|
|
260
|
+
return result.success
|
|
261
|
+
? textResult(`Evidence #${result.evidence_id} added to trap #${id} in ${result.scope} scope.`)
|
|
262
|
+
: errorResult(`Trap #${id} not found.`);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return errorFrom(error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function cmdArchiveTrap(args: string[], operations: TrapOperations): CommandResult {
|
|
269
|
+
const { opts, positionals } = parseArgs(args);
|
|
270
|
+
const id = parseId(positionals[0], "Usage: codetrap archive_trap <id> [--scope project|global] [--json]");
|
|
271
|
+
if (typeof id !== "number") return id;
|
|
272
|
+
|
|
273
|
+
const result = operations.archiveTrap(id, opts.scope);
|
|
274
|
+
if (opts.json !== undefined) {
|
|
275
|
+
return mutationJsonResult({ id, ...result, status: result.success ? "archived" : undefined }, `Trap #${id} not found.`);
|
|
276
|
+
}
|
|
277
|
+
return result.success
|
|
278
|
+
? textResult(`Trap #${id} archived in ${result.scope} scope.`)
|
|
279
|
+
: errorResult(`Trap #${id} not found.`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function cmdSupersedeTrap(args: string[], operations: TrapOperations): CommandResult {
|
|
283
|
+
const { opts, positionals } = parseArgs(args);
|
|
284
|
+
if (positionals.length < 2) {
|
|
285
|
+
return errorResult("Usage: codetrap supersede_trap <old_id> <new_id> [--scope project|global] [--state_key key] [--json]");
|
|
286
|
+
}
|
|
287
|
+
const id = Number.parseInt(positionals[0], 10);
|
|
288
|
+
const supersededById = Number.parseInt(positionals[1], 10);
|
|
289
|
+
if (Number.isNaN(id) || Number.isNaN(supersededById)) {
|
|
290
|
+
return errorResult("Error: ids must be numbers");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = operations.supersedeTrap(id, supersededById, opts.scope, opts.state_key ?? opts["state-key"]);
|
|
294
|
+
if (opts.json !== undefined) {
|
|
295
|
+
return mutationJsonResult(
|
|
296
|
+
{ id, superseded_by_id: supersededById, ...result },
|
|
297
|
+
`Trap #${id} or #${supersededById} not found in the same scope.`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return result.success
|
|
301
|
+
? textResult(`Trap #${id} superseded by #${supersededById} in ${result.scope} scope.`)
|
|
302
|
+
: errorResult(`Trap #${id} or #${supersededById} not found in the same scope.`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function cmdExport(args: string[], operations: TrapOperations): CommandResult {
|
|
306
|
+
const { opts } = parseArgs(args);
|
|
307
|
+
return jsonResult(operations.exportTraps(opts.scope));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function cmdImport(args: string[], operations: TrapOperations): CommandResult {
|
|
311
|
+
const { opts, positionals } = parseArgs(args);
|
|
312
|
+
if (positionals.length === 0) return errorResult("Usage: codetrap import <file.json>");
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const traps = JSON.parse(readFileSync(positionals[0], "utf-8"));
|
|
316
|
+
if (!Array.isArray(traps)) {
|
|
317
|
+
const message = "Error: JSON file must contain an array of traps";
|
|
318
|
+
return opts.json !== undefined ? jsonResult({ success: false, error: message }, 1) : errorResult(message);
|
|
319
|
+
}
|
|
320
|
+
const imported = operations.importTraps(traps);
|
|
321
|
+
return opts.json !== undefined
|
|
322
|
+
? jsonResult({ imported, success: true })
|
|
323
|
+
: textResult(`Imported ${imported} traps.`);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (opts.json !== undefined) {
|
|
326
|
+
return jsonResult({ success: false, error: errorMessage(error) }, 1);
|
|
327
|
+
}
|
|
328
|
+
return errorFrom(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function cmdStats(args: string[], operations: TrapOperations): CommandResult {
|
|
333
|
+
const { opts } = parseArgs(args);
|
|
334
|
+
const stats = operations.getStats();
|
|
335
|
+
const embeddingStats = operations.getEmbeddingStats();
|
|
336
|
+
return opts.json !== undefined
|
|
337
|
+
? jsonResult(toStatsJson(stats, embeddingStats))
|
|
338
|
+
: textResult(formatStatsText(stats));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function cmdDoctor(args: string[], store: TrapStore, operations: TrapOperations): CommandResult {
|
|
342
|
+
const { opts } = parseArgs(args);
|
|
343
|
+
const report = buildDoctorReport(store, operations);
|
|
344
|
+
return opts.json !== undefined
|
|
345
|
+
? jsonResult(report)
|
|
346
|
+
: textResult(formatDoctorText(report));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function cmdScopeMigration(
|
|
350
|
+
command: ScopeMigrationCommand,
|
|
351
|
+
args: string[],
|
|
352
|
+
operations: TrapOperations
|
|
353
|
+
): CommandResult {
|
|
354
|
+
const { opts } = parseArgs(args);
|
|
355
|
+
if (opts.apply !== undefined && opts["dry-run"] !== undefined) {
|
|
356
|
+
return errorResult("Error: choose either --dry-run or --apply, not both.");
|
|
357
|
+
}
|
|
358
|
+
if (command === "migrate-project" && (!opts["from-project-path"] || !opts["to-project-path"])) {
|
|
359
|
+
return errorResult("Usage: codetrap migrate-project --from-project-path <path> --to-project-path <path> [--dry-run|--apply] [--json]");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const result = runScopeMigration({
|
|
364
|
+
command,
|
|
365
|
+
fromProjectPath: opts["from-project-path"],
|
|
366
|
+
toProjectPath: opts["to-project-path"],
|
|
367
|
+
apply: opts.apply !== undefined,
|
|
368
|
+
cwd: process.cwd(),
|
|
369
|
+
});
|
|
370
|
+
return opts.json !== undefined
|
|
371
|
+
? jsonResult(result)
|
|
372
|
+
: textResult(formatScopeMigrationText(result));
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return errorFrom(error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function cmdEmbed(args: string[], store: TrapStore): Promise<CommandResult> {
|
|
379
|
+
const { opts } = parseArgs(args);
|
|
380
|
+
try {
|
|
381
|
+
const result = await store.ensureEmbeddings({
|
|
382
|
+
scope: opts.scope,
|
|
383
|
+
category: opts.category,
|
|
384
|
+
limit: opts.limit ? parseOptionalInt(opts.limit) : undefined,
|
|
385
|
+
force: opts.force === "true",
|
|
386
|
+
batchSize: opts["batch-size"] ? parseOptionalInt(opts["batch-size"]) : undefined,
|
|
387
|
+
});
|
|
388
|
+
return textResult([
|
|
389
|
+
...result.scopes.map((scoped) =>
|
|
390
|
+
`[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`
|
|
391
|
+
),
|
|
392
|
+
`Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`,
|
|
393
|
+
].join("\n"));
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return errorFrom(error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatStatsText(stats: ReturnType<TrapOperations["getStats"]>): string {
|
|
400
|
+
const sections: string[] = [];
|
|
401
|
+
if (stats.project) {
|
|
402
|
+
sections.push("── Project ──", formatStatsBlock(stats.project));
|
|
403
|
+
}
|
|
404
|
+
sections.push("── Global ──", formatStatsBlock(stats.global));
|
|
405
|
+
return sections.join("\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formatStatsBlock(stats: { total: number; byCategory: Record<string, number>; bySeverity: Record<string, number> }): string {
|
|
409
|
+
return [
|
|
410
|
+
` Total: ${stats.total}`,
|
|
411
|
+
" By category:",
|
|
412
|
+
...Object.entries(stats.byCategory).map(([category, count]) => ` ${category}: ${count}`),
|
|
413
|
+
" By severity:",
|
|
414
|
+
...Object.entries(stats.bySeverity).map(([severity, count]) => ` ${severity}: ${count}`),
|
|
415
|
+
].join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parseSearchMode(mode: string): SearchMode {
|
|
419
|
+
if ((SEARCH_MODES as readonly string[]).includes(mode)) return mode as SearchMode;
|
|
420
|
+
throw new Error(`Invalid search mode: ${mode}. Expected one of: ${SEARCH_MODES.join(", ")}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseId(value: string | undefined, usage: string): number | CommandResult {
|
|
424
|
+
if (value === undefined) return errorResult(usage);
|
|
425
|
+
const id = Number.parseInt(value, 10);
|
|
426
|
+
return Number.isNaN(id) ? errorResult("Error: id must be a number") : id;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseOptionalInt(value: string | undefined, fallback?: number): number {
|
|
430
|
+
if (value === undefined) {
|
|
431
|
+
if (fallback === undefined) throw new Error("Missing numeric value.");
|
|
432
|
+
return fallback;
|
|
433
|
+
}
|
|
434
|
+
const parsed = Number.parseInt(value, 10);
|
|
435
|
+
if (Number.isNaN(parsed)) throw new Error(`Invalid number: ${value}`);
|
|
436
|
+
return parsed;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function readQuery(positionals: string[]): string {
|
|
440
|
+
if (positionals.length > 0) return positionals.join(" ").trim();
|
|
441
|
+
if (process.stdin.isTTY) return "";
|
|
442
|
+
return readFileSync(0, "utf-8").trim();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseCsv(value?: string): string[] | undefined {
|
|
446
|
+
if (!value) return undefined;
|
|
447
|
+
return value
|
|
448
|
+
.split(",")
|
|
449
|
+
.map((item) => item.trim())
|
|
450
|
+
.filter(Boolean);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function mutationJsonResult<T extends Record<string, unknown> & { success: boolean }>(
|
|
454
|
+
value: T,
|
|
455
|
+
error: string
|
|
456
|
+
): CommandResult {
|
|
457
|
+
return jsonResult(mutationJsonPayload(value, error), value.success ? 0 : 1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function errorFrom(error: unknown): CommandResult {
|
|
461
|
+
return errorResult(`Error: ${errorMessage(error)}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function errorMessage(error: unknown): string {
|
|
465
|
+
return error instanceof Error ? error.message : String(error);
|
|
466
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type FreshEmbedding,
|
|
9
9
|
type StoredEmbedding,
|
|
10
10
|
} from "../lib/embedder";
|
|
11
|
+
import type { EmbeddingStateCounts } from "../lib/embedding-health";
|
|
11
12
|
import { embeddingIsFresh, passageHashForTrap } from "../lib/trap-search-document";
|
|
12
13
|
import { countTraps, listTraps } from "./queries";
|
|
13
14
|
|
|
@@ -140,6 +141,38 @@ export function countEmbeddableTraps(
|
|
|
140
141
|
return countTraps(db, opts);
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
export function getEmbeddingStateCounts(
|
|
145
|
+
db: Database,
|
|
146
|
+
config: EmbeddingConfig | null,
|
|
147
|
+
opts: { scope?: string; category?: string; status?: TrapStatus | "all" } = {}
|
|
148
|
+
): EmbeddingStateCounts {
|
|
149
|
+
const traps = listTraps(db, {
|
|
150
|
+
scope: opts.scope,
|
|
151
|
+
category: opts.category,
|
|
152
|
+
status: opts.status,
|
|
153
|
+
limit: 100000,
|
|
154
|
+
});
|
|
155
|
+
const counts: EmbeddingStateCounts = {
|
|
156
|
+
total: traps.length,
|
|
157
|
+
fresh: 0,
|
|
158
|
+
stale: 0,
|
|
159
|
+
missing: 0,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const trap of traps) {
|
|
163
|
+
const embedding = getEmbedding(db, trap.id);
|
|
164
|
+
if (!embedding) {
|
|
165
|
+
counts.missing++;
|
|
166
|
+
} else if (config && embeddingIsFresh(trap, embedding, config)) {
|
|
167
|
+
counts.fresh++;
|
|
168
|
+
} else {
|
|
169
|
+
counts.stale++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return counts;
|
|
174
|
+
}
|
|
175
|
+
|
|
143
176
|
function rowToStoredEmbedding(row: TrapEmbeddingRow): StoredEmbedding {
|
|
144
177
|
return {
|
|
145
178
|
trap_id: row.trap_id,
|