vskill 1.0.16 → 1.0.19

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 (225) hide show
  1. package/README.md +27 -3
  2. package/agents.json +3 -1
  3. package/dist/agents/agents-registry.d.ts +61 -0
  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/bin.js +0 -0
  10. package/dist/commands/add.d.ts +7 -0
  11. package/dist/commands/add.js +94 -1
  12. package/dist/commands/add.js.map +1 -1
  13. package/dist/commands/auth.d.ts +23 -0
  14. package/dist/commands/auth.js +136 -12
  15. package/dist/commands/auth.js.map +1 -1
  16. package/dist/commands/eval/serve.d.ts +2 -0
  17. package/dist/commands/eval/serve.js +126 -4
  18. package/dist/commands/eval/serve.js.map +1 -1
  19. package/dist/commands/orgs.d.ts +21 -0
  20. package/dist/commands/orgs.js +164 -0
  21. package/dist/commands/orgs.js.map +1 -0
  22. package/dist/commands/skill.js +14 -1
  23. package/dist/commands/skill.js.map +1 -1
  24. package/dist/commands/whoami.d.ts +29 -0
  25. package/dist/commands/whoami.js +119 -0
  26. package/dist/commands/whoami.js.map +1 -0
  27. package/dist/eval/anthropic-catalog.js +32 -2
  28. package/dist/eval/anthropic-catalog.js.map +1 -1
  29. package/dist/eval/batch-judge.js +1 -0
  30. package/dist/eval/batch-judge.js.map +1 -1
  31. package/dist/eval/llm.d.ts +1 -1
  32. package/dist/eval/llm.js +104 -2
  33. package/dist/eval/llm.js.map +1 -1
  34. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.d.ts +2 -0
  35. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js +20 -0
  36. package/dist/eval-server/__tests__/helpers/studio-token-test-helpers.js.map +1 -0
  37. package/dist/eval-server/active-root-store.d.ts +19 -0
  38. package/dist/eval-server/active-root-store.js +50 -0
  39. package/dist/eval-server/active-root-store.js.map +1 -0
  40. package/dist/eval-server/active-tenant-routes.d.ts +15 -0
  41. package/dist/eval-server/active-tenant-routes.js +101 -0
  42. package/dist/eval-server/active-tenant-routes.js.map +1 -0
  43. package/dist/eval-server/api-routes.d.ts +1 -1
  44. package/dist/eval-server/api-routes.js +60 -7
  45. package/dist/eval-server/api-routes.js.map +1 -1
  46. package/dist/eval-server/authoring-routes.d.ts +1 -1
  47. package/dist/eval-server/authoring-routes.js +9 -7
  48. package/dist/eval-server/authoring-routes.js.map +1 -1
  49. package/dist/eval-server/desktop-open-routes.d.ts +8 -0
  50. package/dist/eval-server/desktop-open-routes.js +64 -0
  51. package/dist/eval-server/desktop-open-routes.js.map +1 -0
  52. package/dist/eval-server/detect-engines-route.d.ts +1 -1
  53. package/dist/eval-server/detect-engines-route.js +3 -1
  54. package/dist/eval-server/detect-engines-route.js.map +1 -1
  55. package/dist/eval-server/eval-server.js +108 -29
  56. package/dist/eval-server/eval-server.js.map +1 -1
  57. package/dist/eval-server/export-skill-routes.d.ts +9 -0
  58. package/dist/eval-server/export-skill-routes.js +81 -0
  59. package/dist/eval-server/export-skill-routes.js.map +1 -0
  60. package/dist/eval-server/git-routes.d.ts +7 -6
  61. package/dist/eval-server/git-routes.js +123 -15
  62. package/dist/eval-server/git-routes.js.map +1 -1
  63. package/dist/eval-server/improve-routes.d.ts +1 -1
  64. package/dist/eval-server/improve-routes.js +4 -1
  65. package/dist/eval-server/improve-routes.js.map +1 -1
  66. package/dist/eval-server/install-engine-routes.d.ts +4 -17
  67. package/dist/eval-server/install-engine-routes.js +10 -125
  68. package/dist/eval-server/install-engine-routes.js.map +1 -1
  69. package/dist/eval-server/install-jobs.d.ts +41 -0
  70. package/dist/eval-server/install-jobs.js +161 -0
  71. package/dist/eval-server/install-jobs.js.map +1 -0
  72. package/dist/eval-server/install-skill-routes.d.ts +74 -11
  73. package/dist/eval-server/install-skill-routes.js +508 -79
  74. package/dist/eval-server/install-skill-routes.js.map +1 -1
  75. package/dist/eval-server/install-state-routes.d.ts +5 -1
  76. package/dist/eval-server/install-state-routes.js +18 -2
  77. package/dist/eval-server/install-state-routes.js.map +1 -1
  78. package/dist/eval-server/integration-routes.d.ts +1 -1
  79. package/dist/eval-server/integration-routes.js +6 -1
  80. package/dist/eval-server/integration-routes.js.map +1 -1
  81. package/dist/eval-server/model-compare-routes.d.ts +1 -1
  82. package/dist/eval-server/model-compare-routes.js +3 -1
  83. package/dist/eval-server/model-compare-routes.js.map +1 -1
  84. package/dist/eval-server/oauth-github-routes.d.ts +2 -0
  85. package/dist/eval-server/oauth-github-routes.js +505 -0
  86. package/dist/eval-server/oauth-github-routes.js.map +1 -0
  87. package/dist/eval-server/platform-proxy.d.ts +22 -1
  88. package/dist/eval-server/platform-proxy.js +183 -22
  89. package/dist/eval-server/platform-proxy.js.map +1 -1
  90. package/dist/eval-server/plugin-cli-routes.d.ts +1 -1
  91. package/dist/eval-server/plugin-cli-routes.js +19 -10
  92. package/dist/eval-server/plugin-cli-routes.js.map +1 -1
  93. package/dist/eval-server/remove-skill-routes.d.ts +18 -0
  94. package/dist/eval-server/remove-skill-routes.js +147 -0
  95. package/dist/eval-server/remove-skill-routes.js.map +1 -0
  96. package/dist/eval-server/router.d.ts +17 -3
  97. package/dist/eval-server/router.js +166 -9
  98. package/dist/eval-server/router.js.map +1 -1
  99. package/dist/eval-server/settings-store.js +1 -1
  100. package/dist/eval-server/settings-store.js.map +1 -1
  101. package/dist/eval-server/skill-create-routes.d.ts +1 -1
  102. package/dist/eval-server/skill-create-routes.js +8 -1
  103. package/dist/eval-server/skill-create-routes.js.map +1 -1
  104. package/dist/eval-server/supported-agents-routes.d.ts +6 -0
  105. package/dist/eval-server/supported-agents-routes.js +41 -0
  106. package/dist/eval-server/supported-agents-routes.js.map +1 -0
  107. package/dist/eval-server/sweep-routes.d.ts +1 -1
  108. package/dist/eval-server/sweep-routes.js +5 -1
  109. package/dist/eval-server/sweep-routes.js.map +1 -1
  110. package/dist/eval-server/utils/spawn-env.d.ts +1 -0
  111. package/dist/eval-server/utils/spawn-env.js +47 -0
  112. package/dist/eval-server/utils/spawn-env.js.map +1 -0
  113. package/dist/eval-server/workspace-routes.d.ts +12 -0
  114. package/dist/eval-server/workspace-routes.js +57 -2
  115. package/dist/eval-server/workspace-routes.js.map +1 -1
  116. package/dist/eval-ui/assets/AdvancedTab-DOgbx7u0.js +1 -0
  117. package/dist/eval-ui/assets/{CreateSkillPage-CvdYq8Rr.js → CreateSkillPage-Cv93Croj.js} +3 -3
  118. package/dist/eval-ui/assets/FindSkillsPalette-BY9DAhHh.js +2 -0
  119. package/dist/eval-ui/assets/GeneralTab-AwK9sIkP.js +1 -0
  120. package/dist/eval-ui/assets/PrivacyTab-BtNrxpVV.js +1 -0
  121. package/dist/eval-ui/assets/{SearchPaletteCore-Bf3PBC64.js → SearchPaletteCore-DMVcq7UB.js} +2 -2
  122. package/dist/eval-ui/assets/SkillDetailPanel-B_lbhK6q.js +1 -0
  123. package/dist/eval-ui/assets/UpdateDropdown-4AbjZLpq.js +1 -0
  124. package/dist/eval-ui/assets/UpdatesTab-DTmo-vVb.js +1 -0
  125. package/dist/eval-ui/assets/core-DZjBCfjp.js +1 -0
  126. package/dist/eval-ui/assets/event-QtOCMXAv.js +1 -0
  127. package/dist/eval-ui/assets/globals-Dpf9KmYH.css +1 -0
  128. package/dist/eval-ui/assets/globals-hm1COkXX.js +49 -0
  129. package/dist/eval-ui/assets/index-CUEYzTVL.js +1 -0
  130. package/dist/eval-ui/assets/index-DDNzcrhv.js +1 -0
  131. package/dist/eval-ui/assets/index-DhhmQddr.js +1 -0
  132. package/dist/eval-ui/assets/lifecycle-d1Sm9Hts.css +1 -0
  133. package/dist/eval-ui/assets/lifecycle-o_IRibOa.js +1 -0
  134. package/dist/eval-ui/assets/main-tpOyw9SC.js +87 -0
  135. package/dist/eval-ui/assets/preferences-BHZXB5dL.css +1 -0
  136. package/dist/eval-ui/assets/preferences-DCdw0Kvu.js +2 -0
  137. package/dist/eval-ui/assets/useDesktopBridge-9oZFQsrw.js +2 -0
  138. package/dist/eval-ui/index.html +4 -2
  139. package/dist/eval-ui/lifecycle.html +33 -0
  140. package/dist/eval-ui/preferences.html +34 -0
  141. package/dist/index.js +47 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/installer/bundle-files.d.ts +4 -0
  144. package/dist/installer/bundle-files.js +97 -0
  145. package/dist/installer/bundle-files.js.map +1 -0
  146. package/dist/installer/canonical.d.ts +31 -6
  147. package/dist/installer/canonical.js +48 -12
  148. package/dist/installer/canonical.js.map +1 -1
  149. package/dist/installer/clipboard-export.d.ts +19 -0
  150. package/dist/installer/clipboard-export.js +88 -0
  151. package/dist/installer/clipboard-export.js.map +1 -0
  152. package/dist/installer/multi-install.d.ts +43 -0
  153. package/dist/installer/multi-install.js +237 -0
  154. package/dist/installer/multi-install.js.map +1 -0
  155. package/dist/installer/transformers/aider.d.ts +2 -0
  156. package/dist/installer/transformers/aider.js +32 -0
  157. package/dist/installer/transformers/aider.js.map +1 -0
  158. package/dist/installer/transformers/continue-dev.d.ts +2 -0
  159. package/dist/installer/transformers/continue-dev.js +6 -0
  160. package/dist/installer/transformers/continue-dev.js.map +1 -0
  161. package/dist/installer/transformers/cursor.d.ts +2 -0
  162. package/dist/installer/transformers/cursor.js +24 -0
  163. package/dist/installer/transformers/cursor.js.map +1 -0
  164. package/dist/installer/transformers/github-copilot.d.ts +2 -0
  165. package/dist/installer/transformers/github-copilot.js +17 -0
  166. package/dist/installer/transformers/github-copilot.js.map +1 -0
  167. package/dist/installer/transformers/index.d.ts +78 -0
  168. package/dist/installer/transformers/index.js +13 -0
  169. package/dist/installer/transformers/index.js.map +1 -0
  170. package/dist/installer/transformers/junie.d.ts +2 -0
  171. package/dist/installer/transformers/junie.js +6 -0
  172. package/dist/installer/transformers/junie.js.map +1 -0
  173. package/dist/installer/transformers/kiro.d.ts +2 -0
  174. package/dist/installer/transformers/kiro.js +6 -0
  175. package/dist/installer/transformers/kiro.js.map +1 -0
  176. package/dist/installer/transformers/trae.d.ts +2 -0
  177. package/dist/installer/transformers/trae.js +6 -0
  178. package/dist/installer/transformers/trae.js.map +1 -0
  179. package/dist/installer/transformers/windsurf.d.ts +2 -0
  180. package/dist/installer/transformers/windsurf.js +12 -0
  181. package/dist/installer/transformers/windsurf.js.map +1 -0
  182. package/dist/installer/yaml-safe-mutate.d.ts +19 -0
  183. package/dist/installer/yaml-safe-mutate.js +184 -0
  184. package/dist/installer/yaml-safe-mutate.js.map +1 -0
  185. package/dist/lib/active-tenant.d.ts +36 -0
  186. package/dist/lib/active-tenant.js +120 -0
  187. package/dist/lib/active-tenant.js.map +1 -0
  188. package/dist/lib/keychain.d.ts +15 -2
  189. package/dist/lib/keychain.js +173 -8
  190. package/dist/lib/keychain.js.map +1 -1
  191. package/dist/lib/migration/keychain-migration.d.ts +35 -0
  192. package/dist/lib/migration/keychain-migration.js +189 -0
  193. package/dist/lib/migration/keychain-migration.js.map +1 -0
  194. package/dist/lib/tenant-resolver.d.ts +38 -0
  195. package/dist/lib/tenant-resolver.js +79 -0
  196. package/dist/lib/tenant-resolver.js.map +1 -0
  197. package/dist/studio/lib/ops-log.js +140 -57
  198. package/dist/studio/lib/ops-log.js.map +1 -1
  199. package/dist/studio/lib/scope-transfer.d.ts +11 -1
  200. package/dist/studio/lib/scope-transfer.js +48 -24
  201. package/dist/studio/lib/scope-transfer.js.map +1 -1
  202. package/dist/studio/routes/index.d.ts +1 -1
  203. package/dist/studio/routes/index.js +18 -8
  204. package/dist/studio/routes/index.js.map +1 -1
  205. package/dist/studio/routes/ops.js +31 -7
  206. package/dist/studio/routes/ops.js.map +1 -1
  207. package/dist/studio/routes/promote.d.ts +1 -1
  208. package/dist/studio/routes/promote.js +18 -9
  209. package/dist/studio/routes/promote.js.map +1 -1
  210. package/dist/studio/routes/revert.d.ts +1 -1
  211. package/dist/studio/routes/revert.js +15 -2
  212. package/dist/studio/routes/revert.js.map +1 -1
  213. package/dist/studio/routes/test-install.d.ts +1 -1
  214. package/dist/studio/routes/test-install.js +16 -9
  215. package/dist/studio/routes/test-install.js.map +1 -1
  216. package/dist/studio-runtime/lockfile.d.ts +51 -0
  217. package/dist/studio-runtime/lockfile.js +216 -0
  218. package/dist/studio-runtime/lockfile.js.map +1 -0
  219. package/package.json +18 -1
  220. package/dist/eval-ui/assets/FindSkillsPalette-DsSgotS9.js +0 -2
  221. package/dist/eval-ui/assets/SkillDetailPanel-DAD2yJO-.js +0 -1
  222. package/dist/eval-ui/assets/UpdateDropdown-h5Hg3h7Z.js +0 -1
  223. package/dist/eval-ui/assets/index-CKLqBL52.css +0 -1
  224. package/dist/eval-ui/assets/index-JaDg6FlU.js +0 -124
  225. package/dist/eval-ui/assets/skill-studio-logo-CRyKgIrg.png +0 -0
