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
@@ -3,7 +3,7 @@ name: codetrap-search
3
3
  description: Search the codetrap pitfall database for known mistakes and project lessons. Use when starting work in a new area or when the user asks whether similar issues were seen before.
4
4
  ---
5
5
 
6
- Search the codetrap database for recorded pitfalls matching the user's query. Use the MCP tool `search_traps` or the CLI `codetrap search`.
6
+ Search the codetrap database for recorded pitfalls matching the user's query. Default to the CLI `codetrap search --json`; use MCP only when it is already available and scoped to the right project.
7
7
 
8
8
  ## When to use
9
9
 
@@ -13,31 +13,51 @@ Search the codetrap database for recorded pitfalls matching the user's query. Us
13
13
 
14
14
  ## How to search
15
15
 
16
- ### Via MCP (preferred)
16
+ ### Via CLI (preferred)
17
17
 
18
- Call the `search_traps` MCP tool:
19
- ```
20
- search_traps(query="<keywords>", scope=<optional>, category=<optional>)
18
+ Run from the current project cwd:
19
+
20
+ ```bash
21
+ codetrap search "<keywords>" --mode hybrid --json
21
22
  ```
22
23
 
23
- `search_traps` returns compact action cards. Each card includes `avoid`, `do_instead`, and `next_action.details_args` with both `id` and `scope`. Preserve that scope when calling `get_trap`.
24
+ For scoped work, pass known file/module/owner context:
24
25
 
25
- ### Via CLI
26
+ ```bash
27
+ codetrap search "<keywords>" --path src/db/repository.ts --module db --owner platform --json
28
+ ```
29
+
30
+ Or pipe the query through stdin:
26
31
 
27
32
  ```bash
