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
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
- ): DoctorReport {
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 semanticAvailable = store.hasEmbeddingProvider();
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
- next_actions: buildDoctorNextActions(semanticAvailable, embeddings, diagnostics),
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
- semanticAvailable: boolean,
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 (!semanticAvailable) {
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
@@ -19,6 +19,7 @@ export interface EmbeddingConfig {
19
19
 
20
20
  export interface StoredEmbedding {
21
21
  trap_id: number;
22
+ profile_id: string;
22
23
  provider: string;
23
24
  model: string;
24
25
  dimensions: number;
@@ -91,9 +92,119 @@ export class JinaEmbedder implements EmbeddingProvider {
91
92
  }
92
93
  }
93
94
 
94
- export function createDefaultEmbeddingProvider(): EmbeddingProvider | undefined {
95
- const apiKey = process.env.JINA_API_KEY;
96
- return apiKey ? new JinaEmbedder(apiKey) : undefined;
95
+ export type OllamaEmbedderOptions = {
96
+ endpoint?: string;
97
+ model?: string;
98
+ dimensions?: number;
99
+ fetch?: FetchLike;
100
+ };
101
+
102
+ export type OllamaProviderHealth = {
103
+ ok: boolean;
104
+ message?: string;
105
+ command?: string;
106
+ };
107
+
108
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
109
+
110
+ export const DEFAULT_OLLAMA_ENDPOINT = "http://127.0.0.1:11434";
111
+ export const DEFAULT_OLLAMA_MODEL = "qwen3-embedding:0.6b";
112
+ export const DEFAULT_OLLAMA_DIMENSIONS = 1024;
113
+
114
+ export class OllamaEmbedder implements EmbeddingProvider {
115
+ readonly provider = "ollama";
116
+ readonly model: string;
117
+ readonly dimensions: number;
118
+ private readonly endpoint: string;
119
+ private readonly fetchImpl: FetchLike;
120
+
121
+ constructor(options: OllamaEmbedderOptions = {}) {
122
+ this.endpoint = normalizeOllamaEndpoint(options.endpoint ?? DEFAULT_OLLAMA_ENDPOINT);
123
+ this.model = options.model ?? DEFAULT_OLLAMA_MODEL;
124
+ this.dimensions = options.dimensions ?? DEFAULT_OLLAMA_DIMENSIONS;
125
+ this.fetchImpl = options.fetch ?? fetch;
126
+ }
127
+
128
+ async embed(texts: string[], _task: EmbeddingTask): Promise<Float32Array[]> {
129
+ if (texts.length === 0) return [];
130
+
131
+ const response = await this.fetchImpl(`${this.endpoint}/api/embed`, {
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ },
136
+ body: JSON.stringify({
137
+ model: this.model,
138
+ input: texts,
139
+ dimensions: this.dimensions,
140
+ truncate: true,
141
+ }),
142
+ });
143
+
144
+ if (!response.ok) {
145
+ const body = await response.text();
146
+ throw new Error(`Ollama embeddings request failed (${response.status}): ${body}`);
147
+ }
148
+
149
+ const payload = (await response.json()) as {
150
+ embeddings?: number[][];
151
+ };
152
+ const rows = payload.embeddings ?? [];
153
+ if (rows.length !== texts.length) {
154
+ throw new Error(`Ollama embeddings returned ${rows.length} vectors for ${texts.length} inputs.`);
155
+ }
156
+
157
+ return rows.map((embedding) => {
158
+ if (!Array.isArray(embedding)) {
159
+ throw new Error("Ollama embeddings response is missing an embedding vector.");
160
+ }
161
+ if (embedding.length !== this.dimensions) {
162
+ throw new Error(
163
+ `Ollama embeddings returned ${embedding.length} dimensions, expected ${this.dimensions}.`
164
+ );
165
+ }
166
+ return Float32Array.from(embedding);
167
+ });
168
+ }
169
+
170
+ async health(): Promise<OllamaProviderHealth> {
171
+ try {
172
+ const response = await this.fetchImpl(`${this.endpoint}/api/tags`);
173
+ if (!response.ok) {
174
+ return {
175
+ ok: false,
176
+ message: `Ollama is not reachable at ${this.endpoint} (${response.status}).`,
177
+ command: "ollama list",
178
+ };
179
+ }
180
+
181
+ const payload = (await response.json()) as {
182
+ models?: { name?: string; model?: string }[];
183
+ };
184
+ const models = payload.models ?? [];
185
+ const installed = models.some((model) => model.name === this.model || model.model === this.model);
186
+ if (!installed) {
187
+ return {
188
+ ok: false,
189
+ message: `Ollama model ${this.model} is not installed.`,
190
+ command: `ollama pull ${this.model}`,
191
+ };
192
+ }
193
+
194
+ return { ok: true };
195
+ } catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ return {
198
+ ok: false,
199
+ message: `Ollama is not reachable at ${this.endpoint}: ${message}`,
200
+ command: "ollama list",
201
+ };
202
+ }
203
+ }
204
+ }
205
+
206
+ function normalizeOllamaEndpoint(endpoint: string): string {
207
+ return endpoint.replace(/\/+$/, "");
97
208
  }
98
209
 
99
210
  export function embeddingConfig(provider: EmbeddingProvider): EmbeddingConfig {
@@ -105,6 +216,10 @@ export function embeddingConfig(provider: EmbeddingProvider): EmbeddingConfig {
105
216
  };
106
217
  }
107
218
 
219
+ export function embeddingProfileId(config: EmbeddingConfig): string {
220
+ return `${config.provider}:${config.model}:${config.dimensions}:p${config.passageVersion}`;
221
+ }
222
+
108
223
  export function encodeEmbedding(embedding: Float32Array): Buffer {
109
224
  return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
110
225
  }
@@ -1,4 +1,4 @@
1
- import type { EmbeddingConfig } from "./embedder";
1
+ import { embeddingProfileId, type EmbeddingConfig } from "./embedder";
2
2
 
3
3
  export type EmbeddingStateCounts = {
4
4
  total: number;
@@ -13,6 +13,7 @@ export type EmbeddingStateSummary = EmbeddingStateCounts & {
13
13
  model: string | null;
14
14
  dimensions: number | null;
15
15
  passage_version: number | null;
16
+ profile_id: string | null;
16
17
  };
17
18
 
18
19
  export type EmbeddingStatsResult = {
@@ -33,6 +34,7 @@ export function summarizeEmbeddingState(
33
34
  model: config?.model ?? null,
34
35
  dimensions: config?.dimensions ?? null,
35
36
  passage_version: config?.passageVersion ?? null,
37
+ profile_id: config ? embeddingProfileId(config) : null,
36
38
  };
37
39
  }
38
40
 
@@ -1,6 +1,7 @@
1
1
  import type { Trap } from "../domain/trap";
2
2
  import {
3
3
  embeddingConfig,
4
+ embeddingProfileId,
4
5
  type EmbeddingConfig,
5
6
  type EmbeddingProvider,
6
7
  type StoredEmbedding,
@@ -35,6 +36,7 @@ export async function runEmbeddingJob(
35
36
  opts: EmbeddingJobOptions = {}
36
37
  ): Promise<EmbeddingJobResult> {
37
38
  const config = embeddingConfig(provider);
39
+ const profileId = embeddingProfileId(config);
38
40
  const total = adapter.countEmbeddable({ scope: opts.scope, category: opts.category });
39
41
  const traps = adapter.trapsNeedingEmbeddings(config, opts);
40
42
  if (traps.length === 0) return { generated: 0, skipped: total, batches: 0 };
@@ -53,6 +55,7 @@ export async function runEmbeddingJob(
53
55
  for (let i = 0; i < batch.length; i++) {
54
56
  adapter.saveEmbedding({
55
57
  trap_id: batch[i].id,
58
+ profile_id: profileId,
56
59
  provider: config.provider,
57
60
  model: config.model,
58
61
  dimensions: config.dimensions,
@@ -0,0 +1,65 @@
1
+ import type { ConfigWriteResult, EmbeddingSettings } from "./config";
2
+ import type { EmbeddingScopeStatus, TrapEmbeddingProfiles, TrapEmbeddingStatus } from "./store";
3
+ import type { EmbeddingProfileSummary } from "../db/embedding-queries";
4
+
5
+ export type EmbeddingsUseResult = ConfigWriteResult & {
6
+ embeddings: EmbeddingSettings;
7
+ next_action: {
8
+ command: string;
9
+ reason: string;
10
+ };
11
+ };
12
+
13
+ export function formatEmbeddingStatusText(status: TrapEmbeddingStatus): string {
14
+ const runtime = status.runtime.provider
15
+ ? `${status.runtime.provider}/${status.runtime.model} (${status.runtime.dimensions}d)`
16
+ : "unconfigured";
17
+ const lines = [
18
+ `Runtime: ${runtime}`,
19
+ `Available: ${status.runtime.available ? "yes" : "no"}`,
20
+ `Active profile: ${status.runtime.profile_id ?? "(none)"}`,
21
+ ];
22
+
23
+ if (status.runtime.setup_action) {
24
+ lines.push(`Next setup: ${status.runtime.setup_action.command}`);
25
+ }
26
+
27
+ lines.push(...scopeStatusLines("Project", status.project));
28
+ lines.push(...scopeStatusLines("Global", status.global));
29
+ return lines.join("\n");
30
+ }
31
+
32
+ export function formatEmbeddingProfilesText(profiles: TrapEmbeddingProfiles): string {
33
+ const lines = [
34
+ ...profileSectionLines("Project", profiles.project),
35
+ ...profileSectionLines("Global", profiles.global),
36
+ ];
37
+ return lines.length > 0 ? lines.join("\n") : "No stored embedding profiles.";
38
+ }
39
+
40
+ export function formatEmbeddingsUseText(result: EmbeddingsUseResult): string {
41
+ return [
42
+ `Configured embeddings provider: ${result.embeddings.provider}`,
43
+ `Config: ${result.path}`,
44
+ `Next: ${result.next_action.command}`,
45
+ ].join("\n");
46
+ }
47
+
48
+ function scopeStatusLines(label: string, status: EmbeddingScopeStatus | null): string[] {
49
+ if (!status) return [];
50
+ return [
51
+ "",
52
+ `${label}: total ${status.total}, fresh ${status.fresh}, stale ${status.stale}, missing ${status.missing}`,
53
+ ...profileSectionLines("Stored profiles", status.profiles),
54
+ ];
55
+ }
56
+
57
+ function profileSectionLines(label: string, profiles: EmbeddingProfileSummary[] | null): string[] {
58
+ if (!profiles || profiles.length === 0) return [];
59
+ return [
60
+ `${label}:`,
61
+ ...profiles.map((profile) =>
62
+ ` - ${profile.id} (${profile.embedding_count} embeddings, updated ${profile.updated_at ?? "unknown"})`
63
+ ),
64
+ ];
65
+ }
@@ -0,0 +1,177 @@
1
+ import {
2
+ DEFAULT_OLLAMA_DIMENSIONS,
3
+ DEFAULT_OLLAMA_ENDPOINT,
4
+ DEFAULT_OLLAMA_MODEL,
5
+ EmbeddingProviderUnavailableError,
6
+ JinaEmbedder,
7
+ OllamaEmbedder,
8
+ embeddingConfig,
9
+ embeddingProfileId,
10
+ type EmbeddingConfig,
11
+ type EmbeddingProvider,
12
+ } from "./embedder";
13
+ import { loadCodetrapConfig, type CodetrapConfig, type EmbeddingProviderSetting } from "./config";
14
+
15
+ export type EmbeddingRuntimeInput = EmbeddingRuntime | EmbeddingProvider | undefined;
16
+
17
+ export type EmbeddingRuntimeAction = {
18
+ command: string;
19
+ reason: string;
20
+ };
21
+
22
+ export type EmbeddingRuntimeStatus = {
23
+ available: boolean;
24
+ provider: string | null;
25
+ model: string | null;
26
+ dimensions: number | null;
27
+ passage_version: number | null;
28
+ profile_id: string | null;
29
+ setup_action: EmbeddingRuntimeAction | null;
30
+ };
31
+
32
+ export class EmbeddingRuntime {
33
+ constructor(
34
+ private readonly adapter?: EmbeddingProvider,
35
+ private readonly setupProvider: EmbeddingProviderSetting = "ollama",
36
+ private readonly configuredConfig: EmbeddingConfig | null = null
37
+ ) {}
38
+
39
+ static fromEnvironment(
40
+ env: NodeJS.ProcessEnv = process.env,
41
+ config: CodetrapConfig = loadCodetrapConfig()
42
+ ): EmbeddingRuntime {
43
+ const provider = configuredProvider(env, config);
44
+ if (provider === "ollama") {
45
+ return new EmbeddingRuntime(new OllamaEmbedder(ollamaOptions(env, config)), "ollama");
46
+ }
47
+ if (provider === "jina") {
48
+ const apiKey = env.JINA_API_KEY;
49
+ const configuredConfig = embeddingConfig(new JinaEmbedder(apiKey ?? ""));
50
+ return new EmbeddingRuntime(apiKey ? new JinaEmbedder(apiKey) : undefined, "jina", configuredConfig);
51
+ }
52
+
53
+ const apiKey = env.JINA_API_KEY;
54
+ if (apiKey) return new EmbeddingRuntime(new JinaEmbedder(apiKey), "jina");
55
+ return new EmbeddingRuntime(undefined, "ollama");
56
+ }
57
+
58
+ provider(): EmbeddingProvider | undefined {
59
+ return this.adapter;
60
+ }
61
+
62
+ available(): boolean {
63
+ return this.adapter !== undefined;
64
+ }
65
+
66
+ config(): EmbeddingConfig | null {
67
+ return this.adapter ? embeddingConfig(this.adapter) : this.configuredConfig;
68
+ }
69
+
70
+ requireProvider(message?: string): EmbeddingProvider {
71
+ if (!this.adapter) throw this.unavailableError(message);
72
+ return this.adapter;
73
+ }
74
+
75
+ unavailableError(message?: string): EmbeddingProviderUnavailableError {
76
+ return new EmbeddingProviderUnavailableError(
77
+ message ?? "Embedding provider is unavailable. Configure an embedding provider or use --mode fts."
78
+ );
79
+ }
80
+
81
+ setupAction(): EmbeddingRuntimeAction | null {
82
+ if (this.adapter) return null;
83
+ if (this.setupProvider === "jina") {
84
+ return {
85
+ command: "export JINA_API_KEY=<your-jina-api-key>",
86
+ reason: "Enable Jina semantic and hybrid search; otherwise use --mode fts.",
87
+ };
88
+ }
89
+ return {
90
+ command: "export CODETRAP_EMBEDDING_PROVIDER=ollama",
91
+ reason: "Enable local semantic and hybrid search after installing Ollama and pulling qwen3-embedding:0.6b; otherwise use --mode fts.",
92
+ };
93
+ }
94
+
95
+ status(): EmbeddingRuntimeStatus {
96
+ const config = this.config();
97
+ return {
98
+ available: this.available(),
99
+ provider: config?.provider ?? null,
100
+ model: config?.model ?? null,
101
+ dimensions: config?.dimensions ?? null,
102
+ passage_version: config?.passageVersion ?? null,
103
+ profile_id: config ? embeddingProfileId(config) : null,
104
+ setup_action: this.setupAction(),
105
+ };
106
+ }
107
+
108
+ async health(): Promise<EmbeddingRuntimeStatus> {
109
+ const status = this.status();
110
+ if (!this.adapter || !(this.adapter instanceof OllamaEmbedder)) return status;
111
+
112
+ const health = await this.adapter.health();
113
+ if (health.ok) return status;
114
+ return {
115
+ ...status,
116
+ available: false,
117
+ setup_action: {
118
+ command: health.command ?? `ollama pull ${this.adapter.model}`,
119
+ reason: health.message ?? "Ollama embedding provider is unavailable.",
120
+ },
121
+ };
122
+ }
123
+ }
124
+
125
+ export function defaultEmbeddingRuntime(
126
+ env: NodeJS.ProcessEnv = process.env,
127
+ config?: CodetrapConfig
128
+ ): EmbeddingRuntime {
129
+ return EmbeddingRuntime.fromEnvironment(env, config);
130
+ }
131
+
132
+ export function embeddingRuntimeFrom(input: EmbeddingRuntimeInput): EmbeddingRuntime {
133
+ return input instanceof EmbeddingRuntime ? input : new EmbeddingRuntime(input);
134
+ }
135
+
136
+ function configuredProvider(
137
+ env: NodeJS.ProcessEnv,
138
+ config: CodetrapConfig
139
+ ): EmbeddingProviderSetting | undefined {
140
+ if (config.embeddings?.provider) return config.embeddings.provider;
141
+ if (env.CODETRAP_EMBEDDING_PROVIDER) return parseProviderEnv(env.CODETRAP_EMBEDDING_PROVIDER);
142
+ if (config.embeddings?.endpoint || config.embeddings?.model || config.embeddings?.dimensions) return "ollama";
143
+ if (env.CODETRAP_OLLAMA_ENDPOINT || env.CODETRAP_OLLAMA_MODEL || env.CODETRAP_OLLAMA_DIMENSIONS) {
144
+ return "ollama";
145
+ }
146
+ return undefined;
147
+ }
148
+
149
+ function ollamaOptions(env: NodeJS.ProcessEnv, config: CodetrapConfig) {
150
+ return {
151
+ endpoint: normalizeEndpointSetting(
152
+ config.embeddings?.endpoint ?? env.CODETRAP_OLLAMA_ENDPOINT ?? env.OLLAMA_HOST ?? DEFAULT_OLLAMA_ENDPOINT
153
+ ),
154
+ model: config.embeddings?.model ?? env.CODETRAP_OLLAMA_MODEL ?? DEFAULT_OLLAMA_MODEL,
155
+ dimensions:
156
+ config.embeddings?.dimensions ??
157
+ parseOptionalPositiveInt(env.CODETRAP_OLLAMA_DIMENSIONS, "CODETRAP_OLLAMA_DIMENSIONS") ??
158
+ DEFAULT_OLLAMA_DIMENSIONS,
159
+ };
160
+ }
161
+
162
+ function parseProviderEnv(value: string): EmbeddingProviderSetting {
163
+ if (value === "ollama" || value === "jina") return value;
164
+ throw new Error(`Invalid CODETRAP_EMBEDDING_PROVIDER: ${value}. Expected ollama or jina.`);
165
+ }
166
+
167
+ function parseOptionalPositiveInt(value: string | undefined, label: string): number | undefined {
168
+ if (!value) return undefined;
169
+ const parsed = Number.parseInt(value, 10);
170
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
171
+ throw new Error(`Invalid ${label}: expected a positive integer.`);
172
+ }
173
+
174
+ function normalizeEndpointSetting(endpoint: string): string {
175
+ if (/^https?:\/\//.test(endpoint)) return endpoint;
176
+ return `http://${endpoint}`;
177
+ }
@@ -137,5 +137,3 @@ export function toStatsJson(stats: TrapStatsResult, embeddings?: EmbeddingStatsR
137
137
  export function toDoctorJson(input: DoctorReport): DoctorReport {
138
138
  return input;
139
139
  }
140
-
141
- export { toTrapEvidenceJson, toTrapJson, type JsonTrap, type JsonTrapEvidence };
@@ -2,9 +2,10 @@ import { openGlobal, openProject } from "../db/connection";
2
2
  import { TrapRepository } from "../db/repository";
3
3
  import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
4
4
  import type { Scope } from "./constants";
5
- import type { EmbeddingProvider } from "./embedder";
6
- import { findProjectRoot, getGlobalDB, resolveScopePath } from "./scope";
7
- import { ScopePathResolver } from "./scope-path";
5
+ import type { EmbeddingRuntime } from "./embedding-runtime";
6
+ import type { RankingConfig } from "./search-policy";
7
+ import { findProjectRoot, getGlobalDB } from "./scope";
8
+ import { resolveScopePath, ScopePathResolver } from "./scope-path";
8
9
 
9
10
  const scopePath = new ScopePathResolver();
10
11
 
@@ -36,7 +37,12 @@ export class ScopedRepositoryContext {
36
37
  private globalRepository?: TrapRepository;
37
38
  private projectRepository?: TrapRepository;
38
39
 
39
- constructor(cwd = process.cwd(), private readonly embedder?: EmbeddingProvider, private readonly home?: string) {
40
+ constructor(
41
+ cwd = process.cwd(),
42
+ private readonly embeddings?: EmbeddingRuntime,
43
+ private readonly home?: string,
44
+ private readonly ranking?: RankingConfig
45
+ ) {
40
46
  this.context = createScopeContext(cwd, home);
41
47
  }
42
48
 
@@ -87,14 +93,14 @@ export class ScopedRepositoryContext {
87
93
  throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
88
94
  }
89
95
  if (!this.projectRepository) {
90
- this.projectRepository = new TrapRepository(openProject(projectRoot), this.embedder);
96
+ this.projectRepository = new TrapRepository(openProject(projectRoot), this.embeddings, this.ranking);
91
97
  }
92
98
  return this.projectRepository;
93
99
  }
94
100
 
95
101
  private globalRepo(): TrapRepository {
96
102
  if (!this.globalRepository) {
97
- this.globalRepository = new TrapRepository(openGlobal(this.home), this.embedder);
103
+ this.globalRepository = new TrapRepository(openGlobal(this.home), this.embeddings, this.ranking);
98
104
  }
99
105
  return this.globalRepository;
100
106
  }
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
3
3
  import { openDatabase } from "../db/connection";
4
4
  import { TrapRepository } from "../db/repository";
5
5
  import type { TrapExportRecord } from "../domain/trap";
6
- import { findProjectRoot, resolveScopePath } from "./scope";
6
+ import { findProjectRoot } from "./scope";
7
+ import { resolveScopePath } from "./scope-path";
7
8
  import {
8
9
  backupScopeDatabase,
9
10
  buildScopeMaintenancePaths,
package/src/lib/scope.ts CHANGED
@@ -3,8 +3,6 @@ import { homedir } from "node:os";
3
3
  import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
4
4
  import { defaultScopePathResolver, resolveScopePath, ScopePathResolver } from "./scope-path";
5
5
 
6
- export { resolveScopePath, ScopePathResolver } from "./scope-path";
7
-
8
6
  export function getGlobalDir(homeDir = homedir()): string {
9
7
  const dir = defaultScopePathResolver.join(homeDir, CODETRAP_DIR);
10
8
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });