codetrap 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +112 -33
  3. package/docs/installation.md +18 -10
  4. package/package.json +4 -1
  5. package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
  6. package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
  7. package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
  8. package/plugins/codetrap-agent/hooks.json +11 -0
  9. package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
  10. package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
  11. package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
  12. package/scripts/release-preflight.ts +55 -0
  13. package/skills/codetrap-add/SKILL.md +4 -1
  14. package/skills/codetrap-check/SKILL.md +24 -4
  15. package/skills/codetrap-search/SKILL.md +32 -12
  16. package/src/commands/command-result.ts +29 -0
  17. package/src/commands/router.ts +6 -400
  18. package/src/commands/workflow.ts +419 -0
  19. package/src/db/embedding-queries.ts +33 -0
  20. package/src/db/queries.ts +165 -48
  21. package/src/db/repository.ts +72 -15
  22. package/src/db/schema.ts +35 -0
  23. package/src/domain/trap.ts +38 -10
  24. package/src/index.ts +13 -1
  25. package/src/lib/command-requests.ts +133 -0
  26. package/src/lib/config.ts +102 -0
  27. package/src/lib/constants.ts +1 -1
  28. package/src/lib/doctor.ts +86 -0
  29. package/src/lib/embedding-health.ts +49 -0
  30. package/src/lib/embedding-index.ts +53 -0
  31. package/src/lib/format.ts +6 -2
  32. package/src/lib/output-json.ts +141 -0
  33. package/src/lib/scope-context.ts +118 -0
  34. package/src/lib/scope-maintenance.ts +71 -0
  35. package/src/lib/scope-migration.ts +315 -0
  36. package/src/lib/scope-path.ts +99 -0
  37. package/src/lib/scope.ts +16 -11
  38. package/src/lib/search-normalizer.ts +6 -0
  39. package/src/lib/search-policy.ts +365 -0
  40. package/src/lib/search-result-card.ts +2 -7
  41. package/src/lib/search-service.ts +67 -120
  42. package/src/lib/store.ts +129 -108
  43. package/src/lib/trap-archive.ts +9 -42
  44. package/src/lib/trap-codec.ts +113 -0
  45. package/src/lib/trap-json-fields.ts +12 -0
  46. package/src/lib/trap-lifecycle.ts +37 -0
  47. package/src/lib/trap-mutation-result.ts +36 -0
  48. package/src/lib/trap-operations.ts +30 -9
  49. package/src/lib/trap-scope-match.ts +112 -0
  50. package/src/lib/trap-search-document.ts +8 -1
  51. package/src/lib/trap-transfer.ts +88 -0
  52. package/src/mcp/server.ts +77 -72
  53. package/src/mcp/tools.ts +32 -5
