codetrap 0.1.0
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/.env.example +3 -0
- package/LICENSE +22 -0
- package/README.md +305 -0
- package/docs/installation.md +306 -0
- package/package.json +62 -0
- package/scripts/build-release.ts +64 -0
- package/scripts/check-release-version.ts +19 -0
- package/skills/codetrap-add/SKILL.md +65 -0
- package/skills/codetrap-check/SKILL.md +47 -0
- package/skills/codetrap-search/SKILL.md +43 -0
- package/src/commands/router.ts +407 -0
- package/src/db/connection.ts +36 -0
- package/src/db/embedding-queries.ts +154 -0
- package/src/db/queries.ts +296 -0
- package/src/db/repository.ts +141 -0
- package/src/db/schema.ts +205 -0
- package/src/domain/trap.ts +304 -0
- package/src/index.ts +58 -0
- package/src/lib/constants.ts +56 -0
- package/src/lib/embedder.ts +133 -0
- package/src/lib/embedding-job.ts +68 -0
- package/src/lib/format.ts +97 -0
- package/src/lib/fts-query.ts +17 -0
- package/src/lib/scope.ts +30 -0
- package/src/lib/search-normalizer.ts +92 -0
- package/src/lib/search-result-card.ts +38 -0
- package/src/lib/search-service.ts +189 -0
- package/src/lib/store.ts +272 -0
- package/src/lib/trap-archive.ts +91 -0
- package/src/lib/trap-json-fields.ts +42 -0
- package/src/lib/trap-operations.ts +127 -0
- package/src/lib/trap-search-document.ts +73 -0
- package/src/mcp/resources.ts +26 -0
- package/src/mcp/server.ts +167 -0
- package/src/mcp/tools.ts +106 -0
- package/src/mcp-server.ts +6 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { chmod, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
type ReleaseTarget = {
|
|
7
|
+
target: string;
|
|
8
|
+
name: string;
|
|
9
|
+
windows?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const releaseDir = join(process.cwd(), "dist", "release");
|
|
13
|
+
|
|
14
|
+
const targets: ReleaseTarget[] = [
|
|
15
|
+
{ target: "bun-darwin-arm64", name: "codetrap-darwin-arm64" },
|
|
16
|
+
{ target: "bun-darwin-x64", name: "codetrap-darwin-x64" },
|
|
17
|
+
{ target: "bun-linux-x64-baseline", name: "codetrap-linux-x64" },
|
|
18
|
+
{ target: "bun-linux-arm64", name: "codetrap-linux-arm64" },
|
|
19
|
+
{ target: "bun-windows-x64-baseline", name: "codetrap-windows-x64.exe", windows: true },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
23
|
+
await mkdir(releaseDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
for (const item of targets) {
|
|
26
|
+
const outfile = join(releaseDir, item.name);
|
|
27
|
+
console.log(`Building ${item.name} (${item.target})`);
|
|
28
|
+
const proc = Bun.spawnSync({
|
|
29
|
+
cmd: [
|
|
30
|
+
"bun",
|
|
31
|
+
"build",
|
|
32
|
+
"--compile",
|
|
33
|
+
`--target=${item.target}`,
|
|
34
|
+
"./src/index.ts",
|
|
35
|
+
"--outfile",
|
|
36
|
+
outfile,
|
|
37
|
+
],
|
|
38
|
+
stdout: "inherit",
|
|
39
|
+
stderr: "inherit",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!proc.success) {
|
|
43
|
+
process.exit(proc.exitCode ?? 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!item.windows) {
|
|
47
|
+
await chmod(outfile, 0o755);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const files = (await readdir(releaseDir)).filter((file) => file !== "sha256sums.txt").sort();
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const path = join(releaseDir, file);
|
|
56
|
+
const bytes = await Bun.file(path).arrayBuffer();
|
|
57
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
58
|
+
const hex = [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
59
|
+
lines.push(`${hex} ${file}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await writeFile(join(releaseDir, "sha256sums.txt"), `${lines.join("\n")}\n`);
|
|
63
|
+
console.log(`Release assets written to ${releaseDir}`);
|
|
64
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const tag = process.argv[2] ?? process.env.RELEASE_TAG ?? process.env.GITHUB_REF_NAME;
|
|
4
|
+
|
|
5
|
+
if (!tag) {
|
|
6
|
+
console.error("Release tag is required. Pass v<package.version> as an argument.");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const packageJson = await Bun.file("package.json").json() as { version?: string };
|
|
11
|
+
const expected = `v${packageJson.version}`;
|
|
12
|
+
|
|
13
|
+
if (tag !== expected) {
|
|
14
|
+
console.error(`Release tag ${tag} does not match package version ${expected}.`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Release tag ${tag} matches package version ${packageJson.version}.`);
|
|
19
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codetrap-add
|
|
3
|
+
description: Record a coding pitfall as a structured codetrap entry. Use when the user wants to save a lesson learned, recurring AI mistake, project convention, or runs /codetrap-add.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are helping the user record a "coding pitfall" (a mistake pattern that AI coding assistants tend to make, and the correct approach). These pitfalls are stored in a local database and will be used to warn AI in future sessions.
|
|
7
|
+
|
|
8
|
+
## Step 1: Gather information
|
|
9
|
+
|
|
10
|
+
Ask the user to describe what went wrong. Guide them to provide:
|
|
11
|
+
|
|
12
|
+
1. **What was the AI asked to do?** (the triggering context)
|
|
13
|
+
2. **What did the AI do wrong?** (the mistake)
|
|
14
|
+
3. **What should it have done instead?** (the fix)
|
|
15
|
+
4. **How serious is this?** (warning / error / critical)
|
|
16
|
+
|
|
17
|
+
If the user already provided enough detail, don't re-ask — just proceed to structuring.
|
|
18
|
+
|
|
19
|
+
## Step 2: Determine scope
|
|
20
|
+
|
|
21
|
+
Ask the user (or infer from context):
|
|
22
|
+
- **project**: This pitfall is specific to the current project (e.g., "this project uses fetchWrapper instead of axios")
|
|
23
|
+
- **global**: This pitfall applies across all projects (e.g., "never store secrets in frontend code")
|
|
24
|
+
|
|
25
|
+
If a `.codetrap/` directory exists in the project, default to `project`. Otherwise default to `global`.
|
|
26
|
+
|
|
27
|
+
## Step 3: Determine category
|
|
28
|
+
|
|
29
|
+
Pick the best-fitting category:
|
|
30
|
+
- `api` — HTTP requests, REST, GraphQL, API design
|
|
31
|
+
- `database` — SQL, ORM, migrations, connections
|
|
32
|
+
- `auth` — Authentication, authorization, sessions, tokens
|
|
33
|
+
- `convention` — Project-specific conventions, naming, file structure
|
|
34
|
+
- `security` — Vulnerabilities, secrets, input validation
|
|
35
|
+
- `performance` — Bottlenecks, caching, bundle size
|
|
36
|
+
- `bug` — Common logic errors, edge cases
|
|
37
|
+
- `other` — Everything else
|
|
38
|
+
|
|
39
|
+
## Step 4: Structure and save
|
|
40
|
+
|
|
41
|
+
Convert the user's description into this JSON structure and call the CLI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
codetrap add --json '{
|
|
45
|
+
"title": "<one-line summary>",
|
|
46
|
+
"category": "<category>",
|
|
47
|
+
"scope": "<project|global>",
|
|
48
|
+
"context": "<when does this happen?>",
|
|
49
|
+
"mistake": "<what the AI does wrong>",
|
|
50
|
+
"fix": "<what should be done instead>",
|
|
51
|
+
"tags": ["<tag1>", "<tag2>"],
|
|
52
|
+
"severity": "<warning|error|critical>",
|
|
53
|
+
"before_code": "<wrong code snippet (optional)>",
|
|
54
|
+
"after_code": "<correct code snippet (optional)>"
|
|
55
|
+
}'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If the CLI is not available, use the MCP tool `add_trap` instead.
|
|
59
|
+
|
|
60
|
+
## Step 5: Confirm
|
|
61
|
+
|
|
62
|
+
Tell the user:
|
|
63
|
+
- The trap ID and scope
|
|
64
|
+
- Suggest running `codetrap stats` to see the growing knowledge base
|
|
65
|
+
- Remind them: next time AI works in this area, `/codetrap-check` will catch this pitfall
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codetrap-check
|
|
3
|
+
description: Check the codetrap pitfall database before code changes and apply relevant lessons. Use before non-trivial coding work, when touching risky areas, or when the user runs /codetrap-check.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Before generating any non-trivial code, pause and check the codetrap database for relevant pitfalls. This is a "pre-flight check" that prevents you from repeating known mistakes.
|
|
7
|
+
|
|
8
|
+
## When to trigger
|
|
9
|
+
|
|
10
|
+
Run this check when:
|
|
11
|
+
1. The user asks you to write or modify code
|
|
12
|
+
2. The task touches an area with recorded pitfalls (API, auth, database, security, etc.)
|
|
13
|
+
3. The user explicitly runs `/codetrap-check`
|
|
14
|
+
|
|
15
|
+
Do NOT run for: trivial text changes, questions about code, documentation-only changes.
|
|
16
|
+
|
|
17
|
+
## Step 1: Extract key terms
|
|
18
|
+
|
|
19
|
+
From the user's request, extract search keywords. Focus on:
|
|
20
|
+
- Technology names: "axios", "prisma", "jwt", "react"
|
|
21
|
+
- Patterns: "middleware", "endpoint", "migration", "hook"
|
|
22
|
+
- Domains: "authentication", "database", "routing", "state"
|
|
23
|
+
|
|
24
|
+
## Step 2: Search the database
|
|
25
|
+
|
|
26
|
+
Call the MCP tool `search_traps` with the extracted keywords. Search both project and global scopes.
|
|
27
|
+
|
|
28
|
+
## Step 3: Apply the lessons
|
|
29
|
+
|
|
30
|
+
For each relevant trap found:
|
|
31
|
+
1. Read the action card's `avoid` and `do_instead`
|
|
32
|
+
2. If the card is highly relevant and you are about to edit code, call `get_trap` with `next_action.details_args.id` and `next_action.details_args.scope`
|
|
33
|
+
3. Adjust your code generation to follow the correct approach
|
|
34
|
+
4. If a trap matches exactly what you were about to do, explicitly tell the user: "I was about to [avoid], but the codetrap database says [do_instead]. I'll do it the right way."
|
|
35
|
+
|
|
36
|
+
## Step 4: Report
|
|
37
|
+
|
|
38
|
+
Briefly tell the user which traps you found and how you adjusted:
|
|
39
|
+
```
|
|
40
|
+
Checked codetrap: found 2 relevant pitfalls. Avoiding [X] and using [Y] instead.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If no traps found, say nothing — don't waste tokens.
|
|
44
|
+
|
|
45
|
+
## Step 5: Record new pitfalls
|
|
46
|
+
|
|
47
|
+
If while writing code you discover a NEW pitfall that isn't in the database, suggest: "This seems like a recurring pitfall. Want me to record it with `/codetrap-add`?"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codetrap-search
|
|
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
|
+
---
|
|
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`.
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
- Before writing code in a new area (e.g., "I need to write authentication middleware")
|
|
11
|
+
- When the user asks "have we seen issues with X before?"
|
|
12
|
+
- When starting a task that resembles something that caused problems in the past
|
|
13
|
+
|
|
14
|
+
## How to search
|
|
15
|
+
|
|
16
|
+
### Via MCP (preferred)
|
|
17
|
+
|
|
18
|
+
Call the `search_traps` MCP tool:
|
|
19
|
+
```
|
|
20
|
+
search_traps(query="<keywords>", scope=<optional>, category=<optional>)
|
|
21
|
+
```
|
|
22
|
+
|
|
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
|
+
|
|
25
|
+
### Via CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
codetrap search "<keywords>" [--scope project|global] [--category api|database|...]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How to present results
|
|
32
|
+
|
|
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
|
|
36
|
+
4. If no results, tell the user (this is a new area with no recorded pitfalls yet)
|
|
37
|
+
|
|
38
|
+
## Example
|
|
39
|
+
|
|
40
|
+
User: "I need to add a new API endpoint"
|
|
41
|
+
→ Search: `search_traps(query="API endpoint")`
|
|
42
|
+
→ Results show: "Don't use axios, use fetchWrapper" (project, error)
|
|
43
|
+
→ Tell user: "I see a project convention: always use fetchWrapper instead of axios. I'll follow that."
|
|
@@ -0,0 +1,407 @@
|
|
|
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 { TrapOperations } from "../lib/trap-operations";
|
|
7
|
+
|
|
8
|
+
type ParsedArgs = {
|
|
9
|
+
opts: Record<string, string>;
|
|
10
|
+
positionals: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
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(", ")}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
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);
|
|
407
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { getGlobalDB, getProjectDB } from "../lib/scope";
|
|
3
|
+
import { initSchema } from "./schema";
|
|
4
|
+
|
|
5
|
+
let globalDB: Database | null = null;
|
|
6
|
+
const projectDBs = new Map<string, Database>();
|
|
7
|
+
|
|
8
|
+
export function openDatabase(path = ":memory:"): Database {
|
|
9
|
+
const db = new Database(path);
|
|
10
|
+
configureDatabase(db);
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function openGlobal(): Database {
|
|
15
|
+
if (!globalDB) {
|
|
16
|
+
const path = getGlobalDB();
|
|
17
|
+
globalDB = openDatabase(path);
|
|
18
|
+
}
|
|
19
|
+
return globalDB;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function openProject(root: string): Database {
|
|
23
|
+
const path = getProjectDB(root);
|
|
24
|
+
if (!projectDBs.has(path)) {
|
|
25
|
+
const db = openDatabase(path);
|
|
26
|
+
projectDBs.set(path, db);
|
|
27
|
+
}
|
|
28
|
+
return projectDBs.get(path)!;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function configureDatabase(db: Database): void {
|
|
32
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
33
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
34
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
35
|
+
initSchema(db);
|
|
36
|
+
}
|