codetrap 0.1.7 → 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 (58) hide show
  1. package/README.md +151 -52
  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 +144 -68
  15. package/src/db/embedding-queries.ts +230 -48
  16. package/src/db/queries.ts +0 -25
  17. package/src/db/repository.ts +32 -21
  18. package/src/db/schema.ts +80 -0
  19. package/src/index.ts +28 -3
  20. package/src/lib/command-requests.ts +112 -1
  21. package/src/lib/config.ts +57 -7
  22. package/src/lib/constants.ts +1 -1
  23. package/src/lib/doctor.ts +42 -12
  24. package/src/lib/embedder.ts +118 -3
  25. package/src/lib/embedding-health.ts +3 -1
  26. package/src/lib/embedding-job.ts +3 -0
  27. package/src/lib/embedding-management.ts +65 -0
  28. package/src/lib/embedding-runtime.ts +177 -0
  29. package/src/lib/output-json.ts +0 -2
  30. package/src/lib/scope-context.ts +12 -6
  31. package/src/lib/scope-migration.ts +2 -1
  32. package/src/lib/scope.ts +0 -2
  33. package/src/lib/search-eval.ts +38 -18
  34. package/src/lib/search-policy-sweep.ts +563 -0
  35. package/src/lib/search-policy.ts +0 -4
  36. package/src/lib/search-service.ts +14 -15
  37. package/src/lib/session-candidate-document.ts +175 -0
  38. package/src/lib/session-candidate-scope.ts +6 -0
  39. package/src/lib/session-capture.ts +298 -32
  40. package/src/lib/session-codec.ts +1 -8
  41. package/src/lib/session-operations.ts +83 -60
  42. package/src/lib/session-review.ts +327 -0
  43. package/src/lib/session-store.ts +87 -73
  44. package/src/lib/store.ts +74 -10
  45. package/src/lib/string-list.ts +3 -0
  46. package/src/lib/text-lines.ts +7 -0
  47. package/src/lib/trap-search-document.ts +2 -1
  48. package/src/lib/value-types.ts +3 -0
  49. package/src/web/client-review.ts +171 -0
  50. package/src/web/client-script.ts +426 -51
  51. package/src/web/client-shell.ts +414 -0
  52. package/src/web/client-text.ts +112 -0
  53. package/src/web/project-registry.ts +3 -5
  54. package/src/web/server.ts +117 -103
  55. package/src/web/static.ts +364 -19
  56. package/skills/codetrap-capture-external/SKILL.md +0 -62
  57. package/skills/codetrap-check/SKILL.md +0 -69
  58. package/src/lib/embedding-index.ts +0 -53
@@ -2,7 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join } from "node:path";
4
4
  import { CODETRAP_DIR } from "../lib/constants";
5
- import { findProjectRoot, resolveScopePath } from "../lib/scope";
5
+ import { findProjectRoot } from "../lib/scope";
6
+ import { resolveScopePath } from "../lib/scope-path";
7
+ import { isRecord } from "../lib/value-types";
6
8
 
7
9
  export const WEB_PROJECTS_FILE = "web-projects.json";
8
10
  export const WEB_PROJECTS_VERSION = 1;
@@ -100,7 +102,3 @@ function uniqueProjects(projects: WebProject[]): WebProject[] {
100
102
  }
101
103
  return out;
102
104
  }
103
-
104
- function isRecord(value: unknown): value is Record<string, unknown> {
105
- return typeof value === "object" && value !== null && !Array.isArray(value);
106
- }
package/src/web/server.ts CHANGED
@@ -1,11 +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";
8
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";
9
15
  import { WEB_INDEX_HTML } from "./static";
10
16
  import {
11
17
  addWebProject,
@@ -30,33 +36,6 @@ type WebContext = {
30
36
  currentProjectRoot: string | null;
31
37
  };
32
38
 
33
- type WebCandidateReview =
34
- | { status: "pending"; label: string }
35
- | {
36
- status: "accepted";
37
- label: string;
38
- trap_id: number;
39
- scope: string;
40
- trap_present: true;
41
- trap_status: string;
42
- trap_title: string;
43
- }
44
- | {
45
- status: "accepted_missing";
46
- label: string;
47
- trap_id?: number;
48
- scope?: string;
49
- trap_present: false;
50
- }
51
- | {
52
- status: "rejected";
53
- label: string;
54
- rejected_at?: string;
55
- rejection_reason?: string;
56
- };
57
-
58
- type WebCandidate = CandidateTrap & { review: WebCandidateReview };
59
-
60
39
  export async function startWebServerFromArgs(args: string[], cwd = process.cwd()): Promise<void> {
61
40
  const options = webServerOptionsFromArgs(args, cwd);
62
41
  const token = options.token ?? randomBytes(18).toString("base64url");
@@ -145,8 +124,13 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
145
124
 
146
125
  if (request.method === "GET" && url.pathname === "/api/sessions") {
147
126
  const projectRoot = projectRootFromQuery(url, context);
148
- const sessions = sessionOperations(projectRoot, context.home).sessions.listSessions({ status: "all", limit: 100 });
149
- 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
+ });
150
134
  }
151
135
 
152
136
  if (request.method === "GET" && url.pathname === "/api/candidates") {
@@ -158,7 +142,7 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
158
142
  return jsonResponse({
159
143
  project_root: projectRoot,
160
144
  session,
161
- candidates: webCandidates(document.candidates, ops.traps),
145
+ candidates: reviewedSessionCandidates(document.candidates, ops.traps),
162
146
  });
163
147
  }