@@ -0,0 +1,133 @@
1
+ import { SEARCH_MODES, type SearchMode } from "./constants";
2
+ import type { SearchDefaults } from "./config";
3
+ import type { SearchTrapsArgs, ListTrapsArgs } from "./trap-operations";
4
+
5
+ type RawArgs = Record<string, unknown>;
6
+
7
+ export type EmbedRequest = {
8
+ scope?: string;
9
+ category?: string;
10
+ limit?: number;
11
+ force?: boolean;
12
+ batchSize?: number;
13
+ };
14
+
15
+ export type StatsRequest = {
16
+ scope?: string;
17
+ };
18
+
19
+ export function searchRequestFromArgs(query: string, args: RawArgs, defaults: SearchDefaults): SearchTrapsArgs {
20
+ return {
21
+ query,
22
+ category: stringOption(args, "category"),
23
+ scope: stringOption(args, "scope") ?? defaults.scope,
24
+ limit: intOption(args, "limit", defaults.limit),
25
+ mode: searchModeOption(args, "mode") ?? defaults.mode,
26
+ status: stringOption(args, "status"),
27
+ path: stringOption(args, "path"),
28
+ module: stringOption(args, "module"),
29
+ owner: stringOption(args, "owner"),
30
+ rerank: flagPresent(args, "no-rerank") ? false : booleanOption(args, "rerank") ?? defaults.rerank,
31
+ includeRankingSignals: booleanOption(args, "ranking_signals", "ranking-signals") ?? false,
32
+ };
33
+ }
34
+
35
+ export function listRequestFromArgs(args: RawArgs): ListTrapsArgs {
36
+ return {
37
+ category: stringOption(args, "category"),
38
+ scope: stringOption(args, "scope"),
39
+ status: stringOption(args, "status"),
40
+ path: stringOption(args, "path"),
41
+ module: stringOption(args, "module"),
42
+ owner: stringOption(args, "owner"),
43
+ limit: intOption(args, "limit", 50),
44
+ };
45
+ }
46
+
47
+ export function statsRequestFromArgs(args: RawArgs): StatsRequest {
48
+ return {
49
+ scope: stringOption(args, "scope"),
50
+ };
51
+ }
52
+
53
+ export function embedRequestFromArgs(args: RawArgs): EmbedRequest {
54
+ return {
55
+ scope: stringOption(args, "scope"),
56
+ category: stringOption(args, "category"),
57
+ limit: optionalIntOption(args, "limit"),
58
+ force: booleanOption(args, "force") === true,
59
+ batchSize: optionalIntOption(args, "batch_size", "batch-size"),
60
+ };
61
+ }
62
+
63
+ export function evidenceRequestFromArgs(args: RawArgs): RawArgs {
64
+ return {
65
+ source_type: stringOption(args, "source_type", "source-type"),
66
+ source_ref: stringOption(args, "source_ref", "source-ref"),
67
+ observed_at: stringOption(args, "observed_at", "observed-at"),
68
+ related_files: csvOrArrayOption(args, "related_files", "related-files"),
69
+ note: stringOption(args, "note"),
70
+ };
71
+ }
72
+
73
+ function stringOption(args: RawArgs, ...keys: string[]): string | undefined {
74
+ for (const key of keys) {
75
+ const value = args[key];
76
+ if (typeof value === "string" && value.trim() !== "") return value;
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ function intOption(args: RawArgs, key: string, fallback: number): number {
82
+ return optionalIntOption(args, key) ?? fallback;
83
+ }
84
+
85
+ function optionalIntOption(args: RawArgs, ...keys: string[]): number | undefined {
86
+ for (const key of keys) {
87
+ const value = args[key];
88
+ if (value === undefined) continue;
89
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
90
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
91
+ throw new Error(`Invalid number: ${String(value)}`);
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function searchModeOption(args: RawArgs, key: string): SearchMode | undefined {
97
+ const value = stringOption(args, key);
98
+ if (!value) return undefined;
99
+ if ((SEARCH_MODES as readonly string[]).includes(value)) return value as SearchMode;
100
+ throw new Error(`Invalid search mode: ${value}. Expected one of: ${SEARCH_MODES.join(", ")}`);
101
+ }
102
+
103
+ function booleanOption(args: RawArgs, ...keys: string[]): boolean | undefined {
104
+ for (const key of keys) {
105
+ const value = args[key];
106
+ if (value === undefined) continue;
107
+ if (typeof value === "boolean") return value;
108
+ if (typeof value === "string") {
109
+ if (["true", "1", "yes", "on"].includes(value.toLowerCase())) return true;
110
+ if (["false", "0", "no", "off"].includes(value.toLowerCase())) return false;
111
+ }
112
+ throw new Error(`Invalid boolean: ${String(value)}`);
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function flagPresent(args: RawArgs, key: string): boolean {
118
+ return args[key] !== undefined;
119
+ }
120
+
121
+ function csvOrArrayOption(args: RawArgs, ...keys: string[]): string[] | undefined {
122
+ for (const key of keys) {
123
+ const value = args[key];
124
+ if (Array.isArray(value)) return value.map(String);
125
+ if (typeof value === "string" && value.trim() !== "") {
126
+ return value
127
+ .split(",")
128
+ .map((item) => item.trim())
129
+ .filter(Boolean);
130
+ }
131
+ }
132
+ return undefined;
133
+ }
@@ -0,0 +1,102 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { CODETRAP_DIR, SCOPES, SEARCH_MODES, type Scope, type SearchMode } from "./constants";
5
+
6
+ export type CodetrapConfig = {
7
+ search?: {
8
+ mode?: SearchMode;
9
+ limit?: number;
10
+ scope?: Scope;
11
+ rerank?: boolean;
12
+ };
13
+ };
14
+
15
+ export type SearchDefaults = {
16
+ mode: SearchMode;
17
+ limit: number;
18
+ scope?: Scope;
19
+ rerank: boolean;
20
+ };
21
+
22
+ const BUILT_IN_SEARCH_DEFAULTS: SearchDefaults = {
23
+ mode: "hybrid",
24
+ limit: 20,
25
+ rerank: true,
26
+ };
27
+
28
+ export function loadCodetrapConfig(home = homedir()): CodetrapConfig {
29
+ const path = join(home, CODETRAP_DIR, "config.json");
30
+ if (!existsSync(path)) return {};
31
+
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
34
+ return normalizeConfig(parsed);
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ throw new Error(`Invalid codetrap config at ${path}: ${message}`);
38
+ }
39
+ }
40
+
41
+ export function searchDefaultsFromConfig(config = loadCodetrapConfig(), env = process.env): SearchDefaults {
42
+ return {
43
+ mode: config.search?.mode ?? parseSearchModeEnv(env.CODETRAP_SEARCH_MODE) ?? BUILT_IN_SEARCH_DEFAULTS.mode,
44
+ limit: config.search?.limit ?? parsePositiveIntEnv(env.CODETRAP_SEARCH_LIMIT) ?? BUILT_IN_SEARCH_DEFAULTS.limit,
45
+ scope: config.search?.scope ?? parseScopeEnv(env.CODETRAP_SEARCH_SCOPE),
46
+ rerank: config.search?.rerank ?? parseBooleanEnv(env.CODETRAP_RERANK) ?? BUILT_IN_SEARCH_DEFAULTS.rerank,
47
+ };
48
+ }
49
+
50
+ function normalizeConfig(value: unknown): CodetrapConfig {
51
+ if (!isRecord(value)) return {};
52
+ const search = isRecord(value.search) ? normalizeSearchConfig(value.search) : undefined;
53
+ return search ? { search } : {};
54
+ }
55
+
56
+ function normalizeSearchConfig(value: Record<string, unknown>): CodetrapConfig["search"] {
57
+ const out: NonNullable<CodetrapConfig["search"]> = {};
58
+ if (typeof value.mode === "string") out.mode = parseSearchMode(value.mode);
59
+ if (typeof value.limit === "number") out.limit = parsePositiveInt(value.limit, "search.limit");
60
+ if (typeof value.scope === "string") out.scope = parseScope(value.scope);
61
+ if (typeof value.rerank === "boolean") out.rerank = value.rerank;
62
+ return out;
63
+ }
64
+
65
+ function parseSearchModeEnv(value?: string): SearchMode | undefined {
66
+ return value ? parseSearchMode(value) : undefined;
67
+ }
68
+
69
+ function parseScopeEnv(value?: string): Scope | undefined {
70
+ return value ? parseScope(value) : undefined;
71
+ }
72
+
73
+ function parsePositiveIntEnv(value?: string): number | undefined {
74
+ if (!value) return undefined;
75
+ return parsePositiveInt(Number.parseInt(value, 10), "CODETRAP_SEARCH_LIMIT");
76
+ }
77
+
78
+ function parseBooleanEnv(value?: string): boolean | undefined {
79
+ if (!value) return undefined;
80
+ if (["1", "true", "yes", "on"].includes(value.toLowerCase())) return true;
81
+ if (["0", "false", "no", "off"].includes(value.toLowerCase())) return false;
82
+ throw new Error(`Invalid CODETRAP_RERANK: ${value}. Expected true or false.`);
83
+ }
84
+
85
+ function parseSearchMode(value: string): SearchMode {
86
+ if ((SEARCH_MODES as readonly string[]).includes(value)) return value as SearchMode;
87
+ throw new Error(`Invalid search mode: ${value}. Expected one of: ${SEARCH_MODES.join(", ")}`);
88
+ }
89
+
90
+ function parseScope(value: string): Scope {
91
+ if ((SCOPES as readonly string[]).includes(value)) return value as Scope;
92
+ throw new Error(`Invalid scope: ${value}. Expected one of: ${SCOPES.join(", ")}`);
93
+ }
94
+
95
+ function parsePositiveInt(value: number, label: string): number {
96
+ if (Number.isInteger(value) && value > 0) return value;
97
+ throw new Error(`Invalid ${label}: expected a positive integer.`);
98
+ }
99
+
100
+ function isRecord(value: unknown): value is Record<string, unknown> {
101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
+ }
@@ -49,7 +49,7 @@ export const DEFAULT_TRAP_STATUS: TrapStatus = "active";
49
49
 
50
50
  // Increment this when schema changes in a breaking way.
51
51
  // Migrations are stored in src/db/migrations.ts
52
- export const SCHEMA_VERSION = 4;
52
+ export const SCHEMA_VERSION = 5;
53
53
 
54
54
  // Directory and file names
55
55
  export const CODETRAP_DIR = ".codetrap";
@@ -0,0 +1,86 @@
1
+ import type { TrapStore } from "./store";
2
+ import type { TrapOperations } from "./trap-operations";
3
+ import type { EmbeddingStatsResult, HybridFallbackReason } from "./embedding-health";
4
+ import { createScopeContext } from "./scope-context";
5
+ import { hybridFallbackReason } from "./embedding-health";
6
+
7
+ export type DoctorReport = {
8
+ cwd: string;
9
+ project_root: string | null;
10
+ project_db: string | null;
11
+ global_db: string;
12
+ traps: {
13
+ project: number | null;
14
+ global: number;
15
+ };
16
+ embeddings: EmbeddingStatsResult;
17
+ hybrid_search: {
18
+ semantic_available: boolean;
19
+ fallback_reason: HybridFallbackReason | null;
20
+ };
21
+ diagnostics: {
22
+ mis_scoped_traps: {
23
+ global_db_project_traps: ReturnType<TrapStore["diagnostics"]>["mis_scoped_traps"]["global_db_project_traps"];
24
+ };
25
+ };
26
+ mcp_hint: string;
27
+ };
28
+
29
+ export function buildDoctorReport(
30
+ store: TrapStore,
31
+ operations: TrapOperations,
32
+ cwd = process.cwd()
33
+ ): DoctorReport {
34
+ const scope = createScopeContext(cwd);
35
+ const stats = operations.getStats();
36
+ const embeddings = operations.getEmbeddingStats();
37
+ const semanticAvailable = store.hasEmbeddingProvider();
38
+
39
+ return {
40
+ ...scope,
41
+ traps: {
42
+ project: stats.project?.total ?? null,
43
+ global: stats.global?.total ?? 0,
44
+ },
45
+ embeddings,
46
+ hybrid_search: {
47
+ semantic_available: semanticAvailable,
48
+ fallback_reason: hybridFallbackReason(semanticAvailable, embeddings),
49
+ },
50
+ diagnostics: {
51
+ mis_scoped_traps: store.diagnostics().mis_scoped_traps,
52
+ },
53
+ mcp_hint: "Pass cwd in MCP tool calls, or restart codetrap serve after changing projects.",
54
+ };
55
+ }
56
+
57
+ export function formatDoctorText(report: DoctorReport): string {
58
+ return [
59
+ `cwd: ${report.cwd}`,
60
+ `project_root: ${report.project_root ?? "(none)"}`,
61
+ `project_db: ${report.project_db ?? "(none)"}`,
62
+ `global_db: ${report.global_db}`,
63
+ `project_traps: ${report.traps.project ?? "(none)"}`,
64
+ `global_traps: ${report.traps.global}`,
65
+ "Embeddings:",
66
+ formatEmbeddingStats("project", report.embeddings.project),
67
+ formatEmbeddingStats("global", report.embeddings.global),
68
+ "Hybrid search:",
69
+ ` semantic_available: ${report.hybrid_search.semantic_available ? "yes" : "no"}`,
70
+ ` fallback_reason: ${report.hybrid_search.fallback_reason ?? "(none)"}`,
71
+ "Diagnostics:",
72
+ ` global_db_project_traps: ${report.diagnostics.mis_scoped_traps.global_db_project_traps.length}`,
73
+ `mcp_hint: ${report.mcp_hint}`,
74
+ ].join("\n");
75
+ }
76
+
77
+ function formatEmbeddingStats(
78
+ label: string,
79
+ stats: EmbeddingStatsResult["global"] | null
80
+ ): string {
81
+ if (!stats) return ` ${label}: unavailable`;
82
+ const provider = stats.provider_available
83
+ ? `${stats.provider}/${stats.model}`
84
+ : "unavailable";
85
+ return ` ${label}: fresh=${stats.fresh}, stale=${stats.stale}, missing=${stats.missing}, total=${stats.total}, provider=${provider}`;
86
+ }
@@ -0,0 +1,49 @@
1
+ import type { EmbeddingConfig } from "./embedder";
2
+
3
+ export type EmbeddingStateCounts = {
4
+ total: number;
5
+ fresh: number;
6
+ stale: number;
7
+ missing: number;
8
+ };
9
+
10
+ export type EmbeddingStateSummary = EmbeddingStateCounts & {
11
+ provider_available: boolean;
12
+ provider: string | null;
13
+ model: string | null;
14
+ dimensions: number | null;
15
+ passage_version: number | null;
16
+ };
17
+
18
+ export type EmbeddingStatsResult = {
19
+ project: EmbeddingStateSummary | null;
20
+ global: EmbeddingStateSummary | null;
21
+ };
22
+
23
+ export type HybridFallbackReason = "semantic_unavailable" | "semantic_no_candidates";
24
+
25
+ export function summarizeEmbeddingState(
26
+ counts: EmbeddingStateCounts,
27
+ config: EmbeddingConfig | null
28
+ ): EmbeddingStateSummary {
29
+ return {
30
+ ...counts,
31
+ provider_available: config !== null,
32
+ provider: config?.provider ?? null,
33
+ model: config?.model ?? null,
34
+ dimensions: config?.dimensions ?? null,
35
+ passage_version: config?.passageVersion ?? null,
36
+ };
37
+ }
38
+
39
+ export function hybridFallbackReason(
40
+ providerAvailable: boolean,
41
+ embeddings: EmbeddingStatsResult
42
+ ): HybridFallbackReason | null {
43
+ if (!providerAvailable) return "semantic_unavailable";
44
+
45
+ const fresh = (embeddings.project?.fresh ?? 0) + (embeddings.global?.fresh ?? 0);
46
+ const total = (embeddings.project?.total ?? 0) + (embeddings.global?.total ?? 0);
47
+ if (total > 0 && fresh === 0) return "semantic_no_candidates";
48
+ return null;
49
+ }
@@ -0,0 +1,53 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import * as embeddingQueries from "../db/embedding-queries";
3
+ import type { Trap } from "../domain/trap";
4
+ import type { TrapStatus } from "./constants";
5
+ import type {
6
+ EmbeddingConfig,
7
+ FreshEmbedding,
8
+ StoredEmbedding,
9
+ } from "./embedder";
10
+ import type { EmbeddingStateCounts } from "./embedding-health";
11
+
12
+ export type EmbeddingIndexFilter = {
13
+ scope?: string;
14
+ category?: string;
15
+ status?: TrapStatus | "all";
16
+ };
17
+
18
+ export type EmbeddingRefreshFilter = EmbeddingIndexFilter & {
19
+ force?: boolean;
20
+ limit?: number;
21
+ };
22
+
23
+ export class DatabaseEmbeddingIndex {
24
+ constructor(private readonly db: Database) {}
25
+
26
+ get(trapId: number): StoredEmbedding | null {
27
+ return embeddingQueries.getEmbedding(this.db, trapId);
28
+ }
29
+
30
+ save(record: StoredEmbedding): void {
31
+ embeddingQueries.upsertEmbedding(this.db, record);
32
+ }
33
+
34
+ delete(trapId: number): void {
35
+ embeddingQueries.deleteEmbedding(this.db, trapId);
36
+ }
37
+
38
+ freshEmbeddings(config: EmbeddingConfig, filter: EmbeddingIndexFilter = {}): FreshEmbedding[] {
39
+ return embeddingQueries.getAllFreshEmbeddings(this.db, config, filter);
40
+ }
41
+
42
+ trapsNeedingEmbeddings(config: EmbeddingConfig, filter: EmbeddingRefreshFilter = {}): Trap[] {
43
+ return embeddingQueries.getTrapsNeedingEmbeddings(this.db, config, filter);
44
+ }
45
+
46
+ countEmbeddable(filter: EmbeddingIndexFilter = {}): number {
47
+ return embeddingQueries.countEmbeddableTraps(this.db, filter);
48
+ }
49
+
50
+ stateCounts(config: EmbeddingConfig | null, filter: EmbeddingIndexFilter = {}): EmbeddingStateCounts {
51
+ return embeddingQueries.getEmbeddingStateCounts(this.db, config, filter);
52
+ }
53
+ }
package/src/lib/format.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CATEGORY_LABELS, SEVERITY_ICONS, type Category, type Severity } from "./constants";
2
2
  import type { Trap, TrapActionCard, TrapDetails, TrapEvidence } from "../domain/trap";
3
- import { parseEvidenceRelatedFiles, parseTrapTags } from "./trap-json-fields";
3
+ import { parseEvidenceRelatedFiles, parseTrapPathGlobs, parseTrapTags } from "./trap-json-fields";
4
4
  export type { Trap } from "../domain/trap";
5
5
 
6
6
  export function formatTrapShort(t: Trap, scopeLabel: string): string {
@@ -18,13 +18,14 @@ Why relevant: ${card.why_relevant}
18
18
  Avoid: ${card.avoid}
19
19
  Do instead: ${card.do_instead}
20
20
  Score: ${formatScore(card.score)} (${sourceLabel})
21
- Next: get_trap id=${card.next_action.details_args.id} scope=${card.next_action.details_args.scope}`;
21
+ Next: codetrap show ${card.trap_id} --scope ${card.scope} --json`;
22
22
  }
23
23
 
24
24
  export function formatTrapDetail(t: Trap, scopeLabel: string): string {
25
25
  const sev = SEVERITY_ICONS[t.severity as Severity] ?? t.severity;
26
26
  const cat = CATEGORY_LABELS[t.category as Category] ?? t.category;
27
27
  const tags = parseTrapTags(t.tags);
28
+ const pathGlobs = parseTrapPathGlobs(t.path_globs);
28
29
  let out = `\
29
30
  ══════════════════════════════════════════
30
31
  #${t.id} ${t.title}
@@ -33,6 +34,9 @@ Scope: ${scopeLabel} (${t.scope})
33
34
  Severity: ${sev}
34
35
  Category: ${cat}
35
36
  Tags: ${tags.join(", ") || "-"}
37
+ Paths: ${pathGlobs.join(", ") || "-"}
38
+ Module: ${t.module ?? "-"}
39
+ Owner: ${t.owner ?? "-"}
36
40
  Hit count: ${t.hit_count}
37
41
  Created: ${t.created_at}
38
42
  Updated: ${t.updated_at}
@@ -0,0 +1,141 @@
1
+ import type { RankingSignal, TrapActionCard, TrapDetails, TrapSearchDiagnostic } from "../domain/trap";
2
+ import type { Scope } from "./constants";
3
+ import type { DoctorReport } from "./doctor";
4
+ import type { EmbeddingStatsResult } from "./embedding-health";
5
+ import type { TrapListGroup, TrapStatsResult } from "./trap-operations";
6
+ import {
7
+ toTrapEvidenceJson,
8
+ toTrapJson,
9
+ type JsonTrap,
10
+ type JsonTrapEvidence,
11
+ } from "./trap-codec";
12
+
13
+ export type JsonTrapDetails = {
14
+ scope: Scope;
15
+ trap: JsonTrap;
16
+ evidence: JsonTrapEvidence[];
17
+ };
18
+
19
+ export type CliTrapActionCard = TrapActionCard & {
20
+ next_action: {
21
+ command: string;
22
+ };
23
+ ranking_signals?: RankingSignal[];
24
+ diagnostics?: TrapSearchDiagnostic[];
25
+ };
26
+
27
+ export type McpTrapActionCard = TrapActionCard & {
28
+ next_action: {
29
+ details_tool: "get_trap";
30
+ details_args: {
31
+ id: number;
32
+ scope: Scope;
33
+ };
34
+ };
35
+ ranking_signals?: RankingSignal[];
36
+ diagnostics?: TrapSearchDiagnostic[];
37
+ };
38
+
39
+ export type McpTextResult = {
40
+ content: { type: "text"; text: string }[];
41
+ isError?: boolean;
42
+ };
43
+
44
+ export type McpResourceResult = {
45
+ contents: { uri: string; mimeType: string; text: string }[];
46
+ };
47
+
48
+ export function toCliSearchJson(cards: TrapActionCard[]): CliTrapActionCard[] {
49
+ return cards.map((card) => ({
50
+ trap_id: card.trap_id,
51
+ scope: card.scope,
52
+ title: card.title,
53
+ why_relevant: card.why_relevant,
54
+ avoid: card.avoid,
55
+ do_instead: card.do_instead,
56
+ severity: card.severity,
57
+ score: card.score,
58
+ sources: card.sources,
59
+ ...(card.diagnostics ? { diagnostics: card.diagnostics } : {}),
60
+ ...(card.ranking_signals ? { ranking_signals: card.ranking_signals } : {}),
61
+ next_action: {
62
+ command: `codetrap show ${card.trap_id} --scope ${card.scope} --json`,
63
+ },
64
+ }));
65
+ }
66
+
67
+ export function toMcpSearchJson(cards: TrapActionCard[]): McpTrapActionCard[] {
68
+ return cards.map((card) => ({
69
+ ...card,
70
+ next_action: {
71
+ details_tool: "get_trap",
72
+ details_args: {
73
+ id: card.trap_id,
74
+ scope: card.scope,
75
+ },
76
+ },
77
+ }));
78
+ }
79
+
80
+ export function toMcpTextJson(value: unknown, isError = false): McpTextResult {
81
+ return {
82
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
83
+ ...(isError ? { isError: true } : {}),
84
+ };
85
+ }
86
+
87
+ export function toMcpTextError(message: string): McpTextResult {
88
+ return toMcpTextJson({ error: message }, true);
89
+ }
90
+
91
+ export function toMcpResourceJson(uri: string, value: unknown): McpResourceResult {
92
+ return {
93
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(value, null, 2) }],
94
+ };
95
+ }
96
+
97
+ export function toMcpResourceText(uri: string, text: string): McpResourceResult {
98
+ return {
99
+ contents: [{ uri, mimeType: "text/plain", text }],
100
+ };
101
+ }
102
+
103
+ export function toTrapDetailsJson(details: TrapDetails): JsonTrapDetails {
104
+ return {
105
+ scope: details.scope,
106
+ trap: toTrapJson(details.trap),
107
+ evidence: details.evidence.map(toTrapEvidenceJson),
108
+ };
109
+ }
110
+
111
+ export function toListJson(groups: TrapListGroup[]): JsonTrap[] {
112
+ return groups.flatMap((group) =>
113
+ group.traps.map((trap) => ({
114
+ ...toTrapJson(trap),
115
+ scope: group.scope,
116
+ }))
117
+ );
118
+ }
119
+
120
+ export function toStatsJson(stats: TrapStatsResult, embeddings?: EmbeddingStatsResult) {
121
+ return {
122
+ project: stats.project
123
+ ? {
124
+ ...stats.project,
125
+ embeddings: embeddings?.project ?? null,
126
+ }
127
+ : null,
128
+ global: stats.global
129
+ ? {
130
+ ...stats.global,
131
+ embeddings: embeddings?.global ?? null,
132
+ }
133
+ : null,
134
+ };
135
+ }
136
+
137
+ export function toDoctorJson(input: DoctorReport): DoctorReport {
138
+ return input;
139
+ }
140
+
141
+ export { toTrapEvidenceJson, toTrapJson, type JsonTrap, type JsonTrapEvidence };