aiwcli 0.12.6 → 0.12.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 (163) hide show
  1. package/bin/dev.cmd +3 -3
  2. package/bin/dev.js +16 -16
  3. package/bin/run.cmd +3 -3
  4. package/bin/run.js +21 -21
  5. package/dist/commands/branch.js +7 -2
  6. package/dist/lib/bmad-installer.js +37 -37
  7. package/dist/lib/terminal.d.ts +2 -0
  8. package/dist/lib/terminal.js +57 -7
  9. package/dist/templates/CLAUDE.md +232 -205
  10. package/dist/templates/_shared/.claude/settings.json +65 -65
  11. package/dist/templates/_shared/.claude/{commands/handoff.md → skills/handoff/SKILL.md} +13 -12
  12. package/dist/templates/_shared/.claude/{commands/handoff-resume.md → skills/handoff-resume/SKILL.md} +13 -12
  13. package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
  14. package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
  15. package/dist/templates/_shared/handoff-system/CLAUDE.md +15 -3
  16. package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
  17. package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
  18. package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
  19. package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
  20. package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
  21. package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
  22. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
  23. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
  24. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
  25. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
  26. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
  27. package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
  28. package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
  29. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
  30. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
  31. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
  32. package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
  33. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
  34. package/dist/templates/_shared/lib-ts/base/constants.ts +24 -6
  35. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
  36. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
  37. package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
  38. package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
  39. package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
  40. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
  41. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
  42. package/dist/templates/_shared/lib-ts/context/CLAUDE.md +134 -0
  43. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
  44. package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
  45. package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
  46. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
  47. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
  48. package/dist/templates/_shared/lib-ts/package.json +20 -20
  49. package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
  50. package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
  51. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
  52. package/dist/templates/_shared/lib-ts/types.ts +186 -186
  53. package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
  54. package/dist/templates/_shared/scripts/status_line.ts +687 -690
  55. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
  56. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
  57. package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
  58. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
  59. package/dist/templates/cc-native/.claude/settings.json +3 -2
  60. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
  61. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
  62. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
  63. package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
  64. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
  65. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
  66. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
  67. package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
  68. package/dist/templates/cc-native/_cc-native/artifacts/CLAUDE.md +64 -0
  69. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/format.ts +1 -1
  70. package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/write.ts +2 -2
  71. package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
  72. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +14 -24
  73. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1 -1
  74. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
  75. package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
  76. package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
  78. package/dist/templates/cc-native/_cc-native/hooks/validate_task_prompt.ts +76 -0
  79. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +9 -2
  80. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
  81. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
  82. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
  83. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
  84. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
  85. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +4 -4
  86. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
  87. package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
  88. package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
  89. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
  90. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
  91. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
  92. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
  93. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
  94. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
  95. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
  96. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
  97. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
  98. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
  99. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
  100. package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
  101. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
  102. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
  103. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
  104. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +1 -1
  105. package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +149 -0
  106. package/dist/templates/cc-native/_cc-native/plan-review/agents/CLAUDE.md +143 -0
  107. package/dist/templates/cc-native/_cc-native/plan-review/agents/PLAN-ORCHESTRATOR.md +213 -0
  108. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
  109. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-EVOLUTION.md +62 -0
  110. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-PATTERNS.md +61 -0
  111. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-STRUCTURE.md +62 -0
  112. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ASSUMPTION-TRACER.md +56 -0
  113. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CLARITY-AUDITOR.md +53 -0
  114. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -0
  115. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-GAPS.md +70 -0
  116. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-ORDERING.md +62 -0
  117. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -0
  118. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -0
  119. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -0
  120. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DEVILS-ADVOCATE.md +56 -0
  121. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -0
  122. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HANDOFF-READINESS.md +59 -0
  123. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -0
  124. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -0
  125. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-DEPENDENCY.md +62 -0
  126. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-FMEA.md +66 -0
  127. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-PREMORTEM.md +71 -0
  128. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-REVERSIBILITY.md +74 -0
  129. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SCOPE-BOUNDARY.md +77 -0
  130. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -0
  131. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SKEPTIC.md +68 -0
  132. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -0
  133. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -0
  134. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -0
  135. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -0
  136. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-COSTS.md +67 -0
  137. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -0
  138. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-COVERAGE.md +74 -0
  139. package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-STRENGTH.md +69 -0
  140. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/agent-selection.ts +3 -3
  141. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/corroboration.ts +1 -1
  142. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/graduation.ts +1 -1
  143. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/orchestrator.ts +2 -2
  144. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/output-builder.ts +3 -3
  145. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/plan-questions.ts +6 -6
  146. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/review-pipeline.ts +15 -15
  147. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/agent.ts +5 -5
  148. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/base/base-agent.ts +4 -4
  149. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/claude-agent.ts +4 -4
  150. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/codex-agent.ts +6 -6
  151. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/gemini-agent.ts +1 -1
  152. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/orchestrator-claude-agent.ts +4 -4
  153. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/types.ts +3 -3
  154. package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/verdict.ts +1 -1
  155. package/oclif.manifest.json +1 -1
  156. package/package.json +108 -108
  157. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +0 -21
  158. package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
  159. /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/index.ts +0 -0
  160. /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/tracker.ts +0 -0
  161. /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/index.ts +0 -0
  162. /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/schemas.ts +0 -0
  163. /package/dist/templates/cc-native/_cc-native/{workflows → plan-review/workflows}/specdev.md +0 -0
