selftune 0.2.8 → 0.2.10
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/README.md +35 -35
- package/apps/local-dashboard/dist/assets/index-BZVLv70T.js +16 -0
- package/apps/local-dashboard/dist/assets/{index-CRtLkBTi.css → index-Bs3Y4ixf.css} +1 -1
- package/apps/local-dashboard/dist/assets/{vendor-react-BQH_6WrG.js → vendor-react-BXP54cYo.js} +4 -4
- package/apps/local-dashboard/dist/assets/{vendor-table-dK1QMLq9.js → vendor-table-DTF_SXoy.js} +1 -1
- package/apps/local-dashboard/dist/assets/{vendor-ui-CO2mrx6e.js → vendor-ui-CWU0d1wd.js} +66 -66
- package/apps/local-dashboard/dist/index.html +15 -15
- package/bin/selftune.cjs +1 -1
- package/cli/selftune/activation-rules.ts +37 -18
- package/cli/selftune/agent-guidance.ts +16 -16
- package/cli/selftune/alpha-identity.ts +1 -2
- package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
- package/cli/selftune/alpha-upload/flush.ts +2 -2
- package/cli/selftune/alpha-upload/stage-canonical.ts +106 -3
- package/cli/selftune/auth/device-code.ts +32 -0
- package/cli/selftune/auto-update.ts +12 -0
- package/cli/selftune/badge/badge.ts +1 -0
- package/cli/selftune/canonical-export.ts +5 -0
- package/cli/selftune/claude-agents.ts +154 -0
- package/cli/selftune/contribute/bundle.ts +2 -0
- package/cli/selftune/contribute/contribute.ts +1 -0
- package/cli/selftune/cron/setup.ts +2 -2
- package/cli/selftune/dashboard-contract.ts +1 -1
- package/cli/selftune/dashboard-server.ts +11 -52
- package/cli/selftune/eval/hooks-to-evals.ts +13 -6
- package/cli/selftune/eval/import-skillsbench.ts +1 -0
- package/cli/selftune/eval/synthetic-evals.ts +2 -3
- package/cli/selftune/eval/unit-test.ts +1 -0
- package/cli/selftune/evolution/deploy-proposal.ts +1 -0
- package/cli/selftune/evolution/evolve-body.ts +93 -6
- package/cli/selftune/evolution/evolve.ts +0 -1
- package/cli/selftune/evolution/propose-body.ts +3 -2
- package/cli/selftune/evolution/propose-routing.ts +3 -2
- package/cli/selftune/evolution/refine-body.ts +3 -2
- package/cli/selftune/export.ts +1 -0
- package/cli/selftune/grading/auto-grade.ts +1 -0
- package/cli/selftune/grading/grade-session.ts +9 -0
- package/cli/selftune/hooks/auto-activate.ts +6 -0
- package/cli/selftune/hooks/evolution-guard.ts +12 -15
- package/cli/selftune/hooks/prompt-log.ts +1 -0
- package/cli/selftune/hooks/session-stop.ts +34 -40
- package/cli/selftune/hooks/skill-change-guard.ts +1 -0
- package/cli/selftune/hooks/skill-eval.ts +1 -1
- package/cli/selftune/index.ts +23 -14
- package/cli/selftune/ingestors/claude-replay.ts +1 -0
- package/cli/selftune/ingestors/codex-rollout.ts +1 -0
- package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
- package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
- package/cli/selftune/init.ts +197 -96
- package/cli/selftune/localdb/db.ts +1 -0
- package/cli/selftune/localdb/direct-write.ts +93 -12
- package/cli/selftune/localdb/materialize.ts +2 -0
- package/cli/selftune/localdb/queries.ts +210 -0
- package/cli/selftune/localdb/schema.ts +72 -1
- package/cli/selftune/monitoring/watch.ts +1 -0
- package/cli/selftune/normalization.ts +4 -0
- package/cli/selftune/observability.ts +14 -7
- package/cli/selftune/orchestrate.ts +15 -37
- package/cli/selftune/repair/skill-usage.ts +7 -3
- package/cli/selftune/routes/orchestrate-runs.ts +1 -0
- package/cli/selftune/routes/overview.ts +1 -0
- package/cli/selftune/routes/skill-report.ts +1 -0
- package/cli/selftune/sync.ts +31 -1
- package/cli/selftune/types.ts +2 -2
- package/cli/selftune/uninstall.ts +412 -0
- package/cli/selftune/utils/canonical-log.ts +2 -0
- package/cli/selftune/utils/jsonl.ts +1 -0
- package/cli/selftune/utils/llm-call.ts +131 -3
- package/cli/selftune/utils/skill-log.ts +1 -0
- package/cli/selftune/utils/transcript.ts +1 -0
- package/cli/selftune/utils/trigger-check.ts +1 -1
- package/cli/selftune/workflows/skill-md-writer.ts +5 -5
- package/cli/selftune/workflows/workflows.ts +1 -0
- package/package.json +38 -33
- package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
- package/packages/telemetry-contract/package.json +3 -3
- package/packages/telemetry-contract/src/index.ts +0 -1
- package/packages/telemetry-contract/src/schemas.ts +6 -24
- package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
- package/packages/ui/README.md +35 -34
- package/packages/ui/package.json +3 -3
- package/packages/ui/src/components/ActivityTimeline.tsx +49 -42
- package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
- package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
- package/packages/ui/src/components/InfoTip.tsx +4 -3
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
- package/packages/ui/src/components/section-cards.tsx +19 -24
- package/packages/ui/src/components/skill-health-grid.tsx +213 -193
- package/packages/ui/src/lib/constants.tsx +1 -0
- package/packages/ui/src/primitives/badge.tsx +12 -15
- package/packages/ui/src/primitives/button.tsx +7 -7
- package/packages/ui/src/primitives/card.tsx +15 -26
- package/packages/ui/src/primitives/checkbox.tsx +7 -8
- package/packages/ui/src/primitives/collapsible.tsx +5 -5
- package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
- package/packages/ui/src/primitives/label.tsx +6 -6
- package/packages/ui/src/primitives/select.tsx +28 -37
- package/packages/ui/src/primitives/table.tsx +17 -44
- package/packages/ui/src/primitives/tabs.tsx +14 -21
- package/packages/ui/src/primitives/tooltip.tsx +10 -22
- package/skill/SKILL.md +72 -59
- package/skill/Workflows/AlphaUpload.md +4 -4
- package/skill/Workflows/AutoActivation.md +11 -6
- package/skill/Workflows/Badge.md +22 -16
- package/skill/Workflows/Baseline.md +34 -36
- package/skill/Workflows/Composability.md +16 -11
- package/skill/Workflows/Contribute.md +26 -21
- package/skill/Workflows/Cron.md +23 -22
- package/skill/Workflows/Dashboard.md +40 -40
- package/skill/Workflows/Doctor.md +40 -34
- package/skill/Workflows/Evals.md +48 -47
- package/skill/Workflows/EvolutionMemory.md +31 -21
- package/skill/Workflows/Evolve.md +84 -82
- package/skill/Workflows/EvolveBody.md +58 -47
- package/skill/Workflows/Grade.md +16 -13
- package/skill/Workflows/ImportSkillsBench.md +9 -6
- package/skill/Workflows/Ingest.md +36 -21
- package/skill/Workflows/Initialize.md +138 -97
- package/skill/Workflows/Orchestrate.md +22 -16
- package/skill/Workflows/Replay.md +12 -7
- package/skill/Workflows/Rollback.md +13 -6
- package/skill/Workflows/Schedule.md +6 -6
- package/skill/Workflows/Sync.md +18 -11
- package/skill/Workflows/UnitTest.md +28 -17
- package/skill/Workflows/Watch.md +28 -21
- package/skill/agents/diagnosis-analyst.md +11 -0
- package/skill/agents/evolution-reviewer.md +15 -1
- package/skill/agents/integration-guide.md +10 -0
- package/skill/agents/pattern-analyst.md +12 -1
- package/skill/references/grading-methodology.md +23 -24
- package/skill/references/interactive-config.md +7 -7
- package/skill/references/invocation-taxonomy.md +22 -20
- package/skill/references/logs.md +20 -6
- package/skill/references/setup-patterns.md +4 -2
- package/.claude/agents/diagnosis-analyst.md +0 -156
- package/.claude/agents/evolution-reviewer.md +0 -180
- package/.claude/agents/integration-guide.md +0 -212
- package/.claude/agents/pattern-analyst.md +0 -160
- package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +0 -15
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* selftune uninstall — Clean removal of all selftune data and configuration.
|
|
4
|
+
*
|
|
5
|
+
* Removes:
|
|
6
|
+
* 1. Autonomy scheduling (launchd/cron/systemd + OpenClaw cron)
|
|
7
|
+
* 2. Selftune hooks from ~/.claude/settings.json (surgical — preserves user hooks)
|
|
8
|
+
* 3. Selftune-managed Claude subagents from ~/.claude/agents/
|
|
9
|
+
* 4. JSONL telemetry logs from ~/.claude/
|
|
10
|
+
* 5. Selftune config directory (~/.selftune/)
|
|
11
|
+
* 6. Ingest marker files
|
|
12
|
+
* 7. Optionally: `npm uninstall -g selftune`
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* selftune uninstall [--dry-run] [--keep-logs] [--npm-uninstall]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { parseArgs } from "node:util";
|
|
22
|
+
|
|
23
|
+
import { removeInstalledAgentFiles } from "./claude-agents.js";
|
|
24
|
+
import {
|
|
25
|
+
CLAUDE_CODE_MARKER,
|
|
26
|
+
CLAUDE_SETTINGS_PATH,
|
|
27
|
+
CODEX_INGEST_MARKER,
|
|
28
|
+
EVOLUTION_AUDIT_LOG,
|
|
29
|
+
EVOLUTION_EVIDENCE_LOG,
|
|
30
|
+
OPENCODE_INGEST_MARKER,
|
|
31
|
+
OPENCLAW_INGEST_MARKER,
|
|
32
|
+
ORCHESTRATE_LOCK,
|
|
33
|
+
ORCHESTRATE_RUN_LOG,
|
|
34
|
+
QUERY_LOG,
|
|
35
|
+
REPAIRED_SKILL_LOG,
|
|
36
|
+
REPAIRED_SKILL_SESSIONS_MARKER,
|
|
37
|
+
SELFTUNE_CONFIG_DIR,
|
|
38
|
+
SIGNAL_LOG,
|
|
39
|
+
SKILL_LOG,
|
|
40
|
+
TELEMETRY_LOG,
|
|
41
|
+
CANONICAL_LOG,
|
|
42
|
+
} from "./constants.js";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Types
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
interface UninstallResult {
|
|
49
|
+
dryRun: boolean;
|
|
50
|
+
schedule: { removed: boolean; details: string };
|
|
51
|
+
hooks: { removed: number; details: string };
|
|
52
|
+
agents: { removed: number; files: string[] };
|
|
53
|
+
logs: { removed: number; skipped: boolean; files: string[] };
|
|
54
|
+
config: { removed: boolean; path: string };
|
|
55
|
+
markers: { removed: number; files: string[] };
|
|
56
|
+
npm: { uninstalled: boolean; skipped: boolean };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Step 1: Remove autonomy scheduling
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
async function removeScheduling(dryRun: boolean): Promise<{ removed: boolean; details: string }> {
|
|
64
|
+
// Try launchd first (macOS)
|
|
65
|
+
const label = "dev.selftune.orchestrate";
|
|
66
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
67
|
+
|
|
68
|
+
if (existsSync(plistPath)) {
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
return { removed: false, details: `Would remove launchd plist: ${plistPath}` };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
// Unload before removing
|
|
74
|
+
Bun.spawnSync(["launchctl", "unload", plistPath], { stderr: "pipe" });
|
|
75
|
+
unlinkSync(plistPath);
|
|
76
|
+
return { removed: true, details: `Removed launchd plist: ${plistPath}` };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
removed: false,
|
|
80
|
+
details: `Failed to remove launchd plist: ${err instanceof Error ? err.message : String(err)}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Try OpenClaw cron jobs
|
|
86
|
+
if (dryRun) {
|
|
87
|
+
return { removed: false, details: "Would remove cron jobs via selftune cron remove" };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const proc = Bun.spawnSync(["selftune", "cron", "remove"], {
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
});
|
|
94
|
+
if (proc.exitCode === 0) {
|
|
95
|
+
return { removed: true, details: "Removed cron jobs via selftune cron remove" };
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// selftune cron remove not available or failed — not critical
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { removed: false, details: "No scheduling artifacts found" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Step 2: Remove selftune hooks from settings.json
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/** Selftune hook scripts — used to identify which entries to remove. */
|
|
109
|
+
const SELFTUNE_HOOK_SCRIPTS = [
|
|
110
|
+
"hooks/prompt-log.ts",
|
|
111
|
+
"hooks/auto-activate.ts",
|
|
112
|
+
"hooks/skill-change-guard.ts",
|
|
113
|
+
"hooks/evolution-guard.ts",
|
|
114
|
+
"hooks/skill-eval.ts",
|
|
115
|
+
"hooks/session-stop.ts",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function isSelfttuneHookEntry(entry: unknown): boolean {
|
|
119
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
120
|
+
const obj = entry as Record<string, unknown>;
|
|
121
|
+
|
|
122
|
+
// Check direct command
|
|
123
|
+
if (typeof obj.command === "string") {
|
|
124
|
+
return SELFTUNE_HOOK_SCRIPTS.some((script) => obj.command?.includes(script));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check hooks array (the nested structure used in settings.json)
|
|
128
|
+
if (Array.isArray(obj.hooks)) {
|
|
129
|
+
return obj.hooks.some(
|
|
130
|
+
(h: unknown) =>
|
|
131
|
+
typeof h === "object" &&
|
|
132
|
+
h !== null &&
|
|
133
|
+
typeof (h as Record<string, unknown>).command === "string" &&
|
|
134
|
+
SELFTUNE_HOOK_SCRIPTS.some((script) =>
|
|
135
|
+
((h as Record<string, unknown>).command as string).includes(script),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function removeHooksFromSettings(
|
|
144
|
+
dryRun: boolean,
|
|
145
|
+
settingsPath?: string,
|
|
146
|
+
): { removed: number; details: string } {
|
|
147
|
+
const path = settingsPath ?? CLAUDE_SETTINGS_PATH;
|
|
148
|
+
if (!existsSync(path)) {
|
|
149
|
+
return { removed: 0, details: "No settings.json found" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let settings: Record<string, unknown>;
|
|
153
|
+
try {
|
|
154
|
+
settings = JSON.parse(readFileSync(path, "utf-8"));
|
|
155
|
+
} catch {
|
|
156
|
+
return { removed: 0, details: "Failed to parse settings.json" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
160
|
+
if (!hooks || typeof hooks !== "object") {
|
|
161
|
+
return { removed: 0, details: "No hooks section in settings.json" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let totalRemoved = 0;
|
|
165
|
+
|
|
166
|
+
for (const key of Object.keys(hooks)) {
|
|
167
|
+
if (!Array.isArray(hooks[key])) continue;
|
|
168
|
+
|
|
169
|
+
const before = hooks[key].length;
|
|
170
|
+
hooks[key] = hooks[key].filter((entry) => !isSelfttuneHookEntry(entry));
|
|
171
|
+
const removed = before - hooks[key].length;
|
|
172
|
+
totalRemoved += removed;
|
|
173
|
+
|
|
174
|
+
// Clean up empty arrays
|
|
175
|
+
if (hooks[key].length === 0) {
|
|
176
|
+
delete hooks[key];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Clean up empty hooks object
|
|
181
|
+
if (Object.keys(hooks).length === 0) {
|
|
182
|
+
delete settings.hooks;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (totalRemoved > 0 && !dryRun) {
|
|
186
|
+
writeFileSync(path, JSON.stringify(settings, null, 2), "utf-8");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
removed: totalRemoved,
|
|
191
|
+
details: dryRun
|
|
192
|
+
? `Would remove ${totalRemoved} selftune hook entries from ${path}`
|
|
193
|
+
: `Removed ${totalRemoved} selftune hook entries from ${path}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Step 3: Remove bundled Claude subagents
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
function removeAgents(dryRun: boolean): { removed: number; files: string[] } {
|
|
202
|
+
return removeInstalledAgentFiles({ dryRun });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Step 4: Remove JSONL log files
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
const LOG_FILES = [
|
|
210
|
+
TELEMETRY_LOG,
|
|
211
|
+
SKILL_LOG,
|
|
212
|
+
REPAIRED_SKILL_LOG,
|
|
213
|
+
CANONICAL_LOG,
|
|
214
|
+
QUERY_LOG,
|
|
215
|
+
EVOLUTION_AUDIT_LOG,
|
|
216
|
+
EVOLUTION_EVIDENCE_LOG,
|
|
217
|
+
ORCHESTRATE_RUN_LOG,
|
|
218
|
+
SIGNAL_LOG,
|
|
219
|
+
ORCHESTRATE_LOCK,
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
function removeLogs(dryRun: boolean): { removed: number; files: string[] } {
|
|
223
|
+
const removed: string[] = [];
|
|
224
|
+
|
|
225
|
+
for (const logPath of LOG_FILES) {
|
|
226
|
+
if (existsSync(logPath)) {
|
|
227
|
+
if (!dryRun) {
|
|
228
|
+
try {
|
|
229
|
+
unlinkSync(logPath);
|
|
230
|
+
removed.push(logPath);
|
|
231
|
+
} catch {
|
|
232
|
+
// Skip files we can't remove
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
removed.push(logPath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { removed: removed.length, files: removed };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Step 5: Remove config directory
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
function removeConfig(dryRun: boolean): { removed: boolean; path: string } {
|
|
248
|
+
if (!existsSync(SELFTUNE_CONFIG_DIR)) {
|
|
249
|
+
return { removed: false, path: SELFTUNE_CONFIG_DIR };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!dryRun) {
|
|
253
|
+
try {
|
|
254
|
+
rmSync(SELFTUNE_CONFIG_DIR, { recursive: true, force: true });
|
|
255
|
+
return { removed: true, path: SELFTUNE_CONFIG_DIR };
|
|
256
|
+
} catch {
|
|
257
|
+
return { removed: false, path: SELFTUNE_CONFIG_DIR };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { removed: false, path: SELFTUNE_CONFIG_DIR };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Step 6: Remove ingest marker files
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
const MARKER_FILES = [
|
|
269
|
+
CLAUDE_CODE_MARKER,
|
|
270
|
+
CODEX_INGEST_MARKER,
|
|
271
|
+
OPENCODE_INGEST_MARKER,
|
|
272
|
+
OPENCLAW_INGEST_MARKER,
|
|
273
|
+
REPAIRED_SKILL_SESSIONS_MARKER,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
function removeMarkers(dryRun: boolean): { removed: number; files: string[] } {
|
|
277
|
+
const removed: string[] = [];
|
|
278
|
+
|
|
279
|
+
for (const markerPath of MARKER_FILES) {
|
|
280
|
+
if (existsSync(markerPath)) {
|
|
281
|
+
if (!dryRun) {
|
|
282
|
+
try {
|
|
283
|
+
unlinkSync(markerPath);
|
|
284
|
+
removed.push(markerPath);
|
|
285
|
+
} catch {
|
|
286
|
+
// Skip files we can't remove
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
removed.push(markerPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { removed: removed.length, files: removed };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Step 7: npm uninstall
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
async function npmUninstall(dryRun: boolean): Promise<{ uninstalled: boolean }> {
|
|
302
|
+
if (dryRun) {
|
|
303
|
+
return { uninstalled: false };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const proc = Bun.spawnSync(["npm", "uninstall", "-g", "selftune"], {
|
|
308
|
+
stdout: "pipe",
|
|
309
|
+
stderr: "pipe",
|
|
310
|
+
});
|
|
311
|
+
return { uninstalled: proc.exitCode === 0 };
|
|
312
|
+
} catch {
|
|
313
|
+
return { uninstalled: false };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Main orchestrator
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
export interface UninstallOptions {
|
|
322
|
+
dryRun: boolean;
|
|
323
|
+
keepLogs: boolean;
|
|
324
|
+
npmUninstall: boolean;
|
|
325
|
+
settingsPath?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function uninstall(options: UninstallOptions): Promise<UninstallResult> {
|
|
329
|
+
const { dryRun, keepLogs, settingsPath } = options;
|
|
330
|
+
|
|
331
|
+
// Step 1: Remove scheduling
|
|
332
|
+
const schedule = await removeScheduling(dryRun);
|
|
333
|
+
|
|
334
|
+
// Step 2: Remove hooks
|
|
335
|
+
const hooks = removeHooksFromSettings(dryRun, settingsPath);
|
|
336
|
+
|
|
337
|
+
// Step 3: Remove bundled Claude subagents
|
|
338
|
+
const agents = removeAgents(dryRun);
|
|
339
|
+
|
|
340
|
+
// Step 4: Remove logs
|
|
341
|
+
const logs = keepLogs
|
|
342
|
+
? { removed: 0, skipped: true, files: [] }
|
|
343
|
+
: { ...removeLogs(dryRun), skipped: false };
|
|
344
|
+
|
|
345
|
+
// Step 5: Remove config directory
|
|
346
|
+
const config = removeConfig(dryRun);
|
|
347
|
+
|
|
348
|
+
// Step 6: Remove ingest markers
|
|
349
|
+
const markers = removeMarkers(dryRun);
|
|
350
|
+
|
|
351
|
+
// Step 7: npm uninstall (optional)
|
|
352
|
+
const npm = options.npmUninstall
|
|
353
|
+
? { ...(await npmUninstall(dryRun)), skipped: false }
|
|
354
|
+
: { uninstalled: false, skipped: true };
|
|
355
|
+
|
|
356
|
+
return { dryRun, schedule, hooks, agents, logs, config, markers, npm };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// CLI entry point
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
export async function cliMain(): Promise<void> {
|
|
364
|
+
const { values } = parseArgs({
|
|
365
|
+
options: {
|
|
366
|
+
"dry-run": { type: "boolean", default: false },
|
|
367
|
+
"keep-logs": { type: "boolean", default: false },
|
|
368
|
+
"npm-uninstall": { type: "boolean", default: false },
|
|
369
|
+
help: { type: "boolean", default: false },
|
|
370
|
+
},
|
|
371
|
+
strict: true,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (values.help) {
|
|
375
|
+
console.log(`selftune uninstall — Clean removal of all selftune data and configuration
|
|
376
|
+
|
|
377
|
+
Usage:
|
|
378
|
+
selftune uninstall [options]
|
|
379
|
+
|
|
380
|
+
Options:
|
|
381
|
+
--dry-run Preview what would be removed without deleting anything
|
|
382
|
+
--keep-logs Preserve JSONL telemetry logs (remove everything else)
|
|
383
|
+
--npm-uninstall Also run 'npm uninstall -g selftune'
|
|
384
|
+
--help Show this help message
|
|
385
|
+
|
|
386
|
+
Removes:
|
|
387
|
+
1. Autonomy scheduling (launchd/cron/systemd)
|
|
388
|
+
2. Selftune hooks from ~/.claude/settings.json (preserves user hooks)
|
|
389
|
+
3. Selftune-managed Claude subagents from ~/.claude/agents/
|
|
390
|
+
4. JSONL telemetry logs from ~/.claude/
|
|
391
|
+
5. Selftune config directory (~/.selftune/)
|
|
392
|
+
6. Ingest marker files
|
|
393
|
+
7. npm global package (with --npm-uninstall)`);
|
|
394
|
+
process.exit(0);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const result = await uninstall({
|
|
398
|
+
dryRun: values["dry-run"] ?? false,
|
|
399
|
+
keepLogs: values["keep-logs"] ?? false,
|
|
400
|
+
npmUninstall: values["npm-uninstall"] ?? false,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
console.log(JSON.stringify(result, null, 2));
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (import.meta.main) {
|
|
408
|
+
cliMain().catch((err) => {
|
|
409
|
+
console.error(`[FATAL] ${err}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
type CanonicalPlatform,
|
|
4
5
|
type CanonicalRecord,
|
|
5
6
|
type CanonicalRecordKind,
|
|
6
7
|
isCanonicalRecord,
|
|
7
8
|
} from "@selftune/telemetry-contract";
|
|
9
|
+
|
|
8
10
|
import { CANONICAL_LOG } from "../constants.js";
|
|
9
11
|
import { readJsonl } from "./jsonl.js";
|
|
10
12
|
|
|
@@ -123,6 +123,9 @@ function sleep(ms: number): Promise<void> {
|
|
|
123
123
|
// Call LLM via agent subprocess
|
|
124
124
|
// ---------------------------------------------------------------------------
|
|
125
125
|
|
|
126
|
+
/** Effort level for Claude CLI (controls thinking depth). Opus 4.6 only for 'max'. */
|
|
127
|
+
export type EffortLevel = "low" | "medium" | "high" | "max";
|
|
128
|
+
|
|
126
129
|
/** Call LLM via agent subprocess (claude/codex/opencode). Returns raw text. */
|
|
127
130
|
export async function callViaAgent(
|
|
128
131
|
systemPrompt: string,
|
|
@@ -130,6 +133,7 @@ export async function callViaAgent(
|
|
|
130
133
|
agent: string,
|
|
131
134
|
modelFlag?: string,
|
|
132
135
|
retryOpts?: RetryOptions,
|
|
136
|
+
effort?: EffortLevel,
|
|
133
137
|
): Promise<string> {
|
|
134
138
|
// Write prompt to temp file to avoid shell quoting issues
|
|
135
139
|
const promptFile = join(tmpdir(), `selftune-llm-${Date.now()}.txt`);
|
|
@@ -145,6 +149,9 @@ export async function callViaAgent(
|
|
|
145
149
|
const resolved = resolveModelFlag(modelFlag);
|
|
146
150
|
cmd.push("--model", resolved);
|
|
147
151
|
}
|
|
152
|
+
if (effort) {
|
|
153
|
+
cmd.push("--effort", effort);
|
|
154
|
+
}
|
|
148
155
|
} else if (agent === "codex") {
|
|
149
156
|
cmd = ["codex", "exec", "--skip-git-repo-check", promptContent];
|
|
150
157
|
} else if (agent === "opencode") {
|
|
@@ -173,9 +180,10 @@ export async function callViaAgent(
|
|
|
173
180
|
env: { ...process.env, CLAUDECODE: "" },
|
|
174
181
|
});
|
|
175
182
|
|
|
176
|
-
// Longer timeout for heavier models
|
|
183
|
+
// Longer timeout for heavier models and thinking effort levels
|
|
177
184
|
const isLightModel = modelFlag === "haiku" || modelFlag?.includes("haiku");
|
|
178
|
-
const
|
|
185
|
+
const isThinking = effort === "high" || effort === "max";
|
|
186
|
+
const timeoutMs = isThinking ? 600_000 : isLightModel ? 120_000 : 300_000;
|
|
179
187
|
const timeout = setTimeout(() => proc.kill(), timeoutMs);
|
|
180
188
|
const exitCode = await proc.exited;
|
|
181
189
|
clearTimeout(timeout);
|
|
@@ -210,6 +218,125 @@ export async function callViaAgent(
|
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Call LLM via named subagent (multi-turn, agentic)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** Options for calling a named Claude Code subagent. */
|
|
226
|
+
export interface SubagentCallOptions {
|
|
227
|
+
/** Name of the subagent (synced into ~/.claude/agents/ by selftune init/update). */
|
|
228
|
+
agentName: string;
|
|
229
|
+
/** The task prompt for the subagent. */
|
|
230
|
+
prompt: string;
|
|
231
|
+
/** Optional system prompt appended to the agent's built-in instructions. */
|
|
232
|
+
appendSystemPrompt?: string;
|
|
233
|
+
/** Maximum agentic turns (default: 8). */
|
|
234
|
+
maxTurns?: number;
|
|
235
|
+
/** Model override (overrides the agent's frontmatter model). */
|
|
236
|
+
modelFlag?: string;
|
|
237
|
+
/** Effort level for thinking depth. */
|
|
238
|
+
effort?: EffortLevel;
|
|
239
|
+
/** Retry options. */
|
|
240
|
+
retryOpts?: RetryOptions;
|
|
241
|
+
/** Tools the agent is allowed to use without prompting. */
|
|
242
|
+
allowedTools?: string[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Call a named Claude Code subagent in print mode. The subagent runs its
|
|
247
|
+
* multi-turn workflow (reading files, running commands, etc.) and returns
|
|
248
|
+
* the final text output.
|
|
249
|
+
*
|
|
250
|
+
* Unlike callViaAgent(), this does NOT use --bare (agents need discovery)
|
|
251
|
+
* and passes --agent + --max-turns for agentic multi-turn behavior.
|
|
252
|
+
* Only supports the claude CLI.
|
|
253
|
+
*/
|
|
254
|
+
export async function callViaSubagent(options: SubagentCallOptions): Promise<string> {
|
|
255
|
+
const {
|
|
256
|
+
agentName,
|
|
257
|
+
prompt,
|
|
258
|
+
appendSystemPrompt,
|
|
259
|
+
maxTurns = 8,
|
|
260
|
+
modelFlag,
|
|
261
|
+
effort,
|
|
262
|
+
retryOpts,
|
|
263
|
+
allowedTools,
|
|
264
|
+
} = options;
|
|
265
|
+
|
|
266
|
+
const cmd: string[] = [
|
|
267
|
+
"claude",
|
|
268
|
+
"-p",
|
|
269
|
+
prompt,
|
|
270
|
+
"--agent",
|
|
271
|
+
agentName,
|
|
272
|
+
"--max-turns",
|
|
273
|
+
String(maxTurns),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
if (appendSystemPrompt) {
|
|
277
|
+
cmd.push("--append-system-prompt", appendSystemPrompt);
|
|
278
|
+
}
|
|
279
|
+
if (modelFlag) {
|
|
280
|
+
const resolved = resolveModelFlag(modelFlag);
|
|
281
|
+
cmd.push("--model", resolved);
|
|
282
|
+
}
|
|
283
|
+
if (effort) {
|
|
284
|
+
cmd.push("--effort", effort);
|
|
285
|
+
}
|
|
286
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
287
|
+
cmd.push("--allowedTools", ...allowedTools);
|
|
288
|
+
}
|
|
289
|
+
// Skip permissions since this runs non-interactively in a pipeline
|
|
290
|
+
cmd.push("--dangerously-skip-permissions");
|
|
291
|
+
|
|
292
|
+
const maxRetries = retryOpts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
293
|
+
const initialBackoffMs = retryOpts?.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
|
|
294
|
+
let lastError: Error | undefined;
|
|
295
|
+
|
|
296
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
297
|
+
if (attempt > 0) {
|
|
298
|
+
const backoffMs = initialBackoffMs * 2 ** (attempt - 1);
|
|
299
|
+
logger.warn(
|
|
300
|
+
`Retry ${attempt}/${maxRetries} for subagent '${agentName}' after ${backoffMs}ms backoff`,
|
|
301
|
+
);
|
|
302
|
+
await sleep(backoffMs);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const proc = Bun.spawn(cmd, {
|
|
307
|
+
stdout: "pipe",
|
|
308
|
+
stderr: "pipe",
|
|
309
|
+
env: { ...process.env, CLAUDECODE: "" },
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Subagents get a generous timeout — they do multi-turn work
|
|
313
|
+
const isThinking = effort === "high" || effort === "max";
|
|
314
|
+
const timeoutMs = isThinking ? 600_000 : 300_000;
|
|
315
|
+
const timeout = setTimeout(() => proc.kill(), timeoutMs);
|
|
316
|
+
const exitCode = await proc.exited;
|
|
317
|
+
clearTimeout(timeout);
|
|
318
|
+
|
|
319
|
+
if (exitCode !== 0) {
|
|
320
|
+
const stderr = await new Response(proc.stderr).text();
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Subagent '${agentName}' exited with code ${exitCode}.\nstderr: ${stderr.slice(0, 500)}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const raw = await new Response(proc.stdout).text();
|
|
327
|
+
return raw;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
330
|
+
if (!isTransientError(lastError) || attempt === maxRetries) {
|
|
331
|
+
throw lastError;
|
|
332
|
+
}
|
|
333
|
+
logger.warn(`Transient failure on attempt ${attempt + 1}: ${lastError.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
throw lastError ?? new Error("callViaSubagent: unexpected retry loop exit");
|
|
338
|
+
}
|
|
339
|
+
|
|
213
340
|
// ---------------------------------------------------------------------------
|
|
214
341
|
// Unified dispatcher
|
|
215
342
|
// ---------------------------------------------------------------------------
|
|
@@ -220,9 +347,10 @@ export async function callLlm(
|
|
|
220
347
|
userPrompt: string,
|
|
221
348
|
agent: string,
|
|
222
349
|
modelFlag?: string,
|
|
350
|
+
effort?: EffortLevel,
|
|
223
351
|
): Promise<string> {
|
|
224
352
|
if (!agent) {
|
|
225
353
|
throw new Error("Agent must be specified for callLlm");
|
|
226
354
|
}
|
|
227
|
-
return callViaAgent(systemPrompt, userPrompt, agent, modelFlag);
|
|
355
|
+
return callViaAgent(systemPrompt, userPrompt, agent, modelFlag, undefined, effort);
|
|
228
356
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
|
|
3
4
|
import { REPAIRED_SKILL_LOG, REPAIRED_SKILL_SESSIONS_MARKER, SKILL_LOG } from "../constants.js";
|
|
4
5
|
import type { SkillUsageRecord } from "../types.js";
|
|
5
6
|
import { loadMarker, readJsonl, saveMarker } from "./jsonl.js";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { basename, dirname } from "node:path";
|
|
7
|
+
|
|
7
8
|
import { CLAUDE_CODE_PROJECTS_DIR } from "../constants.js";
|
|
8
9
|
import type { SessionTelemetryRecord, TranscriptMetrics } from "../types.js";
|
|
9
10
|
import { isActionableQueryText } from "./query-filter.js";
|
|
@@ -64,7 +64,7 @@ export function buildBatchTriggerCheckPrompt(description: string, queries: strin
|
|
|
64
64
|
* original query order. Defaults to false for unparseable or missing lines.
|
|
65
65
|
*/
|
|
66
66
|
export function parseBatchTriggerResponse(response: string, queryCount: number): boolean[] {
|
|
67
|
-
const results: boolean[] =
|
|
67
|
+
const results: boolean[] = Array.from({ length: queryCount }, () => false);
|
|
68
68
|
const lines = response.trim().split("\n");
|
|
69
69
|
|
|
70
70
|
for (const line of lines) {
|
|
@@ -36,7 +36,7 @@ export function parseWorkflowsSection(content: string): CodifiedWorkflow[] {
|
|
|
36
36
|
// Find the end of the section (next ## heading or EOF)
|
|
37
37
|
let sectionEnd = lines.length;
|
|
38
38
|
for (let i = sectionStart; i < lines.length; i++) {
|
|
39
|
-
if (
|
|
39
|
+
if (lines[i].startsWith("## ") && lines[i].trim() !== "## Workflows") {
|
|
40
40
|
sectionEnd = i;
|
|
41
41
|
break;
|
|
42
42
|
}
|
|
@@ -155,7 +155,7 @@ export function appendWorkflow(content: string, workflow: CodifiedWorkflow): str
|
|
|
155
155
|
// Find the end of the workflows section (next ## heading or EOF)
|
|
156
156
|
let sectionEnd = lines.length;
|
|
157
157
|
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
158
|
-
if (
|
|
158
|
+
if (lines[i].startsWith("## ")) {
|
|
159
159
|
sectionEnd = i;
|
|
160
160
|
break;
|
|
161
161
|
}
|
|
@@ -210,7 +210,7 @@ export function removeWorkflow(content: string, name: string): string {
|
|
|
210
210
|
// Find the end of the workflows section
|
|
211
211
|
let sectionEnd = lines.length;
|
|
212
212
|
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
213
|
-
if (
|
|
213
|
+
if (lines[i].startsWith("## ")) {
|
|
214
214
|
sectionEnd = i;
|
|
215
215
|
break;
|
|
216
216
|
}
|
|
@@ -226,7 +226,7 @@ export function removeWorkflow(content: string, name: string): string {
|
|
|
226
226
|
// Find the end of this subsection (next ### or ## or sectionEnd)
|
|
227
227
|
subEnd = sectionEnd;
|
|
228
228
|
for (let j = i + 1; j < sectionEnd; j++) {
|
|
229
|
-
if (
|
|
229
|
+
if (lines[j].startsWith("### ")) {
|
|
230
230
|
subEnd = j;
|
|
231
231
|
break;
|
|
232
232
|
}
|
|
@@ -255,7 +255,7 @@ export function removeWorkflow(content: string, name: string): string {
|
|
|
255
255
|
|
|
256
256
|
// Check if the workflows section is now empty
|
|
257
257
|
const remaining = [...before.slice(sectionStart + 1), ...after.slice(0, sectionEnd - removeTo)];
|
|
258
|
-
const hasRemainingWorkflows = remaining.some((l) =>
|
|
258
|
+
const hasRemainingWorkflows = remaining.some((l) => l.startsWith("### "));
|
|
259
259
|
|
|
260
260
|
if (!hasRemainingWorkflows) {
|
|
261
261
|
// Remove the entire ## Workflows section (heading + any blank lines)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { parseArgs } from "node:util";
|
|
13
|
+
|
|
13
14
|
import { getDb } from "../localdb/db.js";
|
|
14
15
|
import { querySessionTelemetry, querySkillUsageRecords } from "../localdb/queries.js";
|
|
15
16
|
import type {
|