codetrap 0.1.6 → 0.1.8

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 (60) hide show
  1. package/README.md +159 -51
  2. package/docs/installation.md +113 -29
  3. package/package.json +4 -3
  4. package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
  5. package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
  6. package/plugins/codetrap-agent/hooks.json +2 -2
  7. package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
  8. package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
  9. package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
  10. package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
  11. package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
  12. package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +31 -5
  13. package/scripts/search-policy-sweep.ts +131 -0
  14. package/src/commands/workflow.ts +186 -68
  15. package/src/db/connection.ts +6 -6
  16. package/src/db/embedding-queries.ts +230 -48
  17. package/src/db/queries.ts +0 -25
  18. package/src/db/repository.ts +32 -21
  19. package/src/db/schema.ts +80 -0
  20. package/src/index.ts +32 -7
  21. package/src/lib/command-requests.ts +134 -1
  22. package/src/lib/config.ts +57 -7
  23. package/src/lib/constants.ts +1 -1
  24. package/src/lib/doctor.ts +96 -6
  25. package/src/lib/embed-output.ts +26 -0
  26. package/src/lib/embedder.ts +118 -3
  27. package/src/lib/embedding-health.ts +3 -1
  28. package/src/lib/embedding-job.ts +3 -0
  29. package/src/lib/embedding-management.ts +65 -0
  30. package/src/lib/embedding-runtime.ts +177 -0
  31. package/src/lib/output-json.ts +0 -2
  32. package/src/lib/scope-context.ts +17 -11
  33. package/src/lib/scope-migration.ts +2 -1
  34. package/src/lib/scope.ts +4 -6
  35. package/src/lib/search-eval.ts +136 -23
  36. package/src/lib/search-policy-sweep.ts +563 -0
  37. package/src/lib/search-policy.ts +0 -4
  38. package/src/lib/search-service.ts +14 -15
  39. package/src/lib/session-candidate-document.ts +175 -0
  40. package/src/lib/session-candidate-scope.ts +6 -0
  41. package/src/lib/session-capture.ts +298 -32
  42. package/src/lib/session-codec.ts +1 -8
  43. package/src/lib/session-operations.ts +111 -51
  44. package/src/lib/session-review.ts +327 -0
  45. package/src/lib/session-store.ts +177 -55
  46. package/src/lib/store.ts +79 -11
  47. package/src/lib/string-list.ts +3 -0
  48. package/src/lib/text-lines.ts +7 -0
  49. package/src/lib/trap-search-document.ts +2 -1
  50. package/src/lib/value-types.ts +3 -0
  51. package/src/web/client-review.ts +171 -0
  52. package/src/web/client-script.ts +1543 -0
  53. package/src/web/client-shell.ts +414 -0
  54. package/src/web/client-text.ts +447 -0
  55. package/src/web/project-registry.ts +3 -5
  56. package/src/web/server.ts +184 -111
  57. package/src/web/static.ts +581 -484
  58. package/skills/codetrap-capture-external/SKILL.md +0 -62
  59. package/skills/codetrap-check/SKILL.md +0 -69
  60. package/src/lib/embedding-index.ts +0 -53
package/src/web/server.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { DEFAULT_OLLAMA_DIMENSIONS, DEFAULT_OLLAMA_ENDPOINT, DEFAULT_OLLAMA_MODEL, EmbeddingProviderUnavailableError } from "../lib/embedder";
2
3
  import { CATEGORIES, SCOPES, SEVERITIES } from "../lib/constants";
3
- import type { CandidateTrap } from "../domain/session";
4
+ import { loadCodetrapConfig, type EmbeddingProviderSetting, type EmbeddingSettings } from "../lib/config";
4
5
  import { TrapStore } from "../lib/store";
5
6
  import { TrapOperations } from "../lib/trap-operations";
6
- import { SessionOperations, type SessionAcceptResult } from "../lib/session-operations";
7
+ import { SessionOperations } from "../lib/session-operations";
7
8
  import { SessionStore } from "../lib/session-store";
9
+ import { toListJson, toTrapDetailsJson } from "../lib/output-json";
10
+ import { isRecord } from "../lib/value-types";
11
+ import {
12
+ reviewedSessionCandidates,
13
+ sessionConflictPayload,
14
+ } from "../lib/session-review";
8
15
  import { WEB_INDEX_HTML } from "./static";
