vskill 1.0.15 → 1.0.18

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 (220) hide show
  1. package/README.md +84 -9
  2. package/agents.json +3 -1
  3. package/dist/agents/agents-registry.d.ts +69 -3
  4. package/dist/agents/agents-registry.js +203 -0
  5. package/dist/agents/agents-registry.js.map +1 -1
  6. package/dist/api/client.d.ts +85 -0
  7. package/dist/api/client.js +193 -24
  8. package/dist/api/client.js.map +1 -1
  9. package/dist/commands/add-lockfile.d.ts +6 -0
  10. package/dist/commands/add-lockfile.js +10 -0
  11. package/dist/commands/add-lockfile.js.map +1 -1
  12. package/dist/commands/add.d.ts +7 -0
  13. package/dist/commands/add.js +110 -2
  14. package/dist/commands/add.js.map +1 -1
  15. package/dist/commands/auth.d.ts +23 -0
  16. package/dist/commands/auth.js +105 -11
  17. package/dist/commands/auth.js.map +1 -1
  18. package/dist/commands/eval/serve.d.ts +2 -0
  19. package/dist/commands/eval/serve.js +126 -4
  20. package/dist/commands/eval/serve.js.map +1 -1
  21. package/dist/commands/orgs.d.ts +21 -0
  22. package/dist/commands/orgs.js +164 -0
  23. package/dist/commands/orgs.js.map +1 -0
  24. package/dist/commands/skill.js +14 -1
  25. package/dist/commands/skill.js.map +1 -1
  26. package/dist/commands/whoami.d.ts +29 -0
  27. package/dist/commands/whoami.js +119 -0
  28. package/dist/commands/whoami.js.map +1 -0
  29. package/dist/discovery/github-tree.d.ts +23 -3
  30. package/dist/discovery/github-tree.js +172 -24
  31. package/dist/discovery/github-tree.js.map +1 -1
  32. package/dist/eval/anthropic-catalog.js +32 -2
  33. package/dist/eval/anthropic-catalog.js.map +1 -1
  34. package/dist/eval/batch-judge.js +1 -0
  35. package/dist/eval/batch-judge.js.map +1 -1
  36. package/dist/eval/llm.d.ts +1 -1
  37. package/dist/eval/llm.js +104 -2
  38. package/dist/eval/llm.js.map +1 -1
  39. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.d.ts +2 -0
  40. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js +20 -0
  41. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js.map +1 -0
  42. package/dist/eval-server/active-tenant-routes.d.ts +15 -0
  43. package/dist/eval-server/active-tenant-routes.js +101 -0
  44. package/dist/eval-server/active-tenant-routes.js.map +1 -0
  45. package/dist/eval-server/api-routes.js +206 -6
  46. package/dist/eval-server/api-routes.js.map +1 -1
  47. package/dist/eval-server/desktop-open-routes.d.ts +8 -0
  48. package/dist/eval-server/desktop-open-routes.js +64 -0
  49. package/dist/eval-server/desktop-open-routes.js.map +1 -0
  50. package/dist/eval-server/eval-server.js +90 -6
  51. package/dist/eval-server/eval-server.js.map +1 -1
  52. package/dist/eval-server/export-skill-routes.d.ts +9 -0
  53. package/dist/eval-server/export-skill-routes.js +81 -0
  54. package/dist/eval-server/export-skill-routes.js.map +1 -0
  55. package/dist/eval-server/git-routes.d.ts +1 -0
  56. package/dist/eval-server/git-routes.js +101 -4
  57. package/dist/eval-server/git-routes.js.map +1 -1
  58. package/dist/eval-server/install-engine-routes.d.ts +3 -16
  59. package/dist/eval-server/install-engine-routes.js +9 -124
  60. package/dist/eval-server/install-engine-routes.js.map +1 -1
  61. package/dist/eval-server/install-jobs.d.ts +41 -0
  62. package/dist/eval-server/install-jobs.js +161 -0
  63. package/dist/eval-server/install-jobs.js.map +1 -0
  64. package/dist/eval-server/install-skill-routes.d.ts +74 -11
  65. package/dist/eval-server/install-skill-routes.js +506 -79
  66. package/dist/eval-server/install-skill-routes.js.map +1 -1
  67. package/dist/eval-server/install-state-routes.d.ts +25 -0
  68. package/dist/eval-server/install-state-routes.js +125 -0
  69. package/dist/eval-server/install-state-routes.js.map +1 -0
  70. package/dist/eval-server/oauth-github-routes.d.ts +2 -0
  71. package/dist/eval-server/oauth-github-routes.js +505 -0
  72. package/dist/eval-server/oauth-github-routes.js.map +1 -0
  73. package/dist/eval-server/platform-proxy.d.ts +17 -1
  74. package/dist/eval-server/platform-proxy.js +125 -13
  75. package/dist/eval-server/platform-proxy.js.map +1 -1
  76. package/dist/eval-server/plugin-cli-routes.js +9 -9
  77. package/dist/eval-server/plugin-cli-routes.js.map +1 -1
  78. package/dist/eval-server/remove-skill-routes.d.ts +18 -0
  79. package/dist/eval-server/remove-skill-routes.js +145 -0
  80. package/dist/eval-server/remove-skill-routes.js.map +1 -0
  81. package/dist/eval-server/router.d.ts +17 -3
  82. package/dist/eval-server/router.js +166 -9
  83. package/dist/eval-server/router.js.map +1 -1
  84. package/dist/eval-server/settings-store.js +1 -1
  85. package/dist/eval-server/settings-store.js.map +1 -1
  86. package/dist/eval-server/supported-agents-routes.d.ts +6 -0
  87. package/dist/eval-server/supported-agents-routes.js +41 -0
  88. package/dist/eval-server/supported-agents-routes.js.map +1 -0
  89. package/dist/eval-server/utils/spawn-env.d.ts +1 -0
  90. package/dist/eval-server/utils/spawn-env.js +47 -0
  91. package/dist/eval-server/utils/spawn-env.js.map +1 -0
  92. package/dist/eval-ui/assets/AdvancedTab-D8zbE5fH.js +1 -0
  93. package/dist/eval-ui/assets/{CreateSkillPage-BmbvQEzE.js → CreateSkillPage-DOBhKdgr.js} +5 -5
  94. package/dist/eval-ui/assets/FindSkillsPalette-CyMmNPr-.js +2 -0
  95. package/dist/eval-ui/assets/GeneralTab-DYR9NWC4.js +1 -0
  96. package/dist/eval-ui/assets/PrivacyTab-CXIqQokl.js +1 -0
  97. package/dist/eval-ui/assets/SearchPaletteCore-Dn5gQJS_.js +14 -0
  98. package/dist/eval-ui/assets/SkillDetailPanel-DTrRnyyJ.js +1 -0
  99. package/dist/eval-ui/assets/UpdateDropdown-Cvr2fe0z.js +1 -0
  100. package/dist/eval-ui/assets/UpdatesTab-DwJIUDPX.js +1 -0
  101. package/dist/eval-ui/assets/core-DZAvsxlC.js +1 -0
  102. package/dist/eval-ui/assets/event-CDYWU2X3.js +1 -0
  103. package/dist/eval-ui/assets/globals-BRZwPAPF.js +49 -0
  104. package/dist/eval-ui/assets/globals-C3oEdsJh.css +1 -0
  105. package/dist/eval-ui/assets/index-D7M0Jdss.js +1 -0
  106. package/dist/eval-ui/assets/lifecycle-DSleOV-l.js +1 -0
  107. package/dist/eval-ui/assets/lifecycle-d1Sm9Hts.css +1 -0
  108. package/dist/eval-ui/assets/main-D2shn1dH.js +87 -0
  109. package/dist/eval-ui/assets/preferences-BHZXB5dL.css +1 -0
  110. package/dist/eval-ui/assets/preferences-BKv6X7fK.js +2 -0
  111. package/dist/eval-ui/assets/useDesktopBridge-DxVWbYqK.js +2 -0
  112. package/dist/eval-ui/index.html +4 -2
  113. package/dist/eval-ui/lifecycle.html +33 -0
  114. package/dist/eval-ui/preferences.html +34 -0
  115. package/dist/index.js +47 -1
  116. package/dist/index.js.map +1 -1
  117. package/dist/installer/bundle-files.d.ts +4 -0
  118. package/dist/installer/bundle-files.js +97 -0
  119. package/dist/installer/bundle-files.js.map +1 -0
  120. package/dist/installer/canonical.d.ts +31 -6
  121. package/dist/installer/canonical.js +50 -23
  122. package/dist/installer/canonical.js.map +1 -1
  123. package/dist/installer/clipboard-export.d.ts +19 -0
  124. package/dist/installer/clipboard-export.js +88 -0
  125. package/dist/installer/clipboard-export.js.map +1 -0
  126. package/dist/installer/frontmatter.js +1 -1
  127. package/dist/installer/frontmatter.js.map +1 -1
  128. package/dist/installer/multi-install.d.ts +43 -0
  129. package/dist/installer/multi-install.js +237 -0
  130. package/dist/installer/multi-install.js.map +1 -0
  131. package/dist/installer/transformers/aider.d.ts +2 -0
  132. package/dist/installer/transformers/aider.js +32 -0
  133. package/dist/installer/transformers/aider.js.map +1 -0
  134. package/dist/installer/transformers/continue-dev.d.ts +2 -0
  135. package/dist/installer/transformers/continue-dev.js +6 -0
  136. package/dist/installer/transformers/continue-dev.js.map +1 -0
  137. package/dist/installer/transformers/cursor.d.ts +2 -0
  138. package/dist/installer/transformers/cursor.js +24 -0
  139. package/dist/installer/transformers/cursor.js.map +1 -0
  140. package/dist/installer/transformers/github-copilot.d.ts +2 -0
  141. package/dist/installer/transformers/github-copilot.js +17 -0
  142. package/dist/installer/transformers/github-copilot.js.map +1 -0
  143. package/dist/installer/transformers/index.d.ts +78 -0
  144. package/dist/installer/transformers/index.js +13 -0
  145. package/dist/installer/transformers/index.js.map +1 -0
  146. package/dist/installer/transformers/junie.d.ts +2 -0
  147. package/dist/installer/transformers/junie.js +6 -0
  148. package/dist/installer/transformers/junie.js.map +1 -0
  149. package/dist/installer/transformers/kiro.d.ts +2 -0
  150. package/dist/installer/transformers/kiro.js +6 -0
  151. package/dist/installer/transformers/kiro.js.map +1 -0
  152. package/dist/installer/transformers/trae.d.ts +2 -0
  153. package/dist/installer/transformers/trae.js +6 -0
  154. package/dist/installer/transformers/trae.js.map +1 -0
  155. package/dist/installer/transformers/windsurf.d.ts +2 -0
  156. package/dist/installer/transformers/windsurf.js +12 -0
  157. package/dist/installer/transformers/windsurf.js.map +1 -0
  158. package/dist/installer/yaml-safe-mutate.d.ts +19 -0
  159. package/dist/installer/yaml-safe-mutate.js +184 -0
  160. package/dist/installer/yaml-safe-mutate.js.map +1 -0
  161. package/dist/lib/active-tenant.d.ts +36 -0
  162. package/dist/lib/active-tenant.js +120 -0
  163. package/dist/lib/active-tenant.js.map +1 -0
  164. package/dist/lib/github-fetch.d.ts +1 -0
  165. package/dist/lib/github-fetch.js +11 -1
  166. package/dist/lib/github-fetch.js.map +1 -1
  167. package/dist/lib/keychain.d.ts +15 -2
  168. package/dist/lib/keychain.js +156 -8
  169. package/dist/lib/keychain.js.map +1 -1
  170. package/dist/lib/migration/keychain-migration.d.ts +35 -0
  171. package/dist/lib/migration/keychain-migration.js +189 -0
  172. package/dist/lib/migration/keychain-migration.js.map +1 -0
  173. package/dist/lib/tenant-resolver.d.ts +38 -0
  174. package/dist/lib/tenant-resolver.js +79 -0
  175. package/dist/lib/tenant-resolver.js.map +1 -0
  176. package/dist/lockfile/types.d.ts +8 -0
  177. package/dist/sidecar/eval-ui-manifest.json +1 -0
  178. package/dist/sidecar/sea-config.json +57 -0
  179. package/dist/sidecar/sea-prep.blob +0 -0
  180. package/dist/sidecar/server.cjs +141627 -0
  181. package/dist/sidecar/vskill-version.txt +1 -0
  182. package/dist/studio/lib/ops-log.js +140 -57
  183. package/dist/studio/lib/ops-log.js.map +1 -1
  184. package/dist/studio/lib/provenance.js +3 -2
  185. package/dist/studio/lib/provenance.js.map +1 -1
  186. package/dist/studio/lib/query.d.ts +1 -0
  187. package/dist/studio/lib/query.js +7 -0
  188. package/dist/studio/lib/query.js.map +1 -0
  189. package/dist/studio/lib/scope-transfer.d.ts +16 -0
  190. package/dist/studio/lib/scope-transfer.js +52 -25
  191. package/dist/studio/lib/scope-transfer.js.map +1 -1
  192. package/dist/studio/routes/index.js +13 -1
  193. package/dist/studio/routes/index.js.map +1 -1
  194. package/dist/studio/routes/ops.js +31 -9
  195. package/dist/studio/routes/ops.js.map +1 -1
  196. package/dist/studio/routes/promote.js +16 -11
  197. package/dist/studio/routes/promote.js.map +1 -1
  198. package/dist/studio/routes/revert.js +14 -18
  199. package/dist/studio/routes/revert.js.map +1 -1
  200. package/dist/studio/routes/test-install.js +14 -11
  201. package/dist/studio/routes/test-install.js.map +1 -1
  202. package/dist/studio-runtime/lockfile.d.ts +51 -0
  203. package/dist/studio-runtime/lockfile.js +216 -0
  204. package/dist/studio-runtime/lockfile.js.map +1 -0
  205. package/dist/updater/source-fetcher.js +2 -2
  206. package/dist/updater/source-fetcher.js.map +1 -1
  207. package/dist/utils/skill-builder-detection.d.ts +14 -1
  208. package/dist/utils/skill-builder-detection.js +20 -8
  209. package/dist/utils/skill-builder-detection.js.map +1 -1
  210. package/dist/utils/skill-creator-detection.d.ts +10 -2
  211. package/dist/utils/skill-creator-detection.js +12 -43
  212. package/dist/utils/skill-creator-detection.js.map +1 -1
  213. package/package.json +17 -1
  214. package/dist/eval-ui/assets/FindSkillsPalette-D0Zjhm31.js +0 -2
  215. package/dist/eval-ui/assets/SearchPaletteCore-EhcN1xEa.js +0 -14
  216. package/dist/eval-ui/assets/SkillDetailPanel-B5J60ffv.js +0 -1
  217. package/dist/eval-ui/assets/UpdateDropdown-Celf0_Cr.js +0 -1
  218. package/dist/eval-ui/assets/index-BV7k6fdk.js +0 -124
  219. package/dist/eval-ui/assets/index-CKLqBL52.css +0 -1
  220. package/dist/eval-ui/assets/skill-studio-logo-CRyKgIrg.png +0 -0
