codetrap 0.1.2 → 0.1.4

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