selftune 0.2.16 → 0.2.19
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 +32 -22
- package/apps/local-dashboard/dist/assets/index-DnhnXQm6.js +60 -0
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/alpha-upload/build-payloads.ts +14 -1
- package/cli/selftune/alpha-upload/client.ts +51 -1
- package/cli/selftune/alpha-upload/flush.ts +46 -5
- package/cli/selftune/alpha-upload/stage-canonical.ts +32 -10
- package/cli/selftune/alpha-upload-contract.ts +9 -0
- package/cli/selftune/constants.ts +92 -5
- package/cli/selftune/contribute/contribute.ts +30 -2
- package/cli/selftune/contribute/sanitize.ts +52 -5
- package/cli/selftune/contribution-config.ts +249 -0
- package/cli/selftune/contribution-relay.ts +177 -0
- package/cli/selftune/contribution-signals.ts +219 -0
- package/cli/selftune/contribution-staging.ts +147 -0
- package/cli/selftune/contributions.ts +532 -0
- package/cli/selftune/creator-contributions.ts +333 -0
- package/cli/selftune/dashboard-contract.ts +305 -1
- package/cli/selftune/dashboard-server.ts +47 -13
- package/cli/selftune/eval/family-overlap.ts +395 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/evolution/description-quality.ts +12 -11
- package/cli/selftune/evolution/evolve.ts +214 -51
- package/cli/selftune/evolution/validate-proposal.ts +9 -6
- package/cli/selftune/export.ts +2 -2
- package/cli/selftune/grading/grade-session.ts +20 -0
- package/cli/selftune/hooks/commit-track.ts +188 -0
- package/cli/selftune/hooks/prompt-log.ts +10 -1
- package/cli/selftune/hooks/session-stop.ts +2 -2
- package/cli/selftune/hooks/skill-eval.ts +15 -1
- package/cli/selftune/hooks/stdin-preview.ts +32 -0
- package/cli/selftune/index.ts +41 -5
- package/cli/selftune/ingestors/codex-rollout.ts +31 -35
- package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
- package/cli/selftune/localdb/db.ts +2 -2
- package/cli/selftune/localdb/direct-write.ts +69 -6
- package/cli/selftune/localdb/queries.ts +1253 -37
- package/cli/selftune/localdb/schema.ts +66 -0
- package/cli/selftune/orchestrate.ts +32 -4
- package/cli/selftune/recover.ts +153 -0
- package/cli/selftune/repair/skill-usage.ts +363 -4
- package/cli/selftune/routes/actions.ts +35 -1
- package/cli/selftune/routes/analytics.ts +14 -0
- package/cli/selftune/routes/index.ts +1 -0
- package/cli/selftune/routes/overview.ts +150 -4
- package/cli/selftune/routes/skill-report.ts +648 -18
- package/cli/selftune/status.ts +81 -2
- package/cli/selftune/sync.ts +56 -2
- package/cli/selftune/trust-model.ts +66 -0
- package/cli/selftune/types.ts +80 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/utils/transcript.ts +210 -1
- package/cli/selftune/watchlist.ts +65 -0
- package/node_modules/@selftune/telemetry-contract/src/types.ts +11 -0
- package/package.json +1 -1
- package/packages/telemetry-contract/src/types.ts +11 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +165 -150
- package/packages/ui/src/components/EvidenceViewer.tsx +335 -144
- package/packages/ui/src/components/EvolutionTimeline.tsx +58 -28
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
- package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
- package/packages/ui/src/components/section-cards.tsx +12 -9
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/skill/SKILL.md +40 -2
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +64 -0
- package/skill/Workflows/Contribute.md +6 -3
- package/skill/Workflows/Contributions.md +97 -0
- package/skill/Workflows/CreatorContributions.md +74 -0
- package/skill/Workflows/Dashboard.md +31 -0
- package/skill/Workflows/Evals.md +57 -8
- package/skill/Workflows/Evolve.md +31 -13
- package/skill/Workflows/ExportCanonical.md +121 -0
- package/skill/Workflows/Hook.md +131 -0
- package/skill/Workflows/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +29 -9
- package/skill/Workflows/Orchestrate.md +27 -5
- package/skill/Workflows/Quickstart.md +94 -0
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +95 -0
- package/skill/Workflows/Sync.md +18 -12
- package/skill/Workflows/Uninstall.md +82 -0
- package/skill/settings_snippet.json +11 -0
- package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
- package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
|
@@ -6,9 +6,15 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
6
6
|
import { basename, dirname } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { CLAUDE_CODE_PROJECTS_DIR } from "../constants.js";
|
|
9
|
-
import type { SessionTelemetryRecord, TranscriptMetrics } from "../types.js";
|
|
9
|
+
import type { SessionTelemetryRecord, SessionType, TranscriptMetrics } from "../types.js";
|
|
10
10
|
import { isActionableQueryText } from "./query-filter.js";
|
|
11
11
|
|
|
12
|
+
/** Tools that produce durable output artifacts (not reads or exploration). */
|
|
13
|
+
const ARTIFACT_TOOLS = new Set(["Write", "Edit", "WebFetch", "WebSearch", "Skill", "Agent"]);
|
|
14
|
+
|
|
15
|
+
/** Matches any bash command containing a git invocation. */
|
|
16
|
+
const GIT_CMD_RE = /\bgit\b/;
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* Parse a Claude Code transcript JSONL and extract process metrics.
|
|
14
20
|
*
|
|
@@ -32,10 +38,18 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
32
38
|
let lastUserQuery = "";
|
|
33
39
|
let inputTokens = 0;
|
|
34
40
|
let outputTokens = 0;
|
|
41
|
+
let cachedInputTokens = 0;
|
|
42
|
+
let reasoningOutputTokens = 0;
|
|
35
43
|
let firstTimestamp: string | null = null;
|
|
36
44
|
let lastTimestamp: string | null = null;
|
|
37
45
|
let model: string | undefined;
|
|
38
46
|
|
|
47
|
+
// File change tracking (Win 2)
|
|
48
|
+
const changedFiles = new Set<string>();
|
|
49
|
+
let linesAdded = 0;
|
|
50
|
+
let linesRemoved = 0;
|
|
51
|
+
let linesModified = 0;
|
|
52
|
+
|
|
39
53
|
for (const raw of lines) {
|
|
40
54
|
const line = raw.trim();
|
|
41
55
|
if (!line) continue;
|
|
@@ -61,6 +75,14 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
61
75
|
if (usage && typeof usage === "object") {
|
|
62
76
|
if (typeof usage.input_tokens === "number") inputTokens += usage.input_tokens;
|
|
63
77
|
if (typeof usage.output_tokens === "number") outputTokens += usage.output_tokens;
|
|
78
|
+
// Win 3: Token granularity — cached input tokens
|
|
79
|
+
if (typeof usage.cache_read_input_tokens === "number")
|
|
80
|
+
cachedInputTokens += usage.cache_read_input_tokens;
|
|
81
|
+
if (typeof usage.cache_creation_input_tokens === "number")
|
|
82
|
+
cachedInputTokens += usage.cache_creation_input_tokens;
|
|
83
|
+
// Win 3: Reasoning output tokens
|
|
84
|
+
if (typeof usage.reasoning_output_tokens === "number")
|
|
85
|
+
reasoningOutputTokens += usage.reasoning_output_tokens;
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
// Normalise: unwrap nested message if present
|
|
@@ -119,6 +141,26 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
119
141
|
const cmd = ((inp.command as string) ?? "").trim();
|
|
120
142
|
if (cmd) bashCommands.push(cmd);
|
|
121
143
|
}
|
|
144
|
+
|
|
145
|
+
// Win 2: Track file changes from Write and Edit tools
|
|
146
|
+
if (toolName === "Write" || toolName === "Edit") {
|
|
147
|
+
const fp = (inp.file_path as string) ?? "";
|
|
148
|
+
if (fp) changedFiles.add(fp);
|
|
149
|
+
}
|
|
150
|
+
if (toolName === "Write" && typeof inp.content === "string") {
|
|
151
|
+
linesAdded += inp.content.split("\n").length;
|
|
152
|
+
}
|
|
153
|
+
if (toolName === "Edit") {
|
|
154
|
+
const oldStr = inp.old_string;
|
|
155
|
+
const newStr = inp.new_string;
|
|
156
|
+
if (typeof oldStr === "string" && typeof newStr === "string") {
|
|
157
|
+
const oldLines = oldStr.split("\n").length;
|
|
158
|
+
const newLines = newStr.split("\n").length;
|
|
159
|
+
linesModified += Math.min(oldLines, newLines);
|
|
160
|
+
linesAdded += Math.max(0, newLines - oldLines);
|
|
161
|
+
linesRemoved += Math.max(0, oldLines - newLines);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
122
164
|
}
|
|
123
165
|
}
|
|
124
166
|
}
|
|
@@ -143,6 +185,12 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
143
185
|
}
|
|
144
186
|
}
|
|
145
187
|
|
|
188
|
+
// Compute artifact count: output-producing tool calls
|
|
189
|
+
let artifactCount = 0;
|
|
190
|
+
for (const [tool, count] of Object.entries(toolCalls)) {
|
|
191
|
+
if (ARTIFACT_TOOLS.has(tool)) artifactCount += count;
|
|
192
|
+
}
|
|
193
|
+
|
|
146
194
|
// Compute duration from first to last timestamp
|
|
147
195
|
let durationMs: number | undefined;
|
|
148
196
|
if (firstTimestamp && lastTimestamp && firstTimestamp !== lastTimestamp) {
|
|
@@ -153,6 +201,12 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
153
201
|
}
|
|
154
202
|
}
|
|
155
203
|
|
|
204
|
+
// Win 3: Calculate cost from model and token counts
|
|
205
|
+
const costUsd = calculateCost(model, inputTokens, outputTokens);
|
|
206
|
+
|
|
207
|
+
// Infer session type from tool distribution
|
|
208
|
+
const sessionType = inferSessionType(toolCalls, bashCommands);
|
|
209
|
+
|
|
156
210
|
return {
|
|
157
211
|
tool_calls: toolCalls,
|
|
158
212
|
total_tool_calls: Object.values(toolCalls).reduce((a, b) => a + b, 0),
|
|
@@ -163,8 +217,18 @@ export function parseTranscript(transcriptPath: string): TranscriptMetrics {
|
|
|
163
217
|
errors_encountered: errors,
|
|
164
218
|
transcript_chars: totalChars,
|
|
165
219
|
last_user_query: lastUserQuery,
|
|
220
|
+
// Win 2: File change metrics
|
|
221
|
+
files_changed: changedFiles.size,
|
|
222
|
+
lines_added: linesAdded,
|
|
223
|
+
lines_removed: linesRemoved,
|
|
224
|
+
lines_modified: linesModified,
|
|
225
|
+
artifact_count: artifactCount,
|
|
226
|
+
session_type: sessionType,
|
|
166
227
|
...(inputTokens > 0 ? { input_tokens: inputTokens } : {}),
|
|
167
228
|
...(outputTokens > 0 ? { output_tokens: outputTokens } : {}),
|
|
229
|
+
...(cachedInputTokens > 0 ? { cached_input_tokens: cachedInputTokens } : {}),
|
|
230
|
+
...(reasoningOutputTokens > 0 ? { reasoning_output_tokens: reasoningOutputTokens } : {}),
|
|
231
|
+
...(costUsd !== undefined ? { cost_usd: costUsd } : {}),
|
|
168
232
|
...(durationMs !== undefined ? { duration_ms: durationMs } : {}),
|
|
169
233
|
...(model ? { model } : {}),
|
|
170
234
|
...(firstTimestamp ? { started_at: firstTimestamp } : {}),
|
|
@@ -307,6 +371,16 @@ export function buildTelemetryFromTranscript(
|
|
|
307
371
|
source,
|
|
308
372
|
input_tokens: metrics.input_tokens,
|
|
309
373
|
output_tokens: metrics.output_tokens,
|
|
374
|
+
cached_input_tokens: metrics.cached_input_tokens,
|
|
375
|
+
reasoning_output_tokens: metrics.reasoning_output_tokens,
|
|
376
|
+
cost_usd: metrics.cost_usd,
|
|
377
|
+
files_changed: metrics.files_changed,
|
|
378
|
+
lines_added: metrics.lines_added,
|
|
379
|
+
lines_removed: metrics.lines_removed,
|
|
380
|
+
lines_modified: metrics.lines_modified,
|
|
381
|
+
artifact_count: metrics.artifact_count,
|
|
382
|
+
session_type: metrics.session_type,
|
|
383
|
+
agent_summary: generateSessionSummary(metrics),
|
|
310
384
|
};
|
|
311
385
|
}
|
|
312
386
|
|
|
@@ -518,6 +592,141 @@ export function extractTokenUsage(transcriptPath: string): { input: number; outp
|
|
|
518
592
|
return { input, output };
|
|
519
593
|
}
|
|
520
594
|
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Win 3: Model cost lookup (USD per million tokens)
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
const MODEL_COSTS: Record<string, { input: number; output: number }> = {
|
|
600
|
+
"claude-sonnet-4-20250514": { input: 3.0, output: 15.0 },
|
|
601
|
+
"claude-opus-4-20250514": { input: 15.0, output: 75.0 },
|
|
602
|
+
"claude-haiku-3-5-20241022": { input: 0.8, output: 4.0 },
|
|
603
|
+
"claude-3-5-sonnet-20241022": { input: 3.0, output: 15.0 },
|
|
604
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4.0 },
|
|
605
|
+
"claude-3-opus-20240229": { input: 15.0, output: 75.0 },
|
|
606
|
+
"claude-3-sonnet-20240229": { input: 3.0, output: 15.0 },
|
|
607
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Calculate estimated cost in USD from model name and token counts.
|
|
612
|
+
* Returns undefined if the model is unknown or not provided.
|
|
613
|
+
*/
|
|
614
|
+
export function calculateCost(
|
|
615
|
+
model: string | undefined,
|
|
616
|
+
inputTokens: number,
|
|
617
|
+
outputTokens: number,
|
|
618
|
+
): number | undefined {
|
|
619
|
+
if (!model) return undefined;
|
|
620
|
+
const costs =
|
|
621
|
+
MODEL_COSTS[model] ??
|
|
622
|
+
Object.entries(MODEL_COSTS).find(([k]) =>
|
|
623
|
+
model.startsWith(k.split("-").slice(0, -1).join("-")),
|
|
624
|
+
)?.[1];
|
|
625
|
+
if (!costs) return undefined;
|
|
626
|
+
return (inputTokens * costs.input + outputTokens * costs.output) / 1_000_000;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Infer session type from tool call distribution.
|
|
631
|
+
*
|
|
632
|
+
* - "dev": majority of output tools are Write/Edit/Bash with git commands
|
|
633
|
+
* - "research": majority are WebFetch/WebSearch/Read
|
|
634
|
+
* - "content": majority are Write/Edit but no git commands
|
|
635
|
+
* - "mixed": no clear majority
|
|
636
|
+
*/
|
|
637
|
+
export function inferSessionType(
|
|
638
|
+
toolCalls: Record<string, number>,
|
|
639
|
+
bashCommands: string[],
|
|
640
|
+
): "dev" | "research" | "content" | "mixed" {
|
|
641
|
+
const total = Object.values(toolCalls).reduce((a, b) => a + b, 0);
|
|
642
|
+
if (total === 0) return "mixed";
|
|
643
|
+
|
|
644
|
+
const writeEdit = (toolCalls.Write ?? 0) + (toolCalls.Edit ?? 0);
|
|
645
|
+
const research = (toolCalls.WebFetch ?? 0) + (toolCalls.WebSearch ?? 0);
|
|
646
|
+
const bash = toolCalls.Bash ?? 0;
|
|
647
|
+
const read = toolCalls.Read ?? 0;
|
|
648
|
+
const hasGit = bashCommands.some((cmd) => GIT_CMD_RE.test(cmd));
|
|
649
|
+
|
|
650
|
+
// Dev: file mutations + git commands OR bash-heavy with git
|
|
651
|
+
if (hasGit && (writeEdit + bash) / total > 0.3) return "dev";
|
|
652
|
+
|
|
653
|
+
// Research: web tools + read-heavy, low file mutations
|
|
654
|
+
if (research > 0 && research / total > 0.2 && writeEdit / total < 0.15) return "research";
|
|
655
|
+
if (read / total > 0.5 && writeEdit / total < 0.1) return "research";
|
|
656
|
+
|
|
657
|
+
// Content: file mutations but no git
|
|
658
|
+
if (writeEdit / total > 0.2 && !hasGit) return "content";
|
|
659
|
+
|
|
660
|
+
return "mixed";
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Generate a short heuristic session summary from transcript metrics.
|
|
665
|
+
* No LLM call — pure template-based approach. Kept under 120 chars.
|
|
666
|
+
*/
|
|
667
|
+
export function generateSessionSummary(metrics: TranscriptMetrics): string {
|
|
668
|
+
const MAX_LEN = 120;
|
|
669
|
+
const sessionType: SessionType = metrics.session_type ?? "mixed";
|
|
670
|
+
const lastQuery = truncateQuery(metrics.last_user_query, 60);
|
|
671
|
+
|
|
672
|
+
if (metrics.total_tool_calls === 0 && !lastQuery) {
|
|
673
|
+
return "Empty session — no tool calls or queries";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const topTools = getTopTools(metrics.tool_calls, 2);
|
|
677
|
+
|
|
678
|
+
let summary: string;
|
|
679
|
+
switch (sessionType) {
|
|
680
|
+
case "dev": {
|
|
681
|
+
const filesChanged = metrics.files_changed ?? 0;
|
|
682
|
+
const toolStr = topTools.length > 0 ? ` via ${topTools.join(", ")}` : "";
|
|
683
|
+
const queryStr = lastQuery ? ` — ${lastQuery}` : "";
|
|
684
|
+
summary = `${filesChanged} files changed${toolStr}${queryStr}`;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "research": {
|
|
688
|
+
const searches = (metrics.tool_calls.WebSearch ?? 0) + (metrics.tool_calls.WebFetch ?? 0);
|
|
689
|
+
const reads = metrics.tool_calls.Read ?? 0;
|
|
690
|
+
const queryStr = lastQuery ? ` — ${lastQuery}` : "";
|
|
691
|
+
summary = `${searches} searches + ${reads} reads${queryStr}`;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "content": {
|
|
695
|
+
const filesChanged = metrics.files_changed ?? 0;
|
|
696
|
+
const queryStr = lastQuery ? ` — ${lastQuery}` : "";
|
|
697
|
+
summary = `${filesChanged} files created/edited${queryStr}`;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
default: {
|
|
701
|
+
const toolCount = Object.keys(metrics.tool_calls).length;
|
|
702
|
+
const queryStr = lastQuery ? ` — ${lastQuery}` : "";
|
|
703
|
+
summary = `${metrics.total_tool_calls} tool calls across ${toolCount} tools${queryStr}`;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (summary.length > MAX_LEN) {
|
|
709
|
+
return `${summary.slice(0, MAX_LEN - 3)}...`;
|
|
710
|
+
}
|
|
711
|
+
return summary;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Get the top N tools by call count. */
|
|
715
|
+
function getTopTools(toolCalls: Record<string, number>, n: number): string[] {
|
|
716
|
+
return Object.entries(toolCalls)
|
|
717
|
+
.sort((a, b) => b[1] - a[1])
|
|
718
|
+
.slice(0, n)
|
|
719
|
+
.map(([name]) => name);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Truncate a query string to maxLen, adding ellipsis if needed. */
|
|
723
|
+
function truncateQuery(query: string, maxLen: number): string {
|
|
724
|
+
const trimmed = query.trim();
|
|
725
|
+
if (!trimmed) return "";
|
|
726
|
+
if (trimmed.length <= maxLen) return trimmed;
|
|
727
|
+
return `${trimmed.slice(0, maxLen - 3)}...`;
|
|
728
|
+
}
|
|
729
|
+
|
|
521
730
|
function emptyMetrics(): TranscriptMetrics {
|
|
522
731
|
return {
|
|
523
732
|
tool_calls: {},
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
|
|
10
|
+
import { SELFTUNE_CONFIG_DIR, WATCHED_SKILLS_PATH } from "./constants.js";
|
|
11
|
+
|
|
12
|
+
const CURRENT_WATCHLIST_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
interface WatchlistPayload {
|
|
15
|
+
version: typeof CURRENT_WATCHLIST_VERSION;
|
|
16
|
+
skills: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeSkills(skills: string[]): string[] {
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
const normalized: string[] = [];
|
|
22
|
+
for (const skill of skills) {
|
|
23
|
+
const trimmed = skill.trim();
|
|
24
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
25
|
+
seen.add(trimmed);
|
|
26
|
+
normalized.push(trimmed);
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadWatchedSkills(): string[] {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(WATCHED_SKILLS_PATH)) return [];
|
|
34
|
+
const parsed = JSON.parse(
|
|
35
|
+
readFileSync(WATCHED_SKILLS_PATH, "utf-8"),
|
|
36
|
+
) as Partial<WatchlistPayload>;
|
|
37
|
+
return parsed.version === CURRENT_WATCHLIST_VERSION && Array.isArray(parsed.skills)
|
|
38
|
+
? normalizeSkills(parsed.skills.filter((skill): skill is string => typeof skill === "string"))
|
|
39
|
+
: [];
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveWatchedSkills(skills: string[]): string[] {
|
|
46
|
+
const normalized = normalizeSkills(skills);
|
|
47
|
+
mkdirSync(SELFTUNE_CONFIG_DIR, { recursive: true });
|
|
48
|
+
const tempPath = `${WATCHED_SKILLS_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(
|
|
51
|
+
tempPath,
|
|
52
|
+
JSON.stringify({ version: CURRENT_WATCHLIST_VERSION, skills: normalized }, null, 2),
|
|
53
|
+
"utf-8",
|
|
54
|
+
);
|
|
55
|
+
renameSync(tempPath, WATCHED_SKILLS_PATH);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
try {
|
|
58
|
+
if (existsSync(tempPath)) unlinkSync(tempPath);
|
|
59
|
+
} catch {
|
|
60
|
+
// Best-effort cleanup for interrupted temp writes.
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
@@ -143,7 +143,18 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
|
|
|
143
143
|
errors_encountered: number;
|
|
144
144
|
input_tokens?: number;
|
|
145
145
|
output_tokens?: number;
|
|
146
|
+
cached_input_tokens?: number;
|
|
147
|
+
reasoning_output_tokens?: number;
|
|
148
|
+
cost_usd?: number;
|
|
146
149
|
duration_ms?: number;
|
|
150
|
+
files_changed?: number;
|
|
151
|
+
lines_added?: number;
|
|
152
|
+
lines_removed?: number;
|
|
153
|
+
lines_modified?: number;
|
|
154
|
+
/** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
|
|
155
|
+
artifact_count?: number;
|
|
156
|
+
/** Inferred session type based on tool distribution. */
|
|
157
|
+
session_type?: "dev" | "research" | "content" | "mixed";
|
|
147
158
|
completion_status?: CanonicalCompletionStatus;
|
|
148
159
|
end_reason?: string;
|
|
149
160
|
}
|
package/package.json
CHANGED
|
@@ -143,7 +143,18 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
|
|
|
143
143
|
errors_encountered: number;
|
|
144
144
|
input_tokens?: number;
|
|
145
145
|
output_tokens?: number;
|
|
146
|
+
cached_input_tokens?: number;
|
|
147
|
+
reasoning_output_tokens?: number;
|
|
148
|
+
cost_usd?: number;
|
|
146
149
|
duration_ms?: number;
|
|
150
|
+
files_changed?: number;
|
|
151
|
+
lines_added?: number;
|
|
152
|
+
lines_removed?: number;
|
|
153
|
+
lines_modified?: number;
|
|
154
|
+
/** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
|
|
155
|
+
artifact_count?: number;
|
|
156
|
+
/** Inferred session type based on tool distribution. */
|
|
157
|
+
session_type?: "dev" | "research" | "content" | "mixed";
|
|
147
158
|
completion_status?: CanonicalCompletionStatus;
|
|
148
159
|
end_reason?: string;
|
|
149
160
|
}
|