@@ -12,106 +12,447 @@
12
12
  // 3. Scope allowlist — "project" | "user" | "global" only.
13
13
  // 4. Hard-coded command name "vskill" — no path injection.
14
14
  //
15
+ // 0845 T-017: extended to accept agentIds[] for in-process multi-install
16
+ // dispatch. Backward-compatible: legacy `agent: string` or no agent field
17
+ // still goes through the CLI-spawn single-agent path (AC-US2-09).
18
+ //
19
+ // 0845 closure fix: server-side fallback resolves `parsedSkill` from the
20
+ // local skill registry (project / personal / plugin cache) when callers
21
+ // omit it on the multi-agent path. Keeps the API surface minimal — the
22
+ // frontend just sends `{ skill, agentIds, scope }`.
23
+ //
15
24
  // Endpoints:
16
- // POST /api/studio/install-skill { skill, scope } → 202 + { jobId }
25
+ // POST /api/studio/install-skill { skill, scope, agentIds?, parsedSkill? } → 202 + { jobId }
17
26
  // GET /api/studio/install-skill/:id/stream SSE progress | done
18
- import { spawn } from "node:child_process";
19
- import { randomUUID } from "node:crypto";
27
+ import * as os from "node:os";
28
+ import { promises as fsPromises } from "node:fs";
29
+ import { join as pathJoin } from "node:path";
30
+ import { createHash, randomUUID } from "node:crypto";
20
31
  import { sendJson, readBody } from "./router.js";
