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
@@ -931,6 +931,12 @@ export const PROVIDER_MODELS = {
931
931
  // what models the user has loaded. The probe at probeLmStudio() populates
932
932
  // this dynamically from GET /v1/models.
933
933
  "lm-studio": [],
934
+ // 0857: `stub` is a deterministic TEST SEAM (src/eval/llm.ts createStubClient),
935
+ // NOT a user-facing provider. This empty list exists only to satisfy the
936
+ // `Record<ProviderName, ModelOption[]>` exhaustiveness check — `stub` is
937
+ // deliberately excluded from `detectAvailableProviders()` and
938
+ // `KNOWN_PROVIDER_NAMES`, so it never reaches /api/config or the picker.
939
+ "stub": [],
934
940
  };
935
941
  // ---------------------------------------------------------------------------
936
942
  // Local provider detection caches — avoid 500ms+ probes on every /api/config
@@ -2425,6 +2431,47 @@ export function registerRoutes(router, root, projectName) {
2425
2431
  sendJson(res, { error: "Invalid skill path", code: "invalid-path" }, 400, req);
2426
2432
  return;
2427
2433
  }
2434
+ // 0786 AC-US3-03: lockfile-first gate. The previous flow attempted the
2435
+ // lockfile delete first, then the dir trash, and only returned a generic
2436
+ // 404 not-installed when both fell through — which surfaced as a
2437
+ // cryptic "Couldn't uninstall <skill>: api…" toast on source-authored
2438
+ // skills. Now: read the lockfile up front, and if the skill is not a
2439
+ // lockfile-tracked installed plugin skill, return HTTP 422 with the
2440
+ // structured `not-installed` code so the UI can render a friendly
2441
+ // "use Delete instead" message (see App.tsx pendingUninstall onFailure).
2442
+ //
2443
+ // Response contract:
2444
+ // 400 invalid-skill-name — bad kebab regex
2445
+ // 400 invalid-path — path-traversal attempt
2446
+ // 422 not-installed — skill not in lockfile (this gate)
2447
+ // 500 lockfile-read-failed — readLockfile threw
2448
+ // 500 lockfile-write-failed — writeLockfile threw
2449
+ // 500 trash-failed — trash threw
2450
+ // 200 ok — entry removed from lockfile, dir trashed
2451
+ //
2452
+ // 0786 review F-003: lockfile key is currently the bare skill name
2453
+ // (`lock.skills[params.skill]`) — params.plugin is intentionally NOT part
2454
+ // of the key because the on-disk layout `~/.claude/skills/<skill>/` is
2455
+ // unscoped. If two installed skills ever share the same name across
2456
+ // different plugins, this gate disambiguates the FIRST uninstall correctly
2457
+ // (entry removed) and 422s the SECOND (no entry left). The pre-0786 flow
2458
+ // had the same shape; documenting the limitation here so future contributors
2459
+ // don't accidentally introduce a plugin-scoped check on one side without
2460
+ // updating writeLockfile / skill-list (api-routes.ts:1940 same shape).
2461
+ let lockfileEntryExists = false;
2462
+ try {
2463
+ const lockBefore = readLockfile(root);
2464
+ lockfileEntryExists = !!(lockBefore?.skills &&
2465
+ Object.prototype.hasOwnProperty.call(lockBefore.skills, params.skill));
2466
+ }
2467
+ catch (err) {
2468
+ sendJson(res, { error: `Failed to read lockfile: ${err.message}`, code: "lockfile-read-failed" }, 500, req);
2469
+ return;
2470
+ }
2471
+ if (!lockfileEntryExists) {
2472
+ sendJson(res, { error: `${params.skill} is not installed`, code: "not-installed" }, 422, req);
2473
+ return;
2474
+ }
2428
2475
  let removedFromLockfile = false;
2429
2476
  try {
2430
2477
  const lock = readLockfile(root);
@@ -2454,10 +2501,6 @@ export function registerRoutes(router, root, projectName) {
2454
2501
  return;
2455
2502
  }
2456
2503
  }
2457
- if (!removedFromLockfile && trashedDir === null) {
2458
- sendJson(res, { error: "Skill is not installed", code: "not-installed" }, 404, req);
2459
- return;
2460
- }
2461
2504
  sendJson(res, { ok: true, removedFromLockfile, trashedDir }, 200, req);
2462
2505
  });
2463
2506
  // Get skill description (for activation testing preview)
