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
@@ -1,2849 +1,52 @@
1
1
  // ═══════════════════════════════════════════
2
2
  // SynaBun Neural Interface — Skills Studio
3
- // Browse, edit, create, import/export Claude Code
4
- // skills, commands, and agents.
5
3
  // ═══════════════════════════════════════════
4
+ //
5
+ // The window where every skill, agent and command the runtimes load is browsed, edited, versioned, published and
6
+ // reviewed: Skills Studio 2.0 (docs/skills-studio.md → Studio 2.0), in public/shared/skills/studio/ and loaded the
7
+ // first time it is opened. This file is what the 2D and 3D pages load: it answers the menu's events, warms the
8
+ // Library after the page painted, and passes on the changes other windows and agents make.
6
9
 
7
- import { emit, on } from './state.js';
8
- import { storage } from './storage.js';
9
- import { safeHref } from './assistant/asst-sanitize.js';
10
- import { isGuest, hasPermission, showGuestToast } from './ui-sync.js';
10
+ import { on } from './state.js';
11
+ import { isGuest, hasPermission } from './ui-sync.js';
11
12
  import { registerLaunchPrefetch } from './launch-prefetch.js';
12
- import {
13
- fetchSkillsLibrary, fetchSkillsArtifact, saveSkillsArtifact,
14
- fetchSkillsSubFile, saveSkillsSubFile, createSkillsSubFile, deleteSkillsSubFile,
15
- createSkillsArtifact, deleteSkillsArtifact, validateSkillsArtifact,
16
- installSkillsBundled, uninstallSkillsBundled,
17
- importSkillsBundleAsking, getSkillsExportUrl,
18
- getSkillsIconUrl, uploadSkillsIcon, deleteSkillsIcon,
19
- } from './api.js';
20
13
 
21
- const $ = (id) => document.getElementById(id);
22
-
23
- // ── Module-local state ──
24
- let _panel = null;
25
- let _backdrop = null;
26
- let _library = [];
27
- let _libraryLoaded = false; // the library was read once: later opens paint from it and read it again behind
28
- let _panelLife = null; // document listeners of the open panel end with it
29
- let _toolsMenuLife = null; // the tools dropdown's outside-click listener (one at a time)
30
- let _projects = [];
31
- let _selected = null; // currently selected artifact
32
- let _selectedContent = null; // loaded content of selected artifact
33
- let _filterType = 'all';
34
- let _filterScope = 'all';
35
- let _searchQuery = '';
36
- let _fetchSeq = 0; // fetch sequence counter — guards against race conditions
37
- let _view = 'library'; // 'library' | 'editor' | 'wizard'
38
- let _editorDirty = false;
39
- let _wizardStep = 0;
40
- let _wizardData = {};
41
- let _previewMode = false; // false = raw editor, true = rendered preview
42
- let _wizPreviewMode = false; // wizard step 4 preview mode
43
-
44
- // ── Tab system ──
45
- // Each tab: { id, artifactId, artifact, artifactContent, label, path, content, originalContent, dirty }
46
- // Tabs persist across artifact selections — clicking a new skill adds a tab
47
- let _tabs = [];
48
- let _activeTabIdx = 0;
49
-
50
- // ── Constants ──
51
- const PANEL_KEY = 'neural-panel-skills-studio';
52
- const TYPE_ICONS = {
53
- skill: `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v2M8 12v2M2 8h2M12 8h2M4.2 4.2l1.4 1.4M10.4 10.4l1.4 1.4M11.8 4.2l-1.4 1.4M5.6 10.4l-1.4 1.4"/><circle cx="8" cy="8" r="2.5"/></svg>`,
54
- command: `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 11 7 8 3 5"/><line x1="9" y1="12" x2="13" y2="12"/></svg>`,
55
- agent: `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="10" height="8" rx="2"/><line x1="6" y1="6" x2="6" y2="6.01"/><line x1="10" y1="6" x2="10" y2="6.01"/><path d="M6 14v-3M10 14v-3"/><path d="M1 7h2M13 7h2"/></svg>`,
14
+ let _studio = null;
15
+ const studio = () => {
16
+ if (!_studio) _studio = import('./skills/studio/sx-main.js').catch((err) => { _studio = null; throw err; });
17
+ return _studio;
56
18
  };
57
- const TYPE_LABELS = { skill: 'Skill', command: 'Command', agent: 'Agent' };
58
- const SCOPE_LABELS = { global: 'Global', project: 'Project', bundled: 'Bundled' };
59
-
60
- // ── Tab helpers ──
61
- function activeTab() {
62
- return _tabs[_activeTabIdx] || _tabs[0] || {
63
- id: '__empty__', artifactId: null, artifact: null, artifactContent: null,
64
- label: '', path: null, content: '', originalContent: '', dirty: false
65
- };
66
- }
67
-
68
- /** Find the main (path===null) tab for the given artifact */
69
- function findMainTab(artifactId) { return _tabs.find(t => t.artifactId === artifactId && t.path === null); }
70
-
71
- function renderTabBar() {
72
- const bar = $('ss-tab-bar');
73
- if (!bar) return;
74
- bar.innerHTML = _tabs.map((tab, i) => {
75
- const active = i === _activeTabIdx ? ' active' : '';
76
- const dirty = tab.dirty ? ' dirty' : '';
77
- const isMain = tab.path === null;
78
- const typeClass = isMain ? ` type-${tab.artifact?.type || 'skill'}` : ' subfile';
79
- const iconSvg = isMain ? `<span class="ss-tab-type-dot ${tab.artifact?.type || 'skill'}"></span>` : '';
80
- const icon = '';
81
- const dot = tab.dirty ? '<span class="ss-tab-dot"></span>' : '';
82
- const close = `<button class="ss-tab-close" data-idx="${i}">\u00d7</button>`;
83
- return `<div class="ss-tab${active}${dirty}${typeClass}" data-idx="${i}">${dot}${iconSvg}<span class="ss-tab-label">${esc(tab.label)}</span>${close}</div>`;
84
- }).join('');
85
-
86
- // Wire tab clicks
87
- bar.querySelectorAll('.ss-tab').forEach(el => {
88
- el.addEventListener('click', (e) => {
89
- if (e.target.closest('.ss-tab-close')) return;
90
- switchTab(parseInt(el.dataset.idx));
91
- });
92
- });
93
-
94
- // Wire close buttons
95
- bar.querySelectorAll('.ss-tab-close').forEach(btn => {
96
- btn.addEventListener('click', (e) => {
97
- e.stopPropagation();
98
- closeTab(parseInt(btn.dataset.idx));
99
- });
100
- });
101
- }
102
-
103
- function switchTab(idx) {
104
- if (idx === _activeTabIdx || idx < 0 || idx >= _tabs.length) return;
105
- storeActiveTabContent();
106
- captureMetadataIntoContent();
107
19
 
108
- const prevTab = activeTab();
109
- _activeTabIdx = idx;
110
- const newTab = activeTab();
111
- _previewMode = false;
112
-
113
- // Update artifact context
114
- _selected = newTab.artifact;
115
- _selectedContent = newTab.artifactContent;
116
-
117
- // If different artifact, re-render everything (metadata panel changes)
118
- if (prevTab.artifactId !== newTab.artifactId) {
119
- renderEditor();
120
- renderLibrary();
121
- } else {
122
- // Same artifact — just swap content
123
- loadActiveTabContent();
124
- renderTabBar();
125
- updateDirtyState();
126
- }
127
- }
128
-
129
- function closeTab(idx) {
130
- if (idx < 0 || idx >= _tabs.length) return;
131
- const tab = _tabs[idx];
132
- if (tab.dirty && !confirm(`Discard changes to "${tab.label}"?`)) return;
133
- _tabs.splice(idx, 1);
134
-
135
- // If no tabs left, go back to library
136
- if (_tabs.length === 0) {
137
- _activeTabIdx = 0;
138
- switchToLibrary();
139
- return;
140
- }
141
-
142
- // Adjust active index
143
- if (_activeTabIdx > idx) _activeTabIdx--;
144
- else if (_activeTabIdx >= _tabs.length) _activeTabIdx = _tabs.length - 1;
145
-
146
- // Update artifact context from the new active tab
147
- const newTab = activeTab();
148
- _selected = newTab.artifact;
149
- _selectedContent = newTab.artifactContent;
150
-
151
- renderEditor();
152
- renderLibrary();
153
- }
154
-
155
- function storeActiveTabContent() {
156
- const editor = $('ss-content-editor');
157
- if (!editor) return;
158
- const tab = _tabs[_activeTabIdx];
159
- if (!tab) return;
160
- tab.content = editor.value;
161
- // Only ADD dirtiness, never clear it — dirty is cleared explicitly by save/discard
162
- if (tab.content !== tab.originalContent) tab.dirty = true;
163
- }
164
-
165
- /** Merge metadata form values into tab content for the active main tab.
166
- * Called before saving and before switching away from a tab. */
167
- function captureMetadataIntoContent() {
168
- const tab = _tabs[_activeTabIdx];
169
- if (!tab || tab.path !== null) return; // only for main tabs
170
- const meta = collectMetadataFromForm();
171
- if (!meta) return;
172
- const editor = $('ss-content-editor');
173
- const raw = editor ? editor.value : tab.content;
174
- tab.content = mergeMetadataIntoContent(raw, meta);
175
- if (editor) editor.value = tab.content;
176
- if (tab.content !== tab.originalContent) tab.dirty = true;
177
- }
178
-
179
- function loadActiveTabContent() {
180
- const editor = $('ss-content-editor');
181
- const preview = $('ss-md-preview');
182
- if (!editor) return;
183
- const tab = activeTab();
184
- editor.value = tab.content;
185
- // Reset preview
186
- if (preview) { preview.classList.remove('visible'); preview.innerHTML = ''; }
187
- editor.classList.remove('ss-hidden');
188
- _previewMode = false;
189
- // Update toggle buttons
190
- _panel?.querySelectorAll('.ss-view-toggle-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'raw'));
191
- // Show/hide toolbar tools
192
- _panel?.querySelectorAll('#ss-md-toolbar .ss-md-tool').forEach(t => t.style.visibility = '');
193
- }
194
-
195
- function updateDirtyState() {
196
- const anyDirty = _tabs.some(t => t.dirty);
197
- _editorDirty = anyDirty;
198
- const saveBtn = $('ss-save');
199
- const discardBtn = $('ss-discard');
200
- const tab = activeTab();
201
- if (saveBtn) { saveBtn.disabled = !tab.dirty; saveBtn.classList.toggle('dirty', tab.dirty); }
202
- if (discardBtn) discardBtn.disabled = !tab.dirty;
203
- }
204
-
205
- async function openFileInTab(path) {
206
- if (!_selected) return;
207
- // Check if already open (same artifact + path)
208
- const existingIdx = _tabs.findIndex(t => t.artifactId === _selected.id && t.path === path);
209
- if (existingIdx >= 0) { switchTab(existingIdx); return; }
210
- storeActiveTabContent();
211
- try {
212
- const data = await fetchSkillsSubFile(encodeId(_selected.id), path);
213
- const label = path.includes('/') ? path.split('/').pop() : path;
214
- _tabs.push({
215
- id: `${_selected.id}:${path}`, artifactId: _selected.id,
216
- artifact: _selected, artifactContent: _selectedContent,
217
- label, path, content: data.content, originalContent: data.content, dirty: false,
218
- });
219
- _activeTabIdx = _tabs.length - 1;
220
- _previewMode = false;
221
- loadActiveTabContent();
222
- renderTabBar();
223
- } catch (err) {
224
- toast('Failed to load file: ' + err.message, 'error');
225
- }
226
- }
227
-
228
- // ── Helper ──
229
- function esc(s) {
230
- if (!s) return '';
231
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
232
- }
233
-
234
- function encodeId(id) { return btoa(id).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
235
-
236
- function toast(msg, type = 'info') {
237
- const t = document.createElement('div');
238
- t.className = `ss-toast ss-toast--${type}`;
239
- t.textContent = msg;
240
- document.body.appendChild(t);
241
- requestAnimationFrame(() => t.classList.add('show'));
242
- setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 300); }, 2600);
243
- }
244
-
245
- // ═══════════════════════════════════════════
246
- // PUBLIC API
247
- // ═══════════════════════════════════════════
20
+ /** The file types the menu's "Import…" offers (sx-import.js BUNDLE_ACCEPT). */
21
+ const BUNDLE_ACCEPT = '.json,.skill.json,.command.json,.agent.json';
248
22
 