28
- codetrap search "<keywords>" [--scope project|global] [--category api|database|...]
33
+ echo "<keywords>" | codetrap search --mode hybrid --json
34
+ ```
35
+
36
+ CLI JSON returns compact action cards. Each card includes `avoid`, `do_instead`, and `next_action.command`; run that command to inspect full details.
37
+
38
+ ### Via MCP (optional)
39
+
40
+ Call the `search_traps` MCP tool when available:
41
+ ```
42
+ search_traps(query="<keywords>", scope=<optional>, category=<optional>, path=<optional>, module=<optional>, owner=<optional>, cwd=<optional>)
29
43
  ```
30
44
 
45
+ `search_traps` returns compact action cards. Each card includes `avoid`, `do_instead`, and `next_action.details_args` with both `id` and `scope`. Preserve that scope when calling `get_trap`.
46
+
47
+ ## Result review rule
48
+
49
+ Review the top 3 action cards before deciding that no trap applies. Do not rely only on the first result; a relevant trap can rank second or third. If fewer than 3 cards are returned, review all returned cards.
50
+
31
51
  ## How to present results
32
52
 
33
- 1. Show the most relevant traps first (project scope traps before global)
34
- 2. Summarize each card's title, severity, `avoid`, and `do_instead`
35
- 3. If a card is highly relevant and you are about to edit code, call `get_trap` with the card's `id` and `scope` before proceeding
53
+ 1. Show the most relevant reviewed traps first (project scope traps before global)
54
+ 2. Summarize each reviewed card's title, severity, `avoid`, and `do_instead`
55
+ 3. If any reviewed card is highly relevant, or has `critical`/`error` severity and is plausibly related, and you are about to edit code, run the CLI `next_action.command`; with MCP, call `get_trap` with the card's `id` and `scope` before proceeding
36
56
  4. If no results, tell the user (this is a new area with no recorded pitfalls yet)
37
57
 
38
58
  ## Example
39
59
 
40
60
  User: "I need to add a new API endpoint"
41
- → Search: `search_traps(query="API endpoint")`
61
+ → Search: `codetrap search "API endpoint" --mode hybrid --json`
42
62
  → Results show: "Don't use axios, use fetchWrapper" (project, error)
43
63
  → Tell user: "I see a project convention: always use fetchWrapper instead of axios. I'll follow that."
@@ -0,0 +1,29 @@
1
+ export type CommandResult = {
2
+ exitCode: number;
3
+ stdout?: string;
4
+ stderr?: string;
5
+ };
6
+
7
+ export function textResult(stdout = "", exitCode = 0): CommandResult {
8
+ return { exitCode, stdout };
9
+ }
10
+
11
+ export function jsonResult(value: unknown, exitCode = 0): CommandResult {
12
+ return textResult(JSON.stringify(value, null, 2), exitCode);
13
+ }
14
+
15
+ export function errorResult(message: string, exitCode = 1): CommandResult {
16
+ return { exitCode, stderr: message };
17
+ }
18
+
19
+ export function renderCommandResult(result: CommandResult): void {
20
+ if (result.stdout !== undefined && result.stdout !== "") {
21
+ console.log(result.stdout);
22
+ }
23
+ if (result.stderr !== undefined && result.stderr !== "") {
24
+ console.error(result.stderr);
25
+ }
26
+ if (result.exitCode !== 0) {
27
+ process.exit(result.exitCode);
28
+ }
29
+ }
@@ -1,407 +1,13 @@
1
- import { readFileSync } from "node:fs";
2
1
  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 { TrapOperations } from "../lib/trap-operations";
2
+ import { renderCommandResult, type CommandResult } from "./command-result";
3
+ import { executeCommand as executeWorkflow, parseArgs } from "./workflow";
7
4
 
8
- type ParsedArgs = {
9
- opts: Record<string, string>;
10
- positionals: string[];
11
- };
5
+ export { parseArgs };
12
6
 
13
7
  export async function run(strip: string[], store: TrapStore): Promise<void> {
14
- const sub = strip[0];
15
- const args = strip.slice(1);
16
- const operations = new TrapOperations(store);
17
-
18
- switch (sub) {
19
- case "add":
20
- return cmdAdd(args, operations);
21
- case "search":
22
- return cmdSearch(args, operations);
23
- case "list":
24
- return cmdList(args, operations);
25
- case "show":
26
- return cmdShow(args, operations);
27
- case "edit":
28
- return cmdEdit(args, operations);
29
- case "delete":
30
- case "rm":
31
- return cmdDelete(args, operations);
32
- case "add_trap_evidence":
33
- case "add-evidence":
34
- return cmdAddTrapEvidence(args, operations);
35
- case "archive_trap":
36
- case "archive":
37
- return cmdArchiveTrap(args, operations);
38
- case "supersede_trap":
39
- case "supersede":
40
- return cmdSupersedeTrap(args, operations);
41
- case "init":
42
- return cmdInit(args, store);
43
- case "export":
44
- return cmdExport(args, operations);
45
- case "import":
46
- return cmdImport(args, operations);
47
- case "stats":
48
- return cmdStats(args, operations);
49
- case "embed":
50
- return cmdEmbed(args, store);
51
- default:
52
- console.log(`Unknown command: ${sub}`);
53
- console.log("Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, embed");
54
- process.exit(1);
55
- }
56
- }
57
-
58
- export function parseArgs(args: string[]): ParsedArgs {
59
- const opts: Record<string, string> = {};
60
- const positionals: string[] = [];
61
- for (let i = 0; i < args.length; i++) {
62
- if (args[i].startsWith("--")) {
63
- const key = args[i].slice(2);
64
- const val = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
65
- opts[key] = val;
66
- } else {
67
- positionals.push(args[i]);
68
- }
69
- }
70
- return { opts, positionals };
71
- }
72
-
73
- // ---- commands ----
74
-
75
- function cmdInit(_args: string[], store: TrapStore): void {
76
- if (store.hasProject()) {
77
- console.log(`Already in a project: ${store.getProjectRoot()}`);
78
- return;
79
- }
80
- // init is handled by index.ts before creating the store
81
- console.log("Project initialized.");
82
- }
83
-
84
- function cmdAdd(args: string[], operations: TrapOperations): void {
85
- const { opts, positionals } = parseArgs(args);
86
- // --json mode for AI/script usage
87
- if (opts.json !== undefined) {
88
- if (!opts.json || opts.json === "true") {
89
- console.error("Error: --json requires a JSON string argument");
90
- process.exit(1);
91
- }
92
- try {
93
- const input = JSON.parse(opts.json);
94
- const result = operations.addTrap(input);
95
- console.log(`Trap #${result.id} added to ${result.scope} scope.`);
96
- } catch (e: any) {
97
- console.error(`Error: ${e.message}`);
98
- process.exit(1);
99
- }
100
- return;
101
- }
102
-
103
- // Quick mode: codetrap add "title"
104
- if (positionals.length > 0) {
105
- console.log(`Use --json mode for structured input.`);
106
- console.log(`Quick add: codetrap add --json '{"title":"${positionals.join(" ")}","category":"other","scope":"global","context":"...","mistake":"...","fix":"..."}'`);
107
- return;
108
- }
109
-
110
- // Interactive mode
111
- console.log("Interactive mode not yet implemented. Use --json for now.");
112
- console.log('Example: codetrap add --json \'{"title":"...","category":"convention","scope":"project","context":"...","mistake":"...","fix":"..."}\'');
113
- }
114
-
115
- async function cmdSearch(args: string[], operations: TrapOperations): Promise<void> {
116
- const { opts, positionals } = parseArgs(args);
117
- if (positionals.length === 0) {
118
- console.error("Usage: codetrap search <query> [--category X] [--limit N] [--mode fts|semantic|hybrid] [--status active|superseded|archived|all]");
119
- process.exit(1);
120
- }
121
- let cards: Awaited<ReturnType<TrapOperations["searchTrapCards"]>>;
122
- try {
123
- const mode = opts.mode ? parseSearchMode(opts.mode) : undefined;
124
- cards = await operations.searchTrapCards({
125
- query: positionals.join(" "),
126
- category: opts.category,
127
- scope: opts.scope,
128
- limit: opts.limit ? parseInt(opts.limit) : 20,
129
- mode,
130
- status: opts.status,
131
- });
132
- } catch (e: any) {
133
- console.error(`Error: ${e.message}`);
134
- process.exit(1);
135
- }
136
-
137
- let count = 0;
138
- for (const card of cards) {
139
- console.log(formatTrapActionCard(card));
140
- console.log("");
141
- count++;
142
- }
143
- if (count === 0) console.log("No traps found.");
144
- }
145
-
146
- function cmdList(args: string[], operations: TrapOperations): void {
147
- const { opts } = parseArgs(args);
148
-
149
- let groups: { traps: Trap[]; scope: string }[];
150
- try {
151
- groups = operations.listTraps({
152
- category: opts.category,
153
- scope: opts.scope,
154
- status: opts.status,
155
- limit: opts.limit ? parseInt(opts.limit) : 50,
156
- });
157
- } catch (e: any) {
158
- console.error(`Error: ${e.message}`);
159
- process.exit(1);
160
- }
161
-
162
- let count = 0;
163
- for (const group of groups) {
164
- for (const t of group.traps) {
165
- console.log(formatTrapShort(t, group.scope));
166
- count++;
167
- }
168
- }
169
- if (count === 0) console.log("No traps found.");
170
- }
171
-
172
- function cmdShow(args: string[], operations: TrapOperations): void {
173
- const { opts, positionals } = parseArgs(args);
174
- if (positionals.length === 0) {
175
- console.error("Usage: codetrap show <id> [--scope project|global]");
176
- process.exit(1);
177
- }
178
-
179
- const id = parseInt(positionals[0]);
180
- if (isNaN(id)) {
181
- console.error("Error: id must be a number");
182
- process.exit(1);
183
- }
184
-
185
- const result = operations.getTrapDetails(id, opts.scope);
186
- if (!result) {
187
- console.error(`Trap #${id} not found.`);
188
- process.exit(1);
189
- }
190
-
191
- operations.hitTrap(id, result.scope);
192
- console.log(formatTrapDetails(result));
193
- }
194
-
195
- function cmdEdit(args: string[], operations: TrapOperations): void {
196
- const { opts, positionals } = parseArgs(args);
197
- if (positionals.length === 0) {
198
- console.error("Usage: codetrap edit <id> --json '{\"title\":\"new title\"}' [--scope project|global]");
199
- process.exit(1);
200
- }
201
-
202
- const id = parseInt(positionals[0]);
203
- if (isNaN(id)) {
204
- console.error("Error: id must be a number");
205
- process.exit(1);
206
- }
207
-
208
- if (!opts.json) {
209
- console.error("Error: edit requires --json for now.");
210
- console.error("Example: codetrap edit 1 --json '{\"title\":\"new title\"}' [--scope project|global]");
211
- process.exit(1);
212
- }
213
-
214
- try {
215
- const parsed = JSON.parse(opts.json);
216
- const result = operations.updateTrap(id, parsed, opts.scope);
217
- if (result.success) {
218
- console.log(`Trap #${id} updated in ${result.scope} scope.`);
219
- } else {
220
- console.error(`Trap #${id} not found or no fields changed.`);
221
- process.exit(1);
222
- }
223
- } catch (e: any) {
224
- console.error(`Error: ${e.message}`);
225
- process.exit(1);
226
- }
227
- }
228
-
229
- function cmdDelete(args: string[], operations: TrapOperations): void {
230
- const { opts, positionals } = parseArgs(args);
231
- if (positionals.length === 0) {
232
- console.error("Usage: codetrap delete <id> [--scope project|global]");
233
- process.exit(1);
234
- }
235
-
236
- const id = parseInt(positionals[0]);
237
- if (isNaN(id)) {
238
- console.error("Error: id must be a number");
239
- process.exit(1);
240
- }
241
-
242
- const result = operations.deleteTrap(id, opts.scope);
243
- if (result.success) {
244
- console.log(`Trap #${id} deleted from ${result.scope} scope.`);
245
- } else {
246
- console.error(`Trap #${id} not found.`);
247
- process.exit(1);
248
- }
249
- }
250
-
251
- function cmdAddTrapEvidence(args: string[], operations: TrapOperations): void {
252
- const { opts, positionals } = parseArgs(args);
253
- if (positionals.length === 0) {
254
- console.error("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]");
255
- process.exit(1);
256
- }
257
-
258
- const id = parseInt(positionals[0]);
259
- if (isNaN(id)) {
260
- console.error("Error: id must be a number");
261
- process.exit(1);
262
- }
263
-
264
- try {
265
- const input = opts.json ? JSON.parse(opts.json) : {
266
- source_type: opts.source_type ?? opts["source-type"],
267
- source_ref: opts.source_ref ?? opts["source-ref"],
268
- observed_at: opts.observed_at ?? opts["observed-at"],
269
- related_files: parseCsv(opts.related_files ?? opts["related-files"]),
270
- note: opts.note,
271
- };
272
- const result = operations.addTrapEvidence(id, input, opts.scope);
273
- if (!result.success) {
274
- console.error(`Trap #${id} not found.`);
275
- process.exit(1);
276
- }
277
- console.log(`Evidence #${result.evidence_id} added to trap #${id} in ${result.scope} scope.`);
278
- } catch (e: any) {
279
- console.error(`Error: ${e.message}`);
280
- process.exit(1);
281
- }
282
- }
283
-
284
- function cmdArchiveTrap(args: string[], operations: TrapOperations): void {
285
- const { opts, positionals } = parseArgs(args);
286
- if (positionals.length === 0) {
287
- console.error("Usage: codetrap archive_trap <id> [--scope project|global]");
288
- process.exit(1);
289
- }
290
-
291
- const id = parseInt(positionals[0]);
292
- if (isNaN(id)) {
293
- console.error("Error: id must be a number");
294
- process.exit(1);
295
- }
296
-
297
- const result = operations.archiveTrap(id, opts.scope);
298
- if (result.success) {
299
- console.log(`Trap #${id} archived in ${result.scope} scope.`);
300
- } else {
301
- console.error(`Trap #${id} not found.`);
302
- process.exit(1);
303
- }
304
- }
305
-
306
- function cmdSupersedeTrap(args: string[], operations: TrapOperations): void {
307
- const { opts, positionals } = parseArgs(args);
308
- if (positionals.length < 2) {
309
- console.error("Usage: codetrap supersede_trap <old_id> <new_id> [--scope project|global] [--state_key key]");
310
- process.exit(1);
311
- }
312
-
313
- const id = parseInt(positionals[0]);
314
- const supersededById = parseInt(positionals[1]);
315
- if (isNaN(id) || isNaN(supersededById)) {
316
- console.error("Error: ids must be numbers");
317
- process.exit(1);
318
- }
319
-
320
- const result = operations.supersedeTrap(id, supersededById, opts.scope, opts.state_key ?? opts["state-key"]);
321
- if (result.success) {
322
- console.log(`Trap #${id} superseded by #${supersededById} in ${result.scope} scope.`);
323
- } else {
324
- console.error(`Trap #${id} or #${supersededById} not found in the same scope.`);
325
- process.exit(1);
326
- }
327
- }
328
-
329
- function cmdExport(args: string[], operations: TrapOperations): void {
330
- const { opts } = parseArgs(args);
331
- const traps = operations.exportTraps(opts.scope);
332
- console.log(JSON.stringify(traps, null, 2));
333
- }
334
-
335
- function cmdImport(args: string[], operations: TrapOperations): void {
336
- const { positionals } = parseArgs(args);
337
- if (positionals.length === 0) {
338
- console.error("Usage: codetrap import <file.json>");
339
- process.exit(1);
340
- }
341
-
342
- const data = readFileSync(positionals[0], "utf-8");
343
- const traps = JSON.parse(data);
344
- if (!Array.isArray(traps)) {
345
- console.error("Error: JSON file must contain an array of traps");
346
- process.exit(1);
347
- }
348
-
349
- const count = operations.importTraps(traps);
350
- console.log(`Imported ${count} traps.`);
351
- }
352
-
353
- function cmdStats(_args: string[], operations: TrapOperations): void {
354
- const stats = operations.getStats();
355
-
356
- if (stats.project) {
357
- console.log("── Project ──");
358
- printStats(stats.project);
359
- }
360
- console.log("── Global ──");
361
- printStats(stats.global);
362
- }
363
-
364
- async function cmdEmbed(args: string[], store: TrapStore): Promise<void> {
365
- const { opts } = parseArgs(args);
366
- try {
367
- const result = await store.ensureEmbeddings({
368
- scope: opts.scope,
369
- category: opts.category,
370
- limit: opts.limit ? parseInt(opts.limit) : undefined,
371
- force: opts.force === "true",
372
- batchSize: opts["batch-size"] ? parseInt(opts["batch-size"]) : undefined,
373
- });
374
- for (const scoped of result.scopes) {
375
- console.log(`[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`);
376
- }
377
- console.log(`Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`);
378
- } catch (e: any) {
379
- console.error(`Error: ${e.message}`);
380
- process.exit(1);
381
- }
382
- }
383
-
384
- function printStats(s: { total: number; byCategory: Record<string, number>; bySeverity: Record<string, number> }): void {
385
- console.log(` Total: ${s.total}`);
386
- console.log(" By category:");
387
- for (const [cat, count] of Object.entries(s.byCategory)) {
388
- console.log(` ${cat}: ${count}`);
389
- }
390
- console.log(" By severity:");
391
- for (const [sev, count] of Object.entries(s.bySeverity)) {
392
- console.log(` ${sev}: ${count}`);
393
- }
394
- }
395
-
396
- function parseSearchMode(mode: string): SearchMode {
397
- if ((SEARCH_MODES as readonly string[]).includes(mode)) return mode as SearchMode;
398
- throw new Error(`Invalid search mode: ${mode}. Expected one of: ${SEARCH_MODES.join(", ")}`);
8
+ renderCommandResult(await executeWorkflow(strip, store));
399
9
  }
400
10
 
401
- function parseCsv(value?: string): string[] | undefined {
402
- if (!value) return undefined;
403
- return value
404
- .split(",")
405
- .map((item) => item.trim())
406
- .filter(Boolean);
11
+ export async function executeCommand(strip: string[], store: TrapStore): Promise<CommandResult> {
12
+ return executeWorkflow(strip, store);
407
13
  }