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.
- package/README.md +151 -52
- package/docs/installation.md +113 -29
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
- 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.md +31 -5
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +144 -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 +28 -3
- 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
package/src/db/queries.ts
CHANGED
|
@@ -179,10 +179,6 @@ export function listTrapEvidence(db: Database, trapId: number): TrapEvidence[] {
|
|
|
179
179
|
.all(trapId) as TrapEvidence[];
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
export function archiveTrap(db: Database, id: number): boolean {
|
|
183
|
-
return markTrapArchived(db, id);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
182
|
export function markTrapArchived(db: Database, id: number): boolean {
|
|
187
183
|
const result = db
|
|
188
184
|
.prepare(
|
|
@@ -198,27 +194,6 @@ export function markTrapArchived(db: Database, id: number): boolean {
|
|
|
198
194
|
return result.changes > 0;
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
export function supersedeTrap(
|
|
202
|
-
db: Database,
|
|
203
|
-
id: number,
|
|
204
|
-
supersededById: number,
|
|
205
|
-
stateKey?: string
|
|
206
|
-
): boolean {
|
|
207
|
-
if (id === supersededById) return false;
|
|
208
|
-
|
|
209
|
-
const oldTrap = getTrap(db, id);
|
|
210
|
-
const newTrap = getTrap(db, supersededById);
|
|
211
|
-
if (!oldTrap || !newTrap) return false;
|
|
212
|
-
|
|
213
|
-
const key = stateKey ?? oldTrap.state_key ?? newTrap.state_key ?? `trap:${id}`;
|
|
214
|
-
const tx = db.transaction(() => {
|
|
215
|
-
markTrapSuperseded(db, id, key);
|
|
216
|
-
markTrapSuperseding(db, supersededById, id, key);
|
|
217
|
-
});
|
|
218
|
-
tx();
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
197
|
export function markTrapSuperseded(db: Database, id: number, stateKey: string): boolean {
|
|
223
198
|
const result = db.prepare(supersedeTrapSql).run(stateKey, id);
|
|
224
199
|
return result.changes > 0;
|
package/src/db/repository.ts
CHANGED
|
@@ -11,32 +11,39 @@ import type {
|
|
|
11
11
|
import { SearchService, type SearchOptions } from "../lib/search-service";
|
|
12
12
|
import {
|
|
13
13
|
type EmbeddingConfig,
|
|
14
|
-
type EmbeddingProvider,
|
|
15
14
|
type StoredEmbedding,
|
|
16
15
|
} from "../lib/embedder";
|
|
16
|
+
import {
|
|
17
|
+
embeddingRuntimeFrom,
|
|
18
|
+
type EmbeddingRuntime,
|
|
19
|
+
type EmbeddingRuntimeInput,
|
|
20
|
+
} from "../lib/embedding-runtime";
|
|
17
21
|
import { runEmbeddingJob } from "../lib/embedding-job";
|
|
18
22
|
import { passageFieldsChanged } from "../lib/trap-search-document";
|
|
23
|
+
import * as embeddingQueries from "./embedding-queries";
|
|
19
24
|
import * as queries from "./queries";
|
|
20
25
|
import type { TrapStatus } from "../lib/constants";
|
|
21
26
|
import { TrapSearchPolicy } from "../lib/search-policy";
|
|
22
|
-
import {
|
|
27
|
+
import type { RankingConfig } from "../lib/search-policy";
|
|
23
28
|
import { archiveTrapLifecycle, supersedeTrapLifecycle } from "../lib/trap-lifecycle";
|
|
29
|
+
import type { EmbeddingProfileSummary } from "./embedding-queries";
|
|
24
30
|
|
|
25
31
|
export type TrapStats = ReturnType<typeof queries.getStats>;
|
|
26
|
-
export type EmbeddingStateCounts = ReturnType<
|
|
32
|
+
export type EmbeddingStateCounts = ReturnType<typeof embeddingQueries.getEmbeddingStateCounts>;
|
|
27
33
|
export type TrapRecordInsert = queries.TrapRecordInsert;
|
|
28
34
|
|
|
29
35
|
export class TrapRepository {
|
|
30
36
|
private readonly searchService: SearchService;
|
|
31
37
|
private readonly searchPolicy = new TrapSearchPolicy();
|
|
32
|
-
private readonly
|
|
38
|
+
private readonly embeddings: EmbeddingRuntime;
|
|
33
39
|
|
|
34
40
|
constructor(
|
|
35
41
|
private readonly db: Database,
|
|
36
|
-
|
|
42
|
+
embeddings?: EmbeddingRuntimeInput,
|
|
43
|
+
ranking?: RankingConfig
|
|
37
44
|
) {
|
|
38
|
-
this.
|
|
39
|
-
this.
|
|
45
|
+
this.embeddings = embeddingRuntimeFrom(embeddings);
|
|
46
|
+
this.searchService = new SearchService(db, this.embeddings, ranking);
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
add(input: TrapInput): number {
|
|
@@ -79,7 +86,7 @@ export class TrapRepository {
|
|
|
79
86
|
update(id: number, input: TrapUpdate): boolean {
|
|
80
87
|
const success = queries.updateTrap(this.db, id, input);
|
|
81
88
|
if (success && passageFieldsChanged(input)) {
|
|
82
|
-
this.
|
|
89
|
+
embeddingQueries.deleteEmbedding(this.db, id);
|
|
83
90
|
}
|
|
84
91
|
return success;
|
|
85
92
|
}
|
|
@@ -114,7 +121,11 @@ export class TrapRepository {
|
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
embeddingStats(config: EmbeddingConfig | null, opts: { scope?: string; status?: TrapStatus | "all" } = {}): EmbeddingStateCounts {
|
|
117
|
-
return this.
|
|
124
|
+
return embeddingQueries.getEmbeddingStateCounts(this.db, config, opts);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
embeddingProfiles(opts: { scope?: string; status?: TrapStatus | "all" } = {}): EmbeddingProfileSummary[] {
|
|
128
|
+
return embeddingQueries.listEmbeddingProfiles(this.db, opts);
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
exportAll(): TrapExportRecord[] {
|
|
@@ -145,23 +156,23 @@ export class TrapRepository {
|
|
|
145
156
|
return this.db.transaction(callback)();
|
|
146
157
|
}
|
|
147
158
|
|
|
148
|
-
getEmbedding(trapId: number): StoredEmbedding | null {
|
|
149
|
-
return this.
|
|
159
|
+
getEmbedding(trapId: number, config?: EmbeddingConfig): StoredEmbedding | null {
|
|
160
|
+
return embeddingQueries.getEmbedding(this.db, trapId, config);
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
upsertEmbedding(record: StoredEmbedding): void {
|
|
153
|
-
this.
|
|
164
|
+
embeddingQueries.upsertEmbedding(this.db, record);
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
deleteEmbedding(trapId: number): void {
|
|
157
|
-
this.
|
|
168
|
+
embeddingQueries.deleteEmbedding(this.db, trapId);
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
getTrapsNeedingEmbeddings(
|
|
161
172
|
config: EmbeddingConfig,
|
|
162
173
|
opts: { scope?: string; category?: string; status?: TrapStatus | "all"; force?: boolean; limit?: number } = {}
|
|
163
174
|
): Trap[] {
|
|
164
|
-
return this.
|
|
175
|
+
return embeddingQueries.getTrapsNeedingEmbeddings(this.db, config, opts);
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
async ensureEmbeddings(opts: { scope?: string; category?: string; limit?: number; force?: boolean; batchSize?: number } = {}): Promise<{
|
|
@@ -169,18 +180,18 @@ export class TrapRepository {
|
|
|
169
180
|
skipped: number;
|
|
170
181
|
batches: number;
|
|
171
182
|
}> {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
const provider = this.embeddings.requireProvider(
|
|
184
|
+
"Embedding provider is unavailable. Configure an embedding provider to generate embeddings."
|
|
185
|
+
);
|
|
175
186
|
|
|
176
187
|
return runEmbeddingJob(
|
|
177
188
|
{
|
|
178
|
-
countEmbeddable: (countOpts) => this.
|
|
189
|
+
countEmbeddable: (countOpts) => embeddingQueries.countEmbeddableTraps(this.db, countOpts),
|
|
179
190
|
trapsNeedingEmbeddings: (config, jobOpts) =>
|
|
180
|
-
this.
|
|
181
|
-
saveEmbedding: (record) => this.
|
|
191
|
+
embeddingQueries.getTrapsNeedingEmbeddings(this.db, config, jobOpts),
|
|
192
|
+
saveEmbedding: (record) => embeddingQueries.upsertEmbedding(this.db, record),
|
|
182
193
|
},
|
|
183
|
-
|
|
194
|
+
provider,
|
|
184
195
|
opts
|
|
185
196
|
);
|
|
186
197
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -175,6 +175,86 @@ function applyMigrations(db: Database, from: number): void {
|
|
|
175
175
|
|
|
176
176
|
db.prepare("UPDATE schema_version SET version = ?").run(5);
|
|
177
177
|
}
|
|
178
|
+
|
|
179
|
+
if (from < 6) {
|
|
180
|
+
migrateEmbeddingProfiles(db);
|
|
181
|
+
db.prepare("UPDATE schema_version SET version = ?").run(6);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function migrateEmbeddingProfiles(db: Database): void {
|
|
186
|
+
db.exec(`
|
|
187
|
+
CREATE TABLE IF NOT EXISTS embedding_profiles (
|
|
188
|
+
id TEXT PRIMARY KEY,
|
|
189
|
+
provider TEXT NOT NULL,
|
|
190
|
+
model TEXT NOT NULL,
|
|
191
|
+
dimensions INTEGER NOT NULL,
|
|
192
|
+
passage_version INTEGER NOT NULL,
|
|
193
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
194
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
195
|
+
);
|
|
196
|
+
`);
|
|
197
|
+
|
|
198
|
+
const hasTrapEmbeddings = tableExists(db, "trap_embeddings");
|
|
199
|
+
if (!hasTrapEmbeddings || columnExists(db, "trap_embeddings", "profile_id")) {
|
|
200
|
+
createProfileAwareEmbeddingsTable(db);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
db.exec("ALTER TABLE trap_embeddings RENAME TO trap_embeddings_legacy_v5");
|
|
205
|
+
createProfileAwareEmbeddingsTable(db);
|
|
206
|
+
|
|
207
|
+
db.exec(`
|
|
208
|
+
INSERT OR IGNORE INTO embedding_profiles (
|
|
209
|
+
id, provider, model, dimensions, passage_version, created_at, updated_at
|
|
210
|
+
)
|
|
211
|
+
SELECT
|
|
212
|
+
provider || ':' || model || ':' || dimensions || ':p' || passage_version,
|
|
213
|
+
provider,
|
|
214
|
+
model,
|
|
215
|
+
dimensions,
|
|
216
|
+
passage_version,
|
|
217
|
+
MIN(updated_at),
|
|
218
|
+
MAX(updated_at)
|
|
219
|
+
FROM trap_embeddings_legacy_v5
|
|
220
|
+
GROUP BY provider, model, dimensions, passage_version;
|
|
221
|
+
|
|
222
|
+
INSERT OR REPLACE INTO trap_embeddings (
|
|
223
|
+
trap_id, profile_id, passage_hash, embedding, updated_at
|
|
224
|
+
)
|
|
225
|
+
SELECT
|
|
226
|
+
trap_id,
|
|
227
|
+
provider || ':' || model || ':' || dimensions || ':p' || passage_version,
|
|
228
|
+
passage_hash,
|
|
229
|
+
embedding,
|
|
230
|
+
updated_at
|
|
231
|
+
FROM trap_embeddings_legacy_v5;
|
|
232
|
+
|
|
233
|
+
DROP TABLE trap_embeddings_legacy_v5;
|
|
234
|
+
`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createProfileAwareEmbeddingsTable(db: Database): void {
|
|
238
|
+
db.exec(`
|
|
239
|
+
CREATE TABLE IF NOT EXISTS trap_embeddings (
|
|
240
|
+
trap_id INTEGER NOT NULL REFERENCES traps(id) ON DELETE CASCADE,
|
|
241
|
+
profile_id TEXT NOT NULL REFERENCES embedding_profiles(id) ON DELETE CASCADE,
|
|
242
|
+
passage_hash TEXT NOT NULL,
|
|
243
|
+
embedding BLOB NOT NULL,
|
|
244
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
245
|
+
PRIMARY KEY (trap_id, profile_id)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_trap_embeddings_profile
|
|
249
|
+
ON trap_embeddings(profile_id);
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function tableExists(db: Database, table: string): boolean {
|
|
254
|
+
const row = db
|
|
255
|
+
.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
256
|
+
.get(table) as { name: string } | null;
|
|
257
|
+
return row !== null;
|
|
178
258
|
}
|
|
179
259
|
|
|
180
260
|
function columnExists(db: Database, table: string, column: string): boolean {
|
package/src/index.ts
CHANGED
|
@@ -15,8 +15,12 @@ if (args.length === 0) {
|
|
|
15
15
|
} else if (args[0] === "serve") {
|
|
16
16
|
import("./mcp/server").then((m) => m.start());
|
|
17
17
|
} else if (args[0] === "web") {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (args.slice(1).some(isHelpArg)) {
|
|
19
|
+
showWebHelp();
|
|
20
|
+
} else {
|
|
21
|
+
const { startWebServerFromArgs } = await import("./web/server");
|
|
22
|
+
await startWebServerFromArgs(args.slice(1));
|
|
23
|
+
}
|
|
20
24
|
} else if (args[0] === "init") {
|
|
21
25
|
const cwd = process.cwd();
|
|
22
26
|
if (findProjectRoot(cwd)) {
|
|
@@ -31,6 +35,10 @@ if (args.length === 0) {
|
|
|
31
35
|
await run(args, store);
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
function isHelpArg(arg: string | undefined): boolean {
|
|
39
|
+
return arg === "--help" || arg === "-h" || arg === "help";
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function showHelp(): void {
|
|
35
43
|
console.log("codetrap — capture coding pitfalls so AI doesn't repeat mistakes");
|
|
36
44
|
console.log("");
|
|
@@ -45,7 +53,8 @@ function showHelp(): void {
|
|
|
45
53
|
console.log(" add_trap_evidence Attach evidence to a trap");
|
|
46
54
|
console.log(" archive_trap Archive a trap");
|
|
47
55
|
console.log(" supersede_trap Mark one trap as superseded by another");
|
|
48
|
-
console.log(" embed Generate embeddings for semantic search");
|
|
56
|
+
console.log(" embed Generate embeddings for semantic search (Ollama or Jina)");
|
|
57
|
+
console.log(" embeddings Manage embedding profiles, provider config, and reindexing");
|
|
49
58
|
console.log(" session Record implementation notes and capture candidate traps");
|
|
50
59
|
console.log(" web Start local review and trap library console");
|
|
51
60
|
console.log(" export Export traps as JSON");
|
|
@@ -77,3 +86,19 @@ function showHelp(): void {
|
|
|
77
86
|
console.log(" --to-project-path <path> Destination project path for scope repair/migration");
|
|
78
87
|
console.log(" --dry-run|--apply Preview or apply scope repair/migration");
|
|
79
88
|
}
|
|
89
|
+
|
|
90
|
+
function showWebHelp(): void {
|
|
91
|
+
console.log("codetrap web — start the local review and trap library console");
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log("Usage:");
|
|
94
|
+
console.log(" codetrap web [--project <path>] [--host <host>] [--port <n>]");
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log("Options:");
|
|
97
|
+
console.log(" --project <path> Project path to open in the web console");
|
|
98
|
+
console.log(" --host <host> Host to bind (default 127.0.0.1)");
|
|
99
|
+
console.log(" --port <n> Port to try first (default 4737; next free port is used if busy)");
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log("Examples:");
|
|
102
|
+
console.log(" codetrap web");
|
|
103
|
+
console.log(" codetrap web --project /path/to/project --port 4789");
|
|
104
|
+
}
|
|
@@ -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";
|