selftune 0.2.29 → 0.2.31
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/apps/local-dashboard/dist/assets/index-B7v_o1WC.js +15 -0
- package/apps/local-dashboard/dist/assets/index-CrO77SVi.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +1 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/auto-update.ts +40 -8
- package/cli/selftune/command-surface.ts +1 -1
- package/cli/selftune/constants.ts +5 -0
- package/cli/selftune/dashboard-action-events.ts +117 -0
- package/cli/selftune/dashboard-action-instrumentation.ts +103 -0
- package/cli/selftune/dashboard-action-result.ts +90 -0
- package/cli/selftune/dashboard-action-stream.ts +252 -0
- package/cli/selftune/dashboard-contract.ts +81 -1
- package/cli/selftune/dashboard-server.ts +133 -16
- package/cli/selftune/eval/hooks-to-evals.ts +157 -0
- package/cli/selftune/eval/synthetic-evals.ts +33 -2
- package/cli/selftune/eval/unit-test-cli.ts +53 -5
- package/cli/selftune/evolution/validate-host-replay.ts +191 -14
- package/cli/selftune/index.ts +4 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +117 -8
- package/cli/selftune/localdb/schema.ts +34 -0
- package/cli/selftune/registry/github-install.ts +256 -0
- package/cli/selftune/registry/index.ts +1 -1
- package/cli/selftune/registry/install.ts +58 -7
- package/cli/selftune/routes/actions.ts +273 -42
- package/cli/selftune/testing-readiness.ts +203 -10
- package/cli/selftune/utils/llm-call.ts +90 -1
- package/package.json +1 -1
- package/packages/dashboard-core/src/routes/manifest.ts +2 -2
- package/packages/ui/src/components/EvolutionTimeline.tsx +1 -1
- package/packages/ui/src/components/SkillReportPanels.tsx +7 -7
- package/packages/ui/src/primitives/button.tsx +5 -0
- package/skill/SKILL.md +1 -1
- package/skill/workflows/Dashboard.md +50 -23
- package/skill/workflows/Registry.md +19 -13
- package/apps/local-dashboard/dist/assets/index-BcvtYmmL.js +0 -15
- package/apps/local-dashboard/dist/assets/index-BpRIxnpS.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-DqH_uxum.js +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cp, mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export interface GithubRegistryInstallTarget {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
repoFullName: string;
|
|
14
|
+
ref: string | null;
|
|
15
|
+
skillPath: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeGithubSkillPath(skillPath: string): string {
|
|
19
|
+
const trimmed = skillPath.trim().replace(/\\/g, "/");
|
|
20
|
+
if (!trimmed || trimmed === ".") {
|
|
21
|
+
return ".";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
25
|
+
if (segments.includes("..")) {
|
|
26
|
+
throw new Error("GitHub skill path must stay within the repository");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\/+|\/+$/g, "");
|
|
30
|
+
return normalized || ".";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseGithubRegistryInstallTarget(
|
|
34
|
+
rawTarget: string,
|
|
35
|
+
): GithubRegistryInstallTarget | null {
|
|
36
|
+
if (!rawTarget.startsWith("github:")) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const spec = rawTarget.slice("github:".length).trim();
|
|
41
|
+
if (!spec) {
|
|
42
|
+
throw new Error("GitHub install target must be github:owner/repo[@ref][//path]");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pathSeparatorIndex = spec.indexOf("//");
|
|
46
|
+
const repoWithMaybeRef = pathSeparatorIndex === -1 ? spec : spec.slice(0, pathSeparatorIndex);
|
|
47
|
+
const pathWithMaybeRef = pathSeparatorIndex === -1 ? null : spec.slice(pathSeparatorIndex + 2);
|
|
48
|
+
|
|
49
|
+
let ref: string | null = null;
|
|
50
|
+
let repoSpec = repoWithMaybeRef;
|
|
51
|
+
|
|
52
|
+
const repoRefIndex = repoWithMaybeRef.lastIndexOf("@");
|
|
53
|
+
if (repoRefIndex !== -1) {
|
|
54
|
+
repoSpec = repoWithMaybeRef.slice(0, repoRefIndex);
|
|
55
|
+
ref = repoWithMaybeRef.slice(repoRefIndex + 1) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let skillPath: string | null = null;
|
|
59
|
+
if (pathWithMaybeRef != null) {
|
|
60
|
+
const pathRefIndex = pathWithMaybeRef.lastIndexOf("@");
|
|
61
|
+
if (pathRefIndex !== -1) {
|
|
62
|
+
skillPath = pathWithMaybeRef.slice(0, pathRefIndex) || ".";
|
|
63
|
+
ref = pathWithMaybeRef.slice(pathRefIndex + 1) || ref;
|
|
64
|
+
} else {
|
|
65
|
+
skillPath = pathWithMaybeRef || ".";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const match = repoSpec.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
70
|
+
if (!match) {
|
|
71
|
+
throw new Error("GitHub install target must look like github:owner/repo[@ref][//path]");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
owner: match[1],
|
|
76
|
+
repo: match[2],
|
|
77
|
+
repoFullName: `${match[1]}/${match[2]}`,
|
|
78
|
+
ref,
|
|
79
|
+
skillPath: skillPath ? normalizeGithubSkillPath(skillPath) : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isExcludedEntry(name: string): boolean {
|
|
84
|
+
return name === ".git" || name === "node_modules" || name === ".env" || name.startsWith(".env.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function discoverLocalSkillPaths(rootDir: string): Promise<string[]> {
|
|
88
|
+
async function walk(currentDir: string, basePath: string): Promise<string[]> {
|
|
89
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
90
|
+
const discovered: string[] = [];
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (isExcludedEntry(entry.name)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
98
|
+
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
|
|
99
|
+
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
discovered.push(...(await walk(fullPath, relativePath)));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
106
|
+
discovered.push(basePath ? basePath.split(path.sep).join("/") : ".");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return discovered;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const discovered = await walk(rootDir, "");
|
|
114
|
+
return [...new Set(discovered)].sort((a, b) => a.localeCompare(b));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function resolveGithubSkillPath(
|
|
118
|
+
repoDir: string,
|
|
119
|
+
requestedSkillPath: string | null,
|
|
120
|
+
): Promise<{ skillPath: string; availablePaths: string[] }> {
|
|
121
|
+
const availablePaths = await discoverLocalSkillPaths(repoDir);
|
|
122
|
+
|
|
123
|
+
if (requestedSkillPath) {
|
|
124
|
+
const normalized = normalizeGithubSkillPath(requestedSkillPath);
|
|
125
|
+
const skillMdPath =
|
|
126
|
+
normalized === "."
|
|
127
|
+
? path.join(repoDir, "SKILL.md")
|
|
128
|
+
: path.join(repoDir, ...normalized.split("/"), "SKILL.md");
|
|
129
|
+
await stat(skillMdPath);
|
|
130
|
+
return { skillPath: normalized, availablePaths };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (availablePaths.length === 1) {
|
|
134
|
+
return { skillPath: availablePaths[0] ?? ".", availablePaths };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (availablePaths.length === 0) {
|
|
138
|
+
throw new Error("No SKILL.md found in the GitHub repository");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Multiple skills found in the GitHub repository. Choose one with github:owner/repo//path (available: ${availablePaths.join(", ")})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deriveGithubInstallSkillName(
|
|
147
|
+
frontmatterName: string,
|
|
148
|
+
skillPath: string,
|
|
149
|
+
skillDir: string,
|
|
150
|
+
repoName: string,
|
|
151
|
+
): string {
|
|
152
|
+
const trimmedName = frontmatterName.trim();
|
|
153
|
+
if (trimmedName) {
|
|
154
|
+
return trimmedName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return skillPath === "." ? repoName : path.basename(skillDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cloneGithubRepository(
|
|
161
|
+
target: GithubRegistryInstallTarget,
|
|
162
|
+
cloneDir: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const repoUrl = `https://github.com/${target.repoFullName}.git`;
|
|
165
|
+
const args = ["clone", "--depth=1"];
|
|
166
|
+
|
|
167
|
+
if (target.ref) {
|
|
168
|
+
args.push("--branch", target.ref);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
args.push(repoUrl, cloneDir);
|
|
172
|
+
|
|
173
|
+
await execFileAsync("git", args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function copySkillDirectory(sourceDir: string, targetDir: string): Promise<void> {
|
|
177
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
178
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
179
|
+
|
|
180
|
+
await cp(sourceDir, targetDir, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
filter: (entryPath) => {
|
|
183
|
+
const basename = path.basename(entryPath);
|
|
184
|
+
return !isExcludedEntry(basename);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function installFromGithubTarget(
|
|
190
|
+
rawTarget: string,
|
|
191
|
+
globalFlag: boolean,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const target = parseGithubRegistryInstallTarget(rawTarget);
|
|
194
|
+
if (!target) {
|
|
195
|
+
throw new Error("GitHub install target must start with github:");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "selftune-github-install-"));
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const cloneDir = path.join(tempRoot, "repo");
|
|
202
|
+
await cloneGithubRepository(target, cloneDir);
|
|
203
|
+
|
|
204
|
+
const { skillPath, availablePaths } = await resolveGithubSkillPath(cloneDir, target.skillPath);
|
|
205
|
+
const skillDir = skillPath === "." ? cloneDir : path.join(cloneDir, ...skillPath.split("/"));
|
|
206
|
+
const skillContent = await readFile(path.join(skillDir, "SKILL.md"), "utf-8");
|
|
207
|
+
const frontmatter = parseFrontmatter(skillContent);
|
|
208
|
+
const skillName = deriveGithubInstallSkillName(
|
|
209
|
+
frontmatter.name,
|
|
210
|
+
skillPath,
|
|
211
|
+
skillDir,
|
|
212
|
+
target.repo,
|
|
213
|
+
);
|
|
214
|
+
const resolvedCommit = (
|
|
215
|
+
await execFileAsync("git", ["-C", cloneDir, "rev-parse", "HEAD"])
|
|
216
|
+
).stdout.trim();
|
|
217
|
+
|
|
218
|
+
const targetBase = globalFlag
|
|
219
|
+
? path.join(process.env.HOME || "~", ".claude", "skills")
|
|
220
|
+
: path.join(process.cwd(), ".claude", "skills");
|
|
221
|
+
const targetDir = path.join(targetBase, skillName);
|
|
222
|
+
|
|
223
|
+
await copySkillDirectory(skillDir, targetDir);
|
|
224
|
+
await writeFile(
|
|
225
|
+
path.join(targetDir, ".selftune-source.json"),
|
|
226
|
+
JSON.stringify(
|
|
227
|
+
{
|
|
228
|
+
source: "github-direct",
|
|
229
|
+
repo: target.repoFullName,
|
|
230
|
+
ref: target.ref ?? "HEAD",
|
|
231
|
+
commit: resolvedCommit,
|
|
232
|
+
skill_path: skillPath,
|
|
233
|
+
available_paths: availablePaths,
|
|
234
|
+
},
|
|
235
|
+
null,
|
|
236
|
+
2,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
success: true,
|
|
243
|
+
source: "github-direct",
|
|
244
|
+
name: skillName,
|
|
245
|
+
repo: target.repoFullName,
|
|
246
|
+
ref: target.ref ?? "HEAD",
|
|
247
|
+
commit: resolvedCommit,
|
|
248
|
+
skill_path: skillPath,
|
|
249
|
+
path: targetDir,
|
|
250
|
+
global: globalFlag,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
} finally {
|
|
254
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -24,7 +24,7 @@ Usage:
|
|
|
24
24
|
|
|
25
25
|
Subcommands:
|
|
26
26
|
push [name] Push current skill folder as a new version
|
|
27
|
-
install <name> Download
|
|
27
|
+
install <name> Download from the registry or install github:owner/repo[@ref][//path]
|
|
28
28
|
sync Check for updates and pull latest versions
|
|
29
29
|
status Show installed entries and version drift
|
|
30
30
|
rollback <name> Rollback to a previous version
|
|
@@ -8,6 +8,7 @@ import { hostname } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import { registryRequest } from "./client.js";
|
|
11
|
+
import { installFromGithubTarget, parseGithubRegistryInstallTarget } from "./github-install.js";
|
|
11
12
|
|
|
12
13
|
export async function cliMain() {
|
|
13
14
|
const args = process.argv.slice(2);
|
|
@@ -17,13 +18,45 @@ export async function cliMain() {
|
|
|
17
18
|
if (!name) {
|
|
18
19
|
console.error(
|
|
19
20
|
JSON.stringify({
|
|
20
|
-
error: "Usage: selftune registry install <name>",
|
|
21
|
+
error: "Usage: selftune registry install <name|github:owner/repo[@ref][//path]>",
|
|
21
22
|
guidance: { next_command: "selftune registry list" },
|
|
22
23
|
}),
|
|
23
24
|
);
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
let githubTarget = null;
|
|
29
|
+
try {
|
|
30
|
+
githubTarget = parseGithubRegistryInstallTarget(name);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
error: error instanceof Error ? error.message : "Invalid GitHub install target",
|
|
35
|
+
guidance: {
|
|
36
|
+
next_command: "selftune registry install github:owner/repo//path",
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (githubTarget) {
|
|
44
|
+
try {
|
|
45
|
+
await installFromGithubTarget(name, globalFlag);
|
|
46
|
+
return;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
error: error instanceof Error ? error.message : "GitHub install failed",
|
|
51
|
+
guidance: {
|
|
52
|
+
next_command: "selftune registry install github:owner/repo//path",
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
// Find entry by name
|
|
28
61
|
const listResult = await registryRequest<{
|
|
29
62
|
entries: Array<{
|
|
@@ -49,7 +82,12 @@ export async function cliMain() {
|
|
|
49
82
|
// Get detail with versions
|
|
50
83
|
const detailResult = await registryRequest<{
|
|
51
84
|
entry: { id: string; name: string };
|
|
52
|
-
versions: Array<{
|
|
85
|
+
versions: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
version: string;
|
|
88
|
+
content_hash: string;
|
|
89
|
+
is_current: boolean;
|
|
90
|
+
}>;
|
|
53
91
|
}>("GET", `/${entryId}`);
|
|
54
92
|
|
|
55
93
|
if (!detailResult.success) {
|
|
@@ -71,7 +109,9 @@ export async function cliMain() {
|
|
|
71
109
|
latest_content_hash: string;
|
|
72
110
|
}>;
|
|
73
111
|
}>("POST", "/sync", {
|
|
74
|
-
body: {
|
|
112
|
+
body: {
|
|
113
|
+
installations: [{ entry_id: entryId, current_version_hash: "none" }],
|
|
114
|
+
},
|
|
75
115
|
});
|
|
76
116
|
|
|
77
117
|
const downloadUrl = syncResult.data?.entries?.[0]?.download_url;
|
|
@@ -82,7 +122,9 @@ export async function cliMain() {
|
|
|
82
122
|
|
|
83
123
|
// Download archive
|
|
84
124
|
console.log(`Installing ${name} v${currentVersion.version}...`);
|
|
85
|
-
const response = await fetch(downloadUrl, {
|
|
125
|
+
const response = await fetch(downloadUrl, {
|
|
126
|
+
signal: AbortSignal.timeout(60_000),
|
|
127
|
+
});
|
|
86
128
|
if (!response.ok) {
|
|
87
129
|
console.error(JSON.stringify({ error: `Download failed: HTTP ${response.status}` }));
|
|
88
130
|
process.exit(1);
|
|
@@ -119,13 +161,22 @@ export async function cliMain() {
|
|
|
119
161
|
|
|
120
162
|
// Update local state
|
|
121
163
|
const statePath = join(process.env.HOME || "~", ".selftune", "registry-state.json");
|
|
122
|
-
let state: Array<{
|
|
123
|
-
|
|
164
|
+
let state: Array<{
|
|
165
|
+
entryId: string;
|
|
166
|
+
name: string;
|
|
167
|
+
versionHash: string;
|
|
168
|
+
installPath: string;
|
|
169
|
+
}> = [];
|
|
124
170
|
try {
|
|
125
171
|
state = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
126
172
|
} catch {}
|
|
127
173
|
state = state.filter((s) => s.entryId !== entryId);
|
|
128
|
-
state.push({
|
|
174
|
+
state.push({
|
|
175
|
+
entryId,
|
|
176
|
+
name,
|
|
177
|
+
versionHash: currentVersion.content_hash,
|
|
178
|
+
installPath: targetDir,
|
|
179
|
+
});
|
|
129
180
|
await mkdir(join(process.env.HOME || "~", ".selftune"), { recursive: true });
|
|
130
181
|
await writeFile(statePath, JSON.stringify(state, null, 2));
|
|
131
182
|
|