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.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +112 -33
- package/docs/installation.md +18 -10
- package/package.json +4 -1
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
- package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
- package/plugins/codetrap-agent/hooks.json +11 -0
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
- package/scripts/release-preflight.ts +55 -0
- package/skills/codetrap-add/SKILL.md +4 -1
- package/skills/codetrap-check/SKILL.md +24 -4
- package/skills/codetrap-search/SKILL.md +32 -12
- package/src/commands/command-result.ts +29 -0
- package/src/commands/router.ts +6 -400
- package/src/commands/workflow.ts +419 -0
- package/src/db/embedding-queries.ts +33 -0
- package/src/db/queries.ts +165 -48
- package/src/db/repository.ts +72 -15
- package/src/db/schema.ts +35 -0
- package/src/domain/trap.ts +38 -10
- package/src/index.ts +13 -1
- package/src/lib/command-requests.ts +133 -0
- package/src/lib/config.ts +102 -0
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +86 -0
- package/src/lib/embedding-health.ts +49 -0
- package/src/lib/embedding-index.ts +53 -0
- package/src/lib/format.ts +6 -2
- package/src/lib/output-json.ts +141 -0
- package/src/lib/scope-context.ts +118 -0
- package/src/lib/scope-maintenance.ts +71 -0
- package/src/lib/scope-migration.ts +315 -0
- package/src/lib/scope-path.ts +99 -0
- package/src/lib/scope.ts +16 -11
- package/src/lib/search-normalizer.ts +6 -0
- package/src/lib/search-policy.ts +365 -0
- package/src/lib/search-result-card.ts +2 -7
- package/src/lib/search-service.ts +67 -120
- package/src/lib/store.ts +129 -108
- package/src/lib/trap-archive.ts +9 -42
- package/src/lib/trap-codec.ts +113 -0
- package/src/lib/trap-json-fields.ts +12 -0
- package/src/lib/trap-lifecycle.ts +37 -0
- package/src/lib/trap-mutation-result.ts +36 -0
- package/src/lib/trap-operations.ts +30 -9
- package/src/lib/trap-scope-match.ts +112 -0
- package/src/lib/trap-search-document.ts +8 -1
- package/src/lib/trap-transfer.ts +88 -0
- package/src/mcp/server.ts +77 -72
- package/src/mcp/tools.ts +32 -5
|
@@ -12,17 +12,14 @@ import {
|
|
|
12
12
|
import type { TrapStore, TrapStats } from "./store";
|
|
13
13
|
import type { SearchMode } from "./constants";
|
|
14
14
|
import { toTrapActionCards } from "./search-result-card";
|
|
15
|
+
import type { AddTrapEvidenceResult, TrapMutationResult } from "./trap-mutation-result";
|
|
15
16
|
|
|
16
17
|
export type TrapListGroup = { traps: Trap[]; scope: string };
|
|
17
18
|
export type TrapSearchGroup = { results: TrapSearchResult[]; scope: string };
|
|
18
|
-
export type
|
|
19
|
-
export type AddTrapEvidenceResult = {
|
|
20
|
-
scope: string;
|
|
21
|
-
evidence_id: number | null;
|
|
22
|
-
success: boolean;
|
|
23
|
-
};
|
|
19
|
+
export type { AddTrapEvidenceResult, TrapMutationResult };
|
|
24
20
|
|
|
25
|
-
export type TrapStatsResult = { project: TrapStats | null; global: TrapStats };
|
|
21
|
+
export type TrapStatsResult = { project: TrapStats | null; global: TrapStats | null };
|
|
22
|
+
export type EmbeddingStatsResult = ReturnType<TrapStore["embeddingStats"]>;
|
|
26
23
|
|
|
27
24
|
export interface SearchTrapsArgs {
|
|
28
25
|
query: string;
|
|
@@ -31,6 +28,11 @@ export interface SearchTrapsArgs {
|
|
|
31
28
|
limit?: number;
|
|
32
29
|
mode?: SearchMode;
|
|
33
30
|
status?: string;
|
|
31
|
+
path?: string;
|
|
32
|
+
module?: string;
|
|
33
|
+
owner?: string;
|
|
34
|
+
rerank?: boolean;
|
|
35
|
+
includeRankingSignals?: boolean;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface ListTrapsArgs {
|
|
@@ -39,6 +41,9 @@ export interface ListTrapsArgs {
|
|
|
39
41
|
limit?: number;
|
|
40
42
|
offset?: number;
|
|
41
43
|
status?: string;
|
|
44
|
+
path?: string;
|
|
45
|
+
module?: string;
|
|
46
|
+
owner?: string;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export class TrapOperations {
|
|
@@ -55,6 +60,11 @@ export class TrapOperations {
|
|
|
55
60
|
limit: args.limit ?? 20,
|
|
56
61
|
mode: args.mode,
|
|
57
62
|
status: parseTrapStatus(args.status),
|
|
63
|
+
path: args.path,
|
|
64
|
+
module: args.module,
|
|
65
|
+
owner: args.owner,
|
|
66
|
+
rerank: args.rerank,
|
|
67
|
+
includeRankingSignals: args.includeRankingSignals,
|
|
58
68
|
});
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -77,9 +87,16 @@ export class TrapOperations {
|
|
|
77
87
|
status: parseTrapStatus(args.status),
|
|
78
88
|
limit: args.limit ?? 50,
|
|
79
89
|
offset: args.offset,
|
|
90
|
+
path: args.path,
|
|
91
|
+
module: args.module,
|
|
92
|
+
owner: args.owner,
|
|
80
93
|
});
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
topTraps(scope: string, limit = 20): TrapListGroup[] {
|
|
97
|
+
return [{ traps: this.store.topTraps(scope, limit), scope }];
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
updateTrap(
|
|
84
101
|
id: number,
|
|
85
102
|
args: Record<string, unknown>,
|
|
@@ -113,8 +130,12 @@ export class TrapOperations {
|
|
|
113
130
|
return this.store.supersede(id, supersededById, scope, stateKey);
|
|
114
131
|
}
|
|
115
132
|
|
|
116
|
-
getStats(): TrapStatsResult {
|
|
117
|
-
return this.store.stats();
|
|
133
|
+
getStats(scope?: string): TrapStatsResult {
|
|
134
|
+
return this.store.stats({ scope });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getEmbeddingStats(scope?: string): EmbeddingStatsResult {
|
|
138
|
+
return this.store.embeddingStats({ scope });
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
exportTraps(scope?: string): ReturnType<TrapStore["exportAll"]> {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative } from "node:path";
|
|
3
|
+
import type { Trap } from "../domain/trap";
|
|
4
|
+
import { parseTrapPathGlobs } from "./trap-json-fields";
|
|
5
|
+
|
|
6
|
+
export type ApplicabilityFilter = {
|
|
7
|
+
path?: string;
|
|
8
|
+
module?: string;
|
|
9
|
+
owner?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function trapMatchesApplicability(trap: Trap, filter: ApplicabilityFilter): boolean {
|
|
13
|
+
return (
|
|
14
|
+
trapAppliesToPath(trap, filter.path) &&
|
|
15
|
+
trapAppliesToModule(trap, filter.module) &&
|
|
16
|
+
trapAppliesToOwner(trap, filter.owner)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function trapAppliesToPath(trap: Trap, path?: string): boolean {
|
|
21
|
+
if (!path) return true;
|
|
22
|
+
const globs = parseTrapPathGlobs(trap.path_globs);
|
|
23
|
+
if (globs.length === 0) return true;
|
|
24
|
+
const candidates = pathCandidates(trap, path);
|
|
25
|
+
return globs.some((glob) => candidates.some((candidate) => globMatchesPath(glob, candidate)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function trapAppliesToModule(trap: Trap, module?: string): boolean {
|
|
29
|
+
if (!module || !trap.module) return true;
|
|
30
|
+
return trap.module === module;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function trapAppliesToOwner(trap: Trap, owner?: string): boolean {
|
|
34
|
+
if (!owner || !trap.owner) return true;
|
|
35
|
+
return trap.owner === owner;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasSpecificPathMatch(trap: Trap, path?: string): boolean {
|
|
39
|
+
if (!path) return false;
|
|
40
|
+
const globs = parseTrapPathGlobs(trap.path_globs);
|
|
41
|
+
const candidates = pathCandidates(trap, path);
|
|
42
|
+
return globs.length > 0 && globs.some((glob) => candidates.some((candidate) => globMatchesPath(glob, candidate)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizePath(path: string): string {
|
|
46
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function globMatchesPath(glob: string, path: string): boolean {
|
|
50
|
+
const normalizedGlob = normalizePath(glob);
|
|
51
|
+
return new RegExp(`^${globToRegExp(normalizedGlob)}$`).test(path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pathCandidates(trap: Trap, path: string): string[] {
|
|
55
|
+
const candidates = new Set([normalizePath(path)]);
|
|
56
|
+
if (trap.project_path && isAbsolute(path)) {
|
|
57
|
+
for (const root of projectPathAliases(trap.project_path)) {
|
|
58
|
+
const relativePath = relative(root, path);
|
|
59
|
+
const normalizedRelative = normalizePath(relativePath);
|
|
60
|
+
if (
|
|
61
|
+
normalizedRelative &&
|
|
62
|
+
normalizedRelative !== ".." &&
|
|
63
|
+
!normalizedRelative.startsWith("../") &&
|
|
64
|
+
!isAbsolute(relativePath)
|
|
65
|
+
) {
|
|
66
|
+
candidates.add(normalizedRelative);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...candidates];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function projectPathAliases(projectPath: string): string[] {
|
|
74
|
+
const roots = new Set([projectPath]);
|
|
75
|
+
try {
|
|
76
|
+
roots.add(realpathSync(projectPath));
|
|
77
|
+
} catch {
|
|
78
|
+
// The project may have moved since the trap was recorded; use the stored path.
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const root of [...roots]) {
|
|
82
|
+
if (root.startsWith("/private/var/")) {
|
|
83
|
+
roots.add(root.replace(/^\/private\/var\//, "/var/"));
|
|
84
|
+
} else if (root.startsWith("/var/")) {
|
|
85
|
+
roots.add(`/private${root}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...roots];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function globToRegExp(glob: string): string {
|
|
92
|
+
let out = "";
|
|
93
|
+
for (let i = 0; i < glob.length; i++) {
|
|
94
|
+
const char = glob[i];
|
|
95
|
+
const next = glob[i + 1];
|
|
96
|
+
if (char === "*" && next === "*") {
|
|
97
|
+
out += ".*";
|
|
98
|
+
i++;
|
|
99
|
+
} else if (char === "*") {
|
|
100
|
+
out += "[^/]*";
|
|
101
|
+
} else if (char === "?") {
|
|
102
|
+
out += "[^/]";
|
|
103
|
+
} else {
|
|
104
|
+
out += escapeRegExp(char);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function escapeRegExp(value: string): string {
|
|
111
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
112
|
+
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Trap, TrapInput, TrapUpdate } from "../domain/trap";
|
|
3
3
|
import type { EmbeddingConfig, StoredEmbedding } from "./embedder";
|
|
4
4
|
import { buildSearchText, SEARCH_TEXT_FIELD_NAMES, type SearchTextFields } from "./search-normalizer";
|
|
5
|
-
import { parseTrapTags } from "./trap-json-fields";
|
|
5
|
+
import { parseTrapPathGlobs, parseTrapTags } from "./trap-json-fields";
|
|
6
6
|
|
|
7
7
|
export const PASSAGE_VERSION = 1;
|
|
8
8
|
|
|
@@ -16,6 +16,9 @@ export const PASSAGE_FIELD_NAMES = [
|
|
|
16
16
|
"before_code",
|
|
17
17
|
"after_code",
|
|
18
18
|
"severity",
|
|
19
|
+
"path_globs",
|
|
20
|
+
"module",
|
|
21
|
+
"owner",
|
|
19
22
|
] as const;
|
|
20
23
|
|
|
21
24
|
export type TrapSearchDocumentFields = SearchTextFields & {
|
|
@@ -30,11 +33,15 @@ export function buildTrapSearchText(fields: SearchTextFields): string {
|
|
|
30
33
|
|
|
31
34
|
export function buildTrapPassage(trap: TrapSearchDocumentFields): string {
|
|
32
35
|
const tags = parseTrapTags(trap.tags).join(", ");
|
|
36
|
+
const pathGlobs = parseTrapPathGlobs(trap.path_globs).join(", ");
|
|
33
37
|
return [
|
|
34
38
|
trap.title ? `Title: ${trap.title}` : "",
|
|
35
39
|
trap.category ? `Category: ${trap.category}` : "",
|
|
36
40
|
trap.severity ? `Severity: ${trap.severity}` : "",
|
|
37
41
|
tags ? `Tags: ${tags}` : "",
|
|
42
|
+
pathGlobs ? `Path globs: ${pathGlobs}` : "",
|
|
43
|
+
trap.module ? `Module: ${trap.module}` : "",
|
|
44
|
+
trap.owner ? `Owner: ${trap.owner}` : "",
|
|
38
45
|
trap.context ? `Context: ${trap.context}` : "",
|
|
39
46
|
trap.mistake ? `Mistake: ${trap.mistake}` : "",
|
|
40
47
|
trap.fix ? `Fix: ${trap.fix}` : "",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { TrapRecordInsert, TrapRepository } from "../db/repository";
|
|
2
|
+
import type { TrapExportEvidence, TrapExportRecord } from "../domain/trap";
|
|
3
|
+
import { DEFAULT_SEVERITY, DEFAULT_TRAP_STATUS } from "./constants";
|
|
4
|
+
import { buildTrapSearchText } from "./trap-search-document";
|
|
5
|
+
import { encodeTrapPathGlobs } from "./trap-json-fields";
|
|
6
|
+
|
|
7
|
+
export type TrapTransferMapping = {
|
|
8
|
+
source_id: number;
|
|
9
|
+
destination_id: number;
|
|
10
|
+
title: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function importProjectTrapTransfer(
|
|
14
|
+
destination: TrapRepository,
|
|
15
|
+
records: TrapExportRecord[],
|
|
16
|
+
projectPath: string
|
|
17
|
+
): TrapTransferMapping[] {
|
|
18
|
+
const mappings: TrapTransferMapping[] = [];
|
|
19
|
+
const idMap = new Map<number, number>();
|
|
20
|
+
|
|
21
|
+
destination.transaction(() => {
|
|
22
|
+
for (const record of records) {
|
|
23
|
+
const destinationId = destination.insertTrapRecord(toProjectTransferRecord(record, projectPath));
|
|
24
|
+
idMap.set(record.id, destinationId);
|
|
25
|
+
mappings.push({ source_id: record.id, destination_id: destinationId, title: record.title });
|
|
26
|
+
|
|
27
|
+
for (const evidence of record.evidence ?? []) {
|
|
28
|
+
destination.addEvidence(destinationId, toEvidenceInput(evidence));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
if (record.supersedes_id === null) continue;
|
|
34
|
+
const destinationId = idMap.get(record.id);
|
|
35
|
+
const destinationSupersedesId = idMap.get(record.supersedes_id);
|
|
36
|
+
if (destinationId !== undefined && destinationSupersedesId !== undefined) {
|
|
37
|
+
destination.updateTrapSupersedesId(destinationId, destinationSupersedesId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return mappings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function deleteTransferredSourceTraps(
|
|
46
|
+
source: TrapRepository,
|
|
47
|
+
records: TrapExportRecord[]
|
|
48
|
+
): number {
|
|
49
|
+
return source.deleteTrapsByIds(records.map((record) => record.id));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toProjectTransferRecord(record: TrapExportRecord, projectPath: string): TrapRecordInsert {
|
|
53
|
+
return {
|
|
54
|
+
title: record.title,
|
|
55
|
+
category: record.category,
|
|
56
|
+
tags: record.tags,
|
|
57
|
+
scope: "project",
|
|
58
|
+
context: record.context,
|
|
59
|
+
mistake: record.mistake,
|
|
60
|
+
fix: record.fix,
|
|
61
|
+
search_text: record.search_text || buildTrapSearchText(record),
|
|
62
|
+
before_code: record.before_code,
|
|
63
|
+
after_code: record.after_code,
|
|
64
|
+
severity: record.severity ?? DEFAULT_SEVERITY,
|
|
65
|
+
state_key: record.state_key,
|
|
66
|
+
status: record.status ?? DEFAULT_TRAP_STATUS,
|
|
67
|
+
supersedes_id: null,
|
|
68
|
+
valid_from: record.valid_from,
|
|
69
|
+
valid_until: record.valid_until,
|
|
70
|
+
project_path: projectPath,
|
|
71
|
+
path_globs: encodeTrapPathGlobs(record.path_globs),
|
|
72
|
+
module: record.module,
|
|
73
|
+
owner: record.owner,
|
|
74
|
+
hit_count: record.hit_count ?? 0,
|
|
75
|
+
created_at: record.created_at,
|
|
76
|
+
updated_at: record.updated_at,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toEvidenceInput(evidence: TrapExportEvidence) {
|
|
81
|
+
return {
|
|
82
|
+
source_type: evidence.source_type,
|
|
83
|
+
source_ref: evidence.source_ref,
|
|
84
|
+
observed_at: evidence.observed_at,
|
|
85
|
+
related_files: evidence.related_files,
|
|
86
|
+
note: evidence.note,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -10,88 +10,130 @@ import { TrapStore } from "../lib/store";
|
|
|
10
10
|
import { toolDefinitions } from "./tools";
|
|
11
11
|
import { resourceDefinitions } from "./resources";
|
|
12
12
|
import { TrapOperations } from "../lib/trap-operations";
|
|
13
|
+
import {
|
|
14
|
+
toListJson,
|
|
15
|
+
toMcpResourceJson,
|
|
16
|
+
toMcpResourceText,
|
|
17
|
+
toMcpSearchJson,
|
|
18
|
+
toMcpTextError,
|
|
19
|
+
toMcpTextJson,
|
|
20
|
+
toStatsJson,
|
|
21
|
+
toTrapDetailsJson,
|
|
22
|
+
} from "../lib/output-json";
|
|
23
|
+
import { storeForScopeContext } from "../lib/scope-context";
|
|
24
|
+
import {
|
|
25
|
+
listRequestFromArgs,
|
|
26
|
+
searchRequestFromArgs,
|
|
27
|
+
statsRequestFromArgs,
|
|
28
|
+
} from "../lib/command-requests";
|
|
13
29
|
|
|
14
30
|
type ToolArgs = Record<string, any>;
|
|
15
31
|
|
|
16
32
|
export async function handleToolCall(store: TrapStore, name: string, args: ToolArgs) {
|
|
17
|
-
const
|
|
33
|
+
const scopedStore = storeForScopeContext(store, args.cwd);
|
|
34
|
+
const operations = new TrapOperations(scopedStore);
|
|
18
35
|
try {
|
|
19
36
|
switch (name) {
|
|
20
37
|
case "search_traps": {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
category: args.category,
|
|
25
|
-
mode: args.mode,
|
|
26
|
-
limit: args.limit ?? 20,
|
|
27
|
-
status: args.status,
|
|
28
|
-
});
|
|
29
|
-
return { content: [{ type: "text", text: JSON.stringify(cards, null, 2) }] };
|
|
38
|
+
const defaults = { mode: "hybrid" as const, limit: args.limit ?? 20, rerank: true };
|
|
39
|
+
const cards = await operations.searchTrapCards(searchRequestFromArgs(args.query, args, defaults));
|
|
40
|
+
return toMcpTextJson(toMcpSearchJson(cards));
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
case "add_trap": {
|
|
33
44
|
const result = operations.addTrap(args);
|
|
34
|
-
return
|
|
45
|
+
return toMcpTextJson(result);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
case "get_trap": {
|
|
38
49
|
const result = operations.getTrapDetails(args.id, args.scope);
|
|
39
50
|
if (!result) {
|
|
40
|
-
return
|
|
51
|
+
return toMcpTextError("not found");
|
|
41
52
|
}
|
|
42
|
-
return
|
|
53
|
+
return toMcpTextJson(toTrapDetailsJson(result));
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
case "list_traps": {
|
|
46
|
-
const groups = operations.listTraps(
|
|
47
|
-
|
|
48
|
-
category: args.category,
|
|
49
|
-
status: args.status,
|
|
50
|
-
limit: args.limit ?? 50,
|
|
51
|
-
});
|
|
52
|
-
const flat = groups.flatMap((g) =>
|
|
53
|
-
g.traps.map((t) => ({ ...t, scope: g.scope }))
|
|
54
|
-
);
|
|
55
|
-
return { content: [{ type: "text", text: JSON.stringify(flat, null, 2) }] };
|
|
57
|
+
const groups = operations.listTraps(listRequestFromArgs(args));
|
|
58
|
+
return toMcpTextJson(toListJson(groups));
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
case "update_trap": {
|
|
59
62
|
const result = operations.updateTrap(args.id, args, args.scope);
|
|
60
|
-
return
|
|
63
|
+
return toMcpTextJson(result, !result.success);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
case "delete_trap": {
|
|
64
67
|
const result = operations.deleteTrap(args.id, args.scope);
|
|
65
|
-
return
|
|
68
|
+
return toMcpTextJson(result, !result.success);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
case "add_trap_evidence": {
|
|
69
72
|
const result = operations.addTrapEvidence(args.id, args, args.scope);
|
|
70
|
-
return
|
|
73
|
+
return toMcpTextJson(result, !result.success);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
case "archive_trap": {
|
|
74
77
|
const result = operations.archiveTrap(args.id, args.scope);
|
|
75
|
-
return
|
|
78
|
+
return toMcpTextJson(result, !result.success);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
case "supersede_trap": {
|
|
79
82
|
const result = operations.supersedeTrap(args.id, args.superseded_by_id, args.scope, args.state_key);
|
|
80
|
-
return
|
|
83
|
+
return toMcpTextJson(result, !result.success);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
case "get_stats": {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
87
|
+
const request = statsRequestFromArgs(args);
|
|
88
|
+
const stats = operations.getStats(request.scope);
|
|
89
|
+
return toMcpTextJson(toStatsJson(stats, operations.getEmbeddingStats(request.scope)));
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
default:
|
|
91
|
-
return
|
|
93
|
+
return toMcpTextError(`Unknown tool: ${name}`);
|
|
92
94
|
}
|
|
93
95
|
} catch (e: any) {
|
|
94
|
-
return
|
|
96
|
+
return toMcpTextError(e instanceof Error ? e.message : String(e));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function handleResourceRead(store: TrapStore, uri: string) {
|
|
101
|
+
const parsed = parseResourceUri(uri);
|
|
102
|
+
const scopedStore = storeForScopeContext(store, parsed.cwd);
|
|
103
|
+
const operations = new TrapOperations(scopedStore);
|
|
104
|
+
try {
|
|
105
|
+
switch (parsed.baseUri) {
|
|
106
|
+
case "codetrap://project/recent":
|
|
107
|
+
return toMcpResourceJson(uri, toListJson(operations.listTraps({ scope: "project", limit: 10 })));
|
|
108
|
+
case "codetrap://global/recent":
|
|
109
|
+
return toMcpResourceJson(uri, toListJson(operations.listTraps({ scope: "global", limit: 10 })));
|
|
110
|
+
case "codetrap://project/top":
|
|
111
|
+
return toMcpResourceJson(uri, toListJson(operations.topTraps("project", 20)));
|
|
112
|
+
case "codetrap://global/top":
|
|
113
|
+
return toMcpResourceJson(uri, toListJson(operations.topTraps("global", 20)));
|
|
114
|
+
default: {
|
|
115
|
+
const match = parsed.baseUri.match(/^codetrap:\/\/(project|global)\/trap\/(\d+)$/);
|
|
116
|
+
if (match) {
|
|
117
|
+
const details = operations.getTrapDetails(Number.parseInt(match[2], 10), match[1]);
|
|
118
|
+
return toMcpResourceJson(uri, details ? toTrapDetailsJson(details) : { error: "not found" });
|
|
119
|
+
}
|
|
120
|
+
return toMcpResourceText(uri, "Unknown resource");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e: any) {
|
|
124
|
+
return toMcpResourceText(uri, e instanceof Error ? e.message : String(e));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseResourceUri(uri: string): { baseUri: string; cwd?: string } {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(uri);
|
|
131
|
+
return {
|
|
132
|
+
baseUri: `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`,
|
|
133
|
+
cwd: parsed.searchParams.get("cwd") ?? undefined,
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
return { baseUri: uri };
|
|
95
137
|
}
|
|
96
138
|
}
|
|
97
139
|
|
|
@@ -122,44 +164,7 @@ export async function start(): Promise<void> {
|
|
|
122
164
|
|
|
123
165
|
// Read resource
|
|
124
166
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
switch (uri) {
|
|
128
|
-
case "codetrap://project/recent": {
|
|
129
|
-
const groups = store.list({ scope: "project", limit: 10 });
|
|
130
|
-
const traps = groups.flatMap((g) => g.traps);
|
|
131
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
132
|
-
}
|
|
133
|
-
case "codetrap://global/recent": {
|
|
134
|
-
const groups = store.list({ scope: "global", limit: 10 });
|
|
135
|
-
const traps = groups.flatMap((g) => g.traps);
|
|
136
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
137
|
-
}
|
|
138
|
-
case "codetrap://project/top": {
|
|
139
|
-
const traps = store.topTraps("project", 20);
|
|
140
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
141
|
-
}
|
|
142
|
-
case "codetrap://global/top": {
|
|
143
|
-
const traps = store.topTraps("global", 20);
|
|
144
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(traps, null, 2) }] };
|
|
145
|
-
}
|
|
146
|
-
default: {
|
|
147
|
-
// Handle codetrap://{scope}/trap/{id}
|
|
148
|
-
const match = uri.match(/^codetrap:\/\/(project|global)\/trap\/(\d+)$/);
|
|
149
|
-
if (match) {
|
|
150
|
-
const result = store.get(parseInt(match[2]), match[1]);
|
|
151
|
-
if (!result) {
|
|
152
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "not found" }) }] };
|
|
153
|
-
}
|
|
154
|
-
const details = store.getDetails(parseInt(match[2]), match[1]);
|
|
155
|
-
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(details ?? result.trap, null, 2) }] };
|
|
156
|
-
}
|
|
157
|
-
return { contents: [{ uri, mimeType: "text/plain", text: "Unknown resource" }] };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} catch (e: any) {
|
|
161
|
-
return { contents: [{ uri, mimeType: "text/plain", text: e.message }] };
|
|
162
|
-
}
|
|
167
|
+
return handleResourceRead(store, request.params.uri);
|
|
163
168
|
});
|
|
164
169
|
|
|
165
170
|
const transport = new StdioServerTransport();
|
package/src/mcp/tools.ts
CHANGED
|
@@ -11,6 +11,10 @@ const categoryEnum = [...CATEGORIES] as string[];
|
|
|
11
11
|
const scopeEnum = [...SCOPES] as string[];
|
|
12
12
|
const searchModeEnum = [...SEARCH_MODES] as string[];
|
|
13
13
|
const statusEnum = [...TRAP_STATUSES, "all"] as string[];
|
|
14
|
+
const cwdProperty = {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Optional working directory used to resolve project scope for this tool call.",
|
|
17
|
+
};
|
|
14
18
|
|
|
15
19
|
export const toolDefinitions = [
|
|
16
20
|
{
|
|
@@ -26,6 +30,12 @@ export const toolDefinitions = [
|
|
|
26
30
|
mode: { type: "string", enum: searchModeEnum, description: "Search mode (default hybrid)" },
|
|
27
31
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
28
32
|
status: { type: "string", enum: statusEnum, description: "Lifecycle filter (default active; use all for history)" },
|
|
33
|
+
path: { type: "string", description: "Optional file path used to filter path-scoped traps" },
|
|
34
|
+
module: { type: "string", description: "Optional module/subsystem filter" },
|
|
35
|
+
owner: { type: "string", description: "Optional owner/team filter" },
|
|
36
|
+
rerank: { type: "boolean", description: "Whether query-aware reranking is enabled" },
|
|
37
|
+
ranking_signals: { type: "boolean", description: "Include ranking signal diagnostics in returned cards" },
|
|
38
|
+
cwd: cwdProperty,
|
|
29
39
|
},
|
|
30
40
|
required: ["query"],
|
|
31
41
|
},
|
|
@@ -34,7 +44,7 @@ export const toolDefinitions = [
|
|
|
34
44
|
name: "add_trap",
|
|
35
45
|
description:
|
|
36
46
|
"Record a new coding pitfall. Call this when the user wants to save a lesson learned: an AI mistake pattern and the correct approach.",
|
|
37
|
-
inputSchema: trapInputSchema(),
|
|
47
|
+
inputSchema: withCwd(trapInputSchema()),
|
|
38
48
|
},
|
|
39
49
|
{
|
|
40
50
|
name: "get_trap",
|
|
@@ -44,6 +54,7 @@ export const toolDefinitions = [
|
|
|
44
54
|
properties: {
|
|
45
55
|
id: { type: "number", description: "Trap ID" },
|
|
46
56
|
scope: { type: "string", enum: scopeEnum, description: "Which scope to look in" },
|
|
57
|
+
cwd: cwdProperty,
|
|
47
58
|
},
|
|
48
59
|
required: ["id"],
|
|
49
60
|
},
|
|
@@ -58,13 +69,17 @@ export const toolDefinitions = [
|
|
|
58
69
|
category: { type: "string", enum: categoryEnum },
|
|
59
70
|
status: { type: "string", enum: statusEnum, description: "Lifecycle filter (default active; use all for history)" },
|
|
60
71
|
limit: { type: "number", description: "Max results (default 50)" },
|
|
72
|
+
path: { type: "string", description: "Optional file path used to filter path-scoped traps" },
|
|
73
|
+
module: { type: "string", description: "Optional module/subsystem filter" },
|
|
74
|
+
owner: { type: "string", description: "Optional owner/team filter" },
|
|
75
|
+
cwd: cwdProperty,
|
|
61
76
|
},
|
|
62
77
|
},
|
|
63
78
|
},
|
|
64
79
|
{
|
|
65
80
|
name: "update_trap",
|
|
66
81
|
description: "Update an existing trap's fields.",
|
|
67
|
-
inputSchema: trapUpdateSchema(),
|
|
82
|
+
inputSchema: withCwd(trapUpdateSchema()),
|
|
68
83
|
},
|
|
69
84
|
{
|
|
70
85
|
name: "delete_trap",
|
|
@@ -74,6 +89,7 @@ export const toolDefinitions = [
|
|
|
74
89
|
properties: {
|
|
75
90
|
id: { type: "number", description: "Trap ID to delete" },
|
|
76
91
|
scope: { type: "string", enum: scopeEnum },
|
|
92
|
+
cwd: cwdProperty,
|
|
77
93
|
},
|
|
78
94
|
required: ["id"],
|
|
79
95
|
},
|
|
@@ -81,17 +97,17 @@ export const toolDefinitions = [
|
|
|
81
97
|
{
|
|
82
98
|
name: "add_trap_evidence",
|
|
83
99
|
description: "Attach traceable evidence/source metadata to an existing trap.",
|
|
84
|
-
inputSchema: trapEvidenceInputSchema(),
|
|
100
|
+
inputSchema: withCwd(trapEvidenceInputSchema()),
|
|
85
101
|
},
|
|
86
102
|
{
|
|
87
103
|
name: "archive_trap",
|
|
88
104
|
description: "Archive an active trap so default search no longer returns it while history remains recoverable.",
|
|
89
|
-
inputSchema: archiveTrapInputSchema(),
|
|
105
|
+
inputSchema: withCwd(archiveTrapInputSchema()),
|
|
90
106
|
},
|
|
91
107
|
{
|
|
92
108
|
name: "supersede_trap",
|
|
93
109
|
description: "Mark one trap as superseded by another trap in the same scope.",
|
|
94
|
-
inputSchema: supersedeTrapInputSchema(),
|
|
110
|
+
inputSchema: withCwd(supersedeTrapInputSchema()),
|
|
95
111
|
},
|
|
96
112
|
{
|
|
97
113
|
name: "get_stats",
|
|
@@ -100,7 +116,18 @@ export const toolDefinitions = [
|
|
|
100
116
|
type: "object",
|
|
101
117
|
properties: {
|
|
102
118
|
scope: { type: "string", enum: scopeEnum },
|
|
119
|
+
cwd: cwdProperty,
|
|
103
120
|
},
|
|
104
121
|
},
|
|
105
122
|
},
|
|
106
123
|
];
|
|
124
|
+
|
|
125
|
+
function withCwd<T extends { properties: Record<string, unknown> }>(schema: T): T {
|
|
126
|
+
return {
|
|
127
|
+
...schema,
|
|
128
|
+
properties: {
|
|
129
|
+
...schema.properties,
|
|
130
|
+
cwd: cwdProperty,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|