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
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { CATEGORIES, SEVERITIES, SCOPES, SCHEMA_VERSION } from "../lib/constants";
|
|
3
|
+
import { buildTrapSearchText } from "../lib/trap-search-document";
|
|
4
|
+
|
|
5
|
+
export function initSchema(db: Database): void {
|
|
6
|
+
// Schema version table — add migrations here when breaking schema changes are needed
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
9
|
+
version INTEGER NOT NULL
|
|
10
|
+
);
|
|
11
|
+
`);
|
|
12
|
+
|
|
13
|
+
const versionRow = db.query("SELECT version FROM schema_version").get() as { version: number } | null;
|
|
14
|
+
const current = versionRow?.version ?? 0;
|
|
15
|
+
|
|
16
|
+
if (current < SCHEMA_VERSION) {
|
|
17
|
+
applyMigrations(db, current);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!versionRow) {
|
|
21
|
+
db.exec(`
|
|
22
|
+
INSERT INTO schema_version (version) VALUES (${SCHEMA_VERSION})
|
|
23
|
+
`);
|
|
24
|
+
} else if (current === 0) {
|
|
25
|
+
db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyMigrations(db: Database, from: number): void {
|
|
30
|
+
// v0 → v1: initial schema
|
|
31
|
+
if (from < 1) {
|
|
32
|
+
db.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS traps (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
title TEXT NOT NULL,
|
|
36
|
+
category TEXT NOT NULL CHECK(category IN ('${CATEGORIES.join("','")}')),
|
|
37
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
38
|
+
scope TEXT NOT NULL CHECK(scope IN ('${SCOPES.join("','")}')),
|
|
39
|
+
context TEXT NOT NULL,
|
|
40
|
+
mistake TEXT NOT NULL,
|
|
41
|
+
fix TEXT NOT NULL,
|
|
42
|
+
before_code TEXT,
|
|
43
|
+
after_code TEXT,
|
|
44
|
+
severity TEXT NOT NULL DEFAULT 'warning' CHECK(severity IN ('${SEVERITIES.join("','")}')),
|
|
45
|
+
project_path TEXT,
|
|
46
|
+
hit_count INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
48
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS traps_fts USING fts5(
|
|
52
|
+
title, context, mistake, fix, tags,
|
|
53
|
+
content='traps',
|
|
54
|
+
content_rowid='id'
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
createFTSTriggersV1(db);
|
|
59
|
+
db.prepare("UPDATE schema_version SET version = ?").run(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (from < 2) {
|
|
63
|
+
if (!columnExists(db, "traps", "search_text")) {
|
|
64
|
+
db.exec("ALTER TABLE traps ADD COLUMN search_text TEXT NOT NULL DEFAULT ''");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rows = db
|
|
68
|
+
.query("SELECT id, title, context, mistake, fix, tags, before_code, after_code FROM traps")
|
|
69
|
+
.all() as {
|
|
70
|
+
id: number;
|
|
71
|
+
title: string;
|
|
72
|
+
context: string;
|
|
73
|
+
mistake: string;
|
|
74
|
+
fix: string;
|
|
75
|
+
tags: string;
|
|
76
|
+
before_code: string | null;
|
|
77
|
+
after_code: string | null;
|
|
78
|
+
}[];
|
|
79
|
+
|
|
80
|
+
const update = db.prepare("UPDATE traps SET search_text = ? WHERE id = ?");
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
update.run(buildTrapSearchText(row), row.id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dropFTS(db);
|
|
86
|
+
createFTSWithSearchText(db);
|
|
87
|
+
db.exec(`
|
|
88
|
+
INSERT INTO traps_fts(rowid, title, context, mistake, fix, tags, search_text)
|
|
89
|
+
SELECT id, title, context, mistake, fix, tags, search_text FROM traps;
|
|
90
|
+
`);
|
|
91
|
+
db.prepare("UPDATE schema_version SET version = ?").run(2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (from < 3) {
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS trap_embeddings (
|
|
97
|
+
trap_id INTEGER PRIMARY KEY REFERENCES traps(id) ON DELETE CASCADE,
|
|
98
|
+
provider TEXT NOT NULL,
|
|
99
|
+
model TEXT NOT NULL,
|
|
100
|
+
dimensions INTEGER NOT NULL,
|
|
101
|
+
passage_version INTEGER NOT NULL,
|
|
102
|
+
passage_hash TEXT NOT NULL,
|
|
103
|
+
embedding BLOB NOT NULL,
|
|
104
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
105
|
+
);
|
|
106
|
+
`);
|
|
107
|
+
db.prepare("UPDATE schema_version SET version = ?").run(3);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (from < 4) {
|
|
111
|
+
if (!columnExists(db, "traps", "state_key")) {
|
|
112
|
+
db.exec("ALTER TABLE traps ADD COLUMN state_key TEXT");
|
|
113
|
+
}
|
|
114
|
+
if (!columnExists(db, "traps", "status")) {
|
|
115
|
+
db.exec("ALTER TABLE traps ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
|
|
116
|
+
}
|
|
117
|
+
if (!columnExists(db, "traps", "supersedes_id")) {
|
|
118
|
+
db.exec("ALTER TABLE traps ADD COLUMN supersedes_id INTEGER REFERENCES traps(id) ON DELETE SET NULL");
|
|
119
|
+
}
|
|
120
|
+
if (!columnExists(db, "traps", "valid_from")) {
|
|
121
|
+
db.exec("ALTER TABLE traps ADD COLUMN valid_from TEXT");
|
|
122
|
+
db.exec("UPDATE traps SET valid_from = COALESCE(created_at, datetime('now')) WHERE valid_from IS NULL");
|
|
123
|
+
}
|
|
124
|
+
if (!columnExists(db, "traps", "valid_until")) {
|
|
125
|
+
db.exec("ALTER TABLE traps ADD COLUMN valid_until TEXT");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
db.exec(`
|
|
129
|
+
CREATE TABLE IF NOT EXISTS trap_evidence (
|
|
130
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
131
|
+
trap_id INTEGER NOT NULL REFERENCES traps(id) ON DELETE CASCADE,
|
|
132
|
+
source_type TEXT NOT NULL,
|
|
133
|
+
source_ref TEXT,
|
|
134
|
+
observed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
135
|
+
related_files TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
note TEXT,
|
|
137
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
138
|
+
);
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
db.prepare("UPDATE schema_version SET version = ?").run(4);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function columnExists(db: Database, table: string, column: string): boolean {
|
|
146
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
147
|
+
return rows.some((row) => row.name === column);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function dropFTS(db: Database): void {
|
|
151
|
+
db.exec(`
|
|
152
|
+
DROP TRIGGER IF EXISTS traps_ai;
|
|
153
|
+
DROP TRIGGER IF EXISTS traps_ad;
|
|
154
|
+
DROP TRIGGER IF EXISTS traps_au;
|
|
155
|
+
DROP TABLE IF EXISTS traps_fts;
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createFTSTriggersV1(db: Database): void {
|
|
160
|
+
db.exec(`
|
|
161
|
+
CREATE TRIGGER IF NOT EXISTS traps_ai AFTER INSERT ON traps BEGIN
|
|
162
|
+
INSERT INTO traps_fts(rowid, title, context, mistake, fix, tags)
|
|
163
|
+
VALUES (new.id, new.title, new.context, new.mistake, new.fix, new.tags);
|
|
164
|
+
END;
|
|
165
|
+
|
|
166
|
+
CREATE TRIGGER IF NOT EXISTS traps_ad AFTER DELETE ON traps BEGIN
|
|
167
|
+
INSERT INTO traps_fts(traps_fts, rowid, title, context, mistake, fix, tags)
|
|
168
|
+
VALUES ('delete', old.id, old.title, old.context, old.mistake, old.fix, old.tags);
|
|
169
|
+
END;
|
|
170
|
+
|
|
171
|
+
CREATE TRIGGER IF NOT EXISTS traps_au AFTER UPDATE ON traps BEGIN
|
|
172
|
+
INSERT INTO traps_fts(traps_fts, rowid, title, context, mistake, fix, tags)
|
|
173
|
+
VALUES ('delete', old.id, old.title, old.context, old.mistake, old.fix, old.tags);
|
|
174
|
+
INSERT INTO traps_fts(rowid, title, context, mistake, fix, tags)
|
|
175
|
+
VALUES (new.id, new.title, new.context, new.mistake, new.fix, new.tags);
|
|
176
|
+
END;
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createFTSWithSearchText(db: Database): void {
|
|
181
|
+
db.exec(`
|
|
182
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS traps_fts USING fts5(
|
|
183
|
+
title, context, mistake, fix, tags, search_text,
|
|
184
|
+
content='traps',
|
|
185
|
+
content_rowid='id'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE TRIGGER IF NOT EXISTS traps_ai AFTER INSERT ON traps BEGIN
|
|
189
|
+
INSERT INTO traps_fts(rowid, title, context, mistake, fix, tags, search_text)
|
|
190
|
+
VALUES (new.id, new.title, new.context, new.mistake, new.fix, new.tags, new.search_text);
|
|
191
|
+
END;
|
|
192
|
+
|
|
193
|
+
CREATE TRIGGER IF NOT EXISTS traps_ad AFTER DELETE ON traps BEGIN
|
|
194
|
+
INSERT INTO traps_fts(traps_fts, rowid, title, context, mistake, fix, tags, search_text)
|
|
195
|
+
VALUES ('delete', old.id, old.title, old.context, old.mistake, old.fix, old.tags, old.search_text);
|
|
196
|
+
END;
|
|
197
|
+
|
|
198
|
+
CREATE TRIGGER IF NOT EXISTS traps_au AFTER UPDATE ON traps BEGIN
|
|
199
|
+
INSERT INTO traps_fts(traps_fts, rowid, title, context, mistake, fix, tags, search_text)
|
|
200
|
+
VALUES ('delete', old.id, old.title, old.context, old.mistake, old.fix, old.tags, old.search_text);
|
|
201
|
+
INSERT INTO traps_fts(rowid, title, context, mistake, fix, tags, search_text)
|
|
202
|
+
VALUES (new.id, new.title, new.context, new.mistake, new.fix, new.tags, new.search_text);
|
|
203
|
+
END;
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CATEGORIES,
|
|
3
|
+
DEFAULT_SEVERITY,
|
|
4
|
+
EVIDENCE_SOURCE_TYPES,
|
|
5
|
+
SCOPES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
TRAP_STATUSES,
|
|
8
|
+
type Scope,
|
|
9
|
+
type TrapStatus,
|
|
10
|
+
} from "../lib/constants";
|
|
11
|
+
|
|
12
|
+
export type { TrapStatus } from "../lib/constants";
|
|
13
|
+
|
|
14
|
+
export interface Trap {
|
|
15
|
+
id: number;
|
|
16
|
+
title: string;
|
|
17
|
+
category: string;
|
|
18
|
+
tags: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
context: string;
|
|
21
|
+
mistake: string;
|
|
22
|
+
fix: string;
|
|
23
|
+
search_text: string;
|
|
24
|
+
before_code: string | null;
|
|
25
|
+
after_code: string | null;
|
|
26
|
+
severity: string;
|
|
27
|
+
state_key: string | null;
|
|
28
|
+
status: TrapStatus;
|
|
29
|
+
supersedes_id: number | null;
|
|
30
|
+
valid_from: string;
|
|
31
|
+
valid_until: string | null;
|
|
32
|
+
project_path: string | null;
|
|
33
|
+
hit_count: number;
|
|
34
|
+
created_at: string;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TrapInput {
|
|
39
|
+
title: string;
|
|
40
|
+
category: string;
|
|
41
|
+
tags?: string[];
|
|
42
|
+
scope: string;
|
|
43
|
+
context: string;
|
|
44
|
+
mistake: string;
|
|
45
|
+
fix: string;
|
|
46
|
+
before_code?: string;
|
|
47
|
+
after_code?: string;
|
|
48
|
+
severity?: string;
|
|
49
|
+
project_path?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TrapSearchResult {
|
|
53
|
+
trap: Trap;
|
|
54
|
+
rank: number;
|
|
55
|
+
sources?: ("fts" | "semantic")[];
|
|
56
|
+
score?: number;
|
|
57
|
+
diagnostics?: { code: string; message: string }[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TrapActionCard {
|
|
61
|
+
trap_id: number;
|
|
62
|
+
scope: Scope;
|
|
63
|
+
title: string;
|
|
64
|
+
why_relevant: string;
|
|
65
|
+
avoid: string;
|
|
66
|
+
do_instead: string;
|
|
67
|
+
severity: string;
|
|
68
|
+
score: number | null;
|
|
69
|
+
sources: ("fts" | "semantic")[];
|
|
70
|
+
next_action: {
|
|
71
|
+
details_tool: "get_trap";
|
|
72
|
+
details_args: {
|
|
73
|
+
id: number;
|
|
74
|
+
scope: Scope;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type TrapUpdate = Partial<Omit<TrapInput, "scope" | "project_path">>;
|
|
80
|
+
|
|
81
|
+
export interface TrapEvidence {
|
|
82
|
+
id: number;
|
|
83
|
+
trap_id: number;
|
|
84
|
+
source_type: string;
|
|
85
|
+
source_ref: string | null;
|
|
86
|
+
observed_at: string;
|
|
87
|
+
related_files: string;
|
|
88
|
+
note: string | null;
|
|
89
|
+
created_at: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TrapEvidenceInput {
|
|
93
|
+
source_type: string;
|
|
94
|
+
source_ref?: string | null;
|
|
95
|
+
observed_at?: string | null;
|
|
96
|
+
related_files?: string[];
|
|
97
|
+
note?: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface TrapExportEvidence extends Omit<TrapEvidence, "related_files"> {
|
|
101
|
+
related_files: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TrapExportRecord extends Trap {
|
|
105
|
+
evidence: TrapExportEvidence[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type TrapImportEvidence = Partial<Omit<TrapEvidence, "related_files">> & {
|
|
109
|
+
source_type: string;
|
|
110
|
+
related_files?: string[] | string | null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type TrapImportRecord = Omit<TrapInput, "tags" | "before_code" | "after_code" | "project_path"> & {
|
|
114
|
+
tags?: string[] | string | null;
|
|
115
|
+
before_code?: string | null;
|
|
116
|
+
after_code?: string | null;
|
|
117
|
+
project_path?: string | null;
|
|
118
|
+
evidence?: TrapImportEvidence[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export interface TrapDetails {
|
|
122
|
+
trap: Trap;
|
|
123
|
+
evidence: TrapEvidence[];
|
|
124
|
+
scope: Scope;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ArchiveTrapInput {
|
|
128
|
+
id: number;
|
|
129
|
+
scope?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface SupersedeTrapInput {
|
|
133
|
+
id: number;
|
|
134
|
+
superseded_by_id: number;
|
|
135
|
+
scope?: string;
|
|
136
|
+
state_key?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type JsonSchemaProperty = {
|
|
140
|
+
type: string;
|
|
141
|
+
enum?: string[];
|
|
142
|
+
items?: { type: string };
|
|
143
|
+
description?: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type JsonSchema = {
|
|
147
|
+
type: string;
|
|
148
|
+
properties: Record<string, JsonSchemaProperty>;
|
|
149
|
+
required?: string[];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const TRAP_REQUIRED_INPUT_FIELDS = [
|
|
153
|
+
"title",
|
|
154
|
+
"category",
|
|
155
|
+
"scope",
|
|
156
|
+
"context",
|
|
157
|
+
"mistake",
|
|
158
|
+
"fix",
|
|
159
|
+
] as const;
|
|
160
|
+
|
|
161
|
+
export const TRAP_UPDATE_FIELDS = [
|
|
162
|
+
"title",
|
|
163
|
+
"category",
|
|
164
|
+
"context",
|
|
165
|
+
"mistake",
|
|
166
|
+
"fix",
|
|
167
|
+
"tags",
|
|
168
|
+
"before_code",
|
|
169
|
+
"after_code",
|
|
170
|
+
"severity",
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
export const TRAP_INPUT_SCHEMA_PROPERTIES = {
|
|
174
|
+
title: { type: "string", description: "Short summary of the pitfall" },
|
|
175
|
+
category: { type: "string", enum: [...CATEGORIES] as string[] },
|
|
176
|
+
scope: {
|
|
177
|
+
type: "string",
|
|
178
|
+
enum: [...SCOPES] as string[],
|
|
179
|
+
description: "project = this project only, global = all projects",
|
|
180
|
+
},
|
|
181
|
+
context: { type: "string", description: "When does this pitfall typically occur?" },
|
|
182
|
+
mistake: { type: "string", description: "What the AI tends to do wrong" },
|
|
183
|
+
fix: { type: "string", description: "What should be done instead" },
|
|
184
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" },
|
|
185
|
+
before_code: { type: "string", description: "Example of wrong code (optional)" },
|
|
186
|
+
after_code: { type: "string", description: "Example of correct code (optional)" },
|
|
187
|
+
severity: { type: "string", enum: [...SEVERITIES] as string[], description: "How severe is this pitfall?" },
|
|
188
|
+
} satisfies Record<keyof Omit<TrapInput, "project_path">, JsonSchemaProperty>;
|
|
189
|
+
|
|
190
|
+
export function trapInputSchema() {
|
|
191
|
+
return {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: TRAP_INPUT_SCHEMA_PROPERTIES,
|
|
194
|
+
required: [...TRAP_REQUIRED_INPUT_FIELDS],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function trapUpdateSchema() {
|
|
199
|
+
const properties = Object.fromEntries(
|
|
200
|
+
TRAP_UPDATE_FIELDS.map((field) => [field, TRAP_INPUT_SCHEMA_PROPERTIES[field]])
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
id: { type: "number", description: "Trap ID to update" },
|
|
206
|
+
scope: { type: "string", enum: [...SCOPES] as string[] },
|
|
207
|
+
...properties,
|
|
208
|
+
},
|
|
209
|
+
required: ["id"],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function trapEvidenceInputSchema(): JsonSchema {
|
|
214
|
+
return {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
id: { type: "number", description: "Trap ID to attach evidence to" },
|
|
218
|
+
scope: { type: "string", enum: [...SCOPES] as string[], description: "Which scope to look in" },
|
|
219
|
+
source_type: {
|
|
220
|
+
type: "string",
|
|
221
|
+
enum: [...EVIDENCE_SOURCE_TYPES] as string[],
|
|
222
|
+
description: "Where this evidence came from",
|
|
223
|
+
},
|
|
224
|
+
source_ref: { type: "string", description: "Optional file path, commit SHA, issue URL, or transcript ID" },
|
|
225
|
+
observed_at: { type: "string", description: "When this was observed (ISO-like timestamp, optional)" },
|
|
226
|
+
related_files: {
|
|
227
|
+
type: "array",
|
|
228
|
+
items: { type: "string" },
|
|
229
|
+
description: "Optional related file paths",
|
|
230
|
+
},
|
|
231
|
+
note: { type: "string", description: "Optional short note about the evidence" },
|
|
232
|
+
},
|
|
233
|
+
required: ["id", "source_type"],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function archiveTrapInputSchema(): JsonSchema {
|
|
238
|
+
return {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
id: { type: "number", description: "Trap ID to archive" },
|
|
242
|
+
scope: { type: "string", enum: [...SCOPES] as string[], description: "Which scope to look in" },
|
|
243
|
+
},
|
|
244
|
+
required: ["id"],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function supersedeTrapInputSchema(): JsonSchema {
|
|
249
|
+
return {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
id: { type: "number", description: "Trap ID being superseded" },
|
|
253
|
+
superseded_by_id: { type: "number", description: "Trap ID that replaces the older trap" },
|
|
254
|
+
scope: { type: "string", enum: [...SCOPES] as string[], description: "Which scope to look in" },
|
|
255
|
+
state_key: { type: "string", description: "Optional lifecycle group key for this rule family" },
|
|
256
|
+
},
|
|
257
|
+
required: ["id", "superseded_by_id"],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildTrapInput(args: Record<string, any>): TrapInput {
|
|
262
|
+
return {
|
|
263
|
+
title: args.title,
|
|
264
|
+
category: args.category,
|
|
265
|
+
scope: args.scope,
|
|
266
|
+
context: args.context,
|
|
267
|
+
mistake: args.mistake,
|
|
268
|
+
fix: args.fix,
|
|
269
|
+
tags: args.tags,
|
|
270
|
+
before_code: args.before_code,
|
|
271
|
+
after_code: args.after_code,
|
|
272
|
+
severity: args.severity ?? DEFAULT_SEVERITY,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function buildTrapEvidenceInput(args: Record<string, any>): TrapEvidenceInput {
|
|
277
|
+
if (!(EVIDENCE_SOURCE_TYPES as readonly string[]).includes(args.source_type)) {
|
|
278
|
+
throw new Error(`Invalid evidence source_type: ${args.source_type}. Expected one of: ${EVIDENCE_SOURCE_TYPES.join(", ")}`);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
source_type: args.source_type,
|
|
282
|
+
source_ref: args.source_ref,
|
|
283
|
+
observed_at: args.observed_at,
|
|
284
|
+
related_files: Array.isArray(args.related_files) ? args.related_files.map(String) : undefined,
|
|
285
|
+
note: args.note,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function parseTrapStatus(status?: string): TrapStatus | "all" | undefined {
|
|
290
|
+
if (!status) return undefined;
|
|
291
|
+
if (status === "all") return "all";
|
|
292
|
+
if ((TRAP_STATUSES as readonly string[]).includes(status)) return status as TrapStatus;
|
|
293
|
+
throw new Error(`Invalid trap status: ${status}. Expected one of: ${TRAP_STATUSES.join(", ")}, all`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function pickTrapUpdate(args: Record<string, any>): TrapUpdate {
|
|
297
|
+
const update: TrapUpdate = {};
|
|
298
|
+
for (const field of TRAP_UPDATE_FIELDS) {
|
|
299
|
+
if (args[field] !== undefined) {
|
|
300
|
+
update[field] = args[field];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return update;
|
|
304
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { findProjectRoot } from "./lib/scope";
|
|
6
|
+
import { TrapStore } from "./lib/store";
|
|
7
|
+
import { run } from "./commands/router";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
showHelp();
|
|
13
|
+
} else if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
|
|
14
|
+
showHelp();
|
|
15
|
+
} else if (args[0] === "serve") {
|
|
16
|
+
import("./mcp/server").then((m) => m.start());
|
|
17
|
+
} else if (args[0] === "init") {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
if (findProjectRoot(cwd)) {
|
|
20
|
+
console.log("Project already initialized.");
|
|
21
|
+
} else {
|
|
22
|
+
const dir = join(cwd, ".codetrap");
|
|
23
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
24
|
+
console.log(`Initialized .codetrap/ in ${cwd}`);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
const store = new TrapStore(process.cwd());
|
|
28
|
+
await run(args, store);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function showHelp(): void {
|
|
32
|
+
console.log("codetrap — capture coding pitfalls so AI doesn't repeat mistakes");
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("Commands:");
|
|
35
|
+
console.log(" init Initialize .codetrap/ in current project");
|
|
36
|
+
console.log(" add Add a trap (use --json for structured input)");
|
|
37
|
+
console.log(" search <query> Search traps by keyword/semantic/hybrid mode");
|
|
38
|
+
console.log(" list [--category X] List traps");
|
|
39
|
+
console.log(" show <id> Show trap details");
|
|
40
|
+
console.log(" edit <id> --json '{}' Edit a trap");
|
|
41
|
+
console.log(" delete <id> Delete a trap");
|
|
42
|
+
console.log(" add_trap_evidence Attach evidence to a trap");
|
|
43
|
+
console.log(" archive_trap Archive a trap");
|
|
44
|
+
console.log(" supersede_trap Mark one trap as superseded by another");
|
|
45
|
+
console.log(" embed Generate embeddings for semantic search");
|
|
46
|
+
console.log(" export Export traps as JSON");
|
|
47
|
+
console.log(" import <file.json> Import traps from JSON");
|
|
48
|
+
console.log(" stats Show statistics");
|
|
49
|
+
console.log(" serve Start MCP server (for Claude Code)");
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("Flags:");
|
|
52
|
+
console.log(" --scope project|global Filter by scope");
|
|
53
|
+
console.log(" --category <name> Filter by category");
|
|
54
|
+
console.log(" --mode fts|semantic|hybrid Search mode");
|
|
55
|
+
console.log(" --status active|superseded|archived|all Lifecycle filter");
|
|
56
|
+
console.log(" --batch-size <n> Embedding generation batch size");
|
|
57
|
+
console.log(" --json '{}' JSON input for add/edit");
|
|
58
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const CATEGORIES = [
|
|
2
|
+
"api",
|
|
3
|
+
"database",
|
|
4
|
+
"auth",
|
|
5
|
+
"convention",
|
|
6
|
+
"security",
|
|
7
|
+
"performance",
|
|
8
|
+
"bug",
|
|
9
|
+
"other",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type Category = (typeof CATEGORIES)[number];
|
|
13
|
+
|
|
14
|
+
export const CATEGORY_LABELS: Record<Category, string> = {
|
|
15
|
+
api: "API",
|
|
16
|
+
database: "DB",
|
|
17
|
+
auth: "Auth",
|
|
18
|
+
convention: "Conv",
|
|
19
|
+
security: "Sec",
|
|
20
|
+
performance: "Perf",
|
|
21
|
+
bug: "Bug",
|
|
22
|
+
other: "Other",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SEVERITIES = ["warning", "error", "critical"] as const;
|
|
26
|
+
export type Severity = (typeof SEVERITIES)[number];
|
|
27
|
+
|
|
28
|
+
export const SEVERITY_ICONS: Record<Severity, string> = {
|
|
29
|
+
warning: "WARN",
|
|
30
|
+
error: "ERR!",
|
|
31
|
+
critical: "CRIT",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const SCOPES = ["project", "global"] as const;
|
|
35
|
+
export type Scope = (typeof SCOPES)[number];
|
|
36
|
+
|
|
37
|
+
export const TRAP_STATUSES = ["active", "superseded", "archived"] as const;
|
|
38
|
+
export type TrapStatus = (typeof TRAP_STATUSES)[number];
|
|
39
|
+
|
|
40
|
+
export const EVIDENCE_SOURCE_TYPES = ["manual", "conversation", "commit", "issue", "test_failure"] as const;
|
|
41
|
+
export type EvidenceSourceType = (typeof EVIDENCE_SOURCE_TYPES)[number];
|
|
42
|
+
|
|
43
|
+
export const SEARCH_MODES = ["fts", "semantic", "hybrid"] as const;
|
|
44
|
+
export type SearchMode = (typeof SEARCH_MODES)[number];
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_SEVERITY: Severity = "warning";
|
|
47
|
+
export const DEFAULT_CATEGORY: Category = "other";
|
|
48
|
+
export const DEFAULT_TRAP_STATUS: TrapStatus = "active";
|
|
49
|
+
|
|
50
|
+
// Increment this when schema changes in a breaking way.
|
|
51
|
+
// Migrations are stored in src/db/migrations.ts
|
|
52
|
+
export const SCHEMA_VERSION = 4;
|
|
53
|
+
|
|
54
|
+
// Directory and file names
|
|
55
|
+
export const CODETRAP_DIR = ".codetrap";
|
|
56
|
+
export const TRAPS_DB_FILE = "traps.db";
|