selftune 0.2.6 → 0.2.9
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 +1 -0
- package/apps/local-dashboard/dist/assets/index-Bs3Y4ixf.css +1 -0
- package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +60 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-B7VF2Ipl.js → vendor-table-dK1QMLq9.js} +1 -1
- package/apps/local-dashboard/dist/assets/{vendor-ui-r2k_Ku_V.js → vendor-ui-CO2mrx6e.js} +60 -65
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/activation-rules.ts +57 -18
- package/cli/selftune/agent-guidance.ts +96 -0
- package/cli/selftune/alpha-identity.ts +156 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +151 -0
- package/cli/selftune/alpha-upload/client.ts +113 -0
- package/cli/selftune/alpha-upload/flush.ts +191 -0
- package/cli/selftune/alpha-upload/index.ts +194 -0
- package/cli/selftune/alpha-upload/queue.ts +252 -0
- package/cli/selftune/alpha-upload/stage-canonical.ts +251 -0
- package/cli/selftune/alpha-upload-contract.ts +52 -0
- package/cli/selftune/auth/device-code.ts +110 -0
- package/cli/selftune/auto-update.ts +130 -0
- package/cli/selftune/badge/badge.ts +19 -9
- package/cli/selftune/canonical-export.ts +16 -3
- package/cli/selftune/constants.ts +28 -8
- package/cli/selftune/contribute/bundle.ts +33 -5
- package/cli/selftune/dashboard-contract.ts +32 -1
- package/cli/selftune/dashboard-server.ts +215 -693
- package/cli/selftune/dashboard.ts +1 -1
- package/cli/selftune/eval/baseline.ts +11 -7
- package/cli/selftune/eval/hooks-to-evals.ts +39 -15
- package/cli/selftune/eval/synthetic-evals.ts +54 -1
- package/cli/selftune/evolution/audit.ts +24 -19
- package/cli/selftune/evolution/constitutional.ts +176 -0
- package/cli/selftune/evolution/evidence.ts +18 -13
- package/cli/selftune/evolution/evolve-body.ts +104 -7
- package/cli/selftune/evolution/evolve.ts +195 -22
- package/cli/selftune/evolution/propose-body.ts +18 -1
- package/cli/selftune/evolution/propose-description.ts +27 -2
- package/cli/selftune/evolution/rollback.ts +11 -15
- package/cli/selftune/export.ts +84 -0
- package/cli/selftune/grading/auto-grade.ts +14 -4
- package/cli/selftune/grading/grade-session.ts +17 -6
- package/cli/selftune/hooks/auto-activate.ts +5 -0
- package/cli/selftune/hooks/evolution-guard.ts +25 -11
- package/cli/selftune/hooks/prompt-log.ts +23 -9
- package/cli/selftune/hooks/session-stop.ts +78 -15
- package/cli/selftune/hooks/skill-eval.ts +189 -10
- package/cli/selftune/index.ts +274 -2
- package/cli/selftune/ingestors/claude-replay.ts +48 -21
- package/cli/selftune/init.ts +260 -49
- package/cli/selftune/last.ts +7 -7
- package/cli/selftune/localdb/db.ts +90 -10
- package/cli/selftune/localdb/direct-write.ts +573 -0
- package/cli/selftune/localdb/materialize.ts +296 -42
- package/cli/selftune/localdb/queries.ts +482 -32
- package/cli/selftune/localdb/schema.ts +153 -1
- package/cli/selftune/monitoring/watch.ts +27 -8
- package/cli/selftune/normalization.ts +88 -15
- package/cli/selftune/observability.ts +257 -5
- package/cli/selftune/orchestrate.ts +176 -53
- package/cli/selftune/quickstart.ts +34 -10
- package/cli/selftune/repair/skill-usage.ts +15 -2
- package/cli/selftune/routes/actions.ts +77 -0
- package/cli/selftune/routes/badge.ts +66 -0
- package/cli/selftune/routes/doctor.ts +12 -0
- package/cli/selftune/routes/index.ts +14 -0
- package/cli/selftune/routes/orchestrate-runs.ts +13 -0
- package/cli/selftune/routes/overview.ts +14 -0
- package/cli/selftune/routes/report.ts +293 -0
- package/cli/selftune/routes/skill-report.ts +230 -0
- package/cli/selftune/status.ts +203 -7
- package/cli/selftune/sync.ts +14 -1
- package/cli/selftune/types.ts +52 -2
- package/cli/selftune/utils/jsonl.ts +58 -1
- package/cli/selftune/utils/selftune-meta.ts +38 -0
- package/cli/selftune/utils/skill-log.ts +30 -4
- package/cli/selftune/utils/transcript.ts +15 -0
- package/cli/selftune/workflows/workflows.ts +7 -6
- package/package.json +11 -6
- package/packages/telemetry-contract/fixtures/complete-push.ts +184 -0
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +58 -0
- package/packages/telemetry-contract/fixtures/golden.json +1 -0
- package/packages/telemetry-contract/fixtures/index.ts +4 -0
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +40 -0
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +79 -0
- package/packages/telemetry-contract/package.json +6 -1
- package/packages/telemetry-contract/src/schemas.ts +196 -0
- package/packages/telemetry-contract/src/types.ts +3 -1
- package/packages/telemetry-contract/src/validators.ts +3 -1
- package/packages/telemetry-contract/tests/compatibility.test.ts +144 -0
- package/packages/ui/package.json +4 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +61 -29
- package/packages/ui/src/components/section-cards.tsx +31 -14
- package/packages/ui/src/types.ts +1 -0
- package/skill/SKILL.md +214 -174
- package/skill/Workflows/AlphaUpload.md +45 -0
- package/skill/Workflows/Baseline.md +18 -12
- package/skill/Workflows/Composability.md +3 -3
- package/skill/Workflows/Dashboard.md +39 -91
- package/skill/Workflows/Doctor.md +93 -66
- package/skill/Workflows/Evals.md +49 -40
- package/skill/Workflows/Evolve.md +76 -28
- package/skill/Workflows/EvolveBody.md +37 -38
- package/skill/Workflows/Initialize.md +145 -26
- package/skill/Workflows/Orchestrate.md +11 -2
- package/skill/Workflows/Sync.md +23 -0
- package/skill/Workflows/Watch.md +2 -5
- package/skill/agents/diagnosis-analyst.md +163 -0
- package/skill/agents/evolution-reviewer.md +149 -0
- package/skill/agents/integration-guide.md +154 -0
- package/skill/agents/pattern-analyst.md +149 -0
- package/skill/assets/multi-skill-settings.json +1 -1
- package/skill/assets/single-skill-settings.json +1 -1
- package/skill/references/interactive-config.md +39 -0
- package/skill/references/invocation-taxonomy.md +34 -0
- package/skill/references/logs.md +15 -1
- package/skill/references/setup-patterns.md +3 -3
- package/skill/settings_snippet.json +1 -1
- package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +0 -1
- package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +0 -15
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +0 -60
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Claude Code PostToolUse hook: skill-eval.ts
|
|
4
4
|
*
|
|
5
|
-
* Fires whenever Claude reads a file. If
|
|
5
|
+
* Fires whenever Claude reads a file or invokes a skill. If the file is a
|
|
6
|
+
* SKILL.md or the tool is a Skill invocation, this hook:
|
|
6
7
|
* 1. Finds the triggering user query from the transcript JSONL
|
|
7
|
-
* 2.
|
|
8
|
+
* 2. Writes a usage record to SQLite via writeSkillUsageToDb()
|
|
8
9
|
*
|
|
9
10
|
* This builds a real-usage eval dataset over time, seeding the
|
|
10
11
|
* `should_trigger: true` half of trigger evals.
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
getLatestPromptIdentity,
|
|
24
25
|
} from "../normalization.js";
|
|
25
26
|
import type { PostToolUsePayload, SkillUsageRecord } from "../types.js";
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
import { classifySkillPath } from "../utils/skill-discovery.js";
|
|
28
29
|
import { getLastUserMessage } from "../utils/transcript.js";
|
|
29
30
|
|
|
@@ -94,17 +95,26 @@ export function countSkillToolInvocations(transcriptPath: string, skillName: str
|
|
|
94
95
|
* Core processing logic, exported for testability.
|
|
95
96
|
* Returns the record that was appended, or null if skipped.
|
|
96
97
|
*
|
|
97
|
-
*
|
|
98
|
+
* Handles two PostToolUse event types:
|
|
99
|
+
* - Read: when a SKILL.md file is read (original path)
|
|
100
|
+
* - Skill: when a skill is explicitly invoked via the Skill tool
|
|
101
|
+
*
|
|
102
|
+
* For Read events, checks whether the Read of SKILL.md was
|
|
98
103
|
* preceded by an actual Skill tool invocation in the same transcript.
|
|
99
104
|
* If not, the record is still logged but marked as triggered: false.
|
|
100
105
|
*/
|
|
101
|
-
export function processToolUse(
|
|
106
|
+
export async function processToolUse(
|
|
102
107
|
payload: PostToolUsePayload,
|
|
103
108
|
logPath: string = SKILL_LOG,
|
|
104
109
|
canonicalLogPath: string = CANONICAL_LOG,
|
|
105
110
|
promptStatePath?: string,
|
|
106
|
-
): SkillUsageRecord | null {
|
|
107
|
-
//
|
|
111
|
+
): Promise<SkillUsageRecord | null> {
|
|
112
|
+
// Handle Skill tool invocations (e.g., Skill(selftune))
|
|
113
|
+
if (payload.tool_name === "Skill") {
|
|
114
|
+
return await processSkillToolUse(payload, logPath, canonicalLogPath, promptStatePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Only care about Read tool for SKILL.md detection
|
|
108
118
|
if (payload.tool_name !== "Read") return null;
|
|
109
119
|
|
|
110
120
|
const rawPath = payload.tool_input?.file_path;
|
|
@@ -132,11 +142,10 @@ export function processToolUse(
|
|
|
132
142
|
...skillPathMetadata,
|
|
133
143
|
query,
|
|
134
144
|
triggered: wasInvoked,
|
|
145
|
+
invocation_type: "contextual",
|
|
135
146
|
source: "claude_code",
|
|
136
147
|
};
|
|
137
148
|
|
|
138
|
-
appendJsonl(logPath, record);
|
|
139
|
-
|
|
140
149
|
const baseInput: CanonicalBaseInput = {
|
|
141
150
|
platform: "claude_code",
|
|
142
151
|
capture_mode: "hook",
|
|
@@ -155,6 +164,7 @@ export function processToolUse(
|
|
|
155
164
|
const { invocation_mode, confidence } = deriveInvocationMode({
|
|
156
165
|
has_skill_tool_call: wasInvoked,
|
|
157
166
|
has_skill_md_read: !wasInvoked,
|
|
167
|
+
hook_invocation_type: "contextual",
|
|
158
168
|
});
|
|
159
169
|
const canonical = buildCanonicalSkillInvocation({
|
|
160
170
|
...baseInput,
|
|
@@ -172,6 +182,175 @@ export function processToolUse(
|
|
|
172
182
|
confidence,
|
|
173
183
|
tool_name: payload.tool_name,
|
|
174
184
|
});
|
|
185
|
+
|
|
186
|
+
// Write unified record to skill_invocations (replaces separate writeSkillUsageToDb call)
|
|
187
|
+
try {
|
|
188
|
+
const { writeSkillCheckToDb } = await import("../localdb/direct-write.js");
|
|
189
|
+
writeSkillCheckToDb({
|
|
190
|
+
...canonical,
|
|
191
|
+
query: record.query,
|
|
192
|
+
skill_path: record.skill_path,
|
|
193
|
+
skill_scope: record.skill_scope,
|
|
194
|
+
source: record.source,
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
/* hooks must never block */
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
appendCanonicalRecord(canonical, canonicalLogPath);
|
|
201
|
+
|
|
202
|
+
return record;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Classify how a Skill tool invocation was triggered:
|
|
207
|
+
*
|
|
208
|
+
* explicit — User typed /skillName (slash command) or skill was already loaded
|
|
209
|
+
* implicit — User mentioned the skill by name in their prompt; Claude invoked it
|
|
210
|
+
* inferred — User never mentioned the skill; Claude chose it autonomously
|
|
211
|
+
*
|
|
212
|
+
* Examples:
|
|
213
|
+
* "/selftune" → explicit (slash command)
|
|
214
|
+
* "setup selftune" → implicit (user named the skill)
|
|
215
|
+
* "show me the dashboard" → Browser → inferred (user never said "browser")
|
|
216
|
+
*/
|
|
217
|
+
function classifyInvocationType(
|
|
218
|
+
query: string,
|
|
219
|
+
skillName: string,
|
|
220
|
+
): "explicit" | "implicit" | "inferred" {
|
|
221
|
+
const trimmed = query.trim();
|
|
222
|
+
const skillLower = skillName.toLowerCase();
|
|
223
|
+
|
|
224
|
+
// /selftune or /selftune args
|
|
225
|
+
if (trimmed.toLowerCase().startsWith(`/${skillLower}`)) return "explicit";
|
|
226
|
+
|
|
227
|
+
// <command-name>/selftune</command-name> pattern (skill already loaded)
|
|
228
|
+
if (trimmed.includes(`<command-name>/${skillLower}</command-name>`)) return "explicit";
|
|
229
|
+
if (trimmed.includes(`<command-name>${skillLower}</command-name>`)) return "explicit";
|
|
230
|
+
|
|
231
|
+
// User mentioned the skill name in their prompt (case-insensitive word boundary)
|
|
232
|
+
const mentionPattern = new RegExp(
|
|
233
|
+
`\\b${skillLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
234
|
+
"i",
|
|
235
|
+
);
|
|
236
|
+
if (mentionPattern.test(trimmed)) return "implicit";
|
|
237
|
+
|
|
238
|
+
// Claude chose this skill entirely on its own
|
|
239
|
+
return "inferred";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handle Skill tool invocations (e.g., Skill(selftune), Skill(Browser)).
|
|
244
|
+
* The tool_input contains { skill: "skillName", args?: "..." }.
|
|
245
|
+
* Classifies as explicit, implicit, or inferred based on user prompt.
|
|
246
|
+
*/
|
|
247
|
+
/**
|
|
248
|
+
* Detect if the current transcript belongs to a subagent.
|
|
249
|
+
* Returns the agent type (e.g., "Explore", "Engineer") or "main".
|
|
250
|
+
*/
|
|
251
|
+
function detectAgentType(transcriptPath: string): string {
|
|
252
|
+
if (!transcriptPath) return "main";
|
|
253
|
+
try {
|
|
254
|
+
// Subagent transcripts live under .../subagents/agent-<id>.jsonl
|
|
255
|
+
if (!/[/\\]subagents[/\\]/.test(transcriptPath)) return "main";
|
|
256
|
+
const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
|
|
257
|
+
if (existsSync(metaPath)) {
|
|
258
|
+
const meta: unknown = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
259
|
+
const agentType =
|
|
260
|
+
typeof meta === "object" && meta !== null
|
|
261
|
+
? (meta as Record<string, unknown>).agentType
|
|
262
|
+
: undefined;
|
|
263
|
+
return typeof agentType === "string" ? agentType : "subagent";
|
|
264
|
+
}
|
|
265
|
+
return "subagent";
|
|
266
|
+
} catch {
|
|
267
|
+
return "main";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function processSkillToolUse(
|
|
272
|
+
payload: PostToolUsePayload,
|
|
273
|
+
_logPath: string,
|
|
274
|
+
canonicalLogPath: string,
|
|
275
|
+
promptStatePath?: string,
|
|
276
|
+
): Promise<SkillUsageRecord | null> {
|
|
277
|
+
const rawSkill = payload.tool_input?.skill;
|
|
278
|
+
const skillName = typeof rawSkill === "string" ? rawSkill : null;
|
|
279
|
+
if (!skillName) return null;
|
|
280
|
+
|
|
281
|
+
const transcriptPath = payload.transcript_path ?? "";
|
|
282
|
+
const sessionId = payload.session_id ?? "unknown";
|
|
283
|
+
|
|
284
|
+
const query = getLastUserMessage(transcriptPath);
|
|
285
|
+
if (!query) return null;
|
|
286
|
+
|
|
287
|
+
const invocationType = classifyInvocationType(query, skillName);
|
|
288
|
+
const invocationIndex = countSkillToolInvocations(transcriptPath, skillName) - 1;
|
|
289
|
+
|
|
290
|
+
const record: SkillUsageRecord = {
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
session_id: sessionId,
|
|
293
|
+
skill_name: skillName,
|
|
294
|
+
skill_path: "",
|
|
295
|
+
query,
|
|
296
|
+
triggered: true,
|
|
297
|
+
invocation_type: invocationType,
|
|
298
|
+
source: "claude_code",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const baseInput: CanonicalBaseInput = {
|
|
302
|
+
platform: "claude_code",
|
|
303
|
+
capture_mode: "hook",
|
|
304
|
+
source_session_kind: "interactive",
|
|
305
|
+
session_id: sessionId,
|
|
306
|
+
raw_source_ref: {
|
|
307
|
+
path: transcriptPath || undefined,
|
|
308
|
+
event_type: "PostToolUse",
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
const latestPrompt = getLatestPromptIdentity(sessionId, promptStatePath, canonicalLogPath);
|
|
312
|
+
const promptId =
|
|
313
|
+
latestPrompt.last_actionable_prompt_id ??
|
|
314
|
+
latestPrompt.last_prompt_id ??
|
|
315
|
+
derivePromptId(sessionId, 0);
|
|
316
|
+
const { invocation_mode, confidence } = deriveInvocationMode({
|
|
317
|
+
hook_invocation_type: invocationType,
|
|
318
|
+
});
|
|
319
|
+
// Detect if this invocation is from a subagent
|
|
320
|
+
const agentType = detectAgentType(transcriptPath);
|
|
321
|
+
|
|
322
|
+
const canonical = buildCanonicalSkillInvocation({
|
|
323
|
+
...baseInput,
|
|
324
|
+
skill_invocation_id: deriveSkillInvocationId(
|
|
325
|
+
sessionId,
|
|
326
|
+
skillName,
|
|
327
|
+
Math.max(invocationIndex, 0),
|
|
328
|
+
),
|
|
329
|
+
occurred_at: record.timestamp,
|
|
330
|
+
matched_prompt_id: promptId,
|
|
331
|
+
skill_name: skillName,
|
|
332
|
+
skill_path: "",
|
|
333
|
+
invocation_mode,
|
|
334
|
+
triggered: true,
|
|
335
|
+
confidence,
|
|
336
|
+
tool_name: payload.tool_name,
|
|
337
|
+
agent_type: agentType,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Write unified record to skill_invocations (replaces separate writeSkillUsageToDb call)
|
|
341
|
+
try {
|
|
342
|
+
const { writeSkillCheckToDb } = await import("../localdb/direct-write.js");
|
|
343
|
+
writeSkillCheckToDb({
|
|
344
|
+
...canonical,
|
|
345
|
+
query: record.query,
|
|
346
|
+
skill_path: record.skill_path,
|
|
347
|
+
skill_scope: record.skill_scope,
|
|
348
|
+
source: record.source,
|
|
349
|
+
});
|
|
350
|
+
} catch {
|
|
351
|
+
/* hooks must never block */
|
|
352
|
+
}
|
|
353
|
+
|
|
175
354
|
appendCanonicalRecord(canonical, canonicalLogPath);
|
|
176
355
|
|
|
177
356
|
return record;
|
|
@@ -181,7 +360,7 @@ export function processToolUse(
|
|
|
181
360
|
if (import.meta.main) {
|
|
182
361
|
try {
|
|
183
362
|
const payload: PostToolUsePayload = JSON.parse(await Bun.stdin.text());
|
|
184
|
-
processToolUse(payload);
|
|
363
|
+
await processToolUse(payload);
|
|
185
364
|
} catch {
|
|
186
365
|
// silent — hooks must never block Claude
|
|
187
366
|
}
|
package/cli/selftune/index.ts
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
* selftune workflows — Discover and manage multi-skill workflows
|
|
22
22
|
* selftune quickstart — Guided onboarding: init, ingest, status, and suggestions
|
|
23
23
|
* selftune repair-skill-usage — Rebuild trustworthy skill usage from transcripts
|
|
24
|
+
* selftune export — Export SQLite data to JSONL files
|
|
24
25
|
* selftune export-canonical — Export canonical telemetry for downstream ingestion
|
|
25
26
|
* selftune telemetry — Manage anonymous usage analytics (status, enable, disable)
|
|
27
|
+
* selftune alpha <subcommand> — Alpha program management (upload)
|
|
26
28
|
* selftune hook <name> — Run a hook by name (prompt-log, session-stop, etc.)
|
|
27
29
|
*/
|
|
28
30
|
|
|
@@ -53,7 +55,9 @@ Commands:
|
|
|
53
55
|
workflows Discover and manage multi-skill workflows
|
|
54
56
|
quickstart Guided onboarding: init, ingest, status, and suggestions
|
|
55
57
|
repair-skill-usage Rebuild trustworthy skill usage from transcripts
|
|
58
|
+
export Export SQLite data to JSONL files
|
|
56
59
|
export-canonical Export canonical telemetry for downstream ingestion
|
|
60
|
+
alpha <subcommand> Alpha program management (upload)
|
|
57
61
|
telemetry Manage anonymous usage analytics (status, enable, disable)
|
|
58
62
|
hook <name> Run a hook by name (prompt-log, session-stop, etc.)
|
|
59
63
|
|
|
@@ -68,6 +72,12 @@ if (command && command !== "--help" && command !== "-h") {
|
|
|
68
72
|
.catch(() => {});
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// Auto-update check (skip for hooks — they must be fast — and --help)
|
|
76
|
+
if (command && command !== "hook" && command !== "--help" && command !== "-h") {
|
|
77
|
+
const { autoUpdate } = await import("./auto-update.js");
|
|
78
|
+
await autoUpdate();
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
if (!command) {
|
|
72
82
|
// Show status by default — same as `selftune status`
|
|
73
83
|
const { cliMain: statusMain } = await import("./status.js");
|
|
@@ -263,7 +273,6 @@ Run 'selftune eval <action> --help' for action-specific options.`);
|
|
|
263
273
|
process.exit(0);
|
|
264
274
|
}
|
|
265
275
|
const { parseArgs } = await import("node:util");
|
|
266
|
-
const { readJsonl } = await import("./utils/jsonl.js");
|
|
267
276
|
const { TELEMETRY_LOG } = await import("./constants.js");
|
|
268
277
|
const { analyzeComposability } = await import("./eval/composability.js");
|
|
269
278
|
let values: ReturnType<typeof parseArgs>["values"];
|
|
@@ -287,7 +296,22 @@ Run 'selftune eval <action> --help' for action-specific options.`);
|
|
|
287
296
|
process.exit(1);
|
|
288
297
|
}
|
|
289
298
|
const logPath = values["telemetry-log"] ?? TELEMETRY_LOG;
|
|
290
|
-
|
|
299
|
+
let telemetry: unknown[];
|
|
300
|
+
if (logPath === TELEMETRY_LOG) {
|
|
301
|
+
try {
|
|
302
|
+
const { getDb } = await import("./localdb/db.js");
|
|
303
|
+
const { querySessionTelemetry } = await import("./localdb/queries.js");
|
|
304
|
+
const db = getDb();
|
|
305
|
+
telemetry = querySessionTelemetry(db);
|
|
306
|
+
} catch {
|
|
307
|
+
// DB unavailable — fall back to JSONL
|
|
308
|
+
const { readJsonl } = await import("./utils/jsonl.js");
|
|
309
|
+
telemetry = readJsonl(logPath);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
const { readJsonl } = await import("./utils/jsonl.js");
|
|
313
|
+
telemetry = readJsonl(logPath);
|
|
314
|
+
}
|
|
291
315
|
const rawWindow = values.window as string | undefined;
|
|
292
316
|
if (rawWindow !== undefined && !/^[1-9]\d*$/.test(rawWindow)) {
|
|
293
317
|
console.error("Invalid --window value. Use a positive integer number of days.");
|
|
@@ -458,6 +482,67 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
|
|
|
458
482
|
cliMain();
|
|
459
483
|
break;
|
|
460
484
|
}
|
|
485
|
+
case "export": {
|
|
486
|
+
const { parseArgs } = await import("node:util");
|
|
487
|
+
let values: ReturnType<typeof parseArgs>["values"];
|
|
488
|
+
let positionals: string[];
|
|
489
|
+
try {
|
|
490
|
+
({ values, positionals } = parseArgs({
|
|
491
|
+
options: {
|
|
492
|
+
output: { type: "string", short: "o" },
|
|
493
|
+
since: { type: "string" },
|
|
494
|
+
help: { type: "boolean", short: "h" },
|
|
495
|
+
},
|
|
496
|
+
allowPositionals: true,
|
|
497
|
+
strict: true,
|
|
498
|
+
}));
|
|
499
|
+
} catch (error) {
|
|
500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
501
|
+
console.error(`Invalid arguments: ${message}`);
|
|
502
|
+
console.error("Run 'selftune export --help' for usage.");
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
if (values.help) {
|
|
506
|
+
console.log(`selftune export — Export SQLite data to JSONL files
|
|
507
|
+
|
|
508
|
+
Usage:
|
|
509
|
+
selftune export [tables...] [options]
|
|
510
|
+
|
|
511
|
+
Tables (default: all):
|
|
512
|
+
telemetry Session telemetry records
|
|
513
|
+
skills Skill usage records
|
|
514
|
+
queries Query log entries
|
|
515
|
+
audit Evolution audit trail
|
|
516
|
+
evidence Evolution evidence trail
|
|
517
|
+
signals Improvement signals
|
|
518
|
+
orchestrate Orchestrate run log
|
|
519
|
+
|
|
520
|
+
Options:
|
|
521
|
+
-o, --output <dir> Output directory (default: current directory)
|
|
522
|
+
--since <date> Only export records after this date (ISO 8601)
|
|
523
|
+
-h, --help Show this help`);
|
|
524
|
+
process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
const { exportToJsonl } = await import("./export.js");
|
|
527
|
+
const outputDir = (values.output as string | undefined) ?? process.cwd();
|
|
528
|
+
const since = values.since as string | undefined;
|
|
529
|
+
const tables = positionals.length > 0 ? positionals : undefined;
|
|
530
|
+
try {
|
|
531
|
+
const result = exportToJsonl({ outputDir, since, tables });
|
|
532
|
+
console.log(
|
|
533
|
+
`Exported ${result.records} records to ${result.files.length} files in ${outputDir}`,
|
|
534
|
+
);
|
|
535
|
+
for (const file of result.files) {
|
|
536
|
+
console.log(` ${file}`);
|
|
537
|
+
}
|
|
538
|
+
} catch (err: unknown) {
|
|
539
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
540
|
+
console.error(`Export failed: ${message}`);
|
|
541
|
+
console.error("Ensure the SQLite database exists. Run 'selftune sync' first if needed.");
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
461
546
|
case "export-canonical": {
|
|
462
547
|
const { cliMain } = await import("./canonical-export.js");
|
|
463
548
|
cliMain();
|
|
@@ -468,6 +553,193 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
|
|
|
468
553
|
await cliMain();
|
|
469
554
|
break;
|
|
470
555
|
}
|
|
556
|
+
case "alpha": {
|
|
557
|
+
const sub = process.argv[2];
|
|
558
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
559
|
+
console.log(`selftune alpha — Alpha program management
|
|
560
|
+
|
|
561
|
+
Usage:
|
|
562
|
+
selftune alpha <subcommand> [options]
|
|
563
|
+
|
|
564
|
+
Subcommands:
|
|
565
|
+
upload Run a manual alpha data upload cycle
|
|
566
|
+
relink Re-authenticate with the cloud (revokes old key, issues new one)
|
|
567
|
+
|
|
568
|
+
Run 'selftune alpha <subcommand> --help' for subcommand-specific options.`);
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
|
|
572
|
+
switch (sub) {
|
|
573
|
+
case "upload": {
|
|
574
|
+
const { parseArgs } = await import("node:util");
|
|
575
|
+
let values: ReturnType<typeof parseArgs>["values"];
|
|
576
|
+
try {
|
|
577
|
+
({ values } = parseArgs({
|
|
578
|
+
options: {
|
|
579
|
+
"dry-run": { type: "boolean", default: false },
|
|
580
|
+
help: { type: "boolean", short: "h", default: false },
|
|
581
|
+
},
|
|
582
|
+
strict: true,
|
|
583
|
+
}));
|
|
584
|
+
} catch (error) {
|
|
585
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
586
|
+
console.error(`Invalid arguments: ${message}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
if (values.help) {
|
|
590
|
+
console.log(`selftune alpha upload — Run a manual alpha data upload cycle
|
|
591
|
+
|
|
592
|
+
Usage:
|
|
593
|
+
selftune alpha upload [--dry-run]
|
|
594
|
+
|
|
595
|
+
Options:
|
|
596
|
+
--dry-run Log what would be uploaded without sending
|
|
597
|
+
-h, --help Show this help message
|
|
598
|
+
|
|
599
|
+
Output:
|
|
600
|
+
JSON summary: { enrolled, prepared, sent, failed, skipped, guidance? }`);
|
|
601
|
+
process.exit(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
|
|
605
|
+
const { getAlphaGuidance } = await import("./agent-guidance.js");
|
|
606
|
+
const { readAlphaIdentity } = await import("./alpha-identity.js");
|
|
607
|
+
const { getDb } = await import("./localdb/db.js");
|
|
608
|
+
const { runUploadCycle } = await import("./alpha-upload/index.js");
|
|
609
|
+
const { getSelftuneVersion, readConfiguredAgentType } = await import(
|
|
610
|
+
"./utils/selftune-meta.js"
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
|
|
614
|
+
if (!identity?.enrolled) {
|
|
615
|
+
const guidance = getAlphaGuidance(identity);
|
|
616
|
+
console.log(
|
|
617
|
+
JSON.stringify(
|
|
618
|
+
{
|
|
619
|
+
enrolled: false,
|
|
620
|
+
prepared: 0,
|
|
621
|
+
sent: 0,
|
|
622
|
+
failed: 0,
|
|
623
|
+
skipped: 0,
|
|
624
|
+
guidance,
|
|
625
|
+
},
|
|
626
|
+
null,
|
|
627
|
+
2,
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
console.error(`[alpha upload] ${guidance.message}`);
|
|
631
|
+
console.error(`[alpha upload] Next: ${guidance.next_command}`);
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!identity.user_id?.trim() || !identity.api_key?.trim()) {
|
|
636
|
+
const guidance = getAlphaGuidance(identity);
|
|
637
|
+
console.log(
|
|
638
|
+
JSON.stringify(
|
|
639
|
+
{
|
|
640
|
+
enrolled: true,
|
|
641
|
+
prepared: 0,
|
|
642
|
+
sent: 0,
|
|
643
|
+
failed: 0,
|
|
644
|
+
skipped: 0,
|
|
645
|
+
guidance,
|
|
646
|
+
},
|
|
647
|
+
null,
|
|
648
|
+
2,
|
|
649
|
+
),
|
|
650
|
+
);
|
|
651
|
+
console.error(`[alpha upload] ${guidance.message}`);
|
|
652
|
+
console.error(`[alpha upload] Next: ${guidance.next_command}`);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const db = getDb();
|
|
657
|
+
|
|
658
|
+
const result = await runUploadCycle(db, {
|
|
659
|
+
enrolled: true,
|
|
660
|
+
userId: identity.user_id,
|
|
661
|
+
agentType: readConfiguredAgentType(SELFTUNE_CONFIG_PATH, "unknown"),
|
|
662
|
+
selftuneVersion: getSelftuneVersion(),
|
|
663
|
+
dryRun: values["dry-run"] ?? false,
|
|
664
|
+
apiKey: identity.api_key,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
console.log(JSON.stringify(result, null, 2));
|
|
668
|
+
process.exit(result.failed > 0 ? 1 : 0);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case "relink": {
|
|
672
|
+
const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
|
|
673
|
+
const { readAlphaIdentity, writeAlphaIdentity, generateUserId } = await import(
|
|
674
|
+
"./alpha-identity.js"
|
|
675
|
+
);
|
|
676
|
+
const { requestDeviceCode, pollDeviceCode } = await import("./auth/device-code.js");
|
|
677
|
+
const { chmodSync } = await import("node:fs");
|
|
678
|
+
|
|
679
|
+
const existingIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
|
|
680
|
+
process.stderr.write("[alpha relink] Starting device-code authentication flow...\n");
|
|
681
|
+
|
|
682
|
+
const grant = await requestDeviceCode();
|
|
683
|
+
|
|
684
|
+
console.log(
|
|
685
|
+
JSON.stringify({
|
|
686
|
+
level: "info",
|
|
687
|
+
code: "device_code_issued",
|
|
688
|
+
verification_url: grant.verification_url,
|
|
689
|
+
user_code: grant.user_code,
|
|
690
|
+
expires_in: grant.expires_in,
|
|
691
|
+
message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
// Try to open browser
|
|
696
|
+
try {
|
|
697
|
+
const url = `${grant.verification_url}?code=${grant.user_code}`;
|
|
698
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
699
|
+
process.stderr.write("[alpha relink] Browser opened. Waiting for approval...\n");
|
|
700
|
+
} catch {
|
|
701
|
+
process.stderr.write(
|
|
702
|
+
"[alpha relink] Could not open browser. Visit the URL above manually.\n",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
process.stderr.write("[alpha relink] Polling");
|
|
707
|
+
const result = await pollDeviceCode(grant.device_code, grant.interval, grant.expires_in);
|
|
708
|
+
process.stderr.write("\n[alpha relink] Approved!\n");
|
|
709
|
+
|
|
710
|
+
const updatedIdentity = {
|
|
711
|
+
enrolled: true,
|
|
712
|
+
user_id: existingIdentity?.user_id ?? generateUserId(),
|
|
713
|
+
cloud_user_id: result.cloud_user_id,
|
|
714
|
+
cloud_org_id: result.org_id,
|
|
715
|
+
email: existingIdentity?.email,
|
|
716
|
+
display_name: existingIdentity?.display_name,
|
|
717
|
+
consent_timestamp: new Date().toISOString(),
|
|
718
|
+
api_key: result.api_key,
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
writeAlphaIdentity(SELFTUNE_CONFIG_PATH, updatedIdentity);
|
|
722
|
+
chmodSync(SELFTUNE_CONFIG_PATH, 0o600);
|
|
723
|
+
|
|
724
|
+
console.log(
|
|
725
|
+
JSON.stringify({
|
|
726
|
+
level: "info",
|
|
727
|
+
code: "alpha_relinked",
|
|
728
|
+
replaced_existing_key: Boolean(existingIdentity?.api_key),
|
|
729
|
+
cloud_user_id: result.cloud_user_id,
|
|
730
|
+
message: "Successfully relinked. Old key revoked by cloud during approval.",
|
|
731
|
+
}),
|
|
732
|
+
);
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
default:
|
|
736
|
+
console.error(
|
|
737
|
+
`Unknown alpha subcommand: ${sub}\nRun 'selftune alpha --help' for available subcommands.`,
|
|
738
|
+
);
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
471
743
|
case "telemetry": {
|
|
472
744
|
const { cliMain } = await import("./analytics.js");
|
|
473
745
|
await cliMain();
|