aiwcli 0.12.3 → 0.12.7
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/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +205 -205
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -64
- package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
- package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -183
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -130
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -180
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +690 -690
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Embedding Indexer — Builds vector index from existing JSON session indexes.
|
|
4
|
-
*
|
|
5
|
-
* Reads ~/.claude/rlm-index/{project}/*.index.json (built by /rlm:index),
|
|
6
|
-
* embeds each segment via Ollama nomic-embed-text, and stores vectors in
|
|
7
|
-
* SQLite + sqlite-vec at ~/.claude/rlm-vectors.db.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* bun embedding-indexer.ts --batch # Index all sessions
|
|
11
|
-
* bun embedding-indexer.ts --batch --limit=10 # Index first 10 unindexed
|
|
12
|
-
* bun embedding-indexer.ts --batch --project=aiwcli # Index matching project only
|
|
13
|
-
* bun embedding-indexer.ts --stats # Show index statistics
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readdir } from "fs/promises";
|
|
17
|
-
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
18
|
-
import { join } from "path";
|
|
19
|
-
import { homedir } from "os";
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import { RLM_INDEX_DIR, type SessionIndex } from "./types.js";
|
|
22
|
-
import { logInfo, logWarn, logError, logDebug } from "./logger.js";
|
|
23
|
-
import { checkOllamaHealth, embed } from "./ollama-client.js";
|
|
24
|
-
import {
|
|
25
|
-
openVectorDb,
|
|
26
|
-
insertChunks,
|
|
27
|
-
markSessionEmbedded,
|
|
28
|
-
isSessionEmbedded,
|
|
29
|
-
deleteSessionChunks,
|
|
30
|
-
getStats,
|
|
31
|
-
type ChunkRow,
|
|
32
|
-
} from "./vector-store.js";
|
|
33
|
-
import { loadTranscript } from "./transcript-loader.js";
|
|
34
|
-
|
|
35
|
-
const HOOK_NAME = "rlm_embed_idx";
|
|
36
|
-
const MAX_EMBED_CHARS = 8000;
|
|
37
|
-
|
|
38
|
-
// Zod schema for SessionIndex validation
|
|
39
|
-
const SessionIndexSchema = z.object({
|
|
40
|
-
session_id: z.string(),
|
|
41
|
-
project: z.string(),
|
|
42
|
-
date: z.string(),
|
|
43
|
-
source_mtime: z.number(),
|
|
44
|
-
segments: z.array(z.object({
|
|
45
|
-
lines: z.tuple([z.number(), z.number()]),
|
|
46
|
-
topic: z.string(),
|
|
47
|
-
keywords: z.array(z.string()),
|
|
48
|
-
})),
|
|
49
|
-
});
|
|
50
|
-
type ValidatedSessionIndex = z.infer<typeof SessionIndexSchema>;
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// CLI entry
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
const args = process.argv.slice(2);
|
|
57
|
-
const isBatch = args.includes("--batch");
|
|
58
|
-
const isStats = args.includes("--stats");
|
|
59
|
-
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
60
|
-
const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : Infinity;
|
|
61
|
-
const projectArg = args.find((a) => a.startsWith("--project="));
|
|
62
|
-
const projectFilter = projectArg ? projectArg.split("=")[1] : null;
|
|
63
|
-
|
|
64
|
-
if (isStats) {
|
|
65
|
-
showStats();
|
|
66
|
-
} else if (isBatch) {
|
|
67
|
-
runBatch().catch((e) => {
|
|
68
|
-
logError(HOOK_NAME, `Fatal: ${e}`, { stderr: true });
|
|
69
|
-
process.exitCode = 1;
|
|
70
|
-
});
|
|
71
|
-
} else {
|
|
72
|
-
process.stderr.write(
|
|
73
|
-
"Usage: bun embedding-indexer.ts --batch [--limit=N] [--project=name]\n" +
|
|
74
|
-
" bun embedding-indexer.ts --stats\n",
|
|
75
|
-
);
|
|
76
|
-
process.exitCode = 1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Stats
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
function showStats(): void {
|
|
84
|
-
const db = openVectorDb();
|
|
85
|
-
const stats = getStats(db);
|
|
86
|
-
const result = {
|
|
87
|
-
sessions: stats.session_count,
|
|
88
|
-
chunks: stats.chunk_count,
|
|
89
|
-
db_path: db.filename,
|
|
90
|
-
};
|
|
91
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
92
|
-
db.close();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Batch runner
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
async function runBatch(): Promise<void> {
|
|
100
|
-
// Check Ollama health first
|
|
101
|
-
const health = await checkOllamaHealth();
|
|
102
|
-
if (!health.ok) {
|
|
103
|
-
logError(HOOK_NAME, health.error ?? "Unknown Ollama health check error", { stderr: true });
|
|
104
|
-
process.exitCode = 1;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Verify rlm-index exists
|
|
109
|
-
if (!existsSync(RLM_INDEX_DIR)) {
|
|
110
|
-
logError(
|
|
111
|
-
HOOK_NAME,
|
|
112
|
-
`No JSON indexes found at ${RLM_INDEX_DIR}. Run \`/rlm:index\` first to build keyword indexes, then re-run \`/rlm:embed-index\`.`,
|
|
113
|
-
{ stderr: true },
|
|
114
|
-
);
|
|
115
|
-
process.exitCode = 1;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const db = openVectorDb();
|
|
120
|
-
let embedded = 0;
|
|
121
|
-
let skipped = 0;
|
|
122
|
-
let errors = 0;
|
|
123
|
-
let total = 0;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
// Scan project directories
|
|
127
|
-
const projectDirs = await readdir(RLM_INDEX_DIR, { withFileTypes: true });
|
|
128
|
-
const projects = projectDirs
|
|
129
|
-
.filter((d) => d.isDirectory())
|
|
130
|
-
.map((d) => d.name)
|
|
131
|
-
.filter((name) => !projectFilter || name.includes(projectFilter));
|
|
132
|
-
|
|
133
|
-
for (const project of projects) {
|
|
134
|
-
const projectDir = join(RLM_INDEX_DIR, project);
|
|
135
|
-
const files = await readdir(projectDir);
|
|
136
|
-
const indexFiles = files.filter((f) => f.endsWith(".index.json"));
|
|
137
|
-
|
|
138
|
-
for (const indexFile of indexFiles) {
|
|
139
|
-
if (embedded >= limit) break;
|
|
140
|
-
total++;
|
|
141
|
-
|
|
142
|
-
const indexPath = join(projectDir, indexFile);
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const rawJson = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
146
|
-
const parseResult = SessionIndexSchema.safeParse(rawJson);
|
|
147
|
-
if (!parseResult.success) {
|
|
148
|
-
errors++;
|
|
149
|
-
logWarn(HOOK_NAME, `Invalid index format ${indexFile}: ${parseResult.error.message}`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const indexData = parseResult.data;
|
|
153
|
-
|
|
154
|
-
// Skip if already embedded at same mtime
|
|
155
|
-
if (
|
|
156
|
-
isSessionEmbedded(db, indexData.session_id, project, indexData.source_mtime)
|
|
157
|
-
) {
|
|
158
|
-
skipped++;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Re-index: delete old chunks if any
|
|
163
|
-
deleteSessionChunks(db, indexData.session_id, project);
|
|
164
|
-
|
|
165
|
-
// Build embedding texts for each segment
|
|
166
|
-
const texts: string[] = [];
|
|
167
|
-
const segmentMeta: Array<{
|
|
168
|
-
index: number;
|
|
169
|
-
lines: [number, number];
|
|
170
|
-
topic: string;
|
|
171
|
-
}> = [];
|
|
172
|
-
|
|
173
|
-
for (let i = 0; i < indexData.segments.length; i++) {
|
|
174
|
-
const seg = indexData.segments[i];
|
|
175
|
-
|
|
176
|
-
// Load transcript content for this segment
|
|
177
|
-
let content = "";
|
|
178
|
-
try {
|
|
179
|
-
const loaded = await loadTranscript(
|
|
180
|
-
join(
|
|
181
|
-
// Derive source path from index data
|
|
182
|
-
// source_path might be stored in the index, otherwise reconstruct
|
|
183
|
-
getSourcePath(indexData, project),
|
|
184
|
-
),
|
|
185
|
-
seg.lines,
|
|
186
|
-
4000,
|
|
187
|
-
);
|
|
188
|
-
content = loaded.content;
|
|
189
|
-
} catch {
|
|
190
|
-
// Fall back to topic + keywords as embedding text
|
|
191
|
-
content = `Topic: ${seg.topic}. Keywords: ${seg.keywords.join(", ")}`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const embedText = `Project: ${project}. Date: ${indexData.date}. Topic: ${seg.topic}.\n${content}`;
|
|
195
|
-
texts.push(embedText.slice(0, MAX_EMBED_CHARS));
|
|
196
|
-
segmentMeta.push({ index: i, lines: seg.lines, topic: seg.topic });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (texts.length === 0) {
|
|
200
|
-
logWarn(HOOK_NAME, `No segments for ${indexData.session_id}, skipping`);
|
|
201
|
-
skipped++;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Embed all segments
|
|
206
|
-
const embeddings = await embed(texts);
|
|
207
|
-
|
|
208
|
-
// Build chunk rows
|
|
209
|
-
const chunks: ChunkRow[] = embeddings.map((emb, i) => ({
|
|
210
|
-
session_id: indexData.session_id,
|
|
211
|
-
project,
|
|
212
|
-
date: indexData.date,
|
|
213
|
-
segment_index: segmentMeta[i].index,
|
|
214
|
-
line_start: segmentMeta[i].lines[0],
|
|
215
|
-
line_end: segmentMeta[i].lines[1],
|
|
216
|
-
topic: segmentMeta[i].topic,
|
|
217
|
-
chunk_text: texts[i].slice(0, 2000),
|
|
218
|
-
source_path: getSourcePath(indexData, project),
|
|
219
|
-
embedding: emb,
|
|
220
|
-
}));
|
|
221
|
-
|
|
222
|
-
insertChunks(db, chunks);
|
|
223
|
-
markSessionEmbedded(
|
|
224
|
-
db,
|
|
225
|
-
indexData.session_id,
|
|
226
|
-
project,
|
|
227
|
-
indexData.source_mtime,
|
|
228
|
-
chunks.length,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
embedded++;
|
|
232
|
-
if (embedded % 50 === 0) {
|
|
233
|
-
logInfo(HOOK_NAME, `Progress: ${embedded} sessions embedded`, { stderr: true });
|
|
234
|
-
}
|
|
235
|
-
} catch (e) {
|
|
236
|
-
errors++;
|
|
237
|
-
logWarn(HOOK_NAME, `Error embedding ${indexFile}: ${e}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (embedded >= limit) break;
|
|
242
|
-
}
|
|
243
|
-
} finally {
|
|
244
|
-
db.close();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const result = { embedded, skipped, errors, total };
|
|
248
|
-
process.stdout.write(JSON.stringify(result) + "\n");
|
|
249
|
-
logInfo(HOOK_NAME, `Done: ${JSON.stringify(result)}`, { stderr: true });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
// Helpers
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Derive the source JSONL path from a ValidatedSessionIndex.
|
|
258
|
-
* The index stores source_mtime but not always the full path.
|
|
259
|
-
* Reconstruct from ~/.claude/projects/{project-slug}/{session_id}.jsonl
|
|
260
|
-
*/
|
|
261
|
-
function getSourcePath(index: ValidatedSessionIndex, _project: string): string {
|
|
262
|
-
// SessionIndex doesn't have a source_path field, but the searcher derives it.
|
|
263
|
-
// The JSONL files live under ~/.claude/projects/{encoded-project-path}/
|
|
264
|
-
// We need to find the actual file. Use the index's session_id to locate it.
|
|
265
|
-
const claudeProjectsDir = join(homedir(), ".claude", "projects");
|
|
266
|
-
|
|
267
|
-
// Search for the session file across all project dirs
|
|
268
|
-
try {
|
|
269
|
-
const projectDirs = readdirSync(claudeProjectsDir, { withFileTypes: true });
|
|
270
|
-
for (const dir of projectDirs) {
|
|
271
|
-
if (!dir.isDirectory()) continue;
|
|
272
|
-
const candidatePath = join(
|
|
273
|
-
claudeProjectsDir,
|
|
274
|
-
dir.name,
|
|
275
|
-
`${index.session_id}.jsonl`,
|
|
276
|
-
);
|
|
277
|
-
if (existsSync(candidatePath)) {
|
|
278
|
-
return candidatePath;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
} catch {
|
|
282
|
-
// Fall through
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Fallback: best guess
|
|
286
|
-
return join(claudeProjectsDir, _project, `${index.session_id}.jsonl`);
|
|
287
|
-
}
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Embedding Indexer — Builds vector index from existing JSON session indexes.
|
|
4
|
+
*
|
|
5
|
+
* Reads ~/.claude/rlm-index/{project}/*.index.json (built by /rlm:index),
|
|
6
|
+
* embeds each segment via Ollama nomic-embed-text, and stores vectors in
|
|
7
|
+
* SQLite + sqlite-vec at ~/.claude/rlm-vectors.db.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun embedding-indexer.ts --batch # Index all sessions
|
|
11
|
+
* bun embedding-indexer.ts --batch --limit=10 # Index first 10 unindexed
|
|
12
|
+
* bun embedding-indexer.ts --batch --project=aiwcli # Index matching project only
|
|
13
|
+
* bun embedding-indexer.ts --stats # Show index statistics
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readdir } from "fs/promises";
|
|
17
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { RLM_INDEX_DIR, type SessionIndex } from "./types.js";
|
|
22
|
+
import { logInfo, logWarn, logError, logDebug } from "./logger.js";
|
|
23
|
+
import { checkOllamaHealth, embed } from "./ollama-client.js";
|
|
24
|
+
import {
|
|
25
|
+
openVectorDb,
|
|
26
|
+
insertChunks,
|
|
27
|
+
markSessionEmbedded,
|
|
28
|
+
isSessionEmbedded,
|
|
29
|
+
deleteSessionChunks,
|
|
30
|
+
getStats,
|
|
31
|
+
type ChunkRow,
|
|
32
|
+
} from "./vector-store.js";
|
|
33
|
+
import { loadTranscript } from "./transcript-loader.js";
|
|
34
|
+
|
|
35
|
+
const HOOK_NAME = "rlm_embed_idx";
|
|
36
|
+
const MAX_EMBED_CHARS = 8000;
|
|
37
|
+
|
|
38
|
+
// Zod schema for SessionIndex validation
|
|
39
|
+
const SessionIndexSchema = z.object({
|
|
40
|
+
session_id: z.string(),
|
|
41
|
+
project: z.string(),
|
|
42
|
+
date: z.string(),
|
|
43
|
+
source_mtime: z.number(),
|
|
44
|
+
segments: z.array(z.object({
|
|
45
|
+
lines: z.tuple([z.number(), z.number()]),
|
|
46
|
+
topic: z.string(),
|
|
47
|
+
keywords: z.array(z.string()),
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
type ValidatedSessionIndex = z.infer<typeof SessionIndexSchema>;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// CLI entry
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const args = process.argv.slice(2);
|
|
57
|
+
const isBatch = args.includes("--batch");
|
|
58
|
+
const isStats = args.includes("--stats");
|
|
59
|
+
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
60
|
+
const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : Infinity;
|
|
61
|
+
const projectArg = args.find((a) => a.startsWith("--project="));
|
|
62
|
+
const projectFilter = projectArg ? projectArg.split("=")[1] : null;
|
|
63
|
+
|
|
64
|
+
if (isStats) {
|
|
65
|
+
showStats();
|
|
66
|
+
} else if (isBatch) {
|
|
67
|
+
runBatch().catch((e) => {
|
|
68
|
+
logError(HOOK_NAME, `Fatal: ${e}`, { stderr: true });
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
"Usage: bun embedding-indexer.ts --batch [--limit=N] [--project=name]\n" +
|
|
74
|
+
" bun embedding-indexer.ts --stats\n",
|
|
75
|
+
);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Stats
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function showStats(): void {
|
|
84
|
+
const db = openVectorDb();
|
|
85
|
+
const stats = getStats(db);
|
|
86
|
+
const result = {
|
|
87
|
+
sessions: stats.session_count,
|
|
88
|
+
chunks: stats.chunk_count,
|
|
89
|
+
db_path: db.filename,
|
|
90
|
+
};
|
|
91
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
92
|
+
db.close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Batch runner
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
async function runBatch(): Promise<void> {
|
|
100
|
+
// Check Ollama health first
|
|
101
|
+
const health = await checkOllamaHealth();
|
|
102
|
+
if (!health.ok) {
|
|
103
|
+
logError(HOOK_NAME, health.error ?? "Unknown Ollama health check error", { stderr: true });
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Verify rlm-index exists
|
|
109
|
+
if (!existsSync(RLM_INDEX_DIR)) {
|
|
110
|
+
logError(
|
|
111
|
+
HOOK_NAME,
|
|
112
|
+
`No JSON indexes found at ${RLM_INDEX_DIR}. Run \`/rlm:index\` first to build keyword indexes, then re-run \`/rlm:embed-index\`.`,
|
|
113
|
+
{ stderr: true },
|
|
114
|
+
);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const db = openVectorDb();
|
|
120
|
+
let embedded = 0;
|
|
121
|
+
let skipped = 0;
|
|
122
|
+
let errors = 0;
|
|
123
|
+
let total = 0;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Scan project directories
|
|
127
|
+
const projectDirs = await readdir(RLM_INDEX_DIR, { withFileTypes: true });
|
|
128
|
+
const projects = projectDirs
|
|
129
|
+
.filter((d) => d.isDirectory())
|
|
130
|
+
.map((d) => d.name)
|
|
131
|
+
.filter((name) => !projectFilter || name.includes(projectFilter));
|
|
132
|
+
|
|
133
|
+
for (const project of projects) {
|
|
134
|
+
const projectDir = join(RLM_INDEX_DIR, project);
|
|
135
|
+
const files = await readdir(projectDir);
|
|
136
|
+
const indexFiles = files.filter((f) => f.endsWith(".index.json"));
|
|
137
|
+
|
|
138
|
+
for (const indexFile of indexFiles) {
|
|
139
|
+
if (embedded >= limit) break;
|
|
140
|
+
total++;
|
|
141
|
+
|
|
142
|
+
const indexPath = join(projectDir, indexFile);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const rawJson = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
146
|
+
const parseResult = SessionIndexSchema.safeParse(rawJson);
|
|
147
|
+
if (!parseResult.success) {
|
|
148
|
+
errors++;
|
|
149
|
+
logWarn(HOOK_NAME, `Invalid index format ${indexFile}: ${parseResult.error.message}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const indexData = parseResult.data;
|
|
153
|
+
|
|
154
|
+
// Skip if already embedded at same mtime
|
|
155
|
+
if (
|
|
156
|
+
isSessionEmbedded(db, indexData.session_id, project, indexData.source_mtime)
|
|
157
|
+
) {
|
|
158
|
+
skipped++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Re-index: delete old chunks if any
|
|
163
|
+
deleteSessionChunks(db, indexData.session_id, project);
|
|
164
|
+
|
|
165
|
+
// Build embedding texts for each segment
|
|
166
|
+
const texts: string[] = [];
|
|
167
|
+
const segmentMeta: Array<{
|
|
168
|
+
index: number;
|
|
169
|
+
lines: [number, number];
|
|
170
|
+
topic: string;
|
|
171
|
+
}> = [];
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < indexData.segments.length; i++) {
|
|
174
|
+
const seg = indexData.segments[i];
|
|
175
|
+
|
|
176
|
+
// Load transcript content for this segment
|
|
177
|
+
let content = "";
|
|
178
|
+
try {
|
|
179
|
+
const loaded = await loadTranscript(
|
|
180
|
+
join(
|
|
181
|
+
// Derive source path from index data
|
|
182
|
+
// source_path might be stored in the index, otherwise reconstruct
|
|
183
|
+
getSourcePath(indexData, project),
|
|
184
|
+
),
|
|
185
|
+
seg.lines,
|
|
186
|
+
4000,
|
|
187
|
+
);
|
|
188
|
+
content = loaded.content;
|
|
189
|
+
} catch {
|
|
190
|
+
// Fall back to topic + keywords as embedding text
|
|
191
|
+
content = `Topic: ${seg.topic}. Keywords: ${seg.keywords.join(", ")}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const embedText = `Project: ${project}. Date: ${indexData.date}. Topic: ${seg.topic}.\n${content}`;
|
|
195
|
+
texts.push(embedText.slice(0, MAX_EMBED_CHARS));
|
|
196
|
+
segmentMeta.push({ index: i, lines: seg.lines, topic: seg.topic });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (texts.length === 0) {
|
|
200
|
+
logWarn(HOOK_NAME, `No segments for ${indexData.session_id}, skipping`);
|
|
201
|
+
skipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Embed all segments
|
|
206
|
+
const embeddings = await embed(texts);
|
|
207
|
+
|
|
208
|
+
// Build chunk rows
|
|
209
|
+
const chunks: ChunkRow[] = embeddings.map((emb, i) => ({
|
|
210
|
+
session_id: indexData.session_id,
|
|
211
|
+
project,
|
|
212
|
+
date: indexData.date,
|
|
213
|
+
segment_index: segmentMeta[i].index,
|
|
214
|
+
line_start: segmentMeta[i].lines[0],
|
|
215
|
+
line_end: segmentMeta[i].lines[1],
|
|
216
|
+
topic: segmentMeta[i].topic,
|
|
217
|
+
chunk_text: texts[i].slice(0, 2000),
|
|
218
|
+
source_path: getSourcePath(indexData, project),
|
|
219
|
+
embedding: emb,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
insertChunks(db, chunks);
|
|
223
|
+
markSessionEmbedded(
|
|
224
|
+
db,
|
|
225
|
+
indexData.session_id,
|
|
226
|
+
project,
|
|
227
|
+
indexData.source_mtime,
|
|
228
|
+
chunks.length,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
embedded++;
|
|
232
|
+
if (embedded % 50 === 0) {
|
|
233
|
+
logInfo(HOOK_NAME, `Progress: ${embedded} sessions embedded`, { stderr: true });
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
errors++;
|
|
237
|
+
logWarn(HOOK_NAME, `Error embedding ${indexFile}: ${e}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (embedded >= limit) break;
|
|
242
|
+
}
|
|
243
|
+
} finally {
|
|
244
|
+
db.close();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = { embedded, skipped, errors, total };
|
|
248
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
249
|
+
logInfo(HOOK_NAME, `Done: ${JSON.stringify(result)}`, { stderr: true });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Helpers
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Derive the source JSONL path from a ValidatedSessionIndex.
|
|
258
|
+
* The index stores source_mtime but not always the full path.
|
|
259
|
+
* Reconstruct from ~/.claude/projects/{project-slug}/{session_id}.jsonl
|
|
260
|
+
*/
|
|
261
|
+
function getSourcePath(index: ValidatedSessionIndex, _project: string): string {
|
|
262
|
+
// SessionIndex doesn't have a source_path field, but the searcher derives it.
|
|
263
|
+
// The JSONL files live under ~/.claude/projects/{encoded-project-path}/
|
|
264
|
+
// We need to find the actual file. Use the index's session_id to locate it.
|
|
265
|
+
const claudeProjectsDir = join(homedir(), ".claude", "projects");
|
|
266
|
+
|
|
267
|
+
// Search for the session file across all project dirs
|
|
268
|
+
try {
|
|
269
|
+
const projectDirs = readdirSync(claudeProjectsDir, { withFileTypes: true });
|
|
270
|
+
for (const dir of projectDirs) {
|
|
271
|
+
if (!dir.isDirectory()) continue;
|
|
272
|
+
const candidatePath = join(
|
|
273
|
+
claudeProjectsDir,
|
|
274
|
+
dir.name,
|
|
275
|
+
`${index.session_id}.jsonl`,
|
|
276
|
+
);
|
|
277
|
+
if (existsSync(candidatePath)) {
|
|
278
|
+
return candidatePath;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Fall through
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Fallback: best guess
|
|
286
|
+
return join(claudeProjectsDir, _project, `${index.session_id}.jsonl`);
|
|
287
|
+
}
|