164
148
 
@@ -188,6 +172,56 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
188
172
  return jsonResponse(toTrapDetailsJson(details));
189
173
  }
190
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
+ }
223
+ }
224
+
191
225
  if (request.method === "POST" && url.pathname === "/api/candidate/save") {
192
226
  const body = await readJsonBody(request);
193
227
  const projectRoot = projectRootFromBody(body, context);
@@ -205,11 +239,12 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
205
239
  const result = await sessionOperations(projectRoot, context.home).sessions.acceptCandidate({
206
240
  candidateId: stringBodyField(body, "candidateId"),
207
241
  sessionId: optionalStringBodyField(body, "sessionId"),
242
+ edit: optionalRecordBodyField(body, "trap"),
208
243
  acceptAnyway: booleanBodyField(body, "acceptAnyway"),
209
244
  supersedesId: optionalNumberBodyField(body, "supersedesId"),
210
245
  });
211
246
  if (!result.success) {
212
- throw new WebPayloadError(409, conflictPayload(result));
247
+ throw new WebPayloadError(409, sessionConflictPayload(result));
213
248
  }
214
249
  return jsonResponse({
215
250
  success: true,
@@ -254,7 +289,7 @@ async function routeApi(request: Request, url: URL, context: WebContext): Promis
254
289
  session: result.session,
255
290
  removed_count: result.removed_count,
256
291
  removed_candidate_ids: result.removed_candidate_ids,
257
- candidates: webCandidates(result.candidates, ops.traps),
292
+ candidates: reviewedSessionCandidates(result.candidates, ops.traps),
258
293
  });
259
294
  }
260
295
 
@@ -270,65 +305,11 @@ function sessionOperations(projectRoot: string, home?: string): { traps: TrapOpe
270
305
  }
271
306
 
272
307
  function trapOperations(projectRoot: string, home?: string): TrapOperations {
273
- return new TrapOperations(new TrapStore(projectRoot, undefined, home));
308
+ return new TrapOperations(trapStore(projectRoot, home));
274
309
  }
275
310
 
276
- function webCandidates(candidates: CandidateTrap[], traps: TrapOperations): WebCandidate[] {
277
- return candidates.map((candidate) => ({
278
- ...candidate,
279
- review: candidateReview(candidate, traps),
280
- }));
281
- }
282
-
283
- function candidateReview(candidate: CandidateTrap, traps: TrapOperations): WebCandidateReview {
284
- if (candidate.status === "proposed") {
285
- return { status: "pending", label: "pending review" };
286
- }
287
-
288
- if (candidate.status === "rejected") {
289
- return {
290
- status: "rejected",
291
- label: "rejected",
292
- rejected_at: candidate.rejected_at,
293
- rejection_reason: candidate.rejection_reason,
294
- };
295
- }
296
-
297
- const trapId = candidate.accepted_trap_id;
298
- const scope = candidate.accepted_scope ?? acceptedScopeFallback(candidate);
299
- if (trapId === undefined) {
300
- return {
301
- status: "accepted_missing",
302
- label: "accepted -> trap link missing",
303
- scope,
304
- trap_present: false,
305
- };
306
- }
307
-
308
- const details = traps.getTrapDetails(trapId, scope);
309
- if (!details) {
310
- return {
311
- status: "accepted_missing",
312
- label: `accepted -> trap #${trapId} deleted`,
313
- trap_id: trapId,
314
- scope,
315
- trap_present: false,
316
- };
317
- }
318
-
319
- return {
320
- status: "accepted",
321
- label: `accepted -> trap #${trapId}`,
322
- trap_id: trapId,
323
- scope: details.scope,
324
- trap_present: true,
325
- trap_status: details.trap.status,
326
- trap_title: details.trap.title,
327
- };
328
- }
329
-
330
- function acceptedScopeFallback(candidate: CandidateTrap): string {
331
- return candidate.trap.scope === "global" ? "global" : "project";
311
+ function trapStore(projectRoot: string, home?: string): TrapStore {
312
+ return new TrapStore(projectRoot, undefined, home);
332
313
  }
333
314
 
334
315
  function registerInitialProject(options: WebServerOptions): string | null {
@@ -441,6 +422,46 @@ function optionalNumberBodyField(body: Record<string, unknown>, key: string): nu
441
422
  return value;
442
423
  }
443
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
+
444
465
  function booleanBodyField(body: Record<string, unknown>, key: string): boolean {
445
466
  const value = body[key];
446
467
  return typeof value === "boolean" ? value : false;
@@ -452,14 +473,11 @@ function recordBodyField(body: Record<string, unknown>, key: string): Record<str
452
473
  return value;
453
474
  }
454
475
 
455
- function conflictPayload(result: Exclude<SessionAcceptResult, { success: true }>): Record<string, unknown> {
456
- return {
457
- success: false,
458
- error: "Possible active trap conflict found.",
459
- session_id: result.session_id,
460
- candidate_id: result.candidate_id,
461
- possible_conflicts: result.possible_conflicts,
462
- };
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;
463
481
  }
464
482
 
465
483
  function parsePort(value: string): number {
@@ -494,7 +512,3 @@ class WebPayloadError extends Error {
494
512
  super(String(payload.error ?? "Request failed"));
495
513
  }
496
514
  }
497
-
498
- function isRecord(value: unknown): value is Record<string, unknown> {
499
- return typeof value === "object" && value !== null && !Array.isArray(value);
500
- }