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.
- 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 +232 -205
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.claude/{commands/handoff.md → skills/handoff/SKILL.md} +13 -12
- package/dist/templates/_shared/.claude/{commands/handoff-resume.md → skills/handoff-resume/SKILL.md} +13 -12
- 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 +15 -3
- package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
- package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
- package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
- package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
- package/dist/templates/_shared/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 -196
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
- 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 +24 -6
- 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 -202
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/CLAUDE.md +134 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
- 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 -186
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +687 -690
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/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/.claude/settings.json +3 -2
- 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/artifacts/CLAUDE.md +64 -0
- package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/format.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/write.ts +2 -2
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +14 -24
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +1 -1
- 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/hooks/validate_task_prompt.ts +76 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +9 -2
- 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/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +4 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- 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/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 -446
- 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 +1 -1
- package/dist/templates/cc-native/_cc-native/plan-review/CLAUDE.md +149 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/CLAUDE.md +143 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/PLAN-ORCHESTRATOR.md +213 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-EVOLUTION.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-PATTERNS.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ARCH-STRUCTURE.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/ASSUMPTION-TRACER.md +56 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CLARITY-AUDITOR.md +53 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-FEASIBILITY.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-GAPS.md +70 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/COMPLETENESS-ORDERING.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/CONSTRAINT-VALIDATOR.md +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-ADR-VALIDATOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DESIGN-SCALE-MATCHER.md +64 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DEVILS-ADVOCATE.md +56 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/DOCUMENTATION-PHILOSOPHY.md +86 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HANDOFF-READINESS.md +59 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/HIDDEN-COMPLEXITY.md +58 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/INCREMENTAL-DELIVERY.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-DEPENDENCY.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-FMEA.md +66 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-PREMORTEM.md +71 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/RISK-REVERSIBILITY.md +74 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SCOPE-BOUNDARY.md +77 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SIMPLICITY-GUARDIAN.md +62 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/SKEPTIC.md +68 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-CHARACTERIZATION.md +71 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-FIRST-VALIDATOR.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-COSTS.md +67 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/TRADEOFF-STAKEHOLDERS.md +65 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-COVERAGE.md +74 -0
- package/dist/templates/cc-native/_cc-native/plan-review/agents/plan-review/VERIFY-STRENGTH.md +69 -0
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/agent-selection.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/corroboration.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/graduation.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/orchestrator.ts +2 -2
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/output-builder.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/plan-questions.ts +6 -6
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/review-pipeline.ts +15 -15
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/agent.ts +5 -5
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/base/base-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/claude-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/codex-agent.ts +6 -6
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/gemini-agent.ts +1 -1
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/providers/orchestrator-claude-agent.ts +4 -4
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/types.ts +3 -3
- package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/verdict.ts +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +0 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
- /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/index.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts/artifacts → artifacts/lib}/tracker.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/index.ts +0 -0
- /package/dist/templates/cc-native/_cc-native/{lib-ts → plan-review/lib}/reviewers/schemas.ts +0 -0
- /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 };
|