@hera-al/server 1.6.1

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 (348) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/bundled/apple-notes/SKILL.md +77 -0
  4. package/bundled/apple-reminders/SKILL.md +96 -0
  5. package/bundled/blogwatcher/SKILL.md +69 -0
  6. package/bundled/camsnap/SKILL.md +45 -0
  7. package/bundled/discord/SKILL.md +578 -0
  8. package/bundled/gemini/SKILL.md +43 -0
  9. package/bundled/gifgrep/SKILL.md +79 -0
  10. package/bundled/github/SKILL.md +77 -0
  11. package/bundled/gog/SKILL.md +116 -0
  12. package/bundled/goplaces/SKILL.md +52 -0
  13. package/bundled/himalaya/SKILL.md +257 -0
  14. package/bundled/himalaya/references/configuration.md +184 -0
  15. package/bundled/himalaya/references/message-composition.md +199 -0
  16. package/bundled/homebrew/SKILL.md +82 -0
  17. package/bundled/local-places/SERVER_README.md +101 -0
  18. package/bundled/local-places/SKILL.md +102 -0
  19. package/bundled/local-places/pyproject.toml +21 -0
  20. package/bundled/local-places/src/local_places/__init__.py +2 -0
  21. package/bundled/local-places/src/local_places/google_places.py +314 -0
  22. package/bundled/local-places/src/local_places/main.py +65 -0
  23. package/bundled/local-places/src/local_places/schemas.py +107 -0
  24. package/bundled/markitdown/SKILL.md +96 -0
  25. package/bundled/mcporter/SKILL.md +61 -0
  26. package/bundled/merge-pr/SKILL.md +187 -0
  27. package/bundled/merge-pr/agents/openai.yaml +4 -0
  28. package/bundled/nano-banana-pro/SKILL.md +58 -0
  29. package/bundled/nano-banana-pro/scripts/generate_image.py +184 -0
  30. package/bundled/nano-pdf/SKILL.md +38 -0
  31. package/bundled/open-prose/README.md +25 -0
  32. package/bundled/open-prose/index.ts +5 -0
  33. package/bundled/open-prose/openclaw.plugin.json +11 -0
  34. package/bundled/open-prose/package.json +15 -0
  35. package/bundled/open-prose/skills/prose/LICENSE +21 -0
  36. package/bundled/open-prose/skills/prose/SKILL.md +323 -0
  37. package/bundled/open-prose/skills/prose/alt-borges.md +141 -0
  38. package/bundled/open-prose/skills/prose/alts/arabian-nights.md +358 -0
  39. package/bundled/open-prose/skills/prose/alts/borges.md +360 -0
  40. package/bundled/open-prose/skills/prose/alts/folk.md +322 -0
  41. package/bundled/open-prose/skills/prose/alts/homer.md +346 -0
  42. package/bundled/open-prose/skills/prose/alts/kafka.md +373 -0
  43. package/bundled/open-prose/skills/prose/compiler.md +2971 -0
  44. package/bundled/open-prose/skills/prose/examples/01-hello-world.prose +4 -0
  45. package/bundled/open-prose/skills/prose/examples/02-research-and-summarize.prose +6 -0
  46. package/bundled/open-prose/skills/prose/examples/03-code-review.prose +17 -0
  47. package/bundled/open-prose/skills/prose/examples/04-write-and-refine.prose +14 -0
  48. package/bundled/open-prose/skills/prose/examples/05-debug-issue.prose +20 -0
  49. package/bundled/open-prose/skills/prose/examples/06-explain-codebase.prose +17 -0
  50. package/bundled/open-prose/skills/prose/examples/07-refactor.prose +20 -0
  51. package/bundled/open-prose/skills/prose/examples/08-blog-post.prose +20 -0
  52. package/bundled/open-prose/skills/prose/examples/09-research-with-agents.prose +25 -0
  53. package/bundled/open-prose/skills/prose/examples/10-code-review-agents.prose +32 -0
  54. package/bundled/open-prose/skills/prose/examples/11-skills-and-imports.prose +27 -0
  55. package/bundled/open-prose/skills/prose/examples/12-secure-agent-permissions.prose +43 -0
  56. package/bundled/open-prose/skills/prose/examples/13-variables-and-context.prose +51 -0
  57. package/bundled/open-prose/skills/prose/examples/14-composition-blocks.prose +48 -0
  58. package/bundled/open-prose/skills/prose/examples/15-inline-sequences.prose +23 -0
  59. package/bundled/open-prose/skills/prose/examples/16-parallel-reviews.prose +19 -0
  60. package/bundled/open-prose/skills/prose/examples/17-parallel-research.prose +19 -0
  61. package/bundled/open-prose/skills/prose/examples/18-mixed-parallel-sequential.prose +36 -0
  62. package/bundled/open-prose/skills/prose/examples/19-advanced-parallel.prose +71 -0
  63. package/bundled/open-prose/skills/prose/examples/20-fixed-loops.prose +20 -0
  64. package/bundled/open-prose/skills/prose/examples/21-pipeline-operations.prose +35 -0
  65. package/bundled/open-prose/skills/prose/examples/22-error-handling.prose +51 -0
  66. package/bundled/open-prose/skills/prose/examples/23-retry-with-backoff.prose +63 -0
  67. package/bundled/open-prose/skills/prose/examples/24-choice-blocks.prose +86 -0
  68. package/bundled/open-prose/skills/prose/examples/25-conditionals.prose +114 -0
  69. package/bundled/open-prose/skills/prose/examples/26-parameterized-blocks.prose +100 -0
  70. package/bundled/open-prose/skills/prose/examples/27-string-interpolation.prose +105 -0
  71. package/bundled/open-prose/skills/prose/examples/28-automated-pr-review.prose +37 -0
  72. package/bundled/open-prose/skills/prose/examples/28-gas-town.prose +1572 -0
  73. package/bundled/open-prose/skills/prose/examples/29-captains-chair.prose +218 -0
  74. package/bundled/open-prose/skills/prose/examples/30-captains-chair-simple.prose +42 -0
  75. package/bundled/open-prose/skills/prose/examples/31-captains-chair-with-memory.prose +145 -0
  76. package/bundled/open-prose/skills/prose/examples/33-pr-review-autofix.prose +168 -0
  77. package/bundled/open-prose/skills/prose/examples/34-content-pipeline.prose +204 -0
  78. package/bundled/open-prose/skills/prose/examples/35-feature-factory.prose +296 -0
  79. package/bundled/open-prose/skills/prose/examples/36-bug-hunter.prose +237 -0
  80. package/bundled/open-prose/skills/prose/examples/37-the-forge.prose +1474 -0
  81. package/bundled/open-prose/skills/prose/examples/38-skill-scan.prose +455 -0
  82. package/bundled/open-prose/skills/prose/examples/39-architect-by-simulation.prose +277 -0
  83. package/bundled/open-prose/skills/prose/examples/40-rlm-self-refine.prose +32 -0
  84. package/bundled/open-prose/skills/prose/examples/41-rlm-divide-conquer.prose +38 -0
  85. package/bundled/open-prose/skills/prose/examples/42-rlm-filter-recurse.prose +46 -0
  86. package/bundled/open-prose/skills/prose/examples/43-rlm-pairwise.prose +50 -0
  87. package/bundled/open-prose/skills/prose/examples/44-run-endpoint-ux-test.prose +261 -0
  88. package/bundled/open-prose/skills/prose/examples/45-plugin-release.prose +159 -0
  89. package/bundled/open-prose/skills/prose/examples/45-run-endpoint-ux-test-with-remediation.prose +637 -0
  90. package/bundled/open-prose/skills/prose/examples/46-run-endpoint-ux-test-fast.prose +148 -0
  91. package/bundled/open-prose/skills/prose/examples/46-workflow-crystallizer.prose +225 -0
  92. package/bundled/open-prose/skills/prose/examples/47-language-self-improvement.prose +356 -0
  93. package/bundled/open-prose/skills/prose/examples/48-habit-miner.prose +445 -0
  94. package/bundled/open-prose/skills/prose/examples/49-prose-run-retrospective.prose +210 -0
  95. package/bundled/open-prose/skills/prose/examples/README.md +391 -0
  96. package/bundled/open-prose/skills/prose/examples/roadmap/README.md +22 -0
  97. package/bundled/open-prose/skills/prose/examples/roadmap/iterative-refinement.prose +20 -0
  98. package/bundled/open-prose/skills/prose/examples/roadmap/parallel-review.prose +18 -0
  99. package/bundled/open-prose/skills/prose/examples/roadmap/simple-pipeline.prose +17 -0
  100. package/bundled/open-prose/skills/prose/examples/roadmap/syntax/open-prose-syntax.prose +223 -0
  101. package/bundled/open-prose/skills/prose/guidance/antipatterns.md +951 -0
  102. package/bundled/open-prose/skills/prose/guidance/patterns.md +700 -0
  103. package/bundled/open-prose/skills/prose/guidance/system-prompt.md +180 -0
  104. package/bundled/open-prose/skills/prose/help.md +144 -0
  105. package/bundled/open-prose/skills/prose/lib/README.md +108 -0
  106. package/bundled/open-prose/skills/prose/lib/calibrator.prose +215 -0
  107. package/bundled/open-prose/skills/prose/lib/cost-analyzer.prose +174 -0
  108. package/bundled/open-prose/skills/prose/lib/error-forensics.prose +250 -0
  109. package/bundled/open-prose/skills/prose/lib/inspector.prose +196 -0
  110. package/bundled/open-prose/skills/prose/lib/profiler.prose +460 -0
  111. package/bundled/open-prose/skills/prose/lib/program-improver.prose +275 -0
  112. package/bundled/open-prose/skills/prose/lib/project-memory.prose +118 -0
  113. package/bundled/open-prose/skills/prose/lib/user-memory.prose +93 -0
  114. package/bundled/open-prose/skills/prose/lib/vm-improver.prose +243 -0
  115. package/bundled/open-prose/skills/prose/primitives/session.md +593 -0
  116. package/bundled/open-prose/skills/prose/prose.md +1237 -0
  117. package/bundled/open-prose/skills/prose/state/filesystem.md +498 -0
  118. package/bundled/open-prose/skills/prose/state/in-context.md +384 -0
  119. package/bundled/open-prose/skills/prose/state/postgres.md +880 -0
  120. package/bundled/open-prose/skills/prose/state/sqlite.md +574 -0
  121. package/bundled/peekaboo/SKILL.md +190 -0
  122. package/bundled/prepare-pr/SKILL.md +277 -0
  123. package/bundled/prepare-pr/agents/openai.yaml +4 -0
  124. package/bundled/review-pr/SKILL.md +228 -0
  125. package/bundled/review-pr/agents/openai.yaml +4 -0
  126. package/bundled/sag/SKILL.md +87 -0
  127. package/bundled/skill-creator/SKILL.md +370 -0
  128. package/bundled/skill-creator/license.txt +202 -0
  129. package/bundled/skill-creator/scripts/init_skill.py +378 -0
  130. package/bundled/skill-creator/scripts/package_skill.py +111 -0
  131. package/bundled/skill-creator/scripts/quick_validate.py +101 -0
  132. package/bundled/spotify-player/SKILL.md +64 -0
  133. package/bundled/ssh/SKILL.md +119 -0
  134. package/bundled/summarize/SKILL.md +87 -0
  135. package/bundled/video-frames/SKILL.md +46 -0
  136. package/bundled/video-frames/scripts/frame.sh +81 -0
  137. package/bundled/voice-call/SKILL.md +45 -0
  138. package/bundled/wacli/SKILL.md +72 -0
  139. package/bundled/weather/SKILL.md +54 -0
  140. package/dist/agent/agent-service.d.ts +88 -0
  141. package/dist/agent/agent-service.js +1 -0
  142. package/dist/agent/message-queue.d.ts +24 -0
  143. package/dist/agent/message-queue.js +1 -0
  144. package/dist/agent/prompt-builder.d.ts +58 -0
  145. package/dist/agent/prompt-builder.js +1 -0
  146. package/dist/agent/session-agent.d.ts +197 -0
  147. package/dist/agent/session-agent.js +1 -0
  148. package/dist/agent/session-db.d.ts +26 -0
  149. package/dist/agent/session-db.js +1 -0
  150. package/dist/agent/session-error-handler.d.ts +37 -0
  151. package/dist/agent/session-error-handler.js +1 -0
  152. package/dist/agent/session-manager.d.ts +19 -0
  153. package/dist/agent/session-manager.js +1 -0
  154. package/dist/agent/workspace-files.d.ts +51 -0
  155. package/dist/agent/workspace-files.js +1 -0
  156. package/dist/auth/auth-middleware.d.ts +9 -0
  157. package/dist/auth/auth-middleware.js +1 -0
  158. package/dist/auth/node-signature-db.d.ts +30 -0
  159. package/dist/auth/node-signature-db.js +1 -0
  160. package/dist/auth/token-db.d.ts +38 -0
  161. package/dist/auth/token-db.js +1 -0
  162. package/dist/browser/browser-service.d.ts +9 -0
  163. package/dist/browser/browser-service.js +1 -0
  164. package/dist/channels/channel.d.ts +2 -0
  165. package/dist/channels/channel.js +1 -0
  166. package/dist/channels/responses.d.ts +21 -0
  167. package/dist/channels/responses.js +1 -0
  168. package/dist/commands/clear.d.ts +7 -0
  169. package/dist/commands/clear.js +1 -0
  170. package/dist/commands/cmd.d.ts +7 -0
  171. package/dist/commands/cmd.js +1 -0
  172. package/dist/commands/coder.d.ts +12 -0
  173. package/dist/commands/coder.js +1 -0
  174. package/dist/commands/command-registry.d.ts +12 -0
  175. package/dist/commands/command-registry.js +1 -0
  176. package/dist/commands/command.d.ts +22 -0
  177. package/dist/commands/command.js +1 -0
  178. package/dist/commands/compact.d.ts +7 -0
  179. package/dist/commands/compact.js +1 -0
  180. package/dist/commands/customsubagents.d.ts +15 -0
  181. package/dist/commands/customsubagents.js +1 -0
  182. package/dist/commands/help.d.ts +9 -0
  183. package/dist/commands/help.js +1 -0
  184. package/dist/commands/mcp.d.ts +9 -0
  185. package/dist/commands/mcp.js +1 -0
  186. package/dist/commands/model.d.ts +22 -0
  187. package/dist/commands/model.js +1 -0
  188. package/dist/commands/models.d.ts +11 -0
  189. package/dist/commands/models.js +1 -0
  190. package/dist/commands/new.d.ts +7 -0
  191. package/dist/commands/new.js +1 -0
  192. package/dist/commands/plugin.d.ts +7 -0
  193. package/dist/commands/plugin.js +1 -0
  194. package/dist/commands/sandbox.d.ts +12 -0
  195. package/dist/commands/sandbox.js +1 -0
  196. package/dist/commands/showtool.d.ts +12 -0
  197. package/dist/commands/showtool.js +1 -0
  198. package/dist/commands/status.d.ts +24 -0
  199. package/dist/commands/status.js +1 -0
  200. package/dist/commands/stop.d.ts +10 -0
  201. package/dist/commands/stop.js +1 -0
  202. package/dist/commands/subagents.d.ts +12 -0
  203. package/dist/commands/subagents.js +1 -0
  204. package/dist/commands/usage.d.ts +25 -0
  205. package/dist/commands/usage.js +1 -0
  206. package/dist/commands/useplugin.d.ts +7 -0
  207. package/dist/commands/useplugin.js +1 -0
  208. package/dist/config-watcher.d.ts +14 -0
  209. package/dist/config-watcher.js +1 -0
  210. package/dist/config.d.ts +267 -0
  211. package/dist/config.js +1 -0
  212. package/dist/cron/cron-service.d.ts +57 -0
  213. package/dist/cron/cron-service.js +1 -0
  214. package/dist/cron/heartbeat-token.d.ts +29 -0
  215. package/dist/cron/heartbeat-token.js +1 -0
  216. package/dist/cron/schedule.d.ts +3 -0
  217. package/dist/cron/schedule.js +1 -0
  218. package/dist/cron/store.d.ts +4 -0
  219. package/dist/cron/store.js +1 -0
  220. package/dist/cron/types.d.ts +47 -0
  221. package/dist/cron/types.js +1 -0
  222. package/dist/gateway/bridge.d.ts +38 -0
  223. package/dist/gateway/bridge.js +1 -0
  224. package/dist/gateway/channel-manager.d.ts +45 -0
  225. package/dist/gateway/channel-manager.js +1 -0
  226. package/dist/gateway/channels/qr-image.d.ts +5 -0
  227. package/dist/gateway/channels/qr-image.js +1 -0
  228. package/dist/gateway/channels/telegram.d.ts +39 -0
  229. package/dist/gateway/channels/telegram.js +1 -0
  230. package/dist/gateway/channels/webchat.d.ts +51 -0
  231. package/dist/gateway/channels/webchat.js +1 -0
  232. package/dist/gateway/channels/whatsapp.d.ts +40 -0
  233. package/dist/gateway/channels/whatsapp.js +1 -0
  234. package/dist/gateway/node-registry.d.ts +38 -0
  235. package/dist/gateway/node-registry.js +1 -0
  236. package/dist/heracli/index.d.ts +3 -0
  237. package/dist/heracli/index.js +2 -0
  238. package/dist/heracli/logs.d.ts +13 -0
  239. package/dist/heracli/logs.js +1 -0
  240. package/dist/heracli/security/audit.d.ts +17 -0
  241. package/dist/heracli/security/audit.js +1 -0
  242. package/dist/heracli/security/checks/channel-policies.d.ts +6 -0
  243. package/dist/heracli/security/checks/channel-policies.js +1 -0
  244. package/dist/heracli/security/checks/credentials.d.ts +6 -0
  245. package/dist/heracli/security/checks/credentials.js +1 -0
  246. package/dist/heracli/security/checks/fs-permissions.d.ts +6 -0
  247. package/dist/heracli/security/checks/fs-permissions.js +1 -0
  248. package/dist/heracli/security/checks/network.d.ts +4 -0
  249. package/dist/heracli/security/checks/network.js +1 -0
  250. package/dist/heracli/security/report.d.ts +4 -0
  251. package/dist/heracli/security/report.js +1 -0
  252. package/dist/index.d.ts +3 -0
  253. package/dist/index.js +2 -0
  254. package/dist/installer/hera.d.ts +3 -0
  255. package/dist/installer/hera.js +2 -0
  256. package/dist/media/message-processor.d.ts +23 -0
  257. package/dist/media/message-processor.js +1 -0
  258. package/dist/memory/memory-manager.d.ts +21 -0
  259. package/dist/memory/memory-manager.js +1 -0
  260. package/dist/memory/memory-provider.d.ts +22 -0
  261. package/dist/memory/memory-provider.js +1 -0
  262. package/dist/memory/memory-search.d.ts +102 -0
  263. package/dist/memory/memory-search.js +1 -0
  264. package/dist/memory/recall-strategies.d.ts +2 -0
  265. package/dist/memory/recall-strategies.js +1 -0
  266. package/dist/nostromo/auth.d.ts +29 -0
  267. package/dist/nostromo/auth.js +1 -0
  268. package/dist/nostromo/nostromo.d.ts +23 -0
  269. package/dist/nostromo/nostromo.js +1 -0
  270. package/dist/nostromo/ui-html-layout.d.ts +3 -0
  271. package/dist/nostromo/ui-html-layout.js +1 -0
  272. package/dist/nostromo/ui-html-modals.d.ts +3 -0
  273. package/dist/nostromo/ui-html-modals.js +1 -0
  274. package/dist/nostromo/ui-js-agent.d.ts +3 -0
  275. package/dist/nostromo/ui-js-agent.js +1 -0
  276. package/dist/nostromo/ui-js-channels.d.ts +3 -0
  277. package/dist/nostromo/ui-js-channels.js +1 -0
  278. package/dist/nostromo/ui-js-competences.d.ts +3 -0
  279. package/dist/nostromo/ui-js-competences.js +1 -0
  280. package/dist/nostromo/ui-js-config.d.ts +3 -0
  281. package/dist/nostromo/ui-js-config.js +1 -0
  282. package/dist/nostromo/ui-js-core.d.ts +3 -0
  283. package/dist/nostromo/ui-js-core.js +1 -0
  284. package/dist/nostromo/ui-js-ops.d.ts +3 -0
  285. package/dist/nostromo/ui-js-ops.js +1 -0
  286. package/dist/nostromo/ui-js-prompts.d.ts +3 -0
  287. package/dist/nostromo/ui-js-prompts.js +1 -0
  288. package/dist/nostromo/ui-styles.d.ts +3 -0
  289. package/dist/nostromo/ui-styles.js +1 -0
  290. package/dist/nostromo/ui.d.ts +2 -0
  291. package/dist/nostromo/ui.js +1 -0
  292. package/dist/server.d.ts +80 -0
  293. package/dist/server.js +1 -0
  294. package/dist/stt/local-whisper.d.ts +9 -0
  295. package/dist/stt/local-whisper.js +1 -0
  296. package/dist/stt/openai-whisper.d.ts +14 -0
  297. package/dist/stt/openai-whisper.js +1 -0
  298. package/dist/stt/stt-loader.d.ts +4 -0
  299. package/dist/stt/stt-loader.js +1 -0
  300. package/dist/stt/stt-provider.d.ts +4 -0
  301. package/dist/stt/stt-provider.js +1 -0
  302. package/dist/tools/browser-tools.d.ts +9 -0
  303. package/dist/tools/browser-tools.js +1 -0
  304. package/dist/tools/cron-tools.d.ts +4 -0
  305. package/dist/tools/cron-tools.js +1 -0
  306. package/dist/tools/memory-tools.d.ts +3 -0
  307. package/dist/tools/memory-tools.js +1 -0
  308. package/dist/tools/message-tools.d.ts +5 -0
  309. package/dist/tools/message-tools.js +1 -0
  310. package/dist/tools/node-tools.d.ts +3 -0
  311. package/dist/tools/node-tools.js +1 -0
  312. package/dist/tools/server-tools.d.ts +2 -0
  313. package/dist/tools/server-tools.js +1 -0
  314. package/dist/tools/tts-tools.d.ts +3 -0
  315. package/dist/tools/tts-tools.js +1 -0
  316. package/dist/tts/tts-service.d.ts +19 -0
  317. package/dist/tts/tts-service.js +1 -0
  318. package/dist/utils/chunk.d.ts +3 -0
  319. package/dist/utils/chunk.js +1 -0
  320. package/dist/utils/logger.d.ts +16 -0
  321. package/dist/utils/logger.js +1 -0
  322. package/dist/utils/markdown/fences.d.ts +11 -0
  323. package/dist/utils/markdown/fences.js +1 -0
  324. package/dist/utils/markdown/ir.d.ts +33 -0
  325. package/dist/utils/markdown/ir.js +1 -0
  326. package/dist/utils/markdown/render.d.ts +19 -0
  327. package/dist/utils/markdown/render.js +1 -0
  328. package/dist/utils/markdown/tables.d.ts +3 -0
  329. package/dist/utils/markdown/tables.js +1 -0
  330. package/dist/utils/media-response.d.ts +29 -0
  331. package/dist/utils/media-response.js +1 -0
  332. package/dist/utils/package-paths.d.ts +5 -0
  333. package/dist/utils/package-paths.js +1 -0
  334. package/dist/utils/telegram-format.d.ts +13 -0
  335. package/dist/utils/telegram-format.js +1 -0
  336. package/installationPkg/.env.example +26 -0
  337. package/installationPkg/AGENTS.md +143 -0
  338. package/installationPkg/BOOTSTRAP.md +45 -0
  339. package/installationPkg/CBINT.json +16 -0
  340. package/installationPkg/HEARTBEAT.md +5 -0
  341. package/installationPkg/IDENTITY.md +7 -0
  342. package/installationPkg/SOUL.md +36 -0
  343. package/installationPkg/SYSTEM_PROMPT.md +55 -0
  344. package/installationPkg/SYSTEM_PROMPT_SUBAGENT.md +40 -0
  345. package/installationPkg/TOOLS.md +36 -0
  346. package/installationPkg/USER.md +11 -0
  347. package/installationPkg/config.example.yaml +291 -0
  348. package/package.json +95 -0
