selftune 0.2.22 → 0.2.24

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.
Files changed (270) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +95 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/adapters/pi/hook.ts +273 -0
  12. package/cli/selftune/adapters/pi/install.ts +207 -0
  13. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  15. package/cli/selftune/auto-update.ts +200 -8
  16. package/cli/selftune/canonical-export.ts +55 -25
  17. package/cli/selftune/command-surface.ts +397 -0
  18. package/cli/selftune/constants.ts +10 -1
  19. package/cli/selftune/contribute/contribute.ts +64 -13
  20. package/cli/selftune/contribution-config.ts +57 -3
  21. package/cli/selftune/contribution-preferences.ts +117 -0
  22. package/cli/selftune/contribution-signals.ts +8 -4
  23. package/cli/selftune/contribution-staging.ts +13 -2
  24. package/cli/selftune/contributions.ts +55 -121
  25. package/cli/selftune/creator-contributions.ts +29 -10
  26. package/cli/selftune/cron/setup.ts +7 -3
  27. package/cli/selftune/dashboard-contract.ts +87 -0
  28. package/cli/selftune/dashboard-server.ts +168 -17
  29. package/cli/selftune/dashboard.ts +350 -17
  30. package/cli/selftune/eval/baseline.ts +21 -5
  31. package/cli/selftune/eval/execution-eval.ts +170 -0
  32. package/cli/selftune/eval/family-overlap.ts +2 -2
  33. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  34. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  35. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  36. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  37. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  38. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  39. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  40. package/cli/selftune/evolution/engines/replay-engine.ts +180 -0
  41. package/cli/selftune/evolution/evidence.ts +2 -6
  42. package/cli/selftune/evolution/evolve-body.ts +152 -38
  43. package/cli/selftune/evolution/evolve.ts +244 -52
  44. package/cli/selftune/evolution/rollback.ts +0 -1
  45. package/cli/selftune/evolution/validate-body.ts +111 -49
  46. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  47. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  48. package/cli/selftune/evolution/validate-routing.ts +51 -108
  49. package/cli/selftune/evolution/validation-contract.ts +91 -0
  50. package/cli/selftune/grading/auto-grade.ts +11 -7
  51. package/cli/selftune/grading/grade-session.ts +10 -16
  52. package/cli/selftune/hooks/skill-eval.ts +2 -1
  53. package/cli/selftune/hooks-shared/types.ts +1 -0
  54. package/cli/selftune/index.ts +58 -15
  55. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  56. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  57. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  58. package/cli/selftune/ingestors/pi-ingest.ts +727 -0
  59. package/cli/selftune/init.ts +38 -4
  60. package/cli/selftune/localdb/direct-write.ts +120 -1
  61. package/cli/selftune/localdb/materialize.ts +6 -7
  62. package/cli/selftune/localdb/queries/cron.ts +34 -0
  63. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  64. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  65. package/cli/selftune/localdb/queries/execution.ts +133 -0
  66. package/cli/selftune/localdb/queries/json.ts +18 -0
  67. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  68. package/cli/selftune/localdb/queries/raw.ts +95 -0
  69. package/cli/selftune/localdb/queries/staging.ts +270 -0
  70. package/cli/selftune/localdb/queries/trust.ts +392 -0
  71. package/cli/selftune/localdb/queries.ts +60 -2162
  72. package/cli/selftune/localdb/schema.ts +59 -0
  73. package/cli/selftune/monitoring/watch.ts +96 -29
  74. package/cli/selftune/normalization.ts +3 -0
  75. package/cli/selftune/observability.ts +12 -3
  76. package/cli/selftune/orchestrate/cli.ts +161 -0
  77. package/cli/selftune/orchestrate/execute.ts +295 -0
  78. package/cli/selftune/orchestrate/finalize.ts +157 -0
  79. package/cli/selftune/orchestrate/locks.ts +40 -0
  80. package/cli/selftune/orchestrate/plan.ts +131 -0
  81. package/cli/selftune/orchestrate/post-run.ts +59 -0
  82. package/cli/selftune/orchestrate/prepare.ts +334 -0
  83. package/cli/selftune/orchestrate/report.ts +182 -0
  84. package/cli/selftune/orchestrate/runtime.ts +120 -0
  85. package/cli/selftune/orchestrate/signals.ts +48 -0
  86. package/cli/selftune/orchestrate.ts +162 -1142
  87. package/cli/selftune/registry/client.ts +74 -0
  88. package/cli/selftune/registry/history.ts +54 -0
  89. package/cli/selftune/registry/index.ts +90 -0
  90. package/cli/selftune/registry/install.ts +141 -0
  91. package/cli/selftune/registry/list.ts +44 -0
  92. package/cli/selftune/registry/push.ts +171 -0
  93. package/cli/selftune/registry/rollback.ts +49 -0
  94. package/cli/selftune/registry/status.ts +62 -0
  95. package/cli/selftune/registry/sync.ts +125 -0
  96. package/cli/selftune/repair/skill-usage.ts +9 -3
  97. package/cli/selftune/routes/overview.ts +5 -2
  98. package/cli/selftune/routes/skill-report.ts +15 -2
  99. package/cli/selftune/schedule.ts +5 -5
  100. package/cli/selftune/status.ts +70 -2
  101. package/cli/selftune/sync.ts +127 -23
  102. package/cli/selftune/testing-readiness.ts +597 -0
  103. package/cli/selftune/types.ts +46 -5
  104. package/cli/selftune/uninstall.ts +2 -1
  105. package/cli/selftune/utils/canonical-log.ts +1 -9
  106. package/cli/selftune/utils/cli-error.ts +9 -0
  107. package/cli/selftune/utils/jsonl.ts +1 -30
  108. package/cli/selftune/utils/llm-call.ts +126 -6
  109. package/cli/selftune/utils/skill-discovery.ts +24 -0
  110. package/cli/selftune/workflows/proposals.ts +184 -0
  111. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  112. package/cli/selftune/workflows/workflows.ts +100 -26
  113. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  114. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  115. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  116. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  117. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  118. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  119. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  120. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +63 -5
  121. package/node_modules/@selftune/telemetry-contract/src/types.ts +97 -7
  122. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  123. package/package.json +25 -9
  124. package/packages/dashboard-core/AGENTS.md +18 -0
  125. package/packages/dashboard-core/README.md +30 -0
  126. package/packages/dashboard-core/index.ts +3 -0
  127. package/packages/dashboard-core/package.json +39 -0
  128. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  129. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  130. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  131. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  132. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  133. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  134. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  135. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  136. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  137. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  138. package/packages/dashboard-core/src/gates/index.ts +3 -0
  139. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  140. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  141. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  142. package/packages/dashboard-core/src/host/index.ts +3 -0
  143. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  144. package/packages/dashboard-core/src/models/index.ts +4 -0
  145. package/packages/dashboard-core/src/models/overview.ts +98 -0
  146. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  147. package/packages/dashboard-core/src/models/skills.ts +34 -0
  148. package/packages/dashboard-core/src/routes/index.ts +2 -0
  149. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  150. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  151. package/packages/dashboard-core/src/routes/types.ts +39 -0
  152. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  153. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  154. package/packages/dashboard-core/src/screens/index.ts +37 -0
  155. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  156. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  157. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  158. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  159. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  160. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  161. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  162. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  163. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  164. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  165. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  166. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  167. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  168. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  169. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  170. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  171. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  172. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  173. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  174. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  175. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  176. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  177. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  178. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  179. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  180. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  181. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  182. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  183. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  184. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  185. package/packages/telemetry-contract/package.json +1 -1
  186. package/packages/telemetry-contract/src/index.ts +1 -0
  187. package/packages/telemetry-contract/src/schemas.ts +63 -5
  188. package/packages/telemetry-contract/src/types.ts +97 -7
  189. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  190. package/packages/ui/AGENTS.md +16 -0
  191. package/packages/ui/README.md +1 -1
  192. package/packages/ui/package.json +1 -1
  193. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  194. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  195. package/packages/ui/src/components/EvidenceViewer.tsx +229 -464
  196. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  197. package/packages/ui/src/components/InfoTip.tsx +1 -2
  198. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  199. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  200. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  201. package/packages/ui/src/components/OverviewPanels.tsx +693 -0
  202. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  203. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  204. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  205. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  206. package/packages/ui/src/components/index.ts +56 -1
  207. package/packages/ui/src/components/section-cards.tsx +18 -35
  208. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  209. package/packages/ui/src/lib/constants.tsx +0 -1
  210. package/packages/ui/src/primitives/card.tsx +1 -1
  211. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  212. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  213. package/packages/ui/src/primitives/select.tsx +2 -2
  214. package/packages/ui/src/primitives/tabs.tsx +7 -6
  215. package/packages/ui/src/types.ts +182 -4
  216. package/skill/SKILL.md +130 -318
  217. package/skill/agents/diagnosis-analyst.md +3 -3
  218. package/skill/agents/evolution-reviewer.md +3 -3
  219. package/skill/agents/integration-guide.md +3 -3
  220. package/skill/agents/pattern-analyst.md +2 -2
  221. package/skill/references/cli-quick-reference.md +89 -0
  222. package/skill/references/creator-playbook.md +131 -0
  223. package/skill/references/examples.md +48 -0
  224. package/skill/references/troubleshooting.md +47 -0
  225. package/skill/references/version-history.md +1 -1
  226. package/skill/selftune.contribute.json +11 -0
  227. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  228. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  229. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  230. package/skill/workflows/CreateTestDeploy.md +170 -0
  231. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  232. package/skill/{Workflows → workflows}/Cron.md +1 -1
  233. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  234. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  235. package/skill/{Workflows → workflows}/Evals.md +67 -2
  236. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  237. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  238. package/skill/{Workflows → workflows}/Grade.md +1 -1
  239. package/skill/{Workflows → workflows}/Ingest.md +60 -2
  240. package/skill/{Workflows → workflows}/Initialize.md +16 -9
  241. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  242. package/skill/{Workflows → workflows}/PlatformHooks.md +19 -3
  243. package/skill/workflows/Registry.md +99 -0
  244. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  245. package/skill/workflows/SignalsDashboard.md +87 -0
  246. package/skill/{Workflows → workflows}/Sync.md +3 -1
  247. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  248. package/skill/{Workflows → workflows}/Watch.md +42 -2
  249. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  250. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  251. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  252. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  253. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  254. package/cli/selftune/utils/html.ts +0 -27
  255. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
  256. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  257. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  258. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  259. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  260. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  261. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  262. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  263. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  264. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  265. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  266. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  267. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  268. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  269. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  270. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Registry HTTP client. Never throws — returns typed results.