@@ -0,0 +1,2 @@
1
+ /** Returns `{ "x-studio-token": <token> }` for spreading into headers. */
2
+ export declare function studioTokenHeaders(): Record<string, string>;
@@ -0,0 +1,20 @@
1
+ // ---------------------------------------------------------------------------
2
+ // 0836 US-002 — test helpers for the X-Studio-Token gate.
3
+ //
4
+ // Tests that drive `Router.handle()` against `/api/*` paths must include the
5
+ // live studio token; otherwise the gate writes 401 and the route handler
6
+ // never runs. Use:
7
+ //
8
+ // import { studioTokenHeaders } from "./helpers/studio-token-test-helpers.js";
9
+ // const req = { ..., headers: { host: "localhost", ...studioTokenHeaders() } };
10
+ //
11
+ // For real-fetch tests:
12
+ //
13
+ // await fetch(url, { headers: studioTokenHeaders() });
14
+ // ---------------------------------------------------------------------------
15
+ import { getStudioToken } from "../../router.js";
16
+ /** Returns `{ "x-studio-token": <token> }` for spreading into headers. */
17
+ export function studioTokenHeaders() {
18
+ return { "x-studio-token": getStudioToken() };
19
+ }
20
+ //# sourceMappingURL=studio-token-test-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio-token-test-helpers.js","sourceRoot":"","sources":["../../../../src/eval-server/__tests__/helpers/studio-token-test-helpers.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,0DAA0D;AAC1D,EAAE;AACF,6EAA6E;AAC7E,yEAAyE;AACzE,mBAAmB;AACnB,EAAE;AACF,iFAAiF;AACjF,kFAAkF;AAClF,EAAE;AACF,wBAAwB;AACxB,EAAE;AACF,yDAAyD;AACzD,8EAA8E;AAE9E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB;IAChC,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,EAAE,CAAC;AAChD,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface ActiveRootStore {
2
+ /** The scan root all route handlers should use, resolved per request. */
3
+ getRoot(): string;
4
+ /** Replace the active scan root (e.g. after a project switch). */
5
+ setRoot(root: string): void;
6
+ /**
7
+ * Re-derive the active root from `~/.vskill/workspace.json`'s active project.
8
+ * Updates the store and returns the new value; if no active project resolves,
9
+ * the current root is kept and returned.
10
+ */
11
+ reload(workspaceDir: string): string;
12
+ }
13
+ /**
14
+ * Resolve the active project's path from the workspace registry. Returns null
15
+ * when there is no workspace, no active project, or the active id is dangling.
16
+ * Shared by both boot resolution (eval-server) and runtime reloads (this store).
17
+ */
18
+ export declare function resolveActiveRoot(workspaceDir: string): string | null;
19
+ export declare function createActiveRootStore(initialRoot: string): ActiveRootStore;
@@ -0,0 +1,50 @@
1
+ // ---------------------------------------------------------------------------
2
+ // 0863 T-001: ActiveRootStore — runtime-mutable scan root.
3
+ //
4
+ // Before 0863, the eval-server resolved the scan root ONCE at boot and froze
5
+ // it into every route handler's closure, so switching the active project from
6
+ // the UI (POST /api/workspace/active) changed ~/.vskill/workspace.json but the
7
+ // running server kept serving the original folder's skills.
8
+ //
9
+ // This store holds the current scan root in memory and lets handlers read it
10
+ // per-request via `getRoot()`. `POST /api/workspace/active` calls `setRoot()`
11
+ // after persisting, so the next `GET /api/skills` scans the new folder — no
12
+ // process restart, no port change, no studio-token re-mint. The same code path
13
+ // works in `npx vskill studio` (single process) and the desktop Tauri sidecar.
14
+ // ---------------------------------------------------------------------------
15
+ import { loadWorkspace } from "./workspace-store.js";
16
+ /**
17
+ * Resolve the active project's path from the workspace registry. Returns null
18
+ * when there is no workspace, no active project, or the active id is dangling.
19
+ * Shared by both boot resolution (eval-server) and runtime reloads (this store).
20
+ */
21
+ export function resolveActiveRoot(workspaceDir) {
22
+ try {
23
+ const ws = loadWorkspace(workspaceDir);
24
+ if (!ws.activeProjectId)
25
+ return null;
26
+ const active = ws.projects.find((p) => p.id === ws.activeProjectId);
27
+ return active ? active.path : null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export function createActiveRootStore(initialRoot) {
34
+ let current = initialRoot;
35
+ return {
36
+ getRoot() {
37
+ return current;
38
+ },
39
+ setRoot(root) {
40
+ current = root;
41
+ },
42
+ reload(workspaceDir) {
43
+ const resolved = resolveActiveRoot(workspaceDir);
44
+ if (resolved)
45
+ current = resolved;
46
+ return current;
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=active-root-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-root-store.js","sourceRoot":"","sources":["../../src/eval-server/active-root-store.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2DAA2D;AAC3D,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,+EAA+E;AAC/E,4DAA4D;AAC5D,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAerD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QACrC,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,eAAe,CAAC,CAAC;QACpE,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,WAAmB;IACvD,IAAI,OAAO,GAAG,WAAW,CAAC;IAC1B,OAAO;QACL,OAAO;YACL,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,CAAC,IAAY;YAClB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,MAAM,CAAC,YAAoB;YACzB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;YACjD,IAAI,QAAQ;gBAAE,OAAO,GAAG,QAAQ,CAAC;YACjC,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import * as http from "node:http";
2
+ import { type ActiveTenantOptions } from "../lib/active-tenant.js";
3
+ export interface ActiveTenantRoutesOptions {
4
+ /**
5
+ * Forward to the helpers (tests). Production passes nothing; the
6
+ * helpers default to `~/.vskill/config.json`.
7
+ */
8
+ activeTenantOptions?: ActiveTenantOptions;
9
+ }
10
+ /**
11
+ * Returns true when the request was handled (response sent). Returns false
12
+ * when the URL/method doesn't match — the caller (eval-server.ts) falls
13
+ * through to the next handler (proxy / static).
14
+ */
15
+ export declare function handleActiveTenant(req: http.IncomingMessage, res: http.ServerResponse, opts?: ActiveTenantRoutesOptions): Promise<boolean>;
@@ -0,0 +1,101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // active-tenant-routes.ts — 0839 T-011 / US-004.
3
+ //
4
+ // Loopback-only `/__internal/active-tenant` GET/POST handler used by the
5
+ // Studio TenantPicker (eval-ui T-012/T-013). Reads/writes
6
+ // `~/.vskill/config.json` via the shared helpers in
7
+ // `../lib/active-tenant.ts`, so the CLI (`vskill orgs use`) and the
8
+ // Studio always agree on which tenant is active.
9
+ //
10
+ // Why the path doesn't start with `/api/`:
11
+ // The studio-token gate in `router.ts` only applies to `/api/*`. This
12
+ // route is a UX convenience that needs to work without the WebView
13
+ // threading the studio token (it predates the gate). We compensate
14
+ // with three lower-bound guards:
15
+ // 1. Loopback-only (req.socket.remoteAddress).
16
+ // 2. The eval-server already binds 127.0.0.1 (US-001 hardening).
17
+ // 3. The handler refuses anything but GET / POST + tiny body.
18
+ //
19
+ // Audit logging is intentionally absent — this is a local-only,
20
+ // non-secret read/write of the user's own config file. Logging it would
21
+ // be noise (and the path itself isn't sensitive).
22
+ // ---------------------------------------------------------------------------
23
+ import { readBody, sendJson } from "./router.js";
24
+ import { getActiveTenant, setActiveTenant, } from "../lib/active-tenant.js";
25
+ function isLoopback(req) {
26
+ const addr = req.socket?.remoteAddress ?? "";
27
+ return (addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1");
28
+ }
29
+ /**
30
+ * Returns true when the request was handled (response sent). Returns false
31
+ * when the URL/method doesn't match — the caller (eval-server.ts) falls
32
+ * through to the next handler (proxy / static).
33
+ */
34
+ export async function handleActiveTenant(req, res, opts = {}) {
35
+ const url = req.url || "";
36
+ // Path-based match (no query string is honored, but we strip it just in
37
+ // case the caller appended `?_=` for cache-busting).
38
+ const path = url.split("?")[0];
39
+ if (path !== "/__internal/active-tenant")
40
+ return false;
41
+ // Loopback guard — the eval-server binds 127.0.0.1 already, but a
42
+ // misconfigured proxy or future port-forward could subvert that.
43
+ if (!isLoopback(req)) {
44
+ sendJson(res, { ok: false, error: "loopback-only" }, 403, req);
45
+ return true;
46
+ }
47
+ const method = (req.method ?? "GET").toUpperCase();
48
+ if (method === "GET") {
49
+ const slug = getActiveTenant(opts.activeTenantOptions);
50
+ sendJson(res, { currentTenant: slug });
51
+ return true;
52
+ }
53
+ if (method === "POST") {
54
+ let body;
55
+ try {
56
+ body = await readBody(req);
57
+ }
58
+ catch {
59
+ sendJson(res, { ok: false, error: "invalid JSON" }, 400, req);
60
+ return true;
61
+ }
62
+ if (!body || typeof body !== "object") {
63
+ sendJson(res, { ok: false, error: "expected { currentTenant }" }, 400, req);
64
+ return true;
65
+ }
66
+ const incoming = body.currentTenant;
67
+ // Accept null (clear) and non-empty string. Reject anything else so
68
+ // we don't silently coerce numbers/booleans into config corruption.
69
+ let slug;
70
+ if (incoming === null) {
71
+ slug = null;
72
+ }
73
+ else if (typeof incoming === "string" && incoming.length > 0) {
74
+ // Cheap shape check — slugs are URL-safe DNS labels in the platform
75
+ // schema. Refusing weird input here saves a round-trip to a 4xx
76
+ // tenant lookup later.
77
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(incoming)) {
78
+ sendJson(res, { ok: false, error: "invalid slug format" }, 400, req);
79
+ return true;
80
+ }
81
+ slug = incoming;
82
+ }
83
+ else {
84
+ sendJson(res, { ok: false, error: "currentTenant must be string|null" }, 400, req);
85
+ return true;
86
+ }
87
+ try {
88
+ setActiveTenant(slug, opts.activeTenantOptions);
89
+ }
90
+ catch (err) {
91
+ sendJson(res, { ok: false, error: err.message }, 500, req);
92
+ return true;
93
+ }
94
+ sendJson(res, { currentTenant: slug });
95
+ return true;
96
+ }
97
+ // Other methods.
98
+ sendJson(res, { ok: false, error: "method not allowed" }, 405, req);
99
+ return true;
100
+ }
101
+ //# sourceMappingURL=active-tenant-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"active-tenant-routes.js","sourceRoot":"","sources":["../../src/eval-server/active-tenant-routes.ts"],"names":[],"mappings":"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"}
@@ -214,4 +214,4 @@ export declare function detectAvailableProviders(): Promise<Array<{
214
214
  models: ModelOption[];
215
215
  resolvedModel?: string | null;
216
216
  }>>;
217
- export declare function registerRoutes(router: Router, root: string, projectName?: string): void;
217
+ export declare function registerRoutes(router: Router, rootArg: string | (() => string), projectName?: string): void;
@@ -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
@@ -1148,7 +1154,8 @@ export async function detectAvailableProviders() {
1148
1154
  });
1149
1155
  return providers;
1150
1156
  }
1151
- export function registerRoutes(router, root, projectName) {
1157
+ export function registerRoutes(router, rootArg, projectName) {
1158
+ const getRoot = typeof rootArg === "function" ? rootArg : () => rootArg;
1152
1159
  // Health check
1153
1160
  router.get("/api/health", (_req, res) => {
1154
1161
  sendJson(res, { ok: true });
@@ -1167,6 +1174,7 @@ export function registerRoutes(router, root, projectName) {
1167
1174
  // shared-folder grouping. 30s detection cache (mirrors Ollama/LM Studio
1168
1175
  // probe pattern from 0677).
1169
1176
  router.get("/api/agents", async (req, res) => {
1177
+ const root = getRoot();
1170
1178
  try {
1171
1179
  const detected = await detectInstalledAgents();
1172
1180
  const detectedBinaries = new Set(detected.map((a) => a.id));
@@ -1367,6 +1375,7 @@ export function registerRoutes(router, root, projectName) {
1367
1375
  // (e.g. "claude-sonnet"). The frontend round-trips config.model back to
1368
1376
  // generate-evals and other endpoints, so it must be a valid CLI model ID.
1369
1377
  router.get("/api/config", async (_req, res) => {
1378
+ const root = getRoot();
1370
1379
  // 0682 F-001 — Boot-time restoration of .vskill/studio.json selection.
1371
1380
  // Use a dedicated `studioLoaded` flag rather than checking
1372
1381
  // `!currentOverrides.provider` because the module default
@@ -1436,6 +1445,7 @@ export function registerRoutes(router, root, projectName) {
1436
1445
  });
1437
1446
  // Update config — change provider/model at runtime and persist atomically.
1438
1447
  router.post("/api/config", async (req, res) => {
1448
+ const root = getRoot();
1439
1449
  const body = (await readBody(req));
1440
1450
  // 0682 F-001 (review iter 3): validate the incoming provider against the
1441
1451
  // ProviderName union BEFORE mutating currentOverrides. Pre-fix, an
@@ -1497,6 +1507,7 @@ export function registerRoutes(router, root, projectName) {
1497
1507
  // unknown/missing fields default to `null` (not undefined) so the shape
1498
1508
  // remains JSON-stable for all consumers.
1499
1509
  router.get("/api/skills", async (req, res) => {
1510
+ const root = getRoot();
1500
1511
  // 0686: ?scope=own|installed|global and ?agent=<id> query params.
1501
1512
  // When either is present, switch to tri-scope scanning so the response
1502
1513
  // carries the new `scope`/`isSymlink`/`symlinkTarget`/`installMethod`/
@@ -1651,6 +1662,7 @@ export function registerRoutes(router, root, projectName) {
1651
1662
  }
1652
1663
  });
1653
1664
  router.get("/api/skills/updates", async (req, res) => {
1665
+ const root = getRoot();
1654
1666
  try {
1655
1667
  const { getOutdatedJson } = await import("../commands/outdated.js");
1656
1668
  const { scanSkillInstallLocations } = await import("./utils/scan-install-locations.js");
@@ -1708,7 +1720,7 @@ export function registerRoutes(router, root, projectName) {
1708
1720
  * git remote parse for skills authored in this repo. See skill-name-resolver.ts.
1709
1721
  */
1710
1722
  function resolveSkillApiName(skill, plugin = null) {
1711
- return resolveSkillApiNameImpl(skill, root, plugin);
1723
+ return resolveSkillApiNameImpl(skill, getRoot(), plugin);
1712
1724
  }
1713
1725
  // T-009 (proxy) + 0707 T-021 (harden): Versions endpoint
1714
1726
  //
@@ -1727,7 +1739,7 @@ export function registerRoutes(router, root, projectName) {
1727
1739
  // Returns `{ apiPathRoot, origin }` — callers append `/versions` or
1728
1740
  // `/versions/diff` as needed.
1729
1741
  async function buildSkillApiPath(skill, plugin) {
1730
- const origin = await resolveSkillOrigin(skill, plugin, root);
1742
+ const origin = await resolveSkillOrigin(skill, plugin, getRoot());
1731
1743
  if (origin.owner && origin.repo) {
1732
1744
  return {
1733
1745
  apiPathRoot: `/api/v1/skills/${encodeURIComponent(origin.owner)}/${encodeURIComponent(origin.repo)}/${encodeURIComponent(skill)}`,
@@ -1744,6 +1756,7 @@ export function registerRoutes(router, root, projectName) {
1744
1756
  return { apiPathRoot, origin };
1745
1757
  }
1746
1758
  router.get("/api/skills/:plugin/:skill/versions", async (req, res, params) => {
1759
+ const root = getRoot();
1747
1760
  // 0823: comprehensive origin resolver via shared buildSkillApiPath.
1748
1761
  const { apiPathRoot, origin } = await buildSkillApiPath(params.skill, params.plugin);
1749
1762
  const apiPath = `${apiPathRoot}/versions`;
@@ -1906,6 +1919,7 @@ export function registerRoutes(router, root, projectName) {
1906
1919
  //
1907
1920
  // Idempotent + side-effect free on disk.
1908
1921
  router.post("/api/v1/skills/:id/rescan", async (req, res, params) => {
1922
+ const root = getRoot();
1909
1923
  const rawId = String(params.id ?? "");
1910
1924
  const decoded = (() => {
1911
1925
  try {
@@ -1992,6 +2006,7 @@ export function registerRoutes(router, root, projectName) {
1992
2006
  sendJson(res, { jobId }, 200, req);
1993
2007
  });
1994
2008
  router.post("/api/skills/:plugin/:skill/update", async (req, res, params) => {
2009
+ const root = getRoot();
1995
2010
  initSSE(res, req);
1996
2011
  const skillName = params.skill;
1997
2012
  // 0747 grill F-002: reject before doing anything so a malformed slug
@@ -2071,6 +2086,7 @@ export function registerRoutes(router, root, projectName) {
2071
2086
  // T-012: Batch update SSE + 409 conflict guard
2072
2087
  let batchUpdateInProgress = false;
2073
2088
  router.post("/api/skills/batch-update", async (req, res) => {
2089
+ const root = getRoot();
2074
2090
  if (batchUpdateInProgress) {
2075
2091
  sendJson(res, { error: "Update already in progress" }, 409, req);
2076
2092
  return;
@@ -2139,7 +2155,7 @@ export function registerRoutes(router, root, projectName) {
2139
2155
  const skillFsAllowedRoots = () => {
2140
2156
  const home = homedir();
2141
2157
  return [
2142
- root,
2158
+ getRoot(),
2143
2159
  join(home, ".claude/plugins/cache"),
2144
2160
  join(home, ".claude/plugins/marketplaces"),
2145
2161
  join(home, ".claude/skills"),
@@ -2156,7 +2172,7 @@ export function registerRoutes(router, root, projectName) {
2156
2172
  // path via setSkillDirEntry inside ensurePluginCacheEntry.
2157
2173
  const resolveSkillDirForFsRoute = async (plugin, skill) => {
2158
2174
  await ensurePluginCacheEntry(plugin, skill);
2159
- return resolveAllowedSkillDir(root, plugin, skill, skillFsAllowedRoots());
2175
+ return resolveAllowedSkillDir(getRoot(), plugin, skill, skillFsAllowedRoots());
2160
2176
  };
2161
2177
  // Get skill detail
2162
2178
  router.get("/api/skills/:plugin/:skill", async (req, res, params) => {
@@ -2342,6 +2358,7 @@ export function registerRoutes(router, root, projectName) {
2342
2358
  });
2343
2359
  // Save (create/overwrite) a file inside a skill directory
2344
2360
  router.put("/api/skills/:plugin/:skill/file", async (req, res, params) => {
2361
+ const root = getRoot();
2345
2362
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2346
2363
  if (!resolve(skillDir).startsWith(resolve(root))) {
2347
2364
  sendJson(res, { error: "Invalid skill path" }, 400, req);
@@ -2371,6 +2388,7 @@ export function registerRoutes(router, root, projectName) {
2371
2388
  });
2372
2389
  // Delete a source skill (recursively removes its directory)
2373
2390
  router.delete("/api/skills/:plugin/:skill", async (req, res, params) => {
2391
+ const root = getRoot();
2374
2392
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2375
2393
  // Path containment guard — prevent path traversal via ".." in params
2376
2394
  if (!resolve(skillDir).startsWith(resolve(root))) {
@@ -2411,6 +2429,7 @@ export function registerRoutes(router, root, projectName) {
2411
2429
  // this route is the canonical uninstall path for lockfile-tracked
2412
2430
  // installed skills.
2413
2431
  router.post("/api/skills/:plugin/:skill/uninstall", async (req, res, params) => {
2432
+ const root = getRoot();
2414
2433
  // Skill-name validation — same kebab-case regex used by skill-create
2415
2434
  // routes. Performed BEFORE any filesystem access to defang path-traversal
2416
2435
  // attempts (e.g. `../../etc/passwd`).
@@ -2499,6 +2518,7 @@ export function registerRoutes(router, root, projectName) {
2499
2518
  });
2500
2519
  // Get skill description (for activation testing preview)
2501
2520
  router.get("/api/skills/:plugin/:skill/description", async (req, res, params) => {
2521
+ const root = getRoot();
2502
2522
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2503
2523
  const skillMdPath = join(skillDir, "SKILL.md");
2504
2524
  let skillContent = "";
@@ -2524,6 +2544,7 @@ export function registerRoutes(router, root, projectName) {
2524
2544
  // generic client errors — 422 Unprocessable Entity is the correct semantic
2525
2545
  // for well-formed requests whose payload fails validation.
2526
2546
  router.get("/api/skills/:plugin/:skill/evals", async (req, res, params) => {
2547
+ const root = getRoot();
2527
2548
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2528
2549
  const evalsPath = join(skillDir, "evals", "evals.json");
2529
2550
  if (!existsSync(evalsPath)) {
@@ -2548,6 +2569,7 @@ export function registerRoutes(router, root, projectName) {
2548
2569
  });
2549
2570
  // Save evals.json
2550
2571
  router.put("/api/skills/:plugin/:skill/evals", async (req, res, params) => {
2572
+ const root = getRoot();
2551
2573
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2552
2574
  const body = (await readBody(req));
2553
2575
  // Validate before writing
@@ -2565,6 +2587,7 @@ export function registerRoutes(router, root, projectName) {
2565
2587
  // Generate evals using AI — reads SKILL.md and returns generated EvalsFile
2566
2588
  // Accepts optional { provider, model, testType } in request body
2567
2589
  router.post("/api/skills/:plugin/:skill/generate-evals", async (req, res, params) => {
2590
+ const root = getRoot();
2568
2591
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2569
2592
  const skillMdPath = join(skillDir, "SKILL.md");
2570
2593
  if (!existsSync(skillMdPath)) {
@@ -2674,6 +2697,7 @@ export function registerRoutes(router, root, projectName) {
2674
2697
  });
2675
2698
  // Run benchmark (SSE) — optionally accepts { eval_ids, concurrency, judgeModel, noCache }
2676
2699
  router.post("/api/skills/:plugin/:skill/benchmark", async (req, res, params) => {
2700
+ const root = getRoot();
2677
2701
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2678
2702
  let aborted = false;
2679
2703
  res.on("close", () => { aborted = true; });
@@ -2725,6 +2749,7 @@ export function registerRoutes(router, root, projectName) {
2725
2749
  });
2726
2750
  // Run baseline (SSE) — same as benchmark but without skill content
2727
2751
  router.post("/api/skills/:plugin/:skill/baseline", async (req, res, params) => {
2752
+ const root = getRoot();
2728
2753
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2729
2754
  let aborted = false;
2730
2755
  res.on("close", () => { aborted = true; });
@@ -2748,6 +2773,7 @@ export function registerRoutes(router, root, projectName) {
2748
2773
  });
2749
2774
  // Run single case (SSE) — per-case endpoint with semaphore
2750
2775
  router.post("/api/skills/:plugin/:skill/benchmark/case/:evalId", async (req, res, params) => {
2776
+ const root = getRoot();
2751
2777
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2752
2778
  const evalId = parseInt(params.evalId, 10);
2753
2779
  if (isNaN(evalId)) {
@@ -2823,6 +2849,7 @@ export function registerRoutes(router, root, projectName) {
2823
2849
  });
2824
2850
  // Bulk save — client assembles result from per-case runs and saves as one history entry
2825
2851
  router.post("/api/skills/:plugin/:skill/benchmark/bulk-save", async (req, res, params) => {
2852
+ const root = getRoot();
2826
2853
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2827
2854
  try {
2828
2855
  const body = await readBody(req);
@@ -2841,6 +2868,7 @@ export function registerRoutes(router, root, projectName) {
2841
2868
  });
2842
2869
  // Run comparison (SSE)
2843
2870
  router.post("/api/skills/:plugin/:skill/compare", async (req, res, params) => {
2871
+ const root = getRoot();
2844
2872
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
2845
2873
  let aborted = false;
2846
2874
  res.on("close", () => { aborted = true; });
@@ -3047,6 +3075,7 @@ export function registerRoutes(router, root, projectName) {
3047
3075
  });
3048
3076
  // List benchmark history (with optional filters)
3049
3077
  router.get("/api/skills/:plugin/:skill/history", async (req, res, params) => {
3078
+ const root = getRoot();
3050
3079
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3051
3080
  const url = new URL(req.url, `http://localhost`);
3052
3081
  const filter = {};
@@ -3069,6 +3098,7 @@ export function registerRoutes(router, root, projectName) {
3069
3098
  });
3070
3099
  // Compare two history runs
3071
3100
  router.get("/api/skills/:plugin/:skill/history-compare", async (req, res, params) => {
3101
+ const root = getRoot();
3072
3102
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3073
3103
  const url = new URL(req.url, `http://localhost`);
3074
3104
  const tsA = url.searchParams.get("a");
@@ -3132,6 +3162,7 @@ export function registerRoutes(router, root, projectName) {
3132
3162
  });
3133
3163
  // Per-case history
3134
3164
  router.get("/api/skills/:plugin/:skill/history/case/:evalId", async (req, res, params) => {
3165
+ const root = getRoot();
3135
3166
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3136
3167
  const evalId = parseInt(params.evalId, 10);
3137
3168
  if (isNaN(evalId)) {
@@ -3145,6 +3176,7 @@ export function registerRoutes(router, root, projectName) {
3145
3176
  });
3146
3177
  // Get specific history entry
3147
3178
  router.get("/api/skills/:plugin/:skill/history/:timestamp", async (req, res, params) => {
3179
+ const root = getRoot();
3148
3180
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3149
3181
  const entry = await readHistoryEntry(skillDir, params.timestamp);
3150
3182
  if (!entry) {
@@ -3155,6 +3187,7 @@ export function registerRoutes(router, root, projectName) {
3155
3187
  });
3156
3188
  // Delete history entry
3157
3189
  router.delete("/api/skills/:plugin/:skill/history/:timestamp", async (req, res, params) => {
3190
+ const root = getRoot();
3158
3191
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3159
3192
  const deleted = await deleteHistoryEntry(skillDir, params.timestamp);
3160
3193
  if (!deleted) {
@@ -3165,6 +3198,7 @@ export function registerRoutes(router, root, projectName) {
3165
3198
  });
3166
3199
  // Get aggregated stats
3167
3200
  router.get("/api/skills/:plugin/:skill/stats", async (req, res, params) => {
3201
+ const root = getRoot();
3168
3202
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3169
3203
  const stats = await computeStats(skillDir);
3170
3204
  sendJson(res, stats, 200, req);
@@ -3179,6 +3213,7 @@ export function registerRoutes(router, root, projectName) {
3179
3213
  // Works for any plugin slug (including dashes like `google-workspace`)
3180
3214
  // because routing uses `[^/]+` groups (see router.ts T-020).
3181
3215
  router.get("/api/skills/:plugin/:skill/benchmark/latest", async (req, res, params) => {
3216
+ const root = getRoot();
3182
3217
  // 0704: always 200; body null = no benchmark persisted yet.
3183
3218
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3184
3219
  const benchmark = await readBenchmark(skillDir);
@@ -3186,6 +3221,7 @@ export function registerRoutes(router, root, projectName) {
3186
3221
  });
3187
3222
  // Run activation test (SSE)
3188
3223
  router.post("/api/skills/:plugin/:skill/activation-test", async (req, res, params) => {
3224
+ const root = getRoot();
3189
3225
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3190
3226
  let aborted = false;
3191
3227
  res.on("close", () => { aborted = true; });
@@ -3256,6 +3292,7 @@ export function registerRoutes(router, root, projectName) {
3256
3292
  });
3257
3293
  // GET parsed `## Test Cases` block from SKILL.md (increment 0776)
3258
3294
  router.get("/api/skills/:plugin/:skill/test-cases", (req, res, params) => {
3295
+ const root = getRoot();
3259
3296
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3260
3297
  const skillMdPath = join(skillDir, "SKILL.md");
3261
3298
  const content = existsSync(skillMdPath) ? readFileSync(skillMdPath, "utf-8") : "";
@@ -3266,6 +3303,7 @@ export function registerRoutes(router, root, projectName) {
3266
3303
  // Empty prompts array removes the section. Frontmatter and other body
3267
3304
  // sections are preserved verbatim.
3268
3305
  router.put("/api/skills/:plugin/:skill/test-cases", async (req, res, params) => {
3306
+ const root = getRoot();
3269
3307
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3270
3308
  const skillMdPath = join(skillDir, "SKILL.md");
3271
3309
  const body = (await readBody(req));
@@ -3296,6 +3334,7 @@ export function registerRoutes(router, root, projectName) {
3296
3334
  });
3297
3335
  // AI-generate activation test prompts (SSE)
3298
3336
  router.post("/api/skills/:plugin/:skill/activation-prompts", async (req, res, params) => {
3337
+ const root = getRoot();
3299
3338
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3300
3339
  let aborted = false;
3301
3340
  res.on("close", () => { aborted = true; });
@@ -3361,6 +3400,7 @@ Return ONLY the JSON lines, no other text.`;
3361
3400
  // 500 { error } only for unexpected I/O failures
3362
3401
  // (ENOENT is explicitly not one)
3363
3402
  router.get("/api/skills/:plugin/:skill/activation-history", async (req, res, params) => {
3403
+ const root = getRoot();
3364
3404
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3365
3405
  try {
3366
3406
  const runs = await listActivationRuns(skillDir);
@@ -3379,6 +3419,7 @@ Return ONLY the JSON lines, no other text.`;
3379
3419
  });
3380
3420
  // Get full activation test run by ID
3381
3421
  router.get("/api/skills/:plugin/:skill/activation-history/:runId", async (req, res, params) => {
3422
+ const root = getRoot();
3382
3423
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3383
3424
  const run = await getActivationRun(skillDir, params.runId);
3384
3425
  if (!run) {
@@ -3389,6 +3430,7 @@ Return ONLY the JSON lines, no other text.`;
3389
3430
  });
3390
3431
  // Get skill dependencies (MCP + skill-to-skill)
3391
3432
  router.get("/api/skills/:plugin/:skill/dependencies", async (req, res, params) => {
3433
+ const root = getRoot();
3392
3434
  const skillDir = resolveSkillDir(root, params.plugin, params.skill);
3393
3435
  const skillMdPath = join(skillDir, "SKILL.md");
3394
3436
  if (!existsSync(skillMdPath)) {
@@ -3406,6 +3448,7 @@ Return ONLY the JSON lines, no other text.`;
3406
3448
  // server-side via scanSkillInstallLocations and rejects any non-basename
3407
3449
  // `file`. Spawn is detached + unref'd so the response returns immediately.
3408
3450
  router.post("/api/skills/reveal-in-editor", async (req, res) => {
3451
+ const root = getRoot();
3409
3452
  let body;
3410
3453
  try {
3411
3454
  body = (await readBody(req));
@@ -3536,6 +3579,7 @@ Return ONLY the JSON lines, no other text.`;
3536
3579
  // 500 { error: "clone_failed", message: <stderr tail>, exitCode }
3537
3580
  // -------------------------------------------------------------------------
3538
3581
  router.post("/api/skills/clone", async (req, res) => {
3582
+ const root = getRoot();
3539
3583
  let body;
3540
3584
  try {
3541
3585
  body = (await readBody(req));
@@ -3654,14 +3698,23 @@ Return ONLY the JSON lines, no other text.`;
3654
3698
  stdout: stdout.trim().split("\n").slice(-8).join("\n"),
3655
3699
  }, 200, req);
3656
3700
  });
3657
- // Handle CORS preflight
3701
+ // Handle CORS preflight.
3702
+ //
3703
+ // 0836 US-002 (F-017 follow-up): every authenticated /api/* request now
3704
+ // carries the `X-Studio-Token` custom header. Browser-initiated cross-origin
3705
+ // fetches issue a CORS preflight; the response MUST list `X-Studio-Token`
3706
+ // in `Access-Control-Allow-Headers` or the browser blocks the actual
3707
+ // request. The desktop hot path is same-origin (WebView is navigated to
3708
+ // `http://127.0.0.1:{port}/`) and skips preflight, but CLI users opening
3709
+ // the studio in a regular browser tab — and any future cross-origin
3710
+ // tooling — depend on this echo.
3658
3711
  router.options = (req, res) => {
3659
3712
  const origin = req.headers.origin;
3660
3713
  if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
3661
3714
  res.writeHead(204, {
3662
3715
  "Access-Control-Allow-Origin": origin,
3663
3716
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
3664
- "Access-Control-Allow-Headers": "Content-Type",
3717
+ "Access-Control-Allow-Headers": "Content-Type, X-Studio-Token",
3665
3718
  "Access-Control-Max-Age": "3600",
3666
3719
  });
3667
3720
  }