selftune 0.2.0 → 0.2.2
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 +20 -10
- package/.claude/agents/evolution-reviewer.md +14 -1
- package/.claude/agents/integration-guide.md +18 -6
- package/.claude/agents/pattern-analyst.md +18 -5
- package/CHANGELOG.md +12 -4
- package/README.md +43 -35
- 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/cli/selftune/badge/badge-data.ts +1 -1
- package/cli/selftune/badge/badge.ts +4 -8
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +28 -0
- package/cli/selftune/contribute/contribute.ts +1 -1
- package/cli/selftune/cron/setup.ts +17 -17
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +653 -186
- package/cli/selftune/dashboard.ts +41 -176
- package/cli/selftune/eval/baseline.ts +5 -4
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/hooks-to-evals.ts +34 -15
- package/cli/selftune/eval/unit-test-cli.ts +1 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +105 -11
- package/cli/selftune/evolution/evolve.ts +371 -25
- package/cli/selftune/evolution/extract-patterns.ts +87 -29
- package/cli/selftune/evolution/rollback.ts +2 -2
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +448 -97
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +395 -116
- package/cli/selftune/ingestors/claude-replay.ts +140 -114
- 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 +141 -8
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +227 -14
- 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/monitoring/watch.ts +66 -15
- 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 +48 -26
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +148 -0
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +78 -20
- 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 +272 -26
- 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 +21 -8
- 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 +84 -53
- package/skill/Workflows/AutoActivation.md +17 -16
- package/skill/Workflows/Badge.md +6 -0
- package/skill/Workflows/Baseline.md +46 -23
- package/skill/Workflows/Composability.md +12 -5
- package/skill/Workflows/Contribute.md +17 -14
- package/skill/Workflows/Cron.md +56 -79
- package/skill/Workflows/Dashboard.md +45 -34
- package/skill/Workflows/Doctor.md +30 -17
- package/skill/Workflows/Evals.md +64 -40
- package/skill/Workflows/EvolutionMemory.md +2 -0
- package/skill/Workflows/Evolve.md +102 -47
- package/skill/Workflows/EvolveBody.md +6 -6
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +11 -5
- package/skill/Workflows/Ingest.md +43 -36
- package/skill/Workflows/Initialize.md +44 -30
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +39 -18
- package/skill/Workflows/Rollback.md +3 -3
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +34 -22
- package/skill/Workflows/Watch.md +14 -4
- 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 +1 -1
- package/templates/multi-skill-settings.json +7 -7
- package/templates/single-skill-settings.json +6 -6
- package/dashboard/index.html +0 -1680
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* selftune sync — Source-truth telemetry sync across supported agent CLIs.
|
|
4
|
+
*
|
|
5
|
+
* This command is intentionally source-first:
|
|
6
|
+
* - Claude Code transcripts
|
|
7
|
+
* - Codex rollout logs
|
|
8
|
+
* - OpenCode session history
|
|
9
|
+
* - OpenClaw session history
|
|
10
|
+
*
|
|
11
|
+
* After syncing raw session/query/telemetry records, it rebuilds the repaired
|
|
12
|
+
* skill-usage overlay from Claude transcripts and Codex rollouts so monitoring,
|
|
13
|
+
* grading, and evolution are driven from source truth rather than hooks alone.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { parseArgs } from "node:util";
|
|
20
|
+
import {
|
|
21
|
+
CLAUDE_CODE_MARKER,
|
|
22
|
+
CLAUDE_CODE_PROJECTS_DIR,
|
|
23
|
+
CODEX_INGEST_MARKER,
|
|
24
|
+
OPENCLAW_AGENTS_DIR,
|
|
25
|
+
OPENCLAW_INGEST_MARKER,
|
|
26
|
+
OPENCODE_INGEST_MARKER,
|
|
27
|
+
QUERY_LOG,
|
|
28
|
+
REPAIRED_SKILL_LOG,
|
|
29
|
+
REPAIRED_SKILL_SESSIONS_MARKER,
|
|
30
|
+
SKILL_LOG,
|
|
31
|
+
TELEMETRY_LOG,
|
|
32
|
+
} from "./constants.js";
|
|
33
|
+
import {
|
|
34
|
+
findTranscriptFiles,
|
|
35
|
+
parseSession,
|
|
36
|
+
writeSession as writeClaudeReplaySession,
|
|
37
|
+
} from "./ingestors/claude-replay.js";
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_CODEX_HOME,
|
|
40
|
+
findSkillNames as findCodexSkillNames,
|
|
41
|
+
findRolloutFiles,
|
|
42
|
+
ingestFile as ingestCodexRollout,
|
|
43
|
+
parseRolloutFile,
|
|
44
|
+
} from "./ingestors/codex-rollout.js";
|
|
45
|
+
import {
|
|
46
|
+
findOpenClawSessions,
|
|
47
|
+
findOpenClawSkillNames,
|
|
48
|
+
parseOpenClawSession,
|
|
49
|
+
writeSession as writeOpenClawSession,
|
|
50
|
+
} from "./ingestors/openclaw-ingest.js";
|
|
51
|
+
import {
|
|
52
|
+
findSkillNames as findOpenCodeSkillNames,
|
|
53
|
+
readSessionsFromJsonFiles,
|
|
54
|
+
readSessionsFromSqlite,
|
|
55
|
+
writeSession as writeOpenCodeSession,
|
|
56
|
+
} from "./ingestors/opencode-ingest.js";
|
|
57
|
+
import {
|
|
58
|
+
rebuildSkillUsageFromCodexRollouts,
|
|
59
|
+
rebuildSkillUsageFromTranscripts,
|
|
60
|
+
} from "./repair/skill-usage.js";
|
|
61
|
+
import type { SkillUsageRecord } from "./types.js";
|
|
62
|
+
import { loadMarker, readJsonl, saveMarker } from "./utils/jsonl.js";
|
|
63
|
+
import { writeRepairedSkillUsageRecords } from "./utils/skill-log.js";
|
|
64
|
+
|
|
65
|
+
const XDG_DATA_HOME = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
66
|
+
const DEFAULT_OPENCODE_DATA_DIR = join(XDG_DATA_HOME, "opencode");
|
|
67
|
+
|
|
68
|
+
export interface SyncStepResult {
|
|
69
|
+
available: boolean;
|
|
70
|
+
scanned: number;
|
|
71
|
+
synced: number;
|
|
72
|
+
skipped: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SyncPhaseTiming {
|
|
76
|
+
phase: string;
|
|
77
|
+
elapsed_ms: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SyncResult {
|
|
81
|
+
since: string | null;
|
|
82
|
+
dry_run: boolean;
|
|
83
|
+
sources: {
|
|
84
|
+
claude: SyncStepResult;
|
|
85
|
+
codex: SyncStepResult;
|
|
86
|
+
opencode: SyncStepResult;
|
|
87
|
+
openclaw: SyncStepResult;
|
|
88
|
+
};
|
|
89
|
+
repair: {
|
|
90
|
+
ran: boolean;
|
|
91
|
+
repaired_sessions: number;
|
|
92
|
+
repaired_records: number;
|
|
93
|
+
codex_repaired_records: number;
|
|
94
|
+
};
|
|
95
|
+
timings: SyncPhaseTiming[];
|
|
96
|
+
total_elapsed_ms: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SyncOptions {
|
|
100
|
+
projectsDir: string;
|
|
101
|
+
codexHome: string;
|
|
102
|
+
opencodeDataDir: string;
|
|
103
|
+
openclawAgentsDir: string;
|
|
104
|
+
skillLogPath: string;
|
|
105
|
+
repairedSkillLogPath: string;
|
|
106
|
+
repairedSessionsPath: string;
|
|
107
|
+
since?: Date;
|
|
108
|
+
dryRun: boolean;
|
|
109
|
+
force: boolean;
|
|
110
|
+
syncClaude: boolean;
|
|
111
|
+
syncCodex: boolean;
|
|
112
|
+
syncOpenCode: boolean;
|
|
113
|
+
syncOpenClaw: boolean;
|
|
114
|
+
rebuildSkillUsage: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type SyncProgressCallback = (message: string) => void;
|
|
118
|
+
|
|
119
|
+
export interface SyncDeps {
|
|
120
|
+
syncClaude?: (options: SyncOptions) => SyncStepResult;
|
|
121
|
+
syncCodex?: (options: SyncOptions) => SyncStepResult;
|
|
122
|
+
syncOpenCode?: (options: SyncOptions) => SyncStepResult;
|
|
123
|
+
syncOpenClaw?: (options: SyncOptions) => SyncStepResult;
|
|
124
|
+
rebuildSkillUsage?: (options: SyncOptions) => {
|
|
125
|
+
repairedSessions: number;
|
|
126
|
+
repairedRecords: number;
|
|
127
|
+
codexRepairedRecords: number;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createDefaultSyncOptions(overrides: Partial<SyncOptions> = {}): SyncOptions {
|
|
132
|
+
return {
|
|
133
|
+
projectsDir: CLAUDE_CODE_PROJECTS_DIR,
|
|
134
|
+
codexHome: DEFAULT_CODEX_HOME,
|
|
135
|
+
opencodeDataDir: DEFAULT_OPENCODE_DATA_DIR,
|
|
136
|
+
openclawAgentsDir: OPENCLAW_AGENTS_DIR,
|
|
137
|
+
skillLogPath: SKILL_LOG,
|
|
138
|
+
repairedSkillLogPath: REPAIRED_SKILL_LOG,
|
|
139
|
+
repairedSessionsPath: REPAIRED_SKILL_SESSIONS_MARKER,
|
|
140
|
+
dryRun: false,
|
|
141
|
+
force: false,
|
|
142
|
+
syncClaude: true,
|
|
143
|
+
syncCodex: true,
|
|
144
|
+
syncOpenCode: true,
|
|
145
|
+
syncOpenClaw: true,
|
|
146
|
+
rebuildSkillUsage: true,
|
|
147
|
+
...overrides,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Shared file-list cache so repair can reuse the ingest-phase scan. */
|
|
152
|
+
interface FileListCache {
|
|
153
|
+
claudeTranscripts?: string[];
|
|
154
|
+
codexRollouts?: string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function syncClaudeSource(
|
|
158
|
+
options: SyncOptions,
|
|
159
|
+
onProgress?: SyncProgressCallback,
|
|
160
|
+
cache?: FileListCache,
|
|
161
|
+
): SyncStepResult {
|
|
162
|
+
if (!existsSync(options.projectsDir)) {
|
|
163
|
+
return { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onProgress?.("scanning Claude transcripts...");
|
|
167
|
+
const transcriptFiles = findTranscriptFiles(options.projectsDir, options.since);
|
|
168
|
+
if (cache) cache.claudeTranscripts = transcriptFiles;
|
|
169
|
+
|
|
170
|
+
const alreadyIngested = options.force ? new Set<string>() : loadMarker(CLAUDE_CODE_MARKER);
|
|
171
|
+
const pending = transcriptFiles.filter((f) => !alreadyIngested.has(f));
|
|
172
|
+
onProgress?.(`found ${transcriptFiles.length} transcripts, ${pending.length} pending`);
|
|
173
|
+
|
|
174
|
+
const newIngested = new Set<string>();
|
|
175
|
+
let synced = 0;
|
|
176
|
+
let skipped = 0;
|
|
177
|
+
|
|
178
|
+
for (const transcriptFile of pending) {
|
|
179
|
+
const parsed = parseSession(transcriptFile);
|
|
180
|
+
if (!parsed) {
|
|
181
|
+
skipped += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
writeClaudeReplaySession(
|
|
185
|
+
parsed,
|
|
186
|
+
options.dryRun,
|
|
187
|
+
QUERY_LOG,
|
|
188
|
+
TELEMETRY_LOG,
|
|
189
|
+
options.skillLogPath,
|
|
190
|
+
);
|
|
191
|
+
newIngested.add(transcriptFile);
|
|
192
|
+
synced += 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!options.dryRun && newIngested.size > 0) {
|
|
196
|
+
saveMarker(CLAUDE_CODE_MARKER, new Set([...alreadyIngested, ...newIngested]));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
available: true,
|
|
201
|
+
scanned: transcriptFiles.length,
|
|
202
|
+
synced,
|
|
203
|
+
skipped,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function syncCodexSource(
|
|
208
|
+
options: SyncOptions,
|
|
209
|
+
onProgress?: SyncProgressCallback,
|
|
210
|
+
cache?: FileListCache,
|
|
211
|
+
): SyncStepResult {
|
|
212
|
+
onProgress?.("scanning Codex rollouts...");
|
|
213
|
+
const rolloutFiles = findRolloutFiles(options.codexHome, options.since);
|
|
214
|
+
if (cache) cache.codexRollouts = rolloutFiles;
|
|
215
|
+
|
|
216
|
+
if (rolloutFiles.length === 0 && !existsSync(join(options.codexHome, "sessions"))) {
|
|
217
|
+
return { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const alreadyIngested = options.force ? new Set<string>() : loadMarker(CODEX_INGEST_MARKER);
|
|
221
|
+
const pending = rolloutFiles.filter((f) => !alreadyIngested.has(f));
|
|
222
|
+
onProgress?.(`found ${rolloutFiles.length} rollouts, ${pending.length} pending`);
|
|
223
|
+
|
|
224
|
+
const skillNames = findCodexSkillNames();
|
|
225
|
+
const newIngested = new Set<string>();
|
|
226
|
+
let synced = 0;
|
|
227
|
+
let skipped = 0;
|
|
228
|
+
|
|
229
|
+
for (const rolloutFile of pending) {
|
|
230
|
+
const parsed = parseRolloutFile(rolloutFile, skillNames);
|
|
231
|
+
if (!parsed) {
|
|
232
|
+
skipped += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
ingestCodexRollout(parsed, options.dryRun, QUERY_LOG, TELEMETRY_LOG, options.skillLogPath);
|
|
236
|
+
newIngested.add(rolloutFile);
|
|
237
|
+
synced += 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!options.dryRun && newIngested.size > 0) {
|
|
241
|
+
saveMarker(CODEX_INGEST_MARKER, new Set([...alreadyIngested, ...newIngested]));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
available: true,
|
|
246
|
+
scanned: rolloutFiles.length,
|
|
247
|
+
synced,
|
|
248
|
+
skipped,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function syncOpenCodeSource(
|
|
253
|
+
options: SyncOptions,
|
|
254
|
+
onProgress?: SyncProgressCallback,
|
|
255
|
+
): SyncStepResult {
|
|
256
|
+
if (!existsSync(options.opencodeDataDir)) {
|
|
257
|
+
return { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
onProgress?.("scanning OpenCode sessions...");
|
|
261
|
+
const dbPath = join(options.opencodeDataDir, "opencode.db");
|
|
262
|
+
const storageDir = join(options.opencodeDataDir, "storage");
|
|
263
|
+
const skillNames = findOpenCodeSkillNames();
|
|
264
|
+
const sinceTs = options.since ? options.since.getTime() / 1000 : null;
|
|
265
|
+
const allSessions = existsSync(dbPath)
|
|
266
|
+
? readSessionsFromSqlite(dbPath, sinceTs, skillNames)
|
|
267
|
+
: existsSync(storageDir)
|
|
268
|
+
? readSessionsFromJsonFiles(storageDir, sinceTs, skillNames)
|
|
269
|
+
: [];
|
|
270
|
+
|
|
271
|
+
if (allSessions.length === 0 && !existsSync(dbPath) && !existsSync(storageDir)) {
|
|
272
|
+
return { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const alreadyIngested = options.force ? new Set<string>() : loadMarker(OPENCODE_INGEST_MARKER);
|
|
276
|
+
const pending = allSessions.filter((session) => !alreadyIngested.has(session.session_id));
|
|
277
|
+
onProgress?.(`found ${allSessions.length} sessions, ${pending.length} pending`);
|
|
278
|
+
const newIngested = new Set<string>();
|
|
279
|
+
|
|
280
|
+
for (const session of pending) {
|
|
281
|
+
writeOpenCodeSession(session, options.dryRun, QUERY_LOG, TELEMETRY_LOG, options.skillLogPath);
|
|
282
|
+
newIngested.add(session.session_id);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!options.dryRun && newIngested.size > 0) {
|
|
286
|
+
saveMarker(OPENCODE_INGEST_MARKER, new Set([...alreadyIngested, ...newIngested]));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
available: true,
|
|
291
|
+
scanned: allSessions.length,
|
|
292
|
+
synced: pending.length,
|
|
293
|
+
skipped: 0,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function syncOpenClawSource(
|
|
298
|
+
options: SyncOptions,
|
|
299
|
+
onProgress?: SyncProgressCallback,
|
|
300
|
+
): SyncStepResult {
|
|
301
|
+
if (!existsSync(options.openclawAgentsDir)) {
|
|
302
|
+
return { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
onProgress?.("scanning OpenClaw sessions...");
|
|
306
|
+
const sinceTs = options.since ? options.since.getTime() : null;
|
|
307
|
+
const allSessions = findOpenClawSessions(options.openclawAgentsDir, sinceTs);
|
|
308
|
+
const skillNames = findOpenClawSkillNames(options.openclawAgentsDir);
|
|
309
|
+
const alreadyIngested = options.force ? new Set<string>() : loadMarker(OPENCLAW_INGEST_MARKER);
|
|
310
|
+
const pending = allSessions.filter((session) => !alreadyIngested.has(session.sessionId));
|
|
311
|
+
onProgress?.(`found ${allSessions.length} sessions, ${pending.length} pending`);
|
|
312
|
+
const newIngested = new Set<string>();
|
|
313
|
+
let synced = 0;
|
|
314
|
+
let skipped = 0;
|
|
315
|
+
|
|
316
|
+
for (const sessionFile of pending) {
|
|
317
|
+
const session = parseOpenClawSession(sessionFile.filePath, skillNames);
|
|
318
|
+
if (!session.session_id || !session.timestamp) {
|
|
319
|
+
skipped += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
writeOpenClawSession(session, options.dryRun, QUERY_LOG, TELEMETRY_LOG, options.skillLogPath);
|
|
323
|
+
newIngested.add(sessionFile.sessionId);
|
|
324
|
+
synced += 1;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!options.dryRun && newIngested.size > 0) {
|
|
328
|
+
saveMarker(OPENCLAW_INGEST_MARKER, new Set([...alreadyIngested, ...newIngested]));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
available: true,
|
|
333
|
+
scanned: allSessions.length,
|
|
334
|
+
synced,
|
|
335
|
+
skipped,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function rebuildSkillUsageOverlay(
|
|
340
|
+
options: SyncOptions,
|
|
341
|
+
onProgress?: SyncProgressCallback,
|
|
342
|
+
cache?: FileListCache,
|
|
343
|
+
): {
|
|
344
|
+
repairedSessions: number;
|
|
345
|
+
repairedRecords: number;
|
|
346
|
+
codexRepairedRecords: number;
|
|
347
|
+
} {
|
|
348
|
+
// Reuse cached file lists from ingest phase when available to avoid re-walking the filesystem
|
|
349
|
+
const transcriptPaths =
|
|
350
|
+
cache?.claudeTranscripts ?? findTranscriptFiles(options.projectsDir, options.since);
|
|
351
|
+
const rolloutPaths = cache?.codexRollouts ?? findRolloutFiles(options.codexHome, options.since);
|
|
352
|
+
|
|
353
|
+
const reusedClaude = cache?.claudeTranscripts ? " (cached)" : "";
|
|
354
|
+
const reusedCodex = cache?.codexRollouts ? " (cached)" : "";
|
|
355
|
+
onProgress?.(
|
|
356
|
+
`repairing from ${transcriptPaths.length} transcripts${reusedClaude}, ${rolloutPaths.length} rollouts${reusedCodex}`,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const rawSkillRecords = readJsonl<SkillUsageRecord>(options.skillLogPath);
|
|
360
|
+
const { repairedRecords, repairedSessionIds } = rebuildSkillUsageFromTranscripts(
|
|
361
|
+
transcriptPaths,
|
|
362
|
+
rawSkillRecords,
|
|
363
|
+
process.env.HOME ?? "",
|
|
364
|
+
options.codexHome,
|
|
365
|
+
);
|
|
366
|
+
const { records: codexRecords, sessionIds: codexSessionIds } = rebuildSkillUsageFromCodexRollouts(
|
|
367
|
+
rolloutPaths,
|
|
368
|
+
rawSkillRecords,
|
|
369
|
+
process.env.HOME ?? "",
|
|
370
|
+
options.codexHome,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
for (const sessionId of codexSessionIds) repairedSessionIds.add(sessionId);
|
|
374
|
+
repairedRecords.push(...codexRecords);
|
|
375
|
+
|
|
376
|
+
if (!options.dryRun) {
|
|
377
|
+
writeRepairedSkillUsageRecords(
|
|
378
|
+
repairedRecords,
|
|
379
|
+
repairedSessionIds,
|
|
380
|
+
options.repairedSkillLogPath,
|
|
381
|
+
options.repairedSessionsPath,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
onProgress?.(
|
|
386
|
+
`repaired ${repairedRecords.length} records across ${repairedSessionIds.size} sessions`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
repairedSessions: repairedSessionIds.size,
|
|
391
|
+
repairedRecords: repairedRecords.length,
|
|
392
|
+
codexRepairedRecords: codexRecords.length,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function timePhase<T>(name: string, fn: () => T, timings: SyncPhaseTiming[]): T {
|
|
397
|
+
const start = performance.now();
|
|
398
|
+
const result = fn();
|
|
399
|
+
timings.push({ phase: name, elapsed_ms: Math.round(performance.now() - start) });
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function syncSources(
|
|
404
|
+
options: SyncOptions,
|
|
405
|
+
deps: SyncDeps = {},
|
|
406
|
+
onProgress?: SyncProgressCallback,
|
|
407
|
+
): SyncResult {
|
|
408
|
+
const totalStart = performance.now();
|
|
409
|
+
const timings: SyncPhaseTiming[] = [];
|
|
410
|
+
const cache: FileListCache = {};
|
|
411
|
+
|
|
412
|
+
const runClaude = deps.syncClaude;
|
|
413
|
+
const runCodex = deps.syncCodex;
|
|
414
|
+
const runOpenCode = deps.syncOpenCode;
|
|
415
|
+
const runOpenClaw = deps.syncOpenClaw;
|
|
416
|
+
const runRepair = deps.rebuildSkillUsage;
|
|
417
|
+
|
|
418
|
+
const disabledStep: SyncStepResult = { available: false, scanned: 0, synced: 0, skipped: 0 };
|
|
419
|
+
|
|
420
|
+
onProgress?.("starting sync...");
|
|
421
|
+
|
|
422
|
+
const claude = options.syncClaude
|
|
423
|
+
? timePhase(
|
|
424
|
+
"claude",
|
|
425
|
+
() => (runClaude ? runClaude(options) : syncClaudeSource(options, onProgress, cache)),
|
|
426
|
+
timings,
|
|
427
|
+
)
|
|
428
|
+
: disabledStep;
|
|
429
|
+
|
|
430
|
+
const codex = options.syncCodex
|
|
431
|
+
? timePhase(
|
|
432
|
+
"codex",
|
|
433
|
+
() => (runCodex ? runCodex(options) : syncCodexSource(options, onProgress, cache)),
|
|
434
|
+
timings,
|
|
435
|
+
)
|
|
436
|
+
: disabledStep;
|
|
437
|
+
|
|
438
|
+
const opencode = options.syncOpenCode
|
|
439
|
+
? timePhase(
|
|
440
|
+
"opencode",
|
|
441
|
+
() => (runOpenCode ? runOpenCode(options) : syncOpenCodeSource(options, onProgress)),
|
|
442
|
+
timings,
|
|
443
|
+
)
|
|
444
|
+
: disabledStep;
|
|
445
|
+
|
|
446
|
+
const openclaw = options.syncOpenClaw
|
|
447
|
+
? timePhase(
|
|
448
|
+
"openclaw",
|
|
449
|
+
() => (runOpenClaw ? runOpenClaw(options) : syncOpenClawSource(options, onProgress)),
|
|
450
|
+
timings,
|
|
451
|
+
)
|
|
452
|
+
: disabledStep;
|
|
453
|
+
|
|
454
|
+
const repair = options.rebuildSkillUsage
|
|
455
|
+
? timePhase(
|
|
456
|
+
"repair",
|
|
457
|
+
() =>
|
|
458
|
+
runRepair ? runRepair(options) : rebuildSkillUsageOverlay(options, onProgress, cache),
|
|
459
|
+
timings,
|
|
460
|
+
)
|
|
461
|
+
: { repairedSessions: 0, repairedRecords: 0, codexRepairedRecords: 0 };
|
|
462
|
+
|
|
463
|
+
const totalElapsed = Math.round(performance.now() - totalStart);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
since: options.since ? options.since.toISOString() : null,
|
|
467
|
+
dry_run: options.dryRun,
|
|
468
|
+
sources: { claude, codex, opencode, openclaw },
|
|
469
|
+
repair: {
|
|
470
|
+
ran: options.rebuildSkillUsage,
|
|
471
|
+
repaired_sessions: repair.repairedSessions,
|
|
472
|
+
repaired_records: repair.repairedRecords,
|
|
473
|
+
codex_repaired_records: repair.codexRepairedRecords,
|
|
474
|
+
},
|
|
475
|
+
timings,
|
|
476
|
+
total_elapsed_ms: totalElapsed,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function formatMs(ms: number): string {
|
|
481
|
+
if (ms < 1000) return `${ms}ms`;
|
|
482
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function formatStepLine(label: string, step: SyncStepResult, timing?: SyncPhaseTiming): string {
|
|
486
|
+
if (!step.available) return ` ${label}: not available`;
|
|
487
|
+
const parts = [`scanned ${step.scanned}`];
|
|
488
|
+
if (step.synced > 0) parts.push(`synced ${step.synced}`);
|
|
489
|
+
if (step.skipped > 0) parts.push(`skipped ${step.skipped}`);
|
|
490
|
+
const time = timing ? ` (${formatMs(timing.elapsed_ms)})` : "";
|
|
491
|
+
return ` ${label}: ${parts.join(", ")}${time}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function cliMain(): void {
|
|
495
|
+
const { values } = parseArgs({
|
|
496
|
+
options: {
|
|
497
|
+
"projects-dir": { type: "string", default: CLAUDE_CODE_PROJECTS_DIR },
|
|
498
|
+
"codex-home": { type: "string", default: DEFAULT_CODEX_HOME },
|
|
499
|
+
"opencode-data-dir": { type: "string", default: DEFAULT_OPENCODE_DATA_DIR },
|
|
500
|
+
"openclaw-agents-dir": { type: "string", default: OPENCLAW_AGENTS_DIR },
|
|
501
|
+
"skill-log": { type: "string", default: SKILL_LOG },
|
|
502
|
+
"repaired-skill-log": { type: "string", default: REPAIRED_SKILL_LOG },
|
|
503
|
+
"repaired-sessions-marker": { type: "string", default: REPAIRED_SKILL_SESSIONS_MARKER },
|
|
504
|
+
since: { type: "string" },
|
|
505
|
+
"dry-run": { type: "boolean", default: false },
|
|
506
|
+
force: { type: "boolean", default: false },
|
|
507
|
+
"no-claude": { type: "boolean", default: false },
|
|
508
|
+
"no-codex": { type: "boolean", default: false },
|
|
509
|
+
"no-opencode": { type: "boolean", default: false },
|
|
510
|
+
"no-openclaw": { type: "boolean", default: false },
|
|
511
|
+
"no-repair": { type: "boolean", default: false },
|
|
512
|
+
json: { type: "boolean", default: false },
|
|
513
|
+
help: { type: "boolean", short: "h", default: false },
|
|
514
|
+
},
|
|
515
|
+
strict: true,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (values.help) {
|
|
519
|
+
console.log(`selftune sync — Source-truth telemetry sync
|
|
520
|
+
|
|
521
|
+
Usage:
|
|
522
|
+
selftune sync [options]
|
|
523
|
+
|
|
524
|
+
Options:
|
|
525
|
+
--projects-dir <dir> Claude transcript directory (default: ~/.claude/projects)
|
|
526
|
+
--codex-home <dir> Codex home directory (default: ~/.codex)
|
|
527
|
+
--opencode-data-dir <dir> OpenCode data directory
|
|
528
|
+
--openclaw-agents-dir <dir> OpenClaw agents directory
|
|
529
|
+
--skill-log <path> Raw skill usage log path
|
|
530
|
+
--repaired-skill-log <path> Repaired overlay log path
|
|
531
|
+
--repaired-sessions-marker <p> Repaired session marker path
|
|
532
|
+
--since <date> Only sync sessions modified on/after date
|
|
533
|
+
--dry-run Show summary without writing files
|
|
534
|
+
--force Ignore per-source markers and rescan everything
|
|
535
|
+
--no-claude Skip Claude transcript replay
|
|
536
|
+
--no-codex Skip Codex rollout ingest
|
|
537
|
+
--no-opencode Skip OpenCode ingest
|
|
538
|
+
--no-openclaw Skip OpenClaw ingest
|
|
539
|
+
--no-repair Skip rebuilt skill-usage overlay
|
|
540
|
+
--json Output raw JSON instead of human-readable summary
|
|
541
|
+
-h, --help Show this help`);
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let since: Date | undefined;
|
|
546
|
+
if (values.since) {
|
|
547
|
+
since = new Date(values.since);
|
|
548
|
+
if (Number.isNaN(since.getTime())) {
|
|
549
|
+
console.error(`[ERROR] Invalid --since date: ${values.since}`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// JSON output: explicit --json flag, or auto when stdout is not a TTY (preserves contract for automation)
|
|
555
|
+
const jsonOutput = (values.json ?? false) || !process.stdout.isTTY;
|
|
556
|
+
|
|
557
|
+
const onProgress: SyncProgressCallback | undefined = jsonOutput
|
|
558
|
+
? undefined
|
|
559
|
+
: (msg) => {
|
|
560
|
+
process.stderr.write(` ${msg}\n`);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
if (!jsonOutput) {
|
|
564
|
+
const flags: string[] = [];
|
|
565
|
+
if (values.force) flags.push("--force");
|
|
566
|
+
if (values["dry-run"]) flags.push("--dry-run");
|
|
567
|
+
if (since) flags.push(`--since ${values.since}`);
|
|
568
|
+
process.stderr.write(`selftune sync${flags.length ? ` ${flags.join(" ")}` : ""}\n`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const result = syncSources(
|
|
572
|
+
createDefaultSyncOptions({
|
|
573
|
+
projectsDir: values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR,
|
|
574
|
+
codexHome: values["codex-home"] ?? DEFAULT_CODEX_HOME,
|
|
575
|
+
opencodeDataDir: values["opencode-data-dir"] ?? DEFAULT_OPENCODE_DATA_DIR,
|
|
576
|
+
openclawAgentsDir: values["openclaw-agents-dir"] ?? OPENCLAW_AGENTS_DIR,
|
|
577
|
+
skillLogPath: values["skill-log"] ?? SKILL_LOG,
|
|
578
|
+
repairedSkillLogPath: values["repaired-skill-log"] ?? REPAIRED_SKILL_LOG,
|
|
579
|
+
repairedSessionsPath: values["repaired-sessions-marker"] ?? REPAIRED_SKILL_SESSIONS_MARKER,
|
|
580
|
+
since,
|
|
581
|
+
dryRun: values["dry-run"] ?? false,
|
|
582
|
+
force: values.force ?? false,
|
|
583
|
+
syncClaude: !(values["no-claude"] ?? false),
|
|
584
|
+
syncCodex: !(values["no-codex"] ?? false),
|
|
585
|
+
syncOpenCode: !(values["no-opencode"] ?? false),
|
|
586
|
+
syncOpenClaw: !(values["no-openclaw"] ?? false),
|
|
587
|
+
rebuildSkillUsage: !(values["no-repair"] ?? false),
|
|
588
|
+
}),
|
|
589
|
+
{},
|
|
590
|
+
onProgress,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (jsonOutput) {
|
|
594
|
+
console.log(JSON.stringify(result, null, 2));
|
|
595
|
+
} else {
|
|
596
|
+
const timingMap = new Map(result.timings.map((t) => [t.phase, t]));
|
|
597
|
+
|
|
598
|
+
process.stderr.write("\nSources:\n");
|
|
599
|
+
process.stderr.write(
|
|
600
|
+
`${formatStepLine("Claude", result.sources.claude, timingMap.get("claude"))}\n`,
|
|
601
|
+
);
|
|
602
|
+
process.stderr.write(
|
|
603
|
+
`${formatStepLine("Codex", result.sources.codex, timingMap.get("codex"))}\n`,
|
|
604
|
+
);
|
|
605
|
+
process.stderr.write(
|
|
606
|
+
`${formatStepLine("OpenCode", result.sources.opencode, timingMap.get("opencode"))}\n`,
|
|
607
|
+
);
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`${formatStepLine("OpenClaw", result.sources.openclaw, timingMap.get("openclaw"))}\n`,
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
if (result.repair.ran) {
|
|
613
|
+
const repairTiming = timingMap.get("repair");
|
|
614
|
+
const repairTime = repairTiming ? ` (${formatMs(repairTiming.elapsed_ms)})` : "";
|
|
615
|
+
process.stderr.write(
|
|
616
|
+
`\nRepair: ${result.repair.repaired_records} records, ` +
|
|
617
|
+
`${result.repair.repaired_sessions} sessions${repairTime}\n`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
process.stderr.write(`\nDone in ${formatMs(result.total_elapsed_ms)}\n`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (import.meta.main) {
|
|
626
|
+
cliMain();
|
|
627
|
+
}
|