@pellux/goodvibes-tui 1.16.1 → 1.18.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 (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -0,0 +1,72 @@
1
+ // ---------------------------------------------------------------------------
2
+ // acp-runtime.ts — the scripted path for hosting third-party coding agents,
3
+ // over the SAME verbs the Fleet panel's spawn affordance drives (acp.agents.list
4
+ // / acp.sessions.create). `/agents list` shows what the daemon discovered
5
+ // (read-only, quiet when none); `/agents host <agentId> [dir]` spawns one as a
6
+ // long-lived daemon session that shows up as an acp-agent fleet row. A working
7
+ // directory defaults to the current one; a known workspace root can be named
8
+ // instead — but no path is ever required beyond what the operator already has.
9
+ //
10
+ // A structured spawn failure ({binary, stage, message}) is printed verbatim,
11
+ // never left as a hung row: the daemon bounds the handshake and returns a
12
+ // 'failed' record with its three honest fields.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ import type { CommandRegistry } from '../command-registry.ts';
16
+ import { requireShellPaths } from './runtime-services.ts';
17
+ import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
18
+
19
+ export function registerAcpRuntimeCommands(registry: CommandRegistry): void {
20
+ registry.register({
21
+ name: 'agents',
22
+ aliases: ['acp'],
23
+ description: 'Host third-party coding agents (Claude Code, Codex, opencode) as fleet rows over ACP',
24
+ usage: 'list | host <agentId> [directory]',
25
+ argsHint: '[list|host]',
26
+ async handler(args, ctx) {
27
+ const sub = (args[0] ?? 'list').toLowerCase();
28
+ const rpc = getOperatorRpc(ctx);
29
+ if (!rpc.available) { ctx.print(`[agents] ${rpc.reason}`); return; }
30
+
31
+ if (sub === 'list') {
32
+ try {
33
+ const { agents } = await rpc.sdk.operator.invoke('acp.agents.list', {});
34
+ if (agents.length === 0) {
35
+ ctx.print('[agents] No third-party ACP agents discovered. Install Claude Code, Codex, or opencode on PATH to host one.');
36
+ return;
37
+ }
38
+ ctx.print([
39
+ 'Discovered ACP agents (host with /agents host <id> [directory]):',
40
+ ...agents.map((a) => ` ${a.id} — ${a.title} (${a.binaryPath})`),
41
+ ].join('\n'));
42
+ } catch (error) {
43
+ ctx.print(`[agents list] ${describeOperatorRpcError(error)}`);
44
+ }
45
+ return;
46
+ }
47
+
48
+ if (sub === 'host') {
49
+ const agentId = args[1];
50
+ if (!agentId) {
51
+ ctx.print('Usage: /agents host <agentId> [directory] (agentId from /agents list; directory defaults to the current one)');
52
+ return;
53
+ }
54
+ const cwd = args[2] ?? requireShellPaths(ctx).workingDirectory;
55
+ try {
56
+ const { hosted, started } = await rpc.sdk.operator.invoke('acp.sessions.create', { agentId, cwd });
57
+ if (hosted.error) {
58
+ // Structured failure, verbatim — never a hung row.
59
+ ctx.print(`[agents host] Could not host ${hosted.title || agentId}: ${hosted.error.stage} stage failed for ${hosted.error.binary} — ${hosted.error.message}`);
60
+ return;
61
+ }
62
+ ctx.print(`[agents host] Hosting ${hosted.title || agentId} in ${cwd}${started ? '' : ' (queued)'} — it appears as an acp-agent row in the fleet; steer and stop it like any agent.`);
63
+ } catch (error) {
64
+ ctx.print(`[agents host] ${describeOperatorRpcError(error)}`);
65
+ }
66
+ return;
67
+ }
68
+
69
+ ctx.print('Usage: /agents list | /agents host <agentId> [directory]');
70
+ },
71
+ });
72
+ }
@@ -155,8 +155,12 @@ export function registerCiRuntimeCommands(registry: CommandRegistry): void {
155
155
  const result = await rpc.sdk.operator.invoke('ci.watches.run', { watchId: runId });
156
156
  const lines = [renderReport(result.report)];
157
157
  lines.push(`notified: ${result.notified ? 'yes' : 'no'}${result.notificationId ? ` (${result.notificationId})` : ''}`);
158
- lines.push(`fix-session triggered: ${result.fixSessionTriggered ? 'yes' : 'no'}${result.fixSessionId ? ` (${result.fixSessionId})` : ''}`);
158
+ lines.push(`fix-session triggered: ${result.fixSessionTriggered ? 'yes' : 'no'}`);
159
159
  ctx.print(lines.join('\n'));
160
+ // Route the spawned session through the shared one-key jump affordance
161
+ // instead of printing the id for the user to retype — same attach flow
162
+ // as the accepted fix-this card.
163
+ if (result.fixSessionId) ctx.armFixSessionAttach?.(result.fixSessionId);
160
164
  } catch (error) {
161
165
  ctx.print(`[ci watches run] ${describeOperatorRpcError(error)}`);
162
166
  }
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Explicit-query + transparency surface over the repo source-tree code index
5
5
  // (CodeIndexStore, @pellux/goodvibes-sdk/platform/state, landed on SDK main
6
- // as wo802/W5.3 Stage A) via the TUI's own wiring in
6
+ // with explicit-query build/status/search) via the TUI's own wiring in
7
7
  // src/runtime/code-index-services.ts, threaded through
8
8
  // CommandContext.session.codeIndexStore exactly like wrfcController/
9
9
  // workstreamEngine already are (bootstrap-command-context.ts).
@@ -14,8 +14,8 @@
14
14
  // node while it runs), status (counts, skips, degradation state, last
15
15
  // build), search <query> (explicit retrieval, results labeled
16
16
  // 'lexical'|'semantic' honestly — never implied as more precise than they
17
- // are). Stage B (now landed) adds passive auto-injection into coding turns and
18
- // tool-site incremental reindex; `status` surfaces both honestly (the
17
+ // are). Passive auto-injection into coding turns and tool-site incremental
18
+ // reindex followed later; `status` surfaces both honestly (the
19
19
  // auto-injection flag+setting gate, and the last reindex activity).
20
20
  // ---------------------------------------------------------------------------
21
21
 
@@ -35,7 +35,7 @@ function formatBytes(bytes: number): string {
35
35
  }
36
36
 
37
37
  /**
38
- * Honest one-line auto-injection state (Stage B): both the (default-off)
38
+ * Honest one-line auto-injection state: both the (default-off)
39
39
  * `agent-passive-code-injection` feature flag AND storage.codeIndexEnabled must
40
40
  * be on for passive code injection to run — state which is off, and why.
41
41
  */
@@ -49,7 +49,7 @@ function renderAutoInjectionState(flagEnabled: boolean, settingEnabled: boolean)
49
49
  return ` auto-injection: off (${reasons.join('; ')})`;
50
50
  }