@@ -0,0 +1 @@
1
+ export function agentJS(){return"\nvar SA_TOOL_LIST = ['Read','Write','Edit','Bash','Glob','Grep','WebSearch','WebFetch'];\n\nvar _editModelIdx = -1;\n\n/* ---- Models ---- */\nfunction showAddModel(){\n document.getElementById('addModelForm').style.display='';\n // Reset form\n document.getElementById('newModelId').value='';\n document.getElementById('newModelName').value='';\n document.getElementById('newModelBaseURL').value='https://api.openai.com/v1';\n document.getElementById('newModelApiKey').value='';\n document.getElementById('newModelEnvVar').value='';\n document.getElementById('newModelType').value='external';\n document.getElementById('newModelProxy').value='not-used';\n document.getElementById('newModelFastUrl').value='';\n document.getElementById('newModelFastProxyApiKey').value='';\n updateNewModelApiFields();\n}\nfunction hideAddModel(){ document.getElementById('addModelForm').style.display='none'; }\nfunction sanitizeEnvVarInput(el){\n var v = el.value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'');\n if(v !== el.value) el.value = v;\n}\nfunction updateNewModelApiFields(){\n var type = document.getElementById('newModelType').value;\n document.getElementById('newModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('newModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('newModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n updateNewModelProxyFields();\n}\nfunction updateNewModelProxyFields(){\n var proxy = document.getElementById('newModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('newModelFastUrl');\n var fk = document.getElementById('newModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nfunction updateEditModelApiFields(){\n var type = document.getElementById('editModelType').value;\n document.getElementById('editModelApiFields').style.display = type!=='internal' ? '' : 'none';\n var baseField = document.getElementById('editModelBaseURLField');\n if(baseField) baseField.style.display = type==='external' ? '' : 'none';\n var proxyField = document.getElementById('editModelProxyField');\n if(proxyField) proxyField.style.display = type==='external' ? '' : 'none';\n updateEditModelProxyFields();\n}\nfunction updateEditModelProxyFields(){\n var proxy = document.getElementById('editModelProxy').value;\n var enabled = proxy !== 'not-used';\n var fu = document.getElementById('editModelFastUrl');\n var fk = document.getElementById('editModelFastProxyApiKey');\n if(fu) fu.disabled = !enabled;\n if(fk) fk.disabled = !enabled;\n}\nasync function loadModels(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderModelsTable();\n}\nfunction renderModelsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('modelsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')!==-1) continue;\n hasVisible = true;\n var typeBadges = types.map(function(t){ return '<span class=\"badge badge-blue\" style=\"font-size:11px;padding:1px 6px;margin-right:2px\">'+esc(t)+'</span>'; }).join('');\n var proxyBadge = (m.proxy && m.proxy !== 'not-used') ? ' <span class=\"badge badge-green\" style=\"font-size:11px;padding:1px 6px\">'+esc(m.proxy)+'</span>' : '';\n tbody.innerHTML += '<tr data-model-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(m.id)+'</td><td>'+typeBadges+proxyBadge+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditModel('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteModel('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No models in registry</td></tr>';\n }\n // Hide edit form when re-rendering\n document.getElementById('editModelForm').style.display='none';\n _editModelIdx = -1;\n}\nfunction addModel(){\n var id = document.getElementById('newModelId').value.trim();\n var name = document.getElementById('newModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('newModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('newModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('newModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('newModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('newModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('newModelFastProxyApiKey').value.trim() : '';\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar});\n hideAddModel();\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nvar _deleteModelIdx = -1;\nfunction confirmDeleteModel(idx){\n _deleteModelIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this model';\n document.getElementById('modelDeleteName').textContent = name;\n document.getElementById('modelDeleteModal').classList.add('open');\n}\nfunction closeModelDeleteModal(){\n document.getElementById('modelDeleteModal').classList.remove('open');\n _deleteModelIdx = -1;\n}\nfunction doDeleteModel(){\n if(_deleteModelIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteModelIdx,1);\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n }\n closeModelDeleteModal();\n}\nfunction startEditModel(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editModelIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editModelId').value = m.id||'';\n document.getElementById('editModelName').value = m.name||'';\n document.getElementById('editModelBaseURL').value = m.baseURL||'';\n document.getElementById('editModelApiKey').value = m.apiKey||'';\n document.getElementById('editModelEnvVar').value = m.useEnvVar||'';\n document.getElementById('editModelProxy').value = m.proxy||'not-used';\n document.getElementById('editModelFastUrl').value = m.fastUrl||'';\n document.getElementById('editModelFastProxyApiKey').value = m.fastProxyApiKey||'';\n var types = m.types||['external'];\n document.getElementById('editModelType').value = types[0]||'external';\n updateEditModelApiFields();\n // Position edit form after the table\n document.getElementById('editModelForm').style.display='';\n}\nfunction finishEditModel(){\n if(_editModelIdx<0 || !currentConfig.models||!currentConfig.models[_editModelIdx]) return;\n var id = document.getElementById('editModelId').value.trim();\n var name = document.getElementById('editModelName').value.trim();\n if(!id){ toast('Model ID required','err'); return; }\n if(!name) name = id;\n var type = document.getElementById('editModelType').value;\n var types = [type];\n var needsApi = type!=='internal';\n var useEnvVar = needsApi ? document.getElementById('editModelEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim() : '';\n var apiKey = needsApi ? document.getElementById('editModelApiKey').value.trim() : '';\n var baseURL = type==='external' ? document.getElementById('editModelBaseURL').value.trim() : '';\n var proxy = type==='external' ? document.getElementById('editModelProxy').value : 'not-used';\n var fastUrl = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastUrl').value.trim() : '';\n var fastProxyApiKey = (type==='external' && proxy!=='not-used') ? document.getElementById('editModelFastProxyApiKey').value.trim() : '';\n currentConfig.models[_editModelIdx] = {id:id, name:name, types:types, proxy:proxy, fastUrl:fastUrl, fastProxyApiKey:fastProxyApiKey, apiKey:apiKey, baseURL:baseURL, useEnvVar:useEnvVar};\n _editModelIdx = -1;\n renderModelsTable();\n populateModelSelects();\n saveConfig();\n}\nfunction cancelEditModel(){\n _editModelIdx = -1;\n document.getElementById('editModelForm').style.display='none';\n}\nfunction populateModelSelects(){\n var models = (currentConfig&&currentConfig.models)||[];\n var internalModels = models.filter(function(m){ var t=m.types||['external']; return t.indexOf('internal')!==-1 || (t.indexOf('external')!==-1 && m.proxy && m.proxy!=='not-used'); });\n var selects = {\n agentModel: {val:'', allowNone:false},\n agentMainFallback: {val:'', allowNone:true}\n };\n for(var key in selects){\n var el = document.getElementById(key);\n if(!el) continue;\n selects[key].val = el.value;\n el.innerHTML='';\n if(selects[key].allowNone) el.innerHTML += '<option value=\"\">None</option>';\n for(var i=0;i<internalModels.length;i++){\n el.innerHTML += '<option value=\"'+esc(internalModels[i].name)+'\">'+esc(internalModels[i].name)+'</option>';\n }\n el.value = selects[key].val;\n }\n populateSTTModelSelect();\n populateMemSearchModelSelect();\n}\nfunction populateSTTModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('sttModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\nfunction populateMemSearchModelSelect(){\n var models = (currentConfig&&currentConfig.models)||[];\n var el = document.getElementById('memSearchModelRef');\n if(!el) return;\n var prev = el.value;\n el.innerHTML = '<option value=\"\">-- select --</option>';\n for(var i=0;i<models.length;i++){\n var types = models[i].types||['external'];\n if(types.indexOf('external')===-1) continue;\n el.innerHTML += '<option value=\"'+esc(models[i].name)+'\">'+esc(models[i].name)+' ('+esc(models[i].id)+')</option>';\n }\n el.value = prev;\n}\n\n/* ---- Agent ---- */\nasync function loadAgent(){\n currentConfig = await fetchAPI('/config');\n const a = currentConfig.agent||{};\n populateModelSelects();\n document.getElementById('agentModel').value = a.model||'';\n document.getElementById('agentMainFallback').value = a.mainFallback||'';\n document.getElementById('agentMaxTurns').value = a.maxTurns||10;\n document.getElementById('agentPermMode').value = a.permissionMode||'default';\n document.getElementById('agentSessionTTL').value = a.sessionTTL||3600;\n document.getElementById('agentSettingSources').value = a.settingSources||'project';\n document.getElementById('agentCoderSkill').checked = !!a.builtinCoderSkill;\n document.getElementById('agentAutoRenew').value = a.autoRenew||0;\n var allowed = a.allowedTools||[];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){cb.checked = allowed.indexOf(cb.dataset.tool)!==-1;});\n document.getElementById('agentQueueMode').value = a.queueMode||'collect';\n document.getElementById('agentDebounceMs').value = a.queueDebounceMs!=null ? a.queueDebounceMs : 1500;\n document.getElementById('agentQueueCap').value = a.queueCap!=null ? a.queueCap : 20;\n document.getElementById('agentDropPolicy').value = a.queueDropPolicy||'summarize';\n document.getElementById('agentInflightTyping').checked = a.inflightTyping!==false;\n document.getElementById('agentAutoApprove').checked = a.autoApproveTools!==false;\n updateQueueFields();\n sectionsLoaded.agent = true;\n}\n\n/* ---- Queue fields visibility ---- */\nfunction updateQueueFields(){\n var mode = document.getElementById('agentQueueMode').value;\n document.getElementById('queueCollectFields').style.display = mode==='collect' ? '' : 'none';\n}\n\n/* ---- SubAgents ---- */\nvar _saDeleteIdx = -1;\nasync function loadSubAgents(){\n currentConfig = await fetchAPI('/config');\n renderSubAgentCards();\n}\nfunction toggleSaAccordion(idx){\n var el = document.querySelector('.sa-acc[data-sa-idx=\"'+idx+'\"]');\n if(el) el.classList.toggle('open');\n}\nfunction renderSubAgentCards(){\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var container = document.getElementById('subAgentCards');\n var empty = document.getElementById('subAgentEmpty');\n if(sas.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var order = [];\n for(var k=0;k<sas.length;k++) order.push(k);\n order.sort(function(a,b){ return (sas[a].name||'').localeCompare(sas[b].name||''); });\n var html = '';\n for(var oi = 0; oi < order.length; oi++){\n var i = order[oi];\n var sa = sas[i];\n html += '<div class=\"sa-acc\" data-sa-idx=\"'+i+'\">';\n html += '<div class=\"sa-acc-header\" onclick=\"toggleSaAccordion('+i+')\">';\n html += '<svg class=\"sa-acc-chevron\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>';\n html += '<span class=\"sa-acc-name\">' + esc(sa.name) + '</span>';\n if(sa.expandContext) html += '<span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 6px\">expanded</span>';\n html += '<label class=\"toggle\" onclick=\"event.stopPropagation()\"><input type=\"checkbox\" data-sa-toggle=\"'+i+'\" '+(sa.enabled?'checked':'')+'><span></span></label>';\n html += '<button class=\"btn-danger btn-sm\" onclick=\"event.stopPropagation();confirmDeleteSubAgent('+i+')\">Delete</button>';\n html += '</div>';\n html += '<div class=\"sa-acc-body\">';\n html += '<div class=\"field\"><label>Description</label><textarea data-sa-field=\"'+i+'.description\" rows=\"2\" oninput=\"updateSaField('+i+',&quot;description&quot;,this.value)\">'+esc(sa.description)+'</textarea></div>';\n html += '<div class=\"field\"><label>Prompt</label><textarea data-sa-field=\"'+i+'.prompt\" rows=\"3\" oninput=\"updateSaField('+i+',&quot;prompt&quot;,this.value)\">'+esc(sa.prompt)+'</textarea></div>';\n html += '<div class=\"field\"><label>Model</label><select data-sa-field=\"'+i+'.model\" onchange=\"updateSaField('+i+',&quot;model&quot;,this.value)\">';\n var saModels = ['inherit','sonnet','opus','haiku'];\n for(var j=0;j<saModels.length;j++){\n html += '<option value=\"'+saModels[j]+'\"'+(sa.model===saModels[j]?' selected':'')+'>'+saModels[j]+'</option>';\n }\n html += '</select></div>';\n var saTools = sa.tools||[];\n html += '<div class=\"field\"><label>Tools</label><div style=\"display:flex;flex-wrap:wrap;gap:4px;margin-top:4px\">';\n for(var t=0;t<SA_TOOL_LIST.length;t++){\n var tn = SA_TOOL_LIST[t];\n var checked = saTools.indexOf(tn)!==-1;\n html += '<label class=\"tool-toggle-sm\"><label class=\"toggle-sm\"><input type=\"checkbox\" data-sa-tool=\"'+i+'\" data-tool-name=\"'+tn+'\"'+(checked?' checked':'')+'><span></span></label> '+tn+'</label>';\n }\n html += '</div></div>';\n html += '<div style=\"display:flex;align-items:center;gap:12px;margin-top:8px\"><span style=\"font-size:13px;font-weight:500\">Expand Context</span><label class=\"toggle\"><input type=\"checkbox\" data-sa-expand=\"'+i+'\"'+(sa.expandContext?' checked':'')+'><span></span></label></div>';\n html += '</div></div>';\n }\n container.innerHTML = html;\n // Bind expandContext listeners\n container.querySelectorAll('[data-sa-expand]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saExpand);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].expandContext = cb.checked;\n markDirty();\n renderSubAgentCards();\n }\n });\n });\n // Bind toggle listeners\n container.querySelectorAll('[data-sa-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n currentConfig.agent.customSubAgents[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n // Bind tool toggle listeners\n container.querySelectorAll('[data-sa-tool]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.saTool);\n var toolName = cb.dataset.toolName;\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n var tools = currentConfig.agent.customSubAgents[idx].tools || [];\n if(cb.checked){\n if(tools.indexOf(toolName)===-1) tools.push(toolName);\n } else {\n tools = tools.filter(function(t){return t!==toolName;});\n }\n currentConfig.agent.customSubAgents[idx].tools = tools;\n markDirty();\n }\n });\n });\n}\nfunction confirmDeleteSubAgent(idx){\n _saDeleteIdx = idx;\n var sas = (currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents) || [];\n var name = sas[idx] ? sas[idx].name : 'this subagent';\n document.getElementById('saDeleteName').textContent = name;\n document.getElementById('saDeleteModal').classList.add('open');\n}\nfunction closeSaDeleteModal(){\n document.getElementById('saDeleteModal').classList.remove('open');\n _saDeleteIdx = -1;\n}\nfunction doDeleteSubAgent(){\n if(_saDeleteIdx >= 0) deleteSubAgent(_saDeleteIdx);\n closeSaDeleteModal();\n saveConfig();\n}\nfunction updateSaField(idx, field, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx][field] = value;\n markDirty();\n }\n}\nfunction updateSaTools(idx, value){\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents && currentConfig.agent.customSubAgents[idx]){\n currentConfig.agent.customSubAgents[idx].tools = value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n markDirty();\n }\n}\nfunction showAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = '';\n document.getElementById('newSaName').value = '';\n document.getElementById('newSaDesc').value = '';\n document.getElementById('newSaPrompt').value = '';\n document.getElementById('newSaModel').value = 'inherit';\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){cb.checked = cb.dataset.newSaTool!=='Bash';});\n document.getElementById('newSaExpandContext').checked = false;\n document.getElementById('newSaName').focus();\n}\nfunction hideAddSubAgent(){\n document.getElementById('addSubAgentForm').style.display = 'none';\n}\nasync function addSubAgent(){\n var name = document.getElementById('newSaName').value.trim();\n var desc = document.getElementById('newSaDesc').value.trim();\n var prompt = document.getElementById('newSaPrompt').value.trim();\n var model = document.getElementById('newSaModel').value;\n var tools = [];\n document.querySelectorAll('#newSaToolsGrid [data-new-sa-tool]').forEach(function(cb){if(cb.checked) tools.push(cb.dataset.newSaTool);});\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_\\-\\[\\]!]+$/.test(name)){ toast('Name may only contain a-z A-Z 0-9 - _ [ ] !','err'); return; }\n if(!desc || desc.length < 10){ toast('Description must be at least 10 characters','err'); return; }\n if(!prompt || prompt.length < 10){ toast('Prompt must be at least 10 characters','err'); return; }\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.customSubAgents) currentConfig.agent.customSubAgents = [];\n var expandContext = document.getElementById('newSaExpandContext').checked;\n currentConfig.agent.customSubAgents.push({ name:name, description:desc, prompt:prompt, model:model, tools:tools, expandContext:expandContext, enabled:false });\n hideAddSubAgent();\n renderSubAgentCards();\n saveConfig();\n}\nfunction deleteSubAgent(idx){\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.customSubAgents) return;\n currentConfig.agent.customSubAgents.splice(idx, 1);\n renderSubAgentCards();\n markDirty();\n}\n\n/* ---- Vars ---- */\nvar _editVarIdx = -1;\nvar _deleteVarIdx = -1;\n\nfunction showAddVar(){\n document.getElementById('addVarForm').style.display='';\n document.getElementById('newVarName').value='';\n document.getElementById('newVarEnvVar').value='';\n document.getElementById('newVarApiKey').value='';\n}\nfunction hideAddVar(){ document.getElementById('addVarForm').style.display='none'; }\nfunction addVar(){\n var name = document.getElementById('newVarName').value.trim();\n var useEnvVar = document.getElementById('newVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('newVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n if(!currentConfig.models) currentConfig.models=[];\n currentConfig.models.push({id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar});\n hideAddVar();\n renderVarsTable();\n saveConfig();\n}\nasync function loadVars(){\n currentConfig = await fetchAPI('/config');\n if(!currentConfig.models) currentConfig.models=[];\n renderVarsTable();\n}\nfunction renderVarsTable(){\n var models = currentConfig.models||[];\n var tbody = document.getElementById('varsBody');\n tbody.innerHTML='';\n var hasVisible = false;\n for(var i=0;i<models.length;i++){\n var m = models[i];\n var types = m.types||['external'];\n if(types.indexOf('env-var')===-1) continue;\n hasVisible = true;\n var envDisplay = m.useEnvVar||m.id||'';\n tbody.innerHTML += '<tr data-var-idx=\"'+i+'\"><td>'+esc(m.name)+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(envDisplay)+'</td><td style=\"white-space:nowrap\"><button class=\"btn-ghost btn-sm\" onclick=\"startEditVar('+i+')\">Edit</button> <button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteVar('+i+')\">Delete</button></td></tr>';\n }\n if(!hasVisible){\n tbody.innerHTML='<tr><td colspan=\"3\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No vars in registry</td></tr>';\n }\n document.getElementById('editVarForm').style.display='none';\n _editVarIdx = -1;\n}\nfunction startEditVar(idx){\n if(!currentConfig.models||!currentConfig.models[idx]) return;\n _editVarIdx = idx;\n var m = currentConfig.models[idx];\n document.getElementById('editVarName').value = m.name||'';\n document.getElementById('editVarEnvVar').value = m.useEnvVar||'';\n document.getElementById('editVarApiKey').value = m.apiKey||'';\n document.getElementById('editVarForm').style.display='';\n}\nfunction finishEditVar(){\n if(_editVarIdx<0 || !currentConfig.models||!currentConfig.models[_editVarIdx]) return;\n var name = document.getElementById('editVarName').value.trim();\n var useEnvVar = document.getElementById('editVarEnvVar').value.toUpperCase().replace(/[^A-Z0-9_\\-\\+\\[\\]]/g,'').trim();\n var apiKey = document.getElementById('editVarApiKey').value.trim();\n if(!name){ toast('Display Name required','err'); return; }\n if(!useEnvVar){ toast('Env Var required','err'); return; }\n currentConfig.models[_editVarIdx] = {id:useEnvVar, name:name, types:['env-var'], apiKey:apiKey, baseURL:'', useEnvVar:useEnvVar};\n _editVarIdx = -1;\n renderVarsTable();\n saveConfig();\n}\nfunction cancelEditVar(){\n _editVarIdx = -1;\n document.getElementById('editVarForm').style.display='none';\n}\nfunction confirmDeleteVar(idx){\n _deleteVarIdx = idx;\n var models = (currentConfig&&currentConfig.models)||[];\n var name = models[idx] ? models[idx].name : 'this var';\n document.getElementById('varDeleteName').textContent = name;\n document.getElementById('varDeleteModal').classList.add('open');\n}\nfunction closeVarDeleteModal(){\n document.getElementById('varDeleteModal').classList.remove('open');\n _deleteVarIdx = -1;\n}\nfunction doDeleteVar(){\n if(_deleteVarIdx>=0 && currentConfig.models){\n currentConfig.models.splice(_deleteVarIdx,1);\n renderVarsTable();\n saveConfig();\n }\n closeVarDeleteModal();\n}\n\n/* ---- Internal Tools discovery ---- */\nvar _internalToolsLoaded = false;\nasync function openInternalToolsModal(){\n document.getElementById('internalToolsModal').classList.add('open');\n if(_internalToolsLoaded) return;\n try {\n var data = await fetchAPI('/internal-tools');\n var wrap = document.getElementById('internalToolsBody');\n if(!data || !data.length){ wrap.innerHTML = '<p style=\"color:var(--text-muted)\">No internal tool servers registered.</p>'; _internalToolsLoaded = true; return; }\n var html = '<table class=\"tbl\" style=\"font-size:13px\"><thead><tr><th style=\"width:18%\">Server</th><th style=\"width:20%\">Tool</th><th>Parameters</th></tr></thead><tbody>';\n for(var s = 0; s < data.length; s++){\n var srv = data[s];\n var toolCount = srv.tools.length || 1;\n for(var t = 0; t < srv.tools.length; t++){\n var tl = srv.tools[t];\n html += '<tr>';\n if(t === 0) html += '<td rowspan=\"'+toolCount+'\" style=\"vertical-align:top\"><strong>'+esc(srv.server)+'</strong></td>';\n html += '<td style=\"vertical-align:top\"><code>'+esc(tl.name)+'</code><div style=\"color:var(--text-muted);font-size:11px;margin-top:4px\">'+esc(tl.description)+'</div></td>';\n if(tl.params.length === 0){\n html += '<td style=\"color:var(--text-muted);font-style:italic\">none</td>';\n } else {\n html += '<td>';\n for(var p = 0; p < tl.params.length; p++){\n var pm = tl.params[p];\n if(p > 0) html += '<br>';\n html += '<code>'+esc(pm.name)+'</code> <span style=\"color:var(--text-muted)\">('+esc(pm.type)+(pm.required?'':',opt')+')</span>';\n if(pm.description) html += ' — <span style=\"font-size:12px\">'+esc(pm.description)+'</span>';\n }\n html += '</td>';\n }\n html += '</tr>';\n }\n if(srv.tools.length === 0){\n html += '<tr><td><strong>'+esc(srv.server)+'</strong></td><td colspan=\"2\" style=\"color:var(--text-muted);font-style:italic\">No tools</td></tr>';\n }\n }\n html += '</tbody></table>';\n wrap.innerHTML = html;\n _internalToolsLoaded = true;\n } catch(err) {\n document.getElementById('internalToolsBody').innerHTML = '<p style=\"color:var(--danger)\">Failed to load: '+esc(String(err))+'</p>';\n }\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** Browser JS for channel constants, help text, field rendering, and loadChannels(). */
2
+ export declare function channelsJS(): string;
3
+ //# sourceMappingURL=ui-js-channels.d.ts.map
@@ -0,0 +1 @@
1
+ export function channelsJS(){return"\n/* ---- Channels ---- */\nconst CHANNEL_LIST = ['telegram','responses','whatsapp','discord','slack','signal','msteams','googlechat','line','matrix'];\nconst SUPPORTED_CHANNELS = ['telegram','responses','whatsapp'];\n\nconst CHANNEL_HELP = {\n telegram: {\n title: 'Telegram Setup',\n steps: [\n 'Open Telegram and message <a href=\"https://t.me/BotFather\" target=\"_blank\">@BotFather</a>',\n 'Send <code>/newbot</code>',\n 'Choose a display name, then a username (must end in <code>bot</code>)',\n 'BotFather replies with a <b>bot token</b> &mdash; paste it below',\n 'Optional: send <code>/setprivacy</code> &rarr; Disable, so the bot can see all group messages',\n '<b>DM Policy</b> controls who can talk to the bot: <b>open</b> = anyone can message, <b>allowlist</b> = only Telegram user IDs listed in Allow From can message',\n ]\n },\n discord: {\n title: 'Discord Setup',\n steps: [\n 'Go to <a href=\"https://discord.com/developers/applications\" target=\"_blank\">Discord Developer Portal</a> &rarr; <b>New Application</b>',\n '<b>Bot</b> tab &rarr; click <b>Reset Token</b> &rarr; copy the token and paste it below',\n 'On the same page, enable <b>Message Content Intent</b> under Privileged Gateway Intents',\n '<b>OAuth2 &rarr; URL Generator</b> &rarr; scope: <code>bot</code>, permission: <code>Send Messages</code>',\n 'Open the generated URL to invite the bot to your server',\n ]\n },\n slack: {\n title: 'Slack Setup',\n steps: [\n 'Go to <a href=\"https://api.slack.com/apps\" target=\"_blank\">api.slack.com/apps</a> &rarr; <b>Create New App</b> &rarr; From scratch',\n '<b>Socket Mode</b> &rarr; enable &rarr; generate an App-Level Token with scope <code>connections:write</code> &rarr; copy it (this is the <b>App Token</b>, starts with <code>xapp-</code>)',\n '<b>OAuth &amp; Permissions</b> &rarr; add Bot Token Scopes: <code>chat:write</code>, <code>im:history</code>, <code>im:read</code>',\n '<b>Event Subscriptions</b> &rarr; enable &rarr; subscribe to bot event <code>message.im</code>',\n 'Install the app to your workspace &rarr; copy the <b>Bot Token</b> (starts with <code>xoxb-</code>)',\n ]\n },\n whatsapp: {\n title: 'WhatsApp Setup',\n steps: [\n 'Set the <b>Auth Directory</b> below to a local folder (e.g. <code>./data/whatsapp</code>)',\n 'Enable the channel, save, then click <b>Connect WhatsApp</b> &mdash; a QR code will appear in a pop-up',\n 'Open WhatsApp on your phone &rarr; <b>Settings</b> &rarr; <b>Linked Devices</b> &rarr; scan the QR code',\n 'Once scanned, the pop-up confirms the connection. The session persists in the auth directory',\n '<b>DM Policy</b>: <b>open</b> = anyone can message, <b>allowlist</b> = only the phone numbers listed in Allow From can message (E.164 format, e.g. <code>+393331234567</code>)',\n ]\n },\n signal: {\n title: 'Signal Setup',\n steps: [\n 'Install <a href=\"https://github.com/bbernhard/signal-cli-rest-api\" target=\"_blank\">signal-cli-rest-api</a> and start it',\n 'Register or link a phone number via the signal-cli REST API',\n 'Enter the <b>API URL</b> (e.g. <code>http://localhost:8080</code>) and <b>Phone Number</b> below',\n ]\n },\n msteams: {\n title: 'Microsoft Teams Setup',\n steps: [\n 'Go to <a href=\"https://dev.teams.microsoft.com/\" target=\"_blank\">Teams Developer Portal</a> &rarr; <b>Apps</b> &rarr; New App',\n 'Under <b>App Features</b> &rarr; add a <b>Bot</b>',\n 'In Azure, register a new <b>Bot Channel Registration</b> &rarr; copy <b>App ID</b> and <b>App Secret</b>',\n 'Set the messaging endpoint to your server URL',\n 'Paste App ID and App Secret below',\n ]\n },\n googlechat: {\n title: 'Google Chat Setup',\n steps: [\n 'Go to <a href=\"https://console.cloud.google.com/\" target=\"_blank\">Google Cloud Console</a> &rarr; create or select a project',\n 'Enable the <b>Google Chat API</b>',\n '<b>Configuration</b> tab &rarr; set Bot URL to your server endpoint',\n 'Create a <b>Service Account</b> &rarr; download the JSON key file',\n 'Enter the path to the <b>credentials file</b> and <b>space ID</b> below',\n ]\n },\n line: {\n title: 'LINE Setup',\n steps: [\n 'Go to <a href=\"https://developers.line.biz/console/\" target=\"_blank\">LINE Developers Console</a> &rarr; create a Provider &rarr; create a <b>Messaging API</b> channel',\n 'Under <b>Messaging API</b> tab, issue a <b>Channel Access Token</b> &rarr; paste it below',\n 'Copy the <b>Channel Secret</b> from the <b>Basic Settings</b> tab',\n 'Set the <b>Webhook URL</b> to your server endpoint and enable <b>Use Webhook</b>',\n ]\n },\n matrix: {\n title: 'Matrix Setup',\n steps: [\n 'Create a bot account on your Matrix homeserver (e.g. via <code>register_new_matrix_user</code>)',\n 'Enter the <b>Homeserver URL</b> (e.g. <code>https://matrix.example.com</code>)',\n 'Enter the bot <b>User ID</b> (e.g. <code>@bot:example.com</code>) and <b>Access Token</b>',\n 'Invite the bot to the rooms you want it to participate in',\n ]\n },\n responses: {\n title: 'Responses API',\n steps: [\n 'This is a built-in HTTP API compatible with the OpenAI Responses format',\n 'Create an API token in the <b>Tokens</b> section to authenticate requests',\n 'Send requests to <code>POST http://&lt;host&gt;:&lt;port&gt;/v1/responses</code> with <code>Authorization: Bearer &lt;token&gt;</code>',\n ]\n },\n};\n\nfunction channelFields(ch, cfg) {\n const accts = cfg.accounts || {};\n const dfl = Object.values(accts)[0] || {};\n const aid = Object.keys(accts)[0] || 'default';\n let h = '';\n switch (ch) {\n case 'telegram':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n var dp = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dp==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dp==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(Telegram user IDs, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n break;\n case 'discord':\n h += '<div class=\"field\"><label>Bot Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.token\" value=\"'+esc(dfl.token||'')+'\"></div>';\n break;\n case 'slack':\n h += '<div class=\"field\"><label>Bot Token <span style=\"color:var(--text-muted);font-weight:400\">(xoxb-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.botToken\" value=\"'+esc(dfl.botToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Token <span style=\"color:var(--text-muted);font-weight:400\">(xapp-...)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appToken\" value=\"'+esc(dfl.appToken||'')+'\"></div>';\n break;\n case 'whatsapp':\n h += '<div class=\"field\"><label>Auth Directory</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.authDir\" value=\"'+esc(dfl.authDir||'./data/whatsapp')+'\"></div>';\n var dpw = dfl.dmPolicy || 'allowlist';\n h += '<div class=\"field\"><label>DM Policy</label><select data-ch-field=\"'+ch+'.'+aid+'.dmPolicy\"><option value=\"open\"'+(dpw==='open'?' selected':'')+'>open</option><option value=\"allowlist\"'+(dpw==='allowlist'?' selected':'')+'>allowlist</option></select></div>';\n h += '<div class=\"field\"><label>Allow From <span style=\"color:var(--text-muted);font-weight:400\">(phone numbers in E.164 format, comma-separated &mdash; only used with allowlist policy)</span></label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.allowFrom\" value=\"'+esc((dfl.allowFrom||[]).join(', '))+'\"></div>';\n h += '<div class=\"field\"><button type=\"button\" class=\"btn btn-sm\" onclick=\"openWhatsAppQr()\" id=\"wa-connect-btn\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-3px;margin-right:6px\"><path d=\"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4\"/><polyline points=\"10 17 15 12 10 7\"/><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"/></svg>Connect WhatsApp</button></div>';\n break;\n case 'signal':\n h += '<div class=\"field\"><label>API URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.apiUrl\" value=\"'+esc(dfl.apiUrl||'http://localhost:8080')+'\"></div>';\n h += '<div class=\"field\"><label>Phone Number</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.phoneNumber\" value=\"'+esc(dfl.phoneNumber||'')+'\"></div>';\n break;\n case 'msteams':\n h += '<div class=\"field\"><label>App ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.appId\" value=\"'+esc(dfl.appId||'')+'\"></div>';\n h += '<div class=\"field\"><label>App Secret</label><input type=\"password\" data-ch-field=\"'+ch+'.'+aid+'.appSecret\" value=\"'+esc(dfl.appSecret||'')+'\"></div>';\n break;\n case 'googlechat':\n h += '<div class=\"field\"><label>Credentials File</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.credentialsFile\" value=\"'+esc(dfl.credentialsFile||'')+'\"></div>';\n h += '<div class=\"field\"><label>Space ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.spaceId\" value=\"'+esc(dfl.spaceId||'')+'\"></div>';\n break;\n case 'line':\n h += '<div class=\"field\"><label>Channel Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelAccessToken\" value=\"'+esc(dfl.channelAccessToken||'')+'\"></div>';\n h += '<div class=\"field\"><label>Channel Secret</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.channelSecret\" value=\"'+esc(dfl.channelSecret||'')+'\"></div>';\n break;\n case 'matrix':\n h += '<div class=\"field\"><label>Homeserver URL</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.homeserverUrl\" value=\"'+esc(dfl.homeserverUrl||'')+'\"></div>';\n h += '<div class=\"field\"><label>User ID</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.userId\" value=\"'+esc(dfl.userId||'')+'\"></div>';\n h += '<div class=\"field\"><label>Access Token</label><input type=\"text\" data-ch-field=\"'+ch+'.'+aid+'.accessToken\" value=\"'+esc(dfl.accessToken||'')+'\"></div>';\n break;\n case 'responses':\n h += '<div class=\"field\"><label>Port</label><input type=\"number\" data-ch-field=\"'+ch+'.port\" value=\"'+(cfg.port||3000)+'\"></div>';\n break;\n }\n return h;\n}\n\nasync function loadChannels(){\n currentConfig = await fetchAPI('/config');\n const wrap = document.getElementById('channelCards');\n wrap.innerHTML='';\n for(const ch of CHANNEL_LIST){\n const cfg = currentConfig.channels?.[ch]||{enabled:false};\n const help = CHANNEL_HELP[ch];\n const helpId = 'help-'+ch;\n const supported = SUPPORTED_CHANNELS.indexOf(ch) !== -1;\n if(!supported){ continue; }\n let html = '<div class=\"card\" data-channel=\"'+ch+'\">';\n html += '<div class=\"card-header\"><span class=\"card-title\" style=\"text-transform:capitalize\">'+esc(ch)+'</span>';\n html += '<label class=\"toggle\"><input type=\"checkbox\" data-ch-toggle=\"'+ch+'\" '+(cfg.enabled?'checked':'')+'><span></span></label></div>';\n html += '<div class=\"ch-fields\" data-ch-fields=\"'+ch+'\" style=\"'+(cfg.enabled?'':'display:none')+'\">';\n if(help){\n html += '<div style=\"margin-bottom:10px\"><button class=\"help-toggle\" type=\"button\" onclick=\"toggleHelp(this)\" data-help=\"'+helpId+'\" title=\"Setup guide\">?</button> <span style=\"font-size:13px;color:var(--text-muted)\">How to set up</span></div>';\n html += '<div class=\"help-panel\" id=\"'+helpId+'\"><b>'+help.title+'</b><ol>';\n for(const s of help.steps) html += '<li>'+s+'</li>';\n html += '</ol></div>';\n }\n html += channelFields(ch, cfg);\n html += '</div></div>';\n wrap.innerHTML += html;\n }\n wrap.querySelectorAll('[data-ch-toggle]').forEach(inp=>{\n inp.addEventListener('change',e=>{\n const fields = wrap.querySelector('[data-ch-fields=\"'+e.target.dataset.chToggle+'\"]');\n if(fields) fields.style.display = e.target.checked ? '' : 'none';\n });\n });\n markEnvRefs(wrap);\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** Browser JS for Skills, Commands, and Plugins (load/render/add/delete). */
2
+ export declare function competencesJS(): string;
3
+ //# sourceMappingURL=ui-js-competences.d.ts.map
@@ -0,0 +1 @@
1
+ export function competencesJS(){return"\n/* ---- Skills ---- */\nvar _skillDropZoneInited = false;\nasync function loadSkills(){\n try{\n var skills = await fetchAPI('/skills');\n var container = document.getElementById('skillsList');\n var empty = document.getElementById('skillsEmpty');\n if(!skills || skills.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n } else {\n empty.style.display = 'none';\n skills.sort(function(a,b){ return (a.name||'').localeCompare(b.name||''); });\n var html = '<table class=\"tbl\"><thead><tr><th>Name</th><th>Description</th><th>Folder</th><th></th></tr></thead><tbody>';\n for(var i = 0; i < skills.length; i++){\n var sk = skills[i];\n html += '<tr>';\n html += '<td style=\"font-weight:600\">' + esc(sk.name) + '</td>';\n html += '<td style=\"color:var(--text-muted)\">' + esc(sk.description || '\\u2014') + '</td>';\n html += '<td style=\"max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help\" title=\"' + esc(sk.folder) + '\"><code style=\"font-size:12px\">' + esc(sk.folder) + '</code></td>';\n html += '<td style=\"text-align:right;white-space:nowrap\"><button class=\"btn-ghost btn-sm\" data-edit-skill=\"'+esc(sk.folder)+'\" style=\"margin-right:4px\">Edit</button><button class=\"btn-ghost btn-sm\" data-download-skill=\"'+esc(sk.folder)+'\" style=\"margin-right:4px\">Download</button><button class=\"btn-danger btn-sm\" data-delete-skill=\"'+esc(sk.folder)+'\">Delete</button></td>';\n html += '</tr>';\n }\n html += '</tbody></table>';\n container.innerHTML = html;\n container.querySelectorAll('[data-delete-skill]').forEach(function(btn){\n btn.addEventListener('click', function(){ confirmDeleteSkill(btn.dataset.deleteSkill); });\n });\n container.querySelectorAll('[data-download-skill]').forEach(function(btn){\n btn.addEventListener('click', function(){ downloadComponent(API+'/skills/download/'+encodeURIComponent(btn.dataset.downloadSkill)); });\n });\n container.querySelectorAll('[data-edit-skill]').forEach(function(btn){\n btn.addEventListener('click', function(){ openSkillEditor(btn.dataset.editSkill); });\n });\n }\n if(!_skillDropZoneInited){ initSkillDropZone(); _skillDropZoneInited = true; }\n var installedFolders = [];\n if(skills && skills.length > 0){\n for(var j = 0; j < skills.length; j++) installedFolders.push(skills[j].folder);\n }\n loadBundledSkills(installedFolders);\n }catch(e){ toast('Failed to load skills','err'); }\n}\nfunction applySkillsFilter(){\n var filter = (document.getElementById('skillsFilter').value||'').toLowerCase();\n var rows = document.querySelectorAll('#skillsList tbody tr');\n rows.forEach(function(row){\n var text = row.textContent.toLowerCase();\n row.style.display = (!filter || text.indexOf(filter) !== -1) ? '' : 'none';\n });\n}\nfunction applyBundledSkillsFilter(){\n var filter = (document.getElementById('bundledSkillsFilter').value||'').toLowerCase();\n var rows = document.querySelectorAll('#bundledSkillsList tbody tr');\n rows.forEach(function(row){\n var text = row.textContent.toLowerCase();\n row.style.display = (!filter || text.indexOf(filter) !== -1) ? '' : 'none';\n });\n}\nvar _skillDeleteFolder = '';\nfunction confirmDeleteSkill(folder){\n _skillDeleteFolder = folder;\n document.getElementById('skillDeleteName').textContent = folder;\n document.getElementById('skillDeleteModal').classList.add('open');\n}\nfunction closeSkillDeleteModal(){\n document.getElementById('skillDeleteModal').classList.remove('open');\n _skillDeleteFolder = '';\n}\nasync function doDeleteSkill(){\n if(!_skillDeleteFolder) return;\n var delFolder = _skillDeleteFolder;\n closeSkillDeleteModal();\n try{\n var res = await fetch(API+'/skills/delete', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({folder:delFolder})});\n if(!res.ok){ var d=await res.json(); toast(d.error||'Delete failed','err'); return; }\n toast('Skill deleted','ok');\n loadSkills();\n }catch(e){ toast('Delete failed','err'); }\n}\nfunction showAddSkill(){\n var wrap = document.getElementById('skillAddWrap');\n wrap.style.display = '';\n var zone = document.getElementById('skillDropZone');\n zone.classList.remove('drag-over','uploading');\n var status = document.getElementById('skillUploadStatus');\n status.style.display = 'none';\n status.textContent = '';\n}\nfunction hideAddSkill(){\n var wrap = document.getElementById('skillAddWrap');\n wrap.style.display = 'none';\n var zone = document.getElementById('skillDropZone');\n zone.classList.remove('drag-over','uploading');\n}\nfunction initSkillDropZone(){\n var zone = document.getElementById('skillDropZone');\n var fileInput = document.getElementById('skillFolderInput');\n if(!zone || !fileInput) return;\n\n zone.addEventListener('dragover', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragenter', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragleave', function(e){ e.preventDefault(); zone.classList.remove('drag-over'); });\n zone.addEventListener('drop', function(e){\n e.preventDefault();\n zone.classList.remove('drag-over');\n var items = e.dataTransfer && e.dataTransfer.items;\n if(!items || items.length === 0) return;\n var entry = null;\n for(var i = 0; i < items.length; i++){\n var item = items[i];\n if(item.webkitGetAsEntry){\n var ent = item.webkitGetAsEntry();\n if(ent && ent.isDirectory){ entry = ent; break; }\n }\n }\n if(!entry){\n toast('Please drop a folder, not individual files','err');\n return;\n }\n var folderName = entry.name;\n traverseDirectory(entry, '').then(function(files){\n uploadSkillFolder(folderName, files);\n });\n });\n\n zone.addEventListener('click', function(e){\n if(e.target.tagName === 'BUTTON') return;\n fileInput.click();\n });\n\n fileInput.addEventListener('change', function(){\n if(!fileInput.files || fileInput.files.length === 0) return;\n var first = fileInput.files[0];\n var parts = first.webkitRelativePath.split('/');\n var folderName = parts[0] || 'skill';\n var collected = [];\n for(var i = 0; i < fileInput.files.length; i++){\n var f = fileInput.files[i];\n var rel = f.webkitRelativePath.split('/').slice(1).join('/');\n if(rel) collected.push({ file: f, relativePath: rel });\n }\n uploadSkillFolder(folderName, collected);\n fileInput.value = '';\n });\n}\n\nasync function uploadSkillFolder(folderName, files){\n var zone = document.getElementById('skillDropZone');\n var status = document.getElementById('skillUploadStatus');\n zone.classList.add('uploading');\n status.style.display = '';\n status.innerHTML = '<span class=\"spinner\" style=\"display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px\"></span> Uploading ' + files.length + ' file(s)...';\n\n var fd = new FormData();\n fd.append('folderName', folderName);\n for(var i = 0; i < files.length; i++){\n fd.append('files', files[i].file);\n fd.append('paths', files[i].relativePath);\n }\n\n try{\n var res = await fetch(API+'/skills/upload', {method:'POST', body:fd, headers:{'X-CSRF-Token':_csrfToken}});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Upload failed','err'); zone.classList.remove('uploading'); status.style.display='none'; return; }\n hideAddSkill();\n loadSkills();\n showSkillUploadedModal(data.name);\n }catch(e){\n toast('Upload failed','err');\n zone.classList.remove('uploading');\n status.style.display = 'none';\n }\n}\nfunction showSkillUploadedModal(name){\n document.getElementById('skillUploadedName').textContent = name;\n document.getElementById('skillUploadedModal').classList.add('open');\n}\nfunction closeSkillUploadedModal(){\n document.getElementById('skillUploadedModal').classList.remove('open');\n}\n\n/* ---- Skill Editor (Monaco) ---- */\nvar _skillEditor = null;\nvar _skillEditFolder = '';\nasync function openSkillEditor(folder){\n _skillEditFolder = folder;\n document.getElementById('skillEditPath').textContent = folder + '/SKILL.md';\n document.getElementById('skillEditModal').classList.add('open');\n await ensureMonaco();\n var wrap = document.getElementById('skillEditorWrap');\n if(!_skillEditor){\n _skillEditor = createEditor(wrap, { readOnly: false });\n }\n _skillEditor.setValue('Loading...');\n try{\n var data = await fetchAPI('/skills/file?folder='+encodeURIComponent(folder));\n _skillEditor.setValue(data.content);\n _skillEditor.revealLine(1);\n _skillEditor.focus();\n }catch(e){\n _skillEditor.setValue('Error loading file: '+e);\n }\n}\nfunction closeSkillEditModal(){\n document.getElementById('skillEditModal').classList.remove('open');\n _skillEditFolder = '';\n _bundledSkillEditFolder = '';\n}\nasync function saveSkillEdit(){\n if(_bundledSkillEditFolder){ saveBundledSkillEdit(); return; }\n if(!_skillEditFolder || !_skillEditor) return;\n var content = _skillEditor.getValue();\n try{\n var res = await fetch(API+'/skills/file', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({folder:_skillEditFolder, content:content})});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Save failed','err'); return; }\n toast('Skill saved','ok');\n loadSkills();\n }catch(e){ toast('Save failed','err'); }\n}\n\n/* ---- Bundled Skills ---- */\nvar _bundledSkillEditFolder = '';\nasync function loadBundledSkills(installedFolders){\n try{\n var bundled = await fetchAPI('/skills/bundled');\n var container = document.getElementById('bundledSkillsList');\n var empty = document.getElementById('bundledSkillsEmpty');\n if(!bundled || bundled.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n } else {\n empty.style.display = 'none';\n var html = '<table class=\"tbl\"><thead><tr><th>Name</th><th>Description</th><th></th></tr></thead><tbody>';\n for(var i = 0; i < bundled.length; i++){\n var bk = bundled[i];\n var isInstalled = installedFolders.indexOf(bk.folder) !== -1;\n html += '<tr>';\n html += '<td style=\"font-weight:600\">' + esc(bk.name) + '</td>';\n html += '<td style=\"color:var(--text-muted)\">' + esc(bk.description || '\\u2014') + '</td>';\n html += '<td style=\"text-align:right;white-space:nowrap\">';\n html += '<button class=\"btn-ghost btn-sm\" data-edit-bundled=\"'+esc(bk.folder)+'\" style=\"margin-right:4px\">Edit</button>';\n if(isInstalled){\n html += '<button class=\"btn-ghost btn-sm\" disabled style=\"opacity:0.5\">Installed</button>';\n } else {\n html += '<button class=\"btn btn-sm\" data-use-bundled=\"'+esc(bk.folder)+'\">Use</button>';\n }\n html += '</td></tr>';\n }\n html += '</tbody></table>';\n container.innerHTML = html;\n container.querySelectorAll('[data-use-bundled]').forEach(function(btn){\n btn.addEventListener('click', function(){ useBundledSkill(btn.dataset.useBundled); });\n });\n container.querySelectorAll('[data-edit-bundled]').forEach(function(btn){\n btn.addEventListener('click', function(){ openBundledSkillEditor(btn.dataset.editBundled); });\n });\n }\n }catch(e){ toast('Failed to load bundled skills','err'); }\n}\nasync function useBundledSkill(folder){\n try{\n var res = await fetch(API+'/skills/use-bundled', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({folder:folder})});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Install failed','err'); return; }\n toast('Skill activated','ok');\n loadSkills();\n }catch(e){ toast('Install failed','err'); }\n}\nasync function openBundledSkillEditor(folder){\n _bundledSkillEditFolder = folder;\n document.getElementById('skillEditPath').textContent = 'bundled/' + folder + '/SKILL.md';\n document.getElementById('skillEditModal').classList.add('open');\n await ensureMonaco();\n var wrap = document.getElementById('skillEditorWrap');\n if(!_skillEditor){\n _skillEditor = createEditor(wrap, { readOnly: false });\n }\n _skillEditor.setValue('Loading...');\n try{\n var data = await fetchAPI('/skills/bundled/file?folder='+encodeURIComponent(folder));\n _skillEditor.setValue(data.content);\n _skillEditor.revealLine(1);\n _skillEditor.focus();\n }catch(e){\n _skillEditor.setValue('Error loading file: '+e);\n }\n}\nasync function saveBundledSkillEdit(){\n if(!_bundledSkillEditFolder || !_skillEditor) return;\n var content = _skillEditor.getValue();\n try{\n var res = await fetch(API+'/skills/bundled/file', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({folder:_bundledSkillEditFolder, content:content})});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Save failed','err'); return; }\n toast('Bundled skill saved','ok');\n _bundledSkillEditFolder = '';\n loadSkills();\n }catch(e){ toast('Save failed','err'); }\n}\n\n/* ---- Commands ---- */\nfunction applyCommandsFilter(){\n var filter = (document.getElementById('commandsFilter').value||'').toLowerCase();\n var rows = document.querySelectorAll('#commandsList tbody tr');\n rows.forEach(function(row){\n var text = row.textContent.toLowerCase();\n row.style.display = (!filter || text.indexOf(filter) !== -1) ? '' : 'none';\n });\n}\nvar _commandDropZoneInited = false;\nasync function loadCommands(){\n try{\n var cmds = await fetchAPI('/commands');\n var container = document.getElementById('commandsList');\n var empty = document.getElementById('commandsEmpty');\n if(!cmds || cmds.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n } else {\n empty.style.display = 'none';\n cmds.sort(function(a,b){ return (a.name||'').localeCompare(b.name||''); });\n var html = '<table class=\"tbl\"><thead><tr><th>Name</th><th>Description</th><th>Model</th><th>File</th><th></th></tr></thead><tbody>';\n var lastFolder = null;\n for(var i = 0; i < cmds.length; i++){\n var cmd = cmds[i];\n if(cmd.folder && cmd.folder !== lastFolder){\n lastFolder = cmd.folder;\n html += '<tr><td colspan=\"5\" style=\"padding:10px 8px 4px;font-weight:600;font-size:13px;color:var(--text-muted);border-bottom:1px solid var(--border);background:var(--bg-secondary)\">';\n html += '<div style=\"display:flex;justify-content:space-between;align-items:center\">';\n html += '<span><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"vertical-align:-2px;margin-right:4px\"><path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\"/></svg>';\n html += esc(cmd.folder) + '/</span>';\n html += '<button class=\"btn-ghost btn-sm\" data-download-command-folder=\"'+esc(cmd.folder)+'\" style=\"font-size:12px\">Download</button>';\n html += '</div></td></tr>';\n } else if(!cmd.folder && lastFolder !== ''){\n lastFolder = '';\n }\n html += '<tr>';\n html += '<td style=\"font-weight:600' + (cmd.folder ? ';padding-left:24px' : '') + '\">' + esc(cmd.name) + '</td>';\n html += '<td style=\"color:var(--text-muted)\">' + esc(cmd.description || '\\u2014') + '</td>';\n html += '<td>' + (cmd.model ? '<code style=\"font-size:12px\">' + esc(cmd.model) + '</code>' : '<span style=\"color:var(--text-muted)\">\\u2014</span>') + '</td>';\n html += '<td style=\"max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help\" title=\"' + esc(cmd.file) + '\"><code style=\"font-size:12px\">' + esc(cmd.file) + '</code></td>';\n html += '<td style=\"text-align:right;white-space:nowrap\">';\n html += '<button class=\"btn-ghost btn-sm\" data-edit-command=\"'+esc(cmd.file)+'\" style=\"margin-right:4px\">Edit</button>';\n if(!cmd.folder) html += '<button class=\"btn-ghost btn-sm\" data-download-command-file=\"'+esc(cmd.file)+'\" style=\"margin-right:4px\">Download</button>';\n html += '<button class=\"btn-danger btn-sm\" data-delete-command=\"'+esc(cmd.file)+'\">Delete</button></td>';\n html += '</tr>';\n }\n html += '</tbody></table>';\n container.innerHTML = html;\n container.querySelectorAll('[data-delete-command]').forEach(function(btn){\n btn.addEventListener('click', function(){ confirmDeleteCommand(btn.dataset.deleteCommand); });\n });\n container.querySelectorAll('[data-download-command-folder]').forEach(function(btn){\n btn.addEventListener('click', function(){ downloadComponent(API+'/commands/download/'+encodeURIComponent(btn.dataset.downloadCommandFolder)); });\n });\n container.querySelectorAll('[data-download-command-file]').forEach(function(btn){\n btn.addEventListener('click', function(){ downloadComponent(API+'/commands/download-file?path='+encodeURIComponent(btn.dataset.downloadCommandFile)); });\n });\n container.querySelectorAll('[data-edit-command]').forEach(function(btn){\n btn.addEventListener('click', function(){ openCommandEditor(btn.dataset.editCommand); });\n });\n }\n if(!_commandDropZoneInited){ initCommandDropZone(); _commandDropZoneInited = true; }\n }catch(e){ toast('Failed to load commands','err'); }\n}\nvar _commandDeleteFile = '';\nfunction confirmDeleteCommand(filePath){\n _commandDeleteFile = filePath;\n document.getElementById('commandDeleteName').textContent = filePath;\n document.getElementById('commandDeleteModal').classList.add('open');\n}\nfunction closeCommandDeleteModal(){\n document.getElementById('commandDeleteModal').classList.remove('open');\n _commandDeleteFile = '';\n}\nasync function doDeleteCommand(){\n if(!_commandDeleteFile) return;\n var delPath = _commandDeleteFile;\n closeCommandDeleteModal();\n try{\n var res = await fetch(API+'/commands/delete', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({path:delPath})});\n if(!res.ok){ var d=await res.json(); toast(d.error||'Delete failed','err'); return; }\n toast('Command deleted','ok');\n loadCommands();\n }catch(e){ toast('Delete failed','err'); }\n}\nfunction showAddCommand(){\n var wrap = document.getElementById('commandAddWrap');\n wrap.style.display = '';\n var zone = document.getElementById('commandDropZone');\n zone.classList.remove('drag-over','uploading');\n var status = document.getElementById('commandUploadStatus');\n status.style.display = 'none';\n status.textContent = '';\n}\nfunction hideAddCommand(){\n var wrap = document.getElementById('commandAddWrap');\n wrap.style.display = 'none';\n var zone = document.getElementById('commandDropZone');\n zone.classList.remove('drag-over','uploading');\n}\nfunction initCommandDropZone(){\n var zone = document.getElementById('commandDropZone');\n var fileInput = document.getElementById('commandFolderInput');\n if(!zone || !fileInput) return;\n\n zone.addEventListener('dragover', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragenter', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragleave', function(e){ e.preventDefault(); zone.classList.remove('drag-over'); });\n zone.addEventListener('drop', function(e){\n e.preventDefault();\n zone.classList.remove('drag-over');\n var items = e.dataTransfer && e.dataTransfer.items;\n if(!items || items.length === 0) return;\n var entry = null;\n for(var i = 0; i < items.length; i++){\n var item = items[i];\n if(item.webkitGetAsEntry){\n var ent = item.webkitGetAsEntry();\n if(ent && ent.isDirectory){ entry = ent; break; }\n }\n }\n if(!entry){\n toast('Please drop a folder, not individual files','err');\n return;\n }\n var folderName = entry.name;\n traverseDirectory(entry, '').then(function(files){\n uploadCommandFolder(folderName, files);\n });\n });\n\n zone.addEventListener('click', function(e){\n if(e.target.tagName === 'BUTTON') return;\n fileInput.click();\n });\n\n fileInput.addEventListener('change', function(){\n if(!fileInput.files || fileInput.files.length === 0) return;\n var first = fileInput.files[0];\n var parts = first.webkitRelativePath.split('/');\n var folderName = parts[0] || 'command';\n var collected = [];\n for(var i = 0; i < fileInput.files.length; i++){\n var f = fileInput.files[i];\n var rel = f.webkitRelativePath.split('/').slice(1).join('/');\n if(rel) collected.push({ file: f, relativePath: rel });\n }\n uploadCommandFolder(folderName, collected);\n fileInput.value = '';\n });\n}\n\nasync function uploadCommandFolder(folderName, files){\n var zone = document.getElementById('commandDropZone');\n var status = document.getElementById('commandUploadStatus');\n zone.classList.add('uploading');\n status.style.display = '';\n status.innerHTML = '<span class=\"spinner\" style=\"display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px\"></span> Uploading ' + files.length + ' file(s)...';\n\n var fd = new FormData();\n fd.append('folderName', folderName);\n for(var i = 0; i < files.length; i++){\n fd.append('files', files[i].file);\n fd.append('paths', files[i].relativePath);\n }\n\n try{\n var res = await fetch(API+'/commands/upload', {method:'POST', body:fd, headers:{'X-CSRF-Token':_csrfToken}});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Upload failed','err'); zone.classList.remove('uploading'); status.style.display='none'; return; }\n hideAddCommand();\n loadCommands();\n showCommandUploadedModal(data.name);\n }catch(e){\n toast('Upload failed','err');\n zone.classList.remove('uploading');\n status.style.display = 'none';\n }\n}\nfunction showCommandUploadedModal(name){\n document.getElementById('commandUploadedName').textContent = name;\n document.getElementById('commandUploadedModal').classList.add('open');\n}\nfunction closeCommandUploadedModal(){\n document.getElementById('commandUploadedModal').classList.remove('open');\n}\n\n/* ---- Standalone Command Create ---- */\nfunction showAddStandaloneCommand(){\n var wrap = document.getElementById('commandCreateWrap');\n wrap.style.display = '';\n document.getElementById('cmdCreateName').value = '';\n document.getElementById('cmdCreateDesc').value = '';\n document.getElementById('cmdCreateModel').value = '';\n}\nfunction hideAddStandaloneCommand(){\n document.getElementById('commandCreateWrap').style.display = 'none';\n}\nasync function doCreateStandaloneCommand(){\n var name = document.getElementById('cmdCreateName').value.trim();\n if(!name){ toast('Name is required','err'); return; }\n if(!/^[a-zA-Z0-9_-]+$/.test(name)){ toast('Name can only contain letters, digits, hyphens, and underscores','err'); return; }\n var desc = document.getElementById('cmdCreateDesc').value.trim();\n var model = document.getElementById('cmdCreateModel').value.trim();\n try{\n var res = await fetch(API+'/commands/create', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({name:name, description:desc, model:model})});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Create failed','err'); return; }\n hideAddStandaloneCommand();\n toast('Command created','ok');\n loadCommands();\n openCommandEditor(data.file);\n }catch(e){ toast('Create failed','err'); }\n}\n\n/* ---- Command Editor (Monaco) ---- */\nvar _commandEditor = null;\nvar _commandEditPath = '';\nasync function openCommandEditor(filePath){\n _commandEditPath = filePath;\n document.getElementById('commandEditPath').textContent = filePath;\n document.getElementById('commandEditModal').classList.add('open');\n await ensureMonaco();\n var wrap = document.getElementById('commandEditorWrap');\n if(!_commandEditor){\n _commandEditor = createEditor(wrap, { readOnly: false });\n }\n _commandEditor.setValue('Loading...');\n try{\n var data = await fetchAPI('/commands/file?path='+encodeURIComponent(filePath));\n _commandEditor.setValue(data.content);\n _commandEditor.revealLine(1);\n _commandEditor.focus();\n }catch(e){\n _commandEditor.setValue('Error loading file: '+e);\n }\n}\nfunction closeCommandEditModal(){\n document.getElementById('commandEditModal').classList.remove('open');\n _commandEditPath = '';\n}\nasync function saveCommandEdit(){\n if(!_commandEditPath || !_commandEditor) return;\n var content = _commandEditor.getValue();\n try{\n var res = await fetch(API+'/commands/file', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({path:_commandEditPath, content:content})});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Save failed','err'); return; }\n toast('Command saved','ok');\n loadCommands();\n }catch(e){ toast('Save failed','err'); }\n}\n\n/* ---- Plugins ---- */\nvar pluginInfoCache = [];\nvar _pluginDropZoneInited = false;\nasync function loadPlugins(){\n currentConfig = await fetchAPI('/config');\n try{ pluginInfoCache = await fetchAPI('/plugins/info'); }catch(e){ pluginInfoCache = []; }\n renderPluginCards();\n if(!_pluginDropZoneInited){ initPluginDropZone(); _pluginDropZoneInited = true; }\n}\nfunction renderPluginCards(){\n var plugins = (currentConfig && currentConfig.agent && currentConfig.agent.plugins) || [];\n var container = document.getElementById('pluginCards');\n var empty = document.getElementById('pluginEmpty');\n if(plugins.length === 0){\n container.innerHTML = '';\n empty.style.display = '';\n return;\n }\n empty.style.display = 'none';\n var pOrder = [];\n for(var k=0;k<plugins.length;k++) pOrder.push(k);\n pOrder.sort(function(a,b){ return (plugins[a].name||'').localeCompare(plugins[b].name||''); });\n var html = '<table class=\"tbl\"><thead><tr><th>Name</th><th>Description</th><th>Path</th><th>Status</th><th>Enabled</th><th></th></tr></thead><tbody>';\n for(var pi = 0; pi < pOrder.length; pi++){\n var i = pOrder[pi];\n var p = plugins[i];\n var info = pluginInfoCache.find(function(x){ return x.index === i; });\n var displayName = (info && info.name) ? info.name : p.name;\n var desc = (info && info.description) ? info.description : (p.description || '');\n var valid = info ? info.valid : false;\n html += '<tr' + (valid ? '' : ' style=\"opacity:0.6\"') + '>';\n html += '<td style=\"font-weight:600\">' + esc(displayName) + '</td>';\n html += '<td style=\"color:var(--text-muted);font-size:13px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\" title=\"' + esc(desc) + '\">' + esc(desc || '\\u2014') + '</td>';\n html += '<td style=\"max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help\" title=\"' + esc(p.path) + '\"><code style=\"font-size:12px\">' + esc(p.path) + '</code></td>';\n html += '<td style=\"text-align:center\">' + (valid ? '<span class=\"badge badge-green\">valid</span>' : '<span class=\"badge badge-red\">invalid</span>') + '</td>';\n html += '<td><label class=\"toggle\"><input type=\"checkbox\" data-plugin-toggle=\"'+i+'\" '+(p.enabled && valid?'checked':'') + (valid?'':' disabled')+'><span></span></label></td>';\n html += '<td style=\"text-align:right;white-space:nowrap\"><button class=\"btn-ghost btn-sm\" data-download-plugin=\"'+i+'\" style=\"margin-right:4px\">Download</button><button class=\"btn-danger btn-sm\" data-delete-plugin=\"'+i+'\">Delete</button></td>';\n html += '</tr>';\n // Auto-disable invalid plugins in config\n if(!valid && p.enabled){\n p.enabled = false;\n markDirty();\n }\n }\n html += '</tbody></table>';\n container.innerHTML = html;\n container.querySelectorAll('[data-plugin-toggle]').forEach(function(cb){\n cb.addEventListener('change', function(){\n var idx = parseInt(cb.dataset.pluginToggle);\n if(currentConfig && currentConfig.agent && currentConfig.agent.plugins){\n currentConfig.agent.plugins[idx].enabled = cb.checked;\n markDirty();\n }\n });\n });\n container.querySelectorAll('[data-delete-plugin]').forEach(function(btn){\n btn.addEventListener('click', function(){ confirmDeletePlugin(parseInt(btn.dataset.deletePlugin)); });\n });\n container.querySelectorAll('[data-download-plugin]').forEach(function(btn){\n btn.addEventListener('click', function(){ downloadComponent(API+'/plugins/download/'+btn.dataset.downloadPlugin); });\n });\n}\nfunction showAddPlugin(){\n var wrap = document.getElementById('pluginAddWrap');\n wrap.style.display = '';\n var zone = document.getElementById('pluginDropZone');\n zone.classList.remove('drag-over','uploading');\n var status = document.getElementById('pluginUploadStatus');\n status.style.display = 'none';\n status.textContent = '';\n // Pre-fill destination path\n var pathInput = document.getElementById('pluginDestPath');\n var ws = (currentConfig && currentConfig.agent && currentConfig.agent.workspacePath) || './workspace';\n _pluginDefaultDest = ws + '/.plugins';\n if(!pathInput.value) pathInput.value = _pluginDefaultDest;\n}\nfunction hideAddPlugin(){\n var wrap = document.getElementById('pluginAddWrap');\n wrap.style.display = 'none';\n var zone = document.getElementById('pluginDropZone');\n zone.classList.remove('drag-over','uploading');\n // Reset path for next use\n var pathInput = document.getElementById('pluginDestPath');\n if(pathInput) pathInput.value = '';\n}\n\nvar _pluginDefaultDest = '';\nasync function verifyPluginDest(){\n var pathInput = document.getElementById('pluginDestPath');\n var path = pathInput.value.trim();\n if(!path){ toast('Path is empty','err'); pathInput.value = _pluginDefaultDest; return; }\n try{\n var res = await fetch(API+'/plugins/verify-path', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({path:path})});\n var data = await res.json();\n if(data && data.ok){\n pathInput.value = data.path;\n toast('Path verified','ok');\n } else {\n toast((data && data.error) || 'Path does not exist','err');\n pathInput.value = _pluginDefaultDest;\n }\n }catch(e){ toast('Verification failed','err'); pathInput.value = _pluginDefaultDest; }\n}\n\n// Attach drop zone events after DOM ready\nfunction initPluginDropZone(){\n var zone = document.getElementById('pluginDropZone');\n var fileInput = document.getElementById('pluginFolderInput');\n if(!zone || !fileInput) return;\n\n zone.addEventListener('dragover', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragenter', function(e){ e.preventDefault(); zone.classList.add('drag-over'); });\n zone.addEventListener('dragleave', function(e){ e.preventDefault(); zone.classList.remove('drag-over'); });\n zone.addEventListener('drop', function(e){\n e.preventDefault();\n zone.classList.remove('drag-over');\n var items = e.dataTransfer && e.dataTransfer.items;\n if(!items || items.length === 0) return;\n // Find the first directory entry\n var entry = null;\n for(var i = 0; i < items.length; i++){\n var item = items[i];\n if(item.webkitGetAsEntry){\n var ent = item.webkitGetAsEntry();\n if(ent && ent.isDirectory){ entry = ent; break; }\n }\n }\n if(!entry){\n toast('Please drop a folder, not individual files','err');\n return;\n }\n var folderName = entry.name;\n traverseDirectory(entry, '').then(function(files){\n uploadPluginFolder(folderName, files);\n });\n });\n\n zone.addEventListener('click', function(e){\n if(e.target.tagName === 'BUTTON') return;\n fileInput.click();\n });\n\n fileInput.addEventListener('change', function(){\n if(!fileInput.files || fileInput.files.length === 0) return;\n // Derive folder name from webkitRelativePath (first segment)\n var first = fileInput.files[0];\n var parts = first.webkitRelativePath.split('/');\n var folderName = parts[0] || 'plugin';\n var collected = [];\n for(var i = 0; i < fileInput.files.length; i++){\n var f = fileInput.files[i];\n // webkitRelativePath = \"folderName/sub/file.js\" — strip the first segment\n var rel = f.webkitRelativePath.split('/').slice(1).join('/');\n if(rel) collected.push({ file: f, relativePath: rel });\n }\n uploadPluginFolder(folderName, collected);\n fileInput.value = '';\n });\n}\n\nfunction traverseDirectory(entry, basePath){\n return new Promise(function(resolve){\n if(entry.isFile){\n entry.file(function(file){\n resolve([{ file: file, relativePath: basePath ? basePath + '/' + entry.name : entry.name }]);\n });\n } else if(entry.isDirectory){\n var reader = entry.createReader();\n var allEntries = [];\n // readEntries may return partial results, so read until empty\n (function readAll(){\n reader.readEntries(function(entries){\n if(entries.length === 0){\n var promises = [];\n for(var i = 0; i < allEntries.length; i++){\n var childPath = basePath ? basePath + '/' + entry.name : entry.name;\n promises.push(traverseDirectory(allEntries[i], childPath));\n }\n Promise.all(promises).then(function(results){\n var flat = [];\n for(var j = 0; j < results.length; j++){\n for(var k = 0; k < results[j].length; k++) flat.push(results[j][k]);\n }\n resolve(flat);\n });\n } else {\n for(var i = 0; i < entries.length; i++) allEntries.push(entries[i]);\n readAll();\n }\n });\n })();\n } else {\n resolve([]);\n }\n });\n}\n\nasync function uploadPluginFolder(folderName, files){\n var zone = document.getElementById('pluginDropZone');\n var status = document.getElementById('pluginUploadStatus');\n var pathInput = document.getElementById('pluginDestPath');\n var basePath = (pathInput && pathInput.value.trim()) || '';\n zone.classList.add('uploading');\n status.style.display = '';\n status.innerHTML = '<span class=\"spinner\" style=\"display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px\"></span> Uploading ' + files.length + ' file(s)...';\n\n var fd = new FormData();\n fd.append('folderName', folderName);\n if(basePath) fd.append('basePath', basePath);\n for(var i = 0; i < files.length; i++){\n fd.append('files', files[i].file);\n fd.append('paths', files[i].relativePath);\n }\n\n try{\n var res = await fetch(API+'/plugins/upload', {method:'POST', body:fd, headers:{'X-CSRF-Token':_csrfToken}});\n var data = await res.json();\n if(!res.ok){ toast(data.error||'Upload failed','err'); zone.classList.remove('uploading'); status.style.display='none'; return; }\n\n if(!currentConfig){ currentConfig = await fetchAPI('/config'); }\n if(!currentConfig.agent) currentConfig.agent = {};\n if(!currentConfig.agent.plugins) currentConfig.agent.plugins = [];\n currentConfig.agent.plugins.push({ name: data.name, path: data.path, description: data.description, enabled: false });\n hideAddPlugin();\n renderPluginCards();\n await saveConfig();\n showPluginUploadedModal(data.name);\n }catch(e){\n toast('Upload failed','err');\n zone.classList.remove('uploading');\n status.style.display = 'none';\n }\n}\nfunction showPluginUploadedModal(name){\n document.getElementById('pluginUploadedName').textContent = name;\n document.getElementById('pluginUploadedModal').classList.add('open');\n}\nfunction closePluginUploadedModal(){\n document.getElementById('pluginUploadedModal').classList.remove('open');\n}\n\nvar _pluginDeleteIdx = -1;\nfunction confirmDeletePlugin(idx){\n _pluginDeleteIdx = idx;\n var plugins = (currentConfig && currentConfig.agent && currentConfig.agent.plugins) || [];\n var p = plugins[idx];\n document.getElementById('pluginDeleteName').textContent = p ? p.name : '';\n document.getElementById('pluginDeleteModal').classList.add('open');\n}\nfunction closePluginDeleteModal(){\n document.getElementById('pluginDeleteModal').classList.remove('open');\n _pluginDeleteIdx = -1;\n}\nasync function doDeletePlugin(){\n if(_pluginDeleteIdx < 0) return;\n if(!currentConfig || !currentConfig.agent || !currentConfig.agent.plugins) return;\n var p = currentConfig.agent.plugins[_pluginDeleteIdx];\n // Delete folder from filesystem\n if(p && p.path){\n try{\n await fetch(API+'/plugins/delete-folder', {method:'POST', credentials:'include', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, body:JSON.stringify({path:p.path})});\n }catch(e){}\n }\n currentConfig.agent.plugins.splice(_pluginDeleteIdx, 1);\n closePluginDeleteModal();\n renderPluginCards();\n saveConfig();\n}\n\n/* ---- Download helper ---- */\nfunction downloadComponent(url){\n var a = document.createElement('a');\n a.href = url;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** Browser JS for gatherConfig(), saveConfig(), markEnvRefs(), STT, Memory, Settings, key management. */
2
+ export declare function configJS(): string;
3
+ //# sourceMappingURL=ui-js-config.d.ts.map
@@ -0,0 +1 @@
1
+ export function configJS(){return"\n/* ---- Env var ref markers ---- */\nfunction markEnvRefs(container){\n if(!container) return;\n container.querySelectorAll('input').forEach(function(inp){\n if(/^\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$/.test(inp.value)){\n inp.classList.add('env-ref');\n inp.title='Stored as environment variable in .env';\n } else {\n inp.classList.remove('env-ref');\n inp.title='';\n }\n });\n}\n\n/* ---- STT ---- */\nasync function loadSTT(){\n currentConfig = await fetchAPI('/config');\n const s = currentConfig.stt||{};\n document.getElementById('sttEnabled').checked = !!s.enabled;\n document.getElementById('sttProvider').value = s.provider||'openai-whisper';\n const oai = s['openai-whisper']||{};\n populateSTTModelSelect();\n document.getElementById('sttModelRef').value = oai.modelRef||'';\n document.getElementById('sttOAILang').value = oai.language||'';\n const loc = s['local-whisper']||{};\n document.getElementById('sttLocalBin').value = loc.binaryPath||'whisper';\n document.getElementById('sttLocalModel').value = loc.model||'base';\n updateSTTFields();\n if(!loadSTT._bound){\n document.getElementById('sttProvider').addEventListener('change', updateSTTFields);\n loadSTT._bound = true;\n }\n sectionsLoaded.stt = true;\n}\nfunction updateSTTFields(){\n const prov = document.getElementById('sttProvider').value;\n document.getElementById('sttOpenAI').style.display = prov==='openai-whisper'?'':'none';\n document.getElementById('sttLocal').style.display = prov==='local-whisper'?'':'none';\n}\n\n/* ---- Memory ---- */\nasync function loadMemory(){\n currentConfig = await fetchAPI('/config');\n const m = currentConfig.memory||{};\n document.getElementById('memEnabled').checked = !!m.enabled;\n document.getElementById('memDir').value = m.dir||'./memory';\n document.getElementById('memStrategy').value = m.recallStrategy||'builtin-only';\n // Search settings\n const s = m.search||{};\n populateMemSearchModelSelect();\n document.getElementById('memSearchModelRef').value = s.modelRef||'';\n document.getElementById('memSearchEmbModel').value = s.embeddingModel||'text-embedding-3-small';\n document.getElementById('memSearchPrefixQuery').value = s.prefixQuery||'';\n document.getElementById('memSearchPrefixDocument').value = s.prefixDocument||'';\n var dimsVal = String(s.embeddingDimensions||1536);\n document.getElementById('memSearchDims').value = dimsVal;\n document.getElementById('memSearchDimsValue').textContent = dimsVal;\n document.getElementById('memSearchMaxResults').value = s.maxResults||6;\n document.getElementById('memSearchDebounce').value = s.updateDebounceMs||3000;\n document.getElementById('memSearchEmbedInterval').value = s.embedIntervalMs||300000;\n document.getElementById('memSearchMaxSnippet').value = s.maxSnippetChars||700;\n document.getElementById('memSearchMaxInjected').value = s.maxInjectedChars||4000;\n document.getElementById('memSearchRrfK').value = s.rrfK||60;\n updateMemSearchFields();\n // Intercept toggle-off\n document.getElementById('memEnabled').addEventListener('change', function(){\n if(!this.checked){\n this.checked = true; // revert until confirmed\n document.getElementById('memDisableModal').classList.add('open');\n }\n });\n sectionsLoaded.memory = true;\n}\nfunction updateMemSearchFields(){\n var strategy = document.getElementById('memStrategy').value;\n document.getElementById('memSearchSettings').style.display = strategy==='search'?'':'none';\n}\nasync function testEmbedding(){\n var btn = document.getElementById('memSearchTestBtn');\n var result = document.getElementById('memSearchTestResult');\n btn.disabled = true;\n btn.textContent = 'Testing...';\n result.style.color = 'var(--text-muted)';\n result.textContent = '';\n try {\n var body = {\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536\n };\n var resp = await fetch(API+'/memory-search/test-embedding', {method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}, credentials:'include', body:JSON.stringify(body)});\n var data = await resp.json();\n if(data.ok){\n result.style.color = 'var(--success, #22c55e)';\n result.textContent = '✓ OK — model: '+data.model+', dims: '+data.dimensions+', latency: '+data.latencyMs+'ms';\n } else {\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ '+data.error;\n }\n } catch(err){\n result.style.color = 'var(--error, #ef4444)';\n result.textContent = '✗ Connection error: '+err.message;\n } finally {\n btn.disabled = false;\n btn.textContent = 'Test Embedding';\n }\n}\nfunction closeMemDisableModal(confirmed){\n document.getElementById('memDisableModal').classList.remove('open');\n if(confirmed){\n document.getElementById('memEnabled').checked = false;\n saveConfig();\n }\n}\n\n/* ---- Settings ---- */\nasync function loadSettings(){\n currentConfig = await fetchAPI('/config');\n document.getElementById('settingsHost').value = currentConfig.host || '127.0.0.1';\n document.getElementById('settingsUiPort').value = (currentConfig.nostromo && currentConfig.nostromo.port) || '3001';\n document.getElementById('settingsTimezone').value = currentConfig.timezone || '';\n document.getElementById('settingsAutoRestart').checked = !!(currentConfig.nostromo && currentConfig.nostromo.autoRestart);\n document.getElementById('settingsConfigCheckInterval').value = (currentConfig.nostromo && currentConfig.nostromo.configCheckInterval) || 5;\n sectionsLoaded.settings = true;\n}\n\n/* ---- Access key display ---- */\nvar _currentKey = '';\nvar _keyRevealed = false;\n\nfunction maskKey(key){\n // Show first 4 chars, mask the rest: \"ABCD-****-****-****\"\n if(key.length<=4) return key;\n return key.substring(0,4) + key.substring(4).replace(/[A-Za-z0-9]/g, '*');\n}\n\nasync function loadCurrentKey(){\n try{\n const res = await fetch(API+'/key');\n const data = await res.json();\n _currentKey = data.key || '';\n _keyRevealed = false;\n var row = document.getElementById('currentKeyRow');\n var el = document.getElementById('currentKeyValue');\n if(_currentKey && _currentKey !== '0000'){\n el.textContent = maskKey(_currentKey);\n row.style.display = 'flex';\n } else {\n row.style.display = 'none';\n }\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }catch(e){}\n}\n\nfunction toggleKeyVisibility(){\n _keyRevealed = !_keyRevealed;\n var el = document.getElementById('currentKeyValue');\n el.textContent = _keyRevealed ? _currentKey : maskKey(_currentKey);\n document.getElementById('keyEyeOff').style.display = _keyRevealed ? 'none' : '';\n document.getElementById('keyEyeOn').style.display = _keyRevealed ? '' : 'none';\n}\n\nasync function copyKey(){\n try{\n await navigator.clipboard.writeText(_currentKey);\n document.getElementById('keyCopyIcon').style.display = 'none';\n document.getElementById('keyCheckIcon').style.display = '';\n setTimeout(function(){\n document.getElementById('keyCopyIcon').style.display = '';\n document.getElementById('keyCheckIcon').style.display = 'none';\n }, 1500);\n }catch(e){ toast('Copy failed','err'); }\n}\n\n/* ---- Regenerate key ---- */\nfunction confirmRegenKey(){\n document.getElementById('regenKeyModal').classList.add('open');\n}\nfunction closeRegenKeyModal(){\n document.getElementById('regenKeyModal').classList.remove('open');\n}\nasync function regenKey(){\n try{\n const res = await fetch(API+'/key/regenerate',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n const data = await res.json();\n if(data.key){\n showNewKey(data.key);\n _currentKey = data.key;\n _keyRevealed = false;\n var el = document.getElementById('currentKeyValue');\n el.textContent = maskKey(data.key);\n document.getElementById('currentKeyRow').style.display = 'flex';\n document.getElementById('keyEyeOff').style.display = '';\n document.getElementById('keyEyeOn').style.display = 'none';\n }\n }catch(e){ toast('Failed','err'); }\n}\n\n/* ---- Save config ---- */\nfunction gatherConfig(){\n if(!currentConfig){ toast('Config not loaded yet — cannot save','err'); return null; }\n const cfg = JSON.parse(JSON.stringify(currentConfig));\n // Verbose debug logs\n var logVerboseEl = document.getElementById('logVerbose');\n if(logVerboseEl) cfg.verboseDebugLogs = logVerboseEl.checked;\n // Channels\n const wrap = document.getElementById('channelCards');\n if(wrap){\n if(!cfg.channels) cfg.channels={};\n for(const ch of CHANNEL_LIST){\n const toggle = wrap.querySelector('[data-ch-toggle=\"'+ch+'\"]');\n if(!toggle) continue;\n if(!cfg.channels[ch]) cfg.channels[ch]={};\n cfg.channels[ch].enabled = toggle.checked;\n }\n // Collect all channel fields via data-ch-field=\"channel.account.key\" or \"channel.key\"\n wrap.querySelectorAll('[data-ch-field]').forEach(inp=>{\n const parts = inp.dataset.chField.split('.');\n const chName = parts[0];\n if(!cfg.channels[chName]) cfg.channels[chName]={};\n if(parts.length === 3){\n // channel.account.key\n const [, acct, key] = parts;\n if(!cfg.channels[chName].accounts) cfg.channels[chName].accounts={};\n if(!cfg.channels[chName].accounts[acct]) cfg.channels[chName].accounts[acct]={};\n var val;\n if(key==='allowFrom') val = inp.value.split(',').map(function(s){return s.trim()}).filter(Boolean);\n else if(inp.type==='number') val = parseInt(inp.value)||0;\n else val = inp.value;\n cfg.channels[chName].accounts[acct][key] = val;\n } else if(parts.length === 2){\n // channel.key (e.g. responses.port)\n const key = parts[1];\n cfg.channels[chName][key] = inp.type==='number' ? (parseInt(inp.value)||0) : inp.value;\n }\n });\n }\n // Models\n if(currentConfig&&currentConfig.models) cfg.models = currentConfig.models;\n // Agent — only gather from DOM if loadAgent() has populated the fields\n if(sectionsLoaded.agent){\n cfg.agent = cfg.agent||{};\n cfg.agent.model = document.getElementById('agentModel').value;\n cfg.agent.mainFallback = document.getElementById('agentMainFallback').value;\n cfg.agent.maxTurns = parseInt(document.getElementById('agentMaxTurns').value)||10;\n cfg.agent.permissionMode = document.getElementById('agentPermMode').value;\n cfg.agent.sessionTTL = parseInt(document.getElementById('agentSessionTTL').value)||3600;\n cfg.agent.settingSources = document.getElementById('agentSettingSources').value;\n cfg.agent.builtinCoderSkill = document.getElementById('agentCoderSkill').checked;\n cfg.agent.autoRenew = parseInt(document.getElementById('agentAutoRenew').value)||0;\n cfg.agent.allowedTools = [];\n document.querySelectorAll('#agentToolsGrid [data-tool]').forEach(function(cb){if(cb.checked) cfg.agent.allowedTools.push(cb.dataset.tool);});\n cfg.agent.disallowedTools = [];\n cfg.agent.queueMode = document.getElementById('agentQueueMode').value;\n cfg.agent.queueDebounceMs = parseInt(document.getElementById('agentDebounceMs').value)||0;\n cfg.agent.queueCap = parseInt(document.getElementById('agentQueueCap').value)||0;\n cfg.agent.queueDropPolicy = document.getElementById('agentDropPolicy').value;\n cfg.agent.inflightTyping = document.getElementById('agentInflightTyping').checked;\n cfg.agent.autoApproveTools = document.getElementById('agentAutoApprove').checked;\n }\n // Custom SubAgents (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.customSubAgents){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.customSubAgents = currentConfig.agent.customSubAgents;\n }\n // Plugins (preserved from currentConfig, mutated in-place by the UI)\n if(currentConfig && currentConfig.agent && currentConfig.agent.plugins){\n if(!cfg.agent) cfg.agent = {};\n cfg.agent.plugins = currentConfig.agent.plugins;\n }\n // STT — only gather from DOM if loadSTT() has populated the fields\n if(sectionsLoaded.stt){\n cfg.stt = cfg.stt||{};\n cfg.stt.enabled = document.getElementById('sttEnabled').checked;\n cfg.stt.provider = document.getElementById('sttProvider').value;\n var sttModelName = document.getElementById('sttModelRef').value;\n cfg.stt['openai-whisper'] = {\n modelRef: sttModelName,\n model: 'whisper-1',\n language: document.getElementById('sttOAILang').value\n };\n cfg.stt['local-whisper'] = {\n binaryPath: document.getElementById('sttLocalBin').value,\n model: document.getElementById('sttLocalModel').value\n };\n }\n // TTS — only gather from DOM if loadTTS() has populated the fields\n if(sectionsLoaded.tts){\n cfg.tts = cfg.tts||{};\n cfg.tts.enabled = document.getElementById('ttsEnabled').checked;\n cfg.tts.provider = document.getElementById('ttsProvider').value;\n cfg.tts.maxTextLength = parseInt(document.getElementById('ttsMaxTextLength').value)||4096;\n cfg.tts.timeoutMs = parseInt(document.getElementById('ttsTimeoutMs').value)||30000;\n cfg.tts.edge = {\n voice: document.getElementById('ttsEdgeVoice').value || 'en-US-MichelleNeural'\n };\n cfg.tts.openai = {\n modelRef: document.getElementById('ttsOAIModelRef').value,\n model: document.getElementById('ttsOAIModel').value || 'gpt-4o-mini-tts',\n voice: document.getElementById('ttsOAIVoice').value || 'alloy'\n };\n cfg.tts.elevenlabs = {\n modelRef: document.getElementById('ttsELModelRef').value,\n voiceId: document.getElementById('ttsELVoiceId').value || 'pMsXgVXv3BLzUgSXRplE',\n modelId: document.getElementById('ttsELModelId').value || 'eleven_multilingual_v2'\n };\n }\n // Memory — only gather from DOM if loadMemory() has populated the fields\n if(sectionsLoaded.memory){\n cfg.memory = cfg.memory||{};\n cfg.memory.enabled = document.getElementById('memEnabled').checked;\n cfg.memory.dir = document.getElementById('memDir').value;\n cfg.memory.recallStrategy = document.getElementById('memStrategy').value;\n cfg.memory.search = {\n enabled: cfg.memory.recallStrategy === 'search',\n modelRef: document.getElementById('memSearchModelRef').value,\n embeddingModel: document.getElementById('memSearchEmbModel').value || 'text-embedding-3-small',\n prefixQuery: document.getElementById('memSearchPrefixQuery').value || '',\n prefixDocument: document.getElementById('memSearchPrefixDocument').value || '',\n embeddingDimensions: parseInt(document.getElementById('memSearchDims').value) || 1536,\n updateDebounceMs: parseInt(document.getElementById('memSearchDebounce').value) || 3000,\n embedIntervalMs: parseInt(document.getElementById('memSearchEmbedInterval').value) || 300000,\n maxResults: parseInt(document.getElementById('memSearchMaxResults').value) || 6,\n maxSnippetChars: parseInt(document.getElementById('memSearchMaxSnippet').value) || 700,\n maxInjectedChars: parseInt(document.getElementById('memSearchMaxInjected').value) || 4000,\n rrfK: parseInt(document.getElementById('memSearchRrfK').value) || 60\n };\n }\n // Cron — only gather from DOM if loadCron() has populated the fields\n if(sectionsLoaded.cron){\n cfg.cron = cfg.cron||{};\n cfg.cron.enabled = document.getElementById('cronEnabled').checked;\n cfg.cron.isolated = document.getElementById('cronIsolated').checked;\n cfg.cron.broadcastEvents = document.getElementById('cronBroadcast').checked;\n cfg.cron.heartbeat = cfg.cron.heartbeat||{};\n cfg.cron.heartbeat.enabled = document.getElementById('hbEnabled').checked;\n cfg.cron.heartbeat.channel = document.getElementById('hbChannel').value;\n cfg.cron.heartbeat.chatId = document.getElementById('hbChatId').value;\n cfg.cron.heartbeat.every = parseInt(document.getElementById('hbEvery').value)||1800000;\n cfg.cron.heartbeat.message = document.getElementById('hbMessage').value;\n cfg.cron.heartbeat.ackMaxChars = parseInt(document.getElementById('hbAckMaxChars').value)||300;\n }\n // Settings — only gather from DOM if loadSettings() has populated the fields\n if(sectionsLoaded.settings){\n cfg.host = document.getElementById('settingsHost').value || '127.0.0.1';\n cfg.timezone = document.getElementById('settingsTimezone').value || '';\n cfg.nostromo = cfg.nostromo||{};\n cfg.nostromo.port = parseInt(document.getElementById('settingsUiPort').value)||3001;\n cfg.nostromo.autoRestart = document.getElementById('settingsAutoRestart').checked;\n cfg.nostromo.configCheckInterval = parseInt(document.getElementById('settingsConfigCheckInterval').value)||5;\n }\n return cfg;\n}\nasync function saveConfig(){\n const cfg = gatherConfig();\n if(!cfg) return;\n // Validate memory search prefix fields contain {content} if non-empty\n if(cfg.memory && cfg.memory.search){\n var pq = (cfg.memory.search.prefixQuery||'').trim();\n var pd = (cfg.memory.search.prefixDocument||'').trim();\n if(pq && pq.indexOf('{content}')===-1){ toast('Prefix Query must contain the {content} placeholder. Save aborted.','err'); return; }\n if(pd && pd.indexOf('{content}')===-1){ toast('Prefix Document must contain the {content} placeholder. Save aborted.','err'); return; }\n }\n // Block save if heartbeat is enabled but message is too short\n if(cfg.cron && cfg.cron.heartbeat && cfg.cron.heartbeat.enabled){\n var hbMsg = (cfg.cron.heartbeat.message||'').trim();\n if(hbMsg.length < 15){\n toast('Heartbeat message is too short (minimum 15 characters). Save aborted.','err');\n return;\n }\n }\n try{\n const res = await fetch(API+'/config',{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify(cfg)});\n if(!res.ok){ const d=await res.json(); toast(d.error||'Save failed','err'); return; }\n currentConfig = await fetchAPI('/config');\n clearDirty();\n toast('Configuration saved','ok');\n updateConfigCheckPolling();\n // Re-render current section with protected values from server\n var activeSec = document.querySelector('.section.active');\n if(activeSec){\n var id = activeSec.id.replace('sec-','');\n if(id==='models') renderModelsTable();\n if(id==='vars') renderVarsTable();\n }\n }catch(e){ console.error('Save failed:',e); toast('Save failed: '+(e.message||e),'err'); }\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** Core browser JS: globals, fetch wrapper, auth, navigation, theme, toast, auto-save, init. */
2
+ export declare function coreJS(isFirstRun: boolean, basePath?: string): string;
3
+ //# sourceMappingURL=ui-js-core.d.ts.map
@@ -0,0 +1 @@
1
+ export function coreJS(e,n=""){return`\nconst API = '${n}/api';\nconst IS_FIRST_RUN = ${e};\n\n// Override fetch to include credentials and CSRF token\nvar _origFetch = window.fetch;\nvar _csrfToken = sessionStorage.getItem('csrfToken') || '';\nwindow.fetch = function(url, opts) {\n opts = opts || {};\n opts.credentials = 'include';\n // Add CSRF token header on mutative requests\n if(opts.method && opts.method !== 'GET') {\n opts.headers = opts.headers || {};\n if(opts.headers instanceof Headers) {\n opts.headers.set('X-CSRF-Token', _csrfToken);\n } else {\n opts.headers['X-CSRF-Token'] = _csrfToken;\n }\n }\n return _origFetch.call(window, url, opts);\n};\nlet currentConfig = null;\nvar sectionsLoaded = {};\n\n/* ---- Helpers ---- */\nasync function fetchAPI(path){\n const res = await fetch(API+path);\n if(res.status===401){ location.reload(); return; }\n return res.json();\n}\nfunction esc(s){ const d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }\nfunction toggleHelp(btn){ const el=document.getElementById(btn.dataset.help); if(el) el.classList.toggle('open'); }\n\nfunction toast(msg,type){\n const el=document.getElementById('toast');\n el.textContent=msg;\n el.className='toast toast-'+(type||'ok')+' show';\n clearTimeout(el._t);\n el._t=setTimeout(()=>{ el.classList.remove('show'); },2500);\n}\n\n/* ---- First-run setup ---- */\nconst KEY_BOXES = [document.getElementById('k0'),document.getElementById('k1'),document.getElementById('k2'),document.getElementById('k3')];\n(function initLogin(){\n if(IS_FIRST_RUN){\n KEY_BOXES.forEach(b=>{ b.value='0000'; b.disabled=true; });\n document.getElementById('loginSubtitle').textContent='';\n document.getElementById('loginBtn').textContent='Welcome! Press to continue';\n }\n KEY_BOXES.forEach((b,i)=>{\n b.addEventListener('input',()=>{\n b.value = b.value.toUpperCase().replace(/[^A-Z0-9]/g,'');\n if(b.value.length===4 && i<3) KEY_BOXES[i+1].focus();\n });\n b.addEventListener('keydown',e=>{\n if(e.key==='Backspace' && b.value==='' && i>0){ KEY_BOXES[i-1].focus(); }\n if(e.key==='Enter') doLogin();\n });\n b.addEventListener('paste',e=>{\n e.preventDefault();\n var raw = (e.clipboardData||window.clipboardData).getData('text');\n raw = raw.trim().toUpperCase();\n raw = raw.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\uFE58\\uFE63\\uFF0D]/g,'-');\n raw = raw.replace(/-/g,'');\n raw = raw.replace(/[^A-Z0-9]/g,'');\n raw = raw.slice(0,16);\n for(var j=0;j<4;j++){\n KEY_BOXES[j].value = raw.slice(j*4, j*4+4);\n }\n });\n });\n})();\n\n/* ---- Eye toggle ---- */\nvar keyVisible = false;\nfunction toggleKeyVis(){\n keyVisible = !keyVisible;\n KEY_BOXES.forEach(b=>{ b.type = keyVisible ? 'text' : 'password'; });\n document.getElementById('eyeOn').style.display = keyVisible ? '' : 'none';\n document.getElementById('eyeOff').style.display = keyVisible ? 'none' : '';\n}\n\n/* ---- Auth ---- */\nasync function doLogin(){\n const key = IS_FIRST_RUN ? '0000' : [document.getElementById('k0').value,document.getElementById('k1').value,document.getElementById('k2').value,document.getElementById('k3').value].join('-');\n const errEl = document.getElementById('loginError');\n errEl.style.display = 'none';\n try {\n const res = await fetch(API+'/auth/login', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({key})});\n const data = await res.json();\n if(!res.ok){ errEl.textContent = data.error||'Invalid key'; errEl.style.display='block'; return; }\n if(data.csrfToken){ _csrfToken = data.csrfToken; sessionStorage.setItem('csrfToken', _csrfToken); }\n currentConfig = await fetchAPI('/config');\n document.getElementById('loginView').style.display='none';\n document.getElementById('appShell').style.display='flex';\n startSessionCheck();\n if(data.newKey){\n navigate('settings');\n showNewKey(data.newKey);\n } else {\n loadDashboard();\n }\n } catch(e){ errEl.textContent='Connection failed'; errEl.style.display='block'; }\n}\nasync function doLogout(){\n await fetch(API+'/auth/logout',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n location.reload();\n}\nfunction showNewKey(key){\n document.getElementById('newKeyDisplay').innerHTML =\n '<div class="key-card"><div class="key-label">Your new access key (save it now)</div><div class="key-value">'+esc(key)+'</div><div class="key-label">This key will not be shown again</div></div>';\n}\n\n/* ---- Navigation ---- */\nfunction toggleNavGroup(name){\n var el = document.getElementById('navGroup'+name);\n if(el) el.classList.toggle('collapsed');\n}\nfunction navigate(section){\n // Clean up logs auto-refresh when navigating away\n if(logAutoTimer){ clearInterval(logAutoTimer); logAutoTimer=null; }\n var arCb = document.getElementById('logAutoRefresh');\n if(arCb) arCb.checked = false;\n // Discard any unsaved in-memory changes: force fresh config on next load\n currentConfig = null;\n sectionsLoaded = {};\n if(typeof _internalToolsLoaded !== 'undefined') _internalToolsLoaded = false;\n clearDirty();\n document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));\n document.querySelectorAll('#navMenu a').forEach(a=>a.classList.remove('active'));\n const sec = document.getElementById('sec-'+section);\n if(sec) sec.classList.add('active');\n const link = document.querySelector('#navMenu a[data-section="'+section+'"]');\n if(link) link.classList.add('active');\n // Auto-expand parent group if navigating to a child section\n var groupMap = {\n channels:'Interactions', stt:'Interactions', tts:'Interactions',\n models:'Engine', vars:'Engine', agent:'Engine', subagents:'Engine',\n skills:'Competences', commands:'Competences', plugins:'Competences',\n memory:'Identity', prompts:'Identity',\n logs:'Operations', cron:'Operations', nodes:'Operations', tokens:'Operations'\n };\n if(groupMap[section]){\n var grp = document.getElementById('navGroup'+groupMap[section]);\n if(grp) grp.classList.remove('collapsed');\n }\n if(section==='dashboard') loadDashboard();\n if(section==='channels') loadChannels();\n if(section==='models') loadModels();\n if(section==='vars') loadVars();\n if(section==='agent') loadAgent();\n if(section==='subagents') loadSubAgents();\n if(section==='skills') loadSkills();\n if(section==='commands') loadCommands();\n if(section==='plugins') loadPlugins();\n if(section==='stt') loadSTT();\n if(section==='tts') loadTTS();\n if(section==='memory') loadMemory();\n if(section==='logs') loadLogs();\n if(section==='cron') loadCron();\n if(section==='nodes') loadNodes();\n if(section==='tokens') loadTokens();\n if(section==='prompts') loadPrompts();\n if(section==='settings'){ loadSettings(); loadCurrentKey(); }\n}\ndocument.getElementById('navMenu').addEventListener('click',e=>{\n const a = e.target.closest('a[data-section]');\n if(!a) return;\n e.preventDefault();\n navigate(a.dataset.section);\n});\n\n/* ---- Dashboard ---- */\nasync function loadDashboard(){\n try{\n const [cfgRes,statusRes] = await Promise.all([fetchAPI('/config'),fetchAPI('/status')]);\n currentConfig = cfgRes;\n const st = statusRes;\n document.getElementById('statusUptime').textContent = st.uptime||'--';\n document.getElementById('statusModel').textContent = cfgRes.agent?.model||'--';\n document.getElementById('statusFallback').textContent = cfgRes.agent?.mainFallback||'--';\n var arEnabled = !!(cfgRes.nostromo && cfgRes.nostromo.autoRestart);\n var arEl = document.getElementById('statusAutoRestart');\n if(arEnabled){\n arEl.innerHTML = '<span class="badge badge-green">On</span>';\n } else {\n arEl.innerHTML = '<span class="badge badge-red">Off</span>';\n }\n const ch = document.getElementById('dashChannels');\n ch.innerHTML='';\n const channels = cfgRes.channels||{};\n for(const[name,cfg]of Object.entries(channels)){\n if(cfg.enabled){\n ch.innerHTML += '<span class="badge badge-green">'+esc(name)+'</span>';\n }\n }\n if(!ch.innerHTML) ch.innerHTML='<span style="font-size:14px;color:var(--text-muted)">No channels enabled</span>';\n updateConfigCheckPolling();\n }catch(e){ toast('Failed to load dashboard','err'); }\n}\n\n/* ---- WhatsApp QR Modal ---- */\nvar qrPollTimer = null;\nfunction openWhatsAppQr(){\n var modal = document.getElementById('qrModal');\n var img = document.getElementById('qrImg');\n var msg = document.getElementById('qrMsg');\n var spinner = document.getElementById('qrSpinner');\n var status = document.getElementById('qrStatus');\n img.style.display='none';\n msg.textContent='Waiting for QR code...';\n spinner.style.display='block';\n status.innerHTML='';\n modal.classList.add('open');\n pollWhatsAppQr();\n}\nfunction closeQrModal(){\n document.getElementById('qrModal').classList.remove('open');\n if(qrPollTimer){ clearTimeout(qrPollTimer); qrPollTimer=null; }\n}\nfunction pollWhatsAppQr(){\n fetch(API+'/whatsapp/qr').then(function(r){ return r.json(); }).then(function(data){\n var img = document.getElementById('qrImg');\n var msg = document.getElementById('qrMsg');\n var spinner = document.getElementById('qrSpinner');\n var status = document.getElementById('qrStatus');\n if(data.connected){\n img.style.display='none';\n spinner.style.display='none';\n msg.textContent='';\n status.innerHTML='<div class="status-msg ok">WhatsApp connected successfully!</div>';\n return;\n }\n if(data.error){\n img.style.display='none';\n spinner.style.display='none';\n msg.textContent='';\n status.innerHTML='<div class="status-msg err">'+data.error+'</div>';\n return;\n }\n if(data.dataUrl){\n img.src=data.dataUrl;\n img.style.display='block';\n spinner.style.display='none';\n msg.textContent='Scan this QR code in WhatsApp';\n } else {\n img.style.display='none';\n spinner.style.display='block';\n msg.textContent='Waiting for QR code...';\n }\n qrPollTimer = setTimeout(pollWhatsAppQr, 2000);\n }).catch(function(){\n qrPollTimer = setTimeout(pollWhatsAppQr, 3000);\n });\n}\n\n/* ---- Restart ---- */\nfunction showRestartModal(){\n document.getElementById('restartModal').classList.add('open');\n}\nfunction closeRestartModal(){\n document.getElementById('restartModal').classList.remove('open');\n}\nasync function doRestart(){\n closeRestartModal();\n try{\n const res = await fetch(API+'/server/restart',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}});\n const data = await res.json();\n if(data.ok){\n toast('Server restarted successfully','ok');\n setRestartIndicators(false);\n loadDashboard();\n } else {\n toast(data.error||'Restart failed','err');\n }\n }catch(e){\n toast('Restart failed: '+e,'err');\n }\n}\n\n/* ---- Theme ---- */\nfunction initTheme(){\n const saved = localStorage.getItem('nostromo-theme')||'light';\n document.documentElement.setAttribute('data-theme', saved);\n const toggle = document.getElementById('themeToggle');\n if(toggle) toggle.checked = saved==='dark';\n updateThemeLabel();\n}\nfunction toggleTheme(){\n const isDark = document.getElementById('themeToggle').checked;\n const theme = isDark?'dark':'light';\n document.documentElement.setAttribute('data-theme', theme);\n localStorage.setItem('nostromo-theme', theme);\n updateThemeLabel();\n if(typeof monaco !== 'undefined') updateAllMonacoThemes();\n}\nfunction updateThemeLabel(){\n const el = document.getElementById('themeLabel');\n if(el) el.textContent = document.documentElement.getAttribute('data-theme')==='dark'?'Dark':'Light';\n}\n\n/* ---- Auto-save & dirty tracking ---- */\nvar autoSaveTimer = null;\n// IDs of elements that should NOT trigger auto-save\nvar AUTOSAVE_SKIP = ['themeToggle','logAutoRefresh','logLinesSelect','logLevelSelect'];\n\nfunction markDirty(){\n // Enable the save button in the currently active section\n var sec = document.querySelector('.section.active');\n if(!sec) return;\n var btn = sec.querySelector('.save-btn');\n if(btn) btn.disabled = false;\n}\nfunction clearDirty(){\n document.querySelectorAll('.save-btn').forEach(function(b){ b.disabled = true; });\n}\n\n// Event delegation: auto-save on toggle/select change, mark dirty on text input\ndocument.querySelector('.main').addEventListener('change', function(e){\n var el = e.target;\n // Skip non-config elements\n if(AUTOSAVE_SKIP.indexOf(el.id)!==-1) return;\n if(el.closest('#addModelForm')||el.closest('#editModelForm')||el.closest('#addVarForm')||el.closest('#editVarForm')||el.closest('#createTokenForm')||el.closest('#addJobForm')) return;\n if(el.closest('#sec-tokens')||el.closest('#sec-logs')||el.closest('#sec-prompts')) return;\n // Toggles and selects: auto-save immediately\n if(el.type==='checkbox'||el.tagName==='SELECT'){\n clearTimeout(autoSaveTimer);\n autoSaveTimer = setTimeout(function(){ saveConfig(); }, 300);\n }\n});\ndocument.querySelector('.main').addEventListener('input', function(e){\n var el = e.target;\n if(el.closest('#addModelForm')||el.closest('#editModelForm')||el.closest('#addVarForm')||el.closest('#editVarForm')||el.closest('#createTokenForm')||el.closest('#addJobForm')) return;\n if(el.closest('#sec-tokens')||el.closest('#sec-logs')||el.closest('#sec-prompts')) return;\n // Text inputs, textareas, number inputs: mark dirty\n if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){\n var t = el.type||'text';\n if(t==='text'||t==='password'||t==='number'||el.tagName==='TEXTAREA'){\n markDirty();\n }\n }\n});\n\n/* ---- Init ---- */\nasync function checkSession(){\n try{\n const res = await fetch(API+'/auth/session');\n if(res.ok){\n const data = await res.json();\n if(data.csrfToken){ _csrfToken = data.csrfToken; sessionStorage.setItem('csrfToken', _csrfToken); }\n currentConfig = await fetchAPI('/config');\n document.getElementById('loginView').style.display='none';\n document.getElementById('appShell').style.display='flex';\n startSessionCheck();\n loadDashboard();\n }\n }catch(e){}\n}\n/* ---- Config change polling ---- */\nvar _configCheckTimer = null;\nfunction setRestartIndicators(visible){\n var fl = document.getElementById('floatingRestart'); if(fl) fl.style.display = visible ? 'flex' : 'none';\n var rp = document.getElementById('restartPending'); if(rp) rp.style.display = visible ? '' : 'none';\n}\nfunction startConfigCheck(){\n stopConfigCheck();\n _configCheckTimer = setInterval(async function(){\n try{\n var res = await fetch(API+'/config/check');\n if(!res.ok) return;\n var data = await res.json();\n setRestartIndicators(data.restartNeeded);\n }catch(e){}\n }, 5000);\n}\nfunction stopConfigCheck(){\n if(_configCheckTimer){ clearInterval(_configCheckTimer); _configCheckTimer=null; }\n setRestartIndicators(false);\n}\nfunction updateConfigCheckPolling(){\n if(currentConfig && currentConfig.nostromo && currentConfig.nostromo.autoRestart){\n stopConfigCheck();\n } else {\n startConfigCheck();\n }\n}\n\n/* ---- Session heartbeat ---- */\nvar _sessionCheckTimer = null;\nfunction startSessionCheck(){\n if(_sessionCheckTimer) return;\n _sessionCheckTimer = setInterval(async function(){\n try {\n var res = await _origFetch(API+'/auth/session', { credentials:'include' });\n if(res.status === 401){\n clearInterval(_sessionCheckTimer);\n _sessionCheckTimer = null;\n document.getElementById('sessionExpiredModal').classList.add('open');\n }\n } catch(e) {\n // Server unreachable — show expired modal\n clearInterval(_sessionCheckTimer);\n _sessionCheckTimer = null;\n document.getElementById('sessionExpiredModal').classList.add('open');\n }\n }, 10000);\n}\n\ninitTheme();\ncheckSession();\n`}
@@ -0,0 +1,3 @@
1
+ /** Browser JS for Logs, Cron, Nodes, and Tokens. */
2
+ export declare function opsJS(): string;
3
+ //# sourceMappingURL=ui-js-ops.d.ts.map
@@ -0,0 +1 @@
1
+ export function opsJS(){return"\n/* ---- Logs ---- */\nvar logAutoTimer = null;\nasync function loadLogs(){\n try{\n var [levelRes, linesRes, filesRes, cfgRes] = await Promise.all([\n fetchAPI('/logs/level'),\n fetchAPI('/logs/current?lines='+(document.getElementById('logLinesSelect').value||'200')),\n fetchAPI('/logs/files'),\n fetchAPI('/config')\n ]);\n document.getElementById('logLevelSelect').value = levelRes.level||'info';\n document.getElementById('logVerbose').checked = cfgRes.verboseDebugLogs !== false;\n renderLogLines(linesRes.lines||[], linesRes.total||0);\n renderLogFiles(filesRes||[]);\n }catch(e){ toast('Failed to load logs','err'); }\n loadMemorySessions();\n}\nasync function loadLogLines(){\n try{\n var lines = parseInt(document.getElementById('logLinesSelect').value)||200;\n var res = await fetchAPI('/logs/current?lines='+lines);\n renderLogLines(res.lines||[], res.total||0);\n }catch(e){ toast('Failed to load logs','err'); }\n}\nvar _rawLogLines = [];\nvar _rawLogTotal = 0;\nfunction renderLogLines(lines, total){\n _rawLogLines = lines;\n _rawLogTotal = total;\n applyLogFilter();\n}\nfunction applyLogFilter(){\n var viewer = document.getElementById('logViewer');\n var filter = (document.getElementById('logFilter').value||'').toLowerCase();\n var lines = _rawLogLines;\n if(filter){\n lines = lines.filter(function(l){ return l.toLowerCase().indexOf(filter)!==-1; });\n }\n var reversed = lines.slice().reverse();\n viewer.textContent = reversed.join('\\n')||'No log entries';\n document.getElementById('logTotal').textContent = filter\n ? 'Showing '+lines.length+' matching of '+_rawLogLines.length+' lines ('+_rawLogTotal+' total)'\n : 'Showing '+_rawLogLines.length+' of '+_rawLogTotal+' lines';\n}\nfunction toggleLogAutoRefresh(){\n var on = document.getElementById('logAutoRefresh').checked;\n if(on){\n logAutoTimer = setInterval(loadLogLines, 2000);\n } else {\n if(logAutoTimer){ clearInterval(logAutoTimer); logAutoTimer=null; }\n }\n}\nasync function changeLogLevel(){\n var level = document.getElementById('logLevelSelect').value;\n try{\n await fetch(API+'/logs/level',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({level:level})});\n toast('Log level set to '+level,'ok');\n }catch(e){ toast('Failed to set log level','err'); }\n}\nasync function toggleVerboseDebugLogs(){\n var on = document.getElementById('logVerbose').checked;\n try{\n await fetch(API+'/logs/verbose',{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify({enabled:on})});\n if(currentConfig) currentConfig.verboseDebugLogs = on;\n toast('Verbose debug logs '+(on?'enabled':'disabled'),'ok');\n }catch(e){ toast('Failed to save verbose setting','err'); }\n}\nfunction changeLogLines(){ loadLogLines(); }\nfunction downloadCurrentLog(){\n var a = document.createElement('a');\n a.href = API+'/logs/current/download?compress=true';\n a.download = 'gmab.log.gz';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\nfunction downloadLogFile(name){\n var a = document.createElement('a');\n a.href = API+'/logs/files/'+encodeURIComponent(name)+'/download';\n a.download = name+'.gz';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\nfunction renderLogFiles(files){\n var tbody = document.getElementById('logFilesBody');\n tbody.innerHTML='';\n if(!files.length){\n tbody.innerHTML='<tr><td colspan=\"4\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No archived logs</td></tr>';\n return;\n }\n for(var f of files){\n var size = f.size < 1024 ? f.size+' B' : f.size < 1048576 ? (f.size/1024).toFixed(1)+' KB' : (f.size/1048576).toFixed(1)+' MB';\n var mod = new Date(f.modified).toLocaleString();\n tbody.innerHTML += '<tr><td style=\"font-family:monospace;font-size:13px\">'+esc(f.name)+'</td><td>'+size+'</td><td style=\"font-size:13px\">'+mod+'</td><td><button class=\"btn-ghost btn-sm\" onclick=\"downloadLogFile(&#39;'+esc(f.name)+'&#39;)\">Download</button></td></tr>';\n }\n}\n\n/* ---- Cron ---- */\nvar _hbSessions = [];\nasync function loadCron(){\n currentConfig = await fetchAPI('/config');\n var c = currentConfig.cron||{};\n var hb = c.heartbeat||{};\n document.getElementById('cronEnabled').checked = !!c.enabled;\n document.getElementById('cronIsolated').checked = c.isolated!==false;\n document.getElementById('cronBroadcast').checked = !!c.broadcastEvents;\n document.getElementById('hbEnabled').checked = hb.enabled!==false;\n document.getElementById('hbEvery').value = hb.every!=null ? hb.every : 1800000;\n document.getElementById('hbMessage').value = hb.message||'';\n document.getElementById('hbAckMaxChars').value = hb.ackMaxChars!=null ? hb.ackMaxChars : 300;\n // Load sessions and populate channel/chatId dropdowns\n try{ _hbSessions = await fetchAPI('/sessions'); }catch(e){ _hbSessions = []; }\n populateHbChannels(hb.channel||'', hb.chatId||'');\n updateHbEveryHuman();\n updateHbFields();\n if(!loadCron._bound){\n document.getElementById('hbEnabled').addEventListener('change', updateHbFields);\n loadCron._bound = true;\n }\n sectionsLoaded.cron = true;\n loadCronJobs();\n}\nfunction populateChannelSelect(selectId, selectedChannel){\n var channelSelect = document.getElementById(selectId);\n var channels = currentConfig.channels||{};\n var enabled = [];\n for(var ch in channels){\n if(ch==='responses') continue;\n if(channels[ch] && channels[ch].enabled) enabled.push(ch);\n }\n channelSelect.innerHTML = '<option value=\"\">-- select --</option>';\n enabled.forEach(function(ch){\n var opt = document.createElement('option');\n opt.value = ch;\n opt.textContent = ch;\n if(ch === selectedChannel) opt.selected = true;\n channelSelect.appendChild(opt);\n });\n}\nfunction populateChatIdSelect(selectId, channel, selectedChatId){\n var chatSelect = document.getElementById(selectId);\n chatSelect.innerHTML = '<option value=\"\">-- select --</option>';\n if(!channel) return;\n var seen = {};\n var chatIds = [];\n var chCfg = (currentConfig.channels||{})[channel];\n if(chCfg && chCfg.accounts){\n for(var accKey in chCfg.accounts){\n var acc = chCfg.accounts[accKey];\n if(acc && Array.isArray(acc.allowFrom)){\n acc.allowFrom.forEach(function(id){\n String(id).split(',').forEach(function(part){\n var s = part.trim();\n if(s && !seen[s]){ seen[s]=true; chatIds.push(s); }\n });\n });\n }\n }\n }\n (_hbSessions||[]).forEach(function(s){\n var parts = s.sessionKey.split(':');\n if(parts[0] === channel){\n var cid = parts.slice(1).join(':');\n if(cid && !seen[cid]){ seen[cid]=true; chatIds.push(cid); }\n }\n });\n chatIds.forEach(function(cid){\n var opt = document.createElement('option');\n opt.value = cid;\n opt.textContent = cid;\n if(cid === selectedChatId) opt.selected = true;\n chatSelect.appendChild(opt);\n });\n if(selectedChatId && !seen[selectedChatId]){\n var opt = document.createElement('option');\n opt.value = selectedChatId;\n opt.textContent = selectedChatId;\n opt.selected = true;\n chatSelect.appendChild(opt);\n }\n}\nfunction populateHbChannels(selectedChannel, selectedChatId){\n populateChannelSelect('hbChannel', selectedChannel);\n populateChatIdSelect('hbChatId', selectedChannel, selectedChatId);\n}\nfunction onHbChannelChange(){\n var ch = document.getElementById('hbChannel').value;\n populateChatIdSelect('hbChatId', ch, '');\n}\nfunction populateNewJobChannels(){\n populateChannelSelect('newJobChannel', '');\n populateChatIdSelect('newJobChatId', '', '');\n}\nfunction onNewJobChannelChange(){\n var ch = document.getElementById('newJobChannel').value;\n populateChatIdSelect('newJobChatId', ch, '');\n}\nfunction updateHbEveryHuman(){\n var ms = parseInt(document.getElementById('hbEvery').value)||0;\n var s = Math.floor(ms/1000);\n var h = Math.floor(s/3600); s -= h*3600;\n var m = Math.floor(s/60); s -= m*60;\n var parts = [];\n if(h) parts.push(h+'h');\n if(m) parts.push(m+'m');\n if(s) parts.push(s+'s');\n document.getElementById('hbEveryHuman').textContent = parts.length ? '('+parts.join(' ')+')' : '';\n}\nfunction updateHbFields(){\n var on = document.getElementById('hbEnabled').checked;\n document.getElementById('hbFields').style.display = on ? '' : 'none';\n // Check if channel/chatId are selected — warn if heartbeat is on but config is incomplete\n var warn = document.getElementById('hbWarning');\n if(!warn) return;\n if(!on){ warn.style.display='none'; return; }\n var ch = document.getElementById('hbChannel').value;\n var cid = document.getElementById('hbChatId').value;\n var msg = (document.getElementById('hbMessage').value||'').trim();\n if(!ch || !cid){\n warn.textContent = 'Heartbeat requires an active channel and a chat ID to be selected.';\n warn.style.display = '';\n } else if(msg.length < 15){\n warn.textContent = 'Heartbeat message is too short (minimum 15 characters).';\n warn.style.display = '';\n } else {\n warn.style.display = 'none';\n }\n}\nasync function simulateHeartbeat(){\n try{\n var res = await fetch(API+'/heartbeat/simulate',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken}});\n var data = await res.json();\n var resultEl = document.getElementById('heartbeatSimResult');\n var contentEl = document.getElementById('heartbeatSimContent');\n if(!data.fileExists){\n resultEl.innerHTML='<span style=\"color:var(--accent)\">HEARTBEAT.md not found.</span> The heartbeat would <strong>run</strong> and let the agent decide what to do.';\n } else if(data.accepted){\n resultEl.innerHTML='<span style=\"color:var(--success)\">Accepted.</span> HEARTBEAT.md has actionable content. The heartbeat would be <strong>sent to the agent</strong>.';\n } else {\n resultEl.innerHTML='<span style=\"color:var(--warning)\">Ignored.</span> HEARTBEAT.md is effectively empty (only headers/comments). The heartbeat would be <strong>skipped</strong> to save API calls.';\n }\n contentEl.value = data.fileExists ? data.content : '(file does not exist)';\n document.getElementById('heartbeatSimModal').classList.add('open');\n }catch(e){ toast('Simulation failed','err'); }\n}\nfunction closeHeartbeatSimModal(){\n document.getElementById('heartbeatSimModal').classList.remove('open');\n}\nvar _rawCronJobs = [];\nasync function loadCronJobs(){\n try{\n var statusRes = await fetchAPI('/cron/status');\n var el = document.getElementById('cronStatus');\n if(el) el.textContent = statusRes.enabled ? statusRes.jobs+' job(s)' : 'disabled';\n _rawCronJobs = await fetchAPI('/cron/jobs?includeDisabled=true');\n renderCronJobs();\n }catch(e){ toast('Failed to load cron jobs','err'); }\n}\nfunction applyCronJobFilter(){ renderCronJobs(); }\nfunction renderCronJobs(){\n var filter = (document.getElementById('cronJobFilter').value||'').toLowerCase();\n var jobs = _rawCronJobs.slice();\n // Sort: enabled first, then alphabetically by name\n jobs.sort(function(a,b){\n if(a.enabled && !b.enabled) return -1;\n if(!a.enabled && b.enabled) return 1;\n return (a.name||'').localeCompare(b.name||'');\n });\n // Filter on full name including tags\n if(filter) jobs = jobs.filter(function(j){\n var full = j.name;\n if(j.isolated!==false) full += ' isolated';\n if(j.suppressToken) full += ' suppress';\n return full.toLowerCase().indexOf(filter)!==-1;\n });\n var tbody = document.getElementById('cronJobsBody');\n tbody.innerHTML='';\n if(!jobs.length){\n tbody.innerHTML='<tr><td colspan=\"7\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No jobs</td></tr>';\n updateCronScrollHint();\n return;\n }\n for(var j of jobs){\n var sched = j.schedule;\n var schedStr = sched.kind==='every' ? 'every '+formatMs(sched.everyMs) : sched.kind==='cron' ? sched.expr : sched.at;\n var nextStr = j.state&&j.state.nextRunAtMs ? new Date(j.state.nextRunAtMs).toLocaleString() : '--';\n var lastSt = j.state&&j.state.lastStatus ? j.state.lastStatus : 'never';\n var statusBadge = !j.enabled ? '<span class=\"badge badge-red\">disabled</span>' :\n lastSt==='ok' ? '<span class=\"badge badge-green\">ok</span>' :\n lastSt==='error' ? '<span class=\"badge badge-red\">error</span>' :\n '<span class=\"badge\" style=\"background:var(--border);color:var(--text-muted)\">'+esc(lastSt)+'</span>';\n var tags = '';\n var tagsText = '';\n if(j.isolated!==false){ tags += ' <span class=\"badge badge-blue\" style=\"font-size:10px;padding:1px 5px\">isolated</span>'; tagsText += ' [isolated]'; }\n if(j.suppressToken){ tags += ' <span style=\"font-size:11px;color:var(--text-muted)\">(suppress)</span>'; tagsText += ' (suppress)'; }\n var nameTitle = j.name + tagsText;\n var sessionCol = j.isolated!==false ? 'cron:'+esc(j.name) : esc(j.channel)+':'+esc(j.chatId);\n var deliveryCol = j.channel+':'+j.chatId;\n var truncStyle = 'max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default';\n var msgBtn = j.message ? '<button class=\"btn-ghost btn-sm\" onclick=\"showCronMsg(&#39;'+esc(j.name).replace(/'/g,'&#39;')+'&#39;,&#39;'+btoa(unescape(encodeURIComponent(j.message)))+'&#39;)\">Msg</button> ' : '';\n tbody.innerHTML += '<tr><td style=\"'+truncStyle+'\" title=\"'+esc(nameTitle)+'\">'+esc(j.name)+tags+'</td><td style=\"font-size:13px\">'+esc(schedStr)+'</td><td style=\"font-family:monospace;font-size:13px;'+truncStyle+'\" title=\"'+esc(sessionCol)+'\">'+sessionCol+'</td><td style=\"'+truncStyle+'\" title=\"'+esc(deliveryCol)+'\">'+esc(deliveryCol)+'</td><td>'+statusBadge+'</td><td style=\"font-size:13px\">'+esc(nextStr)+'</td><td style=\"white-space:nowrap\">'+msgBtn+'<button class=\"btn btn-sm\" onclick=\"runJob(&#39;'+j.id+'&#39;)\">Run</button> '+(j.enabled?'<button class=\"btn-ghost btn-sm\" onclick=\"toggleJob(&#39;'+j.id+'&#39;,false)\">Disable</button>':'<button class=\"btn-ghost btn-sm\" onclick=\"toggleJob(&#39;'+j.id+'&#39;,true)\">Enable</button>')+' <button class=\"btn-danger btn-sm\" onclick=\"deleteJob(&#39;'+j.id+'&#39;)\">Delete</button></td></tr>';\n }\n updateCronScrollHint();\n}\nfunction updateCronScrollHint(){\n var wrap = document.getElementById('cronJobsWrap');\n var hint = document.getElementById('cronScrollHint');\n if(!wrap||!hint) return;\n hint.style.display = wrap.scrollWidth > wrap.clientWidth ? '' : 'none';\n wrap.removeEventListener('scroll', _onCronScroll);\n wrap.addEventListener('scroll', _onCronScroll);\n}\nfunction _onCronScroll(){\n var wrap = document.getElementById('cronJobsWrap');\n var hint = document.getElementById('cronScrollHint');\n if(!wrap||!hint) return;\n var atEnd = wrap.scrollLeft + wrap.clientWidth >= wrap.scrollWidth - 4;\n hint.style.display = atEnd ? 'none' : '';\n}\nfunction formatMs(ms){\n if(ms>=3600000) return (ms/3600000)+'h';\n if(ms>=60000) return (ms/60000)+'m';\n return (ms/1000)+'s';\n}\nfunction showAddJob(){\n document.getElementById('addJobForm').style.display='';\n document.getElementById('newJobIsolated').checked = currentConfig&&currentConfig.cron ? currentConfig.cron.isolated!==false : true;\n populateNewJobChannels();\n}\nfunction hideAddJob(){ document.getElementById('addJobForm').style.display='none'; }\nfunction updateJobSchedFields(){\n var kind = document.getElementById('newJobSchedKind').value;\n document.getElementById('newJobSchedEvery').style.display = kind==='every'?'':'none';\n document.getElementById('newJobSchedCron').style.display = kind==='cron'?'':'none';\n document.getElementById('newJobSchedAt').style.display = kind==='at'?'':'none';\n}\nasync function addJob(){\n var name = document.getElementById('newJobName').value.trim();\n if(!name){ toast('Name required','err'); return; }\n var channel = document.getElementById('newJobChannel').value;\n var chatId = document.getElementById('newJobChatId').value;\n var message = document.getElementById('newJobMessage').value.trim();\n if(!channel||!chatId||!message){ toast('Channel, Chat ID, and Message required','err'); return; }\n var kind = document.getElementById('newJobSchedKind').value;\n var schedule;\n if(kind==='every') schedule = {kind:'every', everyMs: parseInt(document.getElementById('newJobEveryMs').value)||60000};\n else if(kind==='cron') schedule = {kind:'cron', expr: document.getElementById('newJobCronExpr').value.trim()};\n else schedule = {kind:'at', at: new Date(document.getElementById('newJobAtTime').value).toISOString()};\n var isolated = document.getElementById('newJobIsolated').checked;\n var suppressToken = document.getElementById('newJobSuppress').checked;\n try{\n await fetch(API+'/cron/jobs',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify({name:name,description:document.getElementById('newJobDesc').value.trim()||undefined,channel:channel,chatId:chatId,message:message,schedule:schedule,isolated:isolated,suppressToken:suppressToken,enabled:true})});\n hideAddJob();\n toast('Job created','ok');\n loadCronJobs();\n }catch(e){ toast('Failed to create job','err'); }\n}\nasync function runJob(id){\n try{\n await fetch(API+'/cron/jobs/'+id+'/run',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify({mode:'force'})});\n toast('Job triggered','ok');\n setTimeout(loadCronJobs, 1500);\n }catch(e){ toast('Failed','err'); }\n}\nasync function toggleJob(id,enable){\n try{\n await fetch(API+'/cron/jobs/'+id,{method:'PUT',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify({enabled:enable})});\n toast(enable?'Job enabled':'Job disabled','ok');\n loadCronJobs();\n }catch(e){ toast('Failed','err'); }\n}\nasync function deleteJob(id){\n try{\n await fetch(API+'/cron/jobs/'+id,{method:'DELETE',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Job deleted','ok');\n loadCronJobs();\n }catch(e){ toast('Failed','err'); }\n}\n\nfunction showCronMsg(name, b64msg){\n var msg = decodeURIComponent(escape(atob(b64msg)));\n document.getElementById('cronMsgTitle').textContent = 'Job: ' + name;\n document.getElementById('cronMsgBody').value = msg;\n document.getElementById('cronMsgModal').classList.add('open');\n}\n\n/* ---- TTS ---- */\nasync function loadTTS(){\n currentConfig = await fetchAPI('/config');\n var t = currentConfig.tts||{};\n document.getElementById('ttsEnabled').checked = !!t.enabled;\n document.getElementById('ttsProvider').value = t.provider||'openai';\n var edge = t.edge||{};\n document.getElementById('ttsEdgeVoice').value = edge.voice||'en-US-MichelleNeural';\n var oai = t.openai||{};\n populateTTSModelSelect('ttsOAIModelRef', oai.modelRef||'');\n document.getElementById('ttsOAIModel').value = oai.model||'gpt-4o-mini-tts';\n document.getElementById('ttsOAIVoice').value = oai.voice||'alloy';\n var el = t.elevenlabs||{};\n populateTTSModelSelect('ttsELModelRef', el.modelRef||'');\n document.getElementById('ttsELVoiceId').value = el.voiceId||'pMsXgVXv3BLzUgSXRplE';\n document.getElementById('ttsELModelId').value = el.modelId||'eleven_multilingual_v2';\n document.getElementById('ttsMaxTextLength').value = t.maxTextLength!=null ? t.maxTextLength : 4096;\n document.getElementById('ttsTimeoutMs').value = t.timeoutMs!=null ? t.timeoutMs : 30000;\n updateTTSFields();\n if(!loadTTS._bound){\n document.getElementById('ttsProvider').addEventListener('change', updateTTSFields);\n loadTTS._bound = true;\n }\n sectionsLoaded.tts = true;\n}\nfunction populateTTSModelSelect(selectId, selectedRef){\n var sel = document.getElementById(selectId);\n sel.innerHTML = '<option value=\"\">-- none --</option>';\n if(currentConfig && currentConfig.models){\n currentConfig.models.forEach(function(m){\n var types = m.types||['external'];\n if(types.indexOf('external')===-1) return;\n var opt = document.createElement('option');\n opt.value = m.name;\n opt.textContent = m.name;\n if(m.name === selectedRef) opt.selected = true;\n sel.appendChild(opt);\n });\n }\n}\nfunction updateTTSFields(){\n var prov = document.getElementById('ttsProvider').value;\n document.getElementById('ttsEdge').style.display = prov==='edge'?'':'none';\n document.getElementById('ttsOpenAI').style.display = prov==='openai'?'':'none';\n document.getElementById('ttsElevenLabs').style.display = prov==='elevenlabs'?'':'none';\n}\n\n/* ---- Nodes ---- */\nasync function loadNodes(){\n try{\n var [connected, signatures] = await Promise.all([fetchAPI('/nodes'), fetchAPI('/nodes/signatures')]);\n // Connected nodes\n var cBody = document.getElementById('connectedNodesBody');\n cBody.innerHTML='';\n if(!connected.length){\n cBody.innerHTML='<tr><td colspan=\"5\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No nodes connected</td></tr>';\n } else {\n for(var n of connected){\n var since = n.connectedAt ? new Date(n.connectedAt).toLocaleString() : '--';\n cBody.innerHTML += '<tr><td>'+esc(n.displayName||n.nodeId)+'</td><td>'+esc(n.hostname||'--')+'</td><td>'+esc(n.platform||'--')+'</td><td>'+esc(n.arch||'--')+'</td><td style=\"font-size:13px\">'+esc(since)+'</td></tr>';\n }\n }\n // Pending\n var pending = signatures.filter(function(s){ return s.status==='pending'; });\n var pCard = document.getElementById('pendingNodesCard');\n var pBody = document.getElementById('pendingNodesBody');\n pBody.innerHTML='';\n if(pending.length){\n pCard.style.display='';\n for(var p of pending){\n var sigShort = p.signature.length>16 ? p.signature.slice(0,8)+'...'+p.signature.slice(-4) : p.signature;\n pBody.innerHTML += '<tr><td>'+esc(p.displayName||p.nodeId)+'</td><td>'+esc(p.hostname||'--')+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(sigShort)+'</td><td style=\"font-size:13px\">'+esc(new Date(p.createdAt+'Z').toLocaleString())+'</td><td style=\"white-space:nowrap\"><button class=\"btn btn-sm\" onclick=\"approveNode('+p.id+')\">Approve</button> <button class=\"btn-danger btn-sm\" onclick=\"deleteSignature('+p.id+')\">Reject</button></td></tr>';\n }\n } else {\n pCard.style.display='none';\n }\n // Approved\n var approved = signatures.filter(function(s){ return s.status==='approved'; });\n var aCard = document.getElementById('approvedNodesCard');\n var aBody = document.getElementById('approvedNodesBody');\n aBody.innerHTML='';\n if(approved.length){\n aCard.style.display='';\n for(var a of approved){\n var sigShort2 = a.signature.length>16 ? a.signature.slice(0,8)+'...'+a.signature.slice(-4) : a.signature;\n var approvedAt = a.approvedAt ? new Date(a.approvedAt+'Z').toLocaleString() : '--';\n var lastSeen = a.lastSeenAt ? new Date(a.lastSeenAt+'Z').toLocaleString() : '--';\n aBody.innerHTML += '<tr><td>'+esc(a.displayName||a.nodeId)+'</td><td>'+esc(a.hostname||'--')+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(sigShort2)+'</td><td style=\"font-size:13px\">'+esc(approvedAt)+'</td><td style=\"font-size:13px\">'+esc(lastSeen)+'</td><td><button class=\"btn-danger btn-sm\" onclick=\"revokeNode('+a.id+')\">Revoke</button></td></tr>';\n }\n } else {\n aCard.style.display='none';\n }\n // Revoked\n var revoked = signatures.filter(function(s){ return s.status==='revoked'; });\n var rCard = document.getElementById('revokedNodesCard');\n var rBody = document.getElementById('revokedNodesBody');\n rBody.innerHTML='';\n if(revoked.length){\n rCard.style.display='';\n for(var r of revoked){\n var revokedAt = r.approvedAt ? new Date(r.approvedAt+'Z').toLocaleString() : '--';\n rBody.innerHTML += '<tr><td>'+esc(r.displayName||r.nodeId)+'</td><td>'+esc(r.hostname||'--')+'</td><td style=\"font-size:13px\">'+esc(revokedAt)+'</td><td style=\"white-space:nowrap\"><button class=\"btn btn-sm\" onclick=\"approveNode('+r.id+')\">Re-approve</button> <button class=\"btn-danger btn-sm\" onclick=\"deleteSignature('+r.id+')\">Delete</button></td></tr>';\n }\n } else {\n rCard.style.display='none';\n }\n }catch(e){ toast('Failed to load nodes','err'); }\n}\nasync function approveNode(id){\n try{\n await fetch(API+'/nodes/signatures/'+id+'/approve',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Node approved','ok');\n loadNodes();\n }catch(e){ toast('Failed','err'); }\n}\nasync function revokeNode(id){\n try{\n await fetch(API+'/nodes/signatures/'+id+'/revoke',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Node revoked','ok');\n loadNodes();\n }catch(e){ toast('Failed','err'); }\n}\nasync function deleteSignature(id){\n try{\n await fetch(API+'/nodes/signatures/'+id,{method:'DELETE',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Signature deleted','ok');\n loadNodes();\n }catch(e){ toast('Failed','err'); }\n}\n\n/* ---- Tokens ---- */\nasync function loadTokens(){\n try{\n const tokens = await fetchAPI('/tokens');\n const tbody = document.getElementById('tokenBody');\n tbody.innerHTML='';\n if(!tokens.length){\n tbody.innerHTML='<tr><td colspan=\"7\" style=\"text-align:center;color:var(--text-muted);padding:20px\">No tokens</td></tr>';\n return;\n }\n for(const t of tokens){\n const short = t.token.length>16 ? t.token.slice(0,8)+'...'+t.token.slice(-4) : t.token;\n var actions = '<button class=\"btn-ghost btn-sm\" data-copy-token=\"'+esc(t.token)+'\">Copy</button> ';\n if(t.active){\n actions += '<button class=\"btn-ghost btn-sm\" onclick=\"revokeToken('+t.id+')\">Revoke</button> ';\n } else {\n actions += '<button class=\"btn btn-sm\" onclick=\"approveToken('+t.id+')\">Approve</button> ';\n }\n actions += '<button class=\"btn-danger btn-sm\" onclick=\"confirmDeleteToken('+t.id+')\">Delete</button>';\n tbody.innerHTML += '<tr><td>'+t.id+'</td><td style=\"font-family:monospace;font-size:13px\">'+esc(short)+'</td><td>'+esc(t.userId)+'</td><td>'+esc(t.channel)+'</td><td>'+esc(t.label||'')+'</td><td>'+(t.active?'<span class=\"badge badge-green\">Active</span>':'<span class=\"badge badge-red\">Revoked</span>')+'</td><td style=\"white-space:nowrap\">'+actions+'</td></tr>';\n }\n tbody.querySelectorAll('[data-copy-token]').forEach(function(btn){\n btn.addEventListener('click', function(){ copyToken(btn.dataset.copyToken); });\n });\n }catch(e){ toast('Failed to load tokens','err'); }\n}\nfunction showCreateToken(){ document.getElementById('createTokenForm').style.display=''; }\nfunction hideCreateToken(){ document.getElementById('createTokenForm').style.display='none'; }\nasync function createToken(){\n const userId = document.getElementById('newTokenUser').value.trim();\n if(!userId){ toast('User ID required','err'); return; }\n if(userId.length < 5){ toast('User ID must be at least 5 characters','err'); return; }\n if(!/^[a-zA-Z0-9\\-_!\\[\\]]+$/.test(userId)){ toast('User ID can only contain a-z A-Z 0-9 - _ ! [ ]','err'); return; }\n const channel = document.getElementById('newTokenChannel').value.trim()||'*';\n const label = document.getElementById('newTokenLabel').value.trim()||undefined;\n try{\n await fetch(API+'/tokens',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':_csrfToken},body:JSON.stringify({userId,channel,label})});\n hideCreateToken();\n toast('Token created','ok');\n loadTokens();\n }catch(e){ toast('Failed','err'); }\n}\nasync function revokeToken(id){\n await fetch(API+'/tokens/'+id+'/revoke',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Token revoked','ok');\n loadTokens();\n}\nasync function approveToken(id){\n await fetch(API+'/tokens/'+id+'/approve',{method:'POST',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Token approved','ok');\n loadTokens();\n}\nfunction copyToken(token){\n navigator.clipboard.writeText(token).then(function(){ toast('Token copied','ok'); }).catch(function(){ toast('Copy failed','err'); });\n}\nvar _tokenDeleteId = -1;\nfunction confirmDeleteToken(id){\n _tokenDeleteId = id;\n document.getElementById('tokenDeleteId').textContent = '#' + id;\n document.getElementById('tokenDeleteModal').classList.add('open');\n}\nfunction closeTokenDeleteModal(){\n document.getElementById('tokenDeleteModal').classList.remove('open');\n _tokenDeleteId = -1;\n}\nasync function doDeleteToken(){\n if(_tokenDeleteId < 0) return;\n var delId = _tokenDeleteId;\n closeTokenDeleteModal();\n await fetch(API+'/tokens/'+delId,{method:'DELETE',headers:{'X-CSRF-Token':_csrfToken}});\n toast('Token deleted','ok');\n loadTokens();\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** Browser JS for Monaco editor setup, Prompts editor, Simulate, and Memory viewer. */
2
+ export declare function promptsJS(): string;
3
+ //# sourceMappingURL=ui-js-prompts.d.ts.map
@@ -0,0 +1 @@
1
+ export function promptsJS(){return"\n/* ---- Monaco Editor ---- */\nvar _monacoPromise = null;\nvar _promptEditor = null; // main file editor\nvar _simEditor = null; // simulate modal editor\nvar _memEditor = null; // memory viewer modal editor\nvar _promptFiles = []; // loaded workspace/template file list\nvar _currentFile = null; // currently selected file object\nvar _simData = null;\nvar _memorySessions = [];\n\nfunction monacoTheme(){\n return document.documentElement.getAttribute('data-theme')==='dark' ? 'vs-dark' : 'vs';\n}\n\nfunction ensureMonaco(){\n if(_monacoPromise) return _monacoPromise;\n _monacoPromise = new Promise(function(resolve, reject){\n if(typeof monaco !== 'undefined'){ resolve(); return; }\n require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs' }});\n require(['vs/editor/editor.main'], function(){ resolve(); }, function(err){ reject(err); });\n });\n return _monacoPromise;\n}\n\nfunction createEditor(container, opts){\n var defaults = {\n language: 'markdown',\n theme: monacoTheme(),\n minimap: { enabled: false },\n lineNumbers: 'on',\n wordWrap: 'on',\n scrollBeyondLastLine: false,\n fontSize: 13,\n tabSize: 2,\n automaticLayout: true,\n padding: { top: 8 }\n };\n for(var k in opts){ defaults[k] = opts[k]; }\n return monaco.editor.create(container, defaults);\n}\n\nfunction updateAllMonacoThemes(){\n var th = monacoTheme();\n monaco.editor.setTheme(th);\n}\n\n/* ---- Prompts ---- */\nasync function loadPrompts(){\n try{\n await ensureMonaco();\n _promptFiles = await fetchAPI('/workspace-files');\n _promptFiles.sort(function(a,b){ return a.name.localeCompare(b.name); });\n var sel = document.getElementById('promptFileSelect');\n sel.innerHTML = '<option value=\"\">Select a file...</option>';\n var tplGroup = document.createElement('optgroup');\n tplGroup.label = 'System Prompt Templates';\n var wsGroup = document.createElement('optgroup');\n wsGroup.label = 'Workspace Files';\n for(var i=0;i<_promptFiles.length;i++){\n var f = _promptFiles[i];\n var opt = document.createElement('option');\n opt.value = f.name + (f.isTemplate ? '|tpl' : '|ws');\n var label = f.name;\n if(!f.exists && !f.isTemplate) label += ' (not found)';\n opt.textContent = label;\n if(f.isTemplate) tplGroup.appendChild(opt);\n else wsGroup.appendChild(opt);\n }\n sel.appendChild(tplGroup);\n sel.appendChild(wsGroup);\n // Reset editor\n document.getElementById('promptEditorWrap').style.display = 'none';\n document.getElementById('promptSaveBtn').style.display = 'none';\n document.getElementById('promptFileBadge').style.display = 'none';\n _currentFile = null;\n if(_promptEditor){ _promptEditor.setValue(''); }\n }catch(e){ toast('Failed to load workspace files','err'); }\n}\n\nasync function selectPromptFile(){\n var sel = document.getElementById('promptFileSelect');\n var val = sel.value;\n if(!val){\n document.getElementById('promptEditorWrap').style.display = 'none';\n document.getElementById('promptSaveBtn').style.display = 'none';\n document.getElementById('promptFileBadge').style.display = 'none';\n _currentFile = null;\n return;\n }\n var parts = val.split('|');\n var name = parts[0];\n var isTpl = parts[1] === 'tpl';\n var file = _promptFiles.find(function(f){ return f.name === name && f.isTemplate === isTpl; });\n if(!file) return;\n _currentFile = file;\n\n // Update badge\n var badge = document.getElementById('promptFileBadge');\n badge.style.display = '';\n if(file.isTemplate){ badge.className = 'file-badge template'; badge.textContent = 'Template'; }\n else if(file.exists){ badge.className = 'file-badge workspace'; badge.textContent = 'Loaded'; }\n else { badge.className = 'file-badge missing'; badge.textContent = 'Not found'; }\n\n // Show save button\n document.getElementById('promptSaveBtn').style.display = '';\n\n // Show editor\n var wrap = document.getElementById('promptEditorWrap');\n wrap.style.display = '';\n await ensureMonaco();\n if(!_promptEditor){\n _promptEditor = createEditor(wrap, { readOnly: false });\n }\n _promptEditor.setValue(file.content || '');\n var lang = file.name.endsWith('.json') ? 'json' : 'markdown';\n monaco.editor.setModelLanguage(_promptEditor.getModel(), lang);\n _promptEditor.revealLine(1);\n}\n\nasync function saveCurrentFile(){\n if(!_currentFile || !_promptEditor) return;\n var content = _promptEditor.getValue();\n var name = _currentFile.name;\n var isTemplate = _currentFile.isTemplate;\n try{\n var res = await fetch(API+'/workspace-files/'+encodeURIComponent(name),{\n method:'PUT',\n headers:{'Content-Type':'application/json'},\n body:JSON.stringify({content:content, isTemplate:isTemplate})\n });\n if(!res.ok){ var d=await res.json(); toast(d.error||'Save failed','err'); return; }\n // Update cached content\n _currentFile.content = content;\n _currentFile.exists = true;\n // Update badge\n var badge = document.getElementById('promptFileBadge');\n if(isTemplate){ badge.className = 'file-badge template'; badge.textContent = 'Template'; }\n else { badge.className = 'file-badge workspace'; badge.textContent = 'Loaded'; }\n toast(name+' saved','ok');\n }catch(e){ toast('Save failed','err'); }\n}\n\nfunction formatCharTokenCount(len){\n var tokens = Math.ceil(len / 4);\n return len.toLocaleString()+' chars (~'+tokens.toLocaleString()+' tokens)';\n}\n\n/* ---- Simulate Prompt ---- */\nasync function simulatePrompt(){\n var modal = document.getElementById('simModal');\n var charCount = document.getElementById('simCharCount');\n charCount.textContent = '';\n modal.classList.add('open');\n // Reset tabs\n document.querySelectorAll('.sim-tab').forEach(function(t){t.classList.remove('active')});\n document.querySelector('.sim-tab').classList.add('active');\n await ensureMonaco();\n var wrap = document.getElementById('simEditorWrap');\n if(!_simEditor){\n _simEditor = createEditor(wrap, { readOnly: true });\n }\n _simEditor.setValue('Loading...');\n try{\n _simData = await fetchAPI('/prompt-simulate');\n _simEditor.setValue(_simData.main);\n _simEditor.revealLine(1);\n charCount.textContent = formatCharTokenCount(_simData.main.length);\n }catch(e){\n _simEditor.setValue('Error: '+e);\n _simData = null;\n }\n}\n\nfunction switchSimTab(tab, btn){\n document.querySelectorAll('.sim-tab').forEach(function(t){t.classList.remove('active')});\n btn.classList.add('active');\n if(!_simData || !_simEditor) return;\n var text = tab==='main' ? _simData.main : _simData.subagent;\n _simEditor.setValue(text);\n _simEditor.revealLine(1);\n document.getElementById('simCharCount').textContent = formatCharTokenCount(text.length);\n}\n\nfunction closeSimModal(){\n document.getElementById('simModal').classList.remove('open');\n}\n\nasync function copySimPrompt(){\n if(!_simEditor) return;\n var text = _simEditor.getValue();\n try{\n await navigator.clipboard.writeText(text);\n toast('Copied to clipboard','ok');\n }catch(e){ toast('Copy failed','err'); }\n}\n\n/* ---- Memory Logs ---- */\nasync function loadMemorySessions(){\n try{\n _memorySessions = await fetchAPI('/memory-files');\n var sel = document.getElementById('memorySessionSelect');\n sel.innerHTML = '<option value=\"\">Select a session...</option>';\n for(var i=0;i<_memorySessions.length;i++){\n var s = _memorySessions[i];\n var opt = document.createElement('option');\n opt.value = s.sessionKey;\n opt.textContent = s.sessionKey + ' (' + s.files.length + ' file' + (s.files.length!==1?'s':'') + ')';\n sel.appendChild(opt);\n }\n document.getElementById('memoryFileList').innerHTML = '';\n }catch(e){ toast('Failed to load memory sessions','err'); }\n}\n\nfunction loadMemoryFiles(){\n var sel = document.getElementById('memorySessionSelect');\n var sessionKey = sel.value;\n var container = document.getElementById('memoryFileList');\n container.innerHTML = '';\n if(!sessionKey) return;\n var session = _memorySessions.find(function(s){ return s.sessionKey === sessionKey; });\n if(!session || !session.files.length){\n container.innerHTML = '<p style=\"font-size:13px;color:var(--text-muted)\">No files found.</p>';\n return;\n }\n var html = '<table class=\"tbl\"><thead><tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr></thead><tbody>';\n for(var i=0;i<session.files.length;i++){\n var f = session.files[i];\n var sizeKb = (f.size / 1024).toFixed(1);\n var mod = new Date(f.modified).toLocaleString();\n html += '<tr><td style=\"font-family:monospace;font-size:13px\">'+esc(f.name)+'</td><td style=\"font-size:13px\">'+sizeKb+' KB</td><td style=\"font-size:13px\">'+esc(mod)+'</td><td><button class=\"btn btn-sm btn-ghost\" data-mem-session=\"'+esc(sessionKey)+'\" data-mem-file=\"'+esc(f.name)+'\" onclick=\"viewMemoryFile(this.dataset.memSession,this.dataset.memFile)\">View</button></td></tr>';\n }\n html += '</tbody></table>';\n container.innerHTML = html;\n}\n\nasync function viewMemoryFile(sessionKey, fileName){\n var modal = document.getElementById('memViewModal');\n var title = document.getElementById('memViewTitle');\n var info = document.getElementById('memViewInfo');\n title.textContent = sessionKey + ' / ' + fileName;\n info.textContent = '';\n modal.classList.add('open');\n await ensureMonaco();\n var wrap = document.getElementById('memEditorWrap');\n if(!_memEditor){\n _memEditor = createEditor(wrap, { readOnly: true });\n }\n _memEditor.setValue('Loading...');\n try{\n var data = await fetchAPI('/memory-files/'+encodeURIComponent(sessionKey)+'/'+encodeURIComponent(fileName));\n _memEditor.setValue(data.content);\n _memEditor.revealLine(1);\n info.textContent = data.content.length.toLocaleString() + ' chars';\n }catch(e){\n _memEditor.setValue('Error loading file: '+e);\n }\n}\n\nfunction showPlaceholderRef(){\n document.getElementById('placeholderRefModal').classList.add('open');\n}\n\nfunction closeMemViewModal(){\n document.getElementById('memViewModal').classList.remove('open');\n}\n\nasync function copyMemView(){\n if(!_memEditor) return;\n var text = _memEditor.getValue();\n try{\n await navigator.clipboard.writeText(text);\n toast('Copied to clipboard','ok');\n }catch(e){ toast('Copy failed','err'); }\n}\n"}
@@ -0,0 +1,3 @@
1
+ /** All CSS for the Nostromo admin panel. */
2
+ export declare function renderStyles(): string;
3
+ //# sourceMappingURL=ui-styles.d.ts.map
@@ -0,0 +1 @@
1
+ export function renderStyles(){return'\n:root{\n --bg:#fff;--bg-card:#f3f4f6;--text:#111827;--text-muted:#6b7280;\n --accent:#6366f1;--accent-hover:#4f46e5;--accent-light:#eef2ff;\n --border:#e5e7eb;--danger:#ef4444;--success:#22c55e;\n --radius:8px;--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;\n}\n[data-theme="dark"]{\n --bg:#0b0b11;--bg-card:#16161f;--text:#e5e5ea;--text-muted:#9ca3af;\n --accent:#818cf8;--accent-hover:#6366f1;--accent-light:#1e1e2e;\n --border:#2d2d3a;--danger:#f87171;--success:#4ade80;\n}\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:var(--font);font-size:16px;line-height:1.5;color:var(--text);background:var(--bg);min-height:100vh}\na{color:var(--accent);text-decoration:none}\nbutton{cursor:pointer;font:inherit;border:none;border-radius:var(--radius);padding:8px 16px;transition:background .15s,opacity .15s}\ninput,textarea,select{font:inherit;border:1px solid var(--border);border-radius:var(--radius);padding:8px 12px;background:var(--bg);color:var(--text);width:100%;transition:border-color .15s}\ninput:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent)}\ntextarea{resize:vertical;min-height:80px}\n\n/* Layout */\n.shell{display:flex;min-height:100vh}\n.sidebar{width:220px;background:var(--bg-card);border-right:1px solid var(--border);padding:20px 0;display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:10}\n.sidebar .logo{padding:0 20px 20px;font-size:18px;font-weight:700;letter-spacing:-.02em;color:var(--text);border-bottom:1px solid var(--border);margin-bottom:8px;display:flex;align-items:center;gap:8px}\n.sidebar .logo svg{color:var(--accent)}\n.sidebar nav{flex:1;display:flex;flex-direction:column;gap:2px;padding:0 8px;overflow-y:auto}\n.sidebar nav a{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--radius);color:var(--text-muted);font-size:14px;font-weight:500;transition:background .12s,color .12s}\n.sidebar nav a:hover{background:var(--accent-light);color:var(--text)}\n.sidebar nav a.active{background:var(--accent-light);color:var(--accent);font-weight:600}\n.sidebar nav a svg{flex-shrink:0}\n.nav-group{margin:0}\n.nav-group-header{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--radius);color:var(--text-muted);font-size:14px;font-weight:500;cursor:pointer;transition:background .12s,color .12s;user-select:none}\n.nav-group-header:hover{background:var(--accent-light);color:var(--text)}\n.nav-group-header svg:first-child{flex-shrink:0}\n.nav-group-chevron{margin-left:auto;transition:transform .2s;flex-shrink:0}\n.nav-group.collapsed .nav-group-chevron{transform:rotate(-90deg)}\n.nav-group-items{display:flex;flex-direction:column;gap:2px;padding-left:12px;overflow:hidden;transition:max-height .2s ease}\n.nav-group.collapsed .nav-group-items{max-height:0 !important;overflow:hidden}\n.nav-group-items a{font-size:13px;padding:7px 10px}\n.nav-group-items a svg{width:18px;height:18px}\n.main{margin-left:220px;flex:1;padding:32px 40px;max-width:900px}\n.main h1{font-size:22px;font-weight:700;margin-bottom:0}\n.main h2{font-size:17px;font-weight:600;margin-bottom:12px;margin-top:24px}\n.section-header{position:sticky;top:0;z-index:5;background:var(--bg);padding:16px 0 14px;display:flex;align-items:center;justify-content:space-between}\n.save-btn:disabled{opacity:.4;cursor:default;pointer-events:none;animation:none}\n.save-btn:not(:disabled){animation:pulse-save 1.5s ease-in-out infinite;background:#d32f2f;border-color:#d32f2f;color:#fff}\n.save-btn:not(:disabled):hover{background:#b71c1c;border-color:#b71c1c}\n@keyframes pulse-save{0%,100%{opacity:1}50%{opacity:.5}}\n\n/* Cards */\n.card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:16px}\n.card.disabled{opacity:.45;pointer-events:none;user-select:none}\n.card.disabled .card-header::after{content:\'Coming soon\';font-size:12px;color:var(--text-muted);font-weight:400;margin-left:auto;margin-right:12px}\n.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}\n.card-title{font-weight:600;font-size:15px}\n\n/* Accordion */\n.sa-acc{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}\n.sa-acc:first-child{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}\n.sa-acc:last-child{border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius)}\n.sa-acc+.sa-acc{border-top:none}\n.sa-acc-header{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none;transition:background .15s}\n.sa-acc-header:hover{background:var(--accent-light)}\n.sa-acc:nth-child(odd) .sa-acc-header{background:var(--bg-card)}\n.sa-acc:nth-child(even) .sa-acc-header{background:var(--bg)}\n.sa-acc:nth-child(odd) .sa-acc-header:hover,.sa-acc:nth-child(even) .sa-acc-header:hover{background:var(--accent-light)}\n.sa-acc-name{flex:1;font-weight:600;font-size:14px;color:var(--text)}\n.sa-acc-chevron{width:16px;height:16px;color:var(--text-muted);transition:transform .2s;flex-shrink:0}\n.sa-acc.open .sa-acc-chevron{transform:rotate(180deg)}\n.sa-acc-body{display:none;padding:12px 14px 16px;border-top:1px solid var(--border);background:var(--bg)}\n.sa-acc.open .sa-acc-body{display:block}\n\n/* Buttons */\n.btn{background:var(--accent);color:#fff;font-weight:500;font-size:14px}\n.btn:hover{background:var(--accent-hover)}\n.btn-danger{background:var(--danger);color:#fff}\n.btn-danger:hover{opacity:.85}\n.btn-ghost{background:transparent;color:var(--text-muted);border:1px solid var(--border)}\n.btn-ghost:hover{background:var(--bg-card);color:var(--text)}\n.btn-sm{padding:5px 10px;font-size:13px}\n\n/* Modal */\n.modal-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.55);z-index:1000;align-items:center;justify-content:center}\n.modal-overlay.open{display:flex}\n.modal{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:28px;max-width:400px;width:90%;text-align:center;position:relative}\n.modal h3{margin:0 0 8px;font-size:18px}\n.modal p{margin:0 0 16px;color:var(--text-muted);font-size:14px}\n.modal img{display:block;margin:0 auto 16px;border-radius:8px;max-width:260px}\n.modal .close-btn{position:absolute;top:10px;right:14px;background:none;border:none;font-size:22px;cursor:pointer;color:var(--text-muted);line-height:1}\n.modal .status-msg{font-size:14px;padding:10px;border-radius:6px;margin-bottom:12px}\n.modal .status-msg.ok{background:rgba(34,197,94,.12);color:#16a34a}\n.modal .status-msg.err{background:rgba(239,68,68,.12);color:#ef4444}\n.modal .spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:8px}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n/* Env var ref indicator */\ninput.env-ref{border-left:3px solid var(--accent);background:rgba(59,130,246,0.04)}\n[data-theme="dark"] input.env-ref{background:rgba(59,130,246,0.08)}\n\n/* Form */\n.field{margin-bottom:14px}\n.field label{display:block;font-size:13px;font-weight:500;margin-bottom:4px;color:var(--text-muted)}\n.field-row{display:flex;gap:10px;align-items:flex-end}\n\n/* Range slider */\ninput[type="range"]{-webkit-appearance:none;appearance:none;width:100%;height:6px;border:none;border-radius:3px;background:var(--border);outline:none;padding:0;margin:8px 0 0;cursor:pointer}\ninput[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:18px;height:18px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.2);cursor:pointer;transition:background .15s}\ninput[type="range"]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.2);cursor:pointer}\ninput[type="range"]::-webkit-slider-thumb:hover{background:var(--accent-hover)}\ninput[type="range"]::-moz-range-thumb:hover{background:var(--accent-hover)}\ninput[type="range"]:focus{border-color:transparent}\n\n/* Toggle */\n.toggle{position:relative;display:inline-block;width:42px;height:24px;flex-shrink:0}\n.toggle input{opacity:0;width:0;height:0}\n.toggle span{position:absolute;inset:0;background:var(--border);border-radius:24px;transition:background .2s;cursor:pointer}\n.toggle span::before{content:\'\';position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .2s}\n.toggle input:checked+span{background:var(--accent)}\n.toggle input:checked+span::before{transform:translateX(18px)}\n\n/* Tool toggles */\n.tool-toggle{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:500;color:var(--text);cursor:pointer;padding:6px 10px;border-radius:8px;background:var(--card);border:1px solid var(--border)}\n\n/* Small toggle (subagent tools) */\n.toggle-sm{position:relative;display:inline-block;width:30px;height:16px;flex-shrink:0}\n.toggle-sm input{opacity:0;width:0;height:0}\n.toggle-sm span{position:absolute;inset:0;background:var(--border);border-radius:16px;transition:background .2s;cursor:pointer}\n.toggle-sm span::before{content:\'\';position:absolute;width:12px;height:12px;left:2px;top:2px;background:#fff;border-radius:50%;transition:transform .2s}\n.toggle-sm input:checked+span{background:var(--accent)}\n.toggle-sm input:checked+span::before{transform:translateX(14px)}\n.tool-toggle-sm{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:500;color:var(--text);padding:3px 6px;border-radius:6px;background:var(--bg-card);border:1px solid var(--border)}\n\n/* Table */\n.tbl{width:100%;border-collapse:collapse;font-size:14px}\n.tbl th{text-align:left;font-weight:500;color:var(--text-muted);padding:8px 12px;border-bottom:1px solid var(--border);font-size:13px}\n.tbl td{padding:8px 12px;border-bottom:1px solid var(--border)}\n.tbl tr:last-child td{border-bottom:none}\n.tbl tbody tr:nth-child(odd){background:var(--bg-card)}\n.tbl tbody tr:nth-child(even){background:var(--bg)}\n\n/* Badges */\n.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:500}\n.badge-green{background:#dcfce7;color:#166534}\n.badge-red{background:#fef2f2;color:#991b1b}\n[data-theme="dark"] .badge-green{background:#14532d;color:#86efac}\n[data-theme="dark"] .badge-red{background:#450a0a;color:#fca5a5}\n\n/* Key card */\n.key-card{background:var(--accent-light);border:2px solid var(--accent);border-radius:var(--radius);padding:20px;text-align:center;margin:16px 0}\n.key-card .key-value{font-family:monospace;font-size:22px;font-weight:700;letter-spacing:2px;color:var(--accent);margin:10px 0}\n.key-card .key-label{font-size:13px;color:var(--text-muted)}\n\n/* Current key display */\n.current-key-row{display:flex;align-items:center;gap:8px;margin-bottom:14px}\n.current-key-value{font-family:monospace;font-size:18px;font-weight:600;letter-spacing:2px;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:8px 14px;user-select:all}\n.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg);color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s}\n.icon-btn:hover{color:var(--text);border-color:var(--text-muted)}\n\n/* Login */\n.login-wrap{display:flex;justify-content:center;align-items:center;min-height:100vh;background:var(--bg)}\n.login-card{width:480px;max-width:95vw;padding:32px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius)}\n.login-card h1{text-align:center;margin-bottom:6px}\n.login-card .subtitle{text-align:center;font-size:14px;color:var(--text-muted);margin-bottom:24px}\n.login-card .login-error{color:var(--danger);font-size:13px;margin-top:8px;display:none}\n.key-inputs{display:flex;align-items:center;gap:6px}\n.key-inputs input{width:72px;text-align:center;font-family:monospace;font-size:18px;font-weight:600;letter-spacing:2px;text-transform:uppercase;padding:10px 6px}\n.key-inputs input:disabled{background:var(--bg-card);color:var(--text-muted);opacity:.6;cursor:default}\n.key-sep{color:var(--text-muted);font-size:20px;font-weight:300;user-select:none}\n.key-row{display:flex;align-items:center;gap:8px;justify-content:center}\n.eye-btn{background:none;border:none;padding:6px;color:var(--text-muted);cursor:pointer;flex-shrink:0;display:flex;align-items:center;border-radius:var(--radius);transition:color .15s,background .15s}\n.eye-btn:hover{color:var(--accent);background:var(--accent-light)}\n\n/* Toast */\n.toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:var(--radius);color:#fff;font-size:14px;font-weight:500;z-index:999;opacity:0;transform:translateY(10px);transition:opacity .3s,transform .3s;pointer-events:none}\n.toast.show{opacity:1;transform:translateY(0)}\n.toast-ok{background:var(--success)}\n.toast-err{background:var(--danger)}\n\n/* Restart pending */\n.restart-pending{font-size:12px;font-weight:600;color:var(--danger);animation:pulse-save 1.5s ease-in-out infinite}\n\n/* Status dot */\n.dot{width:8px;height:8px;border-radius:50%;display:inline-block}\n.dot-green{background:var(--success)}\n.dot-red{background:var(--danger)}\n\n/* Responsive */\n@media(max-width:700px){\n .sidebar{width:60px}\n .sidebar .logo span,.sidebar nav a span,.nav-group-header span,.nav-group-chevron{display:none}\n .sidebar nav a{justify-content:center;padding:10px}\n .nav-group-header{justify-content:center;padding:10px}\n .nav-group-items{padding-left:0}\n .main{margin-left:60px;padding:20px 16px}\n}\n\n/* Section visibility */\n.section{display:none}\n.section.active{display:block}\n\n\n/* Channel help */\n.help-toggle{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text-muted);font-size:13px;font-weight:600;cursor:pointer;margin-left:8px;flex-shrink:0;transition:background .15s,color .15s,border-color .15s}\n.help-toggle:hover{background:var(--accent-light);color:var(--accent);border-color:var(--accent)}\n.help-panel{display:none;margin:0 0 14px;padding:14px 16px;background:var(--accent-light);border:1px solid var(--accent);border-radius:var(--radius);font-size:13px;line-height:1.7;color:var(--text)}\n.help-panel.open{display:block}\n.help-panel ol{margin:6px 0 0 18px;padding:0}\n.help-panel li{margin-bottom:2px}\n.help-panel a{color:var(--accent);text-decoration:underline}\n.help-panel code{background:var(--bg);padding:1px 5px;border-radius:4px;font-size:12px}\n/* Monaco editor containers */\n.monaco-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;min-height:350px;position:relative}\n.monaco-wrap.readonly{opacity:.95}\n.file-select-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}\n.file-select-row select{flex:1;max-width:320px}\n.file-select-row .file-badge{font-size:12px;padding:2px 8px;border-radius:10px;font-weight:500}\n.file-select-row .file-badge.template{background:#dbeafe;color:#1e40af}\n.file-select-row .file-badge.workspace{background:#dcfce7;color:#166534}\n.file-select-row .file-badge.missing{background:#fef2f2;color:#991b1b}\n[data-theme="dark"] .file-select-row .file-badge.template{background:#1e3a5f;color:#93c5fd}\n[data-theme="dark"] .file-select-row .file-badge.workspace{background:#14532d;color:#86efac}\n[data-theme="dark"] .file-select-row .file-badge.missing{background:#450a0a;color:#fca5a5}\n\n/* Simulate modal — wider for Monaco */\n.sim-modal{max-width:900px;width:95%;max-height:90vh;text-align:left;display:flex;flex-direction:column}\n.sim-modal h3{text-align:center;margin-bottom:16px}\n.sim-tabs{display:flex;gap:4px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}\n.sim-tab{padding:8px 16px;font-size:14px;font-weight:500;background:none;color:var(--text-muted);border:none;border-bottom:2px solid transparent;cursor:pointer;transition:color .15s,border-color .15s}\n.sim-tab:hover{color:var(--text)}\n.sim-tab.active{color:var(--accent);border-bottom-color:var(--accent)}\n.sim-info{display:flex;justify-content:space-between;align-items:center;margin-top:8px;font-size:12px;color:var(--text-muted)}\n\n/* Drop zone */\n.drop-zone{padding:40px 20px;border:2px dashed var(--border);border-radius:var(--radius);text-align:center;cursor:pointer;transition:border-color .2s,background .2s}\n.drop-zone.drag-over{border-color:var(--accent);background:var(--accent-light)}\n.drop-zone.uploading{pointer-events:none;opacity:.6}\n\n/* Floating restart indicator */\n.floating-restart{position:fixed;top:16px;right:16px;width:48px;height:48px;border-radius:50%;background:var(--danger);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:900;box-shadow:0 2px 12px rgba(0,0,0,.25);animation:blink-restart 2s ease-in-out infinite;transition:transform .15s}\n.floating-restart:hover{transform:scale(1.1);animation:none;opacity:1}\n@keyframes blink-restart{0%,100%{opacity:1;box-shadow:0 2px 12px rgba(239,68,68,.3)}50%{opacity:.45;box-shadow:0 2px 20px rgba(239,68,68,.6)}}\n'}
@@ -0,0 +1,2 @@
1
+ export declare function renderSPA(isFirstRun?: boolean, basePath?: string): string;
2
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ import{renderStyles as t}from"./ui-styles.js";import{renderLayout as i}from"./ui-html-layout.js";import{renderModals as o}from"./ui-html-modals.js";import{coreJS as l}from"./ui-js-core.js";import{channelsJS as s}from"./ui-js-channels.js";import{agentJS as e}from"./ui-js-agent.js";import{competencesJS as n}from"./ui-js-competences.js";import{configJS as r}from"./ui-js-config.js";import{opsJS as p}from"./ui-js-ops.js";import{promptsJS as a}from"./ui-js-prompts.js";export function renderSPA(m=!1,d=""){return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<title>Nostromo</title>\n<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23f472b6'/%3E%3Cstop offset='1' stop-color='%23be185d'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M32 58L4 26l8-16h40l8 16z' fill='url(%23a)'/%3E%3Cpath d='M4 26l8-16h40l8 16H4z' fill='%23f9a8d4' opacity='.5'/%3E%3Cpath d='M12 10L4 26l28 32L12 10z' fill='%23ec4899' opacity='.6'/%3E%3Cpath d='M52 10l8 16L32 58 52 10z' fill='%23be185d' opacity='.6'/%3E%3Cpath d='M32 58L20 26h24z' fill='%23db2777'/%3E%3Cpath d='M12 10h40L44 26H20z' fill='%23f9a8d4' opacity='.4'/%3E%3Cpath d='M4 26h16L32 58z' fill='%23ec4899' opacity='.3'/%3E%3Cpath d='M44 26h16L32 58z' fill='%23be185d' opacity='.3'/%3E%3Cpath d='M20 26l12-16 12 16' fill='%23fce7f3' opacity='.3'/%3E%3C/svg%3E">\n<style>${t()}</style>\n<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.min.js"><\/script>\n</head>\n<body>\n${i(m)}\n${o()}\n<script>\n${l(m,d)}\n${s()}\n${e()}\n${n()}\n${r()}\n${p()}\n${a()}\n<\/script>\n</body>\n</html>`}