codetrap 0.1.7 → 0.1.9
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/README.md +132 -98
- package/docs/installation.md +61 -63
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +2 -3
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
- package/plugins/codetrap-agent/hooks.json +2 -2
- package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
- package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
- package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
- package/plugins/codetrap-agent/templates/AGENTS.codetrap-maintainer.md +15 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +16 -5
- package/scripts/release-preflight.ts +15 -0
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +172 -68
- package/src/db/embedding-queries.ts +230 -48
- package/src/db/queries.ts +0 -25
- package/src/db/repository.ts +32 -21
- package/src/db/schema.ts +80 -0
- package/src/index.ts +34 -4
- package/src/lib/codex-setup.ts +247 -0
- package/src/lib/command-requests.ts +112 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +42 -12
- package/src/lib/embedder.ts +118 -3
- package/src/lib/embedding-health.ts +3 -1
- package/src/lib/embedding-job.ts +3 -0
- package/src/lib/embedding-management.ts +65 -0
- package/src/lib/embedding-runtime.ts +177 -0
- package/src/lib/output-json.ts +0 -2
- package/src/lib/scope-context.ts +12 -6
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +0 -2
- package/src/lib/search-eval.ts +38 -18
- package/src/lib/search-policy-sweep.ts +563 -0
- package/src/lib/search-policy.ts +0 -4
- package/src/lib/search-service.ts +14 -15
- package/src/lib/session-candidate-document.ts +175 -0
- package/src/lib/session-candidate-scope.ts +6 -0
- package/src/lib/session-capture.ts +298 -32
- package/src/lib/session-codec.ts +1 -8
- package/src/lib/session-operations.ts +83 -60
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +87 -73
- package/src/lib/store.ts +74 -10
- package/src/lib/string-list.ts +3 -0
- package/src/lib/text-lines.ts +7 -0
- package/src/lib/trap-search-document.ts +2 -1
- package/src/lib/value-types.ts +3 -0
- package/src/web/client-review.ts +171 -0
- package/src/web/client-script.ts +426 -51
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +112 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +117 -103
- package/src/web/static.ts +364 -19
- package/skills/codetrap-capture-external/SKILL.md +0 -62
- package/skills/codetrap-check/SKILL.md +0 -69
- package/src/lib/embedding-index.ts +0 -53
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { findProjectRoot } from "./scope";
|
|
14
|
+
import agentsTemplateAsset from "../../plugins/codetrap-agent/templates/AGENTS.codetrap.md" with { type: "text" };
|
|
15
|
+
import codetrapAddSkill from "../../plugins/codetrap-agent/skills/codetrap-add/SKILL.md" with { type: "text" };
|
|
16
|
+
import codetrapCaptureSkill from "../../plugins/codetrap-agent/skills/codetrap-capture/SKILL.md" with { type: "text" };
|
|
17
|
+
import codetrapCaptureExternalSkill from "../../plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md" with { type: "text" };
|
|
18
|
+
import codetrapCheckSkill from "../../plugins/codetrap-agent/skills/codetrap-check/SKILL.md" with { type: "text" };
|
|
19
|
+
import codetrapSearchSkill from "../../plugins/codetrap-agent/skills/codetrap-search/SKILL.md" with { type: "text" };
|
|
20
|
+
|
|
21
|
+
export type CodexSetupOptions = {
|
|
22
|
+
cwd: string;
|
|
23
|
+
codexHome?: string;
|
|
24
|
+
agentsFile?: string;
|
|
25
|
+
installMcp?: boolean;
|
|
26
|
+
skipAgents?: boolean;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CodexSetupStatus =
|
|
31
|
+
| "already_present"
|
|
32
|
+
| "created"
|
|
33
|
+
| "appended"
|
|
34
|
+
| "installed"
|
|
35
|
+
| "updated"
|
|
36
|
+
| "unchanged"
|
|
37
|
+
| "skipped"
|
|
38
|
+
| "would_create"
|
|
39
|
+
| "would_append"
|
|
40
|
+
| "would_install"
|
|
41
|
+
| "would_update"
|
|
42
|
+
| "would_run"
|
|
43
|
+
| "failed";
|
|
44
|
+
|
|
45
|
+
export type CodexSetupResult = {
|
|
46
|
+
success: boolean;
|
|
47
|
+
project_root: string;
|
|
48
|
+
codex_home: string;
|
|
49
|
+
plugin_root: string;
|
|
50
|
+
dry_run: boolean;
|
|
51
|
+
project: {
|
|
52
|
+
codetrap_dir: string;
|
|
53
|
+
status: CodexSetupStatus;
|
|
54
|
+
};
|
|
55
|
+
skills: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
source: string;
|
|
58
|
+
destination: string;
|
|
59
|
+
status: CodexSetupStatus;
|
|
60
|
+
}>;
|
|
61
|
+
agents: {
|
|
62
|
+
path: string | null;
|
|
63
|
+
status: CodexSetupStatus;
|
|
64
|
+
};
|
|
65
|
+
mcp: {
|
|
66
|
+
requested: boolean;
|
|
67
|
+
command: string;
|
|
68
|
+
status: CodexSetupStatus;
|
|
69
|
+
exit_code?: number | null;
|
|
70
|
+
error?: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const AGENTS_TEMPLATE_PATH = "templates/AGENTS.codetrap.md";
|
|
75
|
+
const CODEX_MARKER = "codetrap search \"<keywords>\" --mode hybrid --json";
|
|
76
|
+
const MCP_COMMAND = ["codex", "mcp", "add", "codetrap", "--", "codetrap", "serve"];
|
|
77
|
+
const EMBEDDED_PLUGIN_ROOT = "embedded://plugins/codetrap-agent";
|
|
78
|
+
const EMBEDDED_SKILLS = [
|
|
79
|
+
{ name: "codetrap-add", skill: codetrapAddSkill },
|
|
80
|
+
{ name: "codetrap-capture", skill: codetrapCaptureSkill },
|
|
81
|
+
{ name: "codetrap-capture-external", skill: codetrapCaptureExternalSkill },
|
|
82
|
+
{ name: "codetrap-check", skill: codetrapCheckSkill },
|
|
83
|
+
{ name: "codetrap-search", skill: codetrapSearchSkill },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
export function runCodexSetup(options: CodexSetupOptions): CodexSetupResult {
|
|
87
|
+
const cwd = resolve(options.cwd);
|
|
88
|
+
const projectRoot = findProjectRoot(cwd) ?? cwd;
|
|
89
|
+
const codexHome = resolveCodexHome(options.codexHome);
|
|
90
|
+
const pluginRoot = bundledPluginRoot();
|
|
91
|
+
const useEmbeddedAssets = !existsSync(pluginRoot);
|
|
92
|
+
|
|
93
|
+
const dryRun = options.dryRun === true;
|
|
94
|
+
const project = ensureProjectCodetrap(projectRoot, dryRun);
|
|
95
|
+
const skills = useEmbeddedAssets
|
|
96
|
+
? installEmbeddedSkills(codexHome, dryRun)
|
|
97
|
+
: installSkills(pluginRoot, codexHome, dryRun);
|
|
98
|
+
const agents = options.skipAgents
|
|
99
|
+
? { path: null, status: "skipped" as const }
|
|
100
|
+
: installAgentsTemplate(projectRoot, useEmbeddedAssets ? null : pluginRoot, options.agentsFile ?? "AGENTS.md", dryRun);
|
|
101
|
+
const mcp = setupMcp(options.installMcp === true, dryRun);
|
|
102
|
+
const success = mcp.status !== "failed";
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
success,
|
|
106
|
+
project_root: projectRoot,
|
|
107
|
+
codex_home: codexHome,
|
|
108
|
+
plugin_root: useEmbeddedAssets ? EMBEDDED_PLUGIN_ROOT : pluginRoot,
|
|
109
|
+
dry_run: dryRun,
|
|
110
|
+
project,
|
|
111
|
+
skills,
|
|
112
|
+
agents,
|
|
113
|
+
mcp,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatCodexSetupText(result: CodexSetupResult): string {
|
|
118
|
+
const installed = result.skills.filter((skill) =>
|
|
119
|
+
["installed", "updated", "would_install", "would_update"].includes(skill.status)
|
|
120
|
+
).length;
|
|
121
|
+
const unchanged = result.skills.filter((skill) => skill.status === "unchanged").length;
|
|
122
|
+
const lines = [
|
|
123
|
+
result.success ? "Codex setup complete." : "Codex setup completed with errors.",
|
|
124
|
+
`Project: ${result.project.status} (${result.project.codetrap_dir})`,
|
|
125
|
+
`Skills: ${installed} changed, ${unchanged} unchanged (${join(result.codex_home, "skills")})`,
|
|
126
|
+
`AGENTS: ${result.agents.status}${result.agents.path ? ` (${result.agents.path})` : ""}`,
|
|
127
|
+
];
|
|
128
|
+
if (result.mcp.requested) {
|
|
129
|
+
lines.push(`MCP: ${result.mcp.status} (${result.mcp.command})`);
|
|
130
|
+
if (result.mcp.error) lines.push(`MCP error: ${result.mcp.error}`);
|
|
131
|
+
} else {
|
|
132
|
+
lines.push(`MCP: skipped; pass --mcp to run '${result.mcp.command}'.`);
|
|
133
|
+
}
|
|
134
|
+
if (result.dry_run) lines.unshift("Dry run; no files or Codex config were changed.");
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ensureProjectCodetrap(projectRoot: string, dryRun: boolean): CodexSetupResult["project"] {
|
|
139
|
+
const codetrapDir = join(projectRoot, ".codetrap");
|
|
140
|
+
if (existsSync(codetrapDir)) {
|
|
141
|
+
return { codetrap_dir: codetrapDir, status: "already_present" };
|
|
142
|
+
}
|
|
143
|
+
if (!dryRun) mkdirSync(codetrapDir, { recursive: true });
|
|
144
|
+
return { codetrap_dir: codetrapDir, status: dryRun ? "would_create" : "created" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function installSkills(pluginRoot: string, codexHome: string, dryRun: boolean): CodexSetupResult["skills"] {
|
|
148
|
+
const sourceSkillsDir = join(pluginRoot, "skills");
|
|
149
|
+
const targetSkillsDir = join(codexHome, "skills");
|
|
150
|
+
if (!dryRun) mkdirSync(targetSkillsDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
return readdirSync(sourceSkillsDir, { withFileTypes: true })
|
|
153
|
+
.filter((entry) => entry.isDirectory())
|
|
154
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
155
|
+
.map((entry) => {
|
|
156
|
+
const source = join(sourceSkillsDir, entry.name);
|
|
157
|
+
const destination = join(targetSkillsDir, entry.name);
|
|
158
|
+
const sourceSkill = readFileSync(join(source, "SKILL.md"), "utf-8");
|
|
159
|
+
const destinationSkillPath = join(destination, "SKILL.md");
|
|
160
|
+
const exists = existsSync(destinationSkillPath);
|
|
161
|
+
const unchanged = exists && readFileSync(destinationSkillPath, "utf-8") === sourceSkill;
|
|
162
|
+
let status: CodexSetupStatus = unchanged ? "unchanged" : exists ? "updated" : "installed";
|
|
163
|
+
if (dryRun && status === "installed") status = "would_install";
|
|
164
|
+
if (dryRun && status === "updated") status = "would_update";
|
|
165
|
+
if (!dryRun && !unchanged) cpSync(source, destination, { recursive: true, force: true });
|
|
166
|
+
return { name: entry.name, source, destination, status };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function installEmbeddedSkills(codexHome: string, dryRun: boolean): CodexSetupResult["skills"] {
|
|
171
|
+
const targetSkillsDir = join(codexHome, "skills");
|
|
172
|
+
if (!dryRun) mkdirSync(targetSkillsDir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
return EMBEDDED_SKILLS.map((entry) => {
|
|
175
|
+
const destination = join(targetSkillsDir, entry.name);
|
|
176
|
+
const destinationSkillPath = join(destination, "SKILL.md");
|
|
177
|
+
const exists = existsSync(destinationSkillPath);
|
|
178
|
+
const unchanged = exists && readFileSync(destinationSkillPath, "utf-8") === entry.skill;
|
|
179
|
+
let status: CodexSetupStatus = unchanged ? "unchanged" : exists ? "updated" : "installed";
|
|
180
|
+
if (dryRun && status === "installed") status = "would_install";
|
|
181
|
+
if (dryRun && status === "updated") status = "would_update";
|
|
182
|
+
if (!dryRun && !unchanged) {
|
|
183
|
+
mkdirSync(destination, { recursive: true });
|
|
184
|
+
writeFileSync(destinationSkillPath, entry.skill);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
name: entry.name,
|
|
188
|
+
source: `${EMBEDDED_PLUGIN_ROOT}/skills/${entry.name}`,
|
|
189
|
+
destination,
|
|
190
|
+
status,
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function installAgentsTemplate(
|
|
196
|
+
projectRoot: string,
|
|
197
|
+
pluginRoot: string | null,
|
|
198
|
+
agentsFile: string,
|
|
199
|
+
dryRun: boolean
|
|
200
|
+
): CodexSetupResult["agents"] {
|
|
201
|
+
const target = resolve(projectRoot, agentsFile);
|
|
202
|
+
const template = (pluginRoot
|
|
203
|
+
? readFileSync(join(pluginRoot, AGENTS_TEMPLATE_PATH), "utf-8")
|
|
204
|
+
: agentsTemplateAsset
|
|
205
|
+
).trimEnd();
|
|
206
|
+
if (existsSync(target)) {
|
|
207
|
+
const current = readFileSync(target, "utf-8");
|
|
208
|
+
if (current.includes(CODEX_MARKER)) {
|
|
209
|
+
return { path: target, status: "already_present" };
|
|
210
|
+
}
|
|
211
|
+
if (!dryRun) appendFileSync(target, `${current.endsWith("\n") ? "\n" : "\n\n"}${template}\n`);
|
|
212
|
+
return { path: target, status: dryRun ? "would_append" : "appended" };
|
|
213
|
+
}
|
|
214
|
+
if (!dryRun) writeFileSync(target, `${template}\n`);
|
|
215
|
+
return { path: target, status: dryRun ? "would_create" : "created" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setupMcp(requested: boolean, dryRun: boolean): CodexSetupResult["mcp"] {
|
|
219
|
+
const command = MCP_COMMAND.join(" ");
|
|
220
|
+
if (!requested) return { requested, command, status: "skipped" };
|
|
221
|
+
if (dryRun) return { requested, command, status: "would_run" };
|
|
222
|
+
|
|
223
|
+
const result = Bun.spawnSync({
|
|
224
|
+
cmd: MCP_COMMAND,
|
|
225
|
+
stdout: "pipe",
|
|
226
|
+
stderr: "pipe",
|
|
227
|
+
});
|
|
228
|
+
if (result.success) return { requested, command, status: "installed", exit_code: result.exitCode };
|
|
229
|
+
|
|
230
|
+
const stderr = new TextDecoder().decode(result.stderr).trim();
|
|
231
|
+
const stdout = new TextDecoder().decode(result.stdout).trim();
|
|
232
|
+
return {
|
|
233
|
+
requested,
|
|
234
|
+
command,
|
|
235
|
+
status: "failed",
|
|
236
|
+
exit_code: result.exitCode,
|
|
237
|
+
error: stderr || stdout || "codex mcp add failed",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveCodexHome(codexHome?: string): string {
|
|
242
|
+
return resolve(codexHome ?? process.env.CODEX_HOME ?? join(process.env.HOME ?? process.env.USERPROFILE ?? homedir(), ".codex"));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function bundledPluginRoot(): string {
|
|
246
|
+
return join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), "plugins", "codetrap-agent");
|
|
247
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SEARCH_MODES, type SearchMode } from "./constants";
|
|
2
|
-
import type { SearchDefaults } from "./config";
|
|
2
|
+
import type { EmbeddingProviderSetting, EmbeddingSettings, SearchDefaults } from "./config";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_OLLAMA_DIMENSIONS,
|
|
5
|
+
DEFAULT_OLLAMA_ENDPOINT,
|
|
6
|
+
DEFAULT_OLLAMA_MODEL,
|
|
7
|
+
} from "./embedder";
|
|
8
|
+
import { capturedTrapMarkdownInput } from "./session-capture";
|
|
9
|
+
import { uniqueStrings as uniqueStringList } from "./string-list";
|
|
3
10
|
import type { SearchTrapsArgs, ListTrapsArgs } from "./trap-operations";
|
|
4
11
|
|
|
5
12
|
type RawArgs = Record<string, unknown>;
|
|
@@ -12,6 +19,10 @@ export type EmbedRequest = {
|
|
|
12
19
|
batchSize?: number;
|
|
13
20
|
};
|
|
14
21
|
|
|
22
|
+
export type EmbeddingsUseRequest = {
|
|
23
|
+
embeddings: EmbeddingSettings;
|
|
24
|
+
};
|
|
25
|
+
|
|
15
26
|
export type StatsRequest = {
|
|
16
27
|
scope?: string;
|
|
17
28
|
};
|
|
@@ -40,6 +51,15 @@ export type SessionCloseRequest = {
|
|
|
40
51
|
proposeTraps: boolean;
|
|
41
52
|
};
|
|
42
53
|
|
|
54
|
+
export type SessionCaptureRequest = {
|
|
55
|
+
trap: Record<string, unknown>;
|
|
56
|
+
goal?: string;
|
|
57
|
+
kind?: string;
|
|
58
|
+
relatedFiles?: string[];
|
|
59
|
+
sourceRef?: string;
|
|
60
|
+
evidenceNote?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
43
63
|
export type SessionIdRequest = {
|
|
44
64
|
sessionId?: string;
|
|
45
65
|
};
|
|
@@ -73,6 +93,12 @@ export type SessionNoteStdin = {
|
|
|
73
93
|
read: () => string;
|
|
74
94
|
};
|
|
75
95
|
|
|
96
|
+
export type SessionCaptureInput = {
|
|
97
|
+
isTTY: boolean;
|
|
98
|
+
readStdin: () => string;
|
|
99
|
+
readFile: (path: string) => string;
|
|
100
|
+
};
|
|
101
|
+
|
|
76
102
|
export function searchRequestFromArgs(query: string, args: RawArgs, defaults: SearchDefaults): SearchTrapsArgs {
|
|
77
103
|
return {
|
|
78
104
|
query,
|
|
@@ -117,6 +143,27 @@ export function embedRequestFromArgs(args: RawArgs): EmbedRequest {
|
|
|
117
143
|
};
|
|
118
144
|
}
|
|
119
145
|
|
|
146
|
+
export function embeddingsUseRequestFromArgs(
|
|
147
|
+
positionals: string[],
|
|
148
|
+
args: RawArgs
|
|
149
|
+
): EmbeddingsUseRequest {
|
|
150
|
+
const provider = embeddingProviderOption(requiredPositional(positionals, 0, "provider"));
|
|
151
|
+
if (provider === "jina") {
|
|
152
|
+
return {
|
|
153
|
+
embeddings: { provider },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
embeddings: {
|
|
159
|
+
provider,
|
|
160
|
+
endpoint: stringOption(args, "endpoint") ?? DEFAULT_OLLAMA_ENDPOINT,
|
|
161
|
+
model: stringOption(args, "model") ?? DEFAULT_OLLAMA_MODEL,
|
|
162
|
+
dimensions: optionalIntOption(args, "dimensions") ?? DEFAULT_OLLAMA_DIMENSIONS,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
120
167
|
export function evidenceRequestFromArgs(args: RawArgs): RawArgs {
|
|
121
168
|
return {
|
|
122
169
|
source_type: stringOption(args, "source_type", "source-type"),
|
|
@@ -175,6 +222,35 @@ export function sessionCloseRequestFromArgs(positionals: string[], args: RawArgs
|
|
|
175
222
|
};
|
|
176
223
|
}
|
|
177
224
|
|
|
225
|
+
export function sessionCaptureRequestFromArgs(args: RawArgs, input?: SessionCaptureInput): SessionCaptureRequest {
|
|
226
|
+
const sources = [
|
|
227
|
+
args["trap-json"] !== undefined,
|
|
228
|
+
args["trap-markdown"] !== undefined,
|
|
229
|
+
args["trap-markdown-file"] !== undefined,
|
|
230
|
+
].filter(Boolean).length;
|
|
231
|
+
if (sources === 0) throw new Error("--trap-json, --trap-markdown, or --trap-markdown-file is required.");
|
|
232
|
+
if (sources > 1) throw new Error("Choose only one of --trap-json, --trap-markdown, or --trap-markdown-file.");
|
|
233
|
+
|
|
234
|
+
const markdown = markdownCaptureInput(args, input);
|
|
235
|
+
const parsedMarkdown = markdown === undefined ? null : capturedTrapMarkdownInput(markdown);
|
|
236
|
+
const trap: Record<string, unknown> | undefined = parsedMarkdown
|
|
237
|
+
? { ...parsedMarkdown.trap }
|
|
238
|
+
: jsonObjectOption(args, "trap-json");
|
|
239
|
+
if (!trap) throw new Error("--trap-json, --trap-markdown, or --trap-markdown-file is required.");
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
trap,
|
|
243
|
+
goal: stringOption(args, "goal"),
|
|
244
|
+
kind: stringOption(args, "kind"),
|
|
245
|
+
relatedFiles: uniqueStrings([
|
|
246
|
+
...(csvOrArrayOption(args, "related_files", "related-files") ?? []),
|
|
247
|
+
...(parsedMarkdown?.relatedFiles ?? []),
|
|
248
|
+
]),
|
|
249
|
+
sourceRef: stringOption(args, "source_ref", "source-ref"),
|
|
250
|
+
evidenceNote: parsedMarkdown?.evidenceNote,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
178
254
|
export function sessionIdRequestFromArgs(positionals: string[]): SessionIdRequest {
|
|
179
255
|
return {
|
|
180
256
|
sessionId: positionals[0],
|
|
@@ -249,6 +325,11 @@ function searchModeOption(args: RawArgs, key: string): SearchMode | undefined {
|
|
|
249
325
|
throw new Error(`Invalid search mode: ${value}. Expected one of: ${SEARCH_MODES.join(", ")}`);
|
|
250
326
|
}
|
|
251
327
|
|
|
328
|
+
function embeddingProviderOption(value: string): EmbeddingProviderSetting {
|
|
329
|
+
if (value === "ollama" || value === "jina") return value;
|
|
330
|
+
throw new Error(`Invalid embedding provider: ${value}. Expected ollama or jina.`);
|
|
331
|
+
}
|
|
332
|
+
|
|
252
333
|
function booleanOption(args: RawArgs, ...keys: string[]): boolean | undefined {
|
|
253
334
|
for (const key of keys) {
|
|
254
335
|
const value = args[key];
|
|
@@ -296,6 +377,36 @@ function jsonObjectOption(args: RawArgs, key: string): Record<string, unknown> |
|
|
|
296
377
|
}
|
|
297
378
|
}
|
|
298
379
|
|
|
380
|
+
function markdownCaptureInput(args: RawArgs, input: SessionCaptureInput | undefined): string | undefined {
|
|
381
|
+
const inline = stringOption(args, "trap-markdown");
|
|
382
|
+
const file = stringOption(args, "trap-markdown-file");
|
|
383
|
+
if (inline === undefined && file === undefined) return undefined;
|
|
384
|
+
|
|
385
|
+
const text = file !== undefined
|
|
386
|
+
? readMarkdownFile(file, input)
|
|
387
|
+
: inline === "-"
|
|
388
|
+
? readMarkdownStdin(input)
|
|
389
|
+
: inline ?? "";
|
|
390
|
+
if (text.trim() === "") throw new Error("Markdown trap input is required.");
|
|
391
|
+
return text;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readMarkdownStdin(input: SessionCaptureInput | undefined): string {
|
|
395
|
+
if (!input) throw new Error("--trap-markdown - requires stdin support.");
|
|
396
|
+
if (input.isTTY) throw new Error("--trap-markdown - requires piped input.");
|
|
397
|
+
return input.readStdin();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readMarkdownFile(path: string, input: SessionCaptureInput | undefined): string {
|
|
401
|
+
if (!input) throw new Error("--trap-markdown-file requires file read support.");
|
|
402
|
+
return input.readFile(path);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function uniqueStrings(values: string[]): string[] | undefined {
|
|
406
|
+
const unique = uniqueStringList(values);
|
|
407
|
+
return unique.length > 0 ? unique : undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
299
410
|
function parseDurationDays(value: string): number {
|
|
300
411
|
const match = value.trim().match(/^(\d+)\s*(d|day|days)?$/i);
|
|
301
412
|
if (!match) throw new Error(`Invalid duration: ${value}. Use a value like 90d.`);
|
package/src/lib/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { CODETRAP_DIR, SCOPES, SEARCH_MODES, type Scope, type SearchMode } from "./constants";
|
|
5
|
+
import { isRecord } from "./value-types";
|
|
5
6
|
|
|
6
7
|
export type CodetrapConfig = {
|
|
7
8
|
search?: {
|
|
@@ -10,6 +11,16 @@ export type CodetrapConfig = {
|
|
|
10
11
|
scope?: Scope;
|
|
11
12
|
rerank?: boolean;
|
|
12
13
|
};
|
|
14
|
+
embeddings?: EmbeddingSettings;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type EmbeddingProviderSetting = "ollama" | "jina";
|
|
18
|
+
|
|
19
|
+
export type EmbeddingSettings = {
|
|
20
|
+
provider?: EmbeddingProviderSetting;
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
dimensions?: number;
|
|
13
24
|
};
|
|
14
25
|
|
|
15
26
|
export type SearchDefaults = {
|
|
@@ -19,6 +30,11 @@ export type SearchDefaults = {
|
|
|
19
30
|
rerank: boolean;
|
|
20
31
|
};
|
|
21
32
|
|
|
33
|
+
export type ConfigWriteResult = {
|
|
34
|
+
path: string;
|
|
35
|
+
config: CodetrapConfig;
|
|
36
|
+
};
|
|
37
|
+
|
|
22
38
|
const BUILT_IN_SEARCH_DEFAULTS: SearchDefaults = {
|
|
23
39
|
mode: "hybrid",
|
|
24
40
|
limit: 20,
|
|
@@ -26,7 +42,7 @@ const BUILT_IN_SEARCH_DEFAULTS: SearchDefaults = {
|
|
|
26
42
|
};
|
|
27
43
|
|
|
28
44
|
export function loadCodetrapConfig(home = homedir()): CodetrapConfig {
|
|
29
|
-
const path =
|
|
45
|
+
const path = codetrapConfigPath(home);
|
|
30
46
|
if (!existsSync(path)) return {};
|
|
31
47
|
|
|
32
48
|
try {
|
|
@@ -38,6 +54,26 @@ export function loadCodetrapConfig(home = homedir()): CodetrapConfig {
|
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
56
|
|
|
57
|
+
export function codetrapConfigPath(home = homedir()): string {
|
|
58
|
+
return join(home, CODETRAP_DIR, "config.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeCodetrapConfig(config: CodetrapConfig, home = homedir()): ConfigWriteResult {
|
|
62
|
+
const path = codetrapConfigPath(home);
|
|
63
|
+
mkdirSync(join(home, CODETRAP_DIR), { recursive: true });
|
|
64
|
+
const normalized = normalizeConfig(config);
|
|
65
|
+
writeFileSync(path, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
66
|
+
return { path, config: normalized };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function setCodetrapEmbeddingSettings(
|
|
70
|
+
embeddings: EmbeddingSettings,
|
|
71
|
+
home = homedir()
|
|
72
|
+
): ConfigWriteResult {
|
|
73
|
+
const current = loadCodetrapConfig(home);
|
|
74
|
+
return writeCodetrapConfig({ ...current, embeddings }, home);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
export function searchDefaultsFromConfig(config = loadCodetrapConfig(), env = process.env): SearchDefaults {
|
|
42
78
|
return {
|
|
43
79
|
mode: config.search?.mode ?? parseSearchModeEnv(env.CODETRAP_SEARCH_MODE) ?? BUILT_IN_SEARCH_DEFAULTS.mode,
|
|
@@ -50,7 +86,11 @@ export function searchDefaultsFromConfig(config = loadCodetrapConfig(), env = pr
|
|
|
50
86
|
function normalizeConfig(value: unknown): CodetrapConfig {
|
|
51
87
|
if (!isRecord(value)) return {};
|
|
52
88
|
const search = isRecord(value.search) ? normalizeSearchConfig(value.search) : undefined;
|
|
53
|
-
|
|
89
|
+
const embeddings = isRecord(value.embeddings) ? normalizeEmbeddingSettings(value.embeddings) : undefined;
|
|
90
|
+
return {
|
|
91
|
+
...(search ? { search } : {}),
|
|
92
|
+
...(embeddings ? { embeddings } : {}),
|
|
93
|
+
};
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
function normalizeSearchConfig(value: Record<string, unknown>): CodetrapConfig["search"] {
|
|
@@ -62,6 +102,15 @@ function normalizeSearchConfig(value: Record<string, unknown>): CodetrapConfig["
|
|
|
62
102
|
return out;
|
|
63
103
|
}
|
|
64
104
|
|
|
105
|
+
function normalizeEmbeddingSettings(value: Record<string, unknown>): EmbeddingSettings {
|
|
106
|
+
const out: EmbeddingSettings = {};
|
|
107
|
+
if (typeof value.provider === "string") out.provider = parseEmbeddingProvider(value.provider);
|
|
108
|
+
if (typeof value.endpoint === "string") out.endpoint = value.endpoint;
|
|
109
|
+
if (typeof value.model === "string") out.model = value.model;
|
|
110
|
+
if (typeof value.dimensions === "number") out.dimensions = parsePositiveInt(value.dimensions, "embeddings.dimensions");
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
65
114
|
function parseSearchModeEnv(value?: string): SearchMode | undefined {
|
|
66
115
|
return value ? parseSearchMode(value) : undefined;
|
|
67
116
|
}
|
|
@@ -92,11 +141,12 @@ function parseScope(value: string): Scope {
|
|
|
92
141
|
throw new Error(`Invalid scope: ${value}. Expected one of: ${SCOPES.join(", ")}`);
|
|
93
142
|
}
|
|
94
143
|
|
|
144
|
+
function parseEmbeddingProvider(value: string): EmbeddingProviderSetting {
|
|
145
|
+
if (value === "ollama" || value === "jina") return value;
|
|
146
|
+
throw new Error("Invalid embeddings.provider: expected one of: ollama, jina");
|
|
147
|
+
}
|
|
148
|
+
|
|
95
149
|
function parsePositiveInt(value: number, label: string): number {
|
|
96
150
|
if (Number.isInteger(value) && value > 0) return value;
|
|
97
151
|
throw new Error(`Invalid ${label}: expected a positive integer.`);
|
|
98
152
|
}
|
|
99
|
-
|
|
100
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
101
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
|
-
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -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 =
|
|
52
|
+
export const SCHEMA_VERSION = 6;
|
|
53
53
|
|
|
54
54
|
// Directory and file names
|
|
55
55
|
export const CODETRAP_DIR = ".codetrap";
|
package/src/lib/doctor.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TrapStore } from "./store";
|
|
2
2
|
import type { TrapOperations } from "./trap-operations";
|
|
3
|
+
import type { EmbeddingRuntimeStatus } from "./embedding-runtime";
|
|
3
4
|
import type { EmbeddingStateSummary, EmbeddingStatsResult, HybridFallbackReason } from "./embedding-health";
|
|
4
5
|
import { createScopeContext } from "./scope-context";
|
|
5
6
|
import { hybridFallbackReason } from "./embedding-health";
|
|
7
|
+
import type { ProjectCandidateReviewSummary } from "./session-review";
|
|
6
8
|
|
|
7
9
|
export type DoctorNextAction = {
|
|
8
10
|
command: string;
|
|
@@ -28,19 +30,22 @@ export type DoctorReport = {
|
|
|
28
30
|
global_db_project_traps: ReturnType<TrapStore["diagnostics"]>["mis_scoped_traps"]["global_db_project_traps"];
|
|
29
31
|
};
|
|
30
32
|
};
|
|
33
|
+
candidate_review: ProjectCandidateReviewSummary | null;
|
|
31
34
|
next_actions: DoctorNextAction[];
|
|
32
35
|
mcp_hint: string;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
export function buildDoctorReport(
|
|
38
|
+
export async function buildDoctorReport(
|
|
36
39
|
store: TrapStore,
|
|
37
40
|
operations: TrapOperations,
|
|
38
|
-
cwd = process.cwd()
|
|
39
|
-
|
|
41
|
+
cwd = process.cwd(),
|
|
42
|
+
candidateReview: ProjectCandidateReviewSummary | null = null
|
|
43
|
+
): Promise<DoctorReport> {
|
|
40
44
|
const scope = createScopeContext(cwd);
|
|
41
45
|
const stats = operations.getStats();
|
|
42
46
|
const embeddings = operations.getEmbeddingStats();
|
|
43
|
-
const
|
|
47
|
+
const embeddingRuntime = await store.embeddingRuntimeHealth();
|
|
48
|
+
const semanticAvailable = embeddingRuntime.available;
|
|
44
49
|
const diagnostics = store.diagnostics();
|
|
45
50
|
|
|
46
51
|
return {
|
|
@@ -57,7 +62,8 @@ export function buildDoctorReport(
|
|
|
57
62
|
diagnostics: {
|
|
58
63
|
mis_scoped_traps: diagnostics.mis_scoped_traps,
|
|
59
64
|
},
|
|
60
|
-
|
|
65
|
+
candidate_review: candidateReview,
|
|
66
|
+
next_actions: buildDoctorNextActions(embeddingRuntime, embeddings, diagnostics, candidateReview),
|
|
61
67
|
mcp_hint: "Pass cwd in MCP tool calls, or restart codetrap serve after changing projects.",
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -78,6 +84,8 @@ export function formatDoctorText(report: DoctorReport): string {
|
|
|
78
84
|
` fallback_reason: ${report.hybrid_search.fallback_reason ?? "(none)"}`,
|
|
79
85
|
"Diagnostics:",
|
|
80
86
|
` global_db_project_traps: ${report.diagnostics.mis_scoped_traps.global_db_project_traps.length}`,
|
|
87
|
+
"Candidate review:",
|
|
88
|
+
formatCandidateReview(report.candidate_review),
|
|
81
89
|
"Next actions:",
|
|
82
90
|
...formatNextActions(report.next_actions),
|
|
83
91
|
`mcp_hint: ${report.mcp_hint}`,
|
|
@@ -85,16 +93,14 @@ export function formatDoctorText(report: DoctorReport): string {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
function buildDoctorNextActions(
|
|
88
|
-
|
|
96
|
+
embeddingRuntime: EmbeddingRuntimeStatus,
|
|
89
97
|
embeddings: EmbeddingStatsResult,
|
|
90
|
-
diagnostics: ReturnType<TrapStore["diagnostics"]
|
|
98
|
+
diagnostics: ReturnType<TrapStore["diagnostics"]>,
|
|
99
|
+
candidateReview: ProjectCandidateReviewSummary | null
|
|
91
100
|
): DoctorNextAction[] {
|
|
92
101
|
const actions: DoctorNextAction[] = [];
|
|
93
|
-
if (!
|
|
94
|
-
actions.push(
|
|
95
|
-
command: "export JINA_API_KEY=<your-jina-api-key>",
|
|
96
|
-
reason: "Enable semantic and hybrid search; otherwise use --mode fts.",
|
|
97
|
-
});
|
|
102
|
+
if (!embeddingRuntime.available) {
|
|
103
|
+
if (embeddingRuntime.setup_action) actions.push(embeddingRuntime.setup_action);
|
|
98
104
|
} else {
|
|
99
105
|
const projectAction = embeddingRefreshAction("project", embeddings.project);
|
|
100
106
|
const globalAction = embeddingRefreshAction("global", embeddings.global);
|
|
@@ -109,6 +115,18 @@ function buildDoctorNextActions(
|
|
|
109
115
|
reason: `${stranded} project-scoped trap(s) are stored in the global database.`,
|
|
110
116
|
});
|
|
111
117
|
}
|
|
118
|
+
if (candidateReview && candidateReview.pending_count > 0) {
|
|
119
|
+
if (candidateReview.next_session_id) {
|
|
120
|
+
actions.push({
|
|
121
|
+
command: `codetrap session candidates ${candidateReview.next_session_id} --json`,
|
|
122
|
+
reason: `${candidateReview.pending_count} pending candidate trap(s) need review.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
actions.push({
|
|
126
|
+
command: "codetrap web",
|
|
127
|
+
reason: "Open the candidate review console.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
112
130
|
return actions;
|
|
113
131
|
}
|
|
114
132
|
|
|
@@ -134,6 +152,18 @@ function formatNextActions(actions: DoctorNextAction[]): string[] {
|
|
|
134
152
|
return actions.map((action) => ` - ${action.command} # ${action.reason}`);
|
|
135
153
|
}
|
|
136
154
|
|
|
155
|
+
function formatCandidateReview(summary: ProjectCandidateReviewSummary | null): string {
|
|
156
|
+
if (!summary) return " unavailable";
|
|
157
|
+
return [
|
|
158
|
+
` pending=${summary.pending_count}`,
|
|
159
|
+
`reviewed=${summary.reviewed_count}`,
|
|
160
|
+
`sessions=${summary.pending_session_count}/${summary.session_count}`,
|
|
161
|
+
`high_quality_pending=${summary.high_quality_pending_count}`,
|
|
162
|
+
`needs_edit=${summary.needs_edit_count}`,
|
|
163
|
+
`next_session=${summary.next_session_id ?? "(none)"}`,
|
|
164
|
+
].join(", ");
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
function formatEmbeddingStats(
|
|
138
168
|
label: string,
|
|
139
169
|
stats: EmbeddingStatsResult["global"] | null
|