32
+ import { isLocalhost, mountSpawnStreamRoute, startSpawnJob, } from "./install-jobs.js";
21
33
  import { initSSE, sendSSE, sendSSEDone } from "./sse-helpers.js";
34
+ import { installSkillToMultipleAgents, } from "../installer/multi-install.js";
35
+ import { getAgent } from "../agents/agents-registry.js";
36
+ import { locateSkill } from "../clone/skill-locator.js";
37
+ import { extractDescription } from "../installer/frontmatter.js";
38
+ import { collectSkillBundleFiles, sanitizeSkillBundleFiles, } from "../installer/bundle-files.js";
39
+ import { getDefaultKeychain } from "../lib/keychain.js";
40
+ import { ensureLockfile, writeLockfile } from "../lockfile/lockfile.js";
22
41
  const SAFE_NAME = /^[a-zA-Z0-9._@/\-]+$/;
42
+ // Agent IDs are slug-shaped: lowercase alphanumeric + hyphens. Stricter
43
+ // than SAFE_NAME to reject `.`, `/`, `@`, and uppercase at the boundary
44
+ // (FR-007 — every entry in agentIds[] must pass this regex).
45
+ const SAFE_AGENT_ID = /^[a-z0-9][a-z0-9-]*$/;
23
46
  const VALID_SCOPES = new Set(["project", "user", "global"]);
