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,427 @@
1
+ // ── What SynaBun installed, and only that ──
2
+ // Installing a bundled skill into a runtime (Claude Code's skills folder, Codex's prompts, OpenCode's commands)
3
+ // writes into folders that belong to the user. A destination is replaced or removed only when it is provably
4
+ // SynaBun's own:
5
+ // - absent: written;
6
+ // - identical: it already holds exactly what would be written (by digest): nothing to do, and it is recorded;
7
+ // - owned: the install ledger records that SynaBun wrote it, and it still holds exactly that;
8
+ // - modified: SynaBun wrote it but it was changed since; unowned: SynaBun has no record of it, and it holds
9
+ // something else. Both are refused (409) unless the request names the digest of what is there now, which a
10
+ // person sees before confirming (`expectedDestinationDigest`): the decision is bound to that state;
11
+ // - unreadable (too large, a FIFO, changed while read): refused, always.
12
+ // Whatever is replaced or removed is copied first into a backup (DATA_HOME/data/skills/backups/<id>/), and the
13
+ // ledger (DATA_HOME/data/skills/installs.json) keeps every install with its digest and its source. A link is a
14
+ // destination like any other: its target is recorded, never followed, never removed through.
15
+ //
16
+ // This is the first piece of the managed skill library (owner decision, 2026-10-22): per-target install
17
+ // records with digests, backups and rollback. The library's revisions and its per-runtime exports extend it.
18
+
19
+ import { chmodSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync } from 'node:fs';
20
+ import { randomBytes } from 'node:crypto';
21
+ import { basename, dirname, join, resolve } from 'node:path';
22
+ import { PathConfineError, isPlainName, realPathOf } from '../path-confine.js';
23
+ import { listConfined, makeConfinedDir, readConfinedFile, removeConfinedEntry, replaceFileAtomic, writeConfinedFile } from '../confined-fs.js';
24
+ import { SYSTEM_NAMES, fileEntry, manifestDigest, readManifest, shortDigest } from './digest.js';
25
+
26
+ export const LEDGER_VERSION = 1;
27
+ export const MAX_BACKUPS = 100;
28
+ export const MAX_JOURNAL = 500;
29
+ // The ledger is read whole only up to this: one larger is not read, nothing is claimed from it, and nothing is
30
+ // written over it (a write would lose what it holds).
31
+ export const LEDGER_MAX_BYTES = 16 * 1024 * 1024;
32
+
33
+ export class InstallRefusedError extends PathConfineError {
34
+ constructor(message, details = {}) {
35
+ super(409, message);
36
+ Object.assign(this, details);
37
+ }
38
+ }
39
+
40
+ const STATE_WORDS = {
41
+ modified: 'was installed by SynaBun but has been changed since',
42
+ unowned: 'was not installed by SynaBun',
43
+ unreadable: 'could not be read whole',
44
+ };
45
+
46
+ /**
47
+ * @param {object} o
48
+ * @param {string} o.dataDir where the ledger and the backups live (DATA_HOME/data/skills)
49
+ * @param {() => string} [o.now] ISO time
50
+ * @param {() => string} [o.random] a short random tag for backup names
51
+ */
52
+ export function createSkillInstaller({ dataDir, now = () => new Date().toISOString(), random = () => randomBytes(4).toString('hex') }) {
53
+ const ledgerPath = join(dataDir, 'installs.json');
54
+ const backupsDir = join(dataDir, 'backups');
55
+
56
+ const emptyLedger = () => ({ schemaVersion: LEDGER_VERSION, records: [], journal: [] });
57
+ // The parse of the ledger as the file was at `key` (its inode, size and times): a catalog scan or a bridge
58
+ // search asks for one record per entry, and the file is parsed once while it does not change.
59
+ let cached = null;
60
+ let parses = 0;
61
+ const fileKey = () => { try { const st = statSync(ledgerPath); return { st, key: `${st.ino}:${st.size}:${st.mtimeMs}:${st.ctimeMs}` }; } catch { return null; } };
62
+
63
+ /** The ledger as the file holds it now, for reading only (never changed in place). `state`: ok | absent | invalid | too-large. */
64
+ function ledgerView() {
65
+ const at = fileKey();
66
+ if (!at) { cached = null; return { ledger: emptyLedger(), state: 'absent' }; }
67
+ if (cached && cached.key === at.key) return cached;
68
+ let ledger = emptyLedger();
69
+ let state = 'ok';
70
+ if (!at.st.isFile() || at.st.size > LEDGER_MAX_BYTES) state = 'too-large';
71
+ else {
72
+ try {
73
+ const parsed = JSON.parse(readConfinedFile(dataDir, ledgerPath, { maxBytes: LEDGER_MAX_BYTES, encoding: 'utf-8' }));
74
+ parses += 1;
75
+ if (parsed && parsed.schemaVersion === LEDGER_VERSION && Array.isArray(parsed.records)) {
76
+ ledger = { schemaVersion: LEDGER_VERSION, records: parsed.records.filter((r) => r && typeof r.destination === 'string'), journal: Array.isArray(parsed.journal) ? parsed.journal : [] };
77
+ } else state = 'invalid';
78
+ } catch { state = 'invalid'; /* unreadable: no record is a claim */ }
79
+ }
80
+ cached = { key: at.key, ledger, state };
81
+ return cached;
82
+ }
83
+
84
+ /** A copy of the ledger to change and write back. One over its bound is refused: writing it would lose it. */
85
+ function readLedger({ forWrite = false } = {}) {
86
+ const view = ledgerView();
87
+ if (forWrite && view.state === 'too-large') {
88
+ throw Object.assign(new PathConfineError(413, `The install ledger (${ledgerPath}) is larger than ${Math.round(LEDGER_MAX_BYTES / (1024 * 1024))} MB: SynaBun does not write over a ledger it cannot read. Move it away to start a new one.`), { code: 'ledger_too_large' });
89
+ }
90
+ return { schemaVersion: LEDGER_VERSION, records: view.ledger.records.map((r) => ({ ...r })), journal: [...view.ledger.journal] };
91
+ }
92
+
93
+ function writeLedger(ledger) {
94
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 });
95
+ const out = { schemaVersion: LEDGER_VERSION, records: ledger.records, journal: ledger.journal.slice(-MAX_JOURNAL) };
96
+ replaceFileAtomic(ledgerPath, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
97
+ // What was just written is what the file holds: kept, not read again.
98
+ const at = fileKey();
99
+ cached = at ? { key: at.key, ledger: { schemaVersion: LEDGER_VERSION, records: out.records.map((r) => ({ ...r })), journal: [...out.journal] }, state: 'ok' } : null;
100
+ }
101
+
102
+ const keyOf = (destination) => resolve(String(destination));
103
+ const recordOf = (ledger, destination) => ledger.records.find((r) => keyOf(r.destination) === keyOf(destination)) || null;
104
+
105
+ function journal(ledger, entry) {
106
+ ledger.journal.push({ at: now(), ...entry });
107
+ }
108
+
109
+ // ── Payloads: what an install writes ──
110
+
111
+ /** Every regular file and folder of a source folder (links inside it followed only to what is inside it). */
112
+ function payloadFromFolder(dir) {
113
+ const real = realPathOf(dir);
114
+ const listing = listConfined(real, real, { skip: (name) => SYSTEM_NAMES.has(name) });
115
+ if (listing.truncated) throw new PathConfineError(413, 'That skill has too many files to install.');
116
+ const files = []; const dirs = []; const entries = [];
117
+ const walk = (nodes) => {
118
+ for (const node of nodes) {
119
+ if (node.type === 'dir') { dirs.push(node.path); entries.push({ path: node.path, type: 'dir' }); walk(node.children || []); continue; }
120
+ const bytes = readConfinedFile(real, node.real);
121
+ const executable = (lstatSync(node.real).mode & 0o111) !== 0;
122
+ files.push({ path: node.path, bytes, executable });
123
+ entries.push(fileEntry(node.path, bytes, { executable }));
124
+ }
125
+ };
126
+ walk(listing.entries);
127
+ return { kind: 'dir', files, dirs, digest: manifestDigest(entries) };
128
+ }
129
+
130
+ /** One file's content (a Codex or OpenCode bridge). */
131
+ function payloadFromText(text) {
132
+ const bytes = Buffer.from(String(text ?? ''), 'utf-8');
133
+ return { kind: 'file', bytes, executable: false, digest: manifestDigest([fileEntry('.', bytes)]) };
134
+ }
135
+
136
+ // ── The state of a destination ──
137
+
138
+ /**
139
+ * @returns {{ state: 'absent'|'identical'|'owned'|'modified'|'unowned'|'unreadable', current: object, record: object|null }}
140
+ */
141
+ function inspect({ destination, payload = null }) {
142
+ const record = recordOf(ledgerView().ledger, destination);
143
+ const current = readManifest(destination);
144
+ let state;
145
+ if (current.kind === 'absent') state = 'absent';
146
+ else if (!current.digest) state = 'unreadable';
147
+ else if (payload && current.digest === payload.digest && current.kind === payload.kind) state = 'identical';
148
+ else if (record && record.digest === current.digest) state = 'owned';
149
+ else if (record) state = 'modified';
150
+ else state = 'unowned';
151
+ return { state, current, record };
152
+ }
153
+
154
+ function refuse(action, { state, current, destination, target, name }) {
155
+ const what = `"${destination}" ${STATE_WORDS[state] || 'is not SynaBun\'s'}`;
156
+ const how = state === 'unreadable'
157
+ ? 'Move it away yourself and try again.'
158
+ : `Nothing was ${action === 'install' ? 'replaced' : 'removed'}. To ${action === 'install' ? 'replace' : 'remove'} it anyway, confirm it as it is now (${shortDigest(current.digest)}): a backup is kept.`;
159
+ throw new InstallRefusedError(`${what}. ${how}`, {
160
+ code: state === 'modified' ? 'skill_modified' : state === 'unreadable' ? 'skill_unreadable' : 'skill_unowned',
161
+ state, destination, target, name, currentDigest: current.digest || null, action,
162
+ });
163
+ }
164
+
165
+ const confirmed = (decision, current) => !!current.digest && typeof decision?.expectedDestinationDigest === 'string' && decision.expectedDestinationDigest === current.digest;
166
+
167
+ // ── Backups ──
168
+
169
+ function pruneBackups() {
170
+ let names = [];
171
+ try { names = readdirSync(backupsDir).filter((n) => !n.startsWith('.')).sort(); } catch { return; }
172
+ for (const old of names.slice(0, Math.max(0, names.length - MAX_BACKUPS))) {
173
+ try { rmSync(join(backupsDir, old), { recursive: true, force: true }); } catch { /* the next prune tries again */ }
174
+ }
175
+ }
176
+
177
+ /** Copy what is at `destination` into a new backup, without following links. @returns the backup id */
178
+ function backup({ destination, target, name, reason }) {
179
+ const stamp = now().replace(/[:.]/g, '-');
180
+ const id = `${stamp}-${String(target).replace(/[^a-z0-9-]/gi, '')}-${String(name).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64)}-${random()}`;
181
+ const dir = join(backupsDir, id);
182
+ mkdirSync(join(dir, 'entry'), { recursive: true, mode: 0o700 });
183
+ const entryRoot = join(dir, 'entry');
184
+ const manifest = readManifest(destination);
185
+ const links = [];
186
+ const skipped = [];
187
+ // Every file's and folder's own mode (permission bits), put back by a restore or a rollback exactly: a private
188
+ // 0600 file stays 0600. The copies in the backup itself are private (0600 / 0700) whatever the original was.
189
+ const modes = {};
190
+ const st = lstatSync(destination);
191
+ if (st.isSymbolicLink()) links.push({ path: '.', target: readlinkSync(destination) });
192
+ else if (st.isFile()) {
193
+ const real = realPathOf(destination);
194
+ modes['.'] = st.mode & 0o777;
195
+ writeConfinedFile(entryRoot, join(entryRoot, 'file'), readConfinedFile(dirname(real), real), { exclusive: true, mode: (st.mode & 0o111) ? 0o700 : 0o600 });
196
+ } else if (st.isDirectory()) {
197
+ const base = realPathOf(destination);
198
+ modes['.'] = st.mode & 0o777;
199
+ const walk = (from, rel) => {
200
+ for (const n of readdirSync(from).sort()) {
201
+ const full = join(from, n);
202
+ const relPath = rel ? `${rel}/${n}` : n;
203
+ const est = lstatSync(full);
204
+ if (est.isSymbolicLink()) links.push({ path: relPath, target: readlinkSync(full) });
205
+ else if (est.isDirectory()) { modes[relPath] = est.mode & 0o777; makeConfinedDir(entryRoot, join(entryRoot, 'tree', relPath)); walk(full, relPath); }
206
+ else if (est.isFile()) { modes[relPath] = est.mode & 0o777; writeConfinedFile(entryRoot, join(entryRoot, 'tree', relPath), readConfinedFile(base, full), { exclusive: true, mode: (est.mode & 0o111) ? 0o700 : 0o600 }); }
207
+ else skipped.push(relPath);
208
+ }
209
+ };
210
+ makeConfinedDir(entryRoot, join(entryRoot, 'tree'));
211
+ walk(base, '');
212
+ }
213
+ const meta = { schemaVersion: 1, id, at: now(), reason, target, name, destination, kind: manifest.kind, digest: manifest.digest, entries: manifest.entries.map(({ path, type, size, executable, sha256, target: t }) => ({ path, type, size, executable, sha256, target: t })), links, skipped, modes };
214
+ writeConfinedFile(dir, join(dir, 'backup.json'), `${JSON.stringify(meta, null, 2)}\n`, { exclusive: true, mode: 0o600 });
215
+ pruneBackups();
216
+ return id;
217
+ }
218
+
219
+ /** Put a backup back where it was taken from (the destination must be free). */
220
+ function restore(id, { root }) {
221
+ if (!isPlainName(id)) throw new PathConfineError(400, 'Not a backup id.');
222
+ const dir = join(backupsDir, id);
223
+ const meta = JSON.parse(readConfinedFile(dir, join(dir, 'backup.json'), { maxBytes: 8 * 1024 * 1024, encoding: 'utf-8' }));
224
+ const destination = meta.destination;
225
+ if (resolve(dirname(destination)) !== resolve(root)) throw new PathConfineError(403, 'That backup is of another folder.');
226
+ try { lstatSync(destination); throw new PathConfineError(409, `"${destination}" exists: the backup is not put back over it.`); } catch (err) { if (err instanceof PathConfineError) throw err; }
227
+ const rootLink = meta.links.find((l) => l.path === '.');
228
+ if (rootLink) { symlinkSync(rootLink.target, destination); return destination; }
229
+ const modeOf = (path, executable) => backupMode(meta, path, executable);
230
+ if (meta.kind === 'file') {
231
+ const bytes = readConfinedFile(join(dir, 'entry'), join(dir, 'entry', 'file'));
232
+ const mode = modeOf('.', meta.entries[0]?.executable);
233
+ writeConfinedFile(root, destination, bytes, { exclusive: true, mode });
234
+ chmodSync(destination, mode);
235
+ return destination;
236
+ }
237
+ makeConfinedDir(root, destination);
238
+ const real = realPathOf(destination);
239
+ for (const e of meta.entries) {
240
+ if (e.type === 'dir') makeConfinedDir(real, join(real, e.path));
241
+ else if (e.type === 'file') {
242
+ const mode = modeOf(e.path, e.executable);
243
+ writeConfinedFile(real, join(real, e.path), readConfinedFile(join(dir, 'entry'), join(dir, 'entry', 'tree', e.path)), { exclusive: true, mode });
244
+ chmodSync(join(real, e.path), mode);
245
+ }
246
+ }
247
+ for (const l of meta.links) if (l.path !== '.') { try { symlinkSync(l.target, join(real, l.path)); } catch { /* left out, as it is in the backup's list */ } }
248
+ // Folders last, deepest first: a folder that was not writable is made so only after its files are in.
249
+ applyFolderModes(real, meta);
250
+ return destination;
251
+ }
252
+
253
+ // ── Writing a payload ──
254
+
255
+ function writePayload(root, destination, payload) {
256
+ if (payload.kind === 'file') {
257
+ writeConfinedFile(root, destination, payload.bytes, { exclusive: true, mode: payload.executable ? 0o755 : 0o644 });
258
+ return;
259
+ }
260
+ makeConfinedDir(root, destination);
261
+ const real = realPathOf(destination);
262
+ for (const d of payload.dirs) makeConfinedDir(real, join(real, d));
263
+ for (const f of payload.files) writeConfinedFile(real, join(real, f.path), f.bytes, { exclusive: true, mode: f.executable ? 0o755 : 0o644 });
264
+ }
265
+
266
+ function checkPlace(root, destination) {
267
+ if (resolve(dirname(String(destination))) !== resolve(String(root)) || !isPlainName(basename(String(destination)))) {
268
+ throw new PathConfineError(400, 'An install goes into its runtime folder, by a plain name.');
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Install `payload` at `destination` (a plain name in `root`).
274
+ * @param {object} o
275
+ * @param {'claude'|'codex'|'opencode'} o.target @param {string} o.name @param {string} o.root @param {string} o.destination
276
+ * @param {object} o.payload from payloadFromFolder / payloadFromText
277
+ * @param {object} [o.source] where the payload came from ({ kind: 'bundled', name, digest })
278
+ * @param {{ expectedDestinationDigest?: string }} [o.decision] the person confirmed replacing what is there now
279
+ * @returns {{ ok: true, changed: boolean, state: string, backupId: string|null, digest: string }}
280
+ */
281
+ function install({ target, name, root, destination, payload, source = null, decision = {} }) {
282
+ checkPlace(root, destination);
283
+ const { state, current } = inspect({ destination, payload });
284
+ const ledger = readLedger({ forWrite: true });
285
+ const record = { target, name, destination: keyOf(destination), kind: payload.kind, digest: payload.digest, source, installedAt: now() };
286
+ if (state === 'identical') {
287
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination)).concat([{ ...(recordOf(ledger, destination) || {}), ...record }]);
288
+ journal(ledger, { action: 'install', target, name, destination: record.destination, result: 'already-installed', digest: payload.digest });
289
+ writeLedger(ledger);
290
+ return { ok: true, changed: false, state, backupId: null, digest: payload.digest };
291
+ }
292
+ if (state === 'unreadable') refuse('install', { state, current, destination, target, name });
293
+ if ((state === 'modified' || state === 'unowned') && !confirmed(decision, current)) refuse('install', { state, current, destination, target, name });
294
+ let backupId = null;
295
+ if (state !== 'absent') {
296
+ backupId = backup({ destination, target, name, reason: `install over ${state}` });
297
+ // Changed between the look and now: what the backup holds is what was confirmed, or nothing happens.
298
+ const again = readManifest(destination);
299
+ if (again.digest !== current.digest) throw new PathConfineError(409, 'That folder changed while it was being installed over. Nothing was replaced.');
300
+ removeConfinedEntry(root, destination);
301
+ }
302
+ try {
303
+ writePayload(root, destination, payload);
304
+ const written = readManifest(destination);
305
+ if (written.digest !== payload.digest) throw new PathConfineError(500, 'The installed files do not match what was meant to be written.');
306
+ } catch (err) {
307
+ try { removeConfinedEntry(root, destination); } catch { /* nothing more to take away */ }
308
+ if (backupId) { try { restore(backupId, { root }); } catch { /* the backup stays where it is */ } }
309
+ throw err;
310
+ }
311
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination)).concat([{ ...record, backupId }]);
312
+ journal(ledger, { action: 'install', target, name, destination: record.destination, result: state === 'absent' ? 'installed' : `replaced-${state}`, digest: payload.digest, backupId });
313
+ writeLedger(ledger);
314
+ return { ok: true, changed: true, state, backupId, digest: payload.digest };
315
+ }
316
+
317
+ /**
318
+ * Remove what an install put at `destination`.
319
+ * @param {object} o same as install(); `payload` is what installing now would write (a destination that holds
320
+ * exactly that is SynaBun's to remove even without a record)
321
+ * @returns {{ ok: true, changed: boolean, state: string, backupId: string|null }}
322
+ */
323
+ function uninstall({ target, name, root, destination, payload = null, decision = {} }) {
324
+ checkPlace(root, destination);
325
+ const { state, current } = inspect({ destination, payload });
326
+ const ledger = readLedger({ forWrite: true });
327
+ if (state === 'absent') {
328
+ const had = ledger.records.length;
329
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination));
330
+ if (ledger.records.length !== had) { journal(ledger, { action: 'uninstall', target, name, destination: keyOf(destination), result: 'already-absent' }); writeLedger(ledger); }
331
+ return { ok: true, changed: false, state, backupId: null };
332
+ }
333
+ if (state === 'unreadable') refuse('uninstall', { state, current, destination, target, name });
334
+ if ((state === 'modified' || state === 'unowned') && !confirmed(decision, current)) refuse('uninstall', { state, current, destination, target, name });
335
+ const backupId = backup({ destination, target, name, reason: `uninstall ${state}` });
336
+ const again = readManifest(destination);
337
+ if (again.digest !== current.digest) throw new PathConfineError(409, 'That folder changed while it was being removed. Nothing was removed.');
338
+ removeConfinedEntry(root, destination);
339
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination));
340
+ journal(ledger, { action: 'uninstall', target, name, destination: keyOf(destination), result: `removed-${state}`, backupId });
341
+ writeLedger(ledger);
342
+ return { ok: true, changed: true, state, backupId };
343
+ }
344
+
345
+ /** Record something SynaBun wrote by another way (an import, a customised copy): its provenance, for later updates. */
346
+ function recordWrite({ target, name, destination, digest, kind, source }) {
347
+ const ledger = readLedger({ forWrite: true });
348
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination)).concat([{ target, name, destination: keyOf(destination), kind, digest, source, installedAt: now() }]);
349
+ journal(ledger, { action: source?.kind || 'write', target, name, destination: keyOf(destination), result: 'written', digest });
350
+ writeLedger(ledger);
351
+ }
352
+
353
+ /**
354
+ * Stop claiming a destination: what is there is no longer SynaBun's (a rollback put the user's own folder back,
355
+ * or SynaBun's copy was removed by another route). Journaled; nothing on disk changes.
356
+ */
357
+ function forget({ destination, target = null, name = null, reason = 'released' }) {
358
+ const ledger = readLedger({ forWrite: true });
359
+ const had = ledger.records.length;
360
+ ledger.records = ledger.records.filter((r) => keyOf(r.destination) !== keyOf(destination));
361
+ if (ledger.records.length === had) return false;
362
+ journal(ledger, { action: 'release', target, name, destination: keyOf(destination), result: reason });
363
+ writeLedger(ledger);
364
+ return true;
365
+ }
366
+
367
+ /** The ledger's record of a destination, or null. */
368
+ function recordFor(destination) {
369
+ const found = recordOf(ledgerView().ledger, destination);
370
+ return found ? { ...found } : null;
371
+ }
372
+
373
+ /** Append one line to the journal (an action another module carried out on a destination SynaBun owns). */
374
+ function note(entry) {
375
+ const ledger = readLedger({ forWrite: true });
376
+ journal(ledger, entry);
377
+ writeLedger(ledger);
378
+ }
379
+
380
+ /**
381
+ * A backup's files, read back (for a rollback that swaps them into place).
382
+ * @returns {{ meta: object, kind: string, files: {path, bytes, executable}[], dirs: string[], links: object[] }}
383
+ */
384
+ function readBackup(id) {
385
+ if (!isPlainName(id)) throw new PathConfineError(400, 'Not a backup id.');
386
+ const dir = join(backupsDir, id);
387
+ let meta;
388
+ try { meta = JSON.parse(readConfinedFile(dir, join(dir, 'backup.json'), { maxBytes: 8 * 1024 * 1024, encoding: 'utf-8' })); } catch {
389
+ throw new PathConfineError(404, 'That backup is no longer kept.');
390
+ }
391
+ const files = []; const dirs = [];
392
+ if (meta.kind === 'file') {
393
+ files.push({ path: '.', bytes: readConfinedFile(join(dir, 'entry'), join(dir, 'entry', 'file')), executable: !!meta.entries?.[0]?.executable, mode: backupMode(meta, '.', meta.entries?.[0]?.executable) });
394
+ } else if (meta.kind === 'dir') {
395
+ for (const e of meta.entries || []) {
396
+ if (e.type === 'dir') dirs.push(e.path);
397
+ else if (e.type === 'file') files.push({ path: e.path, bytes: readConfinedFile(join(dir, 'entry'), join(dir, 'entry', 'tree', e.path)), executable: !!e.executable, mode: backupMode(meta, e.path, e.executable) });
398
+ }
399
+ }
400
+ const dirModes = {};
401
+ for (const [path, mode] of Object.entries(meta.modes || {})) if (path === '.' || dirs.includes(path)) dirModes[path] = mode & 0o777;
402
+ return { meta, kind: meta.kind, files, dirs, dirModes, links: meta.links || [] };
403
+ }
404
+
405
+ /** How often the ledger file was parsed (it is parsed again only when it changed). */
406
+ const stats = () => ({ ledgerParses: parses });
407
+
408
+ return { inspect, install, uninstall, backup, restore, recordWrite, forget, recordFor, note, readBackup, payloadFromFolder, payloadFromText, readLedger, stats, paths: { ledgerPath, backupsDir } };
409
+ }
410
+
411
+ /**
412
+ * The mode a backed-up file is put back with: its own, as recorded (permission bits only), or, for a backup made
413
+ * before modes were recorded, 0755 / 0644 by its executable bit.
414
+ */
415
+ export function backupMode(meta, path, executable) {
416
+ const recorded = meta?.modes?.[path];
417
+ return Number.isInteger(recorded) ? recorded & 0o777 : (executable ? 0o755 : 0o644);
418
+ }
419
+
420
+ /** Give a restored folder and the folders in it their recorded modes, deepest first. */
421
+ export function applyFolderModes(real, meta) {
422
+ const entries = Object.entries(meta?.modes || {}).filter(([path]) => path === '.' || (meta.entries || []).some((e) => e.type === 'dir' && e.path === path));
423
+ entries.sort((a, b) => (b[0] === '.' ? -1 : b[0].split('/').length) - (a[0] === '.' ? -1 : a[0].split('/').length));
424
+ for (const [path, mode] of entries) {
425
+ try { chmodSync(path === '.' ? real : join(real, path), mode & 0o777); } catch { /* the folder keeps the mode it was made with */ }
426
+ }
427
+ }
@@ -0,0 +1,88 @@
1
+ // Reproducible, checked downloads from immutable revisions. No remote publication or upload.
2
+ import { createHash } from 'node:crypto';
3
+ import AdmZip from 'adm-zip';
4
+ import { buildBundle } from './bundle.js';
5
+ import { buildExport, readHeadOf } from './export.js';
6
+ import { effectiveInstallName } from './scan-context.js';
7
+ import { folderDigest } from './library.js';
8
+ import { PORTABLE_FIELDS } from '../../public/shared/skills/ss-profiles.js';
9
+ import { frontmatter } from './frontmatter.js';
10
+ import { fromEditing, toEditing } from '../../public/shared/skills/ss-doc.js';
11
+
12
+ export const PACKAGE_FORMATS = ['bundle', 'skill', 'portable', 'claude-upload', 'claude-plugin', 'claude-api', 'github'];
13
+ const bad = (code, message, status = 422, extra = {}) => { throw Object.assign(new Error(message), { code, status, ...extra }); };
14
+ export function createSkillPackages({ service }) {
15
+ function prepare(revisionId, { format = 'bundle', profile = null, excludeEvals = false } = {}) {
16
+ if (!PACKAGE_FORMATS.includes(format)) bad('invalid_format', `format is ${PACKAGE_FORMATS.join(', ')}.`, 400);
17
+ if (profile && !service.profileIds().includes(profile)) bad('invalid_profile', 'Unknown export profile.', 400);
18
+ const source = service.library.readRevisionFiles(revisionId);
19
+ const skill = service.library.getSkill(source.revision.skillId);
20
+ const sourceScan = service.scanFolder(source, { name: effectiveInstallName(skill, source.files), digest: source.revision.contentDigest, skill });
21
+ if (sourceScan.status === 'incomplete' || sourceScan.coverage?.complete === false) bad('scan_incomplete', 'The source scan is incomplete.');
22
+ if (sourceScan.gate === 'blocked') bad('scan_blocked', 'The source scan blocks export.');
23
+ let folder = { files: source.files, dirs: source.dirs };
24
+ const losses = [], excluded = [], transforms = [];
25
+ const targetProfile = format === 'claude-upload' || format === 'claude-api' ? 'claude-upload' : format === 'portable' ? 'portable-six-field' : format === 'claude-plugin' ? 'claude-code' : profile;
26
+ if (targetProfile === 'claude-upload' || format === 'portable') {
27
+ const { text, parsed } = readHeadOf(folder.files);
28
+ if (parsed.errors?.length || !parsed.data || typeof parsed.data !== 'object') bad('invalid_frontmatter', 'Fix the frontmatter before exporting.');
29
+ const changes = {};
30
+ for (const key of Object.keys(parsed.data)) if (!PORTABLE_FIELDS.includes(key)) { changes[key] = undefined; losses.push({ field: key, reason: 'This target does not support this runtime behavior.' }); }
31
+ // Lossless patch on the export only. Explicit confirmation of losses is required to download it.
32
+ const result = frontmatter.patch(toEditing(text).text, changes);
33
+ if (!result.ok) bad('export_transform_failed', 'The portable metadata could not be produced.');
34
+ folder = { ...folder, files: folder.files.map((f) => f.path === 'SKILL.md' ? { ...f, bytes: Buffer.from(fromEditing(text, result.text)) } : f) };
35
+ }
36
+ if (excludeEvals) {
37
+ excluded.push(...folder.files.filter((f) => f.path.startsWith('evals/')).map((f) => f.path));
38
+ folder = { files: folder.files.filter((f) => !f.path.startsWith('evals/')), dirs: folder.dirs.filter((d) => d !== 'evals' && !d.startsWith('evals/')) };
39
+ }
40
+ if (targetProfile) { const out = buildExport(folder, targetProfile); folder = out; transforms.push(...out.transforms); if (out.diagnostics.some((d) => d.severity === 'error')) bad('export_incompatible', 'The export cannot preserve invocation policy for this profile.'); }
41
+ const name = effectiveInstallName(skill, folder.files);
42
+ const strictName = ['portable-six-field', 'claude-upload', 'opencode-native'].includes(targetProfile) || format === 'claude-plugin';
43
+ if (strictName && (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64)) bad('invalid_name', 'This target needs a lowercase skill name of at most 64 characters.');
44
+ const lint = service.lintFolder(folder, { profiles: targetProfile ? [targetProfile] : null, folderName: name });
45
+ const profileLint = targetProfile ? lint.profiles[targetProfile] : null;
46
+ // Snapshot formats preserve valid library names; compatibility diagnostics remain in the preview.
47
+ const studioErrors = lint.studio.diagnostics.filter((d) => d.severity === 'error' && (targetProfile || d.ruleId !== 'name-length'));
48
+ const diagnostics = [...studioErrors, ...(profileLint?.diagnostics || []).filter((d) => d.severity === 'error'), ...lint.resources.diagnostics.filter((d) => d.severity === 'error')];
49
+ const digest = folderDigest(folder);
50
+ const scan = service.scanFolder(folder, { name, digest, skill });
51
+ if (diagnostics.length) bad('export_blocked', 'Fix the package validation errors before exporting.', 422, { reason: 'lint', diagnostics });
52
+ if (scan.gate === 'blocked' || scan.coverage?.complete === false) bad(scan.status === 'incomplete' ? 'scan_incomplete' : 'scan_blocked', 'Fix the blocked scan before exporting.');
53
+ const license = readHeadOf(folder.files).parsed.data?.license || null;
54
+ if (format === 'claude-plugin') {
55
+ // Claude plugin layout: components at root; the manifest describes the plugin, never runs commands.
56
+ folder = { files: [
57
+ { path: '.claude-plugin/plugin.json', bytes: Buffer.from(`${JSON.stringify({ name, version: '1.0.0', description: String(readHeadOf(folder.files).parsed.data.description || '').slice(0, 1024) }, null, 2)}\n`) },
58
+ ...folder.files.map((f) => ({ ...f, path: `skills/${name}/${f.path}` })),
59
+ ], dirs: ['.claude-plugin', 'skills', `skills/${name}`, ...folder.dirs.map((d) => `skills/${name}/${d}`)] };
60
+ }
61
+ const artifactDigest = folderDigest(folder);
62
+ return { folder, manifest: { schemaVersion: 1, revisionId, sourceDigest: source.revision.contentDigest, artifactDigest, format, profile: targetProfile,
63
+ name, namingPolicy: strictName ? 'target' : 'preserved', files: folder.files.map((f) => ({ path: f.path, bytes: f.bytes.length, executable: !!f.executable, digest: `sha256:${createHash('sha256').update(f.bytes).digest('hex')}` })), losses, excluded, transforms, license, source: source.revision.source, sourceScan, lint, scan,
64
+ requiresAcknowledgement: losses.length > 0 || sourceScan.gate === 'review' || scan.gate === 'review' || ['github', 'claude-plugin'].includes(format) && !license,
65
+ note: format === 'claude-api' ? 'Offline full snapshot only. Uploading would create a shared remote resource; no API call is made.' : format === 'github' ? 'Reproducible distribution archive only; no push or release is made.' : null } };
66
+ }
67
+ function preview(revisionId, options) { return prepare(revisionId, options).manifest; }
68
+ function download(revisionId, options = {}) {
69
+ const { folder, manifest } = prepare(revisionId, options);
70
+ if (manifest.requiresAcknowledgement && options.acknowledgedDigest !== manifest.artifactDigest) bad('export_review_required', 'Review the losses, license and findings, then acknowledge this exact export digest.', 409);
71
+ const { name, format } = manifest;
72
+ let bytes, filename, contentType;
73
+ if (format === 'bundle') {
74
+ bytes = Buffer.from(buildBundle({ type: 'skill', name, files: folder.files, dirs: folder.dirs }).json); filename = `${name}.skill.json`; contentType = 'application/json';
75
+ } else {
76
+ const zip = new AdmZip({ noSort: true });
77
+ const prefix = format === 'claude-api' ? '' : `${name}/`;
78
+ for (const d of [...folder.dirs].sort()) zip.addFile(`${prefix}${d}/`, Buffer.alloc(0), '', 0o755);
79
+ for (const f of [...folder.files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) zip.addFile(`${prefix}${f.path}`, f.bytes, '', f.executable ? 0o755 : 0o644);
80
+ for (const entry of zip.getEntries()) { entry.header.time = new Date(1980, 0, 1, 0, 0, 0); entry.header.made = 0x0314; }
81
+ bytes = zip.toBuffer(); filename = format === 'skill' ? `${name}${options.profile === 'claude-upload' ? '-claude.zip' : '.skill'}` : `${name}-${format}.zip`; contentType = 'application/zip';
82
+ const back = new AdmZip(bytes).getEntries().filter((e) => !e.isDirectory);
83
+ if (back.length !== folder.files.length || back.some((e) => !folder.files.some((f) => `${prefix}${f.path}` === e.entryName && e.getData().equals(f.bytes)))) bad('package_corrupt', 'The archive did not read back as exported.', 500);
84
+ }
85
+ return { bytes, filename, contentType, manifest: { ...manifest, archiveDigest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` } };
86
+ }
87
+ return { preview, download };
88
+ }