3
+ */
4
+
5
+ import { getSelftuneVersion } from "../utils/selftune-meta.js";
6
+
7
+ export interface RegistryResult<T = unknown> {
8
+ success: boolean;
9
+ data?: T;
10
+ error?: string;
11
+ status?: number;
12
+ }
13
+
14
+ function getConfig(): { apiUrl: string; apiKey: string } | null {
15
+ try {
16
+ const configPath = `${process.env.HOME}/.selftune/config.json`;
17
+ const raw = require("fs").readFileSync(configPath, "utf-8");
18
+ const config = JSON.parse(raw);
19
+ const apiUrl = config?.alpha?.cloud_api_url || "https://api.selftune.dev";
20
+ const apiKey = config?.alpha?.api_key;
21
+ if (!apiKey) return null;
22
+ return { apiUrl, apiKey };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export async function registryRequest<T>(
29
+ method: string,
30
+ path: string,
31
+ opts?: { body?: unknown; formData?: FormData },
32
+ ): Promise<RegistryResult<T>> {
33
+ const config = getConfig();
34
+ if (!config) {
35
+ return { success: false, error: "Not authenticated. Run 'selftune alpha upload' to set up." };
36
+ }
37
+
38
+ try {
39
+ const headers: Record<string, string> = {
40
+ Authorization: `Bearer ${config.apiKey}`,
41
+ "User-Agent": `selftune/${getSelftuneVersion()}`,
42
+ };
43
+
44
+ let fetchBody: BodyInit | undefined;
45
+ if (opts?.formData) {
46
+ fetchBody = opts.formData;
47
+ // Don't set Content-Type — fetch sets multipart boundary automatically
48
+ } else if (opts?.body) {
49
+ headers["Content-Type"] = "application/json";
50
+ fetchBody = JSON.stringify(opts.body);
51
+ }
52
+
53
+ const response = await fetch(`${config.apiUrl}/api/v1/registry${path}`, {
54
+ method,
55
+ headers,
56
+ body: fetchBody,
57
+ signal: AbortSignal.timeout(60_000),
58
+ });
59
+
60
+ const text = await response.text();
61
+ if (!response.ok) {
62
+ return {
63
+ success: false,
64
+ error: `HTTP ${response.status}: ${text.slice(0, 300)}`,
65
+ status: response.status,
66
+ };
67
+ }
68
+
69
+ const data = text ? JSON.parse(text) : {};
70
+ return { success: true, data: data as T, status: response.status };
71
+ } catch (err) {
72
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
73
+ }
74
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * selftune registry history — Show version timeline for a skill.
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
+
11
+ if (!name) {
12
+ console.error(JSON.stringify({ error: "Usage: selftune registry history <name>" }));
13
+ process.exit(1);
14
+ }
15
+
16
+ const listResult = await registryRequest<{ entries: Array<{ id: string }> }>(
17
+ "GET",
18
+ `?name=${encodeURIComponent(name)}`,
19
+ );
20
+ if (!listResult.success || !listResult.data?.entries?.length) {
21
+ console.error(JSON.stringify({ error: `Skill '${name}' not found in registry` }));
22
+ process.exit(1);
23
+ }
24
+
25
+ const entryId = listResult.data.entries[0].id;
26
+ const result = await registryRequest<{
27
+ versions: Array<{
28
+ version: string;
29
+ is_current: boolean;
30
+ rolled_back: boolean;
31
+ aggregate_pass_rate: number | null;
32
+ aggregate_sessions: number;
33
+ change_summary: string | null;
34
+ pushed_at: string;
35
+ }>;
36
+ }>("GET", `/${entryId}/versions`);
37
+
38
+ if (!result.success) {
39
+ console.error(JSON.stringify({ error: result.error }));
40
+ process.exit(1);
41
+ }
42
+
43
+ const versions = result.data?.versions || [];
44
+ const timeline = versions.map((v) => ({
45
+ version: v.version,
46
+ status: v.is_current ? "current" : v.rolled_back ? "rolled_back" : "previous",
47
+ pass_rate: v.aggregate_pass_rate,
48
+ sessions: v.aggregate_sessions,
49
+ summary: v.change_summary,
50
+ pushed_at: v.pushed_at,
51
+ }));
52
+
53
+ console.log(JSON.stringify({ name, versions: timeline }));
54
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * selftune registry — Team skill distribution.
3
+ *
4
+ * Subcommands:
5
+ * push Push current skill folder as a new version
6
+ * install Download and install a skill from the registry
7
+ * sync Check for updates and pull latest versions
8
+ * status Show installed entries and version drift
9
+ * rollback Rollback a skill to a previous version
10
+ * history Show version timeline for a skill
11
+ * list Show all published entries in the org
12
+ */
13
+
14
+ import { CLIError } from "../utils/cli-error.js";
15
+
16
+ const sub = process.argv[2];
17
+
18
+ export async function cliMain() {
19
+ if (!sub || sub === "--help" || sub === "-h") {
20
+ console.log(`selftune registry — Team skill distribution
21
+
22
+ Usage:
23
+ selftune registry <subcommand> [options]
24
+
25
+ Subcommands:
26
+ push [name] Push current skill folder as a new version
27
+ install <name> Download and install a skill from the registry
28
+ sync Check for updates and pull latest versions
29
+ status Show installed entries and version drift
30
+ rollback <name> Rollback to a previous version
31
+ history <name> Show version timeline
32
+ list Show all published entries
33
+
34
+ Options:
35
+ --version=<semver> Set version explicitly (push)
36
+ --summary=<text> Change summary (push)
37
+ --global Install to ~/.claude/skills/ (install)
38
+ --to=<version> Target version (rollback)
39
+ --reason=<text> Rollback reason (rollback)
40
+ `);
41
+ return;
42
+ }
43
+
44
+ // Strip 'registry' from argv so subcommands see the right args
45
+ process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
46
+
47
+ switch (sub) {
48
+ case "push": {
49
+ const { cliMain } = await import("./push.js");
50
+ await cliMain();
51
+ break;
52
+ }
53
+ case "install": {
54
+ const { cliMain } = await import("./install.js");
55
+ await cliMain();
56
+ break;
57
+ }
58
+ case "sync": {
59
+ const { cliMain } = await import("./sync.js");
60
+ await cliMain();
61
+ break;
62
+ }
63
+ case "status": {
64
+ const { cliMain } = await import("./status.js");
65
+ await cliMain();
66
+ break;
67
+ }
68
+ case "rollback": {
69
+ const { cliMain } = await import("./rollback.js");
70
+ await cliMain();
71
+ break;
72
+ }
73
+ case "history": {
74
+ const { cliMain } = await import("./history.js");
75
+ await cliMain();
76
+ break;
77
+ }
78
+ case "list": {
79
+ const { cliMain } = await import("./list.js");
80
+ await cliMain();
81
+ break;
82
+ }
83
+ default:
84
+ throw new CLIError(
85
+ `Unknown registry subcommand: ${sub}`,
86
+ "UNKNOWN_COMMAND",
87
+ "selftune registry --help",
88
+ );
89
+ }
90
+ }
@@ -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
+ }