@pellux/goodvibes-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/.goodvibes/GOODVIBES.md +35 -0
  2. package/.goodvibes/agents/reviewer.md +48 -0
  3. package/.goodvibes/skills/add-provider/SKILL.md +199 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +74 -0
  6. package/bin/goodvibes-agent.ts +2 -0
  7. package/docs/README.md +23 -0
  8. package/docs/deployment-and-services.md +57 -0
  9. package/docs/getting-started.md +53 -0
  10. package/docs/release-and-publishing.md +46 -0
  11. package/package.json +134 -0
  12. package/scripts/check-bun.sh +20 -0
  13. package/src/audio/player.ts +156 -0
  14. package/src/audio/spoken-turn-controller.ts +203 -0
  15. package/src/audio/spoken-turn-model-routing.ts +117 -0
  16. package/src/audio/spoken-turn-wiring.ts +44 -0
  17. package/src/audio/text-chunker.ts +110 -0
  18. package/src/cli/bundle-command.ts +227 -0
  19. package/src/cli/completion.ts +90 -0
  20. package/src/cli/config-overrides.ts +159 -0
  21. package/src/cli/endpoints.ts +63 -0
  22. package/src/cli/entrypoint.ts +172 -0
  23. package/src/cli/help.ts +299 -0
  24. package/src/cli/index.ts +11 -0
  25. package/src/cli/management-commands.ts +426 -0
  26. package/src/cli/management.ts +744 -0
  27. package/src/cli/network-posture.ts +46 -0
  28. package/src/cli/package-verification.ts +123 -0
  29. package/src/cli/parser.ts +369 -0
  30. package/src/cli/provider-auth-routes.ts +22 -0
  31. package/src/cli/provider-classification.ts +107 -0
  32. package/src/cli/redaction.ts +105 -0
  33. package/src/cli/service-command.ts +26 -0
  34. package/src/cli/service-posture.ts +482 -0
  35. package/src/cli/status.ts +383 -0
  36. package/src/cli/surface-command.ts +247 -0
  37. package/src/cli/tui-startup.ts +32 -0
  38. package/src/cli/types.ts +69 -0
  39. package/src/cli-flags.ts +21 -0
  40. package/src/config/goodvibes-home-audit.ts +465 -0
  41. package/src/config/index.ts +57 -0
  42. package/src/config/provider-model.ts +23 -0
  43. package/src/config/secret-config.ts +119 -0
  44. package/src/config/secrets.ts +71 -0
  45. package/src/config/surface.ts +1 -0
  46. package/src/core/composer-state.ts +61 -0
  47. package/src/core/conversation-rendering.ts +359 -0
  48. package/src/core/conversation.ts +551 -0
  49. package/src/core/history.ts +45 -0
  50. package/src/core/orchestrator.ts +7 -0
  51. package/src/core/system-message-router.ts +171 -0
  52. package/src/daemon/cli.ts +55 -0
  53. package/src/daemon/safe-serve.ts +61 -0
  54. package/src/input/agent-workspace.ts +428 -0
  55. package/src/input/autocomplete.ts +96 -0
  56. package/src/input/bookmark-modal.ts +115 -0
  57. package/src/input/command-args-hint.ts +36 -0
  58. package/src/input/command-registry.ts +329 -0
  59. package/src/input/commands/agent-externalized-tui.ts +73 -0
  60. package/src/input/commands/agent-workspace-runtime.ts +17 -0
  61. package/src/input/commands/branch-runtime.ts +72 -0
  62. package/src/input/commands/cloudflare-runtime.ts +370 -0
  63. package/src/input/commands/config.ts +18 -0
  64. package/src/input/commands/control-room-runtime.ts +255 -0
  65. package/src/input/commands/conversation-runtime.ts +207 -0
  66. package/src/input/commands/discovery-runtime.ts +52 -0
  67. package/src/input/commands/eval.ts +204 -0
  68. package/src/input/commands/experience-runtime.ts +278 -0
  69. package/src/input/commands/guidance-runtime.ts +106 -0
  70. package/src/input/commands/health-runtime.ts +434 -0
  71. package/src/input/commands/hooks-runtime.ts +148 -0
  72. package/src/input/commands/incident-runtime.ts +95 -0
  73. package/src/input/commands/integration-runtime.ts +394 -0
  74. package/src/input/commands/intelligence-runtime.ts +223 -0
  75. package/src/input/commands/knowledge.ts +531 -0
  76. package/src/input/commands/local-auth-runtime.ts +105 -0
  77. package/src/input/commands/local-provider-runtime.ts +170 -0
  78. package/src/input/commands/local-runtime.ts +392 -0
  79. package/src/input/commands/local-setup-review.ts +199 -0
  80. package/src/input/commands/local-setup-transfer.ts +135 -0
  81. package/src/input/commands/local-setup.ts +282 -0
  82. package/src/input/commands/managed-runtime.ts +209 -0
  83. package/src/input/commands/marketplace-runtime.ts +290 -0
  84. package/src/input/commands/mcp-runtime.ts +432 -0
  85. package/src/input/commands/memory-product-runtime.ts +111 -0
  86. package/src/input/commands/memory.ts +151 -0
  87. package/src/input/commands/notify-runtime.ts +83 -0
  88. package/src/input/commands/onboarding-runtime.ts +14 -0
  89. package/src/input/commands/operator-panel-runtime.ts +146 -0
  90. package/src/input/commands/operator-runtime.ts +392 -0
  91. package/src/input/commands/planning-runtime.ts +205 -0
  92. package/src/input/commands/platform-access-runtime.ts +422 -0
  93. package/src/input/commands/platform-services-runtime.ts +246 -0
  94. package/src/input/commands/policy-dispatch.ts +339 -0
  95. package/src/input/commands/policy.ts +17 -0
  96. package/src/input/commands/product-runtime.ts +351 -0
  97. package/src/input/commands/profile-sync-runtime.ts +99 -0
  98. package/src/input/commands/provider-accounts-runtime.ts +113 -0
  99. package/src/input/commands/provider.ts +363 -0
  100. package/src/input/commands/qrcode-runtime.ts +20 -0
  101. package/src/input/commands/quit-shared.ts +162 -0
  102. package/src/input/commands/recall-bundle.ts +132 -0
  103. package/src/input/commands/recall-capture.ts +152 -0
  104. package/src/input/commands/recall-query.ts +229 -0
  105. package/src/input/commands/recall-review.ts +98 -0
  106. package/src/input/commands/recall-shared.ts +22 -0
  107. package/src/input/commands/remote-runtime-pool.ts +106 -0
  108. package/src/input/commands/remote-runtime-setup.ts +199 -0
  109. package/src/input/commands/remote-runtime.ts +431 -0
  110. package/src/input/commands/replay-runtime.ts +18 -0
  111. package/src/input/commands/runtime-services.ts +291 -0
  112. package/src/input/commands/schedule-runtime.ts +91 -0
  113. package/src/input/commands/services-runtime.ts +209 -0
  114. package/src/input/commands/session-content.ts +408 -0
  115. package/src/input/commands/session-workflow.ts +464 -0
  116. package/src/input/commands/session.ts +375 -0
  117. package/src/input/commands/settings-sync-runtime.ts +174 -0
  118. package/src/input/commands/share-runtime.ts +119 -0
  119. package/src/input/commands/shell-core.ts +307 -0
  120. package/src/input/commands/skills-runtime.ts +221 -0
  121. package/src/input/commands/subscription-runtime.ts +434 -0
  122. package/src/input/commands/tasks-runtime.ts +230 -0
  123. package/src/input/commands/teamwork-runtime.ts +339 -0
  124. package/src/input/commands/teleport-runtime.ts +57 -0
  125. package/src/input/commands/tts-runtime.ts +29 -0
  126. package/src/input/commands/work-plan-runtime.ts +169 -0
  127. package/src/input/commands.ts +131 -0
  128. package/src/input/feed-context-factory.ts +254 -0
  129. package/src/input/file-picker.ts +192 -0
  130. package/src/input/handler-command-route.ts +180 -0
  131. package/src/input/handler-content-actions.ts +497 -0
  132. package/src/input/handler-feed-routes.ts +648 -0
  133. package/src/input/handler-feed.ts +452 -0
  134. package/src/input/handler-interactions.ts +281 -0
  135. package/src/input/handler-modal-routes.ts +418 -0
  136. package/src/input/handler-modal-stack.ts +263 -0
  137. package/src/input/handler-modal-token-routes.ts +329 -0
  138. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  139. package/src/input/handler-onboarding.ts +620 -0
  140. package/src/input/handler-picker-routes.ts +472 -0
  141. package/src/input/handler-prompt-buffer.ts +320 -0
  142. package/src/input/handler-shortcuts.ts +213 -0
  143. package/src/input/handler-ui-state.ts +372 -0
  144. package/src/input/handler.ts +729 -0
  145. package/src/input/input-history.ts +297 -0
  146. package/src/input/keybindings.ts +292 -0
  147. package/src/input/mcp-workspace.ts +554 -0
  148. package/src/input/model-picker-provider-filter.ts +28 -0
  149. package/src/input/model-picker-types.ts +137 -0
  150. package/src/input/model-picker.ts +797 -0
  151. package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
  152. package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
  153. package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
  154. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
  155. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
  156. package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
  157. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
  158. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
  159. package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
  160. package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
  161. package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
  162. package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
  163. package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
  164. package/src/input/onboarding/onboarding-wizard.ts +711 -0
  165. package/src/input/panel-integration-actions.ts +78 -0
  166. package/src/input/profile-picker-modal.ts +222 -0
  167. package/src/input/search.ts +100 -0
  168. package/src/input/selection-modal.ts +163 -0
  169. package/src/input/selection.ts +135 -0
  170. package/src/input/session-picker-modal.ts +136 -0
  171. package/src/input/settings-modal-behavior.ts +37 -0
  172. package/src/input/settings-modal-secrets.ts +41 -0
  173. package/src/input/settings-modal-subscriptions.ts +95 -0
  174. package/src/input/settings-modal-types.ts +91 -0
  175. package/src/input/settings-modal.ts +793 -0
  176. package/src/input/submission-intent.ts +17 -0
  177. package/src/input/submission-router.ts +59 -0
  178. package/src/input/tts-settings-actions.ts +100 -0
  179. package/src/main.ts +792 -0
  180. package/src/mcp/runtime-reload.ts +81 -0
  181. package/src/panels/agent-inspector-panel.ts +521 -0
  182. package/src/panels/agent-inspector-shared.ts +94 -0
  183. package/src/panels/agent-logs-panel.ts +559 -0
  184. package/src/panels/agent-logs-shared.ts +129 -0
  185. package/src/panels/approval-panel.ts +150 -0
  186. package/src/panels/automation-control-panel.ts +212 -0
  187. package/src/panels/base-panel.ts +254 -0
  188. package/src/panels/builtin/agent.ts +117 -0
  189. package/src/panels/builtin/development.ts +31 -0
  190. package/src/panels/builtin/knowledge.ts +26 -0
  191. package/src/panels/builtin/operations.ts +349 -0
  192. package/src/panels/builtin/session.ts +129 -0
  193. package/src/panels/builtin/shared.ts +274 -0
  194. package/src/panels/builtin-panels.ts +23 -0
  195. package/src/panels/cockpit-panel.ts +183 -0
  196. package/src/panels/communication-panel.ts +153 -0
  197. package/src/panels/confirm-state.ts +61 -0
  198. package/src/panels/context-visualizer-panel.ts +204 -0
  199. package/src/panels/control-plane-panel.ts +211 -0
  200. package/src/panels/cost-tracker-panel.ts +444 -0
  201. package/src/panels/debug-panel.ts +432 -0
  202. package/src/panels/diff-panel.ts +520 -0
  203. package/src/panels/docs-panel.ts +283 -0
  204. package/src/panels/eval-panel.ts +399 -0
  205. package/src/panels/file-explorer-panel.ts +584 -0
  206. package/src/panels/file-preview-panel.ts +434 -0
  207. package/src/panels/forensics-panel.ts +364 -0
  208. package/src/panels/git-panel.ts +638 -0
  209. package/src/panels/hooks-panel.ts +239 -0
  210. package/src/panels/incident-review-panel.ts +197 -0
  211. package/src/panels/index.ts +46 -0
  212. package/src/panels/intelligence-panel.ts +176 -0
  213. package/src/panels/knowledge-panel.ts +345 -0
  214. package/src/panels/local-auth-panel.ts +130 -0
  215. package/src/panels/marketplace-panel.ts +212 -0
  216. package/src/panels/memory-panel.ts +225 -0
  217. package/src/panels/ops-control-panel.ts +150 -0
  218. package/src/panels/ops-strategy-panel.ts +235 -0
  219. package/src/panels/orchestration-panel.ts +273 -0
  220. package/src/panels/panel-list-panel.ts +509 -0
  221. package/src/panels/panel-manager.ts +570 -0
  222. package/src/panels/panel-picker.ts +106 -0
  223. package/src/panels/plan-dashboard-panel.ts +274 -0
  224. package/src/panels/plugins-panel.ts +178 -0
  225. package/src/panels/policy-panel.ts +308 -0
  226. package/src/panels/polish.ts +717 -0
  227. package/src/panels/project-planning-panel.ts +711 -0
  228. package/src/panels/provider-account-snapshot.ts +259 -0
  229. package/src/panels/provider-accounts-panel.ts +218 -0
  230. package/src/panels/provider-health-domains.ts +215 -0
  231. package/src/panels/provider-health-panel.ts +727 -0
  232. package/src/panels/provider-health-tracker.ts +115 -0
  233. package/src/panels/provider-stats-panel.ts +366 -0
  234. package/src/panels/qr-panel.ts +182 -0
  235. package/src/panels/remote-panel.ts +449 -0
  236. package/src/panels/routes-panel.ts +178 -0
  237. package/src/panels/sandbox-panel.ts +283 -0
  238. package/src/panels/schedule-panel.ts +329 -0
  239. package/src/panels/scrollable-list-panel.ts +491 -0
  240. package/src/panels/search-focus.ts +32 -0
  241. package/src/panels/security-panel.ts +295 -0
  242. package/src/panels/services-panel.ts +231 -0
  243. package/src/panels/session-browser-panel.ts +400 -0
  244. package/src/panels/session-maintenance.ts +125 -0
  245. package/src/panels/settings-sync-panel.ts +120 -0
  246. package/src/panels/skills-panel.ts +431 -0
  247. package/src/panels/subscription-panel.ts +263 -0
  248. package/src/panels/symbol-outline-panel.ts +486 -0
  249. package/src/panels/system-messages-panel.ts +230 -0
  250. package/src/panels/tasks-panel.ts +399 -0
  251. package/src/panels/thinking-panel.ts +304 -0
  252. package/src/panels/token-budget-panel.ts +475 -0
  253. package/src/panels/tool-inspector-panel.ts +429 -0
  254. package/src/panels/types.ts +54 -0
  255. package/src/panels/watchers-panel.ts +193 -0
  256. package/src/panels/work-plan-panel.ts +175 -0
  257. package/src/panels/worktree-panel.ts +182 -0
  258. package/src/panels/wrfc-panel.ts +609 -0
  259. package/src/permissions/prompt.ts +165 -0
  260. package/src/planning/project-planning-coordinator.ts +543 -0
  261. package/src/plugins/loader.ts +15 -0
  262. package/src/renderer/agent-detail-modal.ts +331 -0
  263. package/src/renderer/agent-workspace.ts +238 -0
  264. package/src/renderer/ansi-sanitize.ts +76 -0
  265. package/src/renderer/autocomplete-overlay.ts +154 -0
  266. package/src/renderer/block-actions.ts +76 -0
  267. package/src/renderer/bookmark-modal.ts +101 -0
  268. package/src/renderer/bottom-bar.ts +58 -0
  269. package/src/renderer/buffer.ts +113 -0
  270. package/src/renderer/code-block.ts +373 -0
  271. package/src/renderer/compositor.ts +283 -0
  272. package/src/renderer/context-inspector.ts +219 -0
  273. package/src/renderer/conversation-layout.ts +67 -0
  274. package/src/renderer/conversation-overlays.ts +140 -0
  275. package/src/renderer/conversation-surface.ts +260 -0
  276. package/src/renderer/diff-view.ts +132 -0
  277. package/src/renderer/diff.ts +130 -0
  278. package/src/renderer/file-picker-overlay.ts +101 -0
  279. package/src/renderer/file-tree.ts +153 -0
  280. package/src/renderer/fullscreen-primitives.ts +130 -0
  281. package/src/renderer/fullscreen-workspace.ts +199 -0
  282. package/src/renderer/git-status.ts +89 -0
  283. package/src/renderer/help-overlay.ts +267 -0
  284. package/src/renderer/history-search-overlay.ts +73 -0
  285. package/src/renderer/layout-engine.ts +97 -0
  286. package/src/renderer/layout.ts +32 -0
  287. package/src/renderer/live-tail-modal.ts +156 -0
  288. package/src/renderer/markdown.ts +635 -0
  289. package/src/renderer/mcp-workspace.ts +237 -0
  290. package/src/renderer/modal-factory.ts +467 -0
  291. package/src/renderer/modal-utils.ts +24 -0
  292. package/src/renderer/model-picker-overlay.ts +473 -0
  293. package/src/renderer/model-workspace.ts +488 -0
  294. package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
  295. package/src/renderer/overlay-box.ts +146 -0
  296. package/src/renderer/overlay-viewport.ts +104 -0
  297. package/src/renderer/panel-composite.ts +158 -0
  298. package/src/renderer/panel-picker-overlay.ts +202 -0
  299. package/src/renderer/panel-tab-bar.ts +69 -0
  300. package/src/renderer/panel-workspace-bar.ts +42 -0
  301. package/src/renderer/process-indicator.ts +96 -0
  302. package/src/renderer/process-modal.ts +656 -0
  303. package/src/renderer/process-summary.ts +67 -0
  304. package/src/renderer/profile-picker-modal.ts +129 -0
  305. package/src/renderer/progress.ts +98 -0
  306. package/src/renderer/qr-renderer.ts +120 -0
  307. package/src/renderer/search-overlay.ts +54 -0
  308. package/src/renderer/selection-modal-overlay.ts +214 -0
  309. package/src/renderer/semantic-diff.ts +369 -0
  310. package/src/renderer/session-picker-modal.ts +127 -0
  311. package/src/renderer/settings-modal-helpers.ts +193 -0
  312. package/src/renderer/settings-modal.ts +537 -0
  313. package/src/renderer/shell-surface.ts +88 -0
  314. package/src/renderer/status-glyphs.ts +21 -0
  315. package/src/renderer/status-token.ts +67 -0
  316. package/src/renderer/surface-layout.ts +101 -0
  317. package/src/renderer/syntax-highlighter.ts +542 -0
  318. package/src/renderer/system-message.ts +83 -0
  319. package/src/renderer/tab-strip.ts +108 -0
  320. package/src/renderer/text-layout.ts +31 -0
  321. package/src/renderer/thinking.ts +17 -0
  322. package/src/renderer/tool-call.ts +234 -0
  323. package/src/renderer/ui-factory.ts +524 -0
  324. package/src/renderer/ui-primitives.ts +96 -0
  325. package/src/runtime/bootstrap-command-context.ts +278 -0
  326. package/src/runtime/bootstrap-command-parts.ts +386 -0
  327. package/src/runtime/bootstrap-core.ts +540 -0
  328. package/src/runtime/bootstrap-hook-bridge.ts +112 -0
  329. package/src/runtime/bootstrap-shell.ts +283 -0
  330. package/src/runtime/bootstrap.ts +575 -0
  331. package/src/runtime/cloudflare-control-plane.ts +349 -0
  332. package/src/runtime/context.ts +142 -0
  333. package/src/runtime/diagnostics/panels/index.ts +24 -0
  334. package/src/runtime/diagnostics/panels/ops.ts +156 -0
  335. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  336. package/src/runtime/diagnostics/panels/policy.ts +177 -0
  337. package/src/runtime/index.ts +662 -0
  338. package/src/runtime/onboarding/apply.ts +642 -0
  339. package/src/runtime/onboarding/derivation.ts +534 -0
  340. package/src/runtime/onboarding/index.ts +7 -0
  341. package/src/runtime/onboarding/markers.ts +148 -0
  342. package/src/runtime/onboarding/snapshot.ts +406 -0
  343. package/src/runtime/onboarding/state.ts +141 -0
  344. package/src/runtime/onboarding/types.ts +404 -0
  345. package/src/runtime/onboarding/verify.ts +171 -0
  346. package/src/runtime/operator-token-cleanup.ts +27 -0
  347. package/src/runtime/perf/panel-contracts.ts +32 -0
  348. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  349. package/src/runtime/sandbox-public-gaps.ts +358 -0
  350. package/src/runtime/services.ts +670 -0
  351. package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
  352. package/src/runtime/store/domains/index.ts +222 -0
  353. package/src/runtime/store/domains/panels.ts +117 -0
  354. package/src/runtime/store/domains/ui-perf.ts +103 -0
  355. package/src/runtime/store/index.ts +305 -0
  356. package/src/runtime/store/selectors/index.ts +359 -0
  357. package/src/runtime/store/state.ts +145 -0
  358. package/src/runtime/surface-feature-flags.ts +65 -0
  359. package/src/runtime/terminal-output-guard.ts +228 -0
  360. package/src/runtime/ui/index.ts +39 -0
  361. package/src/runtime/ui/model-picker/data-provider.ts +182 -0
  362. package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
  363. package/src/runtime/ui/model-picker/index.ts +59 -0
  364. package/src/runtime/ui/model-picker/types.ts +149 -0
  365. package/src/runtime/ui/provider-health/data-provider.ts +244 -0
  366. package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
  367. package/src/runtime/ui/provider-health/index.ts +46 -0
  368. package/src/runtime/ui/provider-health/types.ts +146 -0
  369. package/src/runtime/ui-events.ts +1 -0
  370. package/src/runtime/ui-read-model-helpers.ts +1 -0
  371. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
  372. package/src/runtime/ui-read-models-observability-options.ts +1 -0
  373. package/src/runtime/ui-read-models-observability-remote.ts +1 -0
  374. package/src/runtime/ui-read-models-observability-security.ts +1 -0
  375. package/src/runtime/ui-read-models-observability-system.ts +1 -0
  376. package/src/runtime/ui-read-models-observability.ts +1 -0
  377. package/src/runtime/ui-read-models.ts +61 -0
  378. package/src/runtime/ui-service-queries.ts +1 -0
  379. package/src/runtime/ui-services.ts +190 -0
  380. package/src/scripts/process-messages.ts +42 -0
  381. package/src/shell/blocking-input.ts +98 -0
  382. package/src/shell/service-settings-sync.ts +273 -0
  383. package/src/shell/ui-openers.ts +352 -0
  384. package/src/tools/index.ts +1 -0
  385. package/src/tools/wrfc-agent-guard.ts +49 -0
  386. package/src/types/grid.ts +48 -0
  387. package/src/types/sql-js.d.ts +15 -0
  388. package/src/utils/clipboard.ts +22 -0
  389. package/src/utils/splash-lines.ts +46 -0
  390. package/src/utils/terminal-width.ts +185 -0
  391. package/src/verification/live-verifier.ts +430 -0
  392. package/src/verification/verification-ledger.ts +242 -0
  393. package/src/version.ts +17 -0
  394. package/src/widget/index.ts +2 -0
  395. package/src/widget/types.ts +9 -0
  396. package/src/widget/widget.ts +8 -0
  397. package/src/work-plans/work-plan-store.ts +374 -0
  398. package/tsconfig.json +18 -0