51
51
 
52
- /** Honest last-reindex activity line (Stage B tool-site incremental reindex). */
52
+ /** Honest last-reindex activity line (tool-site incremental reindex). */
53
53
  function renderReindexActivity(activity: CodeIndexReindexActivity | null, now: number): string {
54
54
  if (!activity) return ' last reindex: none this session (writes/edits reindex touched files once the index is built)';
55
55
  const agoSec = Math.max(0, Math.round((now - activity.at) / 1000));
@@ -82,7 +82,7 @@ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<Config
82
82
  lines.push(
83
83
  ` auto-build on startup: ${autoStart ? 'on' : 'off'} (storage.codeIndexEnabled, default off — /config to change)`,
84
84
  );
85
- // Stage B surfacing: passive code auto-injection state (flag + setting) and the last tool-site reindex.
85
+ // Passive code auto-injection state (flag + setting) and the last tool-site reindex.
86
86
  lines.push(renderAutoInjectionState(extras.flagEnabled, autoStart));
87
87
  lines.push(renderReindexActivity(extras.reindexActivity, extras.now ?? Date.now()));
88
88
  lines.push(
@@ -11,7 +11,7 @@ import {
11
11
  } from './cost-attribution-format.ts';
12
12
 
13
13
  /**
14
- * /cost budget <usd> — makes the CostTrackerPanel's budget alert real (WO-139).
14
+ * /cost budget <usd> — makes the CostTrackerPanel's budget alert real.
15
15
  * Opens the cost panel (creating it via the registered factory if not already
16
16
  * open) and sets its budget threshold directly, so the meter+alert already
17
17
  * built into CostTrackerPanel.render() (:266-290) actually fires. 0 disables
@@ -0,0 +1,109 @@
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import { formatDeviceLine, resolveTokenByIdPrefix } from '../../core/pairing-devices.ts';
3
+
4
+ /**
5
+ * Register the /devices command — manage the per-device pairing tokens minted by
6
+ * the pairing surfaces. Mirrors the pairing.tokens.* gateway verbs the daemon
7
+ * serves (list/rename/revoke/migrate/revoke-shared) so the terminal has the same
8
+ * capability as the web app's device manager.
9
+ *
10
+ * /devices list paired devices (name · created · last-seen · id)
11
+ * /devices rename <id> <name> rename a device token (id or unambiguous prefix)
12
+ * /devices revoke <id> revoke a device token immediately
13
+ * /devices migrate-shared [name] mint a per-device token off the legacy shared token
14
+ * /devices revoke-shared revoke the legacy shared token (it stops authenticating)
15
+ */
16
+ export function registerDevicesRuntimeCommands(registry: CommandRegistry): void {
17
+ registry.register({
18
+ name: 'devices',
19
+ aliases: ['device'],
20
+ description: 'Manage paired-device tokens (list, rename, revoke, migrate-shared)',
21
+ usage: '[list|rename <id> <name>|revoke <id>|migrate-shared [name]|revoke-shared]',
22
+ argsHint: '[list|rename|revoke|migrate-shared|revoke-shared]',
23
+ handler(args, ctx) {
24
+ const pairingTokens = ctx.platform.pairingTokens;
25
+ if (!pairingTokens) {
26
+ ctx.print('Device management is unavailable in this runtime (no pairing token service wired).');
27
+ return;
28
+ }
29
+ const sub = (args[0] ?? 'list').toLowerCase();
30
+
31
+ if (sub === 'list') {
32
+ renderList(ctx, pairingTokens);
33
+ return;
34
+ }
35
+
36
+ if (sub === 'rename') {
37
+ const idOrPrefix = args[1];
38
+ const name = args.slice(2).join(' ').trim();
39
+ if (!idOrPrefix || !name) {
40
+ ctx.print('Usage: /devices rename <id> <new name>');
41
+ return;
42
+ }
43
+ const resolved = resolveTokenByIdPrefix(pairingTokens.list(), idOrPrefix);
44
+ if (!resolved.ok) {
45
+ ctx.print(resolved.reason === 'ambiguous' ? `Ambiguous device id "${idOrPrefix}" — use more characters.` : `No device matches "${idOrPrefix}".`);
46
+ return;
47
+ }
48
+ pairingTokens.rename(resolved.token.id, name);
49
+ ctx.print(`Renamed device to "${name}".`);
50
+ return;
51
+ }
52
+
53
+ if (sub === 'revoke') {
54
+ const idOrPrefix = args[1];
55
+ if (!idOrPrefix) {
56
+ ctx.print('Usage: /devices revoke <id>');
57
+ return;
58
+ }
59
+ const resolved = resolveTokenByIdPrefix(pairingTokens.list(), idOrPrefix);
60
+ if (!resolved.ok) {
61
+ ctx.print(resolved.reason === 'ambiguous' ? `Ambiguous device id "${idOrPrefix}" — use more characters.` : `No device matches "${idOrPrefix}".`);
62
+ return;
63
+ }
64
+ const revoked = pairingTokens.revoke(resolved.token.id);
65
+ ctx.print(revoked ? `Revoked "${resolved.token.name}" — its token stops working immediately.` : `Device "${resolved.token.name}" was already absent.`);
66
+ return;
67
+ }
68
+
69
+ if (sub === 'migrate-shared') {
70
+ const name = args.slice(1).join(' ').trim() || 'migrated device';
71
+ const minted = pairingTokens.mintForMigration({ name });
72
+ ctx.print(
73
+ `Minted a per-device token "${minted.name}" (shown once): ${minted.token}\n` +
74
+ 'Pair this device with it, then run /devices revoke-shared to retire the legacy shared token.',
75
+ );
76
+ return;
77
+ }
78
+
79
+ if (sub === 'revoke-shared') {
80
+ if (pairingTokens.isLegacyRevoked()) {
81
+ ctx.print('The legacy shared token is already revoked.');
82
+ return;
83
+ }
84
+ pairingTokens.revokeLegacyShared();
85
+ ctx.print('Revoked the legacy shared token — devices still on it must re-pair with their own token.');
86
+ return;
87
+ }
88
+
89
+ ctx.print(`Unknown /devices subcommand: ${sub}. Use: list | rename | revoke | migrate-shared | revoke-shared`);
90
+ },
91
+ });
92
+ }
93
+
94
+ function renderList(ctx: CommandContext, pairingTokens: NonNullable<CommandContext['platform']['pairingTokens']>): void {
95
+ const list = pairingTokens.list();
96
+ const lines: string[] = ['Paired devices:'];
97
+ if (list.length === 0) {
98
+ lines.push(' (none yet — pair one with /pair)');
99
+ } else {
100
+ for (const token of list) lines.push(` ${formatDeviceLine(token)}`);
101
+ }
102
+ lines.push(
103
+ '',
104
+ pairingTokens.isLegacyRevoked()
105
+ ? 'Legacy shared token: revoked.'
106
+ : 'Legacy shared token: active — run /devices migrate-shared then /devices revoke-shared to retire it.',
107
+ );
108
+ ctx.print(lines.join('\n'));
109
+ }
@@ -1,6 +1,6 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
2
  import { GitService } from '@pellux/goodvibes-sdk/platform/git';
3
- import { requireShellPaths } from './runtime-services.ts';
3
+ import { requireShellPaths, requirePanelManager } from './runtime-services.ts';
4
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
 
6
6
  export function registerGitRuntimeCommands(registry: CommandRegistry): void {
@@ -64,10 +64,34 @@ export function registerGitRuntimeCommands(registry: CommandRegistry): void {
64
64
  break;
65
65
  }
66
66
  case 'diff': {
67
+ // Route the full, uncapped working-tree diff (GitService.diffStructured
68
+ // — parsed per-file/per-hunk with no size limit) into the real diff
69
+ // panel. The old path sliced the raw text at 4,000 chars and printed a
70
+ // stub; a large diff now renders complete.
67
71
  try {
68
- const diffText = await git.diff();
69
- if (!diffText.trim()) ctx.print('No unstaged changes.');
70
- else ctx.print(diffText.length > 4000 ? `${diffText.slice(0, 4000)}\n\n...(diff truncated)` : diffText);
72
+ const structured = await git.diffStructured();
73
+ if (structured.files.length === 0) {
74
+ ctx.print('No unstaged changes.');
75
+ break;
76
+ }
77
+ const { DiffPanel } = await import('../../panels/diff-panel.ts');
78
+ const pm = requirePanelManager(ctx);
79
+ let panel = pm.getAllOpen().find((p) => p.id === 'diff');
80
+ if (!panel) {
81
+ try {
82
+ panel = pm.open('diff');
83
+ } catch {
84
+ ctx.print('Could not open diff panel.');
85
+ break;
86
+ }
87
+ }
88
+ pm.activateById('diff');
89
+ if (!pm.isVisible()) pm.show();
90
+ ctx.focusPanels?.();
91
+ (panel as InstanceType<typeof DiffPanel>).loadStructuredDiff(structured);
92
+ const fileWord = structured.files.length === 1 ? 'file' : 'files';
93
+ ctx.print(`Diff panel updated: ${structured.files.length} ${fileWord}, +${structured.additions} -${structured.deletions} (complete, uncapped).`);
94
+ ctx.renderRequest();
71
95
  } catch (e) {
72
96
  ctx.print(`Git diff failed: ${summarizeError(e)}`);
73
97
  }
@@ -0,0 +1,45 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { getOperatorRpc, describeOperatorRpcError } from './operator-rpc.ts';
3
+ import { renderWorkstreamGraphLines } from '../../panels/workstream-graph-render.ts';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // /graph — a workstream's task graph (fleet.graph.get) rendered legibly.
7
+ //
8
+ // The observability-layer surface: nodes, edges, per-node states (ready /
9
+ // running / blocked-with-"waiting on: X" / stalled / orphaned) and the elastic
10
+ // pool posture ("N ready, M running, at cap (fleet.maxSize=N)") — the shape of
11
+ // the dependency graph WITHOUT opening any transcript. The fix-phase chain's
12
+ // task graph is one such workstream.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function renderWidth(): number {
16
+ const cols = typeof process !== 'undefined' && process.stdout && process.stdout.columns ? process.stdout.columns : 80;
17
+ return Math.max(40, cols - 2);
18
+ }
19
+
20
+ export function registerGraphRuntimeCommands(registry: CommandRegistry): void {
21
+ registry.register({
22
+ name: 'graph',
23
+ description: "Show a workstream's task graph — nodes, edges, states, and pool posture",
24
+ usage: '<workstreamId>',
25
+ argsHint: '<workstreamId>',
26
+ async handler(args, ctx) {
27
+ const workstreamId = (args[0] ?? '').trim();
28
+ if (!workstreamId) {
29
+ ctx.print('Usage: /graph <workstreamId> — renders that workstream\'s task graph (nodes, edges, states, pool).');
30
+ return;
31
+ }
32
+ const rpc = getOperatorRpc(ctx);
33
+ if (!rpc.available) {
34
+ ctx.print(`[graph] ${rpc.reason}`);
35
+ return;
36
+ }
37
+ try {
38
+ const snapshot = await rpc.sdk.operator.invoke('fleet.graph.get', { workstreamId });
39
+ ctx.print(renderWorkstreamGraphLines(snapshot, renderWidth()).join('\n'));
40
+ } catch (error) {
41
+ ctx.print(`[graph] ${describeOperatorRpcError(error)}`);
42
+ }
43
+ },
44
+ });
45
+ }
@@ -48,7 +48,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
48
48
  const readModels = requireReadModels(ctx);
49
49
 
50
50
  if (sub === 'open' || sub === 'panel' || sub === 'provider') {
51
- ctx.openModal?.('providers-modal'); // W6.1: provider-health panel -> config modal
51
+ ctx.openModal?.('providers-modal'); // provider-health panel -> config modal
52
52
  return;
53
53
  }
54
54
 
@@ -477,7 +477,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
477
477
 
478
478
  ctx.print([
479
479
  'Health Review',
480
- // DEBT-5 item 3: /health stays a cross-domain transcript report (it
480
+ // item 3: /health stays a cross-domain transcript report (it
481
481
  // genuinely spans settings/accounts/auth/sandbox/etc — no single
482
482
  // modal owns this data), but the providers domain DOES have one, so
483
483
  // point at it honestly. "/provider" (no domain) is a DIFFERENT
@@ -55,7 +55,7 @@ export function registerIntelligenceRuntimeCommands(registry: CommandRegistry):
55
55
  async handler(args, ctx) {
56
56
  const sub = (args[0] ?? 'review').toLowerCase();
57
57
  if (sub === 'panel' || sub === 'open') {
58
- // W6.1 (the purge): 'intelligence'/IntelligencePanel was
58
+ // (the purge): 'intelligence'/IntelligencePanel was
59
59
  // DELETE-disposition (no surviving human surface — its read model
60
60
  // stays and still backs every subcommand below). There is no alias
61
61
  // to resolve through, so this prints an honest notice and opens
@@ -10,7 +10,7 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
10
10
  const sub = (args[0] ?? 'review').toLowerCase();
11
11
  const auth = requireLocalUserAuthManager(ctx);
12
12
  if (sub === 'panel' || sub === 'open') {
13
- // W6.1: browse view moved to local-auth-modal. The LocalAuthPanel itself is
13
+ // browse view moved to local-auth-modal. The LocalAuthPanel itself is
14
14
  // kept (masked password-entry host) but is no longer the /local-auth panel
15
15
  // destination — masked entry is reached via add-user/rotate-password below.
16
16
  ctx.openModal?.('local-auth-modal');
@@ -4,29 +4,35 @@ import { writeFile, unlink } from 'node:fs/promises';
4
4
  import type { CommandRegistry } from '../command-registry.ts';
5
5
  import { fetchModelContextWindows } from '@pellux/goodvibes-sdk/platform/discovery';
6
6
  import type { CustomProviderConfig } from '@pellux/goodvibes-sdk/platform/providers';
7
- import { requireProviderApi, requireShellPaths } from './runtime-services.ts';
7
+ import { requireProviderApi, requireSecretsManager, requireShellPaths } from './runtime-services.ts';
8
+ import { ensureProviderKeyThenSelect } from '../provider-key-intake.ts';
8
9
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
9
10
 
10
11
  function isValidProviderName(name: string): boolean {
11
12
  return /^[a-zA-Z0-9_-]+$/.test(name);
12
13
  }
13
14
 
15
+ /** Conventional secrets-store key a custom provider reads its API key from. */
16
+ function customProviderKeyName(providerName: string): string {
17
+ return `${providerName.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}_API_KEY`;
18
+ }
19
+
14
20
  export function registerLocalProviderRuntimeCommands(registry: CommandRegistry): void {
15
21
  registry.register({
16
22
  name: 'provider',
17
23
  aliases: ['p'],
18
24
  description: 'Switch provider or manage custom providers (add/remove)',
19
- usage: '[add <name> <baseURL> [apiKey] | remove <name> | <provider-name>]',
25
+ usage: '[add <name> <baseURL> | remove <name> | <provider-name>]',
20
26
  argsHint: '[add|remove|name]',
21
27
  async handler(args, ctx) {
22
28
  const shellPaths = requireShellPaths(ctx);
23
29
  if (args[0] === 'add') {
24
30
  const addArgs = args.slice(1);
25
31
  if (addArgs.length < 2) {
26
- ctx.print('Usage: /provider add <name> <baseURL> [apiKey]\nExample: /provider add my-server http://192.168.0.85:8001/v1');
32
+ ctx.print('Usage: /provider add <name> <baseURL>\nExample: /provider add my-server http://192.168.0.85:8001/v1\nAfter the provider is created you can enter its API key with the keystrokes masked (or skip it for keyless endpoints).');
27
33
  return;
28
34
  }
29
- const [name, baseURL, apiKey] = addArgs;
35
+ const [name, baseURL] = addArgs;
30
36
  if (!isValidProviderName(name)) {
31
37
  ctx.print('Error: Provider name must contain only letters, numbers, hyphens, and underscores.');
32
38
  return;
@@ -51,7 +57,6 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
51
57
  const controller = new AbortController();
52
58
  const timeoutId = setTimeout(() => controller.abort(), 5000);
53
59
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
54
- if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
55
60
  const res = await fetch(`${baseURL}/models`, { signal: controller.signal, headers });
56
61
  clearTimeout(timeoutId);
57
62
  if (res.ok) {
@@ -91,12 +96,16 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
91
96
  contextWindow: contextWindows[id] ?? 8192,
92
97
  capabilities: { toolCalling: true, codeEditing: true, reasoning: false, multimodal: false },
93
98
  }));
99
+ // The key is never embedded in the provider JSON. The config references
100
+ // it by name (apiKeyEnv) and the actual value lands in the secrets
101
+ // manager below, resolved live at request time.
102
+ const keyName = customProviderKeyName(name);
94
103
  const config: CustomProviderConfig = {
95
104
  name,
96
105
  displayName: name,
97
106
  type: 'openai-compat',
98
107
  baseURL,
99
- ...(apiKey ? { apiKey } : {}),
108
+ apiKeyEnv: keyName,
100
109
  models,
101
110
  };
102
111
  try {
@@ -107,6 +116,32 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
107
116
  return;
108
117
  }
109
118
  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.`);
119
+ // Capture the key with the keystrokes masked, straight into the secrets
120
+ // manager — never a cleartext argument, never written into the JSON.
121
+ if (ctx.beginConcealedInput) {
122
+ const secretsManager = requireSecretsManager(ctx);
123
+ ctx.print(`Enter the API key for provider '${name}' (input is masked; press Enter to store, Esc to skip if the endpoint needs no key).`);
124
+ ctx.beginConcealedInput({
125
+ label: name,
126
+ onSubmit: (value) => {
127
+ if (value.length === 0) {
128
+ ctx.print(`No key stored for '${name}' (endpoint used without a key).`);
129
+ return;
130
+ }
131
+ void (async () => {
132
+ try {
133
+ await secretsManager.set(keyName, value, { scope: 'user', medium: 'secure' });
134
+ ctx.print(`Stored the API key for '${name}' (value hidden). It resolves live for this provider — no restart needed.`);
135
+ } catch (error) {
136
+ ctx.print(`Could not store the key for '${name}': ${summarizeError(error)}`);
137
+ }
138
+ })();
139
+ },
140
+ onCancel: () => {
141
+ ctx.print(`No key stored for '${name}' (endpoint used without a key).`);
142
+ },
143
+ });
144
+ }
110
145
  return;
111
146
  }
112
147
 
@@ -157,10 +192,14 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
157
192
  }
158
193
  try {
159
194
  const selected = await providerApi.selectModel(match.registryKey);
160
- ctx.session.runtime.model = selected.registryKey;
161
- ctx.session.runtime.provider = selected.providerId;
162
- ctx.platform.configManager.set('provider.model', selected.registryKey);
163
- ctx.print(`Switched to provider: ${selected.providerId} (model: ${selected.modelId})`);
195
+ // Unconfigured provider selected → capture its key inline, then finish
196
+ // the switch so it lands ready to chat.
197
+ ensureProviderKeyThenSelect(ctx, selected.providerId, () => {
198
+ ctx.session.runtime.model = selected.registryKey;
199
+ ctx.session.runtime.provider = selected.providerId;
200
+ ctx.platform.configManager.set('provider.model', selected.registryKey);
201
+ ctx.print(`Switched to provider: ${selected.providerId} (model: ${selected.modelId})`);
202
+ });
164
203
  } catch (e) {
165
204
  ctx.print(`Error: ${summarizeError(e)}`);
166
205
  }
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
4
4
  import type { CommandRegistry, CommandContext } from '../command-registry.ts';
5
5
  import type { SelectionItem } from '../selection-modal.ts';
6
6
  import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
7
+ import { formatModelDiscoveryReport } from '@pellux/goodvibes-sdk/platform/providers';
7
8
  import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
8
9
  import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config';
9
10
  import { openCommandPanel, requireBookmarkManager, requireProviderApi, requireSecretsManager } from './runtime-services.ts';
@@ -70,7 +71,7 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
70
71
  handler(args, ctx) {
71
72
  const sub = (args[0] ?? '').toLowerCase();
72
73
  if (sub === 'panel' || sub === 'review') {
73
- // W6.1 (the purge): 'tools'/ToolInspectorPanel was DELETE-disposition
74
+ // (the purge): 'tools'/ToolInspectorPanel was DELETE-disposition
74
75
  // (no surviving human surface — tool results render inline in the
75
76
  // transcript, plus a per-node tool list in Fleet). There is no alias
76
77
  // to resolve through, so this prints an honest notice and opens
@@ -200,10 +201,42 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
200
201
  const flags = new Set(rest.filter((value) => value.startsWith('--')));
201
202
  const valueParts = rest.filter((value) => !value.startsWith('--'));
202
203
  const [key, ...rawValueParts] = valueParts;
203
- if (!key || valueParts.length === 0) {
204
+ if (!key) {
204
205
  ctx.print(`[secrets] Usage: /secrets ${sub} <KEY> <${sub === 'link' ? 'secret-ref' : 'value'}> [--user|--project] [--secure|--plaintext]`);
205
206
  return;
206
207
  }
208
+ const scope = flags.has('--user') ? 'user' : 'project';
209
+ const medium = flags.has('--plaintext') ? 'plaintext' : 'secure';
210
+ // /secrets set KEY (no value) drops into the concealed prompt so the
211
+ // value is typed with the keystrokes masked and never enters history.
212
+ if (sub === 'set' && rawValueParts.length === 0) {
213
+ if (!ctx.beginConcealedInput) {
214
+ ctx.print('[secrets] Usage: /secrets set <KEY> <value> [--user|--project] [--secure|--plaintext]');
215
+ return;
216
+ }
217
+ ctx.print(`[secrets] Enter value for ${key} (input is masked; press Enter to store, Esc to cancel).`);
218
+ ctx.beginConcealedInput({
219
+ label: key,
220
+ onSubmit: (value) => {
221
+ if (value.length === 0) {
222
+ ctx.print(`[secrets] ${key} not set (empty value).`);
223
+ return;
224
+ }
225
+ void (async () => {
226
+ try {
227
+ await mgr.set(key, value, { scope, medium });
228
+ ctx.print(`[secrets] Stored: ${key} (${scope}, ${medium})`);
229
+ } catch (error) {
230
+ ctx.print(`[secrets] Could not store ${key}: ${summarizeError(error)}`);
231
+ }
232
+ })();
233
+ },
234
+ onCancel: () => {
235
+ ctx.print(`[secrets] ${key} cancelled.`);
236
+ },
237
+ });
238
+ return;
239
+ }
207
240
  const value = rawValueParts.join(' ');
208
241
  if (sub === 'link' && !isGoodVibesSecretRefInput(value)) {
209
242
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
@@ -213,8 +246,6 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
213
246
  ctx.print('[secrets] Invalid secret reference. Use /secrets providers for examples.');
214
247
  return;
215
248
  }
216
- const scope = flags.has('--user') ? 'user' : 'project';
217
- const medium = flags.has('--plaintext') ? 'plaintext' : 'secure';
218
249
  await mgr.set(key, value, { scope, medium });
219
250
  ctx.print(sub === 'link'
220
251
  ? `[secrets] Linked: ${key} -> ${describeSecretRef(value)} (${scope}, ${medium})`
@@ -337,7 +368,24 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
337
368
  } catch (e) {
338
369
  ctx.print(`Token limits refresh failed: ${summarizeError(e)}`);
339
370
  }
340
- if (!catalogOk || !benchmarksOk || !limitsOk) ctx.print('Some refreshes failed — see messages above.');
371
+ ctx.print('Refreshing live provider model lists...');
372
+ let discoveryOk = false;
373
+ try {
374
+ // Explicit user refresh → force past each provider's TTL cache and
375
+ // report the delta ("3 new, 1 retired") or the honest fallback reason.
376
+ const reports = await providerApi.refreshLiveModelDiscovery(undefined, { force: true });
377
+ discoveryOk = true;
378
+ if (reports.length === 0) {
379
+ ctx.print('No providers perform live model discovery.');
380
+ } else {
381
+ for (const report of reports) {
382
+ ctx.print(` ${formatModelDiscoveryReport(report.providerId, report)}`);
383
+ }
384
+ }
385
+ } catch (e) {
386
+ ctx.print(`Live model discovery failed: ${summarizeError(e)}`);
387
+ }
388
+ if (!catalogOk || !benchmarksOk || !limitsOk || !discoveryOk) ctx.print('Some refreshes failed — see messages above.');
341
389
  },
342
390
  });
343
391
 
@@ -24,7 +24,7 @@ import { handleRecallFilesApply, handleRecallFilesReview, handleRecallFilesSync
24
24
  // ── Top-level command ─────────────────────────────────────────────────────────
25
25
 
26
26
  /**
27
- * DEBT-5 item 3 divergence note (historical): the work order that shipped
27
+ * item 3 divergence note (historical): the work order that shipped
28
28
  * this command named its front door "/memory", but at the time `/memory` was
29
29
  * already a distinct, unrelated command (session-pinned sticky notes,
30
30
  * src/input/commands/session-content.ts) with no modal surface — so that
@@ -84,7 +84,7 @@ export const recallCommand: SlashCommand = {
84
84
  handler: async (args: string[], context: CommandContext): Promise<void> => {
85
85
  const [sub, ...rest] = args;
86
86
 
87
- // DEBT-5 item 3: bare `/recall` opens the memory-modal surface — the old
87
+ // item 3: bare `/recall` opens the memory-modal surface — the old
88
88
  // bare/unknown-subcommand usage block moved to an explicit `report`
89
89
  // subcommand (scriptability preserved: /recall report).
90
90
  if (sub === undefined) {