@@ -1,446 +1,446 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * TranscriptIndexer — Builds lightweight JSON indexes from Claude Code JSONL transcripts.
4
- *
5
- * Scans all project directories under ~/.claude/projects/, streams each .jsonl
6
- * file line-by-line, extracts metadata, and writes per-session index files to
7
- * ~/.claude/rlm-index/{project-slug}/{session_id}.index.json.
8
- *
9
- * Usage:
10
- * bun transcript-indexer.ts --batch # Index all sessions
11
- * bun transcript-indexer.ts --batch --limit=10 # Index first 10 unindexed
12
- * bun transcript-indexer.ts --batch --project=aiwcli # Index matching project only
13
- */
14
-
15
- import { readdir, stat, mkdir, readFile, writeFile } from "fs/promises";
16
- import { createReadStream, existsSync, readFileSync } from "fs";
17
- import { join, basename } from "path";
18
- import { createInterface } from "readline";
19
- import {
20
- CURRENT_SCHEMA_VERSION,
21
- CLAUDE_PROJECTS_DIR,
22
- RLM_INDEX_DIR,
23
- type SessionIndex,
24
- type IndexSegment,
25
- } from "./types.js";
26
- import { logInfo, logWarn, logError, logDebug } from "./logger.js";
27
-
28
- const HOOK_NAME = "rlm_indexer";
29
-
30
- // ---------------------------------------------------------------------------
31
- // CLI entry
32
- // ---------------------------------------------------------------------------
33
-
34
- const args = process.argv.slice(2);
35
- const isBatch = args.includes("--batch");
36
- const limitArg = args.find((a) => a.startsWith("--limit="));
37
- const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : Infinity;
38
- const projectArg = args.find((a) => a.startsWith("--project="));
39
- const projectFilter = projectArg ? projectArg.split("=")[1] : null;
40
-
41
- if (isBatch) {
42
- runBatch().catch((e) => {
43
- logError(HOOK_NAME, `Fatal: ${e}`, { stderr: true });
44
- process.exitCode = 1;
45
- });
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // Batch runner
50
- // ---------------------------------------------------------------------------
51
-
52
- interface SessionFile {
53
- project: string;
54
- sessionId: string;
55
- jsonlPath: string;
56
- }
57
-
58
- async function discoverSessions(): Promise<SessionFile[]> {
59
- const sessions: SessionFile[] = [];
60
- let projectDirs: string[];
61
- try {
62
- projectDirs = await readdir(CLAUDE_PROJECTS_DIR);
63
- } catch {
64
- logWarn(HOOK_NAME, `Cannot read ${CLAUDE_PROJECTS_DIR} — no Claude Code sessions found`);
65
- return sessions;
66
- }
67
-
68
- for (const project of projectDirs) {
69
- if (projectFilter && !project.toLowerCase().includes(projectFilter.toLowerCase())) {
70
- continue;
71
- }
72
- const projectPath = join(CLAUDE_PROJECTS_DIR, project);
73
- let entries: string[];
74
- try {
75
- entries = await readdir(projectPath);
76
- } catch {
77
- continue;
78
- }
79
- for (const entry of entries) {
80
- if (!entry.endsWith(".jsonl")) continue;
81
- const sessionId = basename(entry, ".jsonl");
82
- sessions.push({
83
- project,
84
- sessionId,
85
- jsonlPath: join(projectPath, entry),
86
- });
87
- }
88
- }
89
- return sessions;
90
- }
91
-
92
- function needsIndexing(session: SessionFile, sourceMtime: number): boolean {
93
- const indexPath = join(RLM_INDEX_DIR, session.project, `${session.sessionId}.index.json`);
94
- if (!existsSync(indexPath)) return true;
95
- try {
96
- // Fast path: Read only first 100 bytes to check schema_version
97
- // If version matches, skip without checking mtime (schema bumps trigger full reindex anyway)
98
- const fd = require("fs").openSync(indexPath, "r");
99
- const buffer = Buffer.alloc(100);
100
- const bytesRead = require("fs").readSync(fd, buffer, 0, 100, 0);
101
- require("fs").closeSync(fd);
102
-
103
- const partial = buffer.toString("utf-8", 0, bytesRead);
104
- const versionMatch = partial.match(/"schema_version"\s*:\s*(\d+)/);
105
-
106
- // If version matches, skip (no mtime check needed - schema version bump handles major changes)
107
- if (versionMatch && parseInt(versionMatch[1]) === CURRENT_SCHEMA_VERSION) {
108
- return false; // Skip - index is current
109
- }
110
-
111
- // Version mismatch or missing - needs reindex
112
- return true;
113
- } catch {
114
- return true;
115
- }
116
- }
117
-
118
- async function runBatch(): Promise<void> {
119
- const allSessions = await discoverSessions();
120
- const filterNote = projectFilter ? ` (filter: ${projectFilter})` : "";
121
- logInfo(HOOK_NAME, `Discovered ${allSessions.length} sessions${filterNote}`, { stderr: true });
122
-
123
- let indexed = 0;
124
- let skipped = 0;
125
- let errors = 0;
126
-
127
- for (const session of allSessions) {
128
- if (indexed >= limit) break;
129
-
130
- let mtime: number;
131
- try {
132
- const st = await stat(session.jsonlPath);
133
- mtime = st.mtimeMs;
134
- } catch {
135
- errors++;
136
- continue;
137
- }
138
-
139
- if (!needsIndexing(session, mtime)) {
140
- skipped++;
141
- continue;
142
- }
143
-
144
- try {
145
- const index = await indexSession(session, mtime);
146
- if (index.user_message_count === 0 && index.assistant_message_count === 0) {
147
- skipped++;
148
- continue;
149
- }
150
- await writeIndex(session.project, session.sessionId, index);
151
- indexed++;
152
- if (indexed % 10 === 0 || indexed === 1) {
153
- logInfo(HOOK_NAME, `Indexing: ${indexed} indexed, ${skipped} skipped, ${errors} errors (of ${allSessions.length} total)`, { stderr: true });
154
- }
155
- } catch (e) {
156
- errors++;
157
- logError(HOOK_NAME, `Error indexing ${session.sessionId}: ${e}`);
158
- }
159
- }
160
-
161
- logInfo(HOOK_NAME, `Done. Indexed: ${indexed}, Skipped: ${skipped}, Errors: ${errors}, Total: ${allSessions.length}`, { stderr: true });
162
-
163
- // Output JSON summary to stdout for programmatic consumption
164
- const summary = { indexed, skipped, errors, total: allSessions.length };
165
- process.stdout.write(JSON.stringify(summary) + "\n");
166
- }
167
-
168
- // ---------------------------------------------------------------------------
169
- // Single session indexer
170
- // ---------------------------------------------------------------------------
171
-
172
- async function indexSession(session: SessionFile, sourceMtime: number): Promise<SessionIndex> {
173
- const index: SessionIndex = {
174
- schema_version: CURRENT_SCHEMA_VERSION,
175
- session_id: session.sessionId,
176
- project: session.project,
177
- date: "",
178
- first_timestamp: "",
179
- line_count: 0,
180
- summary: "",
181
- keywords: [],
182
- user_message_count: 0,
183
- assistant_message_count: 0,
184
- tool_calls: [],
185
- files_touched: [],
186
- commands_run: [],
187
- source_mtime: sourceMtime,
188
- skipped_lines: 0,
189
- segments: [],
190
- };
191
-
192
- const toolCallSet = new Set<string>();
193
- const fileTouchedSet = new Set<string>();
194
- const commandSet = new Set<string>();
195
- const keywordBag = new Map<string, number>();
196
- const userSnippets: string[] = [];
197
-
198
- // Segment tracking
199
- let currentSegmentStart = 1;
200
- let segmentKeywordBag = new Map<string, number>(); // per-segment, reset each boundary
201
- let lastUserMessage = "";
202
- const SEGMENT_SIZE = 50; // lines per segment
203
-
204
- const rl = createInterface({
205
- input: createReadStream(session.jsonlPath, { encoding: "utf-8" }),
206
- crlfDelay: Infinity,
207
- });
208
-
209
- let lineNum = 0;
210
-
211
- try {
212
- for await (const line of rl) {
213
- lineNum++;
214
- if (!line.trim()) continue;
215
-
216
- let obj: Record<string, unknown>;
217
- try {
218
- obj = JSON.parse(line);
219
- } catch {
220
- index.skipped_lines++;
221
- continue;
222
- }
223
-
224
- const type = obj.type as string | undefined;
225
- const timestamp = obj.timestamp as string | undefined;
226
-
227
- // Capture first timestamp
228
- if (timestamp && !index.first_timestamp) {
229
- index.first_timestamp = timestamp;
230
- index.date = timestamp.slice(0, 10); // YYYY-MM-DD
231
- }
232
-
233
- if (type === "user") {
234
- index.user_message_count++;
235
- const msg = obj.message as Record<string, unknown> | undefined;
236
- if (msg) {
237
- const content = msg.content;
238
- if (typeof content === "string") {
239
- const snippet = content.slice(0, 200);
240
- userSnippets.push(snippet);
241
- lastUserMessage = snippet;
242
- extractKeywords(content, keywordBag);
243
- extractKeywords(content, segmentKeywordBag);
244
- } else if (Array.isArray(content)) {
245
- for (const block of content) {
246
- if (typeof block === "object" && block !== null && "text" in block) {
247
- const text = (block as Record<string, unknown>).text;
248
- if (typeof text === "string") {
249
- extractKeywords(text, keywordBag);
250
- extractKeywords(text, segmentKeywordBag);
251
- }
252
- }
253
- }
254
- }
255
- // Extract cwd for context
256
- const cwd = obj.cwd as string | undefined;
257
- if (cwd) {
258
- const parts = cwd.replace(/\\/g, "/").split("/");
259
- const last = parts[parts.length - 1];
260
- if (last) addKeyword(keywordBag, last);
261
- }
262
- // Git branch
263
- const branch = obj.gitBranch as string | undefined;
264
- if (branch && branch !== "master" && branch !== "main") {
265
- addKeyword(keywordBag, branch);
266
- }
267
- }
268
- } else if (type === "assistant") {
269
- index.assistant_message_count++;
270
- const msg = obj.message as Record<string, unknown> | undefined;
271
- if (msg) {
272
- const content = msg.content;
273
- if (Array.isArray(content)) {
274
- for (const block of content) {
275
- if (typeof block !== "object" || block === null) continue;
276
- const b = block as Record<string, unknown>;
277
- if (b.type === "tool_use") {
278
- const toolName = b.name as string;
279
- if (toolName) toolCallSet.add(toolName);
280
- // Extract file paths and commands from tool inputs
281
- const input = b.input as Record<string, unknown> | undefined;
282
- if (input) {
283
- extractToolMetadata(toolName, input, fileTouchedSet, commandSet, keywordBag);
284
- extractToolMetadata(toolName, input, new Set(), new Set(), segmentKeywordBag);
285
- }
286
- }
287
- if (b.type === "text" && typeof b.text === "string") {
288
- extractKeywords(b.text, keywordBag);
289
- extractKeywords(b.text, segmentKeywordBag);
290
- }
291
- }
292
- }
293
- }
294
- }
295
-
296
- // Build segments every SEGMENT_SIZE lines
297
- if (lineNum % SEGMENT_SIZE === 0) {
298
- if (segmentKeywordBag.size > 0 || lastUserMessage) {
299
- index.segments.push({
300
- lines: [currentSegmentStart, lineNum],
301
- topic: lastUserMessage.slice(0, 100) || "continued work",
302
- keywords: getTopKeywords(segmentKeywordBag, 10),
303
- });
304
- }
305
- currentSegmentStart = lineNum + 1;
306
- segmentKeywordBag = new Map<string, number>();
307
- }
308
- }
309
- } finally {
310
- rl.close();
311
- }
312
-
313
- // Final segment
314
- if (lineNum >= currentSegmentStart) {
315
- index.segments.push({
316
- lines: [currentSegmentStart, lineNum],
317
- topic: lastUserMessage.slice(0, 100) || "session end",
318
- keywords: getTopKeywords(segmentKeywordBag, 10),
319
- });
320
- }
321
-
322
- index.line_count = lineNum;
323
- index.tool_calls = [...toolCallSet].sort();
324
- index.files_touched = [...fileTouchedSet].slice(0, 50);
325
- index.commands_run = [...commandSet].slice(0, 30);
326
- index.keywords = getTopKeywords(keywordBag, 30);
327
-
328
- // Summary: first user message or top keywords
329
- if (userSnippets.length > 0) {
330
- index.summary = userSnippets[0].slice(0, 200);
331
- } else {
332
- index.summary = index.keywords.slice(0, 5).join(", ") || "empty session";
333
- }
334
-
335
- return index;
336
- }
337
-
338
- // ---------------------------------------------------------------------------
339
- // Keyword extraction
340
- // ---------------------------------------------------------------------------
341
-
342
- const STOP_WORDS = new Set([
343
- "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
344
- "have", "has", "had", "do", "does", "did", "will", "would", "shall",
345
- "should", "may", "might", "must", "can", "could", "of", "in", "to",
346
- "for", "with", "on", "at", "from", "by", "about", "as", "into",
347
- "through", "during", "before", "after", "above", "below", "between",
348
- "and", "but", "or", "nor", "not", "no", "so", "yet", "both", "either",
349
- "neither", "each", "every", "all", "any", "few", "more", "most", "other",
350
- "some", "such", "than", "too", "very", "just", "also", "now", "then",
351
- "here", "there", "when", "where", "why", "how", "what", "which", "who",
352
- "this", "that", "these", "those", "it", "its", "i", "me", "my", "we",
353
- "our", "you", "your", "he", "she", "they", "them", "their", "if",
354
- "true", "false", "null", "undefined", "function", "return", "const",
355
- "let", "var", "import", "export", "default", "class", "new", "type",
356
- ]);
357
-
358
- function extractKeywords(text: string, bag: Map<string, number>): void {
359
- // Extract meaningful words (3+ chars, not stop words)
360
- const words = text.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g);
361
- if (!words) return;
362
- for (const w of words) {
363
- if (STOP_WORDS.has(w)) continue;
364
- if (w.length > 40) continue; // skip hashes/encoded strings
365
- addKeyword(bag, w);
366
- }
367
- }
368
-
369
- function addKeyword(bag: Map<string, number>, word: string): void {
370
- bag.set(word, (bag.get(word) || 0) + 1);
371
- }
372
-
373
- function getTopKeywords(bag: Map<string, number>, n: number): string[] {
374
- return [...bag.entries()]
375
- .sort((a, b) => b[1] - a[1])
376
- .slice(0, n)
377
- .map(([word]) => word);
378
- }
379
-
380
- // ---------------------------------------------------------------------------
381
- // Tool metadata extraction
382
- // ---------------------------------------------------------------------------
383
-
384
- function extractToolMetadata(
385
- toolName: string,
386
- input: Record<string, unknown>,
387
- files: Set<string>,
388
- commands: Set<string>,
389
- keywords: Map<string, number>,
390
- ): void {
391
- // File paths from Read, Edit, Write, Glob
392
- const filePath = input.file_path as string | undefined;
393
- if (filePath) {
394
- const normalized = filePath.replace(/\\/g, "/");
395
- const parts = normalized.split("/");
396
- const fileName = parts[parts.length - 1];
397
- if (fileName) {
398
- files.add(fileName);
399
- addKeyword(keywords, fileName.replace(/\.[^.]+$/, "")); // stem
400
- }
401
- }
402
-
403
- // Glob patterns
404
- const pattern = input.pattern as string | undefined;
405
- if (pattern && toolName === "Glob") {
406
- extractKeywords(pattern, keywords);
407
- }
408
-
409
- // Grep patterns
410
- if (toolName === "Grep") {
411
- const grepPattern = input.pattern as string | undefined;
412
- if (grepPattern) extractKeywords(grepPattern, keywords);
413
- }
414
-
415
- // Bash commands
416
- const command = input.command as string | undefined;
417
- if (command && toolName === "Bash") {
418
- // Keep first 100 chars of command
419
- commands.add(command.slice(0, 100));
420
- extractKeywords(command, keywords);
421
- }
422
-
423
- // Task/subagent descriptions
424
- const description = input.description as string | undefined;
425
- if (description) extractKeywords(description, keywords);
426
-
427
- const prompt = input.prompt as string | undefined;
428
- if (prompt) extractKeywords(prompt.slice(0, 500), keywords);
429
- }
430
-
431
- // ---------------------------------------------------------------------------
432
- // Index I/O
433
- // ---------------------------------------------------------------------------
434
-
435
- async function writeIndex(project: string, sessionId: string, index: SessionIndex): Promise<void> {
436
- const dir = join(RLM_INDEX_DIR, project);
437
- await mkdir(dir, { recursive: true });
438
- const indexPath = join(dir, `${sessionId}.index.json`);
439
- await writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8");
440
- }
441
-
442
- // ---------------------------------------------------------------------------
443
- // Exports for programmatic use
444
- // ---------------------------------------------------------------------------
445
-
446
- export { discoverSessions, indexSession, writeIndex, needsIndexing, runBatch };
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * TranscriptIndexer — Builds lightweight JSON indexes from Claude Code JSONL transcripts.
4
+ *
5
+ * Scans all project directories under ~/.claude/projects/, streams each .jsonl
6
+ * file line-by-line, extracts metadata, and writes per-session index files to
7
+ * ~/.claude/rlm-index/{project-slug}/{session_id}.index.json.
8
+ *
9
+ * Usage:
10
+ * bun transcript-indexer.ts --batch # Index all sessions
11
+ * bun transcript-indexer.ts --batch --limit=10 # Index first 10 unindexed
12
+ * bun transcript-indexer.ts --batch --project=aiwcli # Index matching project only
13
+ */
14
+
15
+ import { readdir, stat, mkdir, readFile, writeFile } from "fs/promises";
16
+ import { createReadStream, existsSync, readFileSync } from "fs";
17
+ import { join, basename } from "path";
18
+ import { createInterface } from "readline";
19
+ import {
20
+ CURRENT_SCHEMA_VERSION,
21
+ CLAUDE_PROJECTS_DIR,
22
+ RLM_INDEX_DIR,
23
+ type SessionIndex,
24
+ type IndexSegment,
25
+ } from "./types.js";
26
+ import { logInfo, logWarn, logError, logDebug } from "./logger.js";
27
+
28
+ const HOOK_NAME = "rlm_indexer";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // CLI entry
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const args = process.argv.slice(2);
35
+ const isBatch = args.includes("--batch");
36
+ const limitArg = args.find((a) => a.startsWith("--limit="));
37
+ const limit = limitArg ? parseInt(limitArg.split("=")[1], 10) : Infinity;
38
+ const projectArg = args.find((a) => a.startsWith("--project="));
39
+ const projectFilter = projectArg ? projectArg.split("=")[1] : null;
40
+
41
+ if (isBatch) {
42
+ runBatch().catch((e) => {
43
+ logError(HOOK_NAME, `Fatal: ${e}`, { stderr: true });
44
+ process.exitCode = 1;
45
+ });
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Batch runner
50
+ // ---------------------------------------------------------------------------
51
+
52
+ interface SessionFile {
53
+ project: string;
54
+ sessionId: string;
55
+ jsonlPath: string;
56
+ }
57
+
58
+ async function discoverSessions(): Promise<SessionFile[]> {
59
+ const sessions: SessionFile[] = [];
60
+ let projectDirs: string[];
61
+ try {
62
+ projectDirs = await readdir(CLAUDE_PROJECTS_DIR);
63
+ } catch {
64
+ logWarn(HOOK_NAME, `Cannot read ${CLAUDE_PROJECTS_DIR} — no Claude Code sessions found`);
65
+ return sessions;
66
+ }
67
+
68
+ for (const project of projectDirs) {
69
+ if (projectFilter && !project.toLowerCase().includes(projectFilter.toLowerCase())) {
70
+ continue;
71
+ }
72
+ const projectPath = join(CLAUDE_PROJECTS_DIR, project);
73
+ let entries: string[];
74
+ try {
75
+ entries = await readdir(projectPath);
76
+ } catch {
77
+ continue;
78
+ }
79
+ for (const entry of entries) {
80
+ if (!entry.endsWith(".jsonl")) continue;
81
+ const sessionId = basename(entry, ".jsonl");
82
+ sessions.push({
83
+ project,
84
+ sessionId,
85
+ jsonlPath: join(projectPath, entry),
86
+ });
87
+ }
88
+ }
89
+ return sessions;
90
+ }
91
+
92
+ function needsIndexing(session: SessionFile, sourceMtime: number): boolean {
93
+ const indexPath = join(RLM_INDEX_DIR, session.project, `${session.sessionId}.index.json`);
94
+ if (!existsSync(indexPath)) return true;
95
+ try {
96
+ // Fast path: Read only first 100 bytes to check schema_version
97
+ // If version matches, skip without checking mtime (schema bumps trigger full reindex anyway)
98
+ const fd = require("fs").openSync(indexPath, "r");
99
+ const buffer = Buffer.alloc(100);
100
+ const bytesRead = require("fs").readSync(fd, buffer, 0, 100, 0);
101
+ require("fs").closeSync(fd);
102
+
103
+ const partial = buffer.toString("utf-8", 0, bytesRead);
104
+ const versionMatch = partial.match(/"schema_version"\s*:\s*(\d+)/);
105
+
106
+ // If version matches, skip (no mtime check needed - schema version bump handles major changes)
107
+ if (versionMatch && parseInt(versionMatch[1]) === CURRENT_SCHEMA_VERSION) {
108
+ return false; // Skip - index is current
109
+ }
110
+
111
+ // Version mismatch or missing - needs reindex
112
+ return true;
113
+ } catch {
114
+ return true;
115
+ }
116
+ }
117
+
118
+ async function runBatch(): Promise<void> {
119
+ const allSessions = await discoverSessions();
120
+ const filterNote = projectFilter ? ` (filter: ${projectFilter})` : "";
121
+ logInfo(HOOK_NAME, `Discovered ${allSessions.length} sessions${filterNote}`, { stderr: true });
122
+
123
+ let indexed = 0;
124
+ let skipped = 0;
125
+ let errors = 0;
126
+
127
+ for (const session of allSessions) {
128
+ if (indexed >= limit) break;
129
+
130
+ let mtime: number;
131
+ try {
132
+ const st = await stat(session.jsonlPath);
133
+ mtime = st.mtimeMs;
134
+ } catch {
135
+ errors++;
136
+ continue;
137
+ }
138
+
139
+ if (!needsIndexing(session, mtime)) {
140
+ skipped++;
141
+ continue;
142
+ }
143
+
144
+ try {
145
+ const index = await indexSession(session, mtime);
146
+ if (index.user_message_count === 0 && index.assistant_message_count === 0) {
147
+ skipped++;
148
+ continue;
149
+ }
150
+ await writeIndex(session.project, session.sessionId, index);
151
+ indexed++;
152
+ if (indexed % 10 === 0 || indexed === 1) {
153
+ logInfo(HOOK_NAME, `Indexing: ${indexed} indexed, ${skipped} skipped, ${errors} errors (of ${allSessions.length} total)`, { stderr: true });
154
+ }
155
+ } catch (e) {
156
+ errors++;
157
+ logError(HOOK_NAME, `Error indexing ${session.sessionId}: ${e}`);
158
+ }
159
+ }
160
+
161
+ logInfo(HOOK_NAME, `Done. Indexed: ${indexed}, Skipped: ${skipped}, Errors: ${errors}, Total: ${allSessions.length}`, { stderr: true });
162
+
163
+ // Output JSON summary to stdout for programmatic consumption
164
+ const summary = { indexed, skipped, errors, total: allSessions.length };
165
+ process.stdout.write(JSON.stringify(summary) + "\n");
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Single session indexer
170
+ // ---------------------------------------------------------------------------
171
+
172
+ async function indexSession(session: SessionFile, sourceMtime: number): Promise<SessionIndex> {
173
+ const index: SessionIndex = {
174
+ schema_version: CURRENT_SCHEMA_VERSION,
175
+ session_id: session.sessionId,
176
+ project: session.project,
177
+ date: "",
178
+ first_timestamp: "",
179
+ line_count: 0,
180
+ summary: "",
181
+ keywords: [],
182
+ user_message_count: 0,
183
+ assistant_message_count: 0,
184
+ tool_calls: [],
185
+ files_touched: [],
186
+ commands_run: [],
187
+ source_mtime: sourceMtime,
188
+ skipped_lines: 0,
189
+ segments: [],
190
+ };
191
+
192
+ const toolCallSet = new Set<string>();
193
+ const fileTouchedSet = new Set<string>();
194
+ const commandSet = new Set<string>();
195
+ const keywordBag = new Map<string, number>();
196
+ const userSnippets: string[] = [];
197
+
198
+ // Segment tracking
199
+ let currentSegmentStart = 1;
200
+ let segmentKeywordBag = new Map<string, number>(); // per-segment, reset each boundary
201
+ let lastUserMessage = "";
202
+ const SEGMENT_SIZE = 50; // lines per segment
203
+
204
+ const rl = createInterface({
205
+ input: createReadStream(session.jsonlPath, { encoding: "utf-8" }),
206
+ crlfDelay: Infinity,
207
+ });
208
+
209
+ let lineNum = 0;
210
+
211
+ try {
212
+ for await (const line of rl) {
213
+ lineNum++;
214
+ if (!line.trim()) continue;
215
+
216
+ let obj: Record<string, unknown>;
217
+ try {
218
+ obj = JSON.parse(line);
219
+ } catch {
220
+ index.skipped_lines++;
221
+ continue;
222
+ }
223
+
224
+ const type = obj.type as string | undefined;
225
+ const timestamp = obj.timestamp as string | undefined;
226
+
227
+ // Capture first timestamp
228
+ if (timestamp && !index.first_timestamp) {
229
+ index.first_timestamp = timestamp;
230
+ index.date = timestamp.slice(0, 10); // YYYY-MM-DD
231
+ }
232
+
233
+ if (type === "user") {
234
+ index.user_message_count++;
235
+ const msg = obj.message as Record<string, unknown> | undefined;
236
+ if (msg) {
237
+ const content = msg.content;
238
+ if (typeof content === "string") {
239
+ const snippet = content.slice(0, 200);
240
+ userSnippets.push(snippet);
241
+ lastUserMessage = snippet;
242
+ extractKeywords(content, keywordBag);
243
+ extractKeywords(content, segmentKeywordBag);
244
+ } else if (Array.isArray(content)) {
245
+ for (const block of content) {
246
+ if (typeof block === "object" && block !== null && "text" in block) {
247
+ const text = (block as Record<string, unknown>).text;
248
+ if (typeof text === "string") {
249
+ extractKeywords(text, keywordBag);
250
+ extractKeywords(text, segmentKeywordBag);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ // Extract cwd for context
256
+ const cwd = obj.cwd as string | undefined;
257
+ if (cwd) {
258
+ const parts = cwd.replace(/\\/g, "/").split("/");
259
+ const last = parts[parts.length - 1];
260
+ if (last) addKeyword(keywordBag, last);
261
+ }
262
+ // Git branch
263
+ const branch = obj.gitBranch as string | undefined;
264
+ if (branch && branch !== "master" && branch !== "main") {
265
+ addKeyword(keywordBag, branch);
266
+ }
267
+ }
268
+ } else if (type === "assistant") {
269
+ index.assistant_message_count++;
270
+ const msg = obj.message as Record<string, unknown> | undefined;
271
+ if (msg) {
272
+ const content = msg.content;
273
+ if (Array.isArray(content)) {
274
+ for (const block of content) {
275
+ if (typeof block !== "object" || block === null) continue;
276
+ const b = block as Record<string, unknown>;
277
+ if (b.type === "tool_use") {
278
+ const toolName = b.name as string;
279
+ if (toolName) toolCallSet.add(toolName);
280
+ // Extract file paths and commands from tool inputs
281
+ const input = b.input as Record<string, unknown> | undefined;
282
+ if (input) {
283
+ extractToolMetadata(toolName, input, fileTouchedSet, commandSet, keywordBag);
284
+ extractToolMetadata(toolName, input, new Set(), new Set(), segmentKeywordBag);
285
+ }
286
+ }
287
+ if (b.type === "text" && typeof b.text === "string") {
288
+ extractKeywords(b.text, keywordBag);
289
+ extractKeywords(b.text, segmentKeywordBag);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // Build segments every SEGMENT_SIZE lines
297
+ if (lineNum % SEGMENT_SIZE === 0) {
298
+ if (segmentKeywordBag.size > 0 || lastUserMessage) {
299
+ index.segments.push({
300
+ lines: [currentSegmentStart, lineNum],
301
+ topic: lastUserMessage.slice(0, 100) || "continued work",
302
+ keywords: getTopKeywords(segmentKeywordBag, 10),
303
+ });
304
+ }
305
+ currentSegmentStart = lineNum + 1;
306
+ segmentKeywordBag = new Map<string, number>();
307
+ }
308
+ }
309
+ } finally {
310
+ rl.close();
311
+ }
312
+
313
+ // Final segment
314
+ if (lineNum >= currentSegmentStart) {
315
+ index.segments.push({
316
+ lines: [currentSegmentStart, lineNum],
317
+ topic: lastUserMessage.slice(0, 100) || "session end",
318
+ keywords: getTopKeywords(segmentKeywordBag, 10),
319
+ });
320
+ }
321
+
322
+ index.line_count = lineNum;
323
+ index.tool_calls = [...toolCallSet].sort();
324
+ index.files_touched = [...fileTouchedSet].slice(0, 50);
325
+ index.commands_run = [...commandSet].slice(0, 30);
326
+ index.keywords = getTopKeywords(keywordBag, 30);
327
+
328
+ // Summary: first user message or top keywords
329
+ if (userSnippets.length > 0) {
330
+ index.summary = userSnippets[0].slice(0, 200);
331
+ } else {
332
+ index.summary = index.keywords.slice(0, 5).join(", ") || "empty session";
333
+ }
334
+
335
+ return index;
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Keyword extraction
340
+ // ---------------------------------------------------------------------------
341
+
342
+ const STOP_WORDS = new Set([
343
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
344
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
345
+ "should", "may", "might", "must", "can", "could", "of", "in", "to",
346
+ "for", "with", "on", "at", "from", "by", "about", "as", "into",
347
+ "through", "during", "before", "after", "above", "below", "between",
348
+ "and", "but", "or", "nor", "not", "no", "so", "yet", "both", "either",
349
+ "neither", "each", "every", "all", "any", "few", "more", "most", "other",
350
+ "some", "such", "than", "too", "very", "just", "also", "now", "then",
351
+ "here", "there", "when", "where", "why", "how", "what", "which", "who",
352
+ "this", "that", "these", "those", "it", "its", "i", "me", "my", "we",
353
+ "our", "you", "your", "he", "she", "they", "them", "their", "if",
354
+ "true", "false", "null", "undefined", "function", "return", "const",
355
+ "let", "var", "import", "export", "default", "class", "new", "type",
356
+ ]);
357
+
358
+ function extractKeywords(text: string, bag: Map<string, number>): void {
359
+ // Extract meaningful words (3+ chars, not stop words)
360
+ const words = text.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g);
361
+ if (!words) return;
362
+ for (const w of words) {
363
+ if (STOP_WORDS.has(w)) continue;
364
+ if (w.length > 40) continue; // skip hashes/encoded strings
365
+ addKeyword(bag, w);
366
+ }
367
+ }
368
+
369
+ function addKeyword(bag: Map<string, number>, word: string): void {
370
+ bag.set(word, (bag.get(word) || 0) + 1);
371
+ }
372
+
373
+ function getTopKeywords(bag: Map<string, number>, n: number): string[] {
374
+ return [...bag.entries()]
375
+ .sort((a, b) => b[1] - a[1])
376
+ .slice(0, n)
377
+ .map(([word]) => word);
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Tool metadata extraction
382
+ // ---------------------------------------------------------------------------
383
+
384
+ function extractToolMetadata(
385
+ toolName: string,
386
+ input: Record<string, unknown>,
387
+ files: Set<string>,
388
+ commands: Set<string>,
389
+ keywords: Map<string, number>,
390
+ ): void {
391
+ // File paths from Read, Edit, Write, Glob
392
+ const filePath = input.file_path as string | undefined;
393
+ if (filePath) {
394
+ const normalized = filePath.replace(/\\/g, "/");
395
+ const parts = normalized.split("/");
396
+ const fileName = parts[parts.length - 1];
397
+ if (fileName) {
398
+ files.add(fileName);
399
+ addKeyword(keywords, fileName.replace(/\.[^.]+$/, "")); // stem
400
+ }
401
+ }
402
+
403
+ // Glob patterns
404
+ const pattern = input.pattern as string | undefined;
405
+ if (pattern && toolName === "Glob") {
406
+ extractKeywords(pattern, keywords);
407
+ }
408
+
409
+ // Grep patterns
410
+ if (toolName === "Grep") {
411
+ const grepPattern = input.pattern as string | undefined;
412
+ if (grepPattern) extractKeywords(grepPattern, keywords);
413
+ }
414
+
415
+ // Bash commands
416
+ const command = input.command as string | undefined;
417
+ if (command && toolName === "Bash") {
418
+ // Keep first 100 chars of command
419
+ commands.add(command.slice(0, 100));
420
+ extractKeywords(command, keywords);
421
+ }
422
+
423
+ // Task/subagent descriptions
424
+ const description = input.description as string | undefined;
425
+ if (description) extractKeywords(description, keywords);
426
+
427
+ const prompt = input.prompt as string | undefined;
428
+ if (prompt) extractKeywords(prompt.slice(0, 500), keywords);
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // Index I/O
433
+ // ---------------------------------------------------------------------------
434
+
435
+ async function writeIndex(project: string, sessionId: string, index: SessionIndex): Promise<void> {
436
+ const dir = join(RLM_INDEX_DIR, project);
437
+ await mkdir(dir, { recursive: true });
438
+ const indexPath = join(dir, `${sessionId}.index.json`);
439
+ await writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8");
440
+ }
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // Exports for programmatic use
444
+ // ---------------------------------------------------------------------------
445
+
446
+ export { discoverSessions, indexSession, writeIndex, needsIndexing, runBatch };