selftune 0.2.21 → 0.2.23
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 +15 -8
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/adapters/cline/hook.ts +167 -0
- package/cli/selftune/adapters/cline/install.ts +197 -0
- package/cli/selftune/adapters/codex/hook.ts +296 -0
- package/cli/selftune/adapters/codex/install.ts +289 -0
- package/cli/selftune/adapters/opencode/hook.ts +222 -0
- package/cli/selftune/adapters/opencode/install.ts +543 -0
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/dashboard-contract.ts +14 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +73 -20
- package/cli/selftune/evolution/validate-body.ts +78 -42
- package/cli/selftune/evolution/validate-routing.ts +45 -104
- package/cli/selftune/hooks/auto-activate.ts +43 -37
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/git-metadata.ts +149 -0
- package/cli/selftune/hooks-shared/hook-output.ts +105 -0
- package/cli/selftune/hooks-shared/normalize.ts +196 -0
- package/cli/selftune/hooks-shared/session-state.ts +76 -0
- package/cli/selftune/hooks-shared/skill-paths.ts +50 -0
- package/cli/selftune/hooks-shared/stdin-dispatch.ts +59 -0
- package/cli/selftune/hooks-shared/types.ts +91 -0
- package/cli/selftune/index.ts +76 -6
- package/cli/selftune/ingestors/pi-ingest.ts +726 -0
- package/cli/selftune/init.ts +11 -1
- package/cli/selftune/localdb/direct-write.ts +85 -0
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries.ts +126 -0
- package/cli/selftune/localdb/schema.ts +38 -0
- package/cli/selftune/observability.ts +8 -1
- package/cli/selftune/orchestrate.ts +43 -0
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +4 -1
- package/cli/selftune/status.ts +31 -0
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/types.ts +2 -1
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +99 -34
- package/cli/selftune/utils/skill-discovery.ts +22 -0
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
- package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +22 -4
- package/packages/telemetry-contract/src/types.ts +1 -12
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +652 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/types.ts +172 -4
- package/skill/SKILL.md +26 -2
- package/skill/Workflows/Ingest.md +60 -2
- package/skill/Workflows/Initialize.md +54 -9
- package/skill/Workflows/PlatformHooks.md +109 -0
- package/skill/Workflows/Registry.md +99 -0
- package/skill/Workflows/Sync.md +3 -1
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry install — Download and extract a skill from the registry.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { hostname } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { registryRequest } from "./client.js";
|
|
11
|
+
|
|
12
|
+
export async function cliMain() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
15
|
+
const globalFlag = args.includes("--global");
|
|
16
|
+
|
|
17
|
+
if (!name) {
|
|
18
|
+
console.error(
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
error: "Usage: selftune registry install <name>",
|
|
21
|
+
guidance: { next_command: "selftune registry list" },
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Find entry by name
|
|
28
|
+
const listResult = await registryRequest<{
|
|
29
|
+
entries: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
current_version?: { id: string; version: string; content_hash: string };
|
|
33
|
+
}>;
|
|
34
|
+
}>("GET", `?name=${encodeURIComponent(name)}`);
|
|
35
|
+
|
|
36
|
+
if (!listResult.success || !listResult.data?.entries?.length) {
|
|
37
|
+
console.error(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
error: `Skill '${name}' not found in registry`,
|
|
40
|
+
guidance: { next_command: "selftune registry list" },
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entry = listResult.data.entries[0];
|
|
47
|
+
const entryId = entry.id;
|
|
48
|
+
|
|
49
|
+
// Get detail with versions
|
|
50
|
+
const detailResult = await registryRequest<{
|
|
51
|
+
entry: { id: string; name: string };
|
|
52
|
+
versions: Array<{ id: string; version: string; content_hash: string; is_current: boolean }>;
|
|
53
|
+
}>("GET", `/${entryId}`);
|
|
54
|
+
|
|
55
|
+
if (!detailResult.success) {
|
|
56
|
+
console.error(JSON.stringify({ error: detailResult.error }));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentVersion = detailResult.data?.versions?.find((v) => v.is_current);
|
|
61
|
+
if (!currentVersion) {
|
|
62
|
+
console.error(JSON.stringify({ error: "No current version found" }));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Request presigned download via sync
|
|
67
|
+
const syncResult = await registryRequest<{
|
|
68
|
+
entries: Array<{
|
|
69
|
+
download_url?: string;
|
|
70
|
+
latest_version: string;
|
|
71
|
+
latest_content_hash: string;
|
|
72
|
+
}>;
|
|
73
|
+
}>("POST", "/sync", {
|
|
74
|
+
body: { installations: [{ entry_id: entryId, current_version_hash: "none" }] },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const downloadUrl = syncResult.data?.entries?.[0]?.download_url;
|
|
78
|
+
if (!downloadUrl) {
|
|
79
|
+
console.error(JSON.stringify({ error: "Could not get download URL" }));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Download archive
|
|
84
|
+
console.log(`Installing ${name} v${currentVersion.version}...`);
|
|
85
|
+
const response = await fetch(downloadUrl, { signal: AbortSignal.timeout(60_000) });
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
console.error(JSON.stringify({ error: `Download failed: HTTP ${response.status}` }));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const archiveBuffer = Buffer.from(await response.arrayBuffer());
|
|
91
|
+
|
|
92
|
+
// Determine install path
|
|
93
|
+
const targetBase = globalFlag
|
|
94
|
+
? join(process.env.HOME || "~", ".claude", "skills")
|
|
95
|
+
: join(process.cwd(), ".claude", "skills");
|
|
96
|
+
const targetDir = join(targetBase, name);
|
|
97
|
+
|
|
98
|
+
// Extract archive
|
|
99
|
+
await mkdir(targetDir, { recursive: true });
|
|
100
|
+
const archivePath = `/tmp/selftune-install-${Date.now()}.tar.gz`;
|
|
101
|
+
await writeFile(archivePath, archiveBuffer);
|
|
102
|
+
const proc = Bun.spawn(["tar", "xzf", archivePath, "-C", targetDir], {
|
|
103
|
+
stdout: "ignore",
|
|
104
|
+
stderr: "pipe",
|
|
105
|
+
});
|
|
106
|
+
await proc.exited;
|
|
107
|
+
|
|
108
|
+
await unlink(archivePath).catch(() => {});
|
|
109
|
+
|
|
110
|
+
if (proc.exitCode !== 0) {
|
|
111
|
+
console.error(JSON.stringify({ error: "Failed to extract archive" }));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Record installation on server
|
|
116
|
+
await registryRequest("POST", `/${entryId}/install`, {
|
|
117
|
+
body: { install_path: targetDir, device_id: hostname() },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Update local state
|
|
121
|
+
const statePath = join(process.env.HOME || "~", ".selftune", "registry-state.json");
|
|
122
|
+
let state: Array<{ entryId: string; name: string; versionHash: string; installPath: string }> =
|
|
123
|
+
[];
|
|
124
|
+
try {
|
|
125
|
+
state = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
126
|
+
} catch {}
|
|
127
|
+
state = state.filter((s) => s.entryId !== entryId);
|
|
128
|
+
state.push({ entryId, name, versionHash: currentVersion.content_hash, installPath: targetDir });
|
|
129
|
+
await mkdir(join(process.env.HOME || "~", ".selftune"), { recursive: true });
|
|
130
|
+
await writeFile(statePath, JSON.stringify(state, null, 2));
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
success: true,
|
|
135
|
+
name,
|
|
136
|
+
version: currentVersion.version,
|
|
137
|
+
path: targetDir,
|
|
138
|
+
global: globalFlag,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry list — Show all published entries in the org.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registryRequest } from "./client.js";
|
|
6
|
+
|
|
7
|
+
export async function cliMain() {
|
|
8
|
+
const result = await registryRequest<{
|
|
9
|
+
entries: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
entry_type: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
current_version?: { version: string };
|
|
14
|
+
pass_rate: number | null;
|
|
15
|
+
eval_count: number;
|
|
16
|
+
}>;
|
|
17
|
+
}>("GET", "");
|
|
18
|
+
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
console.error(JSON.stringify({ error: result.error }));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const entries = result.data?.entries || [];
|
|
25
|
+
if (entries.length === 0) {
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
message: "No entries in registry. Use 'selftune registry push' to publish a skill.",
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const table = entries.map((e) => ({
|
|
35
|
+
name: e.name,
|
|
36
|
+
type: e.entry_type,
|
|
37
|
+
version: e.current_version?.version || "—",
|
|
38
|
+
pass_rate: e.pass_rate,
|
|
39
|
+
eval_count: e.eval_count,
|
|
40
|
+
description: e.description,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
console.log(JSON.stringify({ entries: table, total: entries.length }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry push — Archive and upload a skill folder as a new version.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { registryRequest } from "./client.js";
|
|
10
|
+
|
|
11
|
+
interface FileManifestEntry {
|
|
12
|
+
path: string;
|
|
13
|
+
hash: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function collectFiles(
|
|
18
|
+
dir: string,
|
|
19
|
+
base?: string,
|
|
20
|
+
): Promise<{ path: string; content: Buffer }[]> {
|
|
21
|
+
const files: { path: string; content: Buffer }[] = [];
|
|
22
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = join(dir, entry.name);
|
|
25
|
+
const relPath = base ? join(base, entry.name) : entry.name;
|
|
26
|
+
if (
|
|
27
|
+
entry.name === ".git" ||
|
|
28
|
+
entry.name === "node_modules" ||
|
|
29
|
+
entry.name === ".env" ||
|
|
30
|
+
entry.name.startsWith(".env.")
|
|
31
|
+
)
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
files.push(...(await collectFiles(fullPath, relPath)));
|
|
35
|
+
} else {
|
|
36
|
+
files.push({ path: relPath, content: await readFile(fullPath) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function cliMain() {
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
const nameArg = args.find((a) => !a.startsWith("--"));
|
|
45
|
+
const versionFlag = args.find((a) => a.startsWith("--version="))?.slice("--version=".length);
|
|
46
|
+
const summaryFlag = args.find((a) => a.startsWith("--summary="))?.slice("--summary=".length);
|
|
47
|
+
|
|
48
|
+
// Find skill folder
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const skillMd = join(cwd, "SKILL.md");
|
|
51
|
+
try {
|
|
52
|
+
await stat(skillMd);
|
|
53
|
+
} catch {
|
|
54
|
+
console.error(
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
error: "No SKILL.md found in current directory. Navigate to a skill folder first.",
|
|
57
|
+
guidance: { next_command: "cd <skill-directory>" },
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read SKILL.md to extract name and description
|
|
64
|
+
const skillContent = await readFile(skillMd, "utf-8");
|
|
65
|
+
const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
|
|
66
|
+
const descMatch = skillContent.match(/^description:\s*(.+)$/m);
|
|
67
|
+
const name = nameArg || nameMatch?.[1]?.trim() || "unnamed-skill";
|
|
68
|
+
const description = descMatch?.[1]?.trim() || "";
|
|
69
|
+
|
|
70
|
+
// Collect all files
|
|
71
|
+
const files = await collectFiles(cwd);
|
|
72
|
+
const manifest: FileManifestEntry[] = files.map((f) => ({
|
|
73
|
+
path: f.path,
|
|
74
|
+
hash: createHash("sha256").update(f.content).digest("hex"),
|
|
75
|
+
size: f.content.length,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// Create tar.gz archive using system tar (available on all platforms)
|
|
79
|
+
const archivePath = `/tmp/selftune-registry-${Date.now()}.tar.gz`;
|
|
80
|
+
const proc = Bun.spawn(
|
|
81
|
+
[
|
|
82
|
+
"tar",
|
|
83
|
+
"czf",
|
|
84
|
+
archivePath,
|
|
85
|
+
"-C",
|
|
86
|
+
cwd,
|
|
87
|
+
"--exclude=.git",
|
|
88
|
+
"--exclude=node_modules",
|
|
89
|
+
"--exclude=.env",
|
|
90
|
+
"--exclude=.env.*",
|
|
91
|
+
".",
|
|
92
|
+
],
|
|
93
|
+
{
|
|
94
|
+
stdout: "ignore",
|
|
95
|
+
stderr: "pipe",
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
await proc.exited;
|
|
99
|
+
if (proc.exitCode !== 0) {
|
|
100
|
+
console.error(JSON.stringify({ error: "Failed to create archive" }));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const archiveBuffer = await readFile(archivePath);
|
|
104
|
+
const archiveHash = createHash("sha256").update(archiveBuffer).digest("hex");
|
|
105
|
+
|
|
106
|
+
// Clean up temp file
|
|
107
|
+
await unlink(archivePath).catch(() => {});
|
|
108
|
+
|
|
109
|
+
// Determine version
|
|
110
|
+
const version = versionFlag || `0.1.${Date.now()}`;
|
|
111
|
+
|
|
112
|
+
// Build multipart form
|
|
113
|
+
const formData = new FormData();
|
|
114
|
+
formData.append(
|
|
115
|
+
"metadata",
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
name,
|
|
118
|
+
entry_type: "skill",
|
|
119
|
+
description,
|
|
120
|
+
version,
|
|
121
|
+
change_summary: summaryFlag || undefined,
|
|
122
|
+
file_manifest: manifest,
|
|
123
|
+
content_hash: archiveHash,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
formData.append(
|
|
127
|
+
"archive",
|
|
128
|
+
new Blob([archiveBuffer], { type: "application/gzip" }),
|
|
129
|
+
`${name}.tar.gz`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Try to push as new version first, fall back to create
|
|
133
|
+
console.log(
|
|
134
|
+
`Pushing ${name} v${version} (${(archiveBuffer.length / 1024).toFixed(1)} KB, ${files.length} files)...`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// First check if entry exists
|
|
138
|
+
const listResult = await registryRequest<{ entries: Array<{ id: string; name: string }> }>(
|
|
139
|
+
"GET",
|
|
140
|
+
`?name=${encodeURIComponent(name)}`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
let result;
|
|
144
|
+
if (listResult.success && listResult.data?.entries?.length) {
|
|
145
|
+
const entryId = listResult.data.entries[0].id;
|
|
146
|
+
result = await registryRequest("POST", `/${entryId}/versions`, { formData });
|
|
147
|
+
} else {
|
|
148
|
+
result = await registryRequest("POST", "", { formData });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.success) {
|
|
152
|
+
console.log(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
success: true,
|
|
155
|
+
name,
|
|
156
|
+
version,
|
|
157
|
+
files: files.length,
|
|
158
|
+
size: archiveBuffer.length,
|
|
159
|
+
hash: archiveHash,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
console.error(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
error: result.error,
|
|
166
|
+
guidance: { next_command: "selftune registry list" },
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry rollback — Rollback a skill to a previous version.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { registryRequest } from "./client.js";
|
|
6
|
+
|
|
7
|
+
export async function cliMain() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const name = args.find((a) => !a.startsWith("--"));
|
|
10
|
+
const toVersion = args.find((a) => a.startsWith("--to="))?.slice("--to=".length);
|
|
11
|
+
const reason = args.find((a) => a.startsWith("--reason="))?.slice("--reason=".length);
|
|
12
|
+
|
|
13
|
+
if (!name) {
|
|
14
|
+
console.error(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
error: "Usage: selftune registry rollback <name> [--to=version] [--reason=text]",
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Find entry
|
|
23
|
+
const listResult = await registryRequest<{ entries: Array<{ id: string; name: string }> }>(
|
|
24
|
+
"GET",
|
|
25
|
+
`?name=${encodeURIComponent(name)}`,
|
|
26
|
+
);
|
|
27
|
+
if (!listResult.success || !listResult.data?.entries?.length) {
|
|
28
|
+
console.error(JSON.stringify({ error: `Skill '${name}' not found in registry` }));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entryId = listResult.data.entries[0].id;
|
|
33
|
+
const result = await registryRequest("POST", `/${entryId}/rollback`, {
|
|
34
|
+
body: { target_version: toVersion, reason },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (result.success) {
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
success: true,
|
|
41
|
+
name,
|
|
42
|
+
message: "Rolled back. Run 'selftune registry sync' to update local installations.",
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
console.error(JSON.stringify({ error: result.error }));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry status — Show installed entries and version drift.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { registryRequest } from "./client.js";
|
|
9
|
+
|
|
10
|
+
export async function cliMain() {
|
|
11
|
+
const statePath = join(process.env.HOME || "~", ".selftune", "registry-state.json");
|
|
12
|
+
let state: Array<{ entryId: string; name: string; versionHash: string; installPath: string }>;
|
|
13
|
+
try {
|
|
14
|
+
state = JSON.parse(await readFile(statePath, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
console.log(JSON.stringify({ message: "No registry installations found." }));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (state.length === 0) {
|
|
21
|
+
console.log(JSON.stringify({ message: "No registry installations found." }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const syncResult = await registryRequest<{
|
|
26
|
+
entries: Array<{
|
|
27
|
+
entry_id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
has_update: boolean;
|
|
30
|
+
latest_version: string;
|
|
31
|
+
current_version: string;
|
|
32
|
+
}>;
|
|
33
|
+
}>("POST", "/sync", {
|
|
34
|
+
body: {
|
|
35
|
+
installations: state.map((s) => ({
|
|
36
|
+
entry_id: s.entryId,
|
|
37
|
+
current_version_hash: s.versionHash,
|
|
38
|
+
})),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!syncResult.success) {
|
|
43
|
+
console.error(JSON.stringify({ error: syncResult.error }));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entries = syncResult.data?.entries || [];
|
|
48
|
+
const table = entries.map((e) => ({
|
|
49
|
+
name: e.name,
|
|
50
|
+
installed: e.current_version,
|
|
51
|
+
latest: e.latest_version,
|
|
52
|
+
status: e.has_update ? "behind" : "up-to-date",
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
console.log(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
installations: table,
|
|
58
|
+
total: state.length,
|
|
59
|
+
updates_available: entries.filter((e) => e.has_update).length,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selftune registry sync — Check for updates and pull latest versions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFile, readFile, mkdir, unlink } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { registryRequest } from "./client.js";
|
|
9
|
+
|
|
10
|
+
interface LocalState {
|
|
11
|
+
entryId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
versionHash: string;
|
|
14
|
+
installPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getStatePath(): string {
|
|
18
|
+
return join(process.env.HOME || "~", ".selftune", "registry-state.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadState(): Promise<LocalState[]> {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(getStatePath(), "utf-8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function saveState(state: LocalState[]): Promise<void> {
|
|
31
|
+
await mkdir(join(process.env.HOME || "~", ".selftune"), { recursive: true });
|
|
32
|
+
await writeFile(getStatePath(), JSON.stringify(state, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function cliMain() {
|
|
36
|
+
const state = await loadState();
|
|
37
|
+
if (state.length === 0) {
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
message: "No registry installations found. Use 'selftune registry install <name>' first.",
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for updates
|
|
47
|
+
const syncResult = await registryRequest<{
|
|
48
|
+
entries: Array<{
|
|
49
|
+
entry_id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
has_update: boolean;
|
|
52
|
+
latest_version: string;
|
|
53
|
+
latest_content_hash: string;
|
|
54
|
+
download_url?: string;
|
|
55
|
+
}>;
|
|
56
|
+
}>("POST", "/sync", {
|
|
57
|
+
body: {
|
|
58
|
+
installations: state.map((s) => ({
|
|
59
|
+
entry_id: s.entryId,
|
|
60
|
+
current_version_hash: s.versionHash,
|
|
61
|
+
})),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!syncResult.success) {
|
|
66
|
+
console.error(JSON.stringify({ error: syncResult.error }));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const updates = syncResult.data?.entries?.filter((e) => e.has_update) || [];
|
|
71
|
+
if (updates.length === 0) {
|
|
72
|
+
console.log(JSON.stringify({ message: "All installations up to date", count: state.length }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Found ${updates.length} update(s)...`);
|
|
77
|
+
let synced = 0;
|
|
78
|
+
let failed = 0;
|
|
79
|
+
|
|
80
|
+
for (const update of updates) {
|
|
81
|
+
if (!update.download_url) {
|
|
82
|
+
failed++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const localEntry = state.find((s) => s.entryId === update.entry_id);
|
|
87
|
+
if (!localEntry) {
|
|
88
|
+
failed++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(update.download_url, { signal: AbortSignal.timeout(60_000) });
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
failed++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const archiveBuffer = Buffer.from(await response.arrayBuffer());
|
|
100
|
+
const archivePath = `/tmp/selftune-sync-${Date.now()}.tar.gz`;
|
|
101
|
+
await writeFile(archivePath, archiveBuffer);
|
|
102
|
+
|
|
103
|
+
// Extract to existing install path
|
|
104
|
+
const proc = Bun.spawn(["tar", "xzf", archivePath, "-C", localEntry.installPath], {
|
|
105
|
+
stdout: "ignore",
|
|
106
|
+
stderr: "pipe",
|
|
107
|
+
});
|
|
108
|
+
await proc.exited;
|
|
109
|
+
await unlink(archivePath).catch(() => {});
|
|
110
|
+
|
|
111
|
+
if (proc.exitCode === 0) {
|
|
112
|
+
localEntry.versionHash = update.latest_content_hash;
|
|
113
|
+
synced++;
|
|
114
|
+
console.log(` updated: ${update.name} -> v${update.latest_version}`);
|
|
115
|
+
} else {
|
|
116
|
+
failed++;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
failed++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await saveState(state);
|
|
124
|
+
console.log(JSON.stringify({ synced, failed, total: state.length }));
|
|
125
|
+
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
findInstalledSkillPath,
|
|
32
32
|
findRepositoryClaudeSkillDirs,
|
|
33
33
|
findRepositorySkillDirs,
|
|
34
|
+
isTestFixturePath,
|
|
34
35
|
} from "../utils/skill-discovery.js";
|
|
35
36
|
import { writeRepairedSkillUsageRecords } from "../utils/skill-log.js";
|
|
36
37
|
|
|
@@ -380,7 +381,7 @@ function extractSessionSkillUsage(
|
|
|
380
381
|
|
|
381
382
|
if (toolName === "Read") {
|
|
382
383
|
const filePath = (input.file_path as string) ?? "";
|
|
383
|
-
if (filePath.endsWith("SKILL.md")) {
|
|
384
|
+
if (filePath.endsWith("SKILL.md") && !isTestFixturePath(filePath)) {
|
|
384
385
|
const inferredSkillName = basename(dirname(filePath)).trim();
|
|
385
386
|
if (inferredSkillName && !skillPathLookup.has(inferredSkillName)) {
|
|
386
387
|
skillPathLookup.set(inferredSkillName.toLowerCase(), filePath);
|
|
@@ -421,6 +422,8 @@ function extractSessionSkillUsage(
|
|
|
421
422
|
? { skillPath: knownSkillPath, resolutionSource: "raw_log" as const }
|
|
422
423
|
: resolveClaudeSkillPath(skillName, sessionCwd, homeDir, codexHome);
|
|
423
424
|
|
|
425
|
+
if (isTestFixturePath(skillPath)) continue;
|
|
426
|
+
|
|
424
427
|
const recordIndex =
|
|
425
428
|
repaired.push({
|
|
426
429
|
timestamp: timestamp || lastUserMessage.timestamp || fallbackTimestamp,
|
package/cli/selftune/status.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { getQueueStats } from "./alpha-upload/queue.js";
|
|
|
17
17
|
import { getBaseUrl } from "./auth/device-code.js";
|
|
18
18
|
import { SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
19
19
|
import { getDb } from "./localdb/db.js";
|
|
20
|
+
import { writeCronRunToDb } from "./localdb/direct-write.js";
|
|
20
21
|
import {
|
|
21
22
|
getLastUploadError,
|
|
22
23
|
getLastUploadSuccess,
|
|
@@ -576,6 +577,8 @@ export function formatAlphaStatus(info: AlphaStatusInfo | null): string {
|
|
|
576
577
|
|
|
577
578
|
export async function cliMain(): Promise<void> {
|
|
578
579
|
const db = getDb();
|
|
580
|
+
const statusStartedAt = new Date();
|
|
581
|
+
const statusStart = performance.now();
|
|
579
582
|
try {
|
|
580
583
|
const telemetry = querySessionTelemetry(db) as SessionTelemetryRecord[];
|
|
581
584
|
const skillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
|
|
@@ -610,9 +613,37 @@ export async function cliMain(): Promise<void> {
|
|
|
610
613
|
}
|
|
611
614
|
console.log(formatAlphaStatus(alphaInfo));
|
|
612
615
|
|
|
616
|
+
// Log cron run for unified timeline visibility
|
|
617
|
+
const statusElapsed = Math.round(performance.now() - statusStart);
|
|
618
|
+
writeCronRunToDb(db, {
|
|
619
|
+
jobName: "status",
|
|
620
|
+
startedAt: statusStartedAt.toISOString(),
|
|
621
|
+
elapsedMs: statusElapsed,
|
|
622
|
+
status: "success",
|
|
623
|
+
metrics: {
|
|
624
|
+
total_skills: result.skills.length,
|
|
625
|
+
healthy: result.skills.filter((s) => s.status === "HEALTHY").length,
|
|
626
|
+
warning: result.skills.filter((s) => s.status === "WARNING").length,
|
|
627
|
+
critical: result.skills.filter((s) => s.status === "CRITICAL").length,
|
|
628
|
+
system_healthy: result.system.healthy,
|
|
629
|
+
unmatched_queries: result.unmatchedQueries,
|
|
630
|
+
pending_proposals: result.pendingProposals,
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
|
|
613
634
|
process.exit(0);
|
|
614
635
|
} catch (err) {
|
|
636
|
+
// Log failed status run
|
|
637
|
+
const statusElapsed = Math.round(performance.now() - statusStart);
|
|
615
638
|
const message = err instanceof Error ? err.message : String(err);
|
|
639
|
+
writeCronRunToDb(db, {
|
|
640
|
+
jobName: "status",
|
|
641
|
+
startedAt: statusStartedAt.toISOString(),
|
|
642
|
+
elapsedMs: statusElapsed,
|
|
643
|
+
status: "error",
|
|
644
|
+
error: message,
|
|
645
|
+
});
|
|
646
|
+
|
|
616
647
|
console.error(`selftune status failed: ${message}`);
|
|
617
648
|
process.exit(1);
|
|
618
649
|
}
|