@@ -3469,14 +3512,171 @@ Return ONLY the JSON lines, no other text.`;
3469
3512
  }
3470
3513
  sendJson(res, { ok: true, command: launch.command, args: launch.args }, 200, req);
3471
3514
  });
3472
- // Handle CORS preflight
3515
+ // -------------------------------------------------------------------------
3516
+ // 0828 — POST /api/skills/clone
3517
+ //
3518
+ // Closes the gap left by 0822 (vskill-clone-skill-fork) which shipped the
3519
+ // CLI but explicitly deferred Studio UI ("This increment is CLI-only" —
3520
+ // 0822/spec.md:149). Forks an installed skill into the authoring scope by
3521
+ // spawning the `vskill clone` binary with --target standalone --path
3522
+ // <root>/skills/<skill-name>-fork --yes (auto-confirm, non-interactive).
3523
+ //
3524
+ // We shell out rather than calling runCloneOnce in-process because the
3525
+ // CLI orchestrator uses `process.exit()` on errors (cloneCommand:580) and
3526
+ // because spawning the same binary the user would run from terminal keeps
3527
+ // a single source of truth — if the CLI gains new flags, this route
3528
+ // automatically benefits.
3529
+ //
3530
+ // Body schema:
3531
+ // { source: string, // skill name to clone
3532
+ // sourcePlugin?: string, // plugin context (informational)
3533
+ // target?: "standalone", // only standalone supported in v1
3534
+ // path?: string, // optional override; defaults to <root>/skills/<name>-fork
3535
+ // author?: string, // defaults to git config user.name
3536
+ // namespace?: string, // defaults to slugified author
3537
+ // force?: boolean } // overwrite existing target
3538
+ //
3539
+ // Returns:
3540
+ // 200 { ok: true, target: <abs path>, files: <count>, stdout: <tail> }
3541
+ // 400 { error: "invalid_body" | "invalid_source" }
3542
+ // 500 { error: "clone_failed", message: <stderr tail>, exitCode }
3543
+ // -------------------------------------------------------------------------
3544
+ router.post("/api/skills/clone", async (req, res) => {
3545
+ let body;
3546
+ try {
3547
+ body = (await readBody(req));
3548
+ }
3549
+ catch {
3550
+ sendJson(res, { error: "invalid_body" }, 400, req);
3551
+ return;
3552
+ }
3553
+ const source = typeof body.source === "string" ? body.source.trim() : "";
3554
+ if (!source || !/^[a-z0-9][a-z0-9-]*$/i.test(source) || source.length > 64) {
3555
+ sendJson(res, { error: "invalid_source" }, 400, req);
3556
+ return;
3557
+ }
3558
+ const validTargets = ["standalone", "plugin", "new-plugin"];
3559
+ const target = validTargets.includes(body.target)
3560
+ ? body.target
3561
+ : "standalone";
3562
+ // Resolve the path inside `root`, with a path-traversal guard for any
3563
+ // client-supplied path. We compute `targetPath` for the response payload;
3564
+ // for plugin / new-plugin targets the CLI computes the actual skill dir
3565
+ // from --plugin / --plugin-name.
3566
+ const rootAbs = resolve(root);
3567
+ function ensureInsideRoot(p) {
3568
+ const abs = resolve(rootAbs, p);
3569
+ if (abs !== rootAbs && !abs.startsWith(rootAbs + "/"))
3570
+ return null;
3571
+ return abs;
3572
+ }
3573
+ const slug = source.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3574
+ let targetPath = "";
3575
+ // Build CLI args. We always pass --yes to avoid hanging on a missing TTY.
3576
+ const cliArgs = ["clone", source, "--yes", "--target", target];
3577
+ if (target === "standalone") {
3578
+ const defaultDir = join(rootAbs, "skills", `${slug}-fork`);
3579
+ targetPath = defaultDir;
3580
+ if (typeof body.path === "string" && body.path.trim()) {
3581
+ const candidate = ensureInsideRoot(body.path);
3582
+ if (!candidate) {
3583
+ sendJson(res, { error: "path_outside_root" }, 400, req);
3584
+ return;
3585
+ }
3586
+ targetPath = candidate;
3587
+ }
3588
+ cliArgs.push("--path", targetPath);
3589
+ }
3590
+ else if (target === "plugin") {
3591
+ // Existing plugin — body.plugin is the plugin name OR an absolute path.
3592
+ const plugin = typeof body.plugin === "string" ? body.plugin.trim() : "";
3593
+ if (!plugin) {
3594
+ sendJson(res, { error: "missing_plugin" }, 400, req);
3595
+ return;
3596
+ }
3597
+ // If it looks like a path, validate it's inside root. Otherwise let the
3598
+ // CLI resolve the plugin name from its registry.
3599
+ if (plugin.startsWith("/") || plugin.startsWith(".") || plugin.includes("/")) {
3600
+ const candidate = ensureInsideRoot(plugin);
3601
+ if (!candidate) {
3602
+ sendJson(res, { error: "path_outside_root" }, 400, req);
3603
+ return;
3604
+ }
3605
+ cliArgs.push("--plugin", candidate);
3606
+ targetPath = join(candidate, "skills", slug);
3607
+ }
3608
+ else {
3609
+ cliArgs.push("--plugin", plugin);
3610
+ targetPath = `<plugin:${plugin}>/skills/${slug}`; // informational
3611
+ }
3612
+ }
3613
+ else {
3614
+ // new-plugin — scaffold a fresh plugin folder.
3615
+ const pluginName = typeof body.pluginName === "string" ? body.pluginName.trim() : "";
3616
+ if (!pluginName || !/^[a-z][a-z0-9-]{0,62}[a-z0-9]$/.test(pluginName)) {
3617
+ sendJson(res, { error: "invalid_plugin_name" }, 400, req);
3618
+ return;
3619
+ }
3620
+ const pluginRoot = join(rootAbs, pluginName);
3621
+ cliArgs.push("--plugin-name", pluginName);
3622
+ cliArgs.push("--path", pluginRoot);
3623
+ targetPath = join(pluginRoot, "skills", slug);
3624
+ }
3625
+ if (typeof body.author === "string" && body.author.trim()) {
3626
+ cliArgs.push("--author", body.author.trim());
3627
+ }
3628
+ if (typeof body.namespace === "string" && body.namespace.trim()) {
3629
+ cliArgs.push("--namespace", body.namespace.trim());
3630
+ }
3631
+ if (body.force === true)
3632
+ cliArgs.push("--force");
3633
+ // Spawn the `vskill` binary on PATH — same pattern used by
3634
+ // install-skill-routes.ts:74 for `vskill install`. This keeps a single
3635
+ // source of truth (whatever the user runs from terminal also runs here).
3636
+ let stdout = "";
3637
+ let stderr = "";
3638
+ const exitCode = await new Promise((resolveCode) => {
3639
+ const child = spawn("vskill", cliArgs, {
3640
+ cwd: root,
3641
+ stdio: ["ignore", "pipe", "pipe"],
3642
+ });
3643
+ child.stdout?.on("data", (c) => { stdout += c.toString(); });
3644
+ child.stderr?.on("data", (c) => { stderr += c.toString(); });
3645
+ child.on("error", () => resolveCode(127));
3646
+ child.on("close", (code) => resolveCode(code));
3647
+ });
3648
+ if (exitCode !== 0) {
3649
+ const tail = (stderr || stdout).trim().split("\n").slice(-6).join("\n");
3650
+ sendJson(res, { error: "clone_failed", message: tail || "non-zero exit", exitCode }, 500, req);
3651
+ return;
3652
+ }
3653
+ // Parse "files: N" from stdout for the response.
3654
+ const filesMatch = stdout.match(/files:\s+(\d+)/);
3655
+ const files = filesMatch ? parseInt(filesMatch[1], 10) : null;
3656
+ sendJson(res, {
3657
+ ok: true,
3658
+ target: targetPath,
3659
+ files,
3660
+ stdout: stdout.trim().split("\n").slice(-8).join("\n"),
3661
+ }, 200, req);
3662
+ });
3663
+ // Handle CORS preflight.
3664
+ //
3665
+ // 0836 US-002 (F-017 follow-up): every authenticated /api/* request now
3666
+ // carries the `X-Studio-Token` custom header. Browser-initiated cross-origin
3667
+ // fetches issue a CORS preflight; the response MUST list `X-Studio-Token`
3668
+ // in `Access-Control-Allow-Headers` or the browser blocks the actual
3669
+ // request. The desktop hot path is same-origin (WebView is navigated to
3670
+ // `http://127.0.0.1:{port}/`) and skips preflight, but CLI users opening
3671
+ // the studio in a regular browser tab — and any future cross-origin
3672
+ // tooling — depend on this echo.
3473
3673
  router.options = (req, res) => {
3474
3674
  const origin = req.headers.origin;
3475
3675
  if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
3476
3676
  res.writeHead(204, {
3477
3677
  "Access-Control-Allow-Origin": origin,
3478
3678
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
3479
- "Access-Control-Allow-Headers": "Content-Type",
3679
+ "Access-Control-Allow-Headers": "Content-Type, X-Studio-Token",
3480
3680
  "Access-Control-Max-Age": "3600",
3481
3681
  });
3482
3682
  }