selftune 0.2.2 → 0.2.6
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 +11 -0
- package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +1 -0
- package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-r2k_Ku_V.js +346 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/analytics.ts +354 -0
- package/cli/selftune/badge/badge.ts +2 -2
- package/cli/selftune/dashboard-server.ts +3 -3
- package/cli/selftune/evolution/evolve-body.ts +1 -1
- package/cli/selftune/evolution/evolve.ts +1 -1
- package/cli/selftune/index.ts +15 -1
- package/cli/selftune/init.ts +5 -1
- package/cli/selftune/observability.ts +63 -2
- package/cli/selftune/orchestrate.ts +1 -1
- package/cli/selftune/quickstart.ts +1 -1
- package/cli/selftune/status.ts +2 -2
- package/cli/selftune/types.ts +1 -0
- package/cli/selftune/utils/llm-call.ts +2 -1
- package/package.json +6 -4
- package/packages/ui/README.md +113 -0
- package/packages/ui/index.ts +10 -0
- package/packages/ui/package.json +62 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +171 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +718 -0
- package/packages/ui/src/components/EvolutionTimeline.tsx +252 -0
- package/packages/ui/src/components/InfoTip.tsx +19 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +164 -0
- package/packages/ui/src/components/index.ts +7 -0
- package/packages/ui/src/components/section-cards.tsx +155 -0
- package/packages/ui/src/components/skill-health-grid.tsx +686 -0
- package/packages/ui/src/lib/constants.tsx +43 -0
- package/packages/ui/src/lib/format.ts +37 -0
- package/packages/ui/src/lib/index.ts +3 -0
- package/packages/ui/src/lib/utils.ts +6 -0
- package/packages/ui/src/primitives/badge.tsx +52 -0
- package/packages/ui/src/primitives/button.tsx +58 -0
- package/packages/ui/src/primitives/card.tsx +103 -0
- package/packages/ui/src/primitives/checkbox.tsx +27 -0
- package/packages/ui/src/primitives/collapsible.tsx +7 -0
- package/packages/ui/src/primitives/dropdown-menu.tsx +266 -0
- package/packages/ui/src/primitives/index.ts +55 -0
- package/packages/ui/src/primitives/label.tsx +20 -0
- package/packages/ui/src/primitives/select.tsx +197 -0
- package/packages/ui/src/primitives/table.tsx +114 -0
- package/packages/ui/src/primitives/tabs.tsx +82 -0
- package/packages/ui/src/primitives/tooltip.tsx +64 -0
- package/packages/ui/src/types.ts +87 -0
- package/packages/ui/tsconfig.json +17 -0
- package/skill/SKILL.md +3 -0
- package/skill/Workflows/Telemetry.md +59 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +0 -15
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +0 -346
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>selftune — Dashboard</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-axE4kz3Q.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-U7zYD9Rg.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-ui-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-ui-r2k_Ku_V.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendor-table-B7VF2Ipl.js">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C75H1Q3n.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune anonymous usage analytics.
|
|
3
|
+
*
|
|
4
|
+
* Collects anonymous, non-identifying usage data to help prioritize
|
|
5
|
+
* features and understand how selftune is used in the wild.
|
|
6
|
+
*
|
|
7
|
+
* Privacy guarantees:
|
|
8
|
+
* - No PII: no usernames, emails, IPs, file paths, or repo names
|
|
9
|
+
* - No session IDs; events are linkable by anonymous_id and sent_at
|
|
10
|
+
* - Anonymous machine ID: random, persisted locally (not derived from any user data)
|
|
11
|
+
* - Fire-and-forget: never blocks CLI execution
|
|
12
|
+
* - Easy opt-out: env var or config flag
|
|
13
|
+
*
|
|
14
|
+
* Opt out:
|
|
15
|
+
* - Set SELFTUNE_NO_ANALYTICS=1 in your environment
|
|
16
|
+
* - Run `selftune telemetry disable`
|
|
17
|
+
* - Set "analytics_disabled": true in ~/.selftune/config.json
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { arch, platform, release } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import { SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
26
|
+
import type { SelftuneConfig } from "./types.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Configuration
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const ANALYTICS_ENDPOINT =
|
|
33
|
+
process.env.SELFTUNE_ANALYTICS_ENDPOINT ?? "https://telemetry.selftune.dev/v1/events";
|
|
34
|
+
|
|
35
|
+
function getVersion(): string {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf-8"));
|
|
38
|
+
return pkg.version ?? "unknown";
|
|
39
|
+
} catch {
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Cached config — read once per process, shared across all functions
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
let cachedConfig: SelftuneConfig | null | undefined;
|
|
49
|
+
|
|
50
|
+
function loadConfig(): SelftuneConfig | null {
|
|
51
|
+
if (cachedConfig !== undefined) return cachedConfig;
|
|
52
|
+
try {
|
|
53
|
+
if (existsSync(SELFTUNE_CONFIG_PATH)) {
|
|
54
|
+
cachedConfig = JSON.parse(readFileSync(SELFTUNE_CONFIG_PATH, "utf-8")) as SelftuneConfig;
|
|
55
|
+
} else {
|
|
56
|
+
cachedConfig = null;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
cachedConfig = null;
|
|
60
|
+
}
|
|
61
|
+
return cachedConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Invalidate cached config (used after writes). */
|
|
65
|
+
function invalidateConfigCache(): void {
|
|
66
|
+
cachedConfig = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Reset all cached state. Exported for test isolation only. */
|
|
70
|
+
export function resetAnalyticsState(): void {
|
|
71
|
+
cachedConfig = undefined;
|
|
72
|
+
cachedAnonymousId = undefined;
|
|
73
|
+
cachedOsContext = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Persisted anonymous ID — random, non-reversible, stable across runs
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const ANONYMOUS_ID_PATH = join(SELFTUNE_CONFIG_DIR, ".anonymous_id");
|
|
81
|
+
let cachedAnonymousId: string | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get or create a random anonymous machine ID.
|
|
85
|
+
* Generated once via crypto.randomBytes and persisted to disk.
|
|
86
|
+
* Cannot be reversed to recover any user/machine information.
|
|
87
|
+
* Result is memoized for the process lifetime.
|
|
88
|
+
*/
|
|
89
|
+
export function getAnonymousId(): string {
|
|
90
|
+
if (cachedAnonymousId) return cachedAnonymousId;
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(ANONYMOUS_ID_PATH)) {
|
|
93
|
+
const stored = readFileSync(ANONYMOUS_ID_PATH, "utf-8").trim();
|
|
94
|
+
if (/^[a-f0-9]{16}$/.test(stored)) {
|
|
95
|
+
cachedAnonymousId = stored;
|
|
96
|
+
return stored;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// fall through to generate
|
|
101
|
+
}
|
|
102
|
+
const id = randomBytes(8).toString("hex"); // 16 hex chars
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(SELFTUNE_CONFIG_DIR, { recursive: true });
|
|
105
|
+
writeFileSync(ANONYMOUS_ID_PATH, id, "utf-8");
|
|
106
|
+
} catch {
|
|
107
|
+
// non-fatal — use ephemeral ID for this process
|
|
108
|
+
}
|
|
109
|
+
cachedAnonymousId = id;
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Cached OS context — doesn't change within a process
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
let cachedOsContext: { os: string; os_release: string; arch: string } | undefined;
|
|
118
|
+
|
|
119
|
+
function getOsContext(): { os: string; os_release: string; arch: string } {
|
|
120
|
+
if (cachedOsContext) return cachedOsContext;
|
|
121
|
+
cachedOsContext = { os: platform(), os_release: release(), arch: arch() };
|
|
122
|
+
return cachedOsContext;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Analytics gate
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check whether analytics is enabled.
|
|
131
|
+
* Returns false if:
|
|
132
|
+
* - SELFTUNE_NO_ANALYTICS env var is set to any truthy value
|
|
133
|
+
* - Config file has analytics_disabled: true
|
|
134
|
+
* - CI environment detected (CI=true)
|
|
135
|
+
*/
|
|
136
|
+
export function isAnalyticsEnabled(): boolean {
|
|
137
|
+
// Env var override (highest priority)
|
|
138
|
+
const envDisabled = process.env.SELFTUNE_NO_ANALYTICS;
|
|
139
|
+
if (envDisabled && envDisabled !== "0" && envDisabled !== "false") {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// CI detection — don't inflate analytics from CI pipelines
|
|
144
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Config file check (uses cached read — no redundant I/O)
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
if (config?.analytics_disabled) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Event tracking
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
export interface AnalyticsEvent {
|
|
162
|
+
event: string;
|
|
163
|
+
properties: Record<string, string | number | boolean>;
|
|
164
|
+
context: {
|
|
165
|
+
anonymous_id: string;
|
|
166
|
+
os: string;
|
|
167
|
+
os_release: string;
|
|
168
|
+
arch: string;
|
|
169
|
+
selftune_version: string;
|
|
170
|
+
node_version: string;
|
|
171
|
+
agent_type: string;
|
|
172
|
+
};
|
|
173
|
+
sent_at: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Build an analytics event payload.
|
|
178
|
+
* Exported for testing — does NOT send the event.
|
|
179
|
+
*/
|
|
180
|
+
export function buildEvent(
|
|
181
|
+
eventName: string,
|
|
182
|
+
properties: Record<string, string | number | boolean> = {},
|
|
183
|
+
): AnalyticsEvent {
|
|
184
|
+
const config = loadConfig();
|
|
185
|
+
const agentType: SelftuneConfig["agent_type"] = config?.agent_type ?? "unknown";
|
|
186
|
+
const osCtx = getOsContext();
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
event: eventName,
|
|
190
|
+
properties,
|
|
191
|
+
context: {
|
|
192
|
+
anonymous_id: getAnonymousId(),
|
|
193
|
+
...osCtx,
|
|
194
|
+
selftune_version: getVersion(),
|
|
195
|
+
node_version: process.version,
|
|
196
|
+
agent_type: agentType,
|
|
197
|
+
},
|
|
198
|
+
sent_at: new Date().toISOString(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Track an analytics event. Fire-and-forget — never blocks, never throws.
|
|
204
|
+
*
|
|
205
|
+
* @param eventName - Event name (e.g., "command_run")
|
|
206
|
+
* @param properties - Event properties (no PII allowed)
|
|
207
|
+
* @param options - Override endpoint or fetch for testing
|
|
208
|
+
*/
|
|
209
|
+
export function trackEvent(
|
|
210
|
+
eventName: string,
|
|
211
|
+
properties: Record<string, string | number | boolean> = {},
|
|
212
|
+
options?: { endpoint?: string; fetchFn?: typeof fetch },
|
|
213
|
+
): void {
|
|
214
|
+
if (!isAnalyticsEnabled()) return;
|
|
215
|
+
|
|
216
|
+
const event = buildEvent(eventName, properties);
|
|
217
|
+
const endpoint = options?.endpoint ?? ANALYTICS_ENDPOINT;
|
|
218
|
+
const fetchFn = options?.fetchFn ?? fetch;
|
|
219
|
+
|
|
220
|
+
// Fire and forget — intentionally not awaited.
|
|
221
|
+
// Wrapped in try + Promise.resolve to catch both sync throws and async rejections.
|
|
222
|
+
try {
|
|
223
|
+
Promise.resolve(
|
|
224
|
+
fetchFn(endpoint, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify(event),
|
|
228
|
+
signal: AbortSignal.timeout(3000), // 3s timeout — don't hang
|
|
229
|
+
}),
|
|
230
|
+
).catch(() => {
|
|
231
|
+
// Silently ignore — analytics should never break the CLI
|
|
232
|
+
});
|
|
233
|
+
} catch {
|
|
234
|
+
// Silently ignore sync throws from fetchFn
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// CLI: selftune telemetry [status|enable|disable]
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
function writeConfigField(field: keyof SelftuneConfig, value: unknown): void {
|
|
243
|
+
let config: Record<string, unknown> = {};
|
|
244
|
+
try {
|
|
245
|
+
if (existsSync(SELFTUNE_CONFIG_PATH)) {
|
|
246
|
+
config = JSON.parse(readFileSync(SELFTUNE_CONFIG_PATH, "utf-8"));
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// start fresh
|
|
250
|
+
}
|
|
251
|
+
config[field] = value;
|
|
252
|
+
mkdirSync(SELFTUNE_CONFIG_DIR, { recursive: true });
|
|
253
|
+
writeFileSync(SELFTUNE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
254
|
+
invalidateConfigCache();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function cliMain(): Promise<void> {
|
|
258
|
+
const sub = process.argv[2];
|
|
259
|
+
|
|
260
|
+
if (sub === "--help" || sub === "-h") {
|
|
261
|
+
console.log(`selftune telemetry — Manage anonymous usage analytics
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
selftune telemetry Show current telemetry status
|
|
265
|
+
selftune telemetry status Show current telemetry status
|
|
266
|
+
selftune telemetry enable Enable anonymous usage analytics
|
|
267
|
+
selftune telemetry disable Disable anonymous usage analytics
|
|
268
|
+
|
|
269
|
+
Environment:
|
|
270
|
+
SELFTUNE_NO_ANALYTICS=1 Disable analytics via env var
|
|
271
|
+
|
|
272
|
+
selftune collects anonymous, non-identifying usage data to help
|
|
273
|
+
prioritize features. No PII is ever collected. See:
|
|
274
|
+
https://github.com/selftune-dev/selftune#telemetry`);
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
switch (sub) {
|
|
279
|
+
case "disable": {
|
|
280
|
+
try {
|
|
281
|
+
writeConfigField("analytics_disabled", true);
|
|
282
|
+
} catch {
|
|
283
|
+
console.error(
|
|
284
|
+
"Failed to disable telemetry: cannot write ~/.selftune/config.json. " +
|
|
285
|
+
"Try checking file permissions, or set SELFTUNE_NO_ANALYTICS=1.",
|
|
286
|
+
);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
console.log("Telemetry disabled. No anonymous usage data will be sent.");
|
|
290
|
+
console.log("You can re-enable with: selftune telemetry enable");
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "enable": {
|
|
294
|
+
try {
|
|
295
|
+
writeConfigField("analytics_disabled", false);
|
|
296
|
+
} catch {
|
|
297
|
+
console.error(
|
|
298
|
+
"Failed to enable telemetry: cannot write ~/.selftune/config.json. " +
|
|
299
|
+
"Try checking file permissions.",
|
|
300
|
+
);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
console.log("Telemetry enabled. Anonymous usage data will be sent.");
|
|
304
|
+
console.log("Disable anytime with: selftune telemetry disable");
|
|
305
|
+
console.log("Or set SELFTUNE_NO_ANALYTICS=1 in your environment.");
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "status":
|
|
309
|
+
case undefined: {
|
|
310
|
+
const enabled = isAnalyticsEnabled();
|
|
311
|
+
const config = loadConfig();
|
|
312
|
+
const envDisabled = process.env.SELFTUNE_NO_ANALYTICS;
|
|
313
|
+
const configDisabled = config?.analytics_disabled ?? false;
|
|
314
|
+
|
|
315
|
+
console.log(`Telemetry: ${enabled ? "enabled" : "disabled"}`);
|
|
316
|
+
if (envDisabled && envDisabled !== "0" && envDisabled !== "false") {
|
|
317
|
+
console.log(" Disabled via: SELFTUNE_NO_ANALYTICS environment variable");
|
|
318
|
+
}
|
|
319
|
+
if (configDisabled) {
|
|
320
|
+
console.log(" Disabled via: config file (~/.selftune/config.json)");
|
|
321
|
+
}
|
|
322
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
323
|
+
console.log(" Disabled via: CI environment detected");
|
|
324
|
+
}
|
|
325
|
+
if (enabled) {
|
|
326
|
+
console.log(` Anonymous ID: ${getAnonymousId()}`);
|
|
327
|
+
console.log(` Endpoint: ${ANALYTICS_ENDPOINT}`);
|
|
328
|
+
}
|
|
329
|
+
console.log("\nTo opt out: selftune telemetry disable");
|
|
330
|
+
console.log("Or set SELFTUNE_NO_ANALYTICS=1 in your environment.");
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
default:
|
|
334
|
+
console.error(
|
|
335
|
+
`Unknown telemetry subcommand: ${sub}\nRun 'selftune telemetry --help' for usage.`,
|
|
336
|
+
);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Telemetry disclosure notice (for init flow)
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
export const TELEMETRY_NOTICE = `
|
|
346
|
+
selftune collects anonymous usage analytics to improve the tool.
|
|
347
|
+
No personal information is ever collected — only command names,
|
|
348
|
+
OS/arch, and selftune version.
|
|
349
|
+
|
|
350
|
+
To opt out at any time:
|
|
351
|
+
selftune telemetry disable
|
|
352
|
+
# or
|
|
353
|
+
export SELFTUNE_NO_ANALYTICS=1
|
|
354
|
+
`;
|
|
@@ -30,7 +30,7 @@ Options:
|
|
|
30
30
|
|
|
31
31
|
const VALID_FORMATS = new Set<BadgeFormat>(["svg", "markdown", "url"]);
|
|
32
32
|
|
|
33
|
-
export function cliMain(): void {
|
|
33
|
+
export async function cliMain(): Promise<void> {
|
|
34
34
|
const { values } = parseArgs({
|
|
35
35
|
args: process.argv.slice(2),
|
|
36
36
|
options: {
|
|
@@ -71,7 +71,7 @@ export function cliMain(): void {
|
|
|
71
71
|
const auditEntries = readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG);
|
|
72
72
|
|
|
73
73
|
// Run doctor for system health
|
|
74
|
-
const doctorResult = doctor();
|
|
74
|
+
const doctorResult = await doctor();
|
|
75
75
|
|
|
76
76
|
// Compute status
|
|
77
77
|
const result = computeStatus(telemetry, skillRecords, queryRecords, auditEntries, doctorResult);
|
|
@@ -100,12 +100,12 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
100
100
|
".ico": "image/x-icon",
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
function computeStatusFromLogs(): StatusResult {
|
|
103
|
+
async function computeStatusFromLogs(): Promise<StatusResult> {
|
|
104
104
|
const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
|
|
105
105
|
const skillRecords = readEffectiveSkillUsageRecords();
|
|
106
106
|
const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
|
|
107
107
|
const auditEntries = readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG);
|
|
108
|
-
const doctorResult = doctor();
|
|
108
|
+
const doctorResult = await doctor();
|
|
109
109
|
return computeStatus(telemetry, skillRecords, queryRecords, auditEntries, doctorResult);
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -531,7 +531,7 @@ export async function startDashboardServer(
|
|
|
531
531
|
|
|
532
532
|
// ---- GET /api/v2/doctor ---- System health diagnostics
|
|
533
533
|
if (url.pathname === "/api/v2/doctor" && req.method === "GET") {
|
|
534
|
-
const result = doctor();
|
|
534
|
+
const result = await doctor();
|
|
535
535
|
return Response.json(result, { headers: corsHeaders() });
|
|
536
536
|
}
|
|
537
537
|
|
|
@@ -410,7 +410,7 @@ export async function evolveBody(
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
if (lastProposal && lastValidation
|
|
413
|
+
if (lastProposal && lastValidation?.improved) {
|
|
414
414
|
// Deploy: write updated SKILL.md
|
|
415
415
|
if (target === "routing") {
|
|
416
416
|
const updatedContent = replaceSection(
|
|
@@ -841,7 +841,7 @@ export async function evolve(
|
|
|
841
841
|
// -----------------------------------------------------------------------
|
|
842
842
|
// Step 15: Update evolution memory
|
|
843
843
|
// -----------------------------------------------------------------------
|
|
844
|
-
const wasDeployed = lastProposal
|
|
844
|
+
const wasDeployed = lastProposal && lastValidation?.improved;
|
|
845
845
|
const evolveResult: EvolveResult = withStats({
|
|
846
846
|
proposal: lastProposal,
|
|
847
847
|
validation: lastValidation,
|
package/cli/selftune/index.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* selftune quickstart — Guided onboarding: init, ingest, status, and suggestions
|
|
23
23
|
* selftune repair-skill-usage — Rebuild trustworthy skill usage from transcripts
|
|
24
24
|
* selftune export-canonical — Export canonical telemetry for downstream ingestion
|
|
25
|
+
* selftune telemetry — Manage anonymous usage analytics (status, enable, disable)
|
|
25
26
|
* selftune hook <name> — Run a hook by name (prompt-log, session-stop, etc.)
|
|
26
27
|
*/
|
|
27
28
|
|
|
@@ -53,12 +54,20 @@ Commands:
|
|
|
53
54
|
quickstart Guided onboarding: init, ingest, status, and suggestions
|
|
54
55
|
repair-skill-usage Rebuild trustworthy skill usage from transcripts
|
|
55
56
|
export-canonical Export canonical telemetry for downstream ingestion
|
|
57
|
+
telemetry Manage anonymous usage analytics (status, enable, disable)
|
|
56
58
|
hook <name> Run a hook by name (prompt-log, session-stop, etc.)
|
|
57
59
|
|
|
58
60
|
Run 'selftune <command> --help' for command-specific options.`);
|
|
59
61
|
process.exit(0);
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// Track command usage (lazy import — avoids loading crypto/os on --help or no-op paths)
|
|
65
|
+
if (command && command !== "--help" && command !== "-h") {
|
|
66
|
+
import("./analytics.js")
|
|
67
|
+
.then(({ trackEvent }) => trackEvent("command_run", { command }))
|
|
68
|
+
.catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
if (!command) {
|
|
63
72
|
// Show status by default — same as `selftune status`
|
|
64
73
|
const { cliMain: statusMain } = await import("./status.js");
|
|
@@ -317,7 +326,7 @@ Run 'selftune eval <action> --help' for action-specific options.`);
|
|
|
317
326
|
}
|
|
318
327
|
case "doctor": {
|
|
319
328
|
const { doctor } = await import("./observability.js");
|
|
320
|
-
const result = doctor();
|
|
329
|
+
const result = await doctor();
|
|
321
330
|
console.log(JSON.stringify(result, null, 2));
|
|
322
331
|
process.exit(result.healthy ? 0 : 1);
|
|
323
332
|
break;
|
|
@@ -459,6 +468,11 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
|
|
|
459
468
|
await cliMain();
|
|
460
469
|
break;
|
|
461
470
|
}
|
|
471
|
+
case "telemetry": {
|
|
472
|
+
const { cliMain } = await import("./analytics.js");
|
|
473
|
+
await cliMain();
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
462
476
|
case "hook": {
|
|
463
477
|
// Dispatch to the appropriate hook file by name.
|
|
464
478
|
const hookName = process.argv[2]; // argv was shifted above
|
package/cli/selftune/init.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { parseArgs } from "node:util";
|
|
26
26
|
|
|
27
|
+
import { TELEMETRY_NOTICE } from "./analytics.js";
|
|
27
28
|
import { CLAUDE_CODE_HOOK_KEYS, SELFTUNE_CONFIG_DIR, SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
28
29
|
import type { SelftuneConfig } from "./types.js";
|
|
29
30
|
import { hookKeyHasSelftuneEntry } from "./utils/hooks.js";
|
|
@@ -589,9 +590,12 @@ export async function cliMain(): Promise<void> {
|
|
|
589
590
|
}),
|
|
590
591
|
);
|
|
591
592
|
|
|
593
|
+
// Print telemetry disclosure
|
|
594
|
+
console.error(TELEMETRY_NOTICE);
|
|
595
|
+
|
|
592
596
|
// Run doctor as post-check
|
|
593
597
|
const { doctor } = await import("./observability.js");
|
|
594
|
-
const doctorResult = doctor();
|
|
598
|
+
const doctorResult = await doctor();
|
|
595
599
|
console.log(
|
|
596
600
|
JSON.stringify({
|
|
597
601
|
level: "info",
|
|
@@ -203,12 +203,73 @@ export function checkConfigHealth(): HealthCheck[] {
|
|
|
203
203
|
return [check];
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Compare two semver strings. Returns:
|
|
208
|
+
* -1 if a < b, 0 if equal, 1 if a > b.
|
|
209
|
+
* Handles standard x.y.z versions; pre-release tags are not compared.
|
|
210
|
+
*/
|
|
211
|
+
function compareSemver(a: string, b: string): -1 | 0 | 1 {
|
|
212
|
+
const pa = a.split(".").map(Number);
|
|
213
|
+
const pb = b.split(".").map(Number);
|
|
214
|
+
for (let i = 0; i < 3; i++) {
|
|
215
|
+
const va = pa[i] ?? 0;
|
|
216
|
+
const vb = pb[i] ?? 0;
|
|
217
|
+
if (va < vb) return -1;
|
|
218
|
+
if (va > vb) return 1;
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Check if the installed version is the latest on npm. Non-blocking, warns on stale. */
|
|
224
|
+
export async function checkVersionHealth(): Promise<HealthCheck[]> {
|
|
225
|
+
const check: HealthCheck = {
|
|
226
|
+
name: "version_up_to_date",
|
|
227
|
+
path: "package.json",
|
|
228
|
+
status: "pass",
|
|
229
|
+
message: "",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const pkgPath = join(import.meta.dir, "../../package.json");
|
|
234
|
+
const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
235
|
+
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
238
|
+
try {
|
|
239
|
+
const res = await fetch("https://registry.npmjs.org/selftune/latest", {
|
|
240
|
+
signal: controller.signal,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (res.ok) {
|
|
244
|
+
const data = (await res.json()) as { version: string };
|
|
245
|
+
const latestVersion = data.version;
|
|
246
|
+
const cmp = compareSemver(currentVersion, latestVersion);
|
|
247
|
+
if (cmp >= 0) {
|
|
248
|
+
check.message = `v${currentVersion} (latest)`;
|
|
249
|
+
} else {
|
|
250
|
+
check.status = "warn";
|
|
251
|
+
check.message = `v${currentVersion} installed, v${latestVersion} available. Run: npx skills add selftune-dev/selftune`;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
check.message = `v${currentVersion} (unable to check npm registry)`;
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timeout);
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
check.message = "Unable to check latest version (network unavailable)";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return [check];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function doctor(): Promise<DoctorResult> {
|
|
207
267
|
const allChecks = [
|
|
208
268
|
...checkConfigHealth(),
|
|
209
269
|
...checkLogHealth(),
|
|
210
270
|
...checkHookInstallation(),
|
|
211
271
|
...checkEvolutionHealth(),
|
|
272
|
+
...(await checkVersionHealth()),
|
|
212
273
|
];
|
|
213
274
|
const passed = allChecks.filter((c) => c.status === "pass").length;
|
|
214
275
|
const failed = allChecks.filter((c) => c.status === "fail").length;
|
|
@@ -224,7 +285,7 @@ export function doctor(): DoctorResult {
|
|
|
224
285
|
}
|
|
225
286
|
|
|
226
287
|
if (import.meta.main) {
|
|
227
|
-
const result = doctor();
|
|
288
|
+
const result = await doctor();
|
|
228
289
|
console.log(JSON.stringify(result, null, 2));
|
|
229
290
|
process.exit(result.healthy ? 0 : 1);
|
|
230
291
|
}
|
|
@@ -652,7 +652,7 @@ export async function orchestrate(
|
|
|
652
652
|
const skillRecords = _readSkillRecords();
|
|
653
653
|
const queryRecords = _readQueryRecords();
|
|
654
654
|
const auditEntries = _readAuditEntries();
|
|
655
|
-
const doctorResult = _doctor();
|
|
655
|
+
const doctorResult = await _doctor();
|
|
656
656
|
|
|
657
657
|
const statusResult = _computeStatus(
|
|
658
658
|
telemetry,
|
|
@@ -115,7 +115,7 @@ export async function quickstart(): Promise<void> {
|
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
117
|
const auditEntries = readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG);
|
|
118
|
-
const doctorResult = doctor();
|
|
118
|
+
const doctorResult = await doctor();
|
|
119
119
|
|
|
120
120
|
const result = computeStatus(telemetry, skillRecords, queryRecords, auditEntries, doctorResult);
|
|
121
121
|
const output = formatStatus(result);
|
package/cli/selftune/status.ts
CHANGED
|
@@ -324,13 +324,13 @@ function colorize(text: string, hex: string): string {
|
|
|
324
324
|
// cliMain — reads logs, runs doctor, prints output
|
|
325
325
|
// ---------------------------------------------------------------------------
|
|
326
326
|
|
|
327
|
-
export function cliMain(): void {
|
|
327
|
+
export async function cliMain(): Promise<void> {
|
|
328
328
|
try {
|
|
329
329
|
const telemetry = readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG);
|
|
330
330
|
const skillRecords = readEffectiveSkillUsageRecords();
|
|
331
331
|
const queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
|
|
332
332
|
const auditEntries = readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG);
|
|
333
|
-
const doctorResult = doctor();
|
|
333
|
+
const doctorResult = await doctor();
|
|
334
334
|
|
|
335
335
|
const result = computeStatus(telemetry, skillRecords, queryRecords, auditEntries, doctorResult);
|
|
336
336
|
const output = formatStatus(result);
|
package/cli/selftune/types.ts
CHANGED
|
@@ -67,7 +67,8 @@ export function stripMarkdownFences(raw: string): string {
|
|
|
67
67
|
const newlineIdx = inner.indexOf("\n");
|
|
68
68
|
inner = newlineIdx >= 0 ? inner.slice(newlineIdx + 1) : inner.slice(fence.length);
|
|
69
69
|
// Find matching closing fence (same length of backticks on its own line)
|
|
70
|
-
const
|
|
70
|
+
const escapedFence = fence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
71
|
+
const closingPattern = new RegExp(`^${escapedFence}\\s*$`, "m");
|
|
71
72
|
const closingMatch = inner.match(closingPattern);
|
|
72
73
|
if (closingMatch && closingMatch.index != null) {
|
|
73
74
|
inner = inner.slice(0, closingMatch.index);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selftune",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Self-improving skills CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"cli/selftune/",
|
|
43
43
|
"apps/local-dashboard/dist/",
|
|
44
44
|
"packages/telemetry-contract/",
|
|
45
|
+
"packages/ui/",
|
|
45
46
|
"templates/",
|
|
46
47
|
".claude/agents/",
|
|
47
48
|
"skill/",
|
|
@@ -63,13 +64,14 @@
|
|
|
63
64
|
"start": "bun run cli/selftune/index.ts --help"
|
|
64
65
|
},
|
|
65
66
|
"workspaces": [
|
|
66
|
-
"packages/*"
|
|
67
|
+
"packages/*",
|
|
68
|
+
"apps/*"
|
|
67
69
|
],
|
|
68
70
|
"dependencies": {
|
|
69
|
-
"@selftune/telemetry-contract": "
|
|
71
|
+
"@selftune/telemetry-contract": "file:packages/telemetry-contract"
|
|
70
72
|
},
|
|
71
73
|
"devDependencies": {
|
|
72
|
-
"@biomejs/biome": "2.4.7",
|
|
74
|
+
"@biomejs/biome": "^2.4.7",
|
|
73
75
|
"@types/bun": "^1.1.0"
|
|
74
76
|
}
|
|
75
77
|
}
|