@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,105 @@
1
+ export const REDACTED_VALUE = '<redacted>';
2
+
3
+ const SENSITIVE_PATH_PATTERN = /(^|\.)(apiKey|accessToken|botToken|appToken|signingSecret|webhookSecret|verifyToken|verificationToken|secret|password|token|keyFile)$/i;
4
+ const SECRET_LIKE_TEXT_PATTERNS: readonly RegExp[] = [
5
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
6
+ /\bghp_[A-Za-z0-9_]{16,}\b/g,
7
+ /\bgho_[A-Za-z0-9_]{16,}\b/g,
8
+ /\bghu_[A-Za-z0-9_]{16,}\b/g,
9
+ /\bghs_[A-Za-z0-9_]{16,}\b/g,
10
+ /\bgithub_pat_[A-Za-z0-9_]{24,}\b/g,
11
+ /\b(?:xoxb|xapp|xoxp|xoxa)-[A-Za-z0-9-]{16,}\b/g,
12
+ /\b[A-Za-z0-9._%+-]+:[A-Za-z0-9._%+-]{8,}@/g,
13
+ ];
14
+
15
+ export function isSensitiveConfigPath(path: string): boolean {
16
+ return SENSITIVE_PATH_PATTERN.test(path);
17
+ }
18
+
19
+ export function isRedactedValue(value: unknown): boolean {
20
+ return value === REDACTED_VALUE;
21
+ }
22
+
23
+ export interface RedactedConfigResult<T> {
24
+ readonly value: T;
25
+ readonly redactedPaths: readonly string[];
26
+ }
27
+
28
+ function shouldRedactValue(path: string, value: unknown): boolean {
29
+ if (!isSensitiveConfigPath(path)) return false;
30
+ if (typeof value !== 'string') return value !== undefined && value !== null;
31
+ if (value.trim().length === 0) return false;
32
+ if (value.startsWith('goodvibes://secrets/')) return false;
33
+ return true;
34
+ }
35
+
36
+ function redactUnknown(value: unknown, path: string, redactedPaths: string[]): unknown {
37
+ if (shouldRedactValue(path, value)) {
38
+ redactedPaths.push(path);
39
+ return REDACTED_VALUE;
40
+ }
41
+
42
+ if (Array.isArray(value)) {
43
+ return value.map((item, index) => redactUnknown(item, `${path}.${index}`, redactedPaths));
44
+ }
45
+
46
+ if (value && typeof value === 'object') {
47
+ const result: Record<string, unknown> = {};
48
+ for (const [key, nested] of Object.entries(value)) {
49
+ const nestedPath = path ? `${path}.${key}` : key;
50
+ result[key] = redactUnknown(nested, nestedPath, redactedPaths);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ return value;
56
+ }
57
+
58
+ export function redactConfig<T>(config: T): RedactedConfigResult<T> {
59
+ const redactedPaths: string[] = [];
60
+ return {
61
+ value: redactUnknown(config, '', redactedPaths) as T,
62
+ redactedPaths,
63
+ };
64
+ }
65
+
66
+ export function redactText(input: string): string {
67
+ let output = input;
68
+ for (const pattern of SECRET_LIKE_TEXT_PATTERNS) {
69
+ output = output.replace(pattern, REDACTED_VALUE);
70
+ }
71
+ return output;
72
+ }
73
+
74
+ function collectSensitiveValues(value: unknown, path: string, values: string[]): void {
75
+ if (shouldRedactValue(path, value) && typeof value === 'string') {
76
+ values.push(value);
77
+ return;
78
+ }
79
+ if (Array.isArray(value)) {
80
+ value.forEach((item, index) => collectSensitiveValues(item, `${path}.${index}`, values));
81
+ return;
82
+ }
83
+ if (value && typeof value === 'object') {
84
+ for (const [key, nested] of Object.entries(value)) {
85
+ collectSensitiveValues(nested, path ? `${path}.${key}` : key, values);
86
+ }
87
+ }
88
+ }
89
+
90
+ export function collectSensitiveConfigValues(config: unknown): readonly string[] {
91
+ const values: string[] = [];
92
+ collectSensitiveValues(config, '', values);
93
+ return [...new Set(values)].sort((left, right) => right.length - left.length);
94
+ }
95
+
96
+ export function redactSerializedSecrets(serialized: string, secretValues: readonly string[]): string {
97
+ let output = redactText(serialized);
98
+ for (const secret of secretValues) {
99
+ if (!secret) continue;
100
+ const encoded = JSON.stringify(secret).slice(1, -1);
101
+ output = output.split(encoded).join(REDACTED_VALUE);
102
+ output = output.split(secret).join(REDACTED_VALUE);
103
+ }
104
+ return output;
105
+ }
@@ -0,0 +1,26 @@
1
+ import type { CliCommandRuntime } from './management.ts';
2
+ import { buildCliServicePosture, formatCliServicePosture } from './service-posture.ts';
3
+ import type { CliCommandOutput } from './types.ts';
4
+
5
+ export async function handleServiceCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
6
+ const [sub = 'status'] = runtime.cli.commandArgs;
7
+ const json = runtime.cli.flags.outputFormat === 'json';
8
+ if (sub === 'status' || sub === 'check') {
9
+ const posture = await buildCliServicePosture(runtime, { probe: sub === 'check' });
10
+ return {
11
+ output: formatCliServicePosture(posture, json),
12
+ exitCode: sub === 'check' && posture.issues.length > 0 ? 1 : 0,
13
+ };
14
+ }
15
+ if (sub === 'install' || sub === 'start' || sub === 'restart' || sub === 'stop' || sub === 'uninstall') {
16
+ const text = 'GoodVibes Agent connects to an existing daemon and does not manage daemon lifecycle. Use GoodVibes TUI or your daemon host tooling for service mutations.';
17
+ return {
18
+ output: json ? JSON.stringify({ ok: false, kind: 'daemon_lifecycle_external', action: sub, error: text }, null, 2) : text,
19
+ exitCode: 2,
20
+ };
21
+ }
22
+ return {
23
+ output: `Usage: ${runtime.cli.binary} service [status|check]`,
24
+ exitCode: 2,
25
+ };
26
+ }
@@ -0,0 +1,482 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { accessSync, closeSync, constants, existsSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
3
+ import net from 'node:net';
4
+ import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { PlatformServiceManager } from '@pellux/goodvibes-sdk/platform/daemon';
7
+ import type { ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon';
8
+ import type { ConfigManager } from '../config/index.ts';
9
+ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
10
+ import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
11
+ import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
12
+ import { redactText } from './redaction.ts';
13
+
14
+ export interface CliServiceRuntime {
15
+ readonly configManager: ConfigManager;
16
+ readonly workingDirectory: string;
17
+ readonly homeDirectory: string;
18
+ }
19
+
20
+ export interface CliServiceEndpointPosture {
21
+ readonly id: RuntimeEndpointId;
22
+ readonly label: string;
23
+ readonly enabled: boolean;
24
+ readonly binding: RuntimeEndpointBinding;
25
+ readonly bindPosture: ReturnType<typeof classifyBindPosture>;
26
+ readonly networkFacing: boolean;
27
+ readonly reachable?: boolean;
28
+ }
29
+
30
+ export interface CliServiceLogPosture {
31
+ readonly path: string | null;
32
+ readonly exists: boolean;
33
+ readonly size: number;
34
+ readonly modifiedAt: number | null;
35
+ readonly tail?: string;
36
+ readonly readError?: string;
37
+ }
38
+
39
+ export interface CliServicePosture {
40
+ readonly config: {
41
+ readonly enabled: boolean;
42
+ readonly autostart: boolean;
43
+ readonly restartOnFailure: boolean;
44
+ readonly daemonEnabled: boolean;
45
+ };
46
+ readonly managed: ManagedServiceStatus & {
47
+ readonly pidPath: string;
48
+ readonly lastError: string | null;
49
+ };
50
+ readonly endpoints: readonly CliServiceEndpointPosture[];
51
+ readonly log: CliServiceLogPosture;
52
+ readonly issues: readonly string[];
53
+ }
54
+
55
+ const ENDPOINTS: readonly { readonly id: RuntimeEndpointId; readonly label: string; readonly enabledKey: string }[] = [
56
+ { id: 'controlPlane', label: 'control plane', enabledKey: 'controlPlane.enabled' },
57
+ { id: 'httpListener', label: 'HTTP listener', enabledKey: 'danger.httpListener' },
58
+ { id: 'web', label: 'web surface', enabledKey: 'web.enabled' },
59
+ ];
60
+
61
+ const SOURCE_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
62
+
63
+ export interface DaemonExecutableResolutionOptions {
64
+ readonly env?: NodeJS.ProcessEnv;
65
+ readonly argv?: readonly string[];
66
+ readonly execPath?: string;
67
+ readonly packageRoot?: string;
68
+ }
69
+
70
+ export interface DaemonExecutableResolution {
71
+ readonly command: string;
72
+ readonly source: 'env' | 'sibling' | 'package' | 'path' | 'fallback';
73
+ readonly absolute: boolean;
74
+ }
75
+
76
+ interface ServiceDefinitionOverride {
77
+ readonly name: string;
78
+ readonly description: string;
79
+ readonly workingDirectory: string;
80
+ readonly command: string;
81
+ readonly args: readonly string[];
82
+ readonly env: Readonly<Record<string, string>>;
83
+ readonly restartOnFailure: boolean;
84
+ }
85
+
86
+ interface CliServiceStatusCommandResult {
87
+ readonly status: number | null;
88
+ readonly stdout?: string;
89
+ readonly stderr?: string;
90
+ }
91
+
92
+ interface CliServiceStatusManager {
93
+ status(): ManagedServiceStatus;
94
+ }
95
+
96
+ interface CliServicePostureOptions {
97
+ readonly probe?: boolean;
98
+ readonly logTailBytes?: number;
99
+ readonly manager?: CliServiceStatusManager;
100
+ readonly runCommand?: (command: string, args: readonly string[]) => CliServiceStatusCommandResult;
101
+ }
102
+
103
+ function connectHostForBindHost(host: string): string {
104
+ if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
105
+ return host || '127.0.0.1';
106
+ }
107
+
108
+ async function probeTcp(host: string, port: number, timeoutMs = 750): Promise<boolean> {
109
+ return await new Promise<boolean>((resolve) => {
110
+ const socket = net.createConnection({ host: connectHostForBindHost(host), port });
111
+ const finish = (value: boolean) => {
112
+ socket.removeAllListeners();
113
+ socket.destroy();
114
+ resolve(value);
115
+ };
116
+ socket.setTimeout(timeoutMs);
117
+ socket.once('connect', () => finish(true));
118
+ socket.once('timeout', () => finish(false));
119
+ socket.once('error', () => finish(false));
120
+ });
121
+ }
122
+
123
+ function isExecutable(path: string): boolean {
124
+ try {
125
+ accessSync(path, constants.X_OK);
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ function platformDaemonArtifactName(platform = process.platform, arch = process.arch): string {
133
+ if (platform === 'linux' && arch === 'x64') return 'goodvibes-daemon-linux-x64';
134
+ if (platform === 'linux' && arch === 'arm64') return 'goodvibes-daemon-linux-arm64';
135
+ if (platform === 'darwin' && arch === 'x64') return 'goodvibes-daemon-macos-x64';
136
+ if (platform === 'darwin' && arch === 'arm64') return 'goodvibes-daemon-macos-arm64';
137
+ if (platform === 'win32') return 'goodvibes-daemon-windows.exe';
138
+ return 'goodvibes-daemon';
139
+ }
140
+
141
+ function executablePathCandidates(path: string): string[] {
142
+ if (!path.trim() || !isAbsolute(path)) return [];
143
+ const dir = dirname(path);
144
+ const name = basename(path);
145
+ const artifact = platformDaemonArtifactName();
146
+ const candidates = [
147
+ join(dir, 'goodvibes-daemon'),
148
+ join(dir, artifact),
149
+ ];
150
+ if (name.startsWith('goodvibes-') && !name.startsWith('goodvibes-daemon-')) {
151
+ candidates.push(join(dir, name.replace(/^goodvibes-/, 'goodvibes-daemon-')));
152
+ }
153
+ if (name === 'goodvibes') candidates.push(join(dir, 'goodvibes-daemon'));
154
+ return [...new Set(candidates)];
155
+ }
156
+
157
+ function resolveCommandFromPath(command: string, pathValue: string | undefined): string | null {
158
+ const pathEntries = (pathValue ?? '').split(delimiter).filter(Boolean);
159
+ const extensions = process.platform === 'win32'
160
+ ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
161
+ : [''];
162
+ for (const entry of pathEntries) {
163
+ for (const extension of extensions) {
164
+ const candidate = join(entry, `${command}${extension}`);
165
+ if (isExecutable(candidate)) return candidate;
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
171
+ function firstExecutable(paths: readonly string[]): string | null {
172
+ for (const path of paths) {
173
+ if (isExecutable(path)) return path;
174
+ }
175
+ return null;
176
+ }
177
+
178
+ function packageArtifactForBinWrapper(path: string): string | null {
179
+ let realPath = path;
180
+ try {
181
+ realPath = realpathSync(path);
182
+ } catch {
183
+ // Keep the original path if resolving a symlink fails.
184
+ }
185
+ if (basename(realPath) !== 'goodvibes-daemon') return null;
186
+ const binDir = dirname(realPath);
187
+ if (basename(binDir) !== 'bin') return null;
188
+ const packageRoot = dirname(binDir);
189
+ return firstExecutable([
190
+ join(packageRoot, 'vendor', platformDaemonArtifactName()),
191
+ join(packageRoot, 'dist', platformDaemonArtifactName()),
192
+ join(packageRoot, 'dist', 'goodvibes-daemon'),
193
+ ]);
194
+ }
195
+
196
+ export function resolveGoodVibesDaemonExecutable(
197
+ options: DaemonExecutableResolutionOptions = {},
198
+ ): DaemonExecutableResolution {
199
+ const env = options.env ?? process.env;
200
+ const override = env.GOODVIBES_DAEMON_COMMAND?.trim();
201
+ if (override) {
202
+ return { command: override, source: 'env', absolute: isAbsolute(override) };
203
+ }
204
+
205
+ const packageRoot = options.packageRoot ?? SOURCE_PACKAGE_ROOT;
206
+ const packaged = firstExecutable([
207
+ join(packageRoot, 'dist', platformDaemonArtifactName()),
208
+ join(packageRoot, 'dist', 'goodvibes-daemon'),
209
+ join(packageRoot, 'vendor', platformDaemonArtifactName()),
210
+ join(packageRoot, 'bin', 'goodvibes-daemon'),
211
+ ]);
212
+ if (packaged) return { command: packaged, source: 'package', absolute: true };
213
+
214
+ const argv = options.argv ?? process.argv;
215
+ const execPath = options.execPath ?? process.execPath;
216
+ const sibling = firstExecutable([
217
+ ...executablePathCandidates(execPath),
218
+ ...executablePathCandidates(argv[1] ?? ''),
219
+ ]);
220
+ if (sibling) {
221
+ return {
222
+ command: packageArtifactForBinWrapper(sibling) ?? sibling,
223
+ source: 'sibling',
224
+ absolute: true,
225
+ };
226
+ }
227
+
228
+ const pathResolved = resolveCommandFromPath('goodvibes-daemon', env.PATH);
229
+ if (pathResolved) {
230
+ return {
231
+ command: packageArtifactForBinWrapper(pathResolved) ?? pathResolved,
232
+ source: 'path',
233
+ absolute: true,
234
+ };
235
+ }
236
+
237
+ return { command: 'goodvibes-daemon', source: 'fallback', absolute: false };
238
+ }
239
+
240
+ export function getServiceStateRoot(runtime: CliServiceRuntime): string {
241
+ return join(runtime.homeDirectory, '.goodvibes', 'daemon');
242
+ }
243
+
244
+ function pidFilePath(runtime: CliServiceRuntime, platform: ManagedServiceStatus['platform']): string {
245
+ return join(getServiceStateRoot(runtime), 'service', `${platform}.pid`);
246
+ }
247
+
248
+ function runStatusCommand(
249
+ command: string,
250
+ args: readonly string[],
251
+ options: CliServicePostureOptions,
252
+ ): CliServiceStatusCommandResult {
253
+ if (options.runCommand) return options.runCommand(command, args);
254
+ return spawnSync(command, [...args], {
255
+ stdio: 'pipe',
256
+ encoding: 'utf-8',
257
+ timeout: 1500,
258
+ });
259
+ }
260
+
261
+ function parseSystemdShowValue(lines: readonly string[], key: string): string | null {
262
+ const prefix = `${key}=`;
263
+ const match = lines.find((line) => line.startsWith(prefix));
264
+ return match ? match.slice(prefix.length).trim() : null;
265
+ }
266
+
267
+ function reconcileSystemdServiceStatus(
268
+ runtime: CliServiceRuntime,
269
+ status: ManagedServiceStatus,
270
+ options: CliServicePostureOptions,
271
+ ): ManagedServiceStatus {
272
+ if (status.platform !== 'systemd') return status;
273
+
274
+ const name = String(runtime.configManager.get('service.serviceName') ?? 'goodvibes').trim() || 'goodvibes';
275
+ const result = runStatusCommand('systemctl', [
276
+ '--user',
277
+ 'show',
278
+ `${name}.service`,
279
+ '--property=LoadState,ActiveState,UnitFileState,MainPID',
280
+ '--no-page',
281
+ ], options);
282
+ if ((result.status ?? 1) !== 0) return status;
283
+
284
+ const lines = (result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
285
+ const loadState = parseSystemdShowValue(lines, 'LoadState');
286
+ const activeState = parseSystemdShowValue(lines, 'ActiveState');
287
+ const unitFileState = parseSystemdShowValue(lines, 'UnitFileState');
288
+ const rawPid = Number.parseInt(parseSystemdShowValue(lines, 'MainPID') ?? '', 10);
289
+ const pid = Number.isFinite(rawPid) && rawPid > 0 ? rawPid : undefined;
290
+
291
+ return {
292
+ ...status,
293
+ installed: loadState === 'loaded' || status.installed,
294
+ autostart: unitFileState === 'enabled' || unitFileState === 'linked' || status.autostart,
295
+ running: activeState === 'active',
296
+ ...(pid === undefined ? {} : { pid }),
297
+ };
298
+ }
299
+
300
+ function readLogPosture(path: string | undefined, tailBytes: number): CliServiceLogPosture {
301
+ if (!path) return { path: null, exists: false, size: 0, modifiedAt: null };
302
+ if (!existsSync(path)) return { path, exists: false, size: 0, modifiedAt: null };
303
+ try {
304
+ const stat = statSync(path);
305
+ const length = Math.min(stat.size, Math.max(0, tailBytes));
306
+ if (length === 0) {
307
+ return { path, exists: true, size: stat.size, modifiedAt: stat.mtimeMs, tail: '' };
308
+ }
309
+ const raw = Buffer.alloc(length);
310
+ const fd = openSync(path, 'r');
311
+ try {
312
+ readSync(fd, raw, 0, length, Math.max(0, stat.size - length));
313
+ } finally {
314
+ closeSync(fd);
315
+ }
316
+ return {
317
+ path,
318
+ exists: true,
319
+ size: stat.size,
320
+ modifiedAt: stat.mtimeMs,
321
+ tail: redactText(raw.toString('utf-8')),
322
+ };
323
+ } catch (error) {
324
+ return {
325
+ path,
326
+ exists: true,
327
+ size: 0,
328
+ modifiedAt: null,
329
+ readError: error instanceof Error ? error.message : String(error),
330
+ };
331
+ }
332
+ }
333
+
334
+ function endpointsConflict(a: CliServiceEndpointPosture, b: CliServiceEndpointPosture): boolean {
335
+ if (a.binding.port !== b.binding.port) return false;
336
+ const hostA = a.binding.host;
337
+ const hostB = b.binding.host;
338
+ return hostA === hostB || hostA === '0.0.0.0' || hostB === '0.0.0.0' || hostA === '::' || hostB === '::';
339
+ }
340
+
341
+ export function createPlatformServiceManager(runtime: CliServiceRuntime): PlatformServiceManager {
342
+ const daemonHomeDir = getServiceStateRoot(runtime);
343
+ const serviceName = String(runtime.configManager.get('service.serviceName') ?? 'goodvibes').trim() || 'goodvibes';
344
+ const daemonExecutable = resolveGoodVibesDaemonExecutable();
345
+ const definition: ServiceDefinitionOverride = {
346
+ name: serviceName,
347
+ description: 'GoodVibes daemon, control-plane, listener, and web host',
348
+ workingDirectory: daemonHomeDir,
349
+ command: daemonExecutable.command,
350
+ args: [],
351
+ env: {
352
+ // The daemon CLI treats this as the GoodVibes home root, not the daemon state directory.
353
+ GOODVIBES_DAEMON_HOME: runtime.homeDirectory,
354
+ GOODVIBES_DAEMON_TOKEN: process.env.GOODVIBES_DAEMON_TOKEN ?? '',
355
+ GOODVIBES_HTTP_TOKEN: process.env.GOODVIBES_HTTP_TOKEN ?? '',
356
+ NODE_ENV: process.env.NODE_ENV ?? 'production',
357
+ },
358
+ restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
359
+ };
360
+ return new PlatformServiceManager(runtime.configManager, {
361
+ workingDirectory: runtime.homeDirectory,
362
+ homeDirectory: runtime.homeDirectory,
363
+ surfaceRoot: 'daemon',
364
+ definitionOverride: definition,
365
+ defaultServiceName: 'goodvibes',
366
+ defaultServiceDescription: 'GoodVibes daemon, control-plane, listener, and web host',
367
+ });
368
+ }
369
+
370
+ export async function buildCliServicePosture(
371
+ runtime: CliServiceRuntime,
372
+ options: CliServicePostureOptions = {},
373
+ ): Promise<CliServicePosture> {
374
+ const manager = options.manager ?? createPlatformServiceManager(runtime);
375
+ const status = reconcileSystemdServiceStatus(runtime, manager.status(), options);
376
+ const endpoints = await Promise.all(ENDPOINTS.map(async (endpoint): Promise<CliServiceEndpointPosture> => {
377
+ const enabled = runtime.configManager.get(endpoint.enabledKey as never) === true;
378
+ const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint.id);
379
+ return {
380
+ id: endpoint.id,
381
+ label: endpoint.label,
382
+ enabled,
383
+ binding,
384
+ bindPosture: classifyBindPosture(binding),
385
+ networkFacing: isNetworkFacing(enabled, binding),
386
+ ...(options.probe && enabled ? { reachable: await probeTcp(binding.host, binding.port) } : {}),
387
+ };
388
+ }));
389
+
390
+ const config = {
391
+ enabled: runtime.configManager.get('service.enabled') === true,
392
+ autostart: runtime.configManager.get('service.autostart') === true,
393
+ restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
394
+ daemonEnabled: runtime.configManager.get('danger.daemon') === true,
395
+ };
396
+ const serverBackedEnabled = config.daemonEnabled || endpoints.some((endpoint) => endpoint.enabled);
397
+ const issues: string[] = [];
398
+
399
+ if (serverBackedEnabled && !config.enabled) {
400
+ issues.push('Server-backed surfaces are enabled but service mode is off.');
401
+ }
402
+ if (config.enabled && !config.autostart) {
403
+ issues.push('Service mode is enabled but autostart is off.');
404
+ }
405
+ if (config.enabled && !config.restartOnFailure) {
406
+ issues.push('Service mode is enabled but restart-on-failure is off.');
407
+ }
408
+ if (config.enabled && !status.installed) {
409
+ issues.push('Service mode is enabled but no platform service definition is installed.');
410
+ }
411
+ if (config.enabled && !status.running) {
412
+ issues.push('Service mode is enabled but the managed service is not running.');
413
+ }
414
+ if (status.actionError) {
415
+ issues.push(`Service manager reported an error: ${status.actionError}`);
416
+ }
417
+ for (const endpoint of endpoints) {
418
+ if (endpoint.enabled && options.probe && endpoint.reachable === false) {
419
+ issues.push(`${endpoint.label} is enabled but not reachable on ${endpoint.binding.host}:${endpoint.binding.port}.`);
420
+ }
421
+ }
422
+ const enabledEndpoints = endpoints.filter((endpoint) => endpoint.enabled);
423
+ for (let outer = 0; outer < enabledEndpoints.length; outer += 1) {
424
+ for (let inner = outer + 1; inner < enabledEndpoints.length; inner += 1) {
425
+ const left = enabledEndpoints[outer]!;
426
+ const right = enabledEndpoints[inner]!;
427
+ if (endpointsConflict(left, right)) {
428
+ issues.push(`${left.label} and ${right.label} are configured to bind the same host/port envelope (${left.binding.host}:${left.binding.port}, ${right.binding.host}:${right.binding.port}).`);
429
+ }
430
+ }
431
+ }
432
+ const log = readLogPosture(status.logPath, options.logTailBytes ?? 4096);
433
+ if (log.readError) {
434
+ issues.push(`Service log exists but could not be read: ${log.readError}`);
435
+ }
436
+
437
+ return {
438
+ config,
439
+ managed: {
440
+ ...status,
441
+ pidPath: pidFilePath(runtime, status.platform),
442
+ lastError: status.actionError ?? null,
443
+ },
444
+ endpoints,
445
+ log,
446
+ issues,
447
+ };
448
+ }
449
+
450
+ function yesNo(value: boolean): string {
451
+ return value ? 'yes' : 'no';
452
+ }
453
+
454
+ export function formatCliServicePosture(posture: CliServicePosture, json = false): string {
455
+ if (json) return JSON.stringify(posture, null, 2);
456
+ return [
457
+ 'GoodVibes service',
458
+ ` enabled: ${yesNo(posture.config.enabled)}`,
459
+ ` autostart: ${yesNo(posture.config.autostart)}`,
460
+ ` restartOnFailure: ${yesNo(posture.config.restartOnFailure)}`,
461
+ ` daemon flag: ${yesNo(posture.config.daemonEnabled)}`,
462
+ '',
463
+ 'Managed service:',
464
+ ` platform: ${posture.managed.platform}`,
465
+ ` installed: ${yesNo(posture.managed.installed)}`,
466
+ ` running: ${yesNo(posture.managed.running)}`,
467
+ ` pid: ${posture.managed.pid ?? 'n/a'}`,
468
+ ` definition: ${posture.managed.path}`,
469
+ ` pid file: ${posture.managed.pidPath}`,
470
+ ` log: ${posture.log.path ?? 'n/a'} (${posture.log.exists ? 'present' : 'missing'})`,
471
+ ...(posture.log.readError ? [` log read error: ${posture.log.readError}`] : []),
472
+ ` command: ${posture.managed.commandPreview}`,
473
+ '',
474
+ 'Endpoints:',
475
+ ...posture.endpoints.map((endpoint) =>
476
+ ` ${endpoint.label}: enabled=${yesNo(endpoint.enabled)} ${endpoint.binding.hostMode} ${endpoint.binding.host}:${endpoint.binding.port} posture=${endpoint.bindPosture.label}${endpoint.reachable === undefined ? '' : ` reachable=${yesNo(endpoint.reachable)}`}`,
477
+ ),
478
+ '',
479
+ posture.issues.length === 0 ? 'Readiness: ready' : 'Readiness: needs attention',
480
+ ...posture.issues.map((issue) => ` - ${issue}`),
481
+ ].join('\n');
482
+ }