249
23
  export function initSkillsStudio() {
250
- // Launch: the library is read once the page has painted (launch-prefetch.js), so the first open paints at once.
24
+ // Launch: the Library is read once the page has painted (launch-prefetch.js), so the first open paints at once.
251
25
  registerLaunchPrefetch('skills-library', prefetchSkillsLibrary, { order: 70 });
252
- on('skills:open', () => openPanel());
253
- on('skills:open-wizard', () => { openPanel(); switchToWizard(); });
254
- on('skills:import', () => { openPanel(); triggerImport(); });
255
- }
256
-
257
- // ═══════════════════════════════════════════
258
- // PANEL LIFECYCLE
259
- // ═══════════════════════════════════════════
260
-
261
- async function openPanel() {
262
- if (isGuest() && !hasPermission('skills')) {
263
- showGuestToast('Skills Studio is disabled by the host');
264
- return;
265
- }
266
- if (_panel) { _panel.focus(); return; }
267
-
268
- // Backdrop
269
- _backdrop = document.createElement('div');
270
- _backdrop.className = 'studio-backdrop';
271
- // Backdrop click disabled — close only via ESC or close button
272
- document.body.appendChild(_backdrop);
273
-
274
- // Panel
275
- _panel = document.createElement('div');
276
- _panel.className = 'skills-studio-panel glass resizable';
277
- _panel.id = 'skills-studio-panel';
278
- _panel.innerHTML = buildPanelHTML();
279
- document.body.appendChild(_panel);
280
-
281
- // Always open centered at default size (matches Automation Studio)
282
- _panel.style.left = Math.max(20, (window.innerWidth - 900) / 2) + 'px';
283
- _panel.style.top = Math.max(48, (window.innerHeight - 520) / 2) + 'px';
284
-
285
- wirePanel();
286
- if (_libraryLoaded) {
287
- // The library as last read paints at once; it is read again behind it and the list follows in place.
288
- const countEl = $('ss-total-count');
289
- if (countEl) countEl.textContent = _library.length;
290
- renderLibrary();
291
- renderView();
292
- requestAnimationFrame(() => { _backdrop?.classList.add('open'); _panel?.classList.add('open'); });
293
- refreshLibraryInPlace();
294
- return;
295
- }
296
- await loadLibrary();
297
- renderView();
298
-
299
- requestAnimationFrame(() => { _backdrop.classList.add('open'); _panel.classList.add('open'); });
300
- }
301
-
302
- function closePanel() {
303
- if (!_panel) return;
304
- _panelLife?.abort();
305
- _panelLife = null;
306
- _toolsMenuLife?.abort();
307
- _toolsMenuLife = null;
308
- if (_backdrop) { _backdrop.remove(); _backdrop = null; }
309
- _panel.remove();
310
- _panel = null;
311
- _selected = null;
312
- _selectedContent = null;
313
- _view = 'library';
314
- _editorDirty = false;
315
- _tabs = [];
316
- _activeTabIdx = 0;
317
- }
318
-
319
- function buildPanelHTML() {
320
- return `
321
- <div class="resize-handle resize-handle-t" data-resize="t"></div>
322
- <div class="resize-handle resize-handle-r" data-resize="r"></div>
323
- <div class="resize-handle resize-handle-b" data-resize="b"></div>
324
- <div class="resize-handle resize-handle-l" data-resize="l"></div>
325
- <div class="resize-handle resize-handle-tl" data-resize="tl"></div>
326
- <div class="resize-handle resize-handle-tr" data-resize="tr"></div>
327
- <div class="resize-handle resize-handle-bl" data-resize="bl"></div>
328
- <div class="resize-handle resize-handle-br" data-resize="br"></div>
329
-
330
- <div class="ss-header drag-handle" data-drag="skills-studio-panel">
331
- <div class="ss-header-left">
332
- <h3>Skills Studio</h3>
333
- <span class="ss-count" id="ss-total-count"></span>
334
- </div>
335
- <div class="ss-header-actions">
336
- <button class="ss-header-btn" id="ss-new-btn">+ New</button>
337
- <button class="ss-header-btn" id="ss-import-btn">Import</button>
338
- </div>
339
- <button class="backdrop-toggle-btn" id="ss-backdrop-toggle" data-tooltip="Toggle backdrop">
340
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
341
- </button>
342
- <button class="ss-close" id="ss-close">&times;</button>
343
- </div>
344
-
345
- <div class="ss-body">
346
- <aside class="ss-sidebar" id="ss-sidebar">
347
- <div class="ss-search-wrap">
348
- <input type="text" class="ss-search" id="ss-search" placeholder="Filter..." autocomplete="off" spellcheck="false">
349
- </div>
350
- <div class="ss-filters">
351
- <button class="ss-filter active" data-type="all">All</button>
352
- <button class="ss-filter" data-type="skill"><span class="ss-filter-dot skill"></span> Skills</button>
353
- <button class="ss-filter" data-type="command"><span class="ss-filter-dot command"></span> Cmds</button>
354
- <button class="ss-filter" data-type="agent"><span class="ss-filter-dot agent"></span> Agents</button>
355
- </div>
356
- <div class="ss-library" id="ss-library"></div>
357
- </aside>
358
- <main class="ss-main" id="ss-main"></main>
359
- </div>
360
- `;
361
- }
362
-
363
- function wirePanel() {
364
- _panelLife?.abort();
365
- _panelLife = new AbortController();
366
- // Close button
367
- $('ss-close')?.addEventListener('click', closePanel);
368
-
369
- // Header buttons
370
- $('ss-new-btn')?.addEventListener('click', () => switchToWizard());
371
- $('ss-import-btn')?.addEventListener('click', () => triggerImport());
372
-
373
- $('ss-backdrop-toggle')?.addEventListener('click', () => {
374
- if (_backdrop) {
375
- _backdrop.classList.toggle('backdrop-hidden');
376
- $('ss-backdrop-toggle')?.classList.toggle('active', _backdrop.classList.contains('backdrop-hidden'));
377
- }
378
- });
379
-
380
- // Search
381
- $('ss-search')?.addEventListener('input', (e) => {
382
- _searchQuery = e.target.value.toLowerCase();
383
- renderLibrary();
384
- });
385
-
386
- // Type filter pills
387
- _panel.querySelectorAll('.ss-filter[data-type]').forEach(btn => {
388
- btn.addEventListener('click', () => {
389
- _filterType = btn.dataset.type;
390
- _panel.querySelectorAll('.ss-filter[data-type]').forEach(b => b.classList.toggle('active', b === btn));
391
- renderLibrary();
26
+ on('skills:open', (detail) => { studio().then((m) => m.openStudio(detail && typeof detail === 'object' ? detail : {})).catch(failed); });
27
+ on('skills:open-wizard', () => { studio().then((m) => m.openStudio({ place: 'create', create: 'blank' })).catch(failed); });
28
+ // A file picker opens only in the click that asked for it: it is opened here, at once, and the Studio (loaded
29
+ // while the file is chosen) imports what was picked.
30
+ on('skills:import', () => {
31
+ const input = document.createElement('input');
32
+ input.type = 'file';
33
+ input.accept = BUNDLE_ACCEPT;
34
+ input.addEventListener('change', () => {
35
+ const file = input.files?.[0];
36
+ if (file) studio().then((m) => m.openStudioWithImport(file)).catch(failed);
392
37
  });
38
+ studio().catch(() => {});
39
+ input.click();
393
40
  });
394
-
395
- // Escape key
396
- const onEsc = (e) => {
397
- if (e.key === 'Escape' && _panel) {
398
- // Close current tab if in editor
399
- if (_view === 'editor' && _tabs.length > 0) { closeTab(_activeTabIdx); return; }
400
- if (_view === 'wizard') { switchToLibrary(); return; }
401
- closePanel();
402
- }
403
- };
404
- // Removed with the panel, however it closes (the close button too).
405
- document.addEventListener('keydown', onEsc, { signal: _panelLife.signal });
406
-
407
- // Drag/resize are handled by the shared panel system via drag-handle/resize-handle.
408
- }
409
-
410
- // ═══════════════════════════════════════════
411
- // DATA LOADING
412
- // ═══════════════════════════════════════════
413
-
414
- async function loadLibrary() {
415
- try {
416
- const data = await fetchSkillsLibrary();
417
- _library = data.artifacts || [];
418
- _projects = data.projects || [];
419
- _libraryLoaded = true;
420
- const countEl = $('ss-total-count');
421
- if (countEl) countEl.textContent = _library.length;
422
- renderLibrary();
423
- } catch (err) {
424
- console.error('Skills Studio: failed to load library', err);
425
- toast('Failed to load skills library', 'error');
426
- }
41
+ on('sync:skills:changed', (msg) => { if (_studio) _studio.then((m) => m.onSkillsChanged(msg?.event || {})).catch(() => {}); });
427
42
  }
428
43
 
429
- // The library read again behind a panel painted from the kept one: the list is redrawn only when it changed,
430
- // at the scroll position it had (a failed read leaves the kept library as it is).
431
- async function refreshLibraryInPlace() {
432
- const before = JSON.stringify([_library, _projects]);
433
- try {
434
- const data = await fetchSkillsLibrary();
435
- const next = [data.artifacts || [], data.projects || []];
436
- if (!_panel || JSON.stringify(next) === before) return;
437
- [_library, _projects] = next;
438
- const list = $('ss-library');
439
- const scroll = list ? list.scrollTop : 0;
440
- const countEl = $('ss-total-count');
441
- if (countEl) countEl.textContent = _library.length;
442
- renderLibrary();
443
- if (list) list.scrollTop = scroll;
444
- } catch { /* the kept library stays */ }
445
- }
446
-
447
- /** Launch prefetch: the library read once, so the first open paints at once (nothing is drawn here). */
44
+ /** Launch prefetch: the Library read once, so the first open paints at once (nothing is drawn here). */
448
45
  export function prefetchSkillsLibrary() {
449
- if (_libraryLoaded || (isGuest() && !hasPermission('skills'))) return Promise.resolve();
450
- return fetchSkillsLibrary().then((data) => {
451
- if (_libraryLoaded) return;
452
- _library = data.artifacts || [];
453
- _projects = data.projects || [];
454
- _libraryLoaded = true;
455
- }).catch(() => { /* the first open reads it */ });
456
- }
457
-
458
- // ═══════════════════════════════════════════
459
- // VIEW SWITCHING
460
- // ═══════════════════════════════════════════
461
-
462
- function renderView() {
463
- if (_view === 'library') renderLibraryMain();
464
- else if (_view === 'editor') renderEditor();
465
- else if (_view === 'wizard') renderWizard();
466
- }
467
-
468
- function switchToLibrary() {
469
- _view = 'library';
470
- _selected = null;
471
- _selectedContent = null;
472
- _editorDirty = false;
473
- // Tabs are already empty when this is called from closeTab
474
- renderLibrary();
475
- renderView();
476
- }
477
-
478
- async function switchToEditor(artifact) {
479
- // Check if this artifact already has a main tab open
480
- const existingIdx = _tabs.findIndex(t => t.artifactId === artifact.id && t.path === null);
481
- if (existingIdx >= 0) {
482
- storeActiveTabContent();
483
- captureMetadataIntoContent();
484
- _activeTabIdx = existingIdx;
485
- _selected = _tabs[existingIdx].artifact;
486
- _selectedContent = _tabs[existingIdx].artifactContent;
487
- _view = 'editor';
488
- _previewMode = false;
489
- renderEditor();
490
- renderLibrary();
491
- return;
492
- }
493
-
494
- // Save current tab content before switching
495
- if (_tabs.length > 0) {
496
- storeActiveTabContent();
497
- captureMetadataIntoContent();
498
- }
499
-
500
- // Reuse the active main tab if it's clean (not dirty, not a sub-file).
501
- // This prevents unbounded tab accumulation — only dirty or sub-file tabs persist.
502
- const curTab = activeTab();
503
- const reuseIdx = (curTab.path === null && !curTab.dirty && _tabs.length > 0)
504
- ? _activeTabIdx : -1;
505
-
506
- // Guard against race conditions: if user clicks quickly, only the latest wins
507
- const seq = ++_fetchSeq;
508
-
509
- _view = 'editor';
510
- _selected = artifact; // optimistically set for sidebar highlight
511
- renderLibrary();
512
- const main = $('ss-main');
513
- if (main) main.innerHTML = '<div class="ss-loading">Loading...</div>';
514
- try {
515
- const id = encodeId(artifact.id);
516
- const data = await fetchSkillsArtifact(id);
517
- // Stale fetch — user already clicked something else
518
- if (seq !== _fetchSeq) return;
519
-
520
- _selected = artifact;
521
- _selectedContent = data;
522
-
523
- const tabObj = {
524
- id: `main:${artifact.id}`, artifactId: artifact.id,
525
- artifact: artifact, artifactContent: data,
526
- label: artifact.name, path: null,
527
- content: data.rawContent || '', originalContent: data.rawContent || '', dirty: false,
528
- };
529
-
530
- if (reuseIdx >= 0 && reuseIdx < _tabs.length) {
531
- // Replace the clean main tab in-place
532
- _tabs[reuseIdx] = tabObj;
533
- _activeTabIdx = reuseIdx;
534
- } else {
535
- _tabs.push(tabObj);
536
- _activeTabIdx = _tabs.length - 1;
537
- }
538
- _previewMode = false;
539
- renderEditor();
540
- renderLibrary();
541
- } catch (err) {
542
- if (seq !== _fetchSeq) return;
543
- console.error('Failed to load artifact', err);
544
- toast('Failed to load artifact', 'error');
545
- // If we have other tabs, stay on them
546
- if (_tabs.length > 0) {
547
- const tab = activeTab();
548
- _selected = tab.artifact;
549
- _selectedContent = tab.artifactContent;
550
- renderEditor();
551
- renderLibrary();
552
- } else {
553
- switchToLibrary();
554
- }
555
- }
556
- }
557
-
558
- function switchToWizard() {
559
- _view = 'wizard';
560
- _wizardStep = 0;
561
- _wizPreviewMode = false;
562
- _wizardData = { type: 'skill', scope: 'global', template: 'blank', name: '', description: '', projectPath: '' };
563
- renderView();
564
- }
565
-
566
- // ═══════════════════════════════════════════
567
- // LIBRARY SIDEBAR
568
- // ═══════════════════════════════════════════
569
-
570
- function renderLibrary() {
571
- const container = $('ss-library');
572
- if (!container) return;
573
-
574
- const filtered = _library.filter(a => {
575
- if (_filterType !== 'all' && a.type !== _filterType) return false;
576
- if (_filterScope !== 'all' && a.scope !== _filterScope) return false;
577
- if (_searchQuery) {
578
- const hay = `${a.name} ${a.description}`.toLowerCase();
579
- if (!hay.includes(_searchQuery)) return false;
580
- }
581
- return true;
582
- });
583
-
584
- // Group by scope
585
- const groups = {};
586
- for (const a of filtered) {
587
- const key = a.scope === 'project' ? `project:${a.scopeLabel || 'Project'}` : a.scope;
588
- if (!groups[key]) groups[key] = [];
589
- groups[key].push(a);
590
- }
591
-
592
- // Render order
593
- const order = ['bundled', 'global'];
594
- // Add project groups
595
- for (const key of Object.keys(groups)) {
596
- if (key.startsWith('project:')) order.push(key);
597
- }
598
-
599
- let html = '';
600
- for (const key of order) {
601
- const items = groups[key];
602
- if (!items || items.length === 0) continue;
603
- const label = key.startsWith('project:') ? key.split(':')[1] : SCOPE_LABELS[key] || key;
604
- html += `<div class="ss-group">
605
- <div class="ss-group-header"><span class="ss-group-title">${esc(label)}</span><span class="ss-group-count">${items.length}</span></div>
606
- <div class="ss-group-body">`;
607
- for (const a of items.sort((x, y) => x.name.localeCompare(y.name))) {
608
- const active = _selected && _selected.id === a.id ? ' active' : '';
609
- const bundledBadge = (a.scope === 'bundled' && !a.installed) ? '<span class="ss-badge ss-badge--muted">not installed</span>' : '';
610
- const installedFromBundled = a.bundledSource ? '<span class="ss-badge ss-badge--blue">bundled</span>' : '';
611
- const iconHtml = a.hasIcon
612
- ? `<span class="ss-item-icon ${a.type} has-custom"><img src="${getSkillsIconUrl(encodeId(a.id))}" alt="" class="ss-item-custom-icon"></span>`
613
- : `<span class="ss-item-icon ${a.type}">${TYPE_ICONS[a.type] || '?'}</span>`;
614
- html += `<div class="ss-item${active}" data-id="${esc(a.id)}">
615
- ${iconHtml}
616
- <div class="ss-item-info">
617
- <div class="ss-item-name">${esc(a.name)}${bundledBadge}${installedFromBundled}</div>
618
- <div class="ss-item-desc">${esc(a.description?.substring(0, 100))}</div>
619
- </div>
620
- </div>`;
621
- }
622
- html += '</div></div>';
623
- }
624
-
625
- if (!html) html = '<div class="ss-empty">No skills found</div>';
626
- container.innerHTML = html;
627
-
628
- // Wire click handlers
629
- container.querySelectorAll('.ss-item').forEach(el => {
630
- el.addEventListener('click', () => {
631
- const id = el.dataset.id;
632
- const artifact = _library.find(a => a.id === id);
633
- if (artifact) switchToEditor(artifact);
634
- });
635
- });
636
- }
637
-
638
- // ═══════════════════════════════════════════
639
- // LIBRARY MAIN (welcome/overview)
640
- // ═══════════════════════════════════════════
641
-
642
- function renderLibraryMain() {
643
- const main = $('ss-main');
644
- if (!main) return;
645
-
646
- const skillCount = _library.filter(a => a.type === 'skill').length;
647
- const cmdCount = _library.filter(a => a.type === 'command').length;
648
- const agentCount = _library.filter(a => a.type === 'agent').length;
649
-
650
- main.innerHTML = `
651
- <div class="ss-welcome">
652
- <div class="ss-welcome-hero">
653
- <img src="synabun.png?v=2" alt="SynaBun" class="ss-welcome-logo">
654
- <h2>Skills Studio</h2>
655
- </div>
656
- <p class="ss-welcome-sub">Browse, edit, and create Claude Code skills, commands, and agents.</p>
657
- <div class="ss-stats-row">
658
- <div class="ss-stat">
659
- <span class="ss-stat-num">${skillCount}</span>
660
- <span class="ss-stat-label">Skills</span>
661
- </div>
662
- <div class="ss-stat">
663
- <span class="ss-stat-num">${cmdCount}</span>
664
- <span class="ss-stat-label">Commands</span>
665
- </div>
666
- <div class="ss-stat">
667
- <span class="ss-stat-num">${agentCount}</span>
668
- <span class="ss-stat-label">Agents</span>
669
- </div>
670
- </div>
671
- <p class="ss-welcome-hint">Select an item from the sidebar, or create a new one.</p>
672
- </div>
673
- `;
674
- }
675
-
676
- // ═══════════════════════════════════════════
677
- // EDITOR VIEW
678
- // ═══════════════════════════════════════════
679
-
680
- function renderEditor() {
681
- const main = $('ss-main');
682
- if (!main || !_selected || !_selectedContent) return;
683
-
684
- const a = _selected;
685
- const c = _selectedContent;
686
- const isSkill = a.type === 'skill';
687
- const isAgent = a.type === 'agent';
688
- const isCommand = a.type === 'command';
689
- const isBundled = a.scope === 'bundled';
690
-
691
- main.innerHTML = `
692
- <div class="ss-editor-header">
693
- <button class="ss-back-btn" id="ss-back">\u2190 Library</button>
694
- <span class="ss-editor-name">${esc(a.name)}</span>
695
- <span class="ss-badge ss-badge--${a.type}"><span class="ss-filter-dot ${a.type}"></span>${TYPE_LABELS[a.type]}</span>
696
- <span class="ss-badge ss-badge--scope">${a.scopeLabel || SCOPE_LABELS[a.scope]}</span>
697
- <div class="ss-editor-spacer"></div>
698
- <button class="ss-header-btn ss-export-btn" id="ss-export">\u2193 Export</button>
699
- ${isBundled ? `<button class="ss-header-btn ss-install-btn" id="ss-install-btn">Install</button>` : ''}
700
- <button class="ss-header-btn ss-delete-btn" id="ss-delete-btn">\u2715 Delete</button>
701
- <button class="ss-header-btn ss-discard-btn" id="ss-discard" disabled>Discard</button>
702
- <button class="ss-header-btn ss-save-btn" id="ss-save" disabled>Save</button>
703
- </div>
704
-
705
- <div class="ss-editor-body">
706
- <div class="ss-editor-meta" id="ss-editor-meta">
707
- ${buildMetadataForm(a, c.frontmatter)}
708
- ${isSkill && c.subFiles && c.subFiles.length > 0 ? buildFileTree(c.subFiles) : ''}
709
- ${isSkill ? '<button class="ss-add-file-btn" id="ss-add-file">+ Add File</button>' : ''}
710
- </div>
711
- <div class="ss-editor-content">
712
- <div class="ss-tab-bar" id="ss-tab-bar"></div>
713
- <div class="ss-md-toolbar" id="ss-md-toolbar">
714
- <button class="ss-md-tool" data-cmd="heading" data-tooltip="Heading (Ctrl+H)"><svg viewBox="0 0 24 24"><path d="M4 4v16M20 4v16M4 12h16"/></svg></button>
715
- <button class="ss-md-tool" data-cmd="bold" data-tooltip="Bold (Ctrl+B)"><b style="font-size:13px">B</b></button>
716
- <button class="ss-md-tool" data-cmd="italic" data-tooltip="Italic (Ctrl+I)"><i style="font-size:13px">I</i></button>
717
- <button class="ss-md-tool" data-cmd="strikethrough" data-tooltip="Strikethrough"><s style="font-size:12px">S</s></button>
718
- <div class="ss-md-toolbar-sep"></div>
719
- <button class="ss-md-tool" data-cmd="ul" data-tooltip="Bullet list"><svg viewBox="0 0 24 24"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="5" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="5" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg></button>
720
- <button class="ss-md-tool" data-cmd="ol" data-tooltip="Numbered list"><svg viewBox="0 0 24 24"><line x1="10" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/><line x1="10" y1="18" x2="20" y2="18"/><text x="4" y="8" font-size="7" fill="currentColor" stroke="none" font-weight="600">1</text><text x="4" y="14" font-size="7" fill="currentColor" stroke="none" font-weight="600">2</text><text x="4" y="20" font-size="7" fill="currentColor" stroke="none" font-weight="600">3</text></svg></button>
721
- <button class="ss-md-tool" data-cmd="quote" data-tooltip="Blockquote"><svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h12"/><rect x="1" y="5" width="2" height="14" rx="1" fill="currentColor" stroke="none" opacity="0.4"/></svg></button>
722
- <div class="ss-md-toolbar-sep"></div>
723
- <button class="ss-md-tool" data-cmd="code" data-tooltip="Inline code"><svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></button>
724
- <button class="ss-md-tool" data-cmd="codeblock" data-tooltip="Code block"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="9 8 5 12 9 16" stroke-width="1.5"/><polyline points="15 8 19 12 15 16" stroke-width="1.5"/></svg></button>
725
- <button class="ss-md-tool" data-cmd="link" data-tooltip="Link (Ctrl+K)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
726
- <button class="ss-md-tool" data-cmd="hr" data-tooltip="Horizontal rule">&mdash;</button>
727
- <div class="ss-view-toggle">
728
- <button class="ss-view-toggle-btn${_previewMode ? '' : ' active'}" data-mode="raw">Raw</button>
729
- <button class="ss-view-toggle-btn${_previewMode ? ' active' : ''}" data-mode="preview">Preview</button>
730
- </div>
731
- </div>
732
- <textarea class="ss-md-editor${_previewMode ? ' ss-hidden' : ''}" id="ss-content-editor" spellcheck="false" autocorrect="off" autocapitalize="off">${esc(activeTab().content)}</textarea>
733
- <div class="ss-md-preview${_previewMode ? ' visible' : ''}" id="ss-md-preview">${_previewMode ? renderMarkdown(activeTab().content) : ''}</div>
734
- </div>
735
- </div>
736
- `;
737
-
738
- wireEditor();
739
- }
740
-
741
- function buildMetadataForm(artifact, fm) {
742
- const isSkill = artifact.type === 'skill';
743
- const isAgent = artifact.type === 'agent';
744
- const isBundled = artifact.scope === 'bundled';
745
- let html = '<div class="ss-meta-section">';
746
-
747
- // Icon upload (all types, unless bundled/read-only)
748
- if (!isBundled) {
749
- const encodedId = encodeId(artifact.id || '');
750
- const hasIcon = artifact.hasIcon;
751
- html += `<div class="ss-field ss-icon-field">
752
- <label>Icon</label>
753
- <div class="ss-icon-upload" id="ss-icon-upload">
754
- <div class="ss-icon-preview" id="ss-icon-preview">
755
- ${hasIcon
756
- ? `<img src="${getSkillsIconUrl(encodedId)}?t=${Date.now()}" alt="" class="ss-icon-preview-img">`
757
- : `<div class="ss-icon-placeholder ${artifact.type}">${TYPE_ICONS[artifact.type] || '?'}</div>`
758
- }
759
- </div>
760
- <div class="ss-icon-actions">
761
- <label class="ss-icon-upload-btn" id="ss-icon-upload-label">
762
- <input type="file" id="ss-icon-file" accept=".png,.svg,.jpg,.jpeg,.webp,image/png,image/svg+xml,image/jpeg,image/webp" hidden>
763
- ${hasIcon ? 'Change' : 'Upload'}
764
- </label>
765
- ${hasIcon ? '<button class="ss-icon-remove-btn" id="ss-icon-remove" type="button">Remove</button>' : ''}
766
- </div>
767
- </div>
768
- </div>`;
769
- }
770
-
771
- // Name (skill + agent only)
772
- if (isSkill || isAgent) {
773
- html += `<div class="ss-field">
774
- <label>Name</label>
775
- <input type="text" class="ss-input" id="ss-f-name" value="${esc(fm.name || '')}" data-fm="name">
776
- </div>`;
777
- }
778
-
779
- // Description (all types)
780
- html += `<div class="ss-field">
781
- <label>Description</label>
782
- <textarea class="ss-textarea ss-textarea-sm" id="ss-f-desc" data-fm="description">${esc(fm.description || '')}</textarea>
783
- </div>`;
784
-
785
- // Argument hint (skill only)
786
- if (isSkill) {
787
- html += `<div class="ss-field">
788
- <label>Argument Hint</label>
789
- <input type="text" class="ss-input" id="ss-f-arghint" value="${esc(fm['argument-hint'] || '')}" data-fm="argument-hint">
790
- </div>`;
791
- }
792
-
793
- // Allowed tools (skill only)
794
- if (isSkill) {
795
- const tools = fm['allowed-tools'] || [];
796
- const allTools = ['Read','Write','Edit','Bash','Grep','Glob','WebFetch','WebSearch','Task','NotebookEdit'];
797
- html += `<div class="ss-field">
798
- <label>Allowed Tools</label>
799
- <div class="ss-tools-list" id="ss-tools-list">
800
- ${tools.map(t => `<span class="ss-chip">${esc(t)}<button class="ss-chip-x" data-tool="${esc(t)}">\u00d7</button></span>`).join('')}
801
- </div>
802
- <div class="ss-dropdown" id="ss-tools-dropdown">
803
- <button class="ss-dropdown-trigger" id="ss-tools-trigger" type="button">
804
- <span>Add tool...</span>
805
- <svg viewBox="0 0 10 6" width="10" height="6"><path d="M0 0l5 6 5-6z" fill="currentColor"/></svg>
806
- </button>
807
- <div class="ss-dropdown-menu" id="ss-tools-menu">
808
- ${allTools.map(t =>
809
- `<div class="ss-dropdown-item${tools.includes(t) ? ' disabled' : ''}" data-value="${esc(t)}">${esc(t)}</div>`
810
- ).join('')}
811
- </div>
812
- </div>
813
- </div>`;
814
- }
815
-
816
- // Options (skill only)
817
- if (isSkill) {
818
- html += `<div class="ss-field">
819
- <label>Options</label>
820
- <div class="ss-checkbox-row">
821
- <input type="checkbox" id="ss-f-user-invocable" ${fm['user-invocable'] !== false ? 'checked' : ''}>
822
- <label for="ss-f-user-invocable">User-invocable</label>
823
- </div>
824
- <div class="ss-checkbox-row">
825
- <input type="checkbox" id="ss-f-disable-model" ${fm['disable-model-invocation'] ? 'checked' : ''}>
826
- <label for="ss-f-disable-model">Disable model invocation</label>
827
- </div>
828
- </div>`;
829
- }
830
-
831
- // Agent-specific fields
832
- if (isAgent) {
833
- html += `<div class="ss-field">
834
- <label>Model</label>
835
- <select class="ss-input" id="ss-f-model" data-fm="model">
836
- <option value="" ${!fm.model ? 'selected' : ''}>inherit (default)</option>
837
- <option value="sonnet" ${fm.model === 'sonnet' ? 'selected' : ''}>sonnet</option>
838
- <option value="haiku" ${fm.model === 'haiku' ? 'selected' : ''}>haiku</option>
839
- <option value="opus" ${fm.model === 'opus' ? 'selected' : ''}>opus</option>
840
- </select>
841
- </div>`;
842
-
843
- const toolsStr = fm.tools || '';
844
- html += `<div class="ss-field">
845
- <label>Tools <span class="ss-field-hint">(comma-separated)</span></label>
846
- <input type="text" class="ss-input" id="ss-f-agent-tools" value="${esc(toolsStr)}" data-fm="tools">
847
- </div>`;
848
-
849
- html += `<div class="ss-field">
850
- <label>Max Turns</label>
851
- <input type="number" class="ss-input" id="ss-f-max-turns" value="${esc(String(fm.maxTurns || ''))}" min="0" data-fm="maxTurns" placeholder="0 = unlimited">
852
- </div>`;
853
-
854
- html += `<div class="ss-field">
855
- <label>Color</label>
856
- <input type="text" class="ss-input" id="ss-f-color" value="${esc(fm.color || '')}" data-fm="color" placeholder="e.g. red, blue">
857
- </div>`;
858
- }
859
-
860
- html += '</div>';
861
- return html;
862
- }
863
-
864
- function buildFileTree(subFiles) {
865
- if (!subFiles || subFiles.length === 0) return '';
866
- let html = '<div class="ss-section-label">Sub-files</div><div class="ss-file-tree" id="ss-file-tree">';
867
- const folderSvg = `<svg viewBox="0 0 16 16" width="13" height="13" class="ss-file-icon-svg"><path d="M2 3h4l1 1h7a1 1 0 011 1v8a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1z" fill="currentColor" opacity="0.7"/></svg>`;
868
- const fileSvg = `<svg viewBox="0 0 16 16" width="13" height="13" class="ss-file-icon-svg"><path d="M3 2h7l3 3v9a1 1 0 01-1 1H4a1 1 0 01-1-1V3a1 1 0 011-1z" fill="currentColor" opacity="0.6"/><path d="M10 2v2.5a.5.5 0 00.5.5H13" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.5"/></svg>`;
869
-
870
- function renderItem(item, depth = 0) {
871
- const pad = depth * 14;
872
- if (item.type === 'dir') {
873
- html += `<div class="ss-file-item ss-file-dir" style="padding-left:${pad + 8}px" data-tooltip="${esc(item.name)}/">
874
- <span class="ss-file-icon">${folderSvg}</span>
875
- <span class="ss-file-name">${esc(item.name)}/</span>
876
- </div>`;
877
- if (item.children) item.children.forEach(c => renderItem(c, depth + 1));
878
- } else {
879
- html += `<div class="ss-file-item" style="padding-left:${pad + 8}px" data-path="${esc(item.path)}" data-tooltip="${esc(item.name)}">
880
- <span class="ss-file-icon">${fileSvg}</span>
881
- <span class="ss-file-name">${esc(item.name)}</span>
882
- <span class="ss-file-size">${item.size > 1024 ? Math.round(item.size / 1024) + 'K' : item.size + 'B'}</span>
883
- <div class="ss-file-actions">
884
- <button class="ss-file-del-btn" data-path="${esc(item.path)}" data-tooltip="Delete">\u2715</button>
885
- </div>
886
- </div>`;
887
- }
888
- }
889
- subFiles.forEach(f => renderItem(f));
890
- html += '</div>';
891
- return html;
892
- }
893
-
894
- function wireEditor() {
895
- // Render the tab bar
896
- renderTabBar();
897
-
898
- // Back button — close current tab
899
- $('ss-back')?.addEventListener('click', () => {
900
- closeTab(_activeTabIdx);
901
- });
902
-
903
- // Content editor — tab-aware input tracking
904
- const editor = $('ss-content-editor');
905
- if (editor) {
906
- editor.addEventListener('input', () => {
907
- storeActiveTabContent();
908
- updateDirtyState();
909
- });
910
- // Tab key support (indentation)
911
- editor.addEventListener('keydown', (e) => {
912
- if (e.key === 'Tab') {
913
- e.preventDefault();
914
- const start = editor.selectionStart;
915
- const end = editor.selectionEnd;
916
- editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
917
- editor.selectionStart = editor.selectionEnd = start + 2;
918
- storeActiveTabContent();
919
- updateDirtyState();
920
- }
921
- });
922
- }
923
-
924
- // Metadata field change tracking — dirties the main tab of the current artifact
925
- _panel.querySelectorAll('#ss-editor-meta input, #ss-editor-meta textarea, #ss-editor-meta select').forEach(el => {
926
- el.addEventListener('input', () => {
927
- const mt = findMainTab(_selected?.id);
928
- if (mt) mt.dirty = true;
929
- updateDirtyState();
930
- });
931
- el.addEventListener('change', () => {
932
- const mt = findMainTab(_selected?.id);
933
- if (mt) mt.dirty = true;
934
- updateDirtyState();
935
- });
936
- });
937
-
938
- // Markdown toolbar
939
- wireMarkdownToolbar();
940
-
941
- // Preview/Raw toggle
942
- _panel.querySelectorAll('.ss-view-toggle-btn').forEach(btn => {
943
- btn.addEventListener('click', () => {
944
- const mode = btn.dataset.mode;
945
- _previewMode = mode === 'preview';
946
- _panel.querySelectorAll('.ss-view-toggle-btn').forEach(b => b.classList.toggle('active', b === btn));
947
- const ed = $('ss-content-editor');
948
- const prev = $('ss-md-preview');
949
- const toolbar = $('ss-md-toolbar');
950
- if (ed) ed.classList.toggle('ss-hidden', _previewMode);
951
- if (prev) {
952
- prev.classList.toggle('visible', _previewMode);
953
- if (_previewMode) {
954
- prev.innerHTML = renderMarkdown(ed?.value || '');
955
- }
956
- }
957
- // Hide formatting tools in preview mode
958
- if (toolbar) toolbar.querySelectorAll('.ss-md-tool').forEach(t => t.style.visibility = _previewMode ? 'hidden' : '');
959
- });
960
- });
961
-
962
- // Ctrl+B/I/K shortcuts in editor
963
- if (editor) {
964
- editor.addEventListener('keydown', (e) => {
965
- if (e.ctrlKey || e.metaKey) {
966
- if (e.key === 'b') { e.preventDefault(); mdWrap('**', '**'); }
967
- else if (e.key === 'i') { e.preventDefault(); mdWrap('*', '*'); }
968
- else if (e.key === 'k') { e.preventDefault(); mdLink(); }
969
- else if (e.key === 'h') { e.preventDefault(); mdPrefix('## '); }
970
- }
971
- });
972
- }
973
-
974
- // Save — tab-aware
975
- $('ss-save')?.addEventListener('click', saveCurrentArtifact);
976
-
977
- // Discard — reset the active tab's content
978
- $('ss-discard')?.addEventListener('click', () => {
979
- const tab = activeTab();
980
- if (!tab.dirty) return;
981
- if (!confirm(`Discard changes to "${tab.label}"?`)) return;
982
- tab.content = tab.originalContent;
983
- tab.dirty = false;
984
- if (tab.path === null) {
985
- // Main tab — re-render editor to reset metadata form fields too
986
- renderEditor();
987
- } else {
988
- loadActiveTabContent();
989
- }
990
- renderTabBar();
991
- updateDirtyState();
992
- });
993
-
994
- // Export
995
- $('ss-export')?.addEventListener('click', () => {
996
- if (!_selected) return;
997
- const url = getSkillsExportUrl(encodeId(_selected.id));
998
- const a = document.createElement('a');
999
- a.href = url;
1000
- a.download = '';
1001
- a.click();
1002
- });
1003
-
1004
- // Delete artifact — also closes all its tabs
1005
- $('ss-delete-btn')?.addEventListener('click', async () => {
1006
- if (!_selected) return;
1007
- if (!confirm(`Delete "${_selected.name}"? This cannot be undone.`)) return;
1008
- try {
1009
- const deletedId = _selected.id;
1010
- await deleteSkillsArtifact(encodeId(deletedId));
1011
- toast(`Deleted "${_selected.name}"`, 'info');
1012
- // Remove all tabs belonging to this artifact
1013
- _tabs = _tabs.filter(t => t.artifactId !== deletedId);
1014
- if (_activeTabIdx >= _tabs.length) _activeTabIdx = Math.max(0, _tabs.length - 1);
1015
- await loadLibrary();
1016
- if (_tabs.length === 0) {
1017
- switchToLibrary();
1018
- } else {
1019
- const tab = activeTab();
1020
- _selected = tab.artifact;
1021
- _selectedContent = tab.artifactContent;
1022
- renderEditor();
1023
- renderLibrary();
1024
- }
1025
- } catch (err) {
1026
- toast('Delete failed: ' + err.message, 'error');
1027
- }
1028
- });
1029
-
1030
- // Install bundled
1031
- $('ss-install-btn')?.addEventListener('click', async () => {
1032
- if (!_selected) return;
1033
- try {
1034
- await installSkillsBundled(_selected.dirName);
1035
- toast(`Installed "${_selected.name}" — restart Claude Code to use`, 'info');
1036
- await loadLibrary();
1037
- } catch (err) {
1038
- toast('Install failed: ' + err.message, 'error');
1039
- }
1040
- });
1041
-
1042
- // Tool chips — custom dropdown
1043
- wireToolsDropdown();
1044
-
1045
- // Remove tool chip (delegated)
1046
- $('ss-tools-list')?.addEventListener('click', (e) => {
1047
- const btn = e.target.closest('.ss-chip-x');
1048
- if (!btn) return;
1049
- const tool = btn.dataset.tool;
1050
- btn.closest('.ss-chip').remove();
1051
- const menuItem = _panel?.querySelector(`#ss-tools-menu .ss-dropdown-item[data-value="${tool}"]`);
1052
- if (menuItem) menuItem.classList.remove('disabled');
1053
- const mt = findMainTab(_selected?.id);
1054
- if (mt) mt.dirty = true;
1055
- updateDirtyState();
1056
- });
1057
-
1058
- // Sub-file edit/delete — opens in tab, not modal
1059
- const fileTree = $('ss-file-tree');
1060
- if (fileTree) {
1061
- fileTree.addEventListener('click', async (e) => {
1062
- const delBtn = e.target.closest('.ss-file-del-btn');
1063
- if (delBtn) { deleteSubFile(delBtn.dataset.path); return; }
1064
- const row = e.target.closest('.ss-file-item[data-path]');
1065
- if (row) openFileInTab(row.dataset.path);
1066
- });
1067
- }
1068
-
1069
- // Add file — custom file browser dialog
1070
- $('ss-add-file')?.addEventListener('click', () => openAddFileDialog());
1071
-
1072
- // Icon upload handler
1073
- const iconFile = $('ss-icon-file');
1074
- if (iconFile) {
1075
- iconFile.addEventListener('change', async () => {
1076
- const file = iconFile.files[0];
1077
- if (!file) return;
1078
- if (file.size > 2 * 1024 * 1024) { toast('Icon must be under 2 MB', 'error'); return; }
1079
- const label = $('ss-icon-upload-label');
1080
- const origText = label?.textContent?.trim();
1081
- if (label) label.textContent = 'Uploading\u2026';
1082
- try {
1083
- const buf = await file.arrayBuffer();
1084
- await uploadSkillsIcon(encodeId(_selected.id), buf, file.type || 'image/png');
1085
- toast('Icon uploaded', 'info');
1086
- _selected.hasIcon = true;
1087
- const libItem = _library.find(a => a.id === _selected.id);
1088
- if (libItem) libItem.hasIcon = true;
1089
- renderEditor();
1090
- renderLibrary();
1091
- } catch (err) {
1092
- toast('Icon upload failed: ' + err.message, 'error');
1093
- if (label) label.textContent = origText || 'Upload';
1094
- }
1095
- });
1096
- }
1097
-
1098
- // Icon remove handler
1099
- const iconRemove = $('ss-icon-remove');
1100
- if (iconRemove) {
1101
- iconRemove.addEventListener('click', async () => {
1102
- try {
1103
- await deleteSkillsIcon(encodeId(_selected.id));
1104
- toast('Icon removed', 'info');
1105
- _selected.hasIcon = false;
1106
- const libItem = _library.find(a => a.id === _selected.id);
1107
- if (libItem) libItem.hasIcon = false;
1108
- renderEditor();
1109
- renderLibrary();
1110
- } catch (err) {
1111
- toast('Failed to remove icon: ' + err.message, 'error');
1112
- }
1113
- });
1114
- }
1115
- }
1116
-
1117
- // ═══════════════════════════════════════════
1118
- // ADD FILE — Custom File Browser Dialog
1119
- // ═══════════════════════════════════════════
1120
-
1121
- function openAddFileDialog() {
1122
- if (!_selected || !_selectedContent) return;
1123
-
1124
- // Extract directories from subFiles tree
1125
- const dirs = ['']; // root = '' (skill directory root)
1126
- function collectDirs(items, prefix) {
1127
- for (const item of items) {
1128
- if (item.type === 'dir') {
1129
- const p = prefix ? prefix + '/' + item.name : item.name;
1130
- dirs.push(p);
1131
- if (item.children) collectDirs(item.children, p);
1132
- }
1133
- }
1134
- }
1135
- if (_selectedContent.subFiles) collectDirs(_selectedContent.subFiles, '');
1136
-
1137
- let selectedDir = '';
1138
- let newFolderMode = false;
1139
-
1140
- // Build the overlay
1141
- const overlay = document.createElement('div');
1142
- overlay.className = 'ss-file-dialog-overlay';
1143
- overlay.innerHTML = buildFileDialogHTML(dirs, selectedDir, newFolderMode);
1144
- _panel.appendChild(overlay);
1145
-
1146
- function refresh() {
1147
- overlay.innerHTML = buildFileDialogHTML(dirs, selectedDir, newFolderMode);
1148
- wireFileDialog();
1149
- }
1150
-
1151
- function wireFileDialog() {
1152
- // Close / Cancel
1153
- overlay.querySelector('#ss-fd-cancel')?.addEventListener('click', () => overlay.remove());
1154
- overlay.querySelector('.ss-fd-backdrop')?.addEventListener('click', () => overlay.remove());
1155
-
1156
- // Directory clicks
1157
- overlay.querySelectorAll('.ss-fd-dir').forEach(el => {
1158
- el.addEventListener('click', () => {
1159
- selectedDir = el.dataset.path;
1160
- refresh();
1161
- });
1162
- });
1163
-
1164
- // New folder toggle
1165
- overlay.querySelector('#ss-fd-new-folder-btn')?.addEventListener('click', () => {
1166
- newFolderMode = true;
1167
- refresh();
1168
- overlay.querySelector('#ss-fd-new-folder-name')?.focus();
1169
- });
1170
-
1171
- // New folder confirm
1172
- overlay.querySelector('#ss-fd-new-folder-ok')?.addEventListener('click', () => {
1173
- const input = overlay.querySelector('#ss-fd-new-folder-name');
1174
- const name = input?.value.trim();
1175
- if (!name) return;
1176
- const newPath = selectedDir ? selectedDir + '/' + name : name;
1177
- dirs.push(newPath);
1178
- selectedDir = newPath;
1179
- newFolderMode = false;
1180
- refresh();
1181
- });
1182
-
1183
- // New folder cancel
1184
- overlay.querySelector('#ss-fd-new-folder-cancel')?.addEventListener('click', () => {
1185
- newFolderMode = false;
1186
- refresh();
1187
- });
1188
-
1189
- // Enter in new folder input
1190
- overlay.querySelector('#ss-fd-new-folder-name')?.addEventListener('keydown', (e) => {
1191
- if (e.key === 'Enter') overlay.querySelector('#ss-fd-new-folder-ok')?.click();
1192
- if (e.key === 'Escape') { newFolderMode = false; refresh(); }
1193
- });
1194
-
1195
- // Create button
1196
- overlay.querySelector('#ss-fd-create')?.addEventListener('click', async () => {
1197
- const nameInput = overlay.querySelector('#ss-fd-filename');
1198
- const filename = nameInput?.value.trim();
1199
- if (!filename) { toast('Filename is required', 'error'); nameInput?.focus(); return; }
1200
- const fullPath = selectedDir ? selectedDir + '/' + filename : filename;
1201
- overlay.remove();
1202
- try {
1203
- await createSkillsSubFile(encodeId(_selected.id), fullPath, '');
1204
- toast('File created', 'info');
1205
- const data = await fetchSkillsArtifact(encodeId(_selected.id));
1206
- _selectedContent = data;
1207
- _tabs.forEach(t => { if (t.artifactId === _selected.id) t.artifactContent = data; });
1208
- renderEditor();
1209
- await openFileInTab(fullPath);
1210
- } catch (err) {
1211
- toast('Failed: ' + err.message, 'error');
1212
- }
1213
- });
1214
-
1215
- // Enter in filename → create
1216
- overlay.querySelector('#ss-fd-filename')?.addEventListener('keydown', (e) => {
1217
- if (e.key === 'Enter') overlay.querySelector('#ss-fd-create')?.click();
1218
- if (e.key === 'Escape') overlay.remove();
1219
- });
1220
-
1221
- // Upload file input
1222
- const uploadInput = overlay.querySelector('#ss-fd-upload-input');
1223
- if (uploadInput) {
1224
- uploadInput.addEventListener('change', async (e) => {
1225
- const file = e.target.files?.[0];
1226
- if (!file) return;
1227
- overlay.remove();
1228
- try {
1229
- const content = await file.text();
1230
- const fullPath = selectedDir ? selectedDir + '/' + file.name : file.name;
1231
- await createSkillsSubFile(encodeId(_selected.id), fullPath, content);
1232
- toast(`File uploaded: ${file.name}`, 'info');
1233
- const data = await fetchSkillsArtifact(encodeId(_selected.id));
1234
- _selectedContent = data;
1235
- _tabs.forEach(t => { if (t.artifactId === _selected.id) t.artifactContent = data; });
1236
- renderEditor();
1237
- await openFileInTab(fullPath);
1238
- } catch (err) {
1239
- toast('Failed: ' + err.message, 'error');
1240
- }
1241
- });
1242
- }
1243
-
1244
- // Upload trigger button
1245
- overlay.querySelector('#ss-fd-upload-btn')?.addEventListener('click', () => {
1246
- overlay.querySelector('#ss-fd-upload-input')?.click();
1247
- });
1248
-
1249
- // Focus filename input
1250
- if (!newFolderMode) overlay.querySelector('#ss-fd-filename')?.focus();
1251
- }
1252
-
1253
- wireFileDialog();
1254
- }
1255
-
1256
- function buildFileDialogHTML(dirs, selectedDir, newFolderMode = false) {
1257
- const pathPreview = selectedDir ? selectedDir + '/' : '';
1258
-
1259
- // Build visual tree of dirs
1260
- const tree = [];
1261
- // Root entry
1262
- tree.push({ path: '', name: '/ (root)', depth: 0 });
1263
- // Sort dirs and compute depths
1264
- const sorted = dirs.filter(d => d !== '').sort();
1265
- for (const d of sorted) {
1266
- const parts = d.split('/');
1267
- tree.push({ path: d, name: parts[parts.length - 1], depth: parts.length });
1268
- }
1269
-
1270
- return `
1271
- <div class="ss-fd-backdrop"></div>
1272
- <div class="ss-file-dialog">
1273
- <div class="ss-fd-body">
1274
- ${dirs.length > 1 ? `
1275
- <div class="ss-fd-section-label">in</div>
1276
- <div class="ss-fd-dirs">
1277
- ${tree.map(d => `
1278
- <div class="ss-fd-dir${d.path === selectedDir ? ' selected' : ''}" data-path="${esc(d.path)}" style="padding-left:${d.depth * 14 + 8}px">
1279
- <svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 13.5H2a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h4l1.5 2H14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1z"/></svg>
1280
- <span>${esc(d.name)}</span>
1281
- </div>
1282
- `).join('')}
1283
- </div>
1284
- ${newFolderMode ? `
1285
- <div class="ss-fd-new-folder">
1286
- <input type="text" class="ss-input ss-fd-input" id="ss-fd-new-folder-name" placeholder="folder-name" spellcheck="false">
1287
- <button class="ss-fd-btn ss-fd-btn--ok" id="ss-fd-new-folder-ok">Add</button>
1288
- <button class="ss-fd-btn ss-fd-btn--cancel" id="ss-fd-new-folder-cancel">&times;</button>
1289
- </div>
1290
- ` : `
1291
- <button class="ss-fd-new-folder-trigger" id="ss-fd-new-folder-btn">+ Folder</button>
1292
- `}
1293
- ` : ''}
1294
-
1295
- <div class="ss-fd-inline-create">
1296
- <div class="ss-fd-filename-row">
1297
- <span class="ss-fd-path-prefix">${esc(pathPreview)}</span>
1298
- <input type="text" class="ss-input ss-fd-input ss-fd-filename-input" id="ss-fd-filename" placeholder="filename.md" spellcheck="false" autocomplete="off">
1299
- </div>
1300
- <button class="ss-fd-create-btn" id="ss-fd-create">Create</button>
1301
- </div>
1302
-
1303
- <div class="ss-fd-or">or</div>
1304
-
1305
- <div class="ss-fd-inline-upload">
1306
- <button class="ss-fd-upload-link" id="ss-fd-upload-btn">Upload from computer</button>
1307
- <input type="file" id="ss-fd-upload-input" style="display:none">
1308
- </div>
1309
- </div>
1310
-
1311
- <div class="ss-fd-footer">
1312
- <button class="ss-header-btn" id="ss-fd-cancel">Cancel</button>
1313
- </div>
1314
- </div>
1315
- `;
1316
- }
1317
-
1318
- function wireToolsDropdown() {
1319
- const trigger = $('ss-tools-trigger');
1320
- const menu = $('ss-tools-menu');
1321
- if (!trigger || !menu) return;
1322
-
1323
- // Toggle menu
1324
- trigger.addEventListener('click', (e) => {
1325
- e.stopPropagation();
1326
- const open = menu.classList.toggle('open');
1327
- trigger.classList.toggle('open', open);
1328
- });
1329
-
1330
- // Item click
1331
- menu.addEventListener('click', (e) => {
1332
- const item = e.target.closest('.ss-dropdown-item');
1333
- if (!item || item.classList.contains('disabled')) return;
1334
- const val = item.dataset.value;
1335
- const list = $('ss-tools-list');
1336
- if (list) {
1337
- list.insertAdjacentHTML('beforeend', `<span class="ss-chip">${esc(val)}<button class="ss-chip-x" data-tool="${esc(val)}">\u00d7</button></span>`);
1338
- }
1339
- item.classList.add('disabled');
1340
- menu.classList.remove('open');
1341
- trigger.classList.remove('open');
1342
- const mt = findMainTab(_selected?.id);
1343
- if (mt) mt.dirty = true;
1344
- updateDirtyState();
1345
- });
1346
-
1347
- // Close on outside click. Every editor render wires a new dropdown: the previous one's listener ends here,
1348
- // and the last one with the panel.
1349
- const closeMenu = (e) => {
1350
- if (!trigger.contains(e.target) && !menu.contains(e.target)) {
1351
- menu.classList.remove('open');
1352
- trigger.classList.remove('open');
1353
- }
1354
- };
1355
- _toolsMenuLife?.abort();
1356
- _toolsMenuLife = new AbortController();
1357
- document.addEventListener('click', closeMenu, { signal: _toolsMenuLife.signal });
1358
- }
1359
-
1360
- // ── Metadata ↔ Frontmatter helpers ──
1361
-
1362
- /** Collect all metadata form values into an object matching frontmatter keys */
1363
- function collectMetadataFromForm() {
1364
- if (!_selected) return null;
1365
- const meta = {};
1366
- const type = _selected.type; // skill | command | agent
1367
-
1368
- // Name (skill + agent)
1369
- const nameEl = $('ss-f-name');
1370
- if (nameEl) meta.name = nameEl.value.trim();
1371
-
1372
- // Description (all types)
1373
- const descEl = $('ss-f-desc');
1374
- if (descEl) meta.description = descEl.value.trim();
1375
-
1376
- if (type === 'skill') {
1377
- // Argument hint
1378
- const argEl = $('ss-f-arghint');
1379
- if (argEl && argEl.value.trim()) meta['argument-hint'] = argEl.value.trim();
1380
-
1381
- // Allowed tools — read from chip list DOM
1382
- const toolsList = $('ss-tools-list');
1383
- if (toolsList) {
1384
- const tools = [...toolsList.querySelectorAll('.ss-chip')].map(chip => {
1385
- const xBtn = chip.querySelector('.ss-chip-x');
1386
- return xBtn ? xBtn.dataset.tool : chip.textContent.replace('\u00d7', '').trim();
1387
- }).filter(Boolean);
1388
- if (tools.length > 0) meta['allowed-tools'] = tools;
1389
- }
1390
-
1391
- // Options checkboxes
1392
- const uiEl = $('ss-f-user-invocable');
1393
- if (uiEl) meta['user-invocable'] = uiEl.checked;
1394
- const dmEl = $('ss-f-disable-model');
1395
- if (dmEl && dmEl.checked) meta['disable-model-invocation'] = true;
1396
- }
1397
-
1398
- if (type === 'agent') {
1399
- // Model
1400
- const modelEl = $('ss-f-model');
1401
- if (modelEl && modelEl.value) meta.model = modelEl.value;
1402
- // Tools (comma-separated string)
1403
- const toolsEl = $('ss-f-agent-tools');
1404
- if (toolsEl && toolsEl.value.trim()) meta.tools = toolsEl.value.trim();
1405
- // Max turns
1406
- const maxEl = $('ss-f-max-turns');
1407
- if (maxEl && maxEl.value) meta.maxTurns = parseInt(maxEl.value, 10) || 0;
1408
- // Color
1409
- const colorEl = $('ss-f-color');
1410
- if (colorEl && colorEl.value.trim()) meta.color = colorEl.value.trim();
1411
- }
1412
-
1413
- return meta;
1414
- }
1415
-
1416
- /** Serialize a metadata object to a YAML frontmatter string (with ---) */
1417
- function buildFrontmatterYaml(meta) {
1418
- if (!meta || Object.keys(meta).length === 0) return '';
1419
- const lines = ['---'];
1420
- for (const [key, val] of Object.entries(meta)) {
1421
- if (val === undefined || val === null || val === '') continue;
1422
- if (Array.isArray(val)) {
1423
- // YAML list
1424
- if (val.length === 0) continue;
1425
- lines.push(`${key}:`);
1426
- for (const item of val) lines.push(` - ${item}`);
1427
- } else if (typeof val === 'boolean') {
1428
- lines.push(`${key}: ${val}`);
1429
- } else if (typeof val === 'number') {
1430
- lines.push(`${key}: ${val}`);
1431
- } else if (typeof val === 'string' && (val.includes('\n') || (key === 'description' && val.length > 80))) {
1432
- // Multi-line or long description → block scalar
1433
- lines.push(`${key}: >`);
1434
- // Wrap long single-line text into ~78-char lines for readability
1435
- const raw = val.replace(/\r\n/g, '\n');
1436
- if (!raw.includes('\n')) {
1437
- const words = raw.split(' ');
1438
- let line = '';
1439
- for (const w of words) {
1440
- if (line && (line.length + 1 + w.length) > 78) {
1441
- lines.push(` ${line}`);
1442
- line = w;
1443
- } else {
1444
- line = line ? line + ' ' + w : w;
1445
- }
1446
- }
1447
- if (line) lines.push(` ${line}`);
1448
- } else {
1449
- for (const l of raw.split('\n')) lines.push(` ${l}`);
1450
- }
1451
- } else if (typeof val === 'string' && (val.includes(':') || val.includes('#') || val.includes('"'))) {
1452
- // Strings with special chars → quoted
1453
- lines.push(`${key}: "${val.replace(/"/g, '\\"')}"`);
1454
- } else {
1455
- lines.push(`${key}: ${val}`);
1456
- }
1457
- }
1458
- lines.push('---');
1459
- return lines.join('\n');
1460
- }
1461
-
1462
- /** Replace or insert frontmatter in raw content using collected form metadata */
1463
- function mergeMetadataIntoContent(rawContent, meta) {
1464
- if (!meta) return rawContent;
1465
- const yaml = buildFrontmatterYaml(meta);
1466
- if (!yaml) return rawContent;
1467
- // Strip existing frontmatter from body
1468
- const body = rawContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
1469
- return yaml + '\n' + body;
1470
- }
1471
-
1472
- function setDirty() {
1473
- // Delegate to tab-based dirty tracking
1474
- storeActiveTabContent();
1475
- updateDirtyState();
1476
- }
1477
-
1478
- async function saveCurrentArtifact() {
1479
- const tab = activeTab();
1480
- if (!tab.artifact || !tab.artifactContent) return;
1481
-
1482
- storeActiveTabContent();
1483
- captureMetadataIntoContent();
1484
- const id = encodeId(tab.artifactId);
1485
-
1486
- try {
1487
- if (tab.path === null) {
1488
- await saveSkillsArtifact(id, tab.content);
1489
- tab.originalContent = tab.content;
1490
- tab.dirty = false;
1491
- // Sync the textarea with the merged content (metadata may have changed it)
1492
- const editor = $('ss-content-editor');
1493
- if (editor) editor.value = tab.content;
1494
- // Refresh tab.artifactContent so re-renders show correct metadata
1495
- try {
1496
- const freshData = await fetchSkillsArtifact(id);
1497
- tab.artifactContent = freshData;
1498
- _selectedContent = freshData;
1499
- // Also update the artifact reference from the library if available
1500
- _tabs.forEach(t => { if (t.artifactId === tab.artifactId) t.artifactContent = freshData; });
1501
- } catch { /* non-critical — stale artifactContent is cosmetic only */ }
1502
- toast('Saved', 'info');
1503
- await loadLibrary();
1504
- } else {
1505
- await saveSkillsSubFile(id, tab.path, tab.content);
1506
- tab.originalContent = tab.content;
1507
- tab.dirty = false;
1508
- toast(`Saved ${tab.label}`, 'info');
1509
- }
1510
- renderTabBar();
1511
- updateDirtyState();
1512
- } catch (err) {
1513
- toast('Save failed: ' + err.message, 'error');
1514
- }
1515
- }
1516
-
1517
- async function deleteSubFile(path) {
1518
- if (!_selected) return;
1519
- if (!confirm(`Delete "${path}"?`)) return;
1520
- try {
1521
- await deleteSkillsSubFile(encodeId(_selected.id), path);
1522
- // Close the tab if this file was open
1523
- const tabIdx = _tabs.findIndex(t => t.artifactId === _selected.id && t.path === path);
1524
- if (tabIdx >= 0) {
1525
- _tabs.splice(tabIdx, 1);
1526
- if (_activeTabIdx > tabIdx) _activeTabIdx--;
1527
- else if (_activeTabIdx >= _tabs.length) _activeTabIdx = Math.max(0, _tabs.length - 1);
1528
- }
1529
- toast('File deleted', 'info');
1530
- // Reload artifact content and update all tabs of this artifact
1531
- const data = await fetchSkillsArtifact(encodeId(_selected.id));
1532
- _selectedContent = data;
1533
- _tabs.forEach(t => { if (t.artifactId === _selected.id) t.artifactContent = data; });
1534
- renderEditor();
1535
- } catch (err) {
1536
- toast('Delete failed: ' + err.message, 'error');
1537
- }
1538
- }
1539
-
1540
- // ═══════════════════════════════════════════
1541
- // MARKDOWN TOOLBAR & PREVIEW
1542
- // ═══════════════════════════════════════════
1543
-
1544
- function wireMarkdownToolbar() {
1545
- const toolbar = $('ss-md-toolbar');
1546
- if (!toolbar) return;
1547
- toolbar.querySelectorAll('.ss-md-tool[data-cmd]').forEach(btn => {
1548
- btn.addEventListener('click', () => {
1549
- const cmd = btn.dataset.cmd;
1550
- switch (cmd) {
1551
- case 'bold': mdWrap('**', '**'); break;
1552
- case 'italic': mdWrap('*', '*'); break;
1553
- case 'strikethrough': mdWrap('~~', '~~'); break;
1554
- case 'code': mdWrap('`', '`'); break;
1555
- case 'codeblock': mdWrapBlock('```\n', '\n```'); break;
1556
- case 'heading': mdPrefix('## '); break;
1557
- case 'ul': mdPrefix('- '); break;
1558
- case 'ol': mdPrefix('1. '); break;
1559
- case 'quote': mdPrefix('> '); break;
1560
- case 'link': mdLink(); break;
1561
- case 'hr': mdInsert('\n---\n'); break;
1562
- }
1563
- });
1564
- });
1565
- }
1566
-
1567
- /** Wrap selection with before/after strings */
1568
- function mdWrap(before, after) {
1569
- const ed = $('ss-content-editor');
1570
- if (!ed) return;
1571
- const start = ed.selectionStart, end = ed.selectionEnd;
1572
- const sel = ed.value.substring(start, end);
1573
- const text = sel || 'text';
1574
- ed.focus();
1575
- // Use execCommand for undo support where available, fallback to manual
1576
- const replacement = before + text + after;
1577
- ed.setRangeText(replacement, start, end, 'select');
1578
- // Move cursor to select just the inner text
1579
- ed.selectionStart = start + before.length;
1580
- ed.selectionEnd = start + before.length + text.length;
1581
- storeActiveTabContent();
1582
- updateDirtyState();
1583
- }
1584
-
1585
- /** Wrap selection as a block (ensures newlines) */
1586
- function mdWrapBlock(before, after) {
1587
- const ed = $('ss-content-editor');
1588
- if (!ed) return;
1589
- const start = ed.selectionStart, end = ed.selectionEnd;
1590
- const sel = ed.value.substring(start, end);
1591
- const text = sel || 'code here';
1592
- const needNewlineBefore = start > 0 && ed.value[start - 1] !== '\n' ? '\n' : '';
1593
- const needNewlineAfter = end < ed.value.length && ed.value[end] !== '\n' ? '\n' : '';
1594
- const replacement = needNewlineBefore + before + text + after + needNewlineAfter;
1595
- ed.setRangeText(replacement, start, end, 'select');
1596
- ed.focus();
1597
- storeActiveTabContent();
1598
- updateDirtyState();
1599
- }
1600
-
1601
- /** Prefix each line in selection (for headings, lists, quotes) */
1602
- function mdPrefix(prefix) {
1603
- const ed = $('ss-content-editor');
1604
- if (!ed) return;
1605
- const start = ed.selectionStart, end = ed.selectionEnd;
1606
- // Expand selection to full lines
1607
- const before = ed.value.substring(0, start);
1608
- const lineStart = before.lastIndexOf('\n') + 1;
1609
- const sel = ed.value.substring(lineStart, end);
1610
- const lines = sel.split('\n');
1611
- const prefixed = lines.map(line => {
1612
- // If line already has this prefix, remove it (toggle)
1613
- if (line.startsWith(prefix)) return line.substring(prefix.length);
1614
- // Remove other list-style prefixes before adding new one
1615
- return prefix + line.replace(/^(#{1,6}\s|[-*]\s|\d+\.\s|>\s)/, '');
1616
- }).join('\n');
1617
- ed.setRangeText(prefixed, lineStart, end, 'select');
1618
- ed.focus();
1619
- storeActiveTabContent();
1620
- updateDirtyState();
1621
- }
1622
-
1623
- /** Insert a markdown link */
1624
- function mdLink() {
1625
- const ed = $('ss-content-editor');
1626
- if (!ed) return;
1627
- const start = ed.selectionStart, end = ed.selectionEnd;
1628
- const sel = ed.value.substring(start, end);
1629
- const text = sel || 'link text';
1630
- const replacement = `[${text}](url)`;
1631
- ed.setRangeText(replacement, start, end, 'select');
1632
- // Select "url" for easy replacement
1633
- ed.selectionStart = start + text.length + 3;
1634
- ed.selectionEnd = start + text.length + 6;
1635
- ed.focus();
1636
- storeActiveTabContent();
1637
- updateDirtyState();
1638
- }
1639
-
1640
- /** Insert text at cursor */
1641
- function mdInsert(text) {
1642
- const ed = $('ss-content-editor');
1643
- if (!ed) return;
1644
- const pos = ed.selectionStart;
1645
- ed.setRangeText(text, pos, ed.selectionEnd, 'end');
1646
- ed.focus();
1647
- storeActiveTabContent();
1648
- updateDirtyState();
1649
- }
1650
-
1651
- /** Simple markdown → HTML renderer (no deps) */
1652
- function renderMarkdown(md) {
1653
- // Strip YAML frontmatter
1654
- let text = md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
1655
-
1656
- // Escape HTML first (quotes too: a link's target lands in an attribute)
1657
- text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1658
-
1659
- // Code blocks (``` ... ```)
1660
- text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1661
- return `<pre><code class="lang-${lang}">${code.trimEnd()}</code></pre>`;
1662
- });
1663
-
1664
- // Headers
1665
- text = text.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
1666
- text = text.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
1667
- text = text.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
1668
- text = text.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
1669
- text = text.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
1670
- text = text.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
1671
-
1672
- // Horizontal rules
1673
- text = text.replace(/^---+$/gm, '<hr>');
1674
-
1675
- // Bold and italic
1676
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
1677
- text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1678
- text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
1679
- text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
1680
-
1681
- // Inline code
1682
- text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
1683
-
1684
- // Links: http(s) and mailto only, checked as the Markdown sanitiser checks them; any other target stays text
1685
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (whole, label, target) => {
1686
- const href = safeHref(target.replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&'));
1687
- return href ? `<a href="${esc(href)}" target="_blank" rel="noopener noreferrer">${label}</a>` : label;
1688
- });
1689
-
1690
- // Blockquotes
1691
- text = text.replace(/^&gt;\s?(.*)$/gm, '<blockquote>$1</blockquote>');
1692
- // Merge consecutive blockquotes
1693
- text = text.replace(/<\/blockquote>\n<blockquote>/g, '\n');
1694
-
1695
- // Tables
1696
- text = text.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)+)/gm, (_, header, sep, body) => {
1697
- const ths = header.split('|').filter(c => c.trim()).map(c => `<th>${c.trim()}</th>`).join('');
1698
- const rows = body.trim().split('\n').map(row => {
1699
- const tds = row.split('|').filter(c => c.trim()).map(c => `<td>${c.trim()}</td>`).join('');
1700
- return `<tr>${tds}</tr>`;
1701
- }).join('');
1702
- return `<table><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table>`;
1703
- });
1704
-
1705
- // Unordered lists
1706
- text = text.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>');
1707
- text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
1708
-
1709
- // Ordered lists
1710
- text = text.replace(/^\d+\.\s+(.+)$/gm, '<oli>$1</oli>');
1711
- text = text.replace(/((?:<oli>.*<\/oli>\n?)+)/g, (match) => {
1712
- return '<ol>' + match.replace(/<\/?oli>/g, (t) => t.replace('oli', 'li')) + '</ol>';
1713
- });
1714
-
1715
- // Paragraphs — wrap remaining loose lines
1716
- text = text.replace(/^(?!<[a-z/])((?!<).+)$/gm, '<p>$1</p>');
1717
-
1718
- // Clean up empty paragraphs and double newlines
1719
- text = text.replace(/<p>\s*<\/p>/g, '');
1720
-
1721
- return text;
1722
- }
1723
-
1724
- // ── Mermaid support (lazy-loaded, vendored in public/vendor/mermaid/) ──
1725
- let _mermaidLoaded = false;
1726
- let _mermaidLoading = false;
1727
- let _mermaidId = 0;
1728
-
1729
- async function loadMermaid() {
1730
- if (_mermaidLoaded) return true;
1731
- if (_mermaidLoading) {
1732
- // Wait for in-flight load
1733
- return new Promise(resolve => {
1734
- const check = setInterval(() => { if (_mermaidLoaded) { clearInterval(check); resolve(true); } }, 100);
1735
- setTimeout(() => { clearInterval(check); resolve(false); }, 8000);
1736
- });
1737
- }
1738
- _mermaidLoading = true;
1739
- try {
1740
- const mod = await import('/vendor/mermaid/mermaid-11.17.2.esm.min.mjs');
1741
- mod.default.initialize({ startOnLoad: false, theme: 'dark', themeVariables: {
1742
- darkMode: true, background: 'transparent', primaryColor: '#1e3a5f',
1743
- primaryTextColor: '#e0e0e0', lineColor: '#94a3b8', secondaryColor: '#2a2a4a',
1744
- }});
1745
- window.__mermaid = mod.default;
1746
- _mermaidLoaded = true;
1747
- return true;
1748
- } catch (err) {
1749
- console.error('Failed to load Mermaid:', err);
1750
- _mermaidLoading = false;
1751
- return false;
1752
- }
1753
- }
1754
-
1755
- async function processMermaidBlocks(container) {
1756
- if (!container) return;
1757
- const blocks = container.querySelectorAll('.ss-mermaid[data-mermaid]');
1758
- if (blocks.length === 0) return;
1759
- const ok = await loadMermaid();
1760
- if (!ok) {
1761
- blocks.forEach(b => { b.innerHTML = '<div class="ss-mermaid-error">Failed to load Mermaid</div>'; });
1762
- return;
1763
- }
1764
- for (const block of blocks) {
1765
- const src = decodeURIComponent(block.dataset.mermaid);
1766
- try {
1767
- const id = `ss-mmd-${++_mermaidId}`;
1768
- const { svg } = await window.__mermaid.render(id, src);
1769
- block.innerHTML = svg;
1770
- // Make SVG responsive
1771
- const svgEl = block.querySelector('svg');
1772
- if (svgEl) { svgEl.style.maxWidth = '100%'; svgEl.style.height = 'auto'; }
1773
- } catch (err) {
1774
- block.innerHTML = `<div class="ss-mermaid-error">Mermaid error: ${esc(err.message || 'parse error')}</div><pre class="ss-mermaid-src">${esc(src)}</pre>`;
1775
- }
1776
- }
1777
- }
1778
-
1779
- // ═══════════════════════════════════════════
1780
- // WIZARD VIEW
1781
- // ═══════════════════════════════════════════
1782
-
1783
- const SKILL_TEMPLATES = {
1784
- blank: { label: 'Blank', desc: 'Empty starting point',
1785
- gen: (n) => `---\nname: ${n}\ndescription: >\n Describe when this skill should trigger.\n---\n\n# ${n}\n\nAdd your instructions here.\n` },
1786
- simple: { label: 'Simple', desc: 'Single-task skill with steps',
1787
- gen: (n) => `---\nname: ${n}\ndescription: >\n This skill handles ${n} tasks.\n Triggers on: "${n}".\nargument-hint: "[input]"\n---\n\n# ${n}\n\nThe user has invoked \`/${n}\`.\n\n**Input**: $ARGUMENTS\n\n## Steps\n\n1. Parse the input\n2. Perform the task\n3. Report results\n` },
1788
- orchestrator: { label: 'Orchestrator', desc: 'Routes to sub-skills based on arguments',
1789
- gen: (n) => `---\nname: ${n}\ndescription: >\n ${n} command hub \u2014 routes to sub-commands.\n Triggers on: "${n}".\nargument-hint: "[subcommand] or leave blank for menu"\n---\n\n# ${n}\n\n## Step 1: Parse Arguments\n\nRead \`$ARGUMENTS\`. If blank, present an interactive menu.\n\n## Subcommands\n\n| Command | Description |\n|---------|-------------|\n| sub1 | Does thing 1 |\n| sub2 | Does thing 2 |\n\n## Step 2: Route\n\nBased on the subcommand, follow the appropriate section below.\n\n### sub1\n...\n\n### sub2\n...\n` },
1790
- modular: { label: 'Modular', desc: 'SKILL.md + reference files loaded on demand',
1791
- gen: (n) => `---\nname: ${n}\ndescription: >\n This skill provides ${n} capabilities with detailed reference docs.\n Triggers on: "${n}".\nallowed-tools:\n - Read\n - Grep\n - Glob\n---\n\n# ${n}\n\n## Overview\n\nThis skill uses progressive loading. Core logic is here; detailed references are loaded on demand.\n\n## References\n\n- \`$SKILL_DIR/references/guide.md\` \u2014 Detailed guide\n\n## Steps\n\n1. Read $ARGUMENTS\n2. If detailed info needed, use \`Read\` tool on the reference file\n3. Follow the instructions from the reference\n` },
1792
- expert: { label: 'Expert', desc: 'Full frontmatter with all options configured',
1793
- gen: (n) => `---\nname: ${n}\ndescription: >\n Expert ${n} skill with comprehensive tool access.\n Triggers on: "${n}", "${n} help".\nargument-hint: "[task description]"\nallowed-tools:\n - Read\n - Write\n - Edit\n - Bash\n - Grep\n - Glob\n - WebFetch\n - WebSearch\n - Task\n---\n\n# ${n}\n\nYou are an expert ${n} assistant. The user has invoked \`/${n}\`.\n\n## Capabilities\n\n- Full file system access\n- Web search and fetch\n- Sub-agent delegation\n\n## Steps\n\n1. Analyze the request: $ARGUMENTS\n2. Plan the approach\n3. Execute with appropriate tools\n4. Report results\n` },
1794
- };
1795
-
1796
- const COMMAND_TEMPLATES = {
1797
- blank: { label: 'Blank', desc: 'Empty command',
1798
- gen: (n) => `---\ndescription: Describe what /${n} does.\n---\n\n# ${n}\n\nAdd your instructions here.\n\n$ARGUMENTS\n` },
1799
- analysis: { label: 'Analysis', desc: 'Code or content analysis pattern',
1800
- gen: (n) => `---\ndescription: Analyze code or content for ${n}.\n---\n\n# ${n} Analysis\n\nAnalyze the following based on user input.\n\n**Target**: $ARGUMENTS\n\n## Steps\n\n1. Read the target file or code\n2. Analyze for patterns, issues, or improvements\n3. Present findings in a structured format\n` },
1801
- workflow: { label: 'Workflow', desc: 'Multi-step workflow with confirmation',
1802
- gen: (n) => `---\ndescription: Execute the ${n} workflow.\n---\n\n# ${n} Workflow\n\n$ARGUMENTS\n\n## Steps\n\n1. **Gather**: Collect necessary information\n2. **Plan**: Design the approach\n3. **Confirm**: Present plan to user for approval\n4. **Execute**: Implement the plan\n5. **Verify**: Check results\n` },
1803
- };
1804
-
1805
- const AGENT_TEMPLATES = {
1806
- blank: { label: 'Blank', desc: 'Minimal agent',
1807
- gen: (n) => `---\nname: ${n}\ndescription: >\n Use this agent when you need ${n} expertise.\n---\n\nYou are a ${n} specialist.\n\nFollow the user's instructions carefully.\n` },
1808
- expert: { label: 'Domain Expert', desc: 'Specialist with deep context',
1809
- gen: (n) => `---\nname: ${n}\ndescription: >\n Use this agent when you need ${n} expertise.\n Triggers on database questions, schema design,\n and performance optimization.\nmodel: sonnet\ntools: Read, Bash, Write, Glob, Grep\n---\n\nYou are an expert ${n} specialist. Your role is to provide deep expertise.\n\n<example>\nContext: User needs help with ${n}.\nuser: "Help me with ${n}"\nassistant: "Let me investigate the ${n} setup and provide recommendations."\n</example>\n\n## Your Approach\n\n1. Understand the current state\n2. Identify issues or opportunities\n3. Provide actionable recommendations\n` },
1810
- researcher: { label: 'Researcher', desc: 'Web + file research pattern',
1811
- gen: (n) => `---\nname: ${n}\ndescription: >\n Use this agent for ${n} research tasks.\ntools: Read, Grep, Glob, WebFetch, WebSearch\n---\n\nYou are a research agent specializing in ${n}.\n\n## Approach\n\n1. Search the codebase for relevant context\n2. Search the web for current best practices\n3. Synthesize findings into actionable insights\n4. Return a concise summary\n` },
1812
- };
1813
-
1814
- // ═══════════════════════════════════════════
1815
- // PRESET / SNIPPET SYSTEM
1816
- // ═══════════════════════════════════════════
1817
-
1818
- const PRESETS_KEY = 'neural-skills-custom-presets';
1819
- const PRESETS_PANEL_KEY = 'neural-skills-presets-open';
1820
- const PRESETS_COLLAPSED_KEY = 'neural-skills-preset-collapsed';
1821
-
1822
- const PRESET_CATEGORIES = {
1823
- structural: { label: 'Structural', desc: 'Section building blocks' },
1824
- frontmatter: { label: 'Frontmatter', desc: 'YAML configuration snippets' },
1825
- patterns: { label: 'Patterns', desc: 'Complete reusable flows' },
1826
- custom: { label: 'Custom', desc: 'Your saved presets' },
1827
- };
1828
-
1829
- const BUILTIN_PRESETS = [
1830
- // ── Structural ──
1831
- { id: 'structural-role', name: 'Role / Persona', description: 'Define who the AI should be',
1832
- category: 'structural', types: ['skill', 'agent'],
1833
- snippet: '## Role\n\nYou are a [specialization] expert. Your role is to [primary responsibility].\n\n### Personality\n- Tone: [professional / casual / technical]\n- Verbosity: [concise / detailed]\n- Focus: [accuracy / speed / creativity]\n',
1834
- educationalNote: 'The Role section is the most impactful part of your prompt. It sets expectations for tone, expertise level, and behavioral boundaries.' },
1835
-
1836
- { id: 'structural-steps', name: 'Step-by-Step', description: 'Numbered workflow steps',
1837
- category: 'structural', types: ['skill', 'command'],
1838
- snippet: '## Steps\n\n1. **Gather**: Collect necessary information\n2. **Analyze**: Process and understand the input\n3. **Execute**: Perform the main task\n4. **Verify**: Check results for correctness\n5. **Report**: Present findings to the user\n',
1839
- educationalNote: 'Numbered steps give the AI a clear execution order. Each step should be a distinct, verifiable action.' },
1840
-
1841
- { id: 'structural-constraints', name: 'Constraints', description: 'Rules and boundaries',
1842
- category: 'structural', types: [],
1843
- snippet: '## Constraints\n\n- **NEVER** modify files outside the project directory\n- **NEVER** delete files without explicit user confirmation\n- **ALWAYS** explain your reasoning before making changes\n- **ALWAYS** check for existing patterns before creating new ones\n- Prefer editing existing files over creating new ones\n',
1844
- educationalNote: 'Constraints prevent the AI from taking unwanted actions. Use NEVER/ALWAYS for hard rules, softer language for preferences.' },
1845
-
1846
- { id: 'structural-output', name: 'Output Format', description: 'Structure the response format',
1847
- category: 'structural', types: [],
1848
- snippet: '## Output Format\n\nReturn results as:\n\n```\n## Summary\n[1-2 sentence overview]\n\n## Findings\n- [Finding 1]\n- [Finding 2]\n\n## Recommendations\n1. [Action item]\n```\n',
1849
- educationalNote: 'Defining output format ensures consistent, parseable responses. Use markdown templates so the AI knows exactly what structure to follow.' },
1850
-
1851
- { id: 'structural-examples', name: 'Examples', description: 'Show expected behavior with examples',
1852
- category: 'structural', types: ['skill', 'agent'],
1853
- snippet: '<example>\nContext: [Describe the situation]\nuser: "[Example user message]"\nassistant: "[Expected AI response]"\n</example>\n\n<example>\nContext: [Different situation]\nuser: "[Another example]"\nassistant: "[Expected response for this case]"\n</example>\n',
1854
- educationalNote: 'Examples are the most powerful teaching tool. The AI pattern-matches from examples to understand your intent better than abstract rules.' },
1855
-
1856
- { id: 'structural-context', name: 'Context Gathering', description: 'Gather info before acting',
1857
- category: 'structural', types: ['skill', 'command'],
1858
- snippet: '## Step 1: Gather Context\n\nBefore proceeding, collect the necessary context:\n\n1. Read the relevant files using `Read` or `Glob`\n2. Search for existing patterns with `Grep`\n3. Check for related configuration\n4. Understand the current state before making changes\n\n**Do not skip this step.** Acting without context leads to errors.\n',
1859
- educationalNote: 'Context gathering prevents the AI from making assumptions. It forces a research-first approach that produces better results.' },
1860
-
1861
- { id: 'structural-error', name: 'Error Handling', description: 'What to do when things fail',
1862
- category: 'structural', types: ['skill', 'command'],
1863
- snippet: '## Error Handling\n\nIf an error occurs:\n1. Report the error clearly to the user\n2. Explain what was attempted and why it failed\n3. Suggest concrete next steps or alternatives\n4. Do NOT retry the same action without changes\n5. Do NOT silently ignore errors\n',
1864
- educationalNote: 'Error handling instructions prevent the AI from getting stuck in retry loops or silently failing.' },
1865
-
1866
- { id: 'structural-guard', name: 'Guard Rails', description: 'Safety boundaries for agents',
1867
- category: 'structural', types: ['agent'],
1868
- snippet: '## Guard Rails\n\n- Never modify files outside the specified scope\n- Always confirm destructive operations with the user\n- Stop and report if encountering unexpected state\n- Maximum 3 retries on any failing operation\n- If blocked, report the blocker — do not force through\n',
1869
- educationalNote: 'Guard rails are critical for agents that run autonomously. They prevent runaway behavior and destructive mistakes.' },
1870
-
1871
- // ── Frontmatter ──
1872
- { id: 'fm-tools-readonly', name: 'Read-Only Tools', description: 'Safe exploration tools only',
1873
- category: 'frontmatter', types: ['skill', 'agent'],
1874
- snippet: 'allowed-tools:\n - Read\n - Grep\n - Glob\n',
1875
- educationalNote: 'Read-only tools make your skill safe. The AI can explore the codebase but cannot modify any files.' },
1876
-
1877
- { id: 'fm-tools-full', name: 'Full Tool Access', description: 'All tools enabled',
1878
- category: 'frontmatter', types: ['skill', 'agent'],
1879
- snippet: 'allowed-tools:\n - Read\n - Write\n - Edit\n - Bash\n - Grep\n - Glob\n - WebFetch\n - WebSearch\n - Task\n - NotebookEdit\n',
1880
- educationalNote: 'Full tool access gives maximum capability but less safety. Use this for trusted, well-tested skills.' },
1881
-
1882
- { id: 'fm-tools-web', name: 'Web Access', description: 'Internet research tools',
1883
- category: 'frontmatter', types: ['skill', 'agent'],
1884
- snippet: 'allowed-tools:\n - WebFetch\n - WebSearch\n',
1885
- educationalNote: 'Web tools let the AI search the internet and fetch URLs. Useful for research, documentation lookup, and staying current.' },
1886
-
1887
- { id: 'fm-tools-task', name: 'Task Delegation', description: 'Sub-agent delegation',
1888
- category: 'frontmatter', types: ['skill'],
1889
- snippet: 'allowed-tools:\n - Task\n - Read\n - Grep\n - Glob\n# Delegates complex subtasks to specialized sub-agents\n',
1890
- educationalNote: 'The Task tool lets your skill spawn sub-agents for parallel or specialized work. Great for orchestrator skills.' },
1891
-
1892
- { id: 'fm-model', name: 'Model Selection', description: 'Choose which Claude model to use',
1893
- category: 'frontmatter', types: ['agent'],
1894
- snippet: 'model: sonnet # Options: haiku (fast/cheap), sonnet (balanced), opus (most capable)\n',
1895
- educationalNote: 'Agents can specify which model to use. Haiku is fast and cheap, Sonnet is balanced, Opus is the most capable.' },
1896
-
1897
- { id: 'fm-trigger', name: 'Trigger Pattern', description: 'When the skill should activate',
1898
- category: 'frontmatter', types: ['skill'],
1899
- snippet: 'description: >\n This skill handles [task type].\n Triggers on: "[keyword1]", "[keyword2]", "[keyword3]".\n',
1900
- educationalNote: 'The description field tells Claude when to auto-detect and invoke your skill. List specific trigger phrases for reliable activation.' },
1901
-
1902
- { id: 'fm-args', name: 'Argument Hint', description: 'Show expected arguments',
1903
- category: 'frontmatter', types: ['skill', 'command'],
1904
- snippet: 'argument-hint: "[target] [--option]"\n',
1905
- educationalNote: 'The argument hint shows users what to type after the slash command. Use brackets for required args, flags for options.' },
1906
-
1907
- // ── Patterns ──
1908
- { id: 'pattern-analyze', name: 'Analysis Pattern', description: 'Analyze-then-report flow',
1909
- category: 'patterns', types: ['skill', 'command'],
1910
- snippet: '## Analysis Workflow\n\n### Step 1: Identify Target\nParse `$ARGUMENTS` to determine what to analyze.\nIf no target specified, ask the user.\n\n### Step 2: Gather Data\n- Read the target files\n- Search for related patterns with `Grep`\n- Note dependencies and connections\n\n### Step 3: Analyze\n- Check for common issues and anti-patterns\n- Evaluate code quality and consistency\n- Identify improvement opportunities\n\n### Step 4: Report\nPresent findings as:\n- **Issues**: Problems that need fixing\n- **Warnings**: Potential concerns\n- **Suggestions**: Optional improvements\n',
1911
- educationalNote: 'The analysis pattern is one of the most useful flows. It follows a gather-analyze-report cycle that works for any review task.' },
1912
-
1913
- { id: 'pattern-transform', name: 'Transform Pattern', description: 'Read-transform-write pipeline',
1914
- category: 'patterns', types: ['skill', 'command'],
1915
- snippet: '## Transform Pipeline\n\n### Step 1: Read Source\nRead the input file(s) specified in `$ARGUMENTS`.\n\n### Step 2: Validate\nCheck that the input is in the expected format.\nIf not, report the issue and stop.\n\n### Step 3: Transform\nApply the transformation:\n- [Describe transformation logic]\n- Preserve formatting and style conventions\n- Handle edge cases gracefully\n\n### Step 4: Write Output\nWrite the transformed result.\nReport what changed and why.\n',
1916
- educationalNote: 'Transform patterns are perfect for code generation, format conversion, and automated refactoring tasks.' },
1917
-
1918
- { id: 'pattern-interactive', name: 'Interactive Menu', description: 'Argument routing with menu fallback',
1919
- category: 'patterns', types: ['skill'],
1920
- snippet: '## Routing\n\nRead `$ARGUMENTS`.\n\n**If arguments provided**, route to the matching subcommand below.\n**If blank**, present this menu to the user:\n\n| Command | Description |\n|---------|-------------|\n| `action1` | Does the first thing |\n| `action2` | Does the second thing |\n| `help` | Shows this menu |\n\nUse `AskUserQuestion` to let them pick.\n\n### action1\n[Instructions for action 1]\n\n### action2\n[Instructions for action 2]\n',
1921
- educationalNote: 'Interactive menus make skills user-friendly. When no arguments are given, show a menu instead of failing.' },
1922
-
1923
- { id: 'pattern-progressive', name: 'Progressive Loading', description: 'Load reference files on demand',
1924
- category: 'patterns', types: ['skill'],
1925
- snippet: '## References\n\nThis skill uses progressive loading. Detailed docs live in sub-files:\n\n- `$SKILL_DIR/references/guide.md` — Complete usage guide\n- `$SKILL_DIR/references/examples.md` — Code examples\n- `$SKILL_DIR/references/api.md` — API reference\n\n## Instructions\n\n1. Read `$ARGUMENTS` to understand the request\n2. If you need detailed information, use `Read` tool on the reference files above\n3. Do NOT load all references upfront — only load what you need\n4. Follow the instructions from the loaded reference\n',
1926
- educationalNote: 'Progressive loading keeps your main SKILL.md small and fast. Sub-files are loaded on demand via $SKILL_DIR, saving context window space.' },
1927
-
1928
- { id: 'pattern-review', name: 'Code Review', description: 'Multi-file review with structured findings',
1929
- category: 'patterns', types: ['skill', 'command'],
1930
- snippet: '## Code Review\n\n### Step 1: Scope\nDetermine files to review from `$ARGUMENTS`.\nIf a directory, find relevant files with `Glob`.\n\n### Step 2: Review Each File\nFor each file, check:\n- [ ] Code correctness and logic errors\n- [ ] Security vulnerabilities (injection, XSS, etc.)\n- [ ] Performance concerns\n- [ ] Style consistency with existing codebase\n- [ ] Missing error handling\n\n### Step 3: Summary\nPresent a severity-ranked list:\n- **Critical**: Must fix before merge\n- **Warning**: Should fix soon\n- **Info**: Nice to have improvements\n',
1931
- educationalNote: 'Code review patterns work great as reusable skills. The checklist format ensures consistent, thorough reviews.' },
1932
-
1933
- { id: 'pattern-crud', name: 'CRUD Operations', description: 'Create/read/update/delete router',
1934
- category: 'patterns', types: ['skill'],
1935
- snippet: '## Subcommands\n\nRoute based on `$ARGUMENTS`:\n\n### create [name]\nCreate a new [resource]:\n1. Validate the name\n2. Check for duplicates\n3. Generate from template\n4. Write to disk\n5. Report success\n\n### list\nList all [resources]:\n1. Scan the directory\n2. Parse metadata\n3. Display as formatted table\n\n### update [name]\nModify an existing [resource]:\n1. Read current state\n2. Apply changes\n3. Validate result\n4. Write back\n\n### delete [name]\nRemove a [resource]:\n1. Confirm with user\n2. Delete file(s)\n3. Clean up references\n',
1936
- educationalNote: 'CRUD patterns are great for skills that manage collections of things — presets, templates, configurations, etc.' },
1937
-
1938
- // ── More Structural ──
1939
- { id: 'structural-variables', name: 'Variables Reference', description: 'All available runtime variables',
1940
- category: 'structural', types: ['skill', 'command'],
1941
- snippet: '## Variables\n\n- `$ARGUMENTS` — User input after the slash command\n- `$SKILL_DIR` — Absolute path to this skill\'s directory\n- Dynamic injection: `!`git branch --show-current`` runs at load time\n',
1942
- educationalNote: '$ARGUMENTS contains everything the user typed after /command-name. $SKILL_DIR is the absolute path to the skill folder, useful for loading sub-files.' },
1943
-
1944
- { id: 'structural-validation', name: 'Input Validation', description: 'Validate arguments before processing',
1945
- category: 'structural', types: ['skill', 'command'],
1946
- snippet: '## Input Validation\n\nBefore proceeding, validate `$ARGUMENTS`:\n\n1. If empty — show usage help and ask for input\n2. If it looks like a file path — verify the file exists with `Read`\n3. If it looks like a URL — validate format\n4. If invalid — explain what\'s expected and give examples\n\n**Never proceed with invalid input.** Always validate first.\n',
1947
- educationalNote: 'Input validation prevents confusing errors downstream. Always check $ARGUMENTS before acting on them.' },
1948
-
1949
- { id: 'structural-askuser', name: 'Interactive Questions', description: 'Use AskUserQuestion for choices',
1950
- category: 'structural', types: ['skill', 'command'],
1951
- snippet: '## User Interaction\n\nUse `AskUserQuestion` to present choices:\n\n- Provide 2-4 clear options with descriptions\n- Include a recommended option marked with "(Recommended)"\n- Set `multiSelect: true` if multiple choices are valid\n- Users can always type "Other" for custom input\n\nExample: Ask which approach to take before making changes.\n',
1952
- educationalNote: 'AskUserQuestion creates a proper selection UI instead of asking open-ended questions. It supports single/multi-select with descriptions.' },
1953
-
1954
- { id: 'structural-todowrite', name: 'Progress Tracking', description: 'Track work with TodoWrite',
1955
- category: 'structural', types: ['skill', 'command'],
1956
- snippet: '## Task Tracking\n\nFor multi-step tasks, use `TodoWrite` to track progress:\n\n1. Create a todo list with all planned steps\n2. Mark each task `in_progress` before starting (only ONE at a time)\n3. Mark `completed` immediately after finishing each task\n4. Add new tasks if discovered during work\n\nThis keeps the user informed and ensures nothing is missed.\n',
1957
- educationalNote: 'TodoWrite creates a visible progress tracker. Users can see which steps are done, in progress, and pending.' },
1958
-
1959
- { id: 'structural-scope', name: 'Scope Definition', description: 'Define what is and isn\'t in scope',
1960
- category: 'structural', types: ['skill', 'agent'],
1961
- snippet: '## Scope\n\n### In Scope\n- [List what this skill/agent handles]\n- [Specific files, directories, or domains]\n- [Types of requests to accept]\n\n### Out of Scope\n- [What to refuse or redirect]\n- [Adjacent tasks that belong to other tools]\n- [Explicitly excluded scenarios]\n\nIf a request is out of scope, explain why and suggest the right tool.\n',
1962
- educationalNote: 'Scope definition prevents scope creep. The AI knows exactly what to handle and what to decline.' },
1963
-
1964
- { id: 'structural-dynamic-context', name: 'Dynamic Context', description: 'Inject live data at load time',
1965
- category: 'structural', types: ['skill', 'command'],
1966
- snippet: '## Current Context\n\n- **Branch**: !`git branch --show-current`\n- **Status**: !`git status --short | head -20`\n- **Last commit**: !`git log -1 --oneline`\n- **Working directory**: !`pwd`\n',
1967
- educationalNote: 'Backtick-bang syntax (!`command`) runs shell commands when the skill loads. The output is injected as static text into the prompt.' },
1968
-
1969
- { id: 'structural-reporting', name: 'Report Template', description: 'Structured findings report',
1970
- category: 'structural', types: [],
1971
- snippet: '## Report\n\nPresent findings using this structure:\n\n### Summary\n[1-2 sentence overview of what was found]\n\n### Critical Issues\n- [file:line] Issue description — **Impact**: [what breaks]\n\n### Warnings\n- [file:line] Warning description — **Risk**: [potential problem]\n\n### Recommendations\n1. [Highest priority action]\n2. [Second priority action]\n\n### Stats\n- Files analyzed: [N]\n- Issues found: [N critical, N warnings]\n',
1972
- educationalNote: 'Structured reports make findings actionable. The severity-ranked format helps users prioritize fixes.' },
1973
-
1974
- { id: 'structural-confirmation', name: 'Confirmation Gate', description: 'Ask before destructive actions',
1975
- category: 'structural', types: ['skill', 'command'],
1976
- snippet: '## Confirmation Required\n\nBefore executing any destructive action:\n\n1. **Show exactly** what will be modified/deleted\n2. **List affected files** with specific changes\n3. **Ask for confirmation** using `AskUserQuestion`\n4. Only proceed after explicit "Yes" confirmation\n5. If user says "No" — explain alternatives\n\n**NEVER** skip confirmation for: file deletion, bulk edits, git operations, or data modifications.\n',
1977
- educationalNote: 'Confirmation gates prevent accidental destructive actions. Always show what will happen before doing it.' },
1978
-
1979
- { id: 'structural-retry', name: 'Retry Logic', description: 'Handle failures with retries',
1980
- category: 'structural', types: ['skill', 'agent'],
1981
- snippet: '## Retry Strategy\n\nWhen an operation fails:\n\n1. **First failure**: Report the error, analyze the cause\n2. **Adjust approach**: Try a different method or fix the root cause\n3. **Second attempt**: Retry with the adjusted approach\n4. **If still failing**: Stop, report what was tried, and ask for help\n\n**Maximum 2 retries.** Do NOT brute-force or repeat the same action.\nDo NOT silently swallow errors.\n',
1982
- educationalNote: 'Retry logic prevents infinite loops. The key is to change approach between retries, not just repeat the same thing.' },
1983
-
1984
- { id: 'structural-phased', name: 'Multi-Phase Workflow', description: 'Large task broken into phases',
1985
- category: 'structural', types: ['skill'],
1986
- snippet: '## Phase 1: Discovery\nGather requirements. Read relevant files. Ask clarifying questions.\n\n## Phase 2: Analysis\nAnalyze the codebase. Identify patterns, dependencies, and constraints.\n\n## Phase 3: Planning\nDesign the approach. Present plan to user for approval.\n\n## Phase 4: Implementation\nExecute the plan. Track progress with `TodoWrite`.\n\n## Phase 5: Verification\nRun tests. Check for regressions. Validate the output.\n\n## Phase 6: Summary\nReport what was done, what changed, and any follow-up items.\n',
1987
- educationalNote: 'Multi-phase workflows are the most robust pattern for complex tasks. Each phase has a clear goal and exit criteria.' },
1988
-
1989
- // ── More Frontmatter ──
1990
- { id: 'fm-tools-filemod', name: 'File Modification Tools', description: 'Read + Write + Edit combo',
1991
- category: 'frontmatter', types: ['skill', 'agent'],
1992
- snippet: 'allowed-tools:\n - Read\n - Write\n - Edit\n - Glob\n - Grep\n',
1993
- educationalNote: 'File modification tools let the AI read, create, and edit files. No Bash access keeps it safer than full tool access.' },
1994
-
1995
- { id: 'fm-tools-git', name: 'Git Operations', description: 'Bash restricted to git commands',
1996
- category: 'frontmatter', types: ['skill'],
1997
- snippet: 'allowed-tools:\n - Read\n - Grep\n - Glob\n - Bash\n# Note: Instruct the skill to only use Bash for git operations\n',
1998
- educationalNote: 'For git-focused skills, include Bash but instruct the skill to only use it for git commands. Explicit instructions in the body reinforce the restriction.' },
1999
-
2000
- { id: 'fm-tools-analysis', name: 'Analysis Tools', description: 'Read + search tools for review',
2001
- category: 'frontmatter', types: ['skill', 'agent'],
2002
- snippet: 'allowed-tools:\n - Read\n - Grep\n - Glob\n - WebSearch\n',
2003
- educationalNote: 'Analysis tools are read-only with web search. Perfect for review, audit, and research skills that should never modify files.' },
2004
-
2005
- { id: 'fm-disable-auto', name: 'User-Only Invocation', description: 'Disable auto-detection',
2006
- category: 'frontmatter', types: ['skill'],
2007
- snippet: 'disable-model-invocation: true\n# Only triggered by user typing /skill-name, never auto-detected\n',
2008
- educationalNote: 'disable-model-invocation prevents Claude from auto-invoking the skill. The user must explicitly type the slash command.' },
2009
-
2010
- { id: 'fm-fork-context', name: 'Forked Context', description: 'Run skill in isolated subagent',
2011
- category: 'frontmatter', types: ['skill'],
2012
- snippet: 'context: fork\nagent: Explore\n# Runs in an isolated subagent context to avoid polluting the main conversation\n',
2013
- educationalNote: 'context: fork runs the skill in a separate subagent. Useful for heavy research that would clutter the main context window.' },
2014
-
2015
- { id: 'fm-agent-fast', name: 'Fast Agent (Haiku)', description: 'Quick, cheap agent for simple tasks',
2016
- category: 'frontmatter', types: ['agent'],
2017
- snippet: 'model: haiku\ntools: Read, Grep, Glob\n# Fast and cheap — ideal for simple checks, linting, quick lookups\n',
2018
- educationalNote: 'Haiku agents are fast and cheap. Use them for simple, repetitive checks where speed matters more than deep reasoning.' },
2019
-
2020
- { id: 'fm-agent-balanced', name: 'Balanced Agent (Sonnet)', description: 'Default agent for most tasks',
2021
- category: 'frontmatter', types: ['agent'],
2022
- snippet: 'model: sonnet\ntools: Read, Write, Edit, Bash, Grep, Glob\n# Balanced cost/capability — the recommended default for most agents\n',
2023
- educationalNote: 'Sonnet is the recommended default for agents. Good balance of capability and cost for analysis, coding, and research tasks.' },
2024
-
2025
- { id: 'fm-agent-powerful', name: 'Powerful Agent (Opus)', description: 'Maximum capability agent',
2026
- category: 'frontmatter', types: ['agent'],
2027
- snippet: 'model: opus\ntools: Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, Task\n# Most capable — use for complex architecture, migrations, and critical decisions\n',
2028
- educationalNote: 'Opus agents are the most capable but expensive. Reserve for complex architecture decisions, large migrations, and critical analysis.' },
2029
-
2030
- { id: 'fm-trigger-multi', name: 'Multiple Triggers', description: 'Rich trigger phrase list',
2031
- category: 'frontmatter', types: ['skill'],
2032
- snippet: 'description: >\n Use this skill when the user asks about [topic].\n Triggers on: "[phrase1]", "[phrase2]", "[phrase3]",\n "[phrase4]", "[phrase5]".\n Also triggers when discussing [related-topic] or\n mentioning [specific-term].\n',
2033
- educationalNote: 'More trigger phrases = more reliable auto-detection. List 5+ phrases covering different ways users might ask for this skill.' },
2034
-
2035
- // ── More Patterns ──
2036
- { id: 'pattern-parallel-agents', name: 'Parallel Agents', description: 'Spawn multiple specialized agents',
2037
- category: 'patterns', types: ['skill'],
2038
- snippet: '## Parallel Analysis\n\nSpawn 3 specialized agents simultaneously using the `Task` tool:\n\n### Agent 1: [Specialist A]\nFocus on [aspect A]. Report findings.\n\n### Agent 2: [Specialist B]\nFocus on [aspect B]. Report findings.\n\n### Agent 3: [Specialist C]\nFocus on [aspect C]. Report findings.\n\n## Synthesis\n\nAfter all agents return, synthesize their findings:\n1. Identify overlapping concerns\n2. Resolve conflicting recommendations\n3. Present unified action plan\n',
2039
- educationalNote: 'Parallel agents run simultaneously, dramatically speeding up multi-faceted analysis. Each agent gets its own context window.' },
2040
-
2041
- { id: 'pattern-memory-recall', name: 'Memory-Powered', description: 'Recall context before acting',
2042
- category: 'patterns', types: ['skill'],
2043
- snippet: '## Step 1: Recall Context\n\nBefore doing anything, search memory for relevant context:\n\n1. `recall` with query about the current topic\n2. `recall` with category filter for related decisions\n3. Check for past bugs, patterns, or architecture decisions\n\n## Step 2: Act with Context\n\nUse the recalled context to inform your approach.\nAvoid repeating past mistakes or contradicting past decisions.\n\n## Step 3: Remember Results\n\nAfter completing the task:\n1. `remember` what was done, why, and how\n2. Include related_files, appropriate importance, and 3-5 tags\n3. One task = one memory (do not batch)\n',
2044
- educationalNote: 'Memory-powered skills use SynaBun recall/remember to build on past work. They get smarter over time as context accumulates.' },
2045
-
2046
- { id: 'pattern-test-gen', name: 'Test Generation', description: 'Generate tests for code',
2047
- category: 'patterns', types: ['skill', 'command'],
2048
- snippet: '## Test Generation\n\n### Step 1: Analyze Target\nRead the target file(s) from `$ARGUMENTS`.\nIdentify:\n- Public functions and their signatures\n- Edge cases and boundary conditions\n- Dependencies that need mocking\n- Existing test patterns in the project\n\n### Step 2: Generate Tests\nFor each function:\n- Happy path test (expected input → expected output)\n- Edge case tests (empty, null, boundary values)\n- Error case tests (invalid input, failures)\n- Integration test if it calls external services\n\n### Step 3: Verify\nRun the tests with `Bash`. Fix any failures.\nEnsure all tests pass before reporting.\n',
2049
- educationalNote: 'Test generation skills are incredibly useful. They analyze code structure and generate comprehensive test suites following existing project patterns.' },
2050
-
2051
- { id: 'pattern-security', name: 'Security Review', description: 'Security-focused code analysis',
2052
- category: 'patterns', types: ['skill', 'agent'],
2053
- snippet: '## Security Review\n\n### Check for OWASP Top 10:\n- [ ] **Injection**: SQL, command, XSS, template injection\n- [ ] **Broken Auth**: Hardcoded secrets, weak tokens, missing MFA\n- [ ] **Sensitive Data**: Exposed API keys, PII in logs, unencrypted storage\n- [ ] **XXE/SSRF**: External entity processing, server-side request forgery\n- [ ] **Broken Access**: Missing authorization checks, IDOR vulnerabilities\n- [ ] **Misconfig**: Debug mode on, default credentials, verbose errors\n- [ ] **Dependency Vulns**: Outdated packages with known CVEs\n\n### Report Format\nFor each finding:\n- **Severity**: Critical / High / Medium / Low\n- **Location**: file:line\n- **Description**: What the vulnerability is\n- **Remediation**: How to fix it\n- **Confidence**: percentage (only report >= 80%)\n',
2054
- educationalNote: 'Security review agents should filter by confidence (>= 80%) to avoid false positives. The OWASP checklist ensures comprehensive coverage.' },
2055
-
2056
- { id: 'pattern-refactor', name: 'Refactoring', description: 'Safe code refactoring workflow',
2057
- category: 'patterns', types: ['skill', 'command'],
2058
- snippet: '## Refactoring Workflow\n\n### Step 1: Understand Current State\n- Read the target code thoroughly\n- Map all callers and dependencies with `Grep`\n- Identify the refactoring goal\n\n### Step 2: Plan Changes\nPresent the refactoring plan to the user:\n- What will change and why\n- What will NOT change\n- Risks and mitigation\n\n### Step 3: Execute (after approval)\n- Make changes incrementally\n- Update all call sites\n- Preserve existing behavior exactly\n\n### Step 4: Verify\n- Run existing tests\n- Check for type errors\n- Verify no regressions\n',
2059
- educationalNote: 'Refactoring skills should always plan before executing, update all call sites, and verify with tests. Never change behavior during refactoring.' },
2060
-
2061
- { id: 'pattern-migration', name: 'Migration', description: 'Database or API migration flow',
2062
- category: 'patterns', types: ['skill'],
2063
- snippet: '## Migration Plan\n\n### Step 1: Audit Current State\n- Catalog all usages of the old API/schema\n- Count affected files with `Grep`\n- Identify breaking changes\n\n### Step 2: Create Migration\n- Write migration script (up + down)\n- Handle data transformation\n- Add rollback capability\n\n### Step 3: Update Code\n- Replace old API calls with new ones\n- Update types and interfaces\n- Update tests\n\n### Step 4: Verify\n- Run migration in dry-run mode\n- Run full test suite\n- Check for orphaned references\n\n### Rollback Plan\nIf migration fails:\n1. Run the down migration\n2. Restore from backup\n3. Report what went wrong\n',
2064
- educationalNote: 'Migration skills need rollback plans. Always write both up and down migrations, and verify with dry-run before executing.' },
2065
-
2066
- { id: 'pattern-documentation', name: 'Documentation Generator', description: 'Auto-generate docs from code',
2067
- category: 'patterns', types: ['skill', 'command'],
2068
- snippet: '## Documentation Generation\n\n### Step 1: Scan\nIdentify documentation targets:\n- Public APIs and their parameters\n- Configuration options\n- Architecture patterns\n- Setup/installation steps\n\n### Step 2: Analyze\nFor each target:\n- Read the source code\n- Extract function signatures, types, and comments\n- Identify usage examples from tests\n- Note dependencies and prerequisites\n\n### Step 3: Generate\nWrite documentation following project conventions:\n- Clear description of purpose\n- Parameters with types and defaults\n- Usage examples (from real code when possible)\n- Related functions/components\n',
2069
- educationalNote: 'Documentation generators are more useful when they pull real examples from tests and existing code rather than inventing hypothetical ones.' },
2070
-
2071
- { id: 'pattern-debug', name: 'Debugging', description: 'Systematic debugging workflow',
2072
- category: 'patterns', types: ['skill', 'command'],
2073
- snippet: '## Debugging Workflow\n\n### Step 1: Reproduce\nUnderstand the bug from `$ARGUMENTS`.\n- What is the expected behavior?\n- What is the actual behavior?\n- What are the reproduction steps?\n\n### Step 2: Investigate\n- Search for the error message with `Grep`\n- Read the relevant source files\n- Trace the execution path\n- Check recent git changes: `git log --oneline -10`\n\n### Step 3: Identify Root Cause\n- Form a hypothesis\n- Verify by reading the code path\n- Check for off-by-one, null checks, async issues, race conditions\n\n### Step 4: Fix\n- Make the minimal change that fixes the root cause\n- Do NOT refactor surrounding code\n- Add a comment if the fix is non-obvious\n\n### Step 5: Verify\n- Run tests\n- Confirm the original reproduction steps no longer fail\n',
2074
- educationalNote: 'Debugging skills follow a systematic approach: reproduce, investigate, hypothesize, fix, verify. The key is finding root cause, not just symptoms.' },
2075
-
2076
- { id: 'pattern-git-workflow', name: 'Git Workflow', description: 'Branch, commit, PR workflow',
2077
- category: 'patterns', types: ['skill', 'command'],
2078
- snippet: '## Git Workflow\n\n### Step 1: Branch\n- Create feature branch: `git checkout -b feature/[name]`\n- Verify clean working tree\n\n### Step 2: Make Changes\n- Implement the feature/fix\n- Stage specific files (avoid `git add .`)\n\n### Step 3: Commit\n- Write clear commit message (why, not what)\n- Follow existing commit style from `git log`\n- Never skip pre-commit hooks\n\n### Step 4: Push & PR\n- Push with `-u` flag: `git push -u origin feature/[name]`\n- Create PR with `gh pr create`\n- Include summary, test plan, and checklist\n\n**Safety**: Never force-push to main. Never amend published commits.\n',
2079
- educationalNote: 'Git workflow skills should always follow safety practices: specific file staging, meaningful commits, never force-push to main.' },
2080
-
2081
- { id: 'pattern-api-client', name: 'API Client', description: 'HTTP API interaction pattern',
2082
- category: 'patterns', types: ['skill'],
2083
- snippet: '## API Integration\n\n### Configuration\n- Base URL: [endpoint]\n- Auth: Bearer token from environment variable\n- Rate limit: [N] requests per [time]\n\n### Request Pattern\n1. Build the request URL and headers\n2. Validate parameters before sending\n3. Use `WebFetch` to make the request\n4. Parse the JSON response\n5. Handle errors (4xx, 5xx, network)\n\n### Error Handling\n- 401: Token expired — report to user\n- 429: Rate limited — wait and retry once\n- 500: Server error — report and don\'t retry\n- Network error: Report connectivity issue\n',
2084
- educationalNote: 'API client skills should always handle auth, rate limits, and errors gracefully. Never hardcode secrets — use environment variables.' },
2085
-
2086
- { id: 'pattern-scaffold', name: 'Scaffolding / Generator', description: 'Generate files from templates',
2087
- category: 'patterns', types: ['skill', 'command'],
2088
- snippet: '## Scaffolding\n\nGenerate project files from the input `$ARGUMENTS`:\n\n### Step 1: Parse Input\nExtract: name, type, options from arguments.\n\n### Step 2: Check Conventions\n- Read existing files to match project style\n- Check naming conventions (kebab-case, PascalCase, etc.)\n- Identify import patterns and directory structure\n\n### Step 3: Generate Files\nFor each file to create:\n1. Verify the directory exists\n2. Check for conflicts (don\'t overwrite)\n3. Write the file using project conventions\n4. Add necessary imports/exports\n\n### Step 4: Report\nList all created files and any manual steps needed.\n',
2089
- educationalNote: 'Scaffolding skills should always check existing conventions before generating. The generated code should look like a human wrote it following project style.' },
2090
-
2091
- { id: 'pattern-lint-fix', name: 'Lint & Fix', description: 'Find and auto-fix code issues',
2092
- category: 'patterns', types: ['skill', 'command'],
2093
- snippet: '## Lint & Fix\n\n### Step 1: Identify Issues\nRun the project linter:\n- `npm run lint` or equivalent\n- Parse the output for file:line:message\n- Group issues by severity and type\n\n### Step 2: Auto-Fix\nFor each fixable issue:\n- Read the file\n- Apply the fix using `Edit`\n- Verify the fix doesn\'t break other code\n\n### Step 3: Report Unfixable\nFor issues requiring human judgment:\n- List them with file:line references\n- Explain why auto-fix wasn\'t applied\n- Suggest the recommended fix\n\n### Step 4: Re-Run\nRun the linter again to confirm all auto-fixes are clean.\n',
2094
- educationalNote: 'Lint-fix skills should always re-run the linter after fixing to confirm no regressions. Some issues need human judgment — report those separately.' },
2095
-
2096
- { id: 'pattern-perf-audit', name: 'Performance Audit', description: 'Identify performance bottlenecks',
2097
- category: 'patterns', types: ['skill', 'agent'],
2098
- snippet: '## Performance Audit\n\n### Check for Common Issues:\n- [ ] **N+1 queries**: Loop with individual DB calls\n- [ ] **Missing indexes**: Queries on unindexed columns\n- [ ] **Unnecessary re-renders**: React components re-rendering without prop changes\n- [ ] **Large bundles**: Importing entire libraries for single functions\n- [ ] **Uncached data**: Repeated expensive computations\n- [ ] **Memory leaks**: Event listeners not cleaned up, growing arrays\n- [ ] **Synchronous blocking**: Long-running sync operations on main thread\n\n### For Each Finding:\n- **Impact**: Estimated performance gain from fixing\n- **Location**: file:line\n- **Fix**: Specific code change needed\n- **Priority**: High (user-facing) / Medium / Low\n',
2099
- educationalNote: 'Performance audits should quantify impact when possible. Prioritize user-facing performance (page load, API response time) over theoretical improvements.' },
2100
-
2101
- { id: 'pattern-config-wizard', name: 'Configuration Wizard', description: 'Interactive setup with questions',
2102
- category: 'patterns', types: ['skill'],
2103
- snippet: '## Configuration Wizard\n\nGuide the user through setup step by step:\n\n### Step 1: Detect Current State\nCheck what\'s already configured:\n- Read existing config files\n- Check for environment variables\n- Identify missing pieces\n\n### Step 2: Ask Questions\nFor each missing configuration:\n- Use `AskUserQuestion` with clear options\n- Provide recommended defaults\n- Explain what each option does\n\n### Step 3: Generate Config\nBased on answers:\n- Create/update config files\n- Set environment variables\n- Generate any needed boilerplate\n\n### Step 4: Validate\n- Verify the configuration works\n- Run a health check if applicable\n- Report success and next steps\n',
2104
- educationalNote: 'Configuration wizards detect existing state first, then only ask about missing pieces. Always provide recommended defaults.' },
2105
-
2106
- { id: 'pattern-changelog', name: 'Changelog Generator', description: 'Generate changelog from git history',
2107
- category: 'patterns', types: ['skill', 'command'],
2108
- snippet: '## Changelog Generation\n\n### Step 1: Get Commits\nRead git history since last tag/release:\n- `git log --oneline [last-tag]..HEAD`\n- Parse commit messages for type (feat, fix, refactor, etc.)\n\n### Step 2: Categorize\nGroup commits by type:\n- **Features**: New functionality\n- **Bug Fixes**: Corrections to existing behavior\n- **Breaking Changes**: Incompatible changes\n- **Other**: Refactors, docs, chores\n\n### Step 3: Format\nGenerate markdown changelog:\n```\n## [version] - YYYY-MM-DD\n\n### Features\n- Description (commit hash)\n\n### Bug Fixes\n- Description (commit hash)\n```\n',
2109
- educationalNote: 'Changelog generators work best when the project follows conventional commit format (feat:, fix:, etc.). They parse git history automatically.' },
2110
-
2111
- { id: 'pattern-search-replace', name: 'Search & Replace', description: 'Project-wide find and replace',
2112
- category: 'patterns', types: ['skill', 'command'],
2113
- snippet: '## Project-Wide Search & Replace\n\n### Step 1: Find All Occurrences\nUse `Grep` to find every occurrence of the target:\n- Search with regex for variations (imports, usages, references)\n- Count total occurrences and affected files\n- Present the full list to the user\n\n### Step 2: Confirm\nShow the replacement plan:\n- **Find**: `[old pattern]`\n- **Replace**: `[new pattern]`\n- **Files affected**: [N]\n- **Occurrences**: [N]\n\nAsk for confirmation before proceeding.\n\n### Step 3: Replace\nFor each file:\n- Read the file\n- Apply the replacement using `Edit`\n- Verify the replacement is correct\n\n### Step 4: Verify\n- Run `Grep` again to confirm zero remaining occurrences\n- Run tests to check for regressions\n',
2114
- educationalNote: 'Search & replace skills should always show the full scope before executing. Regex helps catch variations (imports, type refs, comments).' },
2115
-
2116
- { id: 'pattern-dependency', name: 'Dependency Check', description: 'Audit and update dependencies',
2117
- category: 'patterns', types: ['skill', 'command'],
2118
- snippet: '## Dependency Audit\n\n### Step 1: Scan\n- Read package.json (or equivalent)\n- Check for outdated packages\n- Identify security vulnerabilities\n\n### Step 2: Analyze\nFor each outdated/vulnerable dependency:\n- Check the changelog for breaking changes\n- Assess update risk (major vs minor vs patch)\n- Check if the project uses affected APIs\n\n### Step 3: Recommend\nPresent update plan:\n- **Safe updates** (patch/minor, no breaking changes)\n- **Risky updates** (major, has breaking changes)\n- **Do not update** (known incompatibilities)\n\n### Step 4: Update (with approval)\n- Apply safe updates first\n- Run tests after each group\n- Report any failures\n',
2119
- educationalNote: 'Dependency audits should always check changelogs for breaking changes before updating. Group updates by risk level.' },
2120
- ];
2121
-
2122
- function loadCustomPresets() {
2123
- try {
2124
- return JSON.parse(storage.getItem(PRESETS_KEY) || '[]');
2125
- } catch { return []; }
2126
- }
2127
-
2128
- function saveCustomPresets(arr) {
2129
- storage.setItem(PRESETS_KEY, JSON.stringify(arr));
2130
- }
2131
-
2132
- function getPresetsForType(type) {
2133
- const builtIn = BUILTIN_PRESETS.filter(p => p.types.length === 0 || p.types.includes(type));
2134
- const custom = loadCustomPresets();
2135
- return { builtIn, custom };
2136
- }
2137
-
2138
- function isPresetPanelOpen() {
2139
- return storage.getItem(PRESETS_PANEL_KEY) !== 'false'; // default open
2140
- }
2141
-
2142
- function getPresetCollapsed() {
2143
- try { return JSON.parse(storage.getItem(PRESETS_COLLAPSED_KEY) || '{}'); }
2144
- catch { return {}; }
2145
- }
2146
-
2147
- // ═══════════════════════════════════════════
2148
- // PRESET PANEL RENDERING
2149
- // ═══════════════════════════════════════════
2150
-
2151
- function renderPresetPanel() {
2152
- const panel = $('ss-preset-panel');
2153
- if (!panel) return;
2154
-
2155
- const type = _wizardData.type;
2156
- const { builtIn, custom } = getPresetsForType(type);
2157
- const collapsed = getPresetCollapsed();
2158
-
2159
- const groups = {
2160
- structural: builtIn.filter(p => p.category === 'structural'),
2161
- frontmatter: builtIn.filter(p => p.category === 'frontmatter'),
2162
- patterns: builtIn.filter(p => p.category === 'patterns'),
2163
- custom: custom,
2164
- };
2165
-
2166
- panel.innerHTML = Object.entries(groups).map(([catKey, presets]) => {
2167
- const cat = PRESET_CATEGORIES[catKey];
2168
- const isCollapsed = collapsed[catKey] === true;
2169
- const isCustom = catKey === 'custom';
2170
-
2171
- return `
2172
- <div class="ss-preset-category${isCollapsed ? ' collapsed' : ''}">
2173
- <div class="ss-preset-cat-hdr" data-cat="${catKey}">
2174
- <span class="ss-preset-chevron">${isCollapsed ? '\u25B6' : '\u25BC'}</span>
2175
- <span class="ss-preset-cat-name">${cat.label}</span>
2176
- <span class="ss-preset-cat-count">${presets.length}</span>
2177
- ${isCustom ? `
2178
- <div class="ss-preset-custom-actions">
2179
- <button class="ss-preset-act-btn ss-preset-act-add" data-action="add" data-tooltip="New preset"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
2180
- <button class="ss-preset-act-btn ss-preset-act-import" data-action="import" data-tooltip="Import presets"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="7 10 12 5 17 10"/><line x1="12" y1="5" x2="12" y2="19"/></svg></button>
2181
- <button class="ss-preset-act-btn ss-preset-act-export" data-action="export" data-tooltip="Export presets"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="7 14 12 19 17 14"/><line x1="12" y1="19" x2="12" y2="5"/></svg></button>
2182
- </div>
2183
- ` : ''}
2184
- </div>
2185
- <div class="ss-preset-cat-body">
2186
- ${isCustom ? '<div id="ss-preset-custom-form"></div>' : ''}
2187
- ${presets.map(p => renderPresetCard(p, isCustom)).join('')}
2188
- ${presets.length === 0 && !isCustom ? '<div class="ss-preset-empty">No presets for this type</div>' : ''}
2189
- ${presets.length === 0 && isCustom ? '<div class="ss-preset-empty">Click + to create a preset</div>' : ''}
2190
- </div>
2191
- </div>
2192
- `;
2193
- }).join('');
2194
-
2195
- wirePresetPanel(panel);
2196
- }
2197
-
2198
- function renderPresetCard(preset, isCustom) {
2199
- return `
2200
- <div class="ss-preset-card" draggable="true" data-preset-id="${preset.id}"
2201
- data-tooltip="${esc(preset.educationalNote || preset.description || '')}" data-tooltip-pos="right">
2202
- <div class="ss-preset-card-name">${esc(preset.name)}</div>
2203
- ${preset.description ? `<div class="ss-preset-card-desc">${esc(preset.description)}</div>` : ''}
2204
- ${isCustom ? `
2205
- <div class="ss-preset-card-actions">
2206
- <button class="ss-preset-edit-btn" data-id="${preset.id}" data-tooltip="Edit">&#9998;</button>
2207
- <button class="ss-preset-del-btn" data-id="${preset.id}" data-tooltip="Delete">&times;</button>
2208
- </div>
2209
- ` : ''}
2210
- </div>
2211
- `;
2212
- }
2213
-
2214
- function wirePresetPanel(panel) {
2215
- // Accordion toggle
2216
- panel.querySelectorAll('.ss-preset-cat-hdr').forEach(hdr => {
2217
- hdr.addEventListener('click', (e) => {
2218
- if (e.target.closest('.ss-preset-act-btn') || e.target.closest('.ss-preset-custom-actions')) return; // don't toggle on action buttons
2219
- const cat = hdr.dataset.cat;
2220
- const section = hdr.closest('.ss-preset-category');
2221
- section.classList.toggle('collapsed');
2222
- const collapsed = getPresetCollapsed();
2223
- collapsed[cat] = section.classList.contains('collapsed');
2224
- storage.setItem(PRESETS_COLLAPSED_KEY, JSON.stringify(collapsed));
2225
- hdr.querySelector('.ss-preset-chevron').textContent = collapsed[cat] ? '\u25B6' : '\u25BC';
2226
- });
2227
- });
2228
-
2229
- // Custom action buttons
2230
- panel.querySelectorAll('.ss-preset-act-btn').forEach(btn => {
2231
- btn.addEventListener('click', (e) => {
2232
- e.stopPropagation();
2233
- const action = btn.dataset.action;
2234
- if (action === 'add') showCustomPresetForm();
2235
- else if (action === 'import') importCustomPresets();
2236
- else if (action === 'export') exportCustomPresets();
2237
- });
2238
- });
2239
-
2240
- // Edit/delete custom preset
2241
- panel.querySelectorAll('.ss-preset-edit-btn').forEach(btn => {
2242
- btn.addEventListener('click', (e) => {
2243
- e.stopPropagation();
2244
- const id = btn.dataset.id;
2245
- const customs = loadCustomPresets();
2246
- const preset = customs.find(p => p.id === id);
2247
- if (preset) showCustomPresetForm(preset);
2248
- });
2249
- });
2250
- panel.querySelectorAll('.ss-preset-del-btn').forEach(btn => {
2251
- btn.addEventListener('click', (e) => {
2252
- e.stopPropagation();
2253
- const id = btn.dataset.id;
2254
- if (!confirm('Delete this preset?')) return;
2255
- const customs = loadCustomPresets().filter(p => p.id !== id);
2256
- saveCustomPresets(customs);
2257
- renderPresetPanel();
2258
- toast('Preset deleted', 'info');
2259
- });
2260
- });
2261
-
2262
- // Drag-and-drop on preset cards
2263
- panel.querySelectorAll('.ss-preset-card').forEach(card => {
2264
- const presetId = card.dataset.presetId;
2265
- const preset = findPresetById(presetId);
2266
- if (!preset) return;
2267
-
2268
- card.addEventListener('dragstart', (e) => {
2269
- e.dataTransfer.setData('text/plain', preset.snippet);
2270
- e.dataTransfer.setData('application/x-synabun-preset', JSON.stringify({
2271
- id: preset.id, name: preset.name, snippet: preset.snippet
2272
- }));
2273
- e.dataTransfer.effectAllowed = 'copy';
2274
- const ghost = document.createElement('div');
2275
- ghost.className = 'ss-preset-drag-ghost';
2276
- ghost.textContent = preset.name;
2277
- document.body.appendChild(ghost);
2278
- e.dataTransfer.setDragImage(ghost, 0, 0);
2279
- requestAnimationFrame(() => ghost.remove());
2280
- card.classList.add('dragging');
2281
- });
2282
- card.addEventListener('dragend', () => card.classList.remove('dragging'));
2283
-
2284
- // Click to insert
2285
- card.addEventListener('click', (e) => {
2286
- if (e.target.closest('.ss-preset-edit-btn, .ss-preset-del-btn')) return;
2287
- insertPresetSnippet(preset.snippet);
2288
- card.classList.add('ss-preset-inserted');
2289
- setTimeout(() => card.classList.remove('ss-preset-inserted'), 600);
2290
- });
2291
- });
2292
- }
2293
-
2294
- function findPresetById(id) {
2295
- const builtIn = BUILTIN_PRESETS.find(p => p.id === id);
2296
- if (builtIn) return builtIn;
2297
- return loadCustomPresets().find(p => p.id === id);
2298
- }
2299
-
2300
- function insertPresetSnippet(snippet) {
2301
- const editor = $('ss-wiz-content');
2302
- if (!editor) return;
2303
-
2304
- // Switch to raw mode if in preview
2305
- if (_wizPreviewMode) {
2306
- _panel?.querySelector('.ss-view-toggle-btn[data-mode="raw"]')?.click();
2307
- }
2308
-
2309
- const pos = editor.selectionStart;
2310
- const before = editor.value.substring(0, pos);
2311
- const after = editor.value.substring(editor.selectionEnd);
2312
- const pad = before.length > 0 && !before.endsWith('\n\n')
2313
- ? (before.endsWith('\n') ? '\n' : '\n\n') : '';
2314
- const trail = after.length > 0 && !after.startsWith('\n') ? '\n' : '';
2315
-
2316
- editor.value = before + pad + snippet + trail + after;
2317
- const newPos = before.length + pad.length + snippet.length;
2318
- editor.selectionStart = editor.selectionEnd = newPos;
2319
- editor.focus();
2320
- }
2321
-
2322
- function togglePresetPanel() {
2323
- const panel = $('ss-preset-panel');
2324
- const btn = $('ss-preset-toggle');
2325
- if (!panel || !btn) return;
2326
- const isOpen = !panel.classList.contains('hidden');
2327
- panel.classList.toggle('hidden', isOpen);
2328
- btn.classList.toggle('active', !isOpen);
2329
- storage.setItem(PRESETS_PANEL_KEY, !isOpen ? 'true' : 'false');
2330
- }
2331
-
2332
- // ── Custom preset CRUD ──
2333
-
2334
- function showCustomPresetForm(existing = null) {
2335
- const container = $('ss-preset-custom-form');
2336
- if (!container) return;
2337
- const editor = $('ss-wiz-content');
2338
- const selection = editor ? editor.value.substring(editor.selectionStart, editor.selectionEnd) : '';
2339
-
2340
- container.innerHTML = `
2341
- <div class="ss-preset-form">
2342
- <input class="ss-input ss-preset-form-name" id="ss-pf-name" placeholder="Preset name" value="${esc(existing?.name || '')}">
2343
- <input class="ss-input ss-preset-form-desc" id="ss-pf-desc" placeholder="Description (optional)" value="${esc(existing?.description || '')}">
2344
- <textarea class="ss-preset-form-snippet" id="ss-pf-snippet" placeholder="Snippet content">${esc(existing?.snippet || selection)}</textarea>
2345
- <div class="ss-preset-form-actions">
2346
- <button class="ss-header-btn" id="ss-pf-cancel">Cancel</button>
2347
- <button class="ss-header-btn ss-save-btn" id="ss-pf-save">${existing ? 'Update' : 'Save'}</button>
2348
- </div>
2349
- </div>
2350
- `;
2351
-
2352
- $('ss-pf-cancel')?.addEventListener('click', () => { container.innerHTML = ''; });
2353
- $('ss-pf-save')?.addEventListener('click', () => {
2354
- const name = $('ss-pf-name')?.value.trim();
2355
- const desc = $('ss-pf-desc')?.value.trim();
2356
- const snippet = $('ss-pf-snippet')?.value;
2357
- if (!name) { toast('Name is required', 'error'); return; }
2358
- if (!snippet) { toast('Snippet content is required', 'error'); return; }
2359
-
2360
- const customs = loadCustomPresets();
2361
- if (existing) {
2362
- const idx = customs.findIndex(p => p.id === existing.id);
2363
- if (idx >= 0) customs[idx] = { ...customs[idx], name, description: desc, snippet };
2364
- } else {
2365
- customs.push({
2366
- id: 'custom-' + Date.now(),
2367
- name, description: desc, snippet,
2368
- category: 'custom', types: [],
2369
- });
2370
- }
2371
- saveCustomPresets(customs);
2372
- renderPresetPanel();
2373
- toast(existing ? 'Preset updated' : 'Preset saved', 'info');
2374
- });
2375
-
2376
- // Scroll to form and focus
2377
- container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2378
- $('ss-pf-name')?.focus();
2379
- }
2380
-
2381
- // ── Import / Export ──
2382
-
2383
- function exportCustomPresets() {
2384
- const customs = loadCustomPresets();
2385
- if (customs.length === 0) { toast('No custom presets to export', 'error'); return; }
2386
- const bundle = {
2387
- format: 'synabun-presets-v1',
2388
- exportedAt: new Date().toISOString(),
2389
- presets: customs,
2390
- };
2391
- const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' });
2392
- const url = URL.createObjectURL(blob);
2393
- const a = document.createElement('a');
2394
- a.href = url;
2395
- a.download = `skill-presets-${Date.now()}.json`;
2396
- a.click();
2397
- URL.revokeObjectURL(url);
2398
- toast(`Exported ${customs.length} preset(s)`, 'info');
2399
- }
2400
-
2401
- function importCustomPresets() {
2402
- const input = document.createElement('input');
2403
- input.type = 'file';
2404
- input.accept = '.json';
2405
- input.addEventListener('change', async () => {
2406
- const file = input.files[0];
2407
- if (!file) return;
2408
- try {
2409
- const text = await file.text();
2410
- const bundle = JSON.parse(text);
2411
- if (bundle.format !== 'synabun-presets-v1' || !Array.isArray(bundle.presets)) {
2412
- toast('Invalid preset bundle format', 'error'); return;
2413
- }
2414
- const customs = loadCustomPresets();
2415
- const existingIds = new Set(customs.map(p => p.id));
2416
- let imported = 0, skipped = 0;
2417
- for (const p of bundle.presets) {
2418
- if (!p.name || !p.snippet) { skipped++; continue; }
2419
- if (existingIds.has(p.id)) { skipped++; continue; }
2420
- customs.push({ ...p, category: 'custom', id: p.id || ('custom-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6)) });
2421
- imported++;
2422
- }
2423
- saveCustomPresets(customs);
2424
- renderPresetPanel();
2425
- toast(`Imported ${imported} preset(s)${skipped > 0 ? `, skipped ${skipped}` : ''}`, 'info');
2426
- } catch (err) {
2427
- toast('Import failed: ' + err.message, 'error');
2428
- }
2429
- });
2430
- input.click();
2431
- }
2432
-
2433
- function renderWizard() {
2434
- const main = $('ss-main');
2435
- if (!main) return;
2436
-
2437
- const steps = ['Type', 'Template', 'Details', 'Content'];
2438
- const d = _wizardData;
2439
-
2440
- main.innerHTML = `
2441
- <div class="ss-wizard">
2442
- <div class="ss-wizard-steps">
2443
- ${steps.map((s, i) => `<div class="ss-wizard-step${i === _wizardStep ? ' active' : ''}${i < _wizardStep ? ' done' : ''}">${i + 1}. ${s}</div>`).join('')}
2444
- </div>
2445
- <div class="ss-wizard-content" id="ss-wizard-content"></div>
2446
- <div class="ss-wizard-nav">
2447
- ${_wizardStep > 0 ? '<button class="ss-header-btn" id="ss-wiz-prev">\u2190 Back</button>' : '<span></span>'}
2448
- ${_wizardStep < 3 ? '<button class="ss-header-btn ss-save-btn" id="ss-wiz-next">Next \u2192</button>' : '<button class="ss-header-btn ss-save-btn" id="ss-wiz-create">Create</button>'}
2449
- </div>
2450
- </div>
2451
- `;
2452
-
2453
- renderWizardStep();
2454
-
2455
- $('ss-wiz-prev')?.addEventListener('click', () => { collectWizardData(); _wizardStep--; renderWizard(); });
2456
- $('ss-wiz-next')?.addEventListener('click', () => {
2457
- if (collectWizardData()) { _wizardStep++; renderWizard(); }
2458
- });
2459
- $('ss-wiz-create')?.addEventListener('click', () => {
2460
- if (collectWizardData()) createFromWizard();
2461
- });
2462
- }
2463
-
2464
- function renderWizardStep() {
2465
- const container = $('ss-wizard-content');
2466
- if (!container) return;
2467
- const d = _wizardData;
2468
-
2469
- if (_wizardStep === 0) {
2470
- // Type selection
2471
- container.innerHTML = `
2472
- <h3>What do you want to create?</h3>
2473
- <div class="ss-type-cards">
2474
- <div class="ss-type-card${d.type === 'skill' ? ' selected' : ''}" data-type="skill">
2475
- <div class="ss-type-card-icon skill"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/><circle cx="12" cy="12" r="4"/></svg></div>
2476
- <div class="ss-type-card-name">Skill</div>
2477
- <div class="ss-type-card-desc">Auto-detected by Claude or invoked via /name. Supports sub-files for references and examples.</div>
2478
- </div>
2479
- <div class="ss-type-card${d.type === 'command' ? ' selected' : ''}" data-type="command">
2480
- <div class="ss-type-card-icon command"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg></div>
2481
- <div class="ss-type-card-name">Command</div>
2482
- <div class="ss-type-card-desc">User-invoked via /name. Simple single-file format. Project-scoped.</div>
2483
- </div>
2484
- <div class="ss-type-card${d.type === 'agent' ? ' selected' : ''}" data-type="agent">
2485
- <div class="ss-type-card-icon agent"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="12" rx="3"/><line x1="9" y1="9" x2="9" y2="9.01" stroke-width="2"/><line x1="15" y1="9" x2="15" y2="9.01" stroke-width="2"/><path d="M8 20v-4M16 20v-4"/><path d="M1 10h3M20 10h3"/></svg></div>
2486
- <div class="ss-type-card-name">Agent</div>
2487
- <div class="ss-type-card-desc">Specialized subagent delegated via the Task tool. Runs in its own context window.</div>
2488
- </div>
2489
- </div>
2490
- `;
2491
- container.querySelectorAll('.ss-type-card').forEach(card => {
2492
- card.addEventListener('click', () => {
2493
- d.type = card.dataset.type;
2494
- d.template = 'blank';
2495
- container.querySelectorAll('.ss-type-card').forEach(c => c.classList.toggle('selected', c === card));
2496
- });
2497
- });
2498
- } else if (_wizardStep === 1) {
2499
- // Template selection
2500
- const templates = d.type === 'skill' ? SKILL_TEMPLATES : d.type === 'command' ? COMMAND_TEMPLATES : AGENT_TEMPLATES;
2501
- container.innerHTML = `
2502
- <h3>Choose a template</h3>
2503
- <div class="ss-template-list">
2504
- ${Object.entries(templates).map(([key, t]) => `
2505
- <div class="ss-template-item${d.template === key ? ' selected' : ''}" data-tpl="${key}">
2506
- <div class="ss-template-name">${t.label}</div>
2507
- <div class="ss-template-desc">${t.desc}</div>
2508
- </div>
2509
- `).join('')}
2510
- </div>
2511
- `;
2512
- container.querySelectorAll('.ss-template-item').forEach(item => {
2513
- item.addEventListener('click', () => {
2514
- d.template = item.dataset.tpl;
2515
- container.querySelectorAll('.ss-template-item').forEach(i => i.classList.toggle('selected', i === item));
2516
- });
2517
- });
2518
- } else if (_wizardStep === 2) {
2519
- // Details
2520
- container.innerHTML = `
2521
- <h3>Details</h3>
2522
- <div class="ss-field">
2523
- <label>Name <span class="ss-field-hint">Becomes /${d.type === 'skill' ? 'skill-name' : 'command-name'}</span></label>
2524
- <input type="text" class="ss-input" id="ss-wiz-name" value="${esc(d.name)}" placeholder="my-${d.type}" pattern="[a-z][a-z0-9-.]*">
2525
- <div class="ss-wiz-name-preview" id="ss-wiz-name-preview">/${esc(d.name || `my-${d.type}`)}</div>
2526
- </div>
2527
- <div class="ss-field">
2528
- <label>Scope</label>
2529
- <div class="ss-radio-group">
2530
- <label class="ss-radio"><input type="radio" name="wiz-scope" value="global" ${d.scope === 'global' ? 'checked' : ''}> Global (~/.claude/)</label>
2531
- <label class="ss-radio"><input type="radio" name="wiz-scope" value="project" ${d.scope === 'project' ? 'checked' : ''}> Project</label>
2532
- </div>
2533
- </div>
2534
- <div class="ss-field ${d.scope !== 'project' ? 'ss-hidden' : ''}" id="ss-wiz-project-field">
2535
- <label>Project</label>
2536
- <select class="ss-input" id="ss-wiz-project">
2537
- ${_projects.map(p => `<option value="${esc(p.path)}" ${d.projectPath === p.path ? 'selected' : ''}>${esc(p.label)}</option>`).join('')}
2538
- </select>
2539
- </div>
2540
- <div class="ss-field">
2541
- <label>Description</label>
2542
- <textarea class="ss-textarea ss-textarea-sm" id="ss-wiz-desc" placeholder="What triggers this ${d.type}...">${esc(d.description)}</textarea>
2543
- </div>
2544
- `;
2545
- $('ss-wiz-name')?.addEventListener('input', (e) => {
2546
- d.name = e.target.value;
2547
- const preview = $('ss-wiz-name-preview');
2548
- if (preview) preview.textContent = '/' + (d.name || `my-${d.type}`);
2549
- });
2550
- _panel.querySelectorAll('input[name="wiz-scope"]').forEach(r => {
2551
- r.addEventListener('change', () => {
2552
- d.scope = r.value;
2553
- const projField = $('ss-wiz-project-field');
2554
- if (projField) projField.classList.toggle('ss-hidden', d.scope !== 'project');
2555
- });
2556
- });
2557
- } else if (_wizardStep === 3) {
2558
- // Content editor with preset side panel
2559
- const templates = d.type === 'skill' ? SKILL_TEMPLATES : d.type === 'command' ? COMMAND_TEMPLATES : AGENT_TEMPLATES;
2560
- const tpl = templates[d.template] || templates.blank;
2561
- const content = d.rawContent || tpl.gen(d.name || `my-${d.type}`);
2562
- const panelOpen = isPresetPanelOpen();
2563
- container.innerHTML = `
2564
- <div class="ss-wiz-step4-hdr">
2565
- <h3>Review & Edit</h3>
2566
- <button class="ss-preset-toggle${panelOpen ? ' active' : ''}" id="ss-preset-toggle">Presets</button>
2567
- </div>
2568
- <p class="ss-wizard-hint">Edit the generated content below. Click or drag presets to insert snippets.</p>
2569
- <div class="ss-wiz-step4-body">
2570
- <div class="ss-wiz-editor-wrap">
2571
- <div class="ss-md-toolbar" id="ss-wiz-toolbar">
2572
- <button class="ss-md-tool" data-cmd="heading" data-tooltip="Heading (Ctrl+H)"><svg viewBox="0 0 24 24"><path d="M4 4v16M20 4v16M4 12h16"/></svg></button>
2573
- <button class="ss-md-tool" data-cmd="bold" data-tooltip="Bold (Ctrl+B)"><b style="font-size:13px">B</b></button>
2574
- <button class="ss-md-tool" data-cmd="italic" data-tooltip="Italic (Ctrl+I)"><i style="font-size:13px">I</i></button>
2575
- <button class="ss-md-tool" data-cmd="strikethrough" data-tooltip="Strikethrough"><s style="font-size:12px">S</s></button>
2576
- <div class="ss-md-toolbar-sep"></div>
2577
- <button class="ss-md-tool" data-cmd="ul" data-tooltip="Bullet list"><svg viewBox="0 0 24 24"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="5" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="5" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg></button>
2578
- <button class="ss-md-tool" data-cmd="ol" data-tooltip="Numbered list"><svg viewBox="0 0 24 24"><line x1="10" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/><line x1="10" y1="18" x2="20" y2="18"/><text x="4" y="8" font-size="7" fill="currentColor" stroke="none" font-weight="600">1</text><text x="4" y="14" font-size="7" fill="currentColor" stroke="none" font-weight="600">2</text><text x="4" y="20" font-size="7" fill="currentColor" stroke="none" font-weight="600">3</text></svg></button>
2579
- <button class="ss-md-tool" data-cmd="quote" data-tooltip="Blockquote"><svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h12"/><rect x="1" y="5" width="2" height="14" rx="1" fill="currentColor" stroke="none" opacity="0.4"/></svg></button>
2580
- <div class="ss-md-toolbar-sep"></div>
2581
- <button class="ss-md-tool" data-cmd="code" data-tooltip="Inline code"><svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></button>
2582
- <button class="ss-md-tool" data-cmd="codeblock" data-tooltip="Code block"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><polyline points="9 8 5 12 9 16" stroke-width="1.5"/><polyline points="15 8 19 12 15 16" stroke-width="1.5"/></svg></button>
2583
- <button class="ss-md-tool" data-cmd="link" data-tooltip="Link (Ctrl+K)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
2584
- <button class="ss-md-tool" data-cmd="hr" data-tooltip="Horizontal rule">&mdash;</button>
2585
- <div class="ss-view-toggle">
2586
- <button class="ss-view-toggle-btn${_wizPreviewMode ? '' : ' active'}" data-mode="raw">Raw</button>
2587
- <button class="ss-view-toggle-btn${_wizPreviewMode ? ' active' : ''}" data-mode="preview">Preview</button>
2588
- </div>
2589
- </div>
2590
- <textarea class="ss-md-editor ss-wizard-editor${_wizPreviewMode ? ' ss-hidden' : ''}" id="ss-wiz-content" spellcheck="false" autocorrect="off" autocapitalize="off">${esc(content)}</textarea>
2591
- <div class="ss-md-preview ss-wizard-preview${_wizPreviewMode ? ' visible' : ''}" id="ss-wiz-preview">${_wizPreviewMode ? renderMarkdown(content) : ''}</div>
2592
- </div>
2593
- <div class="ss-preset-panel${panelOpen ? '' : ' hidden'}" id="ss-preset-panel"></div>
2594
- </div>
2595
- `;
2596
- wireWizardEditor();
2597
- renderPresetPanel();
2598
- $('ss-preset-toggle')?.addEventListener('click', togglePresetPanel);
2599
- }
46
+ if (isGuest() && !hasPermission('skills')) return Promise.resolve();
47
+ return studio().then((m) => m.prefetchStudio()).catch(() => {});
2600
48
  }
2601
49
 
2602
- function wireWizardEditor() {
2603
- const edId = 'ss-wiz-content';
2604
- const editor = $(edId);
2605
- const toolbar = $('ss-wiz-toolbar');
2606
- if (!editor || !toolbar) return;
2607
-
2608
- // Helper: get editor element (for md* functions)
2609
- const getEd = () => $(edId);
2610
-
2611
- // Tab key support (indentation)
2612
- editor.addEventListener('keydown', (e) => {
2613
- if (e.key === 'Tab') {
2614
- e.preventDefault();
2615
- const start = editor.selectionStart;
2616
- const end = editor.selectionEnd;
2617
- editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
2618
- editor.selectionStart = editor.selectionEnd = start + 2;
2619
- }
2620
- });
2621
-
2622
- // Toolbar click handlers
2623
- toolbar.querySelectorAll('.ss-md-tool[data-cmd]').forEach(btn => {
2624
- btn.addEventListener('click', () => {
2625
- const ed = getEd();
2626
- if (!ed) return;
2627
- const cmd = btn.dataset.cmd;
2628
- switch (cmd) {
2629
- case 'bold': wizMdWrap(ed, '**', '**'); break;
2630
- case 'italic': wizMdWrap(ed, '*', '*'); break;
2631
- case 'strikethrough': wizMdWrap(ed, '~~', '~~'); break;
2632
- case 'code': wizMdWrap(ed, '`', '`'); break;
2633
- case 'codeblock': wizMdWrapBlock(ed, '```\n', '\n```'); break;
2634
- case 'heading': wizMdPrefix(ed, '## '); break;
2635
- case 'ul': wizMdPrefix(ed, '- '); break;
2636
- case 'ol': wizMdPrefix(ed, '1. '); break;
2637
- case 'quote': wizMdPrefix(ed, '> '); break;
2638
- case 'link': wizMdLink(ed); break;
2639
- case 'hr': wizMdInsert(ed, '\n---\n'); break;
2640
- }
2641
- });
2642
- });
2643
-
2644
- // Ctrl+B/I/K/H shortcuts
2645
- editor.addEventListener('keydown', (e) => {
2646
- if (e.ctrlKey || e.metaKey) {
2647
- const ed = getEd();
2648
- if (!ed) return;
2649
- if (e.key === 'b') { e.preventDefault(); wizMdWrap(ed, '**', '**'); }
2650
- else if (e.key === 'i') { e.preventDefault(); wizMdWrap(ed, '*', '*'); }
2651
- else if (e.key === 'k') { e.preventDefault(); wizMdLink(ed); }
2652
- else if (e.key === 'h') { e.preventDefault(); wizMdPrefix(ed, '## '); }
2653
- }
2654
- });
2655
-
2656
- // Preview/Raw toggle
2657
- toolbar.querySelectorAll('.ss-view-toggle-btn').forEach(btn => {
2658
- btn.addEventListener('click', () => {
2659
- const mode = btn.dataset.mode;
2660
- _wizPreviewMode = mode === 'preview';
2661
- toolbar.querySelectorAll('.ss-view-toggle-btn').forEach(b => b.classList.toggle('active', b === btn));
2662
- const ed = getEd();
2663
- const prev = $('ss-wiz-preview');
2664
- if (ed) ed.classList.toggle('ss-hidden', _wizPreviewMode);
2665
- if (prev) {
2666
- prev.classList.toggle('visible', _wizPreviewMode);
2667
- if (_wizPreviewMode) {
2668
- prev.innerHTML = renderMarkdown(ed?.value || '');
2669
- }
2670
- }
2671
- // Hide formatting tools in preview mode
2672
- toolbar.querySelectorAll('.ss-md-tool').forEach(t => t.style.visibility = _wizPreviewMode ? 'hidden' : '');
2673
- });
2674
- });
2675
-
2676
- // Drag-and-drop target on textarea
2677
- editor.addEventListener('dragover', (e) => {
2678
- if (e.dataTransfer.types.includes('application/x-synabun-preset') ||
2679
- e.dataTransfer.types.includes('text/plain')) {
2680
- e.preventDefault();
2681
- e.dataTransfer.dropEffect = 'copy';
2682
- editor.classList.add('ss-drop-target');
2683
- }
2684
- });
2685
- editor.addEventListener('dragleave', (e) => {
2686
- if (!editor.contains(e.relatedTarget)) {
2687
- editor.classList.remove('ss-drop-target');
2688
- }
2689
- });
2690
- editor.addEventListener('drop', (e) => {
2691
- e.preventDefault();
2692
- editor.classList.remove('ss-drop-target');
2693
- const presetData = e.dataTransfer.getData('application/x-synabun-preset');
2694
- const snippet = presetData
2695
- ? JSON.parse(presetData).snippet
2696
- : e.dataTransfer.getData('text/plain');
2697
- if (!snippet) return;
2698
- insertPresetSnippet(snippet);
2699
- });
2700
- }
2701
-
2702
- // ── Wizard markdown helpers (operate on passed editor element) ──
2703
-
2704
- function wizMdWrap(ed, before, after) {
2705
- const start = ed.selectionStart, end = ed.selectionEnd;
2706
- const sel = ed.value.substring(start, end);
2707
- const text = sel || 'text';
2708
- const replacement = before + text + after;
2709
- ed.setRangeText(replacement, start, end, 'select');
2710
- ed.selectionStart = start + before.length;
2711
- ed.selectionEnd = start + before.length + text.length;
2712
- ed.focus();
2713
- }
2714
-
2715
- function wizMdWrapBlock(ed, before, after) {
2716
- const start = ed.selectionStart, end = ed.selectionEnd;
2717
- const sel = ed.value.substring(start, end);
2718
- const text = sel || 'code here';
2719
- const needNewlineBefore = start > 0 && ed.value[start - 1] !== '\n' ? '\n' : '';
2720
- const needNewlineAfter = end < ed.value.length && ed.value[end] !== '\n' ? '\n' : '';
2721
- const replacement = needNewlineBefore + before + text + after + needNewlineAfter;
2722
- ed.setRangeText(replacement, start, end, 'select');
2723
- ed.focus();
2724
- }
2725
-
2726
- function wizMdPrefix(ed, prefix) {
2727
- const start = ed.selectionStart, end = ed.selectionEnd;
2728
- const before = ed.value.substring(0, start);
2729
- const lineStart = before.lastIndexOf('\n') + 1;
2730
- const sel = ed.value.substring(lineStart, end);
2731
- const lines = sel.split('\n');
2732
- const prefixed = lines.map(line => {
2733
- if (line.startsWith(prefix)) return line.substring(prefix.length);
2734
- return prefix + line.replace(/^(#{1,6}\s|[-*]\s|\d+\.\s|>\s)/, '');
2735
- }).join('\n');
2736
- ed.setRangeText(prefixed, lineStart, end, 'select');
2737
- ed.focus();
2738
- }
2739
-
2740
- function wizMdLink(ed) {
2741
- const start = ed.selectionStart, end = ed.selectionEnd;
2742
- const sel = ed.value.substring(start, end);
2743
- const text = sel || 'link text';
2744
- const replacement = `[${text}](url)`;
2745
- ed.setRangeText(replacement, start, end, 'select');
2746
- ed.selectionStart = start + text.length + 3;
2747
- ed.selectionEnd = start + text.length + 6;
2748
- ed.focus();
2749
- }
2750
-
2751
- function wizMdInsert(ed, text) {
2752
- const pos = ed.selectionStart;
2753
- ed.setRangeText(text, pos, ed.selectionEnd, 'end');
2754
- ed.focus();
2755
- }
2756
-
2757
- function collectWizardData() {
2758
- const d = _wizardData;
2759
- if (_wizardStep === 2) {
2760
- const nameEl = $('ss-wiz-name');
2761
- const descEl = $('ss-wiz-desc');
2762
- const projEl = $('ss-wiz-project');
2763
- if (nameEl) d.name = nameEl.value.trim();
2764
- if (descEl) d.description = descEl.value.trim();
2765
- if (projEl) d.projectPath = projEl.value;
2766
- if (!d.name) { toast('Name is required', 'error'); return false; }
2767
- if (!/^[a-z][a-z0-9-.]*$/.test(d.name)) { toast('Name must be lowercase, start with a letter', 'error'); return false; }
2768
- }
2769
- if (_wizardStep === 3) {
2770
- const contentEl = $('ss-wiz-content');
2771
- if (contentEl) d.rawContent = contentEl.value;
2772
- }
2773
- return true;
2774
- }
2775
-
2776
- async function createFromWizard() {
2777
- const d = _wizardData;
2778
- const contentEl = $('ss-wiz-content');
2779
- if (contentEl) d.rawContent = contentEl.value;
2780
- if (!d.rawContent) { toast('Content is required', 'error'); return; }
2781
-
2782
- try {
2783
- const result = await createSkillsArtifact({
2784
- type: d.type,
2785
- scope: d.scope,
2786
- projectPath: d.projectPath,
2787
- name: d.name,
2788
- rawContent: d.rawContent,
2789
- });
2790
- toast(`Created "${d.name}"!`, 'info');
2791
- await loadLibrary();
2792
- // Find the newly created artifact and open it
2793
- const newArtifact = _library.find(a => a.name === d.name || a.dirName === d.name);
2794
- if (newArtifact) {
2795
- switchToEditor(newArtifact);
2796
- } else {
2797
- switchToLibrary();
2798
- }
2799
- } catch (err) {
2800
- toast('Create failed: ' + err.message, 'error');
2801
- }
2802
- }
2803
-
2804
- // ═══════════════════════════════════════════
2805
- // IMPORT
2806
- // ═══════════════════════════════════════════
2807
-
2808
- // What the import asks when the bundle names files that are already there.
2809
- function importOverwriteQuestion(existing) {
2810
- const names = existing.slice(0, 12);
2811
- const more = existing.length - names.length;
2812
- const what = existing.length === 1 ? 'a file that already exists' : `${existing.length} files that already exist`;
2813
- return `This import would replace ${what}:\n\n${names.map(n => ` ${n}`).join('\n')}${more > 0 ? `\n and ${more} more` : ''}\n\nReplace ${existing.length === 1 ? 'it' : 'them'}?`;
2814
- }
2815
-
2816
- function triggerImport() {
2817
- const input = document.createElement('input');
2818
- input.type = 'file';
2819
- input.accept = '.json,.skill.json,.command.json,.agent.json';
2820
- input.addEventListener('change', async () => {
2821
- const file = input.files[0];
2822
- if (!file) return;
2823
- try {
2824
- const text = await file.text();
2825
- const bundle = JSON.parse(text);
2826
- if (bundle.format !== 'synabun-skill-bundle') {
2827
- toast('Invalid bundle format', 'error');
2828
- return;
2829
- }
2830
- // Ask for scope
2831
- const scope = prompt('Import scope (global or project):', 'global');
2832
- if (!scope) return;
2833
- let projectPath = '';
2834
- if (scope === 'project' && _projects.length > 0) {
2835
- projectPath = _projects[0].path; // default to first project
2836
- }
2837
- // A bundle that would replace files which are already there is refused by
2838
- // the server with their names: ask, and only a yes sends it again as confirmed.
2839
- const result = await importSkillsBundleAsking(bundle, scope, projectPath, (existing) => confirm(importOverwriteQuestion(existing)));
2840
- if (!result) { toast('Import cancelled. Nothing was replaced.', 'info'); return; }
2841
- toast(`Imported "${result.name}"!`, 'info');
2842
- await loadLibrary();
2843
- renderLibrary();
2844
- } catch (err) {
2845
- toast('Import failed: ' + err.message, 'error');
2846
- }
2847
- });
2848
- input.click();
50
+ function failed(err) {
51
+ console.error('Skills Studio could not be loaded', err);
2849
52
  }