9
16
  import {
10
17
  addWebProject,
@@ -29,33 +36,6 @@ type WebContext = {
29
36
  currentProjectRoot: string | null;
30
37
  };
31
38
 
32
- type WebCandidateReview =
33
- | { status: "pending"; label: string }
34
- | {
35
- status: "accepted";
36
- label: string;
37
- trap_id: number;
38
- scope: string;
39
- trap_present: true;
40
- trap_status: string;
41
- trap_title: string;
42
- }
43
- | {
44
- status: "accepted_missing";
45
- label: string;
46
- trap_id?: number;
47
- scope?: string;
48
- trap_present: false;
49
- }
50
- | {
51
- status: "rejected";
52
- label: string;
53
- rejected_at?: string;
54
- rejection_reason?: string;
55
- };
56
-
57
- type WebCandidate = CandidateTrap & { review: WebCandidateReview };
58
-
59
39
  export async function startWebServerFromArgs(args: string[], cwd = process.cwd()): Promise<void> {
60
40
  const options = webServerOptionsFromArgs(args, cwd);
61
41
  const token = options.token ?? randomBytes(18).toString("base64url");
@@ -89,6 +69,9 @@ export function createWebHandler(context: WebContext): (request: Request) => Pro
89
69
  if (url.pathname === "/" || url.pathname === "/index.html") {
90
70
  return htmlResponse(WEB_INDEX_HTML);
91
71
  }
72
+ if (url.pathname === "/favicon.ico") {
73
+ return new Response(null, { status: 204 });
74
+ }
92
75
  return jsonResponse({ error: "Not found" }, 404);
93
76
  } catch (error) {
94
77
  const status = error instanceof WebHttpError || error instanceof WebPayloadError ? error.status : 500;
@@ -141,20 +124,42 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
141
124
 
142
125
  if (request.method === "GET" && url.pathname === "/api/sessions") {
143
126
  const projectRoot = projectRootFromQuery(url, context);
144
- const sessions = sessionOperations(projectRoot).sessions.listSessions({ status: "all", limit: 100 });
145
- return jsonResponse({ project_root: projectRoot, sessions });
127
+ const ops = sessionOperations(projectRoot, context.home);
128
+ const sessions = ops.sessions.listSessions({ status: "all", limit: 100 });
129
+ return jsonResponse({
130
+ project_root: projectRoot,
131
+ candidate_review: ops.sessions.candidateReviewSummary(),
132
+ sessions,
133
+ });
146
134
  }
147
135
 
148
136
  if (request.method === "GET" && url.pathname === "/api/candidates") {
149
137
  const projectRoot = projectRootFromQuery(url, context);
150
138
  const sessionId = requiredQuery(url, "session");
151
- const ops = sessionOperations(projectRoot);
139
+ const ops = sessionOperations(projectRoot, context.home);
152
140
  const session = ops.sessions.showSession(sessionId).session;
153
141
  const document = ops.sessions.candidateDocument(sessionId);
154
142
  return jsonResponse({
155
143
  project_root: projectRoot,
156
144
  session,
157
- candidates: webCandidates(document.candidates, ops.traps),
145
+ candidates: reviewedSessionCandidates(document.candidates, ops.traps),
146
+ });
147
+ }
148
+
149
+ if (request.method === "GET" && url.pathname === "/api/traps") {
150
+ const projectRoot = projectRootFromQuery(url, context);
151
+ const groups = trapOperations(projectRoot, context.home).listTraps({
152
+ category: optionalQuery(url, "category"),
153
+ scope: optionalQuery(url, "scope"),
154
+ status: optionalQuery(url, "status"),
155
+ module: optionalQuery(url, "module"),
156
+ owner: optionalQuery(url, "owner"),
157
+ limit: optionalNumberQuery(url, "limit"),
158
+ offset: optionalNumberQuery(url, "offset"),
159
+ });
160
+ return jsonResponse({
161
+ project_root: projectRoot,
162
+ traps: toListJson(groups),
158
163
  });
159
164
  }
160
165
 
@@ -162,15 +167,65 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
162
167
  const projectRoot = projectRootFromQuery(url, context);
163
168
  const id = numberQuery(url, "id");
164
169
  const scope = url.searchParams.get("scope") ?? undefined;
165
- const details = trapOperations(projectRoot).getTrapDetails(id, scope);
170
+ const details = trapOperations(projectRoot, context.home).getTrapDetails(id, scope);
166
171
  if (!details) throw new WebHttpError(404, `Trap #${id} not found.`);
167
- return jsonResponse(details);
172
+ return jsonResponse(toTrapDetailsJson(details));
173
+ }
174
+
175
+ if (request.method === "GET" && url.pathname === "/api/embeddings") {
176
+ const projectRoot = projectRootFromQuery(url, context);
177
+ const status = await trapStore(projectRoot, context.home).embeddingStatus();
178
+ return jsonResponse({
179
+ project_root: projectRoot,
180
+ settings: loadCodetrapConfig(context.home).embeddings ?? null,
181
+ ...status,
182
+ });
183
+ }
184
+
185
+ if (request.method === "POST" && url.pathname === "/api/embeddings/use") {
186
+ const body = await readJsonBody(request);
187
+ const projectRoot = projectRootFromBody(body, context);
188
+ const embeddings = embeddingSettingsFromBody(body);
189
+ const written = trapStore(projectRoot, context.home).configureEmbeddings(embeddings);
190
+ const refreshed = await trapStore(projectRoot, context.home).embeddingStatus();
191
+ return jsonResponse({
192
+ success: true,
193
+ project_root: projectRoot,
194
+ path: written.path,
195
+ config: written.config,
196
+ embeddings: written.config.embeddings ?? embeddings,
197
+ settings: written.config.embeddings ?? embeddings,
198
+ next_actions: embeddingReindexActions(projectRoot),
199
+ status: refreshed,
200
+ });
201
+ }
202
+
203
+ if (request.method === "POST" && url.pathname === "/api/embeddings/reindex") {
204
+ const body = await readJsonBody(request);
205
+ const projectRoot = projectRootFromBody(body, context);
206
+ const scope = scopeBodyField(body, "scope");
207
+ const store = trapStore(projectRoot, context.home);
208
+ try {
209
+ const result = await store.ensureEmbeddings({ scope });
210
+ return jsonResponse({
211
+ success: true,
212
+ project_root: projectRoot,
213
+ scope,
214
+ result,
215
+ status: await store.embeddingStatus(),
216
+ });
217
+ } catch (error) {
218
+ if (error instanceof EmbeddingProviderUnavailableError) {
219
+ throw new WebHttpError(400, error.message);
220
+ }
221
+ throw error;
222
+ }
168
223
  }
169
224
 
170
225
  if (request.method === "POST" && url.pathname === "/api/candidate/save") {
171
226
  const body = await readJsonBody(request);
172
227
  const projectRoot = projectRootFromBody(body, context);
173
- const result = sessionOperations(projectRoot).sessions.saveCandidate({
228
+ const result = sessionOperations(projectRoot, context.home).sessions.saveCandidate({
174
229
  candidateId: stringBodyField(body, "candidateId"),
175
230
  sessionId: optionalStringBodyField(body, "sessionId"),
176
231
  edit: recordBodyField(body, "trap"),
@@ -181,14 +236,15 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
181
236
  if (request.method === "POST" && url.pathname === "/api/candidate/accept") {
182
237
  const body = await readJsonBody(request);
183
238
  const projectRoot = projectRootFromBody(body, context);
184
- const result = await sessionOperations(projectRoot).sessions.acceptCandidate({
239
+ const result = await sessionOperations(projectRoot, context.home).sessions.acceptCandidate({
185
240
  candidateId: stringBodyField(body, "candidateId"),
186
241
  sessionId: optionalStringBodyField(body, "sessionId"),
242
+ edit: optionalRecordBodyField(body, "trap"),
187
243
  acceptAnyway: booleanBodyField(body, "acceptAnyway"),
188
244
  supersedesId: optionalNumberBodyField(body, "supersedesId"),
189
245
  });
190
246
  if (!result.success) {
191
- throw new WebPayloadError(409, conflictPayload(result));
247
+ throw new WebPayloadError(409, sessionConflictPayload(result));
192
248
  }
193
249
  return jsonResponse({
194
250
  success: true,
@@ -204,7 +260,7 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
204
260
  if (request.method === "POST" && url.pathname === "/api/candidate/reject") {
205
261
  const body = await readJsonBody(request);
206
262
  const projectRoot = projectRootFromBody(body, context);
207
- const result = sessionOperations(projectRoot).sessions.rejectCandidate({
263
+ const result = sessionOperations(projectRoot, context.home).sessions.rejectCandidate({
208
264
  candidateId: stringBodyField(body, "candidateId"),
209
265
  sessionId: optionalStringBodyField(body, "sessionId"),
210
266
  reason: optionalStringBodyField(body, "reason"),
@@ -212,77 +268,48 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
212
268
  return jsonResponse({ success: true, session: result.session, candidate: result.candidate });
213
269
  }
214
270
 
271
+ if (request.method === "POST" && url.pathname === "/api/session/delete") {
272
+ const body = await readJsonBody(request);
273
+ const projectRoot = projectRootFromBody(body, context);
274
+ const result = sessionOperations(projectRoot, context.home).sessions.deleteSession(
275
+ stringBodyField(body, "sessionId")
276
+ );
277
+ return jsonResponse({ success: true, ...result });
278
+ }
279
+
280
+ if (request.method === "POST" && url.pathname === "/api/session/cleanup") {
281
+ const body = await readJsonBody(request);
282
+ const projectRoot = projectRootFromBody(body, context);
283
+ const ops = sessionOperations(projectRoot, context.home);
284
+ const result = ops.sessions.cleanupDeletedTrapCandidates(
285
+ optionalStringBodyField(body, "sessionId")
286
+ );
287
+ return jsonResponse({
288
+ success: true,
289
+ session: result.session,
290
+ removed_count: result.removed_count,
291
+ removed_candidate_ids: result.removed_candidate_ids,
292
+ candidates: reviewedSessionCandidates(result.candidates, ops.traps),
293
+ });
294
+ }
295
+
215
296
  throw new WebHttpError(404, "Not found");
216
297
  }
217
298
 
218
- function sessionOperations(projectRoot: string): { traps: TrapOperations; sessions: SessionOperations } {
219
- const traps = trapOperations(projectRoot);
299
+ function sessionOperations(projectRoot: string, home?: string): { traps: TrapOperations; sessions: SessionOperations } {
300
+ const traps = trapOperations(projectRoot, home);
220
301
  return {
221
302
  traps,
222
303
  sessions: new SessionOperations(new SessionStore(projectRoot), traps),
223
304
  };
224
305
  }
225
306
 
226
- function trapOperations(projectRoot: string): TrapOperations {
227
- return new TrapOperations(new TrapStore(projectRoot));
228
- }
229
-
230
- function webCandidates(candidates: CandidateTrap[], traps: TrapOperations): WebCandidate[] {
231
- return candidates.map((candidate) => ({
232
- ...candidate,
233
- review: candidateReview(candidate, traps),
234
- }));
235
- }
236
-
237
- function candidateReview(candidate: CandidateTrap, traps: TrapOperations): WebCandidateReview {
238
- if (candidate.status === "proposed") {
239
- return { status: "pending", label: "pending review" };
240
- }
241
-
242
- if (candidate.status === "rejected") {
243
- return {
244
- status: "rejected",
245
- label: "rejected",
246
- rejected_at: candidate.rejected_at,
247
- rejection_reason: candidate.rejection_reason,
248
- };
249
- }
250
-
251
- const trapId = candidate.accepted_trap_id;
252
- const scope = candidate.accepted_scope ?? acceptedScopeFallback(candidate);
253
- if (trapId === undefined) {
254
- return {
255
- status: "accepted_missing",
256
- label: "accepted -> trap link missing",
257
- scope,
258
- trap_present: false,
259
- };
260
- }
261
-
262
- const details = traps.getTrapDetails(trapId, scope);
263
- if (!details) {
264
- return {
265
- status: "accepted_missing",
266
- label: `accepted -> trap #${trapId} deleted`,
267
- trap_id: trapId,
268
- scope,
269
- trap_present: false,
270
- };
271
- }
272
-
273
- return {
274
- status: "accepted",
275
- label: `accepted -> trap #${trapId}`,
276
- trap_id: trapId,
277
- scope: details.scope,
278
- trap_present: true,
279
- trap_status: details.trap.status,
280
- trap_title: details.trap.title,
281
- };
307
+ function trapOperations(projectRoot: string, home?: string): TrapOperations {
308
+ return new TrapOperations(trapStore(projectRoot, home));
282
309
  }
283
310
 
284
- function acceptedScopeFallback(candidate: CandidateTrap): string {
285
- return candidate.trap.scope === "global" ? "global" : "project";
311
+ function trapStore(projectRoot: string, home?: string): TrapStore {
312
+ return new TrapStore(projectRoot, undefined, home);
286
313
  }
287
314
 
288
315
  function registerInitialProject(options: WebServerOptions): string | null {
@@ -353,6 +380,19 @@ function numberQuery(url: URL, key: string): number {
353
380
  return value;
354
381
  }
355
382
 
383
+ function optionalQuery(url: URL, key: string): string | undefined {
384
+ const value = url.searchParams.get(key)?.trim();
385
+ return value || undefined;
386
+ }
387
+
388
+ function optionalNumberQuery(url: URL, key: string): number | undefined {
389
+ const value = optionalQuery(url, key);
390
+ if (!value) return undefined;
391
+ const parsed = Number.parseInt(value, 10);
392
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new WebHttpError(400, `${key} must be a positive integer.`);
393
+ return parsed;
394
+ }
395
+
356
396
  function stringBodyField(body: Record<string, unknown>, key: string): string {
357
397
  const value = optionalStringBodyField(body, key);
358
398
  if (!value) throw new WebHttpError(400, `${key} is required.`);
@@ -382,6 +422,46 @@ function optionalNumberBodyField(body: Record<string, unknown>, key: string): nu
382
422
  return value;
383
423
  }
384
424
 
425
+ function scopeBodyField(body: Record<string, unknown>, key: string): "project" | "global" {
426
+ const value = stringBodyField(body, key);
427
+ if (value === "project" || value === "global") return value;
428
+ throw new WebHttpError(400, `${key} must be project or global.`);
429
+ }
430
+
431
+ function embeddingSettingsFromBody(body: Record<string, unknown>): EmbeddingSettings {
432
+ const provider = embeddingProviderBodyField(body, "provider");
433
+ if (provider === "jina") {
434
+ return { provider };
435
+ }
436
+ return {
437
+ provider,
438
+ endpoint: optionalStringBodyField(body, "endpoint") ?? DEFAULT_OLLAMA_ENDPOINT,
439
+ model: optionalStringBodyField(body, "model") ?? DEFAULT_OLLAMA_MODEL,
440
+ dimensions: optionalNumberBodyField(body, "dimensions") ?? DEFAULT_OLLAMA_DIMENSIONS,
441
+ };
442
+ }
443
+
444
+ function embeddingProviderBodyField(body: Record<string, unknown>, key: string): EmbeddingProviderSetting {
445
+ const value = stringBodyField(body, key);
446
+ if (value === "ollama" || value === "jina") return value;
447
+ throw new WebHttpError(400, `${key} must be ollama or jina.`);
448
+ }
449
+
450
+ function embeddingReindexActions(_projectRoot: string): { scope: "project" | "global"; command: string; reason: string }[] {
451
+ return [
452
+ {
453
+ scope: "project",
454
+ command: "codetrap embeddings reindex --scope project",
455
+ reason: "Generate project embeddings for the selected profile.",
456
+ },
457
+ {
458
+ scope: "global",
459
+ command: "codetrap embeddings reindex --scope global",
460
+ reason: "Generate global embeddings for the selected profile.",
461
+ },
462
+ ];
463
+ }
464
+
385
465
  function booleanBodyField(body: Record<string, unknown>, key: string): boolean {
386
466
  const value = body[key];
387
467
  return typeof value === "boolean" ? value : false;
@@ -393,14 +473,11 @@ function recordBodyField(body: Record<string, unknown>, key: string): Record<str
393
473
  return value;
394
474
  }
395
475
 
396
- function conflictPayload(result: Exclude<SessionAcceptResult, { success: true }>): Record<string, unknown> {
397
- return {
398
- success: false,
399
- error: "Possible active trap conflict found.",
400
- session_id: result.session_id,
401
- candidate_id: result.candidate_id,
402
- possible_conflicts: result.possible_conflicts,
403
- };
476
+ function optionalRecordBodyField(body: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
477
+ const value = body[key];
478
+ if (value === undefined || value === null) return undefined;
479
+ if (!isRecord(value)) throw new WebHttpError(400, `${key} must be an object.`);
480
+ return value;
404
481
  }
405
482
 
406
483
  function parsePort(value: string): number {
@@ -435,7 +512,3 @@ class WebPayloadError extends Error {
435
512
  super(String(payload.error ?? "Request failed"));
436
513
  }
437
514
  }
438
-
439
- function isRecord(value: unknown): value is Record<string, unknown> {
440
- return typeof value === "object" && value !== null && !Array.isArray(value);
441
- }