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,355 @@
1
+ // ── What a runtime says about an installed skill ──
2
+ // An install writes bytes; whether a runtime offers the skill is another question, asked two ways:
3
+ //
4
+ // eligibility read from the runtime's own settings, without starting it: Codex's config.toml
5
+ // ([[skills.config]] path = …, enabled = false), Claude Code's settings.json (skillOverrides
6
+ // "off", a deny rule on Skill(name)), OpenCode's opencode.json and opencode.jsonc (the last rule of
7
+ // action `skill` whose resource matches the name: OpenCode 2's `permissions`, an OpenCode 1
8
+ // `permission.skill` read as OpenCode reads it). 'enabled', 'disabled', or 'unknown' when the
9
+ // settings cannot be read.
10
+ // probe asked of the runtime itself, only when the owner asks (Verify in the Studio): Codex lists its
11
+ // skills through `codex app-server` (skills/list, forceReload); OpenCode through SynaBun's own
12
+ // running serve. Claude Code has no listing to ask: it says null with the reason. A probe reads; it
13
+ // changes no setting.
14
+ // enable the one write here, on the owner's click only (POST /api/skills-studio/v2/eligibility/enable):
15
+ // the setting that turns the skill off is changed to let it through, the file is backed up first
16
+ // (DATA_HOME/data/skills/backups/settings/), and the answer names the file, the backup and the lines
17
+ // that changed. Codex: that [[skills.config]] table's `enabled = false` line becomes `enabled = true`
18
+ // (the rest of config.toml byte for byte). OpenCode: `{ "action": "skill", "resource": "<name>",
19
+ // "effect": "allow" }` is added as the last rule of opencode.json's `permissions` (a deny in a .jsonc
20
+ // file is left to the owner, with the rule to add; a deny of every skill is refused unless the owner
21
+ // confirmed loosening it, `confirmDenyAll`).
22
+ // Claude Code: the skillOverrides entry or the Skill(name) deny rule is removed (a deny of every skill
23
+ // is never loosened). A JSON file is changed in one value only, byte for byte otherwise
24
+ // (lib/skills/json-edit.js): never reformatted. Nothing is changed silently.
25
+
26
+ import { spawn } from 'node:child_process';
27
+ import { closeSync, constants, copyFileSync, fstatSync, mkdirSync, openSync, readSync, statSync } from 'node:fs';
28
+ import { basename, join, resolve } from 'node:path';
29
+ import { replaceFileAtomic } from '../confined-fs.js';
30
+ import { parseJsonWithOffsets, nodeAt, appendMember, appendElement, removeMember, removeElement } from './json-edit.js';
31
+ import { OPENCODE_CONFIG_FILES, parseJsonc, permissionRulesOf } from '../opencode-config.js';
32
+
33
+ const MAX_SETTINGS_BYTES = 1024 * 1024;
34
+
35
+ /**
36
+ * A settings file, read bounded: `{ state: 'absent' }` when there is none, `{ state: 'ok', text }`, or
37
+ * `{ state: 'unreadable' }` when it is there but cannot be read whole (over the bound, not a regular file, no
38
+ * permission). The size is known before a byte is read; a FIFO or a device is never opened for reading
39
+ * (O_NONBLOCK, and the open file is checked again before the read).
40
+ */
41
+ export function readSettingsFile(path, { maxBytes = MAX_SETTINGS_BYTES } = {}) {
42
+ let st;
43
+ try { st = statSync(path); } catch (err) { return err?.code === 'ENOENT' || err?.code === 'ENOTDIR' ? { state: 'absent' } : { state: 'unreadable' }; }
44
+ if (!st.isFile() || st.size > maxBytes) return { state: 'unreadable' };
45
+ let fd = null;
46
+ try {
47
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK || 0));
48
+ const open = fstatSync(fd);
49
+ if (!open.isFile() || open.size > maxBytes) return { state: 'unreadable' };
50
+ const buf = Buffer.alloc(open.size);
51
+ let got = 0;
52
+ while (got < buf.length) {
53
+ const n = readSync(fd, buf, got, buf.length - got, got);
54
+ if (n <= 0) break;
55
+ got += n;
56
+ }
57
+ return { state: 'ok', text: buf.subarray(0, got).toString('utf-8') };
58
+ } catch { return { state: 'unreadable' }; }
59
+ finally { if (fd !== null) { try { closeSync(fd); } catch { /* closed */ } } }
60
+ }
61
+
62
+ /** The [[skills.config]] tables of a Codex config.toml: `{ path, name, enabled }` each (a reading of those lines only). */
63
+ export function codexSkillConfigs(toml) {
64
+ const out = [];
65
+ let current = null;
66
+ for (const raw of String(toml || '').split(/\r?\n/)) {
67
+ const line = raw.replace(/\s+#.*$/, '').trim();
68
+ if (!line) continue;
69
+ if (/^\[\[\s*skills\.config\s*\]\]$/.test(line)) { current = { path: null, name: null, enabled: null }; out.push(current); continue; }
70
+ if (/^\[/.test(line)) { current = null; continue; }
71
+ if (!current) continue;
72
+ const kv = /^([A-Za-z_]+)\s*=\s*(.+)$/.exec(line);
73
+ if (!kv) continue;
74
+ const [, key, value] = kv;
75
+ const str = /^"((?:[^"\\]|\\.)*)"$/.exec(value)?.[1]?.replace(/\\(["\\])/g, '$1') ?? /^'([^']*)'$/.exec(value)?.[1] ?? null;
76
+ if (key === 'path' && str !== null) current.path = str;
77
+ else if (key === 'name' && str !== null) current.name = str;
78
+ else if (key === 'enabled' && /^(true|false)$/.test(value)) current.enabled = value === 'true';
79
+ }
80
+ return out;
81
+ }
82
+
83
+ const skillPatternMatches = (pattern, name) => new RegExp(`^${String(pattern).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')}$`).test(name);
84
+
85
+ /**
86
+ * The last rule of a config that decides skill `name`: `{ effect: 'allow' | 'ask' | 'deny', resource }` of the last
87
+ * rule of action `skill` whose resource matches it (OpenCode 2's `permissions`, after an OpenCode 1
88
+ * `permission.skill` converted), or null when none does.
89
+ */
90
+ export function opencodeSkillRule(config, name) {
91
+ let found = null;
92
+ for (const rule of permissionRulesOf(config)) {
93
+ if (rule.action === 'skill' && skillPatternMatches(rule.resource, name)) found = { effect: rule.effect, resource: rule.resource };
94
+ }
95
+ return found;
96
+ }
97
+
98
+ /** What a config decides for skill `name` ('allow' | 'ask' | 'deny' | null): opencodeSkillRule's effect. */
99
+ export function opencodeSkillPermission(config, name) {
100
+ return opencodeSkillRule(config, name)?.effect ?? null;
101
+ }
102
+
103
+ /**
104
+ * @param {object} o
105
+ * @param {(accountId: string) => string} o.codexHome
106
+ * @param {(accountId: string) => string} o.claudeHome
107
+ * @param {() => string} o.opencodeConfigDir <config>/opencode
108
+ * @param {() => string|null} [o.codexBin]
109
+ * @param {(accountId: string) => object} [o.codexEnv] the environment Codex runs under for that account
110
+ * @param {() => object|null} [o.opencodeClient] SynaBun's running OpenCode serve client, when there is one
111
+ */
112
+ export function createSkillsRuntimeChecks({ codexHome, claudeHome, opencodeConfigDir, codexBin = () => null, codexEnv = () => process.env, opencodeClient = () => null, spawnImpl = spawn, timeoutMs = 20000, backupDir = null } = {}) {
113
+ /**
114
+ * Whether the runtime's settings let the skill through, with the setting that does not: `{ state, reason }`.
115
+ * `state` is 'enabled', 'disabled' or 'unknown'; `reason` names the file and the rule only when it is 'disabled'
116
+ * (`kind`: codex-config, claude-override, claude-deny, opencode-permission), or why nothing could be read.
117
+ */
118
+ function eligibilityDetail({ runtime, name, skillMd, accountId = 'default', projectPath = null }) {
119
+ if (runtime === 'codex') {
120
+ const file = join(codexHome(accountId), 'config.toml');
121
+ const read = readSettingsFile(file);
122
+ if (read.state === 'absent') return { state: 'enabled', reason: null }; // no config.toml: nothing turns a skill off
123
+ if (read.state !== 'ok') return { state: 'unknown', reason: { kind: 'unreadable', file } }; // there, but not read whole: it may turn it off
124
+ const tables = codexSkillConfigs(read.text);
125
+ const hit = tables.filter((t) => (t.path && resolve(t.path) === resolve(skillMd)) || (t.name && t.name === name));
126
+ if (!hit.length) return { state: 'enabled', reason: null };
127
+ const last = hit[hit.length - 1];
128
+ return last.enabled === false ? { state: 'disabled', reason: { kind: 'codex-config', file, table: { path: last.path, name: last.name } } } : { state: 'enabled', reason: null };
129
+ }
130
+ if (runtime === 'claude') {
131
+ const files = [join(claudeHome(accountId), 'settings.json')];
132
+ if (projectPath) files.push(join(projectPath, '.claude', 'settings.json'), join(projectPath, '.claude', 'settings.local.json'));
133
+ let unknown = null;
134
+ for (const file of files) {
135
+ const read = readSettingsFile(file);
136
+ if (read.state === 'absent') continue;
137
+ if (read.state !== 'ok') { unknown = { kind: 'unreadable', file }; continue; }
138
+ let data;
139
+ try { data = JSON.parse(read.text); } catch { unknown = { kind: 'invalid-json', file }; continue; }
140
+ if (data?.skillOverrides && data.skillOverrides[name] === 'off') return { state: 'disabled', reason: { kind: 'claude-override', file, key: name } };
141
+ const deny = Array.isArray(data?.permissions?.deny) ? data.permissions.deny : [];
142
+ const rule = deny.find((r) => r === 'Skill' || r === `Skill(${name})` || r === `Skill(${name} *)`);
143
+ if (rule) return { state: 'disabled', reason: { kind: 'claude-deny', file, rule } };
144
+ }
145
+ return unknown ? { state: 'unknown', reason: unknown } : { state: 'enabled', reason: null };
146
+ }
147
+ if (runtime === 'opencode') {
148
+ // OpenCode 2 reads opencode.json, then opencode.jsonc over it: the last matching rule of both decides.
149
+ let decided = null;
150
+ for (const file of OPENCODE_CONFIG_FILES) {
151
+ const full = join(opencodeConfigDir(), file);
152
+ const read = readSettingsFile(full);
153
+ if (read.state === 'absent') continue;
154
+ if (read.state !== 'ok') return { state: 'unknown', reason: { kind: 'unreadable', file: full } };
155
+ let data;
156
+ try { data = file.endsWith('.jsonc') ? parseJsonc(read.text) : JSON.parse(read.text); } catch { return { state: 'unknown', reason: { kind: 'invalid-json', file: full } }; }
157
+ const rule = opencodeSkillRule(data, name);
158
+ if (rule) decided = { ...rule, file: full };
159
+ }
160
+ if (decided?.effect !== 'deny') return { state: 'enabled', reason: null };
161
+ return { state: 'disabled', reason: { kind: 'opencode-permission', file: decided.file, pattern: decided.resource, action: 'deny' } };
162
+ }
163
+ return { state: 'unknown', reason: { kind: 'unknown-runtime' } };
164
+ }
165
+
166
+ /** 'enabled' | 'disabled' | 'unknown' (eligibilityDetail's state). */
167
+ function eligibility(o) { return eligibilityDetail(o).state; }
168
+
169
+ /**
170
+ * Let the skill through the setting that turns it off (the owner pressed "Enable for …"). Reads the detail
171
+ * again first; a skill that is not turned off changes nothing (`changed: false`). The file is copied into
172
+ * `backupDir` before it is written; the answer carries the file, the backup and the lines that changed, so the
173
+ * page can show exactly what was done. Throws an EnableError (status, code) when the file cannot be changed
174
+ * safely: then the answer says what to change by hand.
175
+ */
176
+ function enable({ runtime, name, skillMd, accountId = 'default', projectPath = null, confirmDenyAll = false }) {
177
+ const detail = eligibilityDetail({ runtime, name, skillMd, accountId, projectPath });
178
+ if (detail.state !== 'disabled') return { ok: true, changed: false, state: detail.state, file: detail.reason?.file || null };
179
+ const { file } = detail.reason;
180
+ const read = readSettingsFile(file);
181
+ if (read.state !== 'ok') throw new EnableError(409, `${file} could not be read whole: change it yourself.`, 'settings_unreadable', { file });
182
+ let next;
183
+ let changed = [];
184
+ if (detail.reason.kind === 'codex-config') {
185
+ const out = enableCodexTable(read.text, { skillMd, name });
186
+ if (!out) throw new EnableError(409, `The [[skills.config]] table that turns /${name} off in ${file} could not be changed in place: set its "enabled = true" yourself.`, 'settings_unchanged', { file });
187
+ next = out.text; changed = out.changed;
188
+ } else if (detail.reason.kind === 'opencode-permission') {
189
+ const allowRule = { action: 'skill', resource: name, effect: 'allow' };
190
+ if (/\.jsonc$/i.test(file)) throw new EnableError(409, `${file} has comments, which SynaBun does not rewrite: add ${JSON.stringify(allowRule)} as the last rule of its "permissions" yourself.`, 'jsonc_unsupported', { file, rule: allowRule });
191
+ const out = enableOpencodeRule(read.text, { name, confirmDenyAll, everySkill: detail.reason.pattern === '*' });
192
+ if (out.error) throw new EnableError(409, out.error.message, out.error.code, { file, rule: allowRule });
193
+ next = out.text; changed = out.changed;
194
+ } else if (detail.reason.kind === 'claude-override') {
195
+ const root = parseJsonWithOffsets(read.text);
196
+ const overrides = nodeAt(root, ['skillOverrides']);
197
+ next = overrides?.members?.length === 1 ? removeMember(read.text, root, 'skillOverrides') : removeMember(read.text, overrides, name);
198
+ changed = [`skillOverrides["${name}"] removed`];
199
+ } else if (detail.reason.kind === 'claude-deny') {
200
+ if (detail.reason.rule === 'Skill') throw new EnableError(409, `${file} denies every skill ("Skill" in permissions.deny): SynaBun does not loosen that rule; change it yourself.`, 'deny_all', { file, rule: 'Skill' });
201
+ const root = parseJsonWithOffsets(read.text);
202
+ const deny = nodeAt(root, ['permissions', 'deny']);
203
+ const at = deny?.elements?.findIndex((e) => e.value === detail.reason.rule) ?? -1;
204
+ if (at < 0) throw new EnableError(409, 'The rule moved while it was being changed: nothing was written.', 'settings_unchanged', { file });
205
+ next = removeElement(read.text, deny, at);
206
+ changed = [`permissions.deny: "${detail.reason.rule}" removed`];
207
+ } else throw new EnableError(409, 'Nothing SynaBun can change.', 'settings_unchanged', { file });
208
+ JSON.parse(/\.toml$/i.test(file) ? '{}' : next); // a JSON edit that does not parse is never written
209
+ const backup = backupSettings(file);
210
+ const mode = (() => { try { return statSync(file).mode & 0o777; } catch { return 0o600; } })();
211
+ replaceFileAtomic(file, next, { mode });
212
+ const after = eligibilityDetail({ runtime, name, skillMd, accountId, projectPath });
213
+ return { ok: after.state === 'enabled', changed: true, state: after.state, file, backup, lines: changed };
214
+ }
215
+
216
+ function backupSettings(file) {
217
+ if (!backupDir) return null;
218
+ mkdirSync(backupDir, { recursive: true, mode: 0o700 });
219
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
220
+ const target = join(backupDir, `${stamp}-${basename(file)}`);
221
+ copyFileSync(file, target, constants.COPYFILE_EXCL);
222
+ return target;
223
+ }
224
+
225
+ /** Ask Codex itself: `codex app-server`, initialize, skills/list. */
226
+ function probeCodex({ skillMd, accountId, projectPath }) {
227
+ const bin = codexBin();
228
+ if (!bin) return Promise.resolve({ discovered: null, code: 'not-installed', method: 'codex app-server', reason: 'Codex is not installed.' });
229
+ return new Promise((resolveProbe) => {
230
+ let done = false;
231
+ const env = { ...codexEnv(accountId) };
232
+ const child = spawnImpl(bin, ['app-server'], { cwd: projectPath || codexHome(accountId), env, stdio: ['pipe', 'pipe', 'pipe'] });
233
+ const finish = (result) => { if (done) return; done = true; clearTimeout(timer); try { child.stdin?.end?.(); } catch {} try { child.kill(); } catch {} resolveProbe({ method: 'codex app-server skills/list', ...result }); };
234
+ const timer = setTimeout(() => finish({ discovered: null, code: 'no-answer', reason: 'Codex did not answer in time.' }), timeoutMs);
235
+ // Nothing is written once the probe is settled: Codex exited, could not be started, answered or ran out of time.
236
+ const send = (message) => { if (done || !child.stdin?.writable) return; try { child.stdin.write(`${JSON.stringify(message)}\n`); } catch {} };
237
+ // A Codex that exits or fails to start as a request is written fails the pipe after write() returned (EPIPE, a
238
+ // write after end): unhandled, that error would end the server. A probe whose Codex died early has failed.
239
+ child.stdin?.on?.('error', () => finish({ discovered: null, code: 'no-answer', reason: 'Codex stopped before it answered.' }));
240
+ let buf = '';
241
+ child.on?.('error', (err) => finish({ discovered: null, code: err?.code === 'ENOENT' ? 'not-installed' : 'no-answer', reason: `Codex could not be started (${err?.code || err?.message}).` }));
242
+ child.on?.('exit', () => finish({ discovered: null, code: 'no-answer', reason: 'Codex stopped before it answered.' }));
243
+ child.stdout.on('data', (d) => {
244
+ buf += d;
245
+ let i;
246
+ while ((i = buf.indexOf('\n')) >= 0) {
247
+ const line = buf.slice(0, i); buf = buf.slice(i + 1);
248
+ let m; try { m = JSON.parse(line); } catch { continue; }
249
+ if (m.id === 1) {
250
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
251
+ send({ jsonrpc: '2.0', id: 2, method: 'skills/list', params: { cwds: [projectPath || codexHome(accountId)], forceReload: true } });
252
+ } else if (m.id === 2) {
253
+ const skills = (m.result?.data || []).flatMap((e) => e.skills || []);
254
+ const hit = skills.find((s) => typeof s.path === 'string' && resolve(s.path) === resolve(skillMd));
255
+ finish(hit ? { discovered: true, enabled: hit.enabled !== false, scope: hit.scope || null } : { discovered: false, listed: skills.length });
256
+ }
257
+ }
258
+ });
259
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { clientInfo: { name: 'synabun-skills-verify', version: '1' }, capabilities: { experimentalApi: true } } });
260
+ });
261
+ }
262
+
263
+ async function probe({ runtime, name, skillMd, accountId = 'default', projectPath = null }) {
264
+ if (runtime === 'codex') return probeCodex({ skillMd, accountId, projectPath });
265
+ if (runtime === 'opencode') {
266
+ const client = opencodeClient();
267
+ if (!client?.app?.skills) return { discovered: null, code: 'not-running', method: 'opencode serve', reason: 'SynaBun\'s OpenCode is not running: start a new OpenCode session to check.' };
268
+ try {
269
+ const res = await client.app.skills(projectPath ? { directory: projectPath } : {});
270
+ const list = Array.isArray(res?.data) ? res.data : [];
271
+ const hit = list.find((s) => (s.location && resolve(s.location) === resolve(skillMd)) || s.name === name);
272
+ return { method: 'opencode serve /api/skill', discovered: !!hit, ...(hit ? {} : { listed: list.length }) };
273
+ } catch (err) { return { discovered: null, code: 'no-answer', method: 'opencode serve', reason: err?.message || String(err) }; }
274
+ }
275
+ if (runtime === 'claude') return { discovered: null, code: 'no-list', method: 'none', reason: 'Claude Code has no list to ask; its folder rules were checked instead.' };
276
+ return { discovered: null, method: 'none', reason: 'Unknown runtime.' };
277
+ }
278
+
279
+ return { eligibility, eligibilityDetail, enable, probe };
280
+ }
281
+
282
+ /**
283
+ * opencode.json with `{ "action": "skill", "resource": "<name>", "effect": "allow" }` as the last rule of its
284
+ * `permissions` (the list made when there is none), the rest of the text byte for byte: OpenCode 2 applies the last
285
+ * rule that matches, after the rules of an OpenCode 1 `permission` object, which stays as it was. `everySkill`: the
286
+ * rule that turns the skill off denies every skill, which SynaBun loosens only when the owner confirmed it
287
+ * (`confirmDenyAll`).
288
+ * @returns {{ text?: string, changed?: string[], error?: { code: string, message: string } }}
289
+ */
290
+ export function enableOpencodeRule(text, { name, confirmDenyAll = false, everySkill = false }) {
291
+ const root = parseJsonWithOffsets(text);
292
+ if (root.type !== 'object') return { error: { code: 'settings_unchanged', message: 'The settings file is not a JSON object.' } };
293
+ if (everySkill && !confirmDenyAll) {
294
+ return { error: { code: 'deny_all', message: `The OpenCode configuration denies every skill: SynaBun does not loosen that rule by itself. Allow "${name}" yourself, or confirm loosening it.` } };
295
+ }
296
+ const rule = `{ "action": "skill", "resource": ${JSON.stringify(name)}, "effect": "allow" }`;
297
+ const changed = [`permissions: ${rule} added`];
298
+ const permissions = nodeAt(root, ['permissions']);
299
+ if (!permissions) return { text: appendMember(text, root, 'permissions', `[${rule}]`), changed };
300
+ if (permissions.type !== 'array') return { error: { code: 'settings_unchanged', message: '"permissions" is not a list: change it yourself.' } };
301
+ return { text: appendElement(text, permissions, rule), changed };
302
+ }
303
+
304
+ export class EnableError extends Error {
305
+ constructor(status, message, code, extra = {}) {
306
+ super(message);
307
+ this.status = status;
308
+ this.code = code;
309
+ Object.assign(this, extra);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * The text of a Codex config.toml with the `enabled = false` line of the [[skills.config]] table(s) that name this
315
+ * skill turned to `enabled = true`; every other byte stays (comments and spacing included). Null when no such
316
+ * line is there to change.
317
+ */
318
+ export function enableCodexTable(toml, { skillMd, name }) {
319
+ const lines = String(toml || '').split(/(\r?\n)/); // text and its separators, kept
320
+ const out = [];
321
+ const changed = [];
322
+ let current = null; // the table being read: { path, name, enabledAt }
323
+ const flush = () => {
324
+ if (!current) return;
325
+ const hit = (current.path && resolve(current.path) === resolve(skillMd)) || (current.name && current.name === name);
326
+ if (hit && current.enabledAt !== null) {
327
+ const raw = out[current.enabledAt];
328
+ out[current.enabledAt] = raw.replace(/^(\s*enabled\s*=\s*)false(\s*(#.*)?)$/, '$1true$2');
329
+ changed.push({ line: current.lineNo, before: raw, after: out[current.enabledAt] });
330
+ }
331
+ current = null;
332
+ };
333
+ let lineNo = 0;
334
+ for (let i = 0; i < lines.length; i++) {
335
+ const raw = lines[i];
336
+ out.push(raw);
337
+ if (i % 2 === 1) continue; // a separator
338
+ lineNo++;
339
+ const line = raw.replace(/\s+#.*$/, '').trim();
340
+ if (!line) continue;
341
+ if (/^\[\[\s*skills\.config\s*\]\]$/.test(line)) { flush(); current = { path: null, name: null, enabledAt: null, lineNo: null }; continue; }
342
+ if (/^\[/.test(line)) { flush(); continue; }
343
+ if (!current) continue;
344
+ const kv = /^([A-Za-z_]+)\s*=\s*(.+)$/.exec(line);
345
+ if (!kv) continue;
346
+ const [, key, value] = kv;
347
+ const str = /^"((?:[^"\\]|\\.)*)"$/.exec(value)?.[1]?.replace(/\\(["\\])/g, '$1') ?? /^'([^']*)'$/.exec(value)?.[1] ?? null;
348
+ if (key === 'path' && str !== null) current.path = str;
349
+ else if (key === 'name' && str !== null) current.name = str;
350
+ else if (key === 'enabled' && value === 'false') { current.enabledAt = out.length - 1; current.lineNo = lineNo; }
351
+ else if (key === 'enabled' && value === 'true') { current.enabledAt = null; current.lineNo = null; }
352
+ }
353
+ flush();
354
+ return changed.length ? { text: out.join(''), changed } : null;
355
+ }
@@ -0,0 +1,252 @@
1
+ import { inflateSync, brotliDecompressSync } from 'node:zlib';
2
+
3
+ export const ASSET_EXT = /\.(?:png|jpe?g|gif|webp|ico|woff2?|ttf|otf)$/i;
4
+ const MAX = 16 * 1024 * 1024;
5
+ export const ASSET_LIMITS = Object.freeze({ assetInflated: 8 * 1024 * 1024, scanInflated: 32 * 1024 * 1024, ratio: 100, ratioSlack: 64 * 1024 });
6
+ export const assetBudget = () => ({ inflated: 0, exhausted: false });
7
+ export const scriptMarkers = (s) => /(?:^|\n)[\t ]*#!|<\?php\b|<\?=|<%|<script\b/i.test(s);
8
+ // Long, language-specific markers are evidence even among non-printable bytes.
9
+ // Short markers stay restricted to printable/decoded text to avoid random pixel/glyph hits.
10
+ const rawMarkers = (s) => /<\?php\b|<script\b/i.test(s);
11
+ const printableLines = (s) => s.matchAll(/(?:^|\n)([\t\x20-\x7e]{4,})(?=\r?\n|$)/g);
12
+ const crc32 = (b) => { let crc = -1; for (const x of b) { crc ^= x; for (let n = 0; n < 8; n++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); } return (crc ^ -1) >>> 0; };
13
+
14
+ // Validate framing, offsets and the exact end, without rendering or executing the asset.
15
+ // Bounded decompression is only for metadata/font tables, never archive extraction.
16
+ export function inspectAsset(path, b, suspicious, { budget = assetBudget(), local = { inflated: 0 }, heuristic = () => {} } = {}) {
17
+ let script = false;
18
+ const raw = (data) => {
19
+ const text = data.toString('latin1');
20
+ if (rawMarkers(text)) script = true;
21
+ // Random printable substrings of compressed pixels/glyphs are not commands.
22
+ for (const [, line] of printableLines(text)) { const hit = suspicious(line); if (scriptMarkers(line) || hit === 'strong') script = true; else if (hit) heuristic(line); }
23
+ };
24
+ const text = (data, encoding = 'utf8') => {
25
+ const value = typeof data === 'string' ? data : encoding === 'utf8' ? new TextDecoder('utf8', { fatal: true }).decode(data) : data.toString(encoding);
26
+ if (scriptMarkers(value)) script = true;
27
+ let start = 0;
28
+ while (start < value.length && !script) {
29
+ const newline = value.indexOf('\n', start), end = newline < 0 ? value.length : newline;
30
+ const line = value.slice(start, end), hit = suspicious(line);
31
+ if (hit === 'strong') script = true;
32
+ else if (hit) heuristic(line);
33
+ start = end + 1;
34
+ }
35
+ };
36
+ const range = (at, n) => Number.isSafeInteger(at) && Number.isSafeInteger(n) && at >= 0 && n >= 0 && at + n <= b.length;
37
+ const ascii = (at, n) => b.subarray(at, at + n).toString('latin1');
38
+ const unpack = (data, brotli, visit, expected) => {
39
+ const cap = Math.min(ASSET_LIMITS.assetInflated - local.inflated, ASSET_LIMITS.scanInflated - budget.inflated, data.length * ASSET_LIMITS.ratio + ASSET_LIMITS.ratioSlack);
40
+ const exceeded = () => { budget.exhausted = true; return Object.assign(new Error('asset-decompression-budget'), { budget: true }); };
41
+ if (budget.exhausted || cap <= 0 || expected > cap) throw exceeded();
42
+ let out;
43
+ try {
44
+ const decoded = (brotli ? brotliDecompressSync : inflateSync)(data, { maxOutputLength: cap, info: true });
45
+ if (decoded.engine.bytesWritten !== data.length) throw new Error('trailing compressed bytes');
46
+ out = decoded.buffer;
47
+ }
48
+ catch (err) { if (err.code === 'ERR_BUFFER_TOO_LARGE') throw exceeded(); throw err; }
49
+ local.inflated += out.length; budget.inflated += out.length;
50
+ if (expected != null && out.length !== expected) throw new Error('invalid inflated length');
51
+ visit(out); // Scan now; never retain decompressed outputs across chunks/tables/assets.
52
+ };
53
+ const fontTable = (tag, data) => {
54
+ raw(data);
55
+ if (tag !== 'name') {
56
+ // A table made entirely of text is decoded metadata, not a random glyph byte run.
57
+ if (/^[\t\r\n\x20-\x7e]+$/.test(data.toString('latin1'))) text(data, 'latin1');
58
+ return;
59
+ }
60
+ if (data.length < 6) throw new Error('invalid name table');
61
+ const count = data.readUInt16BE(2), start = data.readUInt16BE(4);
62
+ if (6 + count * 12 > data.length || start > data.length) throw new Error('invalid names');
63
+ for (let i = 0; i < count; i++) {
64
+ const at = 6 + i * 12, platform = data.readUInt16BE(at), length = data.readUInt16BE(at + 8), offset = start + data.readUInt16BE(at + 10);
65
+ if (offset + length > data.length) throw new Error('invalid name');
66
+ const value = data.subarray(offset, offset + length);
67
+ if (platform === 0 || platform === 3) { if (length % 2) throw new Error('invalid unicode name'); text(Buffer.from(value).swap16(), 'utf16le'); }
68
+ else text(value, 'latin1');
69
+ }
70
+ };
71
+ function png() {
72
+ if (b.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') return false;
73
+ let at = 8, image = false, header = false;
74
+ while (range(at, 12)) {
75
+ const n = b.readUInt32BE(at), tag = ascii(at + 4, 4);
76
+ if (!range(at, n + 12) || !/^[A-Za-z]{4}$/.test(tag) || crc32(b.subarray(at + 4, at + 8 + n)) !== b.readUInt32BE(at + 8 + n)) return false;
77
+ if (!header && (tag !== 'IHDR' || n !== 13 || !b.readUInt32BE(at + 8) || !b.readUInt32BE(at + 12))) return false;
78
+ if (header && tag === 'IHDR') return false;
79
+ header = true;
80
+ const data = b.subarray(at + 8, at + 8 + n);
81
+ if (tag === 'IDAT') image = true;
82
+ if (tag === 'tEXt') { const zero = data.indexOf(0); if (zero < 1) return false; text(data.subarray(zero + 1), 'latin1'); }
83
+ if (tag === 'zTXt' || tag === 'iCCP') { const zero = data.indexOf(0); if (zero < 1 || data[zero + 1] !== 0) return false; unpack(data.subarray(zero + 2), false, tag === 'zTXt' ? (out) => text(out, 'latin1') : raw); }
84
+ if (tag === 'iTXt') {
85
+ const zero = data.indexOf(0); if (zero < 1 || ![0, 1].includes(data[zero + 1]) || data[zero + 2] !== 0) return false;
86
+ const lang = data.indexOf(0, zero + 3), translated = data.indexOf(0, lang + 1);
87
+ if (lang < 0 || translated < 0) return false;
88
+ if (data[zero + 1]) unpack(data.subarray(translated + 1), false, text);
89
+ else text(data.subarray(translated + 1));
90
+ }
91
+ at += n + 12;
92
+ if (tag === 'IEND') return n === 0 && image && at === b.length;
93
+ }
94
+ return false;
95
+ }
96
+ function jpeg() {
97
+ if (b[0] !== 255 || b[1] !== 216) return false;
98
+ let at = 2, frame = false, scan = false;
99
+ while (at < b.length) {
100
+ if (b[at++] !== 255) return false;
101
+ while (b[at] === 255) at++;
102
+ const marker = b[at++];
103
+ if (marker === 0xd9) return frame && scan && at === b.length;
104
+ if (!range(at, 2) || marker === 0 || marker === 0xd8) return false;
105
+ const n = b.readUInt16BE(at); if (n < 2 || !range(at, n)) return false;
106
+ if (marker === 0xfe) text(b.subarray(at + 2, at + n), 'latin1');
107
+ if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) frame = true;
108
+ at += n;
109
+ if (marker === 0xda) {
110
+ scan = true;
111
+ while (at < b.length) {
112
+ if (b[at] !== 255) { at++; continue; }
113
+ if (b[at + 1] === 0 || b[at + 1] >= 0xd0 && b[at + 1] <= 0xd7) { at += 2; continue; }
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ function gif() {
121
+ if (!['GIF87a', 'GIF89a'].includes(ascii(0, 6)) || !range(0, 13) || !b.readUInt16LE(6) || !b.readUInt16LE(8)) return false;
122
+ let at = 13 + (b[10] & 128 ? 3 * 2 ** ((b[10] & 7) + 1) : 0), image = false;
123
+ const blocks = (comment = false) => { const parts = []; while (range(at, 1)) { const n = b[at++]; if (!n) { if (comment) text(Buffer.concat(parts), 'latin1'); return true; } if (!range(at, n)) return false; if (comment) parts.push(b.subarray(at, at + n)); at += n; } return false; };
124
+ while (range(at, 1)) {
125
+ const tag = b[at++];
126
+ if (tag === 0x3b) return image && at === b.length;
127
+ if (tag === 0x21) { if (!range(at, 1) || !blocks(b[at++] === 0xfe)) return false; }
128
+ else if (tag === 0x2c) {
129
+ if (!range(at, 9)) return false;
130
+ const flags = b[at + 8]; at += 9 + (flags & 128 ? 3 * 2 ** ((flags & 7) + 1) : 0);
131
+ if (!range(at, 1) || b[at] < 2 || b[at++] > 8 || !blocks()) return false;
132
+ image = true;
133
+ } else return false;
134
+ }
135
+ return false;
136
+ }
137
+ function webp() {
138
+ if (!range(0, 12) || ascii(0, 4) !== 'RIFF' || ascii(8, 4) !== 'WEBP' || b.readUInt32LE(4) + 8 !== b.length) return false;
139
+ let at = 12, image = false;
140
+ while (range(at, 8)) {
141
+ const tag = ascii(at, 4), n = b.readUInt32LE(at + 4);
142
+ if (!range(at + 8, n + (n & 1))) return false;
143
+ if (['VP8 ', 'VP8L', 'ANMF'].includes(tag)) image = true;
144
+ at += 8 + n + (n & 1);
145
+ }
146
+ return image && at === b.length;
147
+ }
148
+ function ico() {
149
+ if (!range(0, 6) || b.readUInt32LE(0) !== 0x10000) return false;
150
+ const count = b.readUInt16LE(4); if (!count || !range(6, count * 16)) return false;
151
+ let end = 6 + count * 16;
152
+ for (let i = 0; i < count; i++) {
153
+ const n = b.readUInt32LE(6 + i * 16 + 8), at = b.readUInt32LE(6 + i * 16 + 12);
154
+ if (at !== end || !range(at, n) || n < 8) return false;
155
+ const image = b.subarray(at, at + n);
156
+ if (image.subarray(0, 8).toString('hex') === '89504e470d0a1a0a') { const reason = inspectAsset('icon.png', image, suspicious, { budget, local, heuristic }); if (reason) throw Object.assign(new Error(reason), { reason }); }
157
+ else {
158
+ if (n < 40 || image.readUInt32LE(0) !== 40 || image.readUInt32LE(16) !== 0) return false;
159
+ const w = image.readInt32LE(4), h = Math.abs(image.readInt32LE(8)) / 2, depth = image.readUInt16LE(14);
160
+ if (w <= 0 || !Number.isInteger(h) || !h || ![1, 4, 8, 16, 24, 32].includes(depth)) return false;
161
+ const colors = image.readUInt32LE(32) || (depth <= 8 ? 2 ** depth : 0);
162
+ if (40 + colors * 4 + (Math.ceil(w * depth / 32) * 4 + Math.ceil(w / 32) * 4) * h !== n) return false;
163
+ }
164
+ end = at + n;
165
+ }
166
+ return end === b.length;
167
+ }
168
+ function font() {
169
+ const tag = ascii(0, 4), woff = /\.woff$/i.test(path), woff2 = /\.woff2$/i.test(path);
170
+ if (woff || woff2) {
171
+ if (tag !== (woff2 ? 'wOF2' : 'wOFF') || !range(0, woff2 ? 48 : 44) || b.readUInt32BE(8) !== b.length || !b.readUInt16BE(12) || b.readUInt16BE(14)) return false;
172
+ const metaAt = b.readUInt32BE(woff2 ? 28 : 24), metaSize = b.readUInt32BE(woff2 ? 32 : 28);
173
+ const metaOriginal = b.readUInt32BE(woff2 ? 36 : 32);
174
+ const privateAt = b.readUInt32BE(woff2 ? 40 : 36), privateSize = b.readUInt32BE(woff2 ? 44 : 40);
175
+ if (!range(metaAt, metaSize) || !range(privateAt, privateSize) || (!metaSize && (metaAt || metaOriginal)) || (!privateSize && privateAt)) return false;
176
+ const count = b.readUInt16BE(12), blocks = [];
177
+ let directoryEnd;
178
+ if (woff2) {
179
+ const tags = 'cmap head hhea hmtx maxp name OS/2 post cvt_ fpgm glyf loca prep CFF_ VORG EBDT EBLC gasp hdmx kern LTSH PCLT VDMX vhea vmtx BASE GDEF GPOS GSUB EBSC JSTF MATH CBDT CBLC COLR CPAL SVG_ sbix acnt avar bdat bloc bsln cvar fdsc feat fmtx fvar gvar hsty just lcar mort morx opbd prop trak Zapf Silf Glat Gloc Feat Sill'.split(' ').map((s) => s.replace('_', ' '));
180
+ let pos = 48, total = 0;
181
+ const byte = () => { if (!range(pos, 1)) throw new Error('short directory'); return b[pos++]; };
182
+ const u32 = () => { if (!range(pos, 4)) throw new Error('short directory'); const n = b.readUInt32BE(pos); pos += 4; return n; };
183
+ const base128 = () => {
184
+ let n = 0;
185
+ for (let i = 0; i < 5; i++) { const v = byte(); if ((!i && v === 128) || n > 0x1ffffff) throw new Error('invalid base128'); n = n * 128 + (v & 127); if (!(v & 128)) return n; }
186
+ throw new Error('invalid base128');
187
+ };
188
+ const u255 = () => { const v = byte(); return v === 253 ? byte() * 256 + byte() : v === 254 ? byte() + 506 : v === 255 ? byte() + 253 : v; };
189
+ const tables = [];
190
+ for (let i = 0; i < count; i++) {
191
+ const flags = byte(), tag = (flags & 63) === 63 ? String.fromCharCode(byte(), byte(), byte(), byte()) : tags[flags & 63];
192
+ const version = flags >>> 6, original = base128(), transformed = ['glyf', 'loca'].includes(tag) ? version !== 3 : version !== 0;
193
+ if (['glyf', 'loca'].includes(tag) ? ![0, 3].includes(version) : version && !(tag === 'hmtx' && version === 1)) return false;
194
+ const size = transformed ? base128() : original;
195
+ if (tag === 'loca' && transformed && size !== 0) return false;
196
+ tables.push({ tag, offset: total, size, transformed }); total += size;
197
+ }
198
+ if (ascii(4, 4) === 'ttcf') {
199
+ if (![0x10000, 0x20000].includes(u32())) return false;
200
+ const fonts = u255(); if (!fonts || fonts > b.length) return false;
201
+ for (let i = 0; i < fonts; i++) {
202
+ const n = u255(), flavor = u32(), used = new Set(), names = new Set();
203
+ if (!n || n > count || ![0x10000, 0x4f54544f].includes(flavor)) return false;
204
+ for (let j = 0; j < n; j++) { const index = u255(); if (index >= count || used.has(index) || names.has(tables[index].tag)) return false; used.add(index); names.add(tables[index].tag); }
205
+ if (names.has('glyf') !== names.has('loca')) return false;
206
+ }
207
+ } else if (new Set(tables.map((t) => t.tag)).size !== count) return false;
208
+ directoryEnd = pos;
209
+ const size = b.readUInt32BE(20); if (!size || !range(pos, size)) return false;
210
+ blocks.push({ offset: pos, size, visit: () => unpack(b.subarray(pos, pos + size), true, (out) => {
211
+ for (const t of tables) fontTable(t.tag, out.subarray(t.offset, t.offset + t.size));
212
+ }, total) });
213
+ } else {
214
+ directoryEnd = 44 + count * 20; if (!range(44, count * 20)) return false;
215
+ for (let i = 0; i < count; i++) {
216
+ const at = 44 + i * 20, offset = b.readUInt32BE(at + 4), size = b.readUInt32BE(at + 8), original = b.readUInt32BE(at + 12), tableTag = ascii(at, 4);
217
+ if (!range(offset, size) || size > original || offset % 4) return false;
218
+ blocks.push({ offset, size, visit: () => size < original ? unpack(b.subarray(offset, offset + size), false, (out) => fontTable(tableTag, out), original) : fontTable(tableTag, b.subarray(offset, offset + size)) });
219
+ }
220
+ }
221
+ blocks.sort((a, b) => a.offset - b.offset);
222
+ if (metaSize) blocks.push({ offset: metaAt, size: metaSize, visit: () => unpack(b.subarray(metaAt, metaAt + metaSize), woff2, text, metaOriginal) });
223
+ if (privateSize) blocks.push({ offset: privateAt, size: privateSize, visit: () => raw(b.subarray(privateAt, privateAt + privateSize)) });
224
+ let end = directoryEnd;
225
+ for (const block of blocks) {
226
+ if (block.offset !== end && block.offset !== Math.ceil(end / 4) * 4 || block.offset < end || !b.subarray(end, block.offset).every((v) => v === 0)) return false;
227
+ end = block.offset + block.size;
228
+ }
229
+ if (end !== b.length && !(Math.ceil(end / 4) * 4 === b.length && b.subarray(end).every((v) => v === 0))) return false;
230
+ for (const block of blocks) block.visit();
231
+ return true;
232
+ }
233
+ if (!range(0, 12) || (/\.ttf$/i.test(path) ? b.readUInt32BE(0) !== 0x10000 : tag !== 'OTTO')) return false;
234
+ const count = b.readUInt16BE(4); if (!count || !range(12, count * 16)) return false;
235
+ let end = 12 + count * 16;
236
+ for (let i = 0; i < count; i++) {
237
+ const at = 12 + i * 16, offset = b.readUInt32BE(at + 8), size = b.readUInt32BE(at + 12);
238
+ if (offset < 12 + count * 16 || !range(offset, size)) return false;
239
+ fontTable(ascii(at, 4), b.subarray(offset, offset + size));
240
+ end = Math.max(end, offset + size);
241
+ }
242
+ return end === b.length || Math.ceil(end / 4) * 4 === b.length && b.subarray(end).every((x) => x === 0);
243
+ }
244
+ try {
245
+ if (b.length > MAX) return 'asset-too-large';
246
+ const valid = /\.png$/i.test(path) ? png() : /\.jpe?g$/i.test(path) ? jpeg() : /\.gif$/i.test(path) ? gif() : /\.webp$/i.test(path) ? webp() : /\.ico$/i.test(path) ? ico() : font();
247
+ if (!valid) return 'asset-invalid-structure';
248
+ raw(b);
249
+ if (script) return 'asset-script-content';
250
+ return null;
251
+ } catch (err) { return err.reason || (err.budget ? 'asset-decompression-budget' : 'asset-invalid-structure'); }
252
+ }
@@ -0,0 +1,26 @@
1
+ import { readHeadOf } from './export.js';
2
+ import { bundleNameProblem } from './bundle.js';
3
+ class NameError extends Error { constructor(status, message, code) { super(message); Object.assign(this, { status, code }); } }
4
+
5
+ /**
6
+ * The name a revision is installed under (its folder's name): its SKILL.md name when that can be a folder name,
7
+ * else the library's. The plan, the review's evidence and an approval all use this one name.
8
+ */
9
+ export function effectiveInstallName(skill, files) {
10
+ const { parsed } = readHeadOf(files);
11
+ const fmName = typeof parsed.data?.name === 'string' ? parsed.data.name.trim() : '';
12
+ const candidate = fmName && !bundleNameProblem(fmName) ? fmName : skill?.name;
13
+ if (!candidate || bundleNameProblem(candidate)) throw new NameError(422, 'This skill has no name that can be a folder\'s name: give its SKILL.md a "name".', 'invalid_name');
14
+ return candidate;
15
+ }
16
+
17
+ /**
18
+ * The names a scan checks a skill's install name against: every name SynaBun ships (look-alikes), and those it
19
+ * must not take (all of them but the skill's own when it is that shipped skill, in the library as bundled).
20
+ */
21
+ export function nameChecks(skill, name, shipped = []) {
22
+ const all = [...new Set((shipped || []).filter((n) => typeof n === 'string' && n))];
23
+ const own = skill?.origin?.kind === 'bundled' ? String(skill.origin.name || skill.name || '').toLowerCase() : '';
24
+ const notOwn = all.filter((n) => n.toLowerCase() !== own || own !== String(name || '').toLowerCase());
25
+ return { knownNames: notOwn, reservedNames: notOwn };
26
+ }