synabun 2.0.10 → 2.1.0

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 (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -0,0 +1,522 @@
1
+ import { attachSkillOperations } from './operations.js';
2
+ // ── The Skills Service ──
3
+ // One service for everything that reads, drafts, checks, reviews and installs skills: the Studio (HTTP v2,
4
+ // lib/skills/api-v2.js), the MCP `skill` tool (which calls HTTP v2), and the legacy install routes of server.js.
5
+ // It composes the pieces and adds no second copy of their rules:
6
+ // roots lib/skills/roots.js every runtime folder, with its policy
7
+ // catalog lib/skills/catalog.js what those folders hold (async, bounded, cached)
8
+ // library lib/skills/library.js complete folders as immutable revisions, drafts, pointers
9
+ // deployer lib/skills/deploy.js plans and per-target installs, rollback, verify
10
+ // reviews lib/skills/reviews.js the Review queue
11
+ // migration lib/skills/migration.js old bridges → native skill folders
12
+ // Every change it makes is announced (`emit`: skill.changed, revision.created, review.changed,
13
+ // installation.changed, migration.changed), which drops the catalog's cache and reaches the pages as
14
+ // `skills:changed` on the sync socket.
15
+
16
+ import { mkdirSync, readdirSync, statSync } from 'node:fs';
17
+ import { dirname, join, relative, sep } from 'node:path';
18
+ import { PathConfineError, isPlainName, realPathOf } from '../path-confine.js';
19
+ import { readConfinedFile, replaceFileAtomic } from '../confined-fs.js';
20
+ import { collectFolder } from './bundle.js';
21
+ import { folderDigest } from './library.js';
22
+ import { readHeadOf, readSidecar } from './export.js';
23
+ import { lintText, frontmatterData, asText } from './frontmatter.js';
24
+ import { scanArtifact, withExternalScans, SCAN_ENGINE, SCAN_ENGINE_VERSION, SCAN_POLICY } from './scan.js';
25
+ import { authorityDelta } from './reviews.js';
26
+ import { effectiveInstallName, nameChecks } from './deploy.js';
27
+ import { PROFILES, TARGET_PROFILE, lintForProfile, profileList, readInvocationIntent, getProfile } from '../../public/shared/skills/ss-profiles.js';
28
+ import { analyzeResources, argumentDiagnostics } from '../../public/shared/skills/ss-resources.js';
29
+ import { isEditableText } from '../../public/shared/skills/ss-doc.js';
30
+
31
+ export const SERVICE_VERSION = 'skills-service-p5-2026-10-27';
32
+ export const SERVICE_CAPABILITIES = Object.freeze([
33
+ 'roots', 'catalog', 'library', 'revisions', 'drafts', 'diff', 'profiles', 'lint-profiles', 'scan', 'reviews',
34
+ 'install-plan', 'install-native', 'rollback', 'verify', 'bridge-migration', 'shared-root-opt-in', 'events',
35
+ // P2 (Studio 2.0): a catalog skill's files, a revision against an installed folder, exports, when a head file changed,
36
+ // taking a skill out of the library (DELETE /skills/:id, the owner's).
37
+ 'catalog-file', 'compare-folder', 'export', 'catalog-updated', 'library-delete',
38
+ // P3: drafting a skill with AI from sources (/creations, lib/skills/creation.js) and the skill-forge guide (/guide).
39
+ 'ai-create', 'skill-guide',
40
+ // Codex and OpenCode parity (2026-10-23): the automatic move of SynaBun's old bridges at start with its one-time
41
+ // notice and confirmed moves (/migrations/notice, `confirm`), the owner's assistants (config `assistants`), the
42
+ // one-click fix of a runtime setting that turns a skill off (/eligibility/enable; plans carry `eligibility`), the
43
+ // Codex picker words in agents/openai.yaml, and the `panel-reload` refresh kind.
44
+ 'auto-migration', 'assistants-preference', 'eligibility-fix', 'codex-sidecar', 'runtime-refresh',
45
+ // The archived skills (deleted from the library) listed, and put back with their versions (GET /skills/archived,
46
+ // POST /skills/:id/restore).
47
+ 'library-restore',
48
+ // P4: evaluations from the Test tab (/evals, lib/skills/evals.js): task and trigger cases, the description
49
+ // optimiser, results, history, export and the suite import.
50
+ 'evals', 'usage', 'health', 'discover', 'packages', 'external-scanner-status',
51
+ ]);
52
+
53
+ export const ASSISTANT_RUNTIMES = Object.freeze(['claude', 'codex', 'opencode']);
54
+
55
+ export class ServiceError extends PathConfineError {
56
+ constructor(status, message, code, extra = {}) {
57
+ super(status, message);
58
+ this.code = code;
59
+ Object.assign(this, extra);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {object} o
65
+ * @param {string} o.dataDir DATA_HOME/data/skills
66
+ * @param {object} o.roots @param {object} o.catalog @param {object} o.library @param {object} o.deployer
67
+ * @param {object} o.reviews @param {object} o.migration @param {object} o.ownership
68
+ * @param {string|null} [o.bundledDir] PACKAGE_ROOT/skills
69
+ * @param {string} [o.appVersion]
70
+ * @param {(event) => void} [o.broadcast]
71
+ * @param {() => ({ claude?: boolean, codex?: boolean, opencode?: boolean }|null)} [o.installedRuntimes] which CLIs are
72
+ * installed on this computer (server.js: the CLI resolver); null when not known
73
+ * @param {object} [o.runtimeChecks] lib/skills/runtime-checks.js (the owner's enable)
74
+ */
75
+ export function createSkillsService({ dataDir, roots, catalog, library, deployer, reviews, migration, ownership, bundledDir = null, appVersion = '', broadcast = () => {}, installedRuntimes = () => null, runtimeChecks = null, operationsOptions = {}, now = () => new Date().toISOString() }) {
76
+ library.setShippedNames(bundledNames);
77
+ const configPath = join(dataDir, 'config.json');
78
+ let bundledSyncedAt = 0;
79
+
80
+ // ── Configuration ──
81
+
82
+ function readConfigFile() {
83
+ try { return JSON.parse(readConfinedFile(dataDir, configPath, { maxBytes: 64 * 1024, encoding: 'utf-8' })) || {}; } catch { return {}; }
84
+ }
85
+ /** The owner's choices: the shared folder, and the assistants installs go to by default (null: as detected). */
86
+ function getConfig() {
87
+ const parsed = readConfigFile();
88
+ const assistants = Array.isArray(parsed?.assistants) ? [...new Set(parsed.assistants.filter((r) => ASSISTANT_RUNTIMES.includes(r)))] : null;
89
+ return { sharedAgentsRoot: parsed?.sharedAgentsRoot === true, assistants };
90
+ }
91
+ function setConfig(patch = {}) {
92
+ const next = { ...getConfig() };
93
+ if (patch.sharedAgentsRoot !== undefined) {
94
+ if (typeof patch.sharedAgentsRoot !== 'boolean') throw new ServiceError(400, 'sharedAgentsRoot is true or false.', 'invalid_config');
95
+ next.sharedAgentsRoot = patch.sharedAgentsRoot;
96
+ }
97
+ if (patch.assistants !== undefined) {
98
+ if (patch.assistants !== null && !(Array.isArray(patch.assistants) && patch.assistants.every((r) => ASSISTANT_RUNTIMES.includes(r)))) throw new ServiceError(400, `assistants is null (as detected) or a list of ${ASSISTANT_RUNTIMES.join(', ')}.`, 'invalid_config');
99
+ next.assistants = patch.assistants === null ? null : [...new Set(patch.assistants)];
100
+ }
101
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
102
+ replaceFileAtomic(configPath, `${JSON.stringify({ schemaVersion: 1, ...next, updatedAt: now() }, null, 2)}\n`, { mode: 0o600 });
103
+ announce({ type: 'skill.changed', reason: 'config' });
104
+ return next;
105
+ }
106
+
107
+ /**
108
+ * The assistants on this computer: `detected` are the runtimes that are installed (the CLI is found; Claude Code
109
+ * when its config folder is there too) and used (their skills or settings folder exists); `chosen` is the
110
+ * owner's list (Settings → Skills → My assistants), `effective` is chosen ?? detected, never empty (Claude Code).
111
+ */
112
+ function assistantsView() {
113
+ const config = getConfig();
114
+ let installed = null;
115
+ try { installed = installedRuntimes() || null; } catch { installed = null; }
116
+ const used = usedRuntimes();
117
+ const detected = ASSISTANT_RUNTIMES.filter((r) => (installed ? installed[r] !== false : true) && used[r]);
118
+ const fallback = detected.length ? detected : ['claude'];
119
+ return { chosen: config.assistants, detected: fallback, effective: config.assistants && config.assistants.length ? config.assistants : fallback, installed: installed ? ASSISTANT_RUNTIMES.filter((r) => installed[r] === true) : null };
120
+ }
121
+ /** Which runtimes have a home on this computer (their skills folder or its parent exists), from the roots. */
122
+ function usedRuntimes() {
123
+ const out = { claude: false, codex: false, opencode: false };
124
+ let list = [];
125
+ try { list = roots.listRoots(); } catch { list = []; }
126
+ for (const r of list) {
127
+ if (r.scope !== 'user' || !['native', 'legacy-bridge', 'compat', 'system'].includes(r.sourceKind)) continue;
128
+ if (!out[r.family] && (r.status === 'ok' || folderExists(dirname(r.path)))) out[r.family] = true;
129
+ }
130
+ return out;
131
+ }
132
+ function folderExists(path) { try { return statSync(path).isDirectory(); } catch { return false; } }
133
+
134
+ function announce(event) {
135
+ try { catalog.invalidate(); } catch {}
136
+ try { broadcast({ ...event, at: now() }); } catch {}
137
+ }
138
+
139
+ // ── Bundled skills ──
140
+
141
+ /** What PACKAGE_ROOT/skills holds now, each folder read without following a link. */
142
+ function shippedSkills() {
143
+ if (!bundledDir) return [];
144
+ let names = [];
145
+ try { names = readdirSync(bundledDir, { withFileTypes: true }).filter((d) => d.isDirectory() && isPlainName(d.name) && !d.name.startsWith('.')).map((d) => d.name).sort(); } catch { return []; }
146
+ const out = [];
147
+ for (const name of names) {
148
+ try {
149
+ const real = realPathOf(join(bundledDir, name));
150
+ const { files, dirs } = collectFolder(real);
151
+ if (!files.some((f) => f.path === 'SKILL.md')) continue;
152
+ out.push({ name, files, dirs });
153
+ } catch { /* one that cannot be read is skipped */ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** Keep the library's copy of the bundled skills current (a new app version is a new `upstream` revision). */
159
+ function syncBundled({ force = false } = {}) {
160
+ if (!force && Date.now() - bundledSyncedAt < 60_000) return null;
161
+ bundledSyncedAt = Date.now();
162
+ const result = library.syncBundled(shippedSkills(), { appVersion });
163
+ if (result.some((r) => r.changed)) announce({ type: 'revision.created', reason: 'bundled' });
164
+ return result;
165
+ }
166
+
167
+ /** The revision of a bundled skill installs use: its upstream (what this app version ships). */
168
+ function bundledRevision(name) {
169
+ syncBundled();
170
+ const skill = library.findSkill((s) => s.origin?.kind === 'bundled' && s.origin?.name === name);
171
+ if (!skill || !skill.pointers.upstream) return null;
172
+ return library.getRevision(skill.pointers.upstream);
173
+ }
174
+
175
+ // ── Checks ──
176
+
177
+ /** Every check of a folder: the Studio's editing checks, each profile's, the references and the arguments. */
178
+ function lintFolder({ files, dirs = [] }, { profiles = null, folderName = '' } = {}) {
179
+ const { text, parsed } = readHeadOf(files);
180
+ const head = files.find((f) => f.path === 'SKILL.md');
181
+ const binaryHead = head ? !isEditableText(head.bytes) : false;
182
+ const base = lintText({ type: 'skill', text, folderName, binary: binaryHead }).lint;
183
+ const sidecar = readSidecar(files);
184
+ const ids = Array.isArray(profiles) && profiles.length ? profiles.filter((p) => getProfile(p)) : ['portable-six-field', 'claude-code', 'codex-native', 'opencode-native'];
185
+ const byProfile = {};
186
+ for (const id of ids) {
187
+ const lint = lintForProfile(id, { parsed, folderName, files: files.map((f) => ({ path: f.path, size: f.bytes.length })), sidecar });
188
+ const args = argumentDiagnostics(getProfile(id).runtime, { body: parsed.body, bodyLine: parsed.bodyLine, declared: [] });
189
+ byProfile[id] = { profile: lint.profile, errors: lint.errors.length + args.filter((d) => d.severity === 'error').length, warnings: lint.warnings.length + args.filter((d) => d.severity === 'warning').length, diagnostics: [...lint.all, ...args] };
190
+ }
191
+ const resources = analyzeResources({ files: files.map((f) => ({ path: f.path, size: f.bytes.length, text: isEditableText(f.bytes) ? f.bytes.toString('utf-8') : null })), dirs });
192
+ return {
193
+ studio: { profile: base.profile, errors: base.errors.length, warnings: base.warnings.length, diagnostics: base.all },
194
+ profiles: byProfile, resources: { references: resources.references.slice(0, 500), orphans: resources.orphans.slice(0, 200), diagnostics: resources.diagnostics },
195
+ invocation: readInvocationIntent({ data: parsed.data, sidecar }),
196
+ sidecar: sidecar.present ? { present: true, error: sidecar.error } : { present: false },
197
+ };
198
+ }
199
+
200
+ /**
201
+ * The scan of a folder under the name it installs as. With `skill`, the shipped names it must not take are those
202
+ * of every bundled skill but its own (deploy.js nameChecks); without, every bundled name.
203
+ */
204
+ function scanFolder({ files }, { name, digest, skill = null }) {
205
+ const { parsed } = readHeadOf(files);
206
+ const names = nameChecks(skill, name, bundledNames());
207
+ return withExternalScans(scanArtifact({ type: 'skill', name, files, frontmatter: parsed.data, digest, knownNames: names.knownNames, reservedNames: names.reservedNames }), service.scanners?.evidence(digest));
208
+ }
209
+
210
+ /** The name a revision installs under, or null when it has none a folder can take (lint says why). */
211
+ function installNameOf(skill, files) {
212
+ try { return effectiveInstallName(skill, files); } catch { return null; }
213
+ }
214
+
215
+ function bundledNames() {
216
+ if (!bundledDir) return [];
217
+ try { return readdirSync(bundledDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); } catch { return []; }
218
+ }
219
+
220
+ // ── The library, through the service ──
221
+
222
+ // The description a skill's SKILL.md gives, for the Library's list: a revision's never changes (cached by its id);
223
+ // a skill with no revision yet is read from its draft.
224
+ const descriptions = new Map();
225
+ function headDescription(read) {
226
+ try {
227
+ const { bytes } = read();
228
+ return isEditableText(bytes) ? asText(frontmatterData(bytes.toString('utf-8')).data.description).slice(0, 1024) : '';
229
+ } catch { return ''; }
230
+ }
231
+ function revisionDescription(revision) {
232
+ if (!revision) return '';
233
+ if (!descriptions.has(revision.revisionId)) {
234
+ if (descriptions.size > 2000) descriptions.clear();
235
+ descriptions.set(revision.revisionId, headDescription(() => library.readRevisionFile(revision.revisionId, revision.entrypoint || 'SKILL.md')));
236
+ }
237
+ return descriptions.get(revision.revisionId);
238
+ }
239
+ function draftDescription(draftId) {
240
+ if (!draftId) return '';
241
+ return headDescription(() => {
242
+ const head = library.readDraftFiles(draftId).files.find((f) => f.path === 'SKILL.md');
243
+ if (!head) throw new Error('no SKILL.md');
244
+ return { bytes: head.bytes };
245
+ });
246
+ }
247
+
248
+ function skillSummary(skill) {
249
+ let latest = null;
250
+ if (skill.pointers.latest) {
251
+ try { latest = library.readRevision(skill.pointers.latest); }
252
+ catch (err) {
253
+ if (!['blob_missing', 'blob_corrupt', 'blob_invalid', 'revision_corrupt'].includes(err.code)) throw err;
254
+ latest = library.listRevisions(skill.skillId).find((r) => r.revisionId === skill.pointers.latest);
255
+ }
256
+ }
257
+ const installs = deployer.listInstallations({ skillId: skill.skillId, active: true });
258
+ const upstream = skill.pointers.upstream;
259
+ return {
260
+ skillId: skill.skillId, name: skill.name, origin: skill.origin, createdAt: skill.createdAt, updatedAt: skill.updatedAt, etag: skill.etag,
261
+ description: latest ? revisionDescription(latest) : draftDescription(skill.pointers.draft),
262
+ pointers: skill.pointers, revisions: skill.revisions.length,
263
+ latest: latest ? { integrity: latest.integrity, error: latest.error || null, revisionId: latest.revisionId, contentDigest: latest.contentDigest, createdAt: latest.createdAt, author: latest.author } : null,
264
+ installations: installs.map((i) => ({ installationId: i.installationId, runtime: i.target.runtime, scope: i.target.scope, accountId: i.target.accountId, projectPath: i.target.projectPath, destination: i.destination.path, sourceRevisionId: i.sourceRevisionId, updateAvailable: !!upstream && i.sourceRevisionId !== upstream && skill.origin?.kind === 'bundled' })),
265
+ };
266
+ }
267
+
268
+ function listSkills() { syncBundled(); return library.listSkills().map(skillSummary); }
269
+ function getSkill(skillId) { const skill = library.getSkill(skillId); return { ...skillSummary(skill), log: skill.log.slice(-50) }; }
270
+
271
+ function revisionView(revisionId) {
272
+ const rev = library.getRevision(revisionId);
273
+ // A skill taken out of the library leaves its versions readable (a closed review or an old installation names
274
+ // them): with no record, no pointer names them.
275
+ const skill = library.readSkill(rev.skillId);
276
+ return { ...rev, labels: skill ? library.labelsOf(skill, revisionId) : [] };
277
+ }
278
+
279
+ /** Make a catalog entry (a skill folder in a runtime's folder) a library skill: its bytes, as they are now. */
280
+ async function adopt({ artifactId, author }) {
281
+ const entry = await catalog.find(artifactId);
282
+ if (!entry) throw new ServiceError(404, 'There is no such entry in the catalog.', 'artifact_not_found');
283
+ if (entry.kind !== 'skill') throw new ServiceError(400, 'Only a skill folder becomes a library skill.', 'not_a_skill');
284
+ const { files, dirs, excluded } = collectFolder(entry.realPath);
285
+ const existing = library.findSkill((s) => s.origin?.kind === 'adopted' && s.origin?.artifactId === artifactId);
286
+ const source = { kind: 'adopted', path: entry.path, rootId: entry.rootId, artifactId, name: entry.folderName };
287
+ const { parsed } = readHeadOf(files);
288
+ const intent = readInvocationIntent({ data: parsed.data, sidecar: readSidecar(files) });
289
+ if (existing) {
290
+ const latest = existing.pointers.latest ? library.getRevision(existing.pointers.latest) : null;
291
+ const revision = putRevisionIfChanged(existing.skillId, { files, dirs, parentRevisionId: latest?.revisionId || null, source, author, invocationIntent: intent });
292
+ return { skill: skillSummary(library.getSkill(existing.skillId)), revision: revision.revision, created: false, unchanged: revision.unchanged, excluded };
293
+ }
294
+ const created = library.createSkill({ name: entry.folderName, files, dirs, source, author, origin: { kind: 'adopted', artifactId, rootId: entry.rootId, path: entry.path }, invocationIntent: intent, note: 'Adopted from a runtime folder.' });
295
+ announce({ type: 'revision.created', skillId: created.skill.skillId, revisionId: created.revision.revisionId });
296
+ return { skill: skillSummary(created.skill), revision: created.revision, created: true, excluded };
297
+ }
298
+
299
+ /**
300
+ * Take a skill out of the library (the owner's Delete in Skills Studio). Refused for a skill SynaBun ships (403
301
+ * `skill_bundled`: the next sync would bring it back; Customize a copy makes one of the owner's) and for one that is
302
+ * installed somewhere (409 `skill_installed`, naming the installations: they are removed first, from Publish, each
303
+ * with its backup). `expectedEtag` is the record the owner read (409 `skill_conflict` when it changed since). Its
304
+ * record and draft move to the library's archive (library.js archiveSkill); its open reviews are withdrawn, by the
305
+ * owner, since nothing is left to approve or install. No folder a runtime reads is touched.
306
+ */
307
+ function removeSkill(skillId, { expectedEtag = null, by = { kind: 'owner' } } = {}) {
308
+ const skill = library.getSkill(skillId);
309
+ if (service.operations.settings().protectedSkillIds.includes(skillId)) throw new ServiceError(409, 'This skill is protected from retirement.', 'skill_protected');
310
+ if (skill.origin?.kind === 'bundled') throw new ServiceError(403, 'A skill SynaBun ships stays in the library: customize a copy to have one of your own.', 'skill_bundled');
311
+ const installs = deployer.listInstallations({ skillId, active: true });
312
+ if (installs.length) {
313
+ throw new ServiceError(409, `It is installed in ${installs.length === 1 ? '1 place' : `${installs.length} places`}: remove ${installs.length === 1 ? 'that installation' : 'those installations'} first.`, 'skill_installed', {
314
+ installations: installs.map((i) => ({ installationId: i.installationId, runtime: i.target.runtime, scope: i.target.scope, accountId: i.target.accountId, projectPath: i.target.projectPath || null, destination: i.destination.path })),
315
+ });
316
+ }
317
+ const out = library.archiveSkill(skillId, { expectedEtag, by: by?.kind || 'owner', reason: 'deleted from the library' });
318
+ const withdrawn = [];
319
+ for (const review of reviews.list({ skillId, open: true })) {
320
+ try { reviews.withdraw(review.reviewId, { by }); withdrawn.push(review.reviewId); } catch { /* one that cannot be closed stays as it is */ }
321
+ }
322
+ announce({ type: 'skill.changed', skillId, action: 'deleted' });
323
+ return { ...out, withdrawnReviews: withdrawn };
324
+ }
325
+
326
+ /**
327
+ * The skills deleted from the library, newest first (library.js listArchived), with how many versions each keeps:
328
+ * one entry per skill (its newest deletion, the one Restore brings back), none for a skill that is in the library.
329
+ */
330
+ function listArchived() {
331
+ const seen = new Set();
332
+ return library.listArchived().filter((a) => {
333
+ if (seen.has(a.skillId)) return false;
334
+ seen.add(a.skillId);
335
+ return !library.readSkill(a.skillId);
336
+ });
337
+ }
338
+
339
+ /** Put a deleted skill back in the library as it was (the owner's Restore in the Library's Archived view). */
340
+ function restoreSkill(skillId) {
341
+ const out = library.restoreSkill(skillId);
342
+ announce({ type: 'skill.changed', skillId, action: 'restored' });
343
+ return { ...out, skill: skillSummary(library.getSkill(skillId)) };
344
+ }
345
+
346
+ function putRevisionIfChanged(skillId, opts) {
347
+ const skill = library.getSkill(skillId);
348
+ if (skill.pointers.latest) {
349
+ const latest = library.getRevision(skill.pointers.latest);
350
+ const { files, dirs } = opts;
351
+ const digest = folderDigest({ files, dirs });
352
+ if (latest.contentDigest === digest) return { revision: latest, unchanged: true };
353
+ }
354
+ const revision = library.putRevision(skillId, opts);
355
+ announce({ type: 'revision.created', skillId, revisionId: revision.revisionId });
356
+ return { revision, unchanged: false };
357
+ }
358
+
359
+ // ── Reviews ──
360
+
361
+ function reviewScanEvidence(scan) {
362
+ return { status: scan.status, gate: scan.gate, digest: scan.digest, counts: scan.counts,
363
+ engine: scan.engine, engineVersion: scan.engineVersion, policy: scan.policy,
364
+ // Already redacted by the scanner. Keep every field the text renderer needs.
365
+ findings: scan.findings.slice(0, 10) };
366
+ }
367
+
368
+ function currentReview(rec) {
369
+ const old = rec.evidence?.scan;
370
+ if (old?.engineVersion === SCAN_ENGINE_VERSION && old?.policy === SCAN_POLICY) return rec;
371
+ let scan;
372
+ try {
373
+ const { files, revision } = library.readRevisionFiles(rec.submittedRevisionId);
374
+ const skill = library.getSkill(revision.skillId);
375
+ scan = scanFolder({ files }, { name: installNameOf(skill, files) || skill.name, digest: revision.contentDigest, skill });
376
+ } catch (err) {
377
+ // One damaged submission must not crash the entire Review page or keep a stale pass.
378
+ scan = { engine: SCAN_ENGINE, engineVersion: SCAN_ENGINE_VERSION, policy: SCAN_POLICY,
379
+ status: 'error', gate: 'blocked', digest: rec.submittedDigest, counts: {}, findings: [] };
380
+ }
381
+ // Submission evidence stays historical on disk; reads show the current scanner's answer.
382
+ return { ...rec, evidence: { ...rec.evidence, scan: reviewScanEvidence(scan) } };
383
+ }
384
+
385
+ function submitReview({ revisionId, targets = [], releaseNotes = '', assumptions = [], author, previousReviewId = null }) {
386
+ const { files, dirs, revision } = library.readRevisionFiles(revisionId);
387
+ const skill = library.getSkill(revision.skillId);
388
+ const profiles = [...new Set((targets || []).map((t) => (t?.runtime === 'agents' ? 'portable-six-field' : TARGET_PROFILE[t?.runtime])).filter(Boolean))];
389
+ // Checked under the name it installs as (its SKILL.md's), not the library's: that is the folder it would take.
390
+ const installName = installNameOf(skill, files);
391
+ const lint = lintFolder({ files, dirs }, { profiles: profiles.length ? profiles : null, folderName: installName || skill.name });
392
+ const scan = scanFolder({ files }, { name: installName || skill.name, digest: revision.contentDigest, skill });
393
+ let delta = null;
394
+ if (skill.pointers.approved && skill.pointers.approved !== revisionId) {
395
+ const base = library.readRevisionFiles(skill.pointers.approved);
396
+ const baseScan = scanFolder({ files: base.files }, { name: installNameOf(skill, base.files) || skill.name, digest: base.revision.contentDigest, skill });
397
+ delta = authorityDelta(baseScan.authority, scan.authority);
398
+ } else delta = authorityDelta({}, scan.authority);
399
+ const diff = skill.pointers.approved && skill.pointers.approved !== revisionId ? library.diffRevisions(skill.pointers.approved, revisionId) : null;
400
+ const evidence = {
401
+ lint: { studioErrors: lint.studio.errors, profiles: Object.fromEntries(Object.entries(lint.profiles).map(([id, p]) => [id, { errors: p.errors, warnings: p.warnings }])), resourceProblems: lint.resources.diagnostics.length },
402
+ scan: reviewScanEvidence(scan),
403
+ installName,
404
+ authority: delta,
405
+ diff: diff ? { from: diff.from, to: diff.to, files: diff.files.map((f) => ({ path: f.path, status: f.status })) } : null,
406
+ invocation: lint.invocation,
407
+ };
408
+ const res = reviews.submit({ revisionId, targets, releaseNotes, assumptions, author, evidence, previousReviewId });
409
+ return { ...res, review: currentReview(res.review), scan, lint };
410
+ }
411
+
412
+ function decideReview(reviewId, { decision, reason, by }) {
413
+ const rec = reviews.get(reviewId);
414
+ let scan = null;
415
+ let installName = null;
416
+ if (decision === 'approve') {
417
+ const { files, revision } = library.readRevisionFiles(rec.submittedRevisionId);
418
+ const skill = library.getSkill(revision.skillId);
419
+ installName = installNameOf(skill, files);
420
+ scan = scanFolder({ files }, { name: installName || skill.name, digest: revision.contentDigest, skill });
421
+ }
422
+ return reviews.decide(reviewId, { decision, reason, by, scan, installName });
423
+ }
424
+
425
+ // ── Installs ──
426
+
427
+ async function installPlan({ revisionId, targets }) {
428
+ return deployer.plan({ revisionId, targets });
429
+ }
430
+
431
+ async function install({ revisionId, targets, planDigest, decisions, principal, reviewId = null }) {
432
+ if (reviewId) {
433
+ const rec = reviews.get(reviewId);
434
+ if (rec.submittedRevisionId !== revisionId) throw new ServiceError(400, 'That review is of another revision.', 'review_mismatch');
435
+ if (!['approved', 'publishing', 'published', 'partial', 'publish_failed'].includes(rec.state)) throw new ServiceError(409, 'Only an approved review is installed.', 'review_state');
436
+ }
437
+ const result = await deployer.install({ revisionId, targets, planDigest, decisions, principal, reviewId });
438
+ if (reviewId) { try { reviews.recordPublication(reviewId, { results: result.results }); } catch {} }
439
+ announce({ type: 'installation.changed', revisionId, results: result.results.map((r) => ({ targetKey: r.targetKey, status: r.status })) });
440
+ return result;
441
+ }
442
+
443
+ // ── Status ──
444
+
445
+ async function status() {
446
+ const installs = deployer.listInstallations({ active: true });
447
+ const pending = reviews.list({ open: true });
448
+ let bridges = [];
449
+ try { bridges = migration.findBridges(); } catch { bridges = []; }
450
+ const updates = [];
451
+ for (const i of installs) {
452
+ const skill = library.readSkill(i.skillId);
453
+ if (skill?.origin?.kind === 'bundled' && skill.pointers.upstream && skill.pointers.upstream !== i.sourceRevisionId) updates.push({ installationId: i.installationId, skillId: i.skillId, name: skill.name, from: i.sourceRevisionId, to: skill.pointers.upstream });
454
+ }
455
+ return {
456
+ version: SERVICE_VERSION, capabilities: SERVICE_CAPABILITIES, config: getConfig(), assistants: assistantsView(),
457
+ installations: installs.length, reviewsOpen: pending.length,
458
+ bridges: { total: bridges.length, synabun: bridges.filter((b) => b.proof !== 'none').length, other: bridges.filter((b) => b.proof === 'none').length },
459
+ updatesAvailable: updates, library: library.stats(),
460
+ };
461
+ }
462
+
463
+ // ── The automatic move of the old bridges, and the one-click eligibility fix ──
464
+
465
+ /** The server's one run at start (lib/skills/migration.js autoMigrate): announced when something moved. */
466
+ async function autoMigrateBridges() {
467
+ syncBundled();
468
+ const out = await migration.autoMigrate();
469
+ if (out.ran) announce({ type: 'migration.changed', journalId: out.result?.journalId || null, auto: true });
470
+ return out;
471
+ }
472
+
473
+ /**
474
+ * The owner pressed "Enable for <runtime>": the runtime's setting that turns the skill off is changed, over a
475
+ * backup, and the answer says what changed (lib/skills/runtime-checks.js enable).
476
+ */
477
+ function enableEligibility({ runtime, name, skillMd, accountId = 'default', projectPath = null, confirmDenyAll = false }) {
478
+ if (!['claude', 'codex', 'opencode'].includes(runtime)) throw new ServiceError(400, 'runtime is claude, codex or opencode.', 'invalid_target');
479
+ if (typeof name !== 'string' || !isPlainName(name)) throw new ServiceError(400, 'name is a skill\'s folder name.', 'invalid_name');
480
+ if (typeof skillMd !== 'string' || !skillMd || !/[\\/]SKILL\.md$/.test(skillMd)) throw new ServiceError(400, 'skillMd is the path of the skill\'s SKILL.md.', 'invalid_path');
481
+ // Only a skill at a place SynaBun installs to (one of its install roots, or a folder a runtime reads): never an
482
+ // arbitrary path of the request.
483
+ const inRoot = roots.listRoots().some((r) => r.kind === 'skills' && relative(r.path, skillMd).split(sep).join('/') === `${name}/SKILL.md`);
484
+ if (!inRoot) throw new ServiceError(403, 'That SKILL.md is not in a folder SynaBun manages.', 'outside_roots');
485
+ if (typeof accountId !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/.test(accountId)) throw new ServiceError(400, 'Not an account id.', 'invalid_target');
486
+ if (!runtimeChecks || typeof runtimeChecks.enable !== 'function') throw new ServiceError(501, 'This server cannot change a runtime\'s settings.', 'not_supported');
487
+ const out = runtimeChecks.enable({ runtime, name, skillMd, accountId, projectPath: typeof projectPath === 'string' && projectPath ? projectPath : null, confirmDenyAll: confirmDenyAll === true });
488
+ announce({ type: 'skill.changed', reason: 'eligibility', runtime, name });
489
+ return out;
490
+ }
491
+
492
+ /** The legacy chips' view of one bundled skill: what each runtime has of it (native folder, old bridge, neither). */
493
+ function legacyTargetStates(name) {
494
+ const out = {};
495
+ for (const runtime of ['claude', 'codex', 'opencode']) {
496
+ const root = roots.installRoot({ runtime, scope: 'user', accountId: 'default' });
497
+ const native = root ? deployer.activeAt(join(root.path, name)) : null;
498
+ let nativeState = 'absent';
499
+ if (root) {
500
+ const inspected = ownership.inspect({ destination: join(root.path, name) });
501
+ nativeState = inspected.state === 'absent' ? 'absent' : (native && inspected.current.digest === native.export.artifactDigest ? 'native' : (inspected.state === 'owned' ? 'native' : (inspected.state === 'modified' ? 'modified' : 'other')));
502
+ }
503
+ out[runtime] = { native: nativeState, installationId: native?.installationId || null, destination: root ? join(root.path, name) : null };
504
+ }
505
+ return out;
506
+ }
507
+
508
+ const service = {
509
+ version: SERVICE_VERSION, capabilities: SERVICE_CAPABILITIES,
510
+ getConfig, setConfig, assistantsView, announce,
511
+ roots: () => roots.listRoots(), installRoot: (t) => roots.installRoot(t),
512
+ catalogPage: (q) => catalog.page(q), catalogDetail: (id) => catalog.detail(id), catalogFind: (id) => catalog.find(id), catalogScan: (o) => catalog.scan(o),
513
+ profiles: () => profileList(), profileIds: () => Object.keys(PROFILES),
514
+ syncBundled, bundledRevision, shippedSkills,
515
+ listSkills, getSkill, revisionView, adopt, removeSkill, listArchived, restoreSkill, lintFolder, scanFolder,
516
+ library, deployer, reviews: { ...reviews, get: (id) => currentReview(reviews.get(id)), list: (opts) => reviews.list(opts).map(currentReview) }, migration,
517
+ submitReview, decideReview, installPlan, install, status, legacyTargetStates,
518
+ autoMigrateBridges, enableEligibility,
519
+ };
520
+ return attachSkillOperations(service, { dataDir, ...operationsOptions });
521
+ }
522
+
@@ -0,0 +1,113 @@
1
+ // Minimal local evidence, from trusted runtime events only. No prompts, commands, paths, or outputs are stored.
2
+ import { createHash } from 'node:crypto';
3
+ import { join, resolve, sep } from 'node:path';
4
+ import { readManifest } from './digest.js';
5
+ import { toolCallsOf, initOf } from './eval-trace.js';
6
+ import { localRecord } from './local-store.js';
7
+
8
+ export const USAGE_COVERAGE = Object.freeze({
9
+ claude: { native: true, method: 'Skill tool request + successful tool result', limit: 'SynaBun dispatched runs only. Discovery is not invocation; task outcome is not skill quality.' },
10
+ codex: { native: false, method: 'Successful exact cat of an installed SKILL.md', limit: 'No native invocation event. File reads are inferred loads, never invocation counts. Complex shell commands are not classified.' },
11
+ opencode: { native: true, method: 'Completed skill tool part', limit: 'SynaBun dispatched runs only. Other terminals and historical transcripts are not mined.' },
12
+ });
13
+ const hash = (s) => createHash('sha256').update(s).digest('hex');
14
+ // Native item IDs can restart on a follow-up turn; the dispatcher retains this turn across restarts.
15
+ const turnOf = (run) => Number(run.usageState?.turn?.n || run.resumeInfo?.inFlight?.turn || 1);
16
+
17
+ export function createSkillTelemetry({ dataDir, deployer, settings, now = Date.now }) {
18
+ const store = localRecord(dataDir, 'usage', { events: [] });
19
+ const pending = new Map();
20
+ const trim = (events) => events.filter((e) => e.at >= now() - settings().retentionDays * 86400000).slice(-10000);
21
+ function read() {
22
+ const previous = store.read().events || [];
23
+ const events = trim(previous);
24
+ if (events.length !== previous.length) store.write({ events });
25
+ return events;
26
+ }
27
+ function add(run, inst, kind, method, eventId, strength = 'measured') {
28
+ if (!eventId) return;
29
+ const runtime = run.provider === 'claude-code' ? 'claude' : run.provider;
30
+ const key = hash(`${run.runId}\0${turnOf(run)}\0${eventId}\0${inst.installationId}\0${kind}`);
31
+ const events = read();
32
+ if (events.some((e) => e.key === key)) return;
33
+ // Only attribute bytes that still match the installed export. Drift is health evidence, not approved usage.
34
+ if (readManifest(inst.destination.path).digest !== inst.export.artifactDigest) return;
35
+ events.push({ key, at: now(), turn: turnOf(run), runRef: hash(String(run.runId)), installationId: inst.installationId,
36
+ skillId: inst.skillId, revisionId: inst.sourceRevisionId, exportDigest: inst.export.artifactDigest,
37
+ runtime, kind, method, strength, outcome: null, costUsd: null });
38
+ store.write({ events: trim(events) });
39
+ }
40
+ function eligible(run) {
41
+ const runtime = run.provider === 'claude-code' ? 'claude' : run.provider;
42
+ return deployer.listInstallations({ active: true }).filter((i) => i.destination.affectedRuntimes.includes(runtime)
43
+ && (i.target.accountId || 'default') === (run.accountId || 'default')
44
+ && (!i.target.projectPath || (run.cwd && (resolve(run.cwd) === resolve(i.target.projectPath) || resolve(run.cwd).startsWith(`${resolve(i.target.projectPath)}${sep}`)))));
45
+ }
46
+ function observe(run, payload) {
47
+ if (!settings().telemetryEnabled || !run?.runId || run.skillEval || run.skillCreation || !USAGE_COVERAGE[run.provider === 'claude-code' ? 'claude' : run.provider]) return;
48
+ const calls = toolCallsOf(payload);
49
+ const init = initOf(payload);
50
+ const results = run.provider === 'claude-code' && payload?.event?.type === 'user' && Array.isArray(payload.event.message?.content) ? payload.event.message.content.filter((b) => b?.type === 'tool_result') : [];
51
+ if (!calls.length && !init?.skills?.length && !results.length) return;
52
+ const installs = eligible(run);
53
+ const unique = (name) => { const found = installs.filter((i) => i.destination.relativePath === name); return found.length === 1 ? found[0] : null; };
54
+ for (const name of init?.skills || []) { const i = unique(name); if (i) add(run, i, 'discovered', 'runtime_catalog', `init:${name}`); }
55
+ for (const c of calls) {
56
+ if (!c.id || c.sub) continue;
57
+ if ((run.provider === 'claude-code' && c.tool === 'Skill') || (run.provider === 'opencode' && c.tool === 'skill')) {
58
+ const name = c.input.skill ?? c.input.id ?? c.input.name;
59
+ const i = typeof name === 'string' ? unique(name) : null;
60
+ if (!i) continue;
61
+ if (run.provider === 'claude-code') {
62
+ add(run, i, 'requested', 'native_skill_tool', c.id);
63
+ pending.set(`${run.runId}:${turnOf(run)}:${c.id}`, i);
64
+ if (pending.size > 2000) pending.delete(pending.keys().next().value);
65
+ } else if (c.ok === true) add(run, i, 'invoked', 'native_skill_tool', c.id);
66
+ else if (c.ok === false) add(run, i, 'failed', 'native_skill_tool', c.id);
67
+ }
68
+ if (run.provider === 'codex' && c.tool === 'shell' && c.phase === 'done' && c.ok === true) {
69
+ // A conservative grammar: no pipes, substitutions, multiple commands, or merely mentioning a path.
70
+ let command = c.input.command || '';
71
+ const wrapper = /^\/bin\/(?:bash|zsh|sh) -lc (['"])([\s\S]*)\1$/.exec(command);
72
+ if (wrapper) command = wrapper[2];
73
+ const m = /^(?:\/bin\/)?cat\s+(?:--\s+)?(?:"([^"$`\n]+)"|'([^'\n]+)'|(\/[^\s;&|<>$`]+))\s*$/.exec(command);
74
+ if (!m) continue;
75
+ const path = m[1] || m[2] || m[3];
76
+ const matches = installs.filter((i) => path === join(i.destination.path, 'SKILL.md'));
77
+ if (matches.length === 1) add(run, matches[0], 'loaded', 'successful_skill_file_read', c.id, 'inferred');
78
+ }
79
+ }
80
+ if (run.provider === 'claude-code' && payload?.event?.type === 'user') {
81
+ for (const block of results) {
82
+ if (block?.type !== 'tool_result') continue;
83
+ const key = `${run.runId}:${turnOf(run)}:${block.tool_use_id}`;
84
+ const inst = pending.get(key);
85
+ if (!inst) continue;
86
+ pending.delete(key);
87
+ add(run, inst, block.is_error ? 'failed' : 'invoked', 'native_skill_tool', block.tool_use_id);
88
+ }
89
+ }
90
+ }
91
+ function complete(run) {
92
+ const ref = hash(String(run.runId));
93
+ for (const key of pending.keys()) if (key.startsWith(`${run.runId}:${turnOf(run)}:`)) pending.delete(key);
94
+ const events = read();
95
+ let changed = false;
96
+ for (const e of events) if (e.runRef === ref && (e.turn || 1) === turnOf(run) && e.outcome === null) {
97
+ e.outcome = ['done', 'blocked', 'needs_input'].includes(run.lastResult?.status) ? run.lastResult.status : null;
98
+ e.outcomeStrength = 'declared-run-result'; changed = true;
99
+ }
100
+ if (changed) store.write({ events });
101
+ }
102
+ function summary({ skillId = null } = {}) {
103
+ const events = read().filter((e) => !skillId || e.skillId === skillId);
104
+ const groups = new Map();
105
+ for (const e of events) {
106
+ const key = `${e.revisionId}:${e.runtime}`;
107
+ if (!groups.has(key)) groups.set(key, { skillId: e.skillId, revisionId: e.revisionId, runtime: e.runtime, discovered: 0, requested: 0, invoked: 0, loaded: 0, failed: 0, lastAt: e.at, costUsd: null });
108
+ const g = groups.get(key); g[e.kind]++; g.lastAt = Math.max(g.lastAt, e.at);
109
+ }
110
+ return { localOnly: true, enabled: settings().telemetryEnabled, retentionDays: settings().retentionDays, maxEvents: 10000, coverage: USAGE_COVERAGE, rows: [...groups.values()], events: events.slice(-100) };
111
+ }
112
+ return { observe, complete, summary, prune: read, clear() { pending.clear(); store.write({ events: [] }); } };
113
+ }