selftune 0.1.4 → 0.2.1
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/.claude/agents/diagnosis-analyst.md +156 -0
- package/.claude/agents/evolution-reviewer.md +180 -0
- package/.claude/agents/integration-guide.md +212 -0
- package/.claude/agents/pattern-analyst.md +160 -0
- package/CHANGELOG.md +46 -1
- package/README.md +105 -257
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +99 -0
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +103 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +1049 -0
- package/cli/selftune/dashboard.ts +43 -156
- package/cli/selftune/eval/baseline.ts +248 -0
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +101 -16
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +586 -0
- package/cli/selftune/evolution/evolve.ts +825 -116
- package/cli/selftune/evolution/extract-patterns.ts +105 -16
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +21 -4
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +513 -42
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +415 -48
- package/cli/selftune/ingestors/claude-replay.ts +377 -0
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +376 -16
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +90 -16
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +59 -33
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +525 -5
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +103 -19
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +307 -26
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +28 -11
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +180 -33
- package/skill/Workflows/AutoActivation.md +145 -0
- package/skill/Workflows/Badge.md +124 -0
- package/skill/Workflows/Baseline.md +144 -0
- package/skill/Workflows/Composability.md +107 -0
- package/skill/Workflows/Contribute.md +94 -0
- package/skill/Workflows/Cron.md +132 -0
- package/skill/Workflows/Dashboard.md +214 -0
- package/skill/Workflows/Doctor.md +63 -14
- package/skill/Workflows/Evals.md +110 -18
- package/skill/Workflows/EvolutionMemory.md +154 -0
- package/skill/Workflows/Evolve.md +181 -21
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +117 -0
- package/skill/Workflows/Ingest.md +142 -21
- package/skill/Workflows/Initialize.md +91 -23
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +91 -0
- package/skill/Workflows/Rollback.md +23 -4
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +150 -0
- package/skill/Workflows/Watch.md +33 -1
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
- package/dashboard/index.html +0 -1119
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code transcript ingestor: claude-replay.ts
|
|
4
|
+
*
|
|
5
|
+
* Retroactively ingests Claude Code session transcripts into our shared
|
|
6
|
+
* skill eval log format.
|
|
7
|
+
*
|
|
8
|
+
* Claude Code saves transcripts to:
|
|
9
|
+
* ~/.claude/projects/<hash>/<session-id>.jsonl
|
|
10
|
+
*
|
|
11
|
+
* This script scans those files and populates:
|
|
12
|
+
* ~/.claude/all_queries_log.jsonl
|
|
13
|
+
* ~/.claude/session_telemetry_log.jsonl
|
|
14
|
+
* ~/.claude/skill_usage_log.jsonl
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* bun claude-replay.ts
|
|
18
|
+
* bun claude-replay.ts --since 2026-01-01
|
|
19
|
+
* bun claude-replay.ts --projects-dir /custom/path
|
|
20
|
+
* bun claude-replay.ts --dry-run
|
|
21
|
+
* bun claude-replay.ts --force
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { statSync } from "node:fs";
|
|
25
|
+
import { basename } from "node:path";
|
|
26
|
+
import { parseArgs } from "node:util";
|
|
27
|
+
import {
|
|
28
|
+
CANONICAL_LOG,
|
|
29
|
+
CLAUDE_CODE_MARKER,
|
|
30
|
+
CLAUDE_CODE_PROJECTS_DIR,
|
|
31
|
+
QUERY_LOG,
|
|
32
|
+
SKILL_LOG,
|
|
33
|
+
TELEMETRY_LOG,
|
|
34
|
+
} from "../constants.js";
|
|
35
|
+
import {
|
|
36
|
+
appendCanonicalRecords,
|
|
37
|
+
buildCanonicalExecutionFact,
|
|
38
|
+
buildCanonicalPrompt,
|
|
39
|
+
buildCanonicalSession,
|
|
40
|
+
buildCanonicalSkillInvocation,
|
|
41
|
+
type CanonicalBaseInput,
|
|
42
|
+
deriveInvocationMode,
|
|
43
|
+
derivePromptId,
|
|
44
|
+
deriveSkillInvocationId,
|
|
45
|
+
} from "../normalization.js";
|
|
46
|
+
import type {
|
|
47
|
+
CanonicalRecord,
|
|
48
|
+
QueryLogRecord,
|
|
49
|
+
SessionTelemetryRecord,
|
|
50
|
+
SkillUsageRecord,
|
|
51
|
+
TranscriptMetrics,
|
|
52
|
+
} from "../types.js";
|
|
53
|
+
import { appendJsonl, loadMarker, saveMarker } from "../utils/jsonl.js";
|
|
54
|
+
import { isActionableQueryText } from "../utils/query-filter.js";
|
|
55
|
+
import {
|
|
56
|
+
extractActionableUserQueries,
|
|
57
|
+
findTranscriptFiles as findTranscriptFilesShared,
|
|
58
|
+
parseTranscript,
|
|
59
|
+
} from "../utils/transcript.js";
|
|
60
|
+
|
|
61
|
+
export interface ParsedSession {
|
|
62
|
+
transcript_path: string;
|
|
63
|
+
session_id: string;
|
|
64
|
+
timestamp: string;
|
|
65
|
+
metrics: TranscriptMetrics;
|
|
66
|
+
user_queries: Array<{ query: string; timestamp: string }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find all .jsonl transcript files under the Claude projects tree.
|
|
71
|
+
*
|
|
72
|
+
* Claude stores the main session transcript at:
|
|
73
|
+
* projects/<hash>/<session>.jsonl
|
|
74
|
+
*
|
|
75
|
+
* But newer sessions and agent-sidechains may also write nested transcripts such as:
|
|
76
|
+
* projects/<hash>/subagents/<agent>.jsonl
|
|
77
|
+
*
|
|
78
|
+
* We scan recursively so replay, repair, and canonical export see the full source-of-truth
|
|
79
|
+
* transcript set instead of only top-level sessions.
|
|
80
|
+
*/
|
|
81
|
+
export function findTranscriptFiles(projectsDir: string, since?: Date): string[] {
|
|
82
|
+
return findTranscriptFilesShared(projectsDir, since);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract all user queries from a Claude Code transcript JSONL.
|
|
87
|
+
*
|
|
88
|
+
* Handles two transcript variants:
|
|
89
|
+
* Variant A: {"type": "user", "message": {"role": "user", "content": [...]}}
|
|
90
|
+
* Variant B: {"role": "user", "content": "..."}
|
|
91
|
+
*
|
|
92
|
+
* Filters out non-user/meta payloads and queries < 4 chars.
|
|
93
|
+
*/
|
|
94
|
+
export function extractAllUserQueries(
|
|
95
|
+
transcriptPath: string,
|
|
96
|
+
): Array<{ query: string; timestamp: string }> {
|
|
97
|
+
return extractActionableUserQueries(transcriptPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a Claude Code session transcript into a ParsedSession.
|
|
102
|
+
* Returns null if no user queries are found after filtering.
|
|
103
|
+
*/
|
|
104
|
+
export function parseSession(transcriptPath: string): ParsedSession | null {
|
|
105
|
+
const metrics = parseTranscript(transcriptPath);
|
|
106
|
+
const userQueries = extractAllUserQueries(transcriptPath);
|
|
107
|
+
|
|
108
|
+
if (userQueries.length === 0) return null;
|
|
109
|
+
|
|
110
|
+
const sessionId = basename(transcriptPath, ".jsonl");
|
|
111
|
+
|
|
112
|
+
// Determine timestamp: use first query's timestamp, or file mtime as fallback
|
|
113
|
+
let timestamp = userQueries[0].timestamp;
|
|
114
|
+
if (!timestamp) {
|
|
115
|
+
try {
|
|
116
|
+
timestamp = statSync(transcriptPath).mtime.toISOString();
|
|
117
|
+
} catch {
|
|
118
|
+
timestamp = new Date().toISOString();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
transcript_path: transcriptPath,
|
|
124
|
+
session_id: sessionId,
|
|
125
|
+
timestamp,
|
|
126
|
+
metrics,
|
|
127
|
+
user_queries: userQueries,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write parsed session data to shared JSONL logs.
|
|
133
|
+
* Writes ONE QueryLogRecord per user query, ONE SessionTelemetryRecord per session,
|
|
134
|
+
* and ONE SkillUsageRecord per triggered skill.
|
|
135
|
+
*/
|
|
136
|
+
export function writeSession(
|
|
137
|
+
session: ParsedSession,
|
|
138
|
+
dryRun = false,
|
|
139
|
+
queryLogPath: string = QUERY_LOG,
|
|
140
|
+
telemetryLogPath: string = TELEMETRY_LOG,
|
|
141
|
+
skillLogPath: string = SKILL_LOG,
|
|
142
|
+
canonicalLogPath: string = CANONICAL_LOG,
|
|
143
|
+
): void {
|
|
144
|
+
if (dryRun) {
|
|
145
|
+
console.log(
|
|
146
|
+
` [DRY RUN] Would ingest: session=${session.session_id.slice(0, 12)}... ` +
|
|
147
|
+
`turns=${session.metrics.assistant_turns} queries=${session.user_queries.length} ` +
|
|
148
|
+
`skills=${JSON.stringify(session.metrics.skills_triggered)}`,
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Write ONE query record per user query
|
|
154
|
+
for (const uq of session.user_queries) {
|
|
155
|
+
const queryRecord: QueryLogRecord = {
|
|
156
|
+
timestamp: uq.timestamp || session.timestamp,
|
|
157
|
+
session_id: session.session_id,
|
|
158
|
+
query: uq.query,
|
|
159
|
+
source: "claude_code_replay",
|
|
160
|
+
};
|
|
161
|
+
appendJsonl(queryLogPath, queryRecord, "all_queries");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Write ONE telemetry record per session
|
|
165
|
+
const telemetry: SessionTelemetryRecord = {
|
|
166
|
+
timestamp: session.timestamp,
|
|
167
|
+
session_id: session.session_id,
|
|
168
|
+
cwd: "",
|
|
169
|
+
transcript_path: session.transcript_path,
|
|
170
|
+
tool_calls: session.metrics.tool_calls,
|
|
171
|
+
total_tool_calls: session.metrics.total_tool_calls,
|
|
172
|
+
bash_commands: session.metrics.bash_commands,
|
|
173
|
+
skills_triggered: session.metrics.skills_triggered,
|
|
174
|
+
skills_invoked: session.metrics.skills_invoked ?? [],
|
|
175
|
+
assistant_turns: session.metrics.assistant_turns,
|
|
176
|
+
errors_encountered: session.metrics.errors_encountered,
|
|
177
|
+
transcript_chars: session.metrics.transcript_chars,
|
|
178
|
+
last_user_query: session.metrics.last_user_query,
|
|
179
|
+
source: "claude_code_replay",
|
|
180
|
+
};
|
|
181
|
+
appendJsonl(telemetryLogPath, telemetry, "session_telemetry");
|
|
182
|
+
|
|
183
|
+
// Write ONE skill record per invoked/triggered skill.
|
|
184
|
+
// Prefer skills_invoked (actual Skill tool calls) for high-confidence records.
|
|
185
|
+
// Fall back to skills_triggered (SKILL.md reads) if no invocations detected.
|
|
186
|
+
const invoked = session.metrics.skills_invoked ?? [];
|
|
187
|
+
const skillSource = invoked.length > 0 ? invoked : session.metrics.skills_triggered;
|
|
188
|
+
const latestActionableQuery =
|
|
189
|
+
session.user_queries[session.user_queries.length - 1]?.query.trim() ??
|
|
190
|
+
session.metrics.last_user_query.trim();
|
|
191
|
+
|
|
192
|
+
for (const skillName of skillSource) {
|
|
193
|
+
const skillQuery = latestActionableQuery;
|
|
194
|
+
if (!isActionableQueryText(skillQuery)) continue;
|
|
195
|
+
|
|
196
|
+
const skillRecord: SkillUsageRecord = {
|
|
197
|
+
timestamp: session.timestamp,
|
|
198
|
+
session_id: session.session_id,
|
|
199
|
+
skill_name: skillName,
|
|
200
|
+
skill_path: `(claude_code:${skillName})`,
|
|
201
|
+
query: skillQuery,
|
|
202
|
+
triggered: true,
|
|
203
|
+
source: "claude_code_replay",
|
|
204
|
+
};
|
|
205
|
+
appendJsonl(skillLogPath, skillRecord, "skill_usage");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Canonical normalization records (additive) ---
|
|
209
|
+
const canonicalRecords = buildCanonicalRecordsFromReplay(session);
|
|
210
|
+
appendCanonicalRecords(canonicalRecords, canonicalLogPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Build canonical records from a parsed Claude Code replay session. */
|
|
214
|
+
export function buildCanonicalRecordsFromReplay(session: ParsedSession): CanonicalRecord[] {
|
|
215
|
+
const records: CanonicalRecord[] = [];
|
|
216
|
+
const latestPromptIndex =
|
|
217
|
+
session.user_queries.length > 0 ? session.user_queries.length - 1 : undefined;
|
|
218
|
+
const latestPromptId =
|
|
219
|
+
latestPromptIndex !== undefined
|
|
220
|
+
? derivePromptId(session.session_id, latestPromptIndex)
|
|
221
|
+
: undefined;
|
|
222
|
+
const baseInput: CanonicalBaseInput = {
|
|
223
|
+
platform: "claude_code",
|
|
224
|
+
capture_mode: "replay",
|
|
225
|
+
source_session_kind: "replayed",
|
|
226
|
+
session_id: session.session_id,
|
|
227
|
+
raw_source_ref: {
|
|
228
|
+
path: session.transcript_path,
|
|
229
|
+
event_type: "claude_code_replay",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
records.push(
|
|
234
|
+
buildCanonicalSession({
|
|
235
|
+
...baseInput,
|
|
236
|
+
started_at: session.timestamp,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// One canonical prompt per user query
|
|
241
|
+
for (let i = 0; i < session.user_queries.length; i++) {
|
|
242
|
+
const uq = session.user_queries[i];
|
|
243
|
+
records.push(
|
|
244
|
+
buildCanonicalPrompt({
|
|
245
|
+
...baseInput,
|
|
246
|
+
prompt_id: derivePromptId(session.session_id, i),
|
|
247
|
+
occurred_at: uq.timestamp || session.timestamp,
|
|
248
|
+
prompt_text: uq.query,
|
|
249
|
+
prompt_index: i,
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Skill invocation records — prefer invoked over triggered
|
|
255
|
+
const invoked = session.metrics.skills_invoked ?? [];
|
|
256
|
+
const skillSource = invoked.length > 0 ? invoked : session.metrics.skills_triggered;
|
|
257
|
+
const wasInvoked = invoked.length > 0;
|
|
258
|
+
|
|
259
|
+
for (let i = 0; i < skillSource.length; i++) {
|
|
260
|
+
const skillName = skillSource[i];
|
|
261
|
+
const { invocation_mode, confidence } = deriveInvocationMode({
|
|
262
|
+
has_skill_tool_call: wasInvoked,
|
|
263
|
+
has_skill_md_read: !wasInvoked,
|
|
264
|
+
});
|
|
265
|
+
records.push(
|
|
266
|
+
buildCanonicalSkillInvocation({
|
|
267
|
+
...baseInput,
|
|
268
|
+
skill_invocation_id: deriveSkillInvocationId(session.session_id, skillName, i),
|
|
269
|
+
occurred_at: session.timestamp,
|
|
270
|
+
matched_prompt_id: latestPromptId ?? derivePromptId(session.session_id, 0),
|
|
271
|
+
skill_name: skillName,
|
|
272
|
+
skill_path: `(claude_code:${skillName})`,
|
|
273
|
+
invocation_mode,
|
|
274
|
+
triggered: true,
|
|
275
|
+
confidence,
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
records.push(
|
|
281
|
+
buildCanonicalExecutionFact({
|
|
282
|
+
...baseInput,
|
|
283
|
+
occurred_at: session.timestamp,
|
|
284
|
+
prompt_id: latestPromptId,
|
|
285
|
+
tool_calls_json: session.metrics.tool_calls,
|
|
286
|
+
total_tool_calls: session.metrics.total_tool_calls,
|
|
287
|
+
bash_commands_redacted: session.metrics.bash_commands,
|
|
288
|
+
assistant_turns: session.metrics.assistant_turns,
|
|
289
|
+
errors_encountered: session.metrics.errors_encountered,
|
|
290
|
+
input_tokens: session.metrics.input_tokens,
|
|
291
|
+
output_tokens: session.metrics.output_tokens,
|
|
292
|
+
duration_ms: session.metrics.duration_ms,
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return records;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- CLI main ---
|
|
300
|
+
export function cliMain(): void {
|
|
301
|
+
const { values } = parseArgs({
|
|
302
|
+
options: {
|
|
303
|
+
"projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
|
|
304
|
+
since: { type: "string" },
|
|
305
|
+
"dry-run": { type: "boolean", default: false },
|
|
306
|
+
force: { type: "boolean", default: false },
|
|
307
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
308
|
+
},
|
|
309
|
+
strict: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const projectsDir = values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR;
|
|
313
|
+
let since: Date | undefined;
|
|
314
|
+
if (values.since) {
|
|
315
|
+
since = new Date(values.since);
|
|
316
|
+
if (Number.isNaN(since.getTime())) {
|
|
317
|
+
console.error(
|
|
318
|
+
`Error: Invalid --since date: "${values.since}". Use a valid date format (e.g., 2026-01-01).`,
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const transcriptFiles = findTranscriptFiles(projectsDir, since);
|
|
325
|
+
if (transcriptFiles.length === 0) {
|
|
326
|
+
console.log(`No transcript files found under ${projectsDir}/`);
|
|
327
|
+
console.log("Make sure you've run some Claude Code sessions.");
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const alreadyIngested = values.force ? new Set<string>() : loadMarker(CLAUDE_CODE_MARKER);
|
|
332
|
+
const newIngested = new Set<string>();
|
|
333
|
+
|
|
334
|
+
const pending = transcriptFiles.filter((f) => !alreadyIngested.has(f));
|
|
335
|
+
console.log(
|
|
336
|
+
`Found ${transcriptFiles.length} transcript files, ${pending.length} not yet ingested.`,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (since) {
|
|
340
|
+
console.log(` Filtering to sessions from ${values.since} onward.`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let ingestedCount = 0;
|
|
344
|
+
let skippedCount = 0;
|
|
345
|
+
|
|
346
|
+
for (const transcriptFile of pending) {
|
|
347
|
+
const session = parseSession(transcriptFile);
|
|
348
|
+
if (session === null) {
|
|
349
|
+
if (values.verbose) {
|
|
350
|
+
console.log(` SKIP (empty/no queries): ${basename(transcriptFile)}`);
|
|
351
|
+
}
|
|
352
|
+
skippedCount += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (values.verbose || values["dry-run"]) {
|
|
357
|
+
console.log(` ${values["dry-run"] ? "[DRY] " : ""}Ingesting: ${basename(transcriptFile)}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
writeSession(session, values["dry-run"]);
|
|
361
|
+
newIngested.add(transcriptFile);
|
|
362
|
+
ingestedCount += 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!values["dry-run"]) {
|
|
366
|
+
saveMarker(CLAUDE_CODE_MARKER, new Set([...alreadyIngested, ...newIngested]));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(`\nDone. Ingested ${ingestedCount} sessions, skipped ${skippedCount}.`);
|
|
370
|
+
if (newIngested.size > 0 && !values["dry-run"]) {
|
|
371
|
+
console.log(`Marker updated: ${CLAUDE_CODE_MARKER}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (import.meta.main) {
|
|
376
|
+
cliMain();
|
|
377
|
+
}
|