24
47
  const INSTALL_TIMEOUT_MS = 180_000; // 3 min — installs can be slow on cold caches.
25
- const STDOUT_BUFFER_CAP = 1024 * 1024;
26
48
  const JOBS = new Map();
27
- function isLocalhost(req) {
28
- const addr = req.socket.remoteAddress ?? "";
29
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
49
+ const MULTI_JOBS = new Map();
50
+ function buildArgs(skill, scope) {
51
+ // No shell every entry is a discrete argv element. Scope is from a
52
+ // closed set; skill is regex-validated. Both flags map to vskill CLI.
53
+ if (scope === "global" || scope === "user")
54
+ return ["install", skill, "--global"];
55
+ return ["install", skill, "--scope", scope];
56
+ }
57
+ function normalizeInstallScope(scope) {
58
+ return scope === "project" ? "project" : "user";
30
59
  }
31
- function trimRing(buf, cap) {
32
- return buf.length <= cap ? buf : buf.slice(buf.length - cap);
60
+ function userLockDir() {
61
+ return pathJoin(os.homedir(), ".agents");
33
62
  }
34
- function emitToJob(job, event, data) {
63
+ function emitMultiJob(job, event, data) {
35
64
  job.pastEvents.push({ event, data });
36
65
  for (const sub of job.subscribers) {
37
66
  try {
38
67
  sub(event, data);
39
68
  }
40
- catch { /* subscriber failed — ignore */ }
69
+ catch { /* subscriber dead — ignore */ }
41
70
  }
42
71
  }
43
- function buildArgs(skill, scope) {
44
- // No shell every entry is a discrete argv element. Scope is from a
45
- // closed set; skill is regex-validated. Both flags map to vskill CLI.
46
- if (scope === "global")
47
- return ["install", skill, "--global"];
48
- return ["install", skill, "--scope", scope];
72
+ function isValidParsedSkill(s) {
73
+ if (!s || typeof s !== "object")
74
+ return false;
75
+ const v = s;
76
+ const requiredFieldsOk = typeof v.name === "string" &&
77
+ typeof v.description === "string" &&
78
+ typeof v.body === "string";
79
+ if (!requiredFieldsOk)
80
+ return false;
81
+ try {
82
+ sanitizeSkillBundleFiles(v.files);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ const SERVER_FALLBACK_FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
90
+ const SERVER_FALLBACK_NAME_RE = /^name:\s*(.+?)\s*$/m;
91
+ const SERVER_FALLBACK_VERSION_RE = /^version:\s*(.+?)\s*$/m;
92
+ const DEFAULT_PLATFORM_URL = "https://verified-skill.com";
93
+ function unquoteYaml(value) {
94
+ const trimmed = value.trim();
95
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
96
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
97
+ return trimmed.slice(1, -1);
98
+ }
99
+ return trimmed;
100
+ }
101
+ function parseRawSkillMd(raw, fallbackName, fallbackDescription) {
102
+ const normalized = raw.replace(/^/, "").replace(/\r\n/g, "\n");
103
+ const fmMatch = normalized.match(SERVER_FALLBACK_FRONTMATTER_RE);
104
+ let originalFrontmatter = "";
105
+ let body = normalized;
106
+ let nameFromFm;
107
+ let version;
108
+ let descriptionFromFm;
109
+ if (fmMatch) {
110
+ originalFrontmatter = fmMatch[1];
111
+ body = fmMatch[2];
112
+ const nameMatch = originalFrontmatter.match(SERVER_FALLBACK_NAME_RE);
113
+ if (nameMatch)
114
+ nameFromFm = unquoteYaml(nameMatch[1]);
115
+ const versionMatch = originalFrontmatter.match(SERVER_FALLBACK_VERSION_RE);
116
+ if (versionMatch)
117
+ version = unquoteYaml(versionMatch[1]);
118
+ const descMatch = originalFrontmatter.match(/^description:\s*(.+?)\s*$/m);
119
+ if (descMatch)
120
+ descriptionFromFm = unquoteYaml(descMatch[1]);
121
+ }
122
+ const name = nameFromFm || fallbackName;
123
+ return {
124
+ name,
125
+ description: descriptionFromFm || fallbackDescription || extractDescription(body, name),
126
+ body,
127
+ originalFrontmatter,
128
+ version,
129
+ };
130
+ }
131
+ function stripIdentifierVersion(identifier) {
132
+ const slash = identifier.lastIndexOf("/");
133
+ const at = identifier.lastIndexOf("@");
134
+ return at > slash ? identifier.slice(0, at) : identifier;
135
+ }
136
+ function platformApiPath(identifier) {
137
+ const base = stripIdentifierVersion(identifier);
138
+ const parts = base.split("/").filter(Boolean);
139
+ if (parts.length !== 3)
140
+ return null;
141
+ return `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}`;
142
+ }
143
+ function platformBaseUrl(opts) {
144
+ const raw = opts?.platformBaseUrl || process.env.VSKILL_PLATFORM_URL || DEFAULT_PLATFORM_URL;
145
+ return raw.replace(/\/$/, "");
146
+ }
147
+ function normalizePlatformSkill(body) {
148
+ if (!body || typeof body !== "object")
149
+ return null;
150
+ const record = body;
151
+ const candidate = record.skill && typeof record.skill === "object"
152
+ ? record.skill
153
+ : record;
154
+ return {
155
+ name: typeof candidate.name === "string" ? candidate.name : undefined,
156
+ displayName: typeof candidate.displayName === "string" ? candidate.displayName : undefined,
157
+ description: typeof candidate.description === "string" ? candidate.description : undefined,
158
+ repoUrl: typeof candidate.repoUrl === "string" ? candidate.repoUrl : undefined,
159
+ skillPath: typeof candidate.skillPath === "string" ? candidate.skillPath : undefined,
160
+ ownerSlug: typeof candidate.ownerSlug === "string" ? candidate.ownerSlug : undefined,
161
+ repoSlug: typeof candidate.repoSlug === "string" ? candidate.repoSlug : undefined,
162
+ skillSlug: typeof candidate.skillSlug === "string" ? candidate.skillSlug : undefined,
163
+ };
164
+ }
165
+ function normalizePlatformVersions(body) {
166
+ if (!body || typeof body !== "object")
167
+ return [];
168
+ const versions = body.versions;
169
+ if (!Array.isArray(versions))
170
+ return [];
171
+ return versions
172
+ .filter((v) => Boolean(v) && typeof v === "object")
173
+ .map((v) => ({
174
+ version: typeof v.version === "string" ? v.version : undefined,
175
+ gitSha: typeof v.gitSha === "string" ? v.gitSha : null,
176
+ }));
177
+ }
178
+ function parseGitHubRepo(repoUrl) {
179
+ if (!repoUrl)
180
+ return null;
181
+ try {
182
+ const url = new URL(repoUrl);
183
+ if (url.hostname !== "github.com")
184
+ return null;
185
+ const [owner, repoRaw] = url.pathname.replace(/^\/+/, "").split("/");
186
+ const repo = repoRaw?.replace(/\.git$/, "");
187
+ if (!owner || !repo)
188
+ return null;
189
+ return { owner, repo };
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
195
+ function isSafeSkillPath(skillPath) {
196
+ if (!skillPath)
197
+ return false;
198
+ if (skillPath.startsWith("/") || skillPath.includes("\\"))
199
+ return false;
200
+ const parts = skillPath.split("/");
201
+ if (parts.some((p) => !p || p === "." || p === ".."))
202
+ return false;
203
+ return parts[parts.length - 1] === "SKILL.md";
204
+ }
205
+ function readGitHubToken(opts) {
206
+ if (opts?.githubTokenProvider)
207
+ return opts.githubTokenProvider();
208
+ try {
209
+ return getDefaultKeychain().getGitHubToken();
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ async function fetchSkillMdFromGitHub(opts) {
216
+ const repo = parseGitHubRepo(opts.repoUrl);
217
+ if (!repo || !isSafeSkillPath(opts.skillPath))
218
+ return null;
219
+ const pathPart = opts.skillPath.split("/").map(encodeURIComponent).join("/");
220
+ const refs = Array.from(new Set(opts.refs.filter(Boolean)));
221
+ if (!refs.includes(""))
222
+ refs.push("");
223
+ for (const ref of refs) {
224
+ const url = new URL(`https://api.github.com/repos/${repo.owner}/${repo.repo}/contents/${pathPart}`);
225
+ if (ref)
226
+ url.searchParams.set("ref", ref);
227
+ const headers = {
228
+ Accept: "application/vnd.github.raw",
229
+ "User-Agent": "vskill-skill-studio",
230
+ };
231
+ if (opts.token)
232
+ headers.Authorization = `Bearer ${opts.token}`;
233
+ try {
234
+ const res = await opts.fetchImpl(url.toString(), { headers });
235
+ if (res.ok)
236
+ return await res.text();
237
+ }
238
+ catch {
239
+ // Try the next ref.
240
+ }
241
+ }
242
+ return null;
243
+ }
244
+ async function resolveParsedSkillFromPlatform(identifier, opts) {
245
+ const apiPath = platformApiPath(identifier);
246
+ if (!apiPath)
247
+ return null;
248
+ const fetchImpl = opts?.fetchImpl ?? fetch;
249
+ const baseUrl = platformBaseUrl(opts);
250
+ let skill = null;
251
+ try {
252
+ const res = await fetchImpl(`${baseUrl}${apiPath}`, { headers: { Accept: "application/json" } });
253
+ if (!res.ok)
254
+ return null;
255
+ skill = normalizePlatformSkill(await res.json());
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ if (!skill?.repoUrl || !isSafeSkillPath(skill.skillPath))
261
+ return null;
262
+ let versions = [];
263
+ try {
264
+ const res = await fetchImpl(`${baseUrl}${apiPath}/versions`, { headers: { Accept: "application/json" } });
265
+ if (res.ok)
266
+ versions = normalizePlatformVersions(await res.json());
267
+ }
268
+ catch {
269
+ versions = [];
270
+ }
271
+ const requestedVersion = (() => {
272
+ const slash = identifier.lastIndexOf("/");
273
+ const at = identifier.lastIndexOf("@");
274
+ return at > slash ? identifier.slice(at + 1) : null;
275
+ })();
276
+ const selectedVersion = requestedVersion
277
+ ? versions.find((v) => v.version === requestedVersion)
278
+ : versions[0];
279
+ const refs = [
280
+ selectedVersion?.gitSha ?? "",
281
+ "main",
282
+ "master",
283
+ ];
284
+ const raw = await fetchSkillMdFromGitHub({
285
+ repoUrl: skill.repoUrl,
286
+ skillPath: skill.skillPath,
287
+ refs,
288
+ fetchImpl,
289
+ token: readGitHubToken(opts),
290
+ });
291
+ if (!raw)
292
+ return null;
293
+ return parseRawSkillMd(raw, skill.skillSlug || skill.displayName || stripIdentifierVersion(identifier).split("/").pop() || "skill", skill.description);
294
+ }
295
+ /**
296
+ * Server-side fallback: when the frontend posts a multi-agent install
297
+ * without `parsedSkill`, locate the skill on disk (project / personal /
298
+ * plugin cache via {@link locateSkill}), read its SKILL.md, and build a
299
+ * minimal {@link ParsedSkill}. Keeps the POST payload trivial — the
300
+ * frontend doesn't need to fetch, parse, and re-marshal SKILL.md itself.
301
+ *
302
+ * Returns `null` when no on-disk skill matches the identifier (the route
303
+ * surfaces this as a 404 so the caller can retry with an explicit
304
+ * `parsedSkill`).
305
+ */
306
+ export async function resolveParsedSkillFromIdentifier(identifier, opts) {
307
+ const cwd = opts?.cwd ?? process.cwd();
308
+ const home = opts?.home ?? os.homedir();
309
+ const matches = await locateSkill(identifier, { cwd, home });
310
+ if (matches.length > 0) {
311
+ const source = matches[0];
312
+ const skillMdPath = pathJoin(source.skillDir, "SKILL.md");
313
+ try {
314
+ const raw = await fsPromises.readFile(skillMdPath, "utf-8");
315
+ const parsed = parseRawSkillMd(raw, source.skillName);
316
+ const files = await collectSkillBundleFiles(source.skillDir);
317
+ return { ...parsed, files };
318
+ }
319
+ catch {
320
+ return null;
321
+ }
322
+ }
323
+ return resolveParsedSkillFromPlatform(identifier, opts);
324
+ }
325
+ function validateAgentIds(ids) {
326
+ if (!Array.isArray(ids))
327
+ return { ok: false, error: "agentIds must be an array" };
328
+ if (ids.length === 0)
329
+ return { ok: false, error: "agentIds[] cannot be empty" };
330
+ const validated = [];
331
+ for (const raw of ids) {
332
+ if (typeof raw !== "string")
333
+ return { ok: false, error: "agentIds[] entries must be strings" };
334
+ const id = raw.trim();
335
+ if (!SAFE_AGENT_ID.test(id))
336
+ return { ok: false, error: `invalid agentId: ${raw}` };
337
+ if (!getAgent(id))
338
+ return { ok: false, error: `unknown agentId: ${id}` };
339
+ validated.push(id);
340
+ }
341
+ return { ok: true, ids: validated };
342
+ }
343
+ function reconstructedSkillMd(skill) {
344
+ const frontmatter = skill.originalFrontmatter.trim()
345
+ ? skill.originalFrontmatter.trim()
346
+ : [
347
+ `name: ${skill.name}`,
348
+ `description: ${JSON.stringify(skill.description)}`,
349
+ skill.version ? `version: ${skill.version}` : null,
350
+ ].filter(Boolean).join("\n");
351
+ return `---\n${frontmatter}\n---\n\n${skill.body.replace(/^\n+/, "")}`;
352
+ }
353
+ function computeSha(content) {
354
+ return createHash("sha256").update(content).digest("hex");
355
+ }
356
+ function sourceFromIdentifier(identifier) {
357
+ const parts = stripIdentifierVersion(identifier).split("/").filter(Boolean);
358
+ if (parts.length >= 2) {
359
+ const [owner, repo, slug] = parts;
360
+ return {
361
+ source: slug
362
+ ? `marketplace:${owner}/${repo}#${slug}`
363
+ : `github:${owner}/${repo}`,
364
+ sourceRepoUrl: `https://github.com/${owner}/${repo}`,
365
+ };
366
+ }
367
+ return {
368
+ source: `registry:${stripIdentifierVersion(identifier)}`,
369
+ };
370
+ }
371
+ function writeMultiInstallLockfile(opts) {
372
+ const installedAgentIds = opts.results
373
+ .filter((result) => result.status === "installed")
374
+ .map((result) => result.agentId);
375
+ if (installedAgentIds.length === 0)
376
+ return;
377
+ const content = reconstructedSkillMd(opts.skill);
378
+ const lockDir = opts.scope === "user" ? userLockDir() : opts.projectRoot;
379
+ const lock = ensureLockfile(lockDir);
380
+ const files = ["SKILL.md", ...Object.keys(opts.skill.files ?? {})].sort();
381
+ lock.skills[opts.skill.name] = {
382
+ version: opts.skill.version || "1.0.0",
383
+ sha: computeSha(content),
384
+ tier: "VERIFIED",
385
+ installedAt: new Date().toISOString(),
386
+ scope: opts.scope,
387
+ files,
388
+ ...sourceFromIdentifier(opts.identifier),
389
+ };
390
+ lock.agents = [...new Set([...(lock.agents || []), ...installedAgentIds])];
391
+ writeLockfile(lock, lockDir);
49
392
  }
50
- function startInstall(skill, scope) {
51
- const args = buildArgs(skill, scope);
52
- const proc = spawn("vskill", args, { stdio: ["ignore", "pipe", "pipe"] });
393
+ async function runMultiInstallJob(opts) {
53
394
  const job = {
54
395
  id: randomUUID(),
55
- skill,
56
- scope,
57
- proc,
58
- stdoutBuffer: "",
59
- stderrBuffer: "",
60
- exitCode: null,
61
396
  done: false,
62
397
  subscribers: new Set(),
63
398
  pastEvents: [],
64
399
  };
65
- proc.stdout?.on("data", (chunk) => {
66
- const text = chunk.toString();
67
- job.stdoutBuffer = trimRing(job.stdoutBuffer + text, STDOUT_BUFFER_CAP);
68
- for (const line of text.split(/\r?\n/)) {
69
- if (line.length > 0)
70
- emitToJob(job, "progress", { stream: "stdout", line });
400
+ MULTI_JOBS.set(job.id, job);
401
+ // 0850 terminal trace so users have visible evidence the install ran.
402
+ console.log("[install] start", JSON.stringify({
403
+ skill: opts.identifier,
404
+ scope: opts.scope,
405
+ agentIds: opts.agentIds,
406
+ projectRoot: opts.projectRoot,
407
+ }));
408
+ // Run install async — caller returns the jobId synchronously, then
409
+ // streams progress + final done event as agents complete.
410
+ (async () => {
411
+ try {
412
+ const result = await installSkillToMultipleAgents({
413
+ skill: opts.skill,
414
+ agentIds: opts.agentIds,
415
+ scope: opts.scope,
416
+ projectRoot: opts.projectRoot,
417
+ });
418
+ for (const agentResult of result.agents) {
419
+ console.log("[install] result", JSON.stringify({
420
+ skill: opts.identifier,
421
+ agentId: agentResult.agentId,
422
+ status: agentResult.status,
423
+ path: agentResult.detail,
424
+ }));
425
+ emitMultiJob(job, "result", agentResult);
426
+ }
427
+ writeMultiInstallLockfile({
428
+ identifier: opts.identifier,
429
+ skill: opts.skill,
430
+ scope: opts.scope,
431
+ projectRoot: opts.projectRoot,
432
+ results: result.agents,
433
+ });
434
+ emitMultiJob(job, "done", {
435
+ success: result.errorCount === 0,
436
+ results: result.agents,
437
+ installedCount: result.installedCount,
438
+ exportedCount: result.exportedCount,
439
+ errorCount: result.errorCount,
440
+ });
71
441
  }
72
- });
73
- proc.stderr?.on("data", (chunk) => {
74
- const text = chunk.toString();
75
- job.stderrBuffer = trimRing(job.stderrBuffer + text, STDOUT_BUFFER_CAP);
76
- for (const line of text.split(/\r?\n/)) {
77
- if (line.length > 0)
78
- emitToJob(job, "progress", { stream: "stderr", line });
442
+ catch (err) {
443
+ const message = err instanceof Error ? err.message : String(err);
444
+ emitMultiJob(job, "done", { success: false, error: message });
79
445
  }
80
- });
81
- let timedOut = false;
82
- const timer = setTimeout(() => {
83
- timedOut = true;
84
- try {
85
- proc.kill("SIGTERM");
86
- }
87
- catch { /* process already gone */ }
88
- }, INSTALL_TIMEOUT_MS);
89
- proc.on("exit", (code) => {
90
- clearTimeout(timer);
91
- job.exitCode = timedOut ? -1 : code;
92
- job.done = true;
93
- const stderr = timedOut ? "timeout" : job.stderrBuffer.trim();
94
- emitToJob(job, "done", {
95
- success: !timedOut && code === 0,
96
- exitCode: timedOut ? -1 : code,
97
- stderr,
98
- });
99
- });
100
- proc.on("error", (err) => {
101
- clearTimeout(timer);
102
- job.exitCode = -1;
103
- job.done = true;
104
- emitToJob(job, "done", {
105
- success: false,
106
- exitCode: -1,
107
- stderr: err.message || "spawn failed",
108
- });
109
- });
110
- JOBS.set(job.id, job);
446
+ finally {
447
+ job.done = true;
448
+ }
449
+ })();
111
450
  return job;
112
451
  }
113
- export function registerInstallSkillRoutes(router) {
114
- // POST /api/studio/install-skill { skill, scope } → 202 { jobId }
452
+ export function registerInstallSkillRoutes(router, root = process.cwd()) {
453
+ // POST /api/studio/install-skill
454
+ // Body shape (legacy single-agent CLI spawn): { skill: string, scope: "project"|"user"|"global" }
455
+ // Body shape (new multi-agent in-process): { skill: string, scope, agentIds: string[], parsedSkill: ParsedSkill, projectRoot?: string }
115
456
  router.post("/api/studio/install-skill", async (req, res) => {
116
457
  if (!isLocalhost(req)) {
117
458
  sendJson(res, { error: "localhost-only endpoint" }, 403, req);
@@ -128,16 +469,81 @@ export function registerInstallSkillRoutes(router) {
128
469
  sendJson(res, { error: "invalid scope (must be project|user|global)" }, 400, req);
129
470
  return;
130
471
  }
131
- const job = startInstall(skill, scope);
472
+ // 0845 T-017 (AC-US2-06): multi-agent in-process path.
473
+ if (body.agentIds !== undefined) {
474
+ const validation = validateAgentIds(body.agentIds);
475
+ if (!validation.ok) {
476
+ sendJson(res, { error: validation.error }, 400, req);
477
+ return;
478
+ }
479
+ const normalizedScope = normalizeInstallScope(scope);
480
+ // 0845 closure fix: server-side fallback resolves parsedSkill from
481
+ // the local skill registry when callers omit it. Keeps the frontend
482
+ // payload minimal — the browser doesn't need to fetch + parse
483
+ // SKILL.md itself. Explicit `parsedSkill` in the body still wins so
484
+ // out-of-tree skills (e.g. authoring flows) keep working.
485
+ let parsedSkill;
486
+ if (isValidParsedSkill(body.parsedSkill)) {
487
+ parsedSkill = {
488
+ ...body.parsedSkill,
489
+ files: sanitizeSkillBundleFiles(body.parsedSkill.files),
490
+ };
491
+ }
492
+ else if (body.parsedSkill !== undefined) {
493
+ sendJson(res, {
494
+ error: "parsedSkill, when provided, must have shape { name, description, body }",
495
+ }, 400, req);
496
+ return;
497
+ }
498
+ else {
499
+ const resolved = await resolveParsedSkillFromIdentifier(skill, { cwd: root });
500
+ if (!resolved) {
501
+ sendJson(res, {
502
+ error: `parsedSkill omitted and could not locate skill "${skill}" in project/personal/plugin-cache; ` +
503
+ "either install the skill locally first or include parsedSkill: { name, description, body } in the request body",
504
+ }, 404, req);
505
+ return;
506
+ }
507
+ parsedSkill = resolved;
508
+ }
509
+ const projectRoot = typeof body.projectRoot === "string" && body.projectRoot
510
+ ? body.projectRoot
511
+ : root;
512
+ const job = await runMultiInstallJob({
513
+ identifier: skill,
514
+ skill: parsedSkill,
515
+ agentIds: validation.ids,
516
+ scope: normalizedScope,
517
+ projectRoot,
518
+ });
519
+ sendJson(res, {
520
+ jobId: job.id,
521
+ mode: "multi-agent",
522
+ streamPath: `/api/studio/install-skill/multi/${encodeURIComponent(job.id)}/stream`,
523
+ }, 202, req);
524
+ return;
525
+ }
526
+ // 0845 T-017 (AC-US2-09): legacy single-agent CLI spawn — backward compat.
527
+ const job = startSpawnJob({
528
+ command: "vskill",
529
+ args: buildArgs(skill, scope),
530
+ meta: { skill, scope: scope },
531
+ timeoutMs: INSTALL_TIMEOUT_MS,
532
+ jobs: JOBS,
533
+ });
132
534
  sendJson(res, { jobId: job.id }, 202, req);
133
535
  });
134
- // GET /api/studio/install-skill/:jobId/stream — SSE
135
- router.get("/api/studio/install-skill/:jobId/stream", async (req, res, params) => {
536
+ // GET /api/studio/install-skill/:jobId/stream — legacy spawn-job stream.
537
+ mountSpawnStreamRoute(router, "/api/studio/install-skill", JOBS);
538
+ // 0845 T-017 — distinct stream for multi-install jobs (in-process).
539
+ // Same SSE shape (event: progress | done) so the frontend can consume
540
+ // both flavors with one EventSource subscription pattern.
541
+ router.get("/api/studio/install-skill/multi/:jobId/stream", async (req, res, params) => {
136
542
  if (!isLocalhost(req)) {
137
543
  sendJson(res, { error: "localhost-only endpoint" }, 403, req);
138
544
  return;
139
545
  }
140
- const job = JOBS.get(params.jobId);
546
+ const job = MULTI_JOBS.get(params.jobId);
141
547
  if (!job) {
142
548
  sendJson(res, { error: "unknown jobId" }, 404, req);
143
549
  return;
@@ -145,7 +551,10 @@ export function registerInstallSkillRoutes(router) {
145
551
  initSSE(res, req);
146
552
  for (const ev of job.pastEvents) {
147
553
  try {
148
- sendSSE(res, ev.event, ev.data);
554
+ if (ev.event === "done")
555
+ sendSSEDone(res, ev.data);
556
+ else
557
+ sendSSE(res, ev.event, ev.data);
149
558
  }
150
559
  catch { /* stream closed */ }
151
560
  }
@@ -163,7 +572,9 @@ export function registerInstallSkillRoutes(router) {
163
572
  else
164
573
  sendSSE(res, event, data);
165
574
  }
166
- catch { /* stream closed */ }
575
+ catch {
576
+ job.subscribers.delete(subscriber);
577
+ }
167
578
  };
168
579
  job.subscribers.add(subscriber);
169
580
  const cleanup = () => { job.subscribers.delete(subscriber); };
@@ -171,5 +582,21 @@ export function registerInstallSkillRoutes(router) {
171
582
  req.on("aborted", cleanup);
172
583
  });
173
584
  }
174
- export const __test__ = { JOBS, SAFE_NAME, VALID_SCOPES, INSTALL_TIMEOUT_MS, buildArgs };
585
+ export const __test__ = {
586
+ JOBS,
587
+ MULTI_JOBS,
588
+ SAFE_NAME,
589
+ SAFE_AGENT_ID,
590
+ VALID_SCOPES,
591
+ INSTALL_TIMEOUT_MS,
592
+ buildArgs,
593
+ normalizeInstallScope,
594
+ validateAgentIds,
595
+ isValidParsedSkill,
596
+ resolveParsedSkillFromIdentifier,
597
+ resolveParsedSkillFromPlatform,
598
+ fetchSkillMdFromGitHub,
599
+ runMultiInstallJob,
600
+ writeMultiInstallLockfile,
601
+ };
175
602
  //# sourceMappingURL=install-skill-routes.js.map