@@ -0,0 +1,170 @@
1
+ import { join } from 'node:path';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { writeFile, unlink } from 'node:fs/promises';
4
+ import type { CommandRegistry } from '../command-registry.ts';
5
+ import { fetchModelContextWindows } from '@pellux/goodvibes-sdk/platform/discovery';
6
+ import type { CustomProviderConfig } from '@pellux/goodvibes-sdk/platform/providers';
7
+ import { requireProviderApi, requireShellPaths } from './runtime-services.ts';
8
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
9
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../../config/surface.ts';
10
+
11
+ function isValidProviderName(name: string): boolean {
12
+ return /^[a-zA-Z0-9_-]+$/.test(name);
13
+ }
14
+
15
+ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry): void {
16
+ registry.register({
17
+ name: 'provider',
18
+ aliases: ['p'],
19
+ description: 'Switch provider or manage custom providers (add/remove)',
20
+ usage: '[add <name> <baseURL> [apiKey] | remove <name> | <provider-name>]',
21
+ argsHint: '[add|remove|name]',
22
+ async handler(args, ctx) {
23
+ const shellPaths = requireShellPaths(ctx);
24
+ if (args[0] === 'add') {
25
+ const addArgs = args.slice(1);
26
+ if (addArgs.length < 2) {
27
+ ctx.print('Usage: /provider add <name> <baseURL> [apiKey]\nExample: /provider add my-server http://192.168.0.85:8001/v1');
28
+ return;
29
+ }
30
+ const [name, baseURL, apiKey] = addArgs;
31
+ if (!isValidProviderName(name)) {
32
+ ctx.print('Error: Provider name must contain only letters, numbers, hyphens, and underscores.');
33
+ return;
34
+ }
35
+ let parsedUrl: URL;
36
+ try {
37
+ parsedUrl = new URL(baseURL);
38
+ } catch {
39
+ ctx.print(`Error: '${baseURL}' is not a valid URL. Example: http://192.168.0.85:8001/v1`);
40
+ return;
41
+ }
42
+ const providersDir = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'providers');
43
+ const providerFile = join(providersDir, `${name}.json`);
44
+ if (existsSync(providerFile)) {
45
+ ctx.print(`Error: Provider '${name}' already exists at ${providerFile}\nRemove it first with: /provider remove ${name}`);
46
+ return;
47
+ }
48
+
49
+ ctx.print(`Probing ${baseURL}/models ...`);
50
+ let discoveredModelIds: string[] = [];
51
+ try {
52
+ const controller = new AbortController();
53
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
54
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
55
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
56
+ const res = await fetch(`${baseURL}/models`, { signal: controller.signal, headers });
57
+ clearTimeout(timeoutId);
58
+ if (res.ok) {
59
+ const body = await res.json() as unknown;
60
+ if (body && typeof body === 'object' && 'data' in body && Array.isArray((body as Record<string, unknown>).data)) {
61
+ discoveredModelIds = ((body as { data: unknown[] }).data)
62
+ .filter((m): m is Record<string, unknown> => typeof m === 'object' && m !== null && 'id' in m)
63
+ .map((m) => String(m.id))
64
+ .filter(Boolean);
65
+ }
66
+ }
67
+ } catch {
68
+ ctx.print(`Could not reach ${baseURL}/models — creating provider with a minimal starter config.`);
69
+ }
70
+
71
+ let contextWindows: Record<string, number> = {};
72
+ if (discoveredModelIds.length > 0) {
73
+ if (parsedUrl.protocol === 'http:') {
74
+ try {
75
+ contextWindows = await fetchModelContextWindows(parsedUrl.hostname, parseInt(parsedUrl.port) || 80, 'unknown', discoveredModelIds);
76
+ } catch {}
77
+ } else {
78
+ ctx.print('Note: Context window detection is only supported for http:// URLs. Using defaults.');
79
+ }
80
+ }
81
+ const defaultModel = `${name}-model`;
82
+ const models: CustomProviderConfig['models'] = discoveredModelIds.length === 0
83
+ ? [{
84
+ id: defaultModel,
85
+ displayName: defaultModel,
86
+ contextWindow: 8192,
87
+ capabilities: { toolCalling: true, codeEditing: true, reasoning: false, multimodal: false },
88
+ }]
89
+ : discoveredModelIds.map((id) => ({
90
+ id,
91
+ displayName: id,
92
+ contextWindow: contextWindows[id] ?? 8192,
93
+ capabilities: { toolCalling: true, codeEditing: true, reasoning: false, multimodal: false },
94
+ }));
95
+ const config: CustomProviderConfig = {
96
+ name,
97
+ displayName: name,
98
+ type: 'openai-compat',
99
+ baseURL,
100
+ ...(apiKey ? { apiKey } : {}),
101
+ models,
102
+ };
103
+ try {
104
+ mkdirSync(providersDir, { recursive: true });
105
+ await writeFile(providerFile, JSON.stringify(config, null, 2), 'utf-8');
106
+ } catch (e) {
107
+ ctx.print(`Error writing provider file: ${summarizeError(e)}`);
108
+ return;
109
+ }
110
+ ctx.print(`Provider '${name}' added with ${models.length} model(s):\n${discoveredModelIds.length > 0 ? discoveredModelIds.map((id) => ` • ${id} (${(contextWindows[id] ?? 8192).toLocaleString()} ctx)`).join('\n') : ` • ${defaultModel} (starter entry)`}\nThe file watcher will auto-register it shortly.`);
111
+ return;
112
+ }
113
+
114
+ if (args[0] === 'remove' || args[0] === 'rm') {
115
+ const name = args[1];
116
+ if (!name) {
117
+ ctx.print('Usage: /provider remove <name>');
118
+ return;
119
+ }
120
+ if (!isValidProviderName(name)) {
121
+ ctx.print('Error: Provider name must contain only letters, numbers, hyphens, and underscores.');
122
+ return;
123
+ }
124
+ const providerFile = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'providers', `${name}.json`);
125
+ if (!existsSync(providerFile)) {
126
+ ctx.print(`Error: No custom provider '${name}' found at ${providerFile}`);
127
+ return;
128
+ }
129
+ try {
130
+ await unlink(providerFile);
131
+ ctx.print(`Provider '${name}' removed. The file watcher will deregister it shortly.`);
132
+ } catch (e) {
133
+ ctx.print(`Error removing provider file: ${summarizeError(e)}`);
134
+ }
135
+ return;
136
+ }
137
+
138
+ if (args.length === 0) {
139
+ if (ctx.openProviderPicker) {
140
+ ctx.openProviderPicker();
141
+ return;
142
+ }
143
+ const providers = requireProviderApi(ctx).listProviderIds();
144
+ ctx.print(['Available providers:', ...providers.map((provider) => ` ${provider === ctx.session.runtime.provider ? '▶' : ' '} ${provider}`)].join('\n'));
145
+ return;
146
+ }
147
+
148
+ const providerName = args[0];
149
+ const providerApi = requireProviderApi(ctx);
150
+ const selectable = await providerApi.listModels({
151
+ providerId: providerName,
152
+ selectableOnly: true,
153
+ });
154
+ const match = selectable[0];
155
+ if (!match) {
156
+ ctx.print(`Unknown provider: ${providerName}. Available: ${providerApi.listProviderIds().join(', ')}`);
157
+ return;
158
+ }
159
+ try {
160
+ const selected = await providerApi.selectModel(match.registryKey);
161
+ ctx.session.runtime.model = selected.registryKey;
162
+ ctx.session.runtime.provider = selected.providerId;
163
+ ctx.platform.configManager.set('provider.model', selected.registryKey);
164
+ ctx.print(`Switched to provider: ${selected.providerId} (model: ${selected.modelId})`);
165
+ } catch (e) {
166
+ ctx.print(`Error: ${summarizeError(e)}`);
167
+ }
168
+ },
169
+ });
170
+ }
@@ -0,0 +1,392 @@
1
+ import { dirname, resolve } from 'path';
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import type { CommandRegistry, CommandContext } from '../command-registry.ts';
5
+ import type { SelectionItem } from '../selection-modal.ts';
6
+ import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
7
+ import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
8
+ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config';
9
+ import { openCommandPanel, requireBookmarkManager, requireProviderApi, requireSecretsManager } from './runtime-services.ts';
10
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
11
+
12
+ function isGoodVibesSecretRefInput(value: string): boolean {
13
+ const normalized = value.trim();
14
+ return normalized.startsWith('goodvibes://secrets/') && isSecretRefInput(normalized);
15
+ }
16
+
17
+ function isMalformedGoodVibesSecretRefInput(value: string): boolean {
18
+ const normalized = value.trim();
19
+ return normalized.startsWith('goodvibes://') && !isGoodVibesSecretRefInput(normalized);
20
+ }
21
+
22
+ function toggleBlocks(typeFilter: string, collapsed: boolean, ctx: CommandContext): void {
23
+ const VALID_TYPES = ['all', 'thinking', 'tool', 'code'] as const;
24
+ if (!VALID_TYPES.includes(typeFilter as typeof VALID_TYPES[number])) {
25
+ ctx.print(`Unknown type: ${typeFilter}\nValid types: ${VALID_TYPES.join(', ')}`);
26
+ return;
27
+ }
28
+ const blockRegistry = ctx.session.conversationManager.getBlockRegistry();
29
+ if (!blockRegistry || blockRegistry.length === 0) {
30
+ ctx.print('No blocks found.');
31
+ return;
32
+ }
33
+ let count = 0;
34
+ for (let i = 0; i < blockRegistry.length; i++) {
35
+ const block = blockRegistry[i];
36
+ const matchesType = typeFilter === 'all'
37
+ || (typeFilter === 'tool' && block.type === 'tool')
38
+ || (typeFilter === 'code' && block.type === 'code')
39
+ || (typeFilter === 'thinking' && block.type === 'thinking');
40
+ if (!matchesType) continue;
41
+ const isCurrentlyCollapsed = ctx.session.conversationManager.isCollapsed(i);
42
+ if (collapsed ? !isCurrentlyCollapsed : isCurrentlyCollapsed) {
43
+ ctx.session.conversationManager.toggleCollapseAtLine(block.startLine);
44
+ count++;
45
+ }
46
+ }
47
+ ctx.print(`${collapsed ? 'Collapsed' : 'Expanded'} ${count} block${count !== 1 ? 's' : ''}${typeFilter !== 'all' ? ` (${typeFilter})` : ''}.`);
48
+ ctx.renderRequest();
49
+ }
50
+
51
+ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
52
+ registry.register({
53
+ name: 'incident-review',
54
+ aliases: [],
55
+ description: 'Alias for /incident open',
56
+ usage: '',
57
+ handler(_args, ctx) {
58
+ if (ctx.openIncidentPanel) {
59
+ ctx.openIncidentPanel();
60
+ return;
61
+ }
62
+ ctx.print('Incident panel is not available in this runtime.');
63
+ },
64
+ });
65
+
66
+ registry.register({
67
+ name: 'tools',
68
+ aliases: ['t'],
69
+ description: 'List available tools and review compact native tool capability surfaces',
70
+ usage: '[review|panel]',
71
+ handler(args, ctx) {
72
+ const sub = (args[0] ?? '').toLowerCase();
73
+ if (sub === 'panel' || sub === 'review') {
74
+ try {
75
+ openCommandPanel(ctx, 'tools');
76
+ } catch {
77
+ // Panel registry may be unavailable in lightweight command-only contexts.
78
+ }
79
+ if (sub === 'review') {
80
+ ctx.print([
81
+ 'Tool Surface Review',
82
+ ' Native file tools stay compact by default.',
83
+ ' Read/write/edit/notebook capabilities are available through the native tool stack, with detail routed to the tools panel and approval surfaces instead of transcript bloat.',
84
+ ' Shell and native tool approvals classify work into read, mutation, destructive, dependency, config, notebook, network, remote, and lifecycle risk families.',
85
+ ' Use /tools panel to inspect risk class, output-policy actions, spill posture, compact summaries, and approval posture for recent calls.',
86
+ ' Use /approval review shell or /approval review file when you need the action-specific why-prompted posture.',
87
+ ].join('\n'));
88
+ }
89
+ return;
90
+ }
91
+ const tools = ctx.extensions.toolRegistry.list();
92
+ if (ctx.openSelection) {
93
+ const items: SelectionItem[] = tools.map(t => ({
94
+ id: t.definition.name,
95
+ label: t.definition.name,
96
+ detail: typeof t.definition.description === 'string' ? t.definition.description.slice(0, 50) : '',
97
+ }));
98
+ ctx.openSelection('Available Tools', items, { allowSearch: true }, (result) => {
99
+ if (!result) return;
100
+ const tool = tools.find(t2 => t2.definition.name === result.item.id);
101
+ if (tool) ctx.print(`Tool: ${tool.definition.name}\n ${tool.definition.description ?? ''}`);
102
+ });
103
+ return;
104
+ }
105
+ ctx.print(['Available tools:', ...tools.map(t => ` • ${t.definition.name}`)].join('\n'));
106
+ },
107
+ });
108
+
109
+ registry.register({ name: 'expand', description: 'Expand blocks by type', usage: '[all|thinking|tool|code]', argsHint: '[all|thinking|tool|code]', handler(args, ctx) { toggleBlocks(args[0] || 'all', false, ctx); } });
110
+ registry.register({ name: 'collapse', description: 'Collapse blocks by type', usage: '[all|thinking|tool|code]', argsHint: '[all|thinking|tool|code]', handler(args, ctx) { toggleBlocks(args[0] || 'all', true, ctx); } });
111
+
112
+ registry.register({
113
+ name: 'bookmarks',
114
+ aliases: ['bm'],
115
+ description: 'List bookmarked blocks',
116
+ handler(_args, ctx) {
117
+ if (ctx.openBookmarkModal) {
118
+ ctx.openBookmarkModal();
119
+ return;
120
+ }
121
+ const bm = requireBookmarkManager(ctx);
122
+ const entries = bm.list();
123
+ if (ctx.openSelection) {
124
+ const deleteAction = new Map([['d', 'delete' as const]]);
125
+ const items: SelectionItem[] = entries.length === 0
126
+ ? [{ id: '_empty', label: 'No bookmarks', detail: 'Use Ctrl+B to bookmark' }]
127
+ : entries.map(entry => ({ id: entry.key, label: entry.label, detail: new Date(entry.timestamp).toLocaleTimeString(), actions: '[d] delete' }));
128
+ ctx.openSelection('Bookmarks', items, { allowSearch: true, customActions: deleteAction }, (result) => {
129
+ if (!result) return;
130
+ if (result.action === 'delete') {
131
+ bm.toggle(result.item.id);
132
+ ctx.print(`Bookmark removed: ${result.item.id}`);
133
+ } else {
134
+ ctx.jumpToBookmark?.(result.item.id);
135
+ }
136
+ });
137
+ return;
138
+ }
139
+ ctx.print(['Bookmarks:', '', ...entries.map(entry => ` ${entry.key.padEnd(32)} ${entry.label} (${new Date(entry.timestamp).toLocaleTimeString()})`)].join('\n'));
140
+ },
141
+ });
142
+
143
+ registry.register({
144
+ name: 'secrets',
145
+ description: 'Manage hierarchy-aware secrets, external secret refs, and secure/plaintext storage policy controls',
146
+ usage: 'set <KEY> <value> [--user|--project] [--secure|--plaintext] | link <KEY> <secret-ref> [--user|--project] [--secure|--plaintext] | get <KEY> | test <secret-ref> | providers | list | delete <KEY> [--user|--project] [--secure|--plaintext]',
147
+ argsHint: '<set|link|get|test|providers|list|delete> [KEY]',
148
+ async handler(args, ctx) {
149
+ const mgr = requireSecretsManager(ctx);
150
+ const [sub, ...rest] = args;
151
+ if (!sub || sub === 'list') {
152
+ const records = await mgr.listDetailed();
153
+ const storedRecords = records.filter((record) => record.source !== 'env');
154
+ ctx.print(storedRecords.length === 0
155
+ ? '[secrets] No secrets stored. Use: /secrets set <KEY> <value>'
156
+ : [
157
+ '[secrets] Stored keys:',
158
+ ...storedRecords.map((record) => ` ${record.key} (${record.source}${record.refSource ? `, ref:${record.refSource}` : ''}${record.overriddenByEnv ? ', env override' : ''})`),
159
+ ].join('\n'));
160
+ return;
161
+ }
162
+ if (sub === 'providers') {
163
+ ctx.print([
164
+ '[secrets] Built-in secret providers:',
165
+ ...BUILTIN_SECRET_PROVIDER_SOURCES.map((source) => ` ${source}`),
166
+ '',
167
+ 'Examples:',
168
+ ' /secrets link OPENAI_API_KEY goodvibes://secrets/env/OPENAI_API_KEY',
169
+ ' /secrets link SLACK_BOT_TOKEN goodvibes://secrets/bitwarden?item=GoodVibes%20Slack&field=password&sessionEnv=BW_SESSION',
170
+ ' /secrets link SLACK_BOT_TOKEN goodvibes://secrets/vaultwarden?item=GoodVibes%20Slack&field=password&server=https%3A%2F%2Fvault.example.test',
171
+ ' /secrets link STRIPE_TOKEN goodvibes://secrets/bws/00000000-0000-0000-0000-000000000000?field=value&accessTokenEnv=BWS_ACCESS_TOKEN',
172
+ ' /secrets link OPENAI_API_KEY goodvibes://secrets/1password?vault=Private&item=GoodVibes%20OpenAI&field=API%20Key',
173
+ ].join('\n'));
174
+ return;
175
+ }
176
+ if (sub === 'test') {
177
+ const refText = rest.join(' ').trim();
178
+ if (!refText) {
179
+ ctx.print('[secrets] Usage: /secrets test <secret-ref>');
180
+ return;
181
+ }
182
+ if (!isGoodVibesSecretRefInput(refText)) {
183
+ ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
184
+ return;
185
+ }
186
+ try {
187
+ const resolved = await resolveSecretRef(refText, { resolveLocalSecret: (key) => mgr.get(key) });
188
+ ctx.print(`[secrets] ${describeSecretRef(refText)}: ${resolved.value ? 'resolved <redacted>' : 'missing'}`);
189
+ } catch (error) {
190
+ ctx.print(`[secrets] ${describeSecretRef(refText)} failed: ${summarizeError(error)}`);
191
+ }
192
+ return;
193
+ }
194
+ if (sub === 'set' || sub === 'link') {
195
+ const flags = new Set(rest.filter((value) => value.startsWith('--')));
196
+ const valueParts = rest.filter((value) => !value.startsWith('--'));
197
+ const [key, ...rawValueParts] = valueParts;
198
+ if (!key || valueParts.length === 0) {
199
+ ctx.print(`[secrets] Usage: /secrets ${sub} <KEY> <${sub === 'link' ? 'secret-ref' : 'value'}> [--user|--project] [--secure|--plaintext]`);
200
+ return;
201
+ }
202
+ const value = rawValueParts.join(' ');
203
+ if (sub === 'link' && !isGoodVibesSecretRefInput(value)) {
204
+ ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
205
+ return;
206
+ }
207
+ if (sub === 'set' && isMalformedGoodVibesSecretRefInput(value)) {
208
+ ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
209
+ return;
210
+ }
211
+ const scope = flags.has('--user') ? 'user' : 'project';
212
+ const medium = flags.has('--plaintext') ? 'plaintext' : 'secure';
213
+ await mgr.set(key, value, { scope, medium });
214
+ ctx.print(sub === 'link'
215
+ ? `[secrets] Linked: ${key} -> ${describeSecretRef(value)} (${scope}, ${medium})`
216
+ : `[secrets] Stored: ${key} (${scope}, ${medium})`);
217
+ return;
218
+ }
219
+ if (sub === 'get') {
220
+ const [key] = rest;
221
+ if (!key) {
222
+ ctx.print('[secrets] Usage: /secrets get <KEY>');
223
+ return;
224
+ }
225
+ const value = await mgr.get(key);
226
+ ctx.print(value === null ? `[secrets] Not found: ${key}` : `[secrets] ${key} = <stored> (use /secrets list to see all keys)`);
227
+ return;
228
+ }
229
+ if (sub === 'delete') {
230
+ const flags = new Set(rest.filter((value) => value.startsWith('--')));
231
+ const [key] = rest.filter((value) => !value.startsWith('--'));
232
+ if (!key) {
233
+ ctx.print('[secrets] Usage: /secrets delete <KEY> [--user|--project] [--secure|--plaintext]');
234
+ return;
235
+ }
236
+ await mgr.delete(key, {
237
+ scope: flags.has('--user') ? 'user' : flags.has('--project') ? 'project' : undefined,
238
+ medium: flags.has('--plaintext') ? 'plaintext' : flags.has('--secure') ? 'secure' : undefined,
239
+ });
240
+ ctx.print(`[secrets] Deleted: ${key}`);
241
+ return;
242
+ }
243
+ ctx.print('[secrets] Usage: /secrets set <KEY> <value> [--user|--project] [--secure|--plaintext] | link <KEY> <secret-ref> [--user|--project] [--secure|--plaintext] | get <KEY> | test <secret-ref> | providers | list | delete <KEY> [--user|--project] [--secure|--plaintext]');
244
+ },
245
+ });
246
+
247
+ registry.register({
248
+ name: 'image',
249
+ aliases: ['img'],
250
+ description: 'Attach an image file to the next message',
251
+ usage: '<path> [prompt text]',
252
+ argsHint: '<path> [prompt]',
253
+ async handler(args, ctx) {
254
+ if (args.length === 0) {
255
+ ctx.print('Usage: /image <path> [prompt text]\nSupported formats: PNG, JPEG, WebP, GIF');
256
+ return;
257
+ }
258
+ const rawPath = args[0];
259
+ const promptText = args.slice(1).join(' ') || `Attached image: ${rawPath.split('/').pop() ?? rawPath}`;
260
+ const projectRoot = ctx.workspace.shellPaths?.workingDirectory ?? ctx.platform.configManager.getWorkingDirectory();
261
+ if (!projectRoot) {
262
+ ctx.print('Error: working directory is unavailable.');
263
+ return;
264
+ }
265
+ let resolvedPath: string;
266
+ try {
267
+ resolvedPath = resolveAndValidatePath(rawPath, projectRoot);
268
+ } catch (err) {
269
+ ctx.print(`Error: ${summarizeError(err)}`);
270
+ return;
271
+ }
272
+ if (!existsSync(resolvedPath)) {
273
+ ctx.print(`File not found: ${rawPath}`);
274
+ return;
275
+ }
276
+ const ext = resolvedPath.slice(resolvedPath.lastIndexOf('.')).toLowerCase();
277
+ const mediaType = ({ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif' } as Record<string, string>)[ext];
278
+ if (!mediaType) {
279
+ ctx.print(`Unsupported image format: ${ext}\nSupported: .png, .jpg, .jpeg, .webp, .gif`);
280
+ return;
281
+ }
282
+ const stat = statSync(resolvedPath);
283
+ if (stat.size > 20 * 1024 * 1024) {
284
+ ctx.print(`Image too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum: 20MB`);
285
+ return;
286
+ }
287
+ let data: string;
288
+ try {
289
+ data = (await readFile(resolvedPath)).toString('base64');
290
+ } catch (err) {
291
+ ctx.print(`Failed to read image: ${summarizeError(err)}`);
292
+ return;
293
+ }
294
+ const currentModel = await requireProviderApi(ctx).getCurrentModel();
295
+ if (!currentModel.capabilities.multimodal) {
296
+ ctx.print(`Warning: ${currentModel.displayName} does not support image input. The image will be stripped when sending.`);
297
+ }
298
+ const content: ContentPart[] = [{ type: 'text', text: promptText }, { type: 'image', data, mediaType }];
299
+ ctx.submitInput?.(promptText, content);
300
+ },
301
+ });
302
+
303
+ registry.register({
304
+ name: 'refresh-models',
305
+ description: 'Refresh model catalog, benchmarks, and token limits',
306
+ async handler(_args, ctx) {
307
+ const providerApi = requireProviderApi(ctx);
308
+ let catalogOk = false;
309
+ let benchmarksOk = false;
310
+ let limitsOk = false;
311
+ ctx.print('Refreshing model catalog...');
312
+ try {
313
+ const catalog = await providerApi.refreshCatalog();
314
+ catalogOk = true;
315
+ ctx.print(`Model catalog refreshed: ${catalog.modelCount} models from ${catalog.providerCount} providers`);
316
+ } catch (e) {
317
+ ctx.print(`Catalog refresh failed: ${summarizeError(e)}`);
318
+ }
319
+ ctx.print('Refreshing benchmarks...');
320
+ try {
321
+ const benchmarkCount = await providerApi.refreshBenchmarks();
322
+ benchmarksOk = true;
323
+ ctx.print(`Benchmarks refreshed${benchmarkCount > 0 ? `: ${benchmarkCount} model records available.` : '.'}`);
324
+ } catch (e) {
325
+ ctx.print(`Benchmarks refresh failed: ${summarizeError(e)}`);
326
+ }
327
+ ctx.print('Refreshing token limits...');
328
+ try {
329
+ const count = await providerApi.refreshModelLimits();
330
+ limitsOk = true;
331
+ ctx.print(`Token limits refreshed: ${count} models updated.`);
332
+ } catch (e) {
333
+ ctx.print(`Token limits refresh failed: ${summarizeError(e)}`);
334
+ }
335
+ if (!catalogOk || !benchmarksOk || !limitsOk) ctx.print('Some refreshes failed — see messages above.');
336
+ },
337
+ });
338
+
339
+ registry.register({
340
+ name: 'pin',
341
+ description: 'Pin a model to the favorites list',
342
+ usage: '<model-id>',
343
+ argsHint: '<model-id>',
344
+ async handler(args, ctx) {
345
+ const providerApi = requireProviderApi(ctx);
346
+ const modelId = args[0];
347
+ if (!modelId) {
348
+ const favorites = await providerApi.getFavorites();
349
+ ctx.print(
350
+ favorites.pinned.length === 0
351
+ ? 'No pinned models. Use /pin <model-id> to pin one.'
352
+ : `Pinned models:\n${favorites.pinned.map((entry) => ` ★ ${entry.registryKey ?? entry.modelId}`).join('\n')}`,
353
+ );
354
+ return;
355
+ }
356
+ const favorites = await providerApi.getFavorites();
357
+ if (favorites.pinned.some((entry) => entry.modelId === modelId || entry.registryKey === modelId)) {
358
+ ctx.print(`Model already pinned: ${modelId}`);
359
+ return;
360
+ }
361
+ try {
362
+ await providerApi.pinModel(modelId);
363
+ ctx.print(`Pinned: ${modelId}`);
364
+ } catch (e) {
365
+ ctx.print(`Error: ${summarizeError(e)}`);
366
+ }
367
+ },
368
+ });
369
+
370
+ registry.register({
371
+ name: 'unpin',
372
+ description: 'Unpin a model from the favorites list',
373
+ usage: '<model-id>',
374
+ argsHint: '<model-id>',
375
+ async handler(args, ctx) {
376
+ const providerApi = requireProviderApi(ctx);
377
+ const modelId = args[0];
378
+ if (!modelId) {
379
+ ctx.print('Usage: /unpin <model-id>');
380
+ return;
381
+ }
382
+ const favorites = await providerApi.getFavorites();
383
+ const pinned = favorites.pinned.find((entry) => entry.registryKey === modelId);
384
+ if (!pinned) {
385
+ ctx.print(`Model is not pinned: ${modelId}`);
386
+ return;
387
+ }
388
+ await providerApi.unpinModel(pinned.registryKey);
389
+ ctx.print(`Unpinned: ${modelId}`);
390
+ },
391
+ });
392
+ }