@pellux/goodvibes-tui 0.21.0 → 0.23.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 (70) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +1 -1
  3. package/package.json +2 -1
  4. package/src/cli/completions/generate.ts +4 -8
  5. package/src/cli/entrypoint.ts +6 -0
  6. package/src/cli/management-commands.ts +1 -1
  7. package/src/cli/management-utils.ts +352 -0
  8. package/src/cli/management.ts +36 -334
  9. package/src/cli/parser.ts +17 -0
  10. package/src/cli/surface-command.ts +1 -1
  11. package/src/cli/types.ts +2 -0
  12. package/src/config/goodvibes-home-audit.ts +2 -0
  13. package/src/core/context-auto-compact.ts +110 -0
  14. package/src/core/conversation-rendering.ts +5 -2
  15. package/src/core/conversation-types.ts +24 -0
  16. package/src/core/conversation.ts +7 -12
  17. package/src/core/stream-event-wiring.ts +125 -7
  18. package/src/core/turn-event-wiring.ts +124 -0
  19. package/src/daemon/cli.ts +5 -0
  20. package/src/input/command-registry.ts +1 -0
  21. package/src/input/commands/channel-runtime.ts +139 -0
  22. package/src/input/commands/control-room-runtime.ts +5 -5
  23. package/src/input/commands/provider.ts +57 -3
  24. package/src/input/commands/runtime-services.ts +30 -1
  25. package/src/input/commands/session-workflow.ts +8 -16
  26. package/src/input/commands/session.ts +70 -20
  27. package/src/input/commands/share-runtime.ts +1 -1
  28. package/src/input/commands/shell-core.ts +54 -4
  29. package/src/input/commands.ts +2 -2
  30. package/src/input/handler-modal-routes.ts +37 -0
  31. package/src/input/handler-modal-token-routes.ts +19 -5
  32. package/src/input/handler-onboarding.ts +18 -0
  33. package/src/input/handler.ts +1 -0
  34. package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
  35. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
  36. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
  37. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  38. package/src/input/settings-modal-behavior.ts +5 -0
  39. package/src/input/settings-modal-data.ts +77 -3
  40. package/src/input/settings-modal-mutations.ts +3 -0
  41. package/src/input/settings-modal-reset.ts +154 -0
  42. package/src/input/settings-modal.ts +55 -13
  43. package/src/main.ts +58 -50
  44. package/src/panels/agent-inspector-panel.ts +120 -18
  45. package/src/panels/agent-inspector-shared.ts +29 -0
  46. package/src/panels/builtin/development.ts +1 -0
  47. package/src/panels/builtin/knowledge.ts +14 -13
  48. package/src/panels/builtin/operations.ts +22 -1
  49. package/src/panels/builtin/shared.ts +7 -0
  50. package/src/panels/cockpit-panel.ts +123 -3
  51. package/src/panels/cockpit-read-model.ts +232 -0
  52. package/src/panels/index.ts +1 -1
  53. package/src/panels/knowledge-graph-panel.ts +84 -0
  54. package/src/panels/memory-panel.ts +370 -40
  55. package/src/panels/session-maintenance.ts +66 -15
  56. package/src/renderer/agent-detail-modal.ts +107 -3
  57. package/src/renderer/compaction-history-modal.ts +55 -0
  58. package/src/renderer/compaction-preview.ts +146 -0
  59. package/src/renderer/context-status-hint.ts +54 -0
  60. package/src/renderer/settings-modal-helpers.ts +2 -2
  61. package/src/renderer/settings-modal.ts +14 -3
  62. package/src/renderer/shell-surface.ts +10 -0
  63. package/src/runtime/bootstrap-command-parts.ts +4 -0
  64. package/src/runtime/bootstrap-core.ts +116 -0
  65. package/src/runtime/bootstrap-shell.ts +11 -0
  66. package/src/runtime/bootstrap.ts +7 -0
  67. package/src/runtime/services.ts +6 -1
  68. package/src/utils/browser.ts +29 -0
  69. package/src/version.ts +1 -1
  70. package/src/panels/knowledge-panel.ts +0 -343
package/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.23.0] — 2026-06-12
8
+
9
+ Third best-in-class program release: provider failover on the turn path, the context/compaction surface completed, the E20 export track closed, and the renderer pinned by golden frames. Every change passed independent review before commit.
10
+
11
+ ### Features
12
+ - Added turn-path provider failover with bounded same-turn retries: the fallback chain is walked with a per-turn visited set, duplicate user messages are prevented by a message-count snapshot and rollback, synthetic models are never failed into, and retries preserve the original content and options.
13
+ - Added /compact preview and after-notice: a clearly labelled estimate before compaction and the real before/after message and token figures from the compaction event when it completes.
14
+ - Added /compact-history: lists past compaction events; restore is list-only until the SDK exposes a snapshot/restore API, and the output says so plainly.
15
+ - Added /keep: pin text into session memory; pinned entries flow into the compaction handoff on both manual and auto-compact paths, with honest session-only wording (named /keep because /pin belongs to model favorites).
16
+ - Added /channel: routes, delivery, status, and policy snapshots for the omnichannel substrate, with --json on every subcommand.
17
+ - Added inbound event narration: GitHub, Slack, ntfy, and webhook events emit a transcript line naming the surface and event before agent dispatch, with self-narration guarded for companion and internal sources.
18
+
19
+ ### Fixes
20
+ - Fixed a command registration collision between the new session-memory pin and the model-favorites /pin by naming the new command /keep.
21
+ - Deflaked the websocket reconnect test with a deadline poll in place of a fixed sleep.
22
+
23
+ ### Internal
24
+ - Architecture gate now detects import cycles (Tarjan SCC over the import graph, proven against an injected scratch cycle) and enforces 8 directional layer rules; the conversation and system-message-router cycle is genuinely broken via the runtime barrel.
25
+ - Auth listener behavior pinned by 18 live tests: default login rate limit (five 401s then 429), proxy-spoofing posture, and empty-password handling.
26
+ - Golden-frame harness pins four renderer surfaces with committed snapshots: regeneration is byte-for-byte stable, a missing golden fails instead of regenerating, and GOODVIBES_UPDATE_GOLDENS=1 is the only write path.
27
+ - CLI management helpers deduplicated into management-utils with openBrowser extracted to a shared utility.
28
+
29
+ ## [0.22.0] — 2026-06-12
30
+
31
+ Second best-in-class program release: the backlog tail, the providers/failover track opened, and the release pipeline made fully honest. Every change passed independent review at 10/10 before commit.
32
+
33
+ ### Features
34
+ - Added a Cockpit control room: live agent roster (status, stalled flag, real cost/tokens), inspect and confirm-gated cancel action keys on the shared orphan-free cancel path.
35
+ - Added /session as the single front-door for session work: lifecycle subcommands (list, rename, resume, fork, save, info, export, search, delete, events, groups, hotspots) and orchestration (link-task with cycle detection, handoff, graph, cancel with task/subtree/session scopes); alias /sess.
36
+ - Added a merged memory panel (records + review queue via Tab) with the knowledge-graph view on its own panel; /project-memory routes to records; session scratch surface renamed to notes.
37
+ - Added settings reset-category and reset-all (Shift+R / Ctrl+Shift+R, confirm-gated, runtime-synced), schema-derived unknown-key warnings at load, restart hints from a typed key table, and width-honest footers proven at 80 and 120 columns.
38
+ - Added wizard required-field gating: apply blocks with per-field messages and a focus jump to the first offender.
39
+ - Added the provider optimizer enable switch: /provider optimizer on|off drives the live instance and persists; /provider route auto says honestly when the optimizer is off.
40
+ - Added wizard security hardening: Cloudflare tunnel apply sets trustProxy with the residual risk named until SDK header validation ships; TLS plaintext hard-warn in the wizard and a startup banner; CORS guidance.
41
+ - Added context truth: auto-compact runs only when behavior.autoCompact is enabled (threshold from config, percent converted correctly), with a passive suggest-compact status hint and honest transcript notices.
42
+ - Added tts.speed to settings, bridged end-to-end today (modal to synthesis call); explicit tts.* defaults with truthful modified markers.
43
+
44
+ ### Fixes
45
+ - Fixed CLI flag truth end-to-end: a drift-proof parity test extracts flags from parser source and asserts equality across help, completions, and docs; ghost completion flags removed; --output-format deprecated with a stderr warning; session lifecycle flags are mutually exclusive with a clear error.
46
+ - Fixed onboarding marker timing residue and wizard copy; sessions, recovery, and WRFC chains read through version-gated, quarantining loaders everywhere.
47
+
48
+ ### Internal
49
+ - Performance gate is real: fail-closed in CI without a baseline, headless startup and frame benches sharing one methodology and one CI-safe budget source (p95 16ms, p99 110ms with runner headroom), committed ratchet baseline.
50
+ - Per-agent stall watchdog constants shared; agent cost/token surfaces fabricated-number-free; concurrency-safe test temp dirs; eval gate fail-closed with a committed baseline; 540 test files green.
51
+
7
52
  ## [0.21.0] — 2026-06-12
8
53
 
9
54
  First release of the best-in-class program: a full UX-first review of the codebase followed by WRFC-gated remediation (every change passed independent review at 10/10 before commit).
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.21.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.23.0-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -57,6 +57,7 @@
57
57
  "build:prod": "bun run scripts/build.ts",
58
58
  "build:all": "bun run scripts/build.ts --all",
59
59
  "perf:check": "bun run scripts/perf-check.ts",
60
+ "perf:baseline": "GOODVIBES_PERF_SAVE_BASELINE=1 bun run scripts/perf-check.ts",
60
61
  "architecture:check": "bun run scripts/check-architecture.ts",
61
62
  "audit:home": "bun run scripts/audit-goodvibes-home.ts",
62
63
  "foundation:artifacts": "bun run scripts/export-foundation-artifacts.ts",
@@ -137,12 +137,8 @@ export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
137
137
  },
138
138
  { name: '--continue', takesValue: false, description: 'Continue the latest session' },
139
139
  { name: '--fork', takesValue: false, description: 'Fork session when supported' },
140
- { name: '--raw-output', takesValue: false, description: 'Raw output mode' },
141
- {
142
- name: '--accept-raw-output-risk',
143
- takesValue: false,
144
- description: 'Acknowledge raw output risk',
145
- },
140
+ { name: '--yes', short: '-y', takesValue: false, description: 'Auto-confirm prompts (non-interactive)' },
141
+ { name: '--non-interactive', takesValue: false, description: 'Disable all interactive prompts (implies --yes)' },
146
142
  ] as const;
147
143
 
148
144
  // ---------------------------------------------------------------------------
@@ -512,8 +508,8 @@ export function generateZsh(surface: CompletionSurface): string {
512
508
  lines.push(` '(-s --session)'{-s,--session}'[Use a specific session]:id:' \\`);
513
509
  lines.push(` '--continue[Continue the latest session]' \\`);
514
510
  lines.push(` '--fork[Fork session when supported]' \\`);
515
- lines.push(` '--raw-output[Raw output mode]' \\`);
516
- lines.push(` '--accept-raw-output-risk[Acknowledge raw output risk]' \\`);
511
+ lines.push(` '(-y --yes)'{-y,--yes}'[Auto-confirm prompts]' \\`);
512
+ lines.push(` '--non-interactive[Disable all interactive prompts]' \\`);
517
513
  lines.push(` '1:command:->cmd' \\`);
518
514
  lines.push(` '*:args:->args';`);
519
515
  lines.push('');
@@ -61,6 +61,12 @@ export async function prepareShellCliRuntime(
61
61
  process.exit(2);
62
62
  }
63
63
 
64
+ if (cli.warnings.length > 0) {
65
+ for (const warning of cli.warnings) {
66
+ console.warn(`[goodvibes] warning: ${warning}`);
67
+ }
68
+ }
69
+
64
70
  if (cli.flags.help || cli.command === 'help') {
65
71
  const helpTopic = cli.command === 'help'
66
72
  ? cli.commandArgs[0]
@@ -10,7 +10,7 @@ import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platfo
10
10
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
11
11
  import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
12
12
  import type { CliCommandRuntime } from './types.ts';
13
- import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
13
+ import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management-utils.ts';
14
14
 
15
15
  export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
16
16
  return await withRuntimeServices(runtime, async (services) => {
@@ -0,0 +1,352 @@
1
+ /**
2
+ * management-utils.ts — shared CLI utility functions.
3
+ *
4
+ * Extracted from management.ts to break the import cycle:
5
+ * management.ts ↔ management-commands.ts
6
+ * management.ts ↔ surface-command.ts
7
+ *
8
+ * Both management-commands.ts and surface-command.ts import from here;
9
+ * management.ts also imports from here (no longer from the child modules
10
+ * for these utilities).
11
+ *
12
+ * No imports from management-commands.ts or surface-command.ts are allowed
13
+ * in this file — that would recreate the cycle.
14
+ */
15
+
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import net from 'node:net';
19
+ import { spawn } from 'node:child_process';
20
+ import { networkInterfaces } from 'node:os';
21
+ import type { ConfigManager, GoodVibesConfig } from '../config/index.ts';
22
+ import { bootstrapRuntime } from '../runtime/bootstrap.ts';
23
+ import { createRuntimeServices } from '../runtime/services.ts';
24
+ import { createRuntimeStore } from '../runtime/store/index.ts';
25
+ import type { RuntimeServices } from '../runtime/services.ts';
26
+ import { RuntimeEventBus, type TurnEvent, createShellPathService } from '@/runtime/index.ts';
27
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
28
+ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
29
+ import { applyRuntimeEndpointFlagOverrides } from './config-overrides.ts';
30
+ import type { RuntimeEndpointId } from './endpoints.ts';
31
+ import type { CliCommandRuntime, GoodVibesCliParseResult } from './types.ts';
32
+ import { openBrowser as _openBrowser } from '../utils/browser.ts';
33
+
34
+ type Formatter = (value: unknown, text: string) => string;
35
+
36
+ export function yesNo(value: unknown): string {
37
+ return value === true ? 'yes' : 'no';
38
+ }
39
+
40
+ export function formatJsonOrText(cli: GoodVibesCliParseResult): Formatter {
41
+ return (value, text) => cli.flags.outputFormat === 'json'
42
+ ? JSON.stringify(value, null, 2)
43
+ : text;
44
+ }
45
+
46
+ export function exitCodeForText(output: string): number {
47
+ if (output.startsWith('Usage:') || output.startsWith('Invalid ')) return 2;
48
+ if (output.startsWith('Session not found:') || output.startsWith('Unknown task:') || output.startsWith('Task submit failed ')) return 1;
49
+ if (output.startsWith('No stored ') || output.startsWith('No pending ') || output.startsWith('No model ') || output.startsWith('No provider ') || output.startsWith('No auth ')) return 1;
50
+ if (output.startsWith('Unknown ')) return 1;
51
+ if (output === 'Bundle has no config object to import.') return 1;
52
+ return 0;
53
+ }
54
+
55
+ export function splitCommandOption(token: string): { readonly name: string; readonly value: string | undefined } {
56
+ const index = token.indexOf('=');
57
+ if (index < 0) return { name: token, value: undefined };
58
+ return { name: token.slice(0, index), value: token.slice(index + 1) };
59
+ }
60
+
61
+ export function readOptionValue(args: readonly string[], name: string): string | undefined {
62
+ for (let index = 0; index < args.length; index += 1) {
63
+ const token = args[index]!;
64
+ const split = splitCommandOption(token);
65
+ if (split.name !== name) continue;
66
+ if (split.value !== undefined) return split.value;
67
+ const next = args[index + 1];
68
+ if (next === undefined || next.startsWith('--')) return undefined;
69
+ return next;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ export function readOptionValues(args: readonly string[], name: string): string[] {
75
+ const values: string[] = [];
76
+ for (let index = 0; index < args.length; index += 1) {
77
+ const token = args[index]!;
78
+ const split = splitCommandOption(token);
79
+ if (split.name !== name) continue;
80
+ if (split.value !== undefined) {
81
+ values.push(split.value);
82
+ continue;
83
+ }
84
+ const next = args[index + 1];
85
+ if (next !== undefined && !next.startsWith('--')) values.push(next);
86
+ }
87
+ return values;
88
+ }
89
+
90
+ export function hasCommandFlag(args: readonly string[], name: string): boolean {
91
+ return args.some((arg) => splitCommandOption(arg).name === name);
92
+ }
93
+
94
+ export function commandValues(args: readonly string[]): string[] {
95
+ const values: string[] = [];
96
+ for (let index = 0; index < args.length; index += 1) {
97
+ const token = args[index]!;
98
+ if (!token.startsWith('--')) {
99
+ values.push(token);
100
+ continue;
101
+ }
102
+ if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
103
+ }
104
+ return values;
105
+ }
106
+
107
+ export function readPassword(args: readonly string[]): string | null {
108
+ const explicit = readOptionValue(args, '--password');
109
+ if (explicit !== undefined) return explicit;
110
+ if (hasCommandFlag(args, '--password-stdin')) return readFileSync(0, 'utf-8').trimEnd();
111
+ return process.env.GOODVIBES_AUTH_PASSWORD ?? null;
112
+ }
113
+
114
+ export function extractAuthorizationCode(input: string): string {
115
+ try {
116
+ const url = new URL(input);
117
+ return url.searchParams.get('code') ?? input;
118
+ } catch {
119
+ return input;
120
+ }
121
+ }
122
+
123
+ export function isPresentConfigValue(value: unknown): boolean {
124
+ if (typeof value === 'string') return value.trim().length > 0;
125
+ return value !== undefined && value !== null && value !== false;
126
+ }
127
+
128
+ export function getNestedValue(source: unknown, key: string): unknown {
129
+ let cursor = source;
130
+ for (const part of key.split('.')) {
131
+ if (cursor == null || typeof cursor !== 'object') return undefined;
132
+ cursor = (cursor as Record<string, unknown>)[part];
133
+ }
134
+ return cursor;
135
+ }
136
+
137
+ function getLocalNetworkIp(): string {
138
+ try {
139
+ const nets = networkInterfaces();
140
+ for (const name of Object.keys(nets)) {
141
+ for (const netInfo of nets[name] ?? []) {
142
+ if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
143
+ }
144
+ }
145
+ } catch {
146
+ return '127.0.0.1';
147
+ }
148
+ return '127.0.0.1';
149
+ }
150
+
151
+ function connectHostForBindHost(host: string): string {
152
+ if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
153
+ return host || '127.0.0.1';
154
+ }
155
+
156
+ export function urlHostForBindHost(host: string): string {
157
+ if (host === '0.0.0.0' || host === '::') return getLocalNetworkIp();
158
+ return host || '127.0.0.1';
159
+ }
160
+
161
+ export function enableServicePosture(config: ConfigManager): void {
162
+ config.setDynamic('service.enabled', true);
163
+ config.setDynamic('service.autostart', true);
164
+ config.setDynamic('service.restartOnFailure', true);
165
+ }
166
+
167
+ export function enableEndpointLanDefault(config: ConfigManager, endpoint: RuntimeEndpointId): void {
168
+ const binding = resolveRuntimeEndpointBinding(config, endpoint);
169
+ if (binding.hostMode === 'custom') return;
170
+ if (endpoint === 'controlPlane') {
171
+ config.setDynamic('controlPlane.hostMode', 'network');
172
+ config.setDynamic('controlPlane.host', '0.0.0.0');
173
+ config.setDynamic('controlPlane.allowRemote', true);
174
+ return;
175
+ }
176
+ if (endpoint === 'httpListener') {
177
+ config.setDynamic('httpListener.hostMode', 'network');
178
+ config.setDynamic('httpListener.host', '0.0.0.0');
179
+ return;
180
+ }
181
+ config.setDynamic('web.hostMode', 'network');
182
+ config.setDynamic('web.host', '0.0.0.0');
183
+ }
184
+
185
+ export function applyTargetEndpointFlagsOrDefault(
186
+ runtime: CliCommandRuntime,
187
+ endpoint: RuntimeEndpointId,
188
+ ): string | null {
189
+ const errors = applyRuntimeEndpointFlagOverrides(runtime.configManager, endpoint, runtime.cli.flags);
190
+ if (errors.length > 0) return errors.join('\n');
191
+ if (runtime.cli.flags.hostname === undefined) {
192
+ enableEndpointLanDefault(runtime.configManager, endpoint);
193
+ }
194
+ if (endpoint === 'controlPlane') {
195
+ const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint);
196
+ runtime.configManager.setDynamic('controlPlane.allowRemote', binding.hostMode !== 'local');
197
+ }
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * Re-export openBrowser from utils/browser.ts for backward compatibility.
203
+ * management.ts and management-commands.ts both used to export/import this;
204
+ * now all callers should prefer importing from utils/browser.ts directly.
205
+ */
206
+ export { openBrowser } from '../utils/browser.ts';
207
+
208
+ export async function probeTcp(host: string, port: number, timeoutMs = 750): Promise<boolean> {
209
+ return await new Promise<boolean>((resolve) => {
210
+ const socket = net.createConnection({ host: connectHostForBindHost(host), port });
211
+ const finish = (value: boolean) => {
212
+ socket.removeAllListeners();
213
+ socket.destroy();
214
+ resolve(value);
215
+ };
216
+ socket.setTimeout(timeoutMs);
217
+ socket.once('connect', () => finish(true));
218
+ socket.once('timeout', () => finish(false));
219
+ socket.once('error', () => finish(false));
220
+ });
221
+ }
222
+
223
+ export async function withRuntimeServices<T>(
224
+ runtime: CliCommandRuntime,
225
+ fn: (services: RuntimeServices) => Promise<T> | T,
226
+ ): Promise<T> {
227
+ const runtimeBus = new RuntimeEventBus();
228
+ const runtimeStore = createRuntimeStore();
229
+ const services = createRuntimeServices({
230
+ configManager: runtime.configManager,
231
+ runtimeBus,
232
+ runtimeStore,
233
+ workingDir: runtime.workingDirectory,
234
+ homeDirectory: runtime.homeDirectory,
235
+ });
236
+ services.providerRegistry.initModelLimits();
237
+ services.benchmarkStore.initBenchmarks();
238
+ services.providerRegistry.initCatalog();
239
+ try {
240
+ await services.providerRegistry.ready();
241
+ return await fn(services);
242
+ } finally {
243
+ services.providerRegistry.stopWatching();
244
+ }
245
+ }
246
+
247
+ export function readAuthPaths(runtime: CliCommandRuntime) {
248
+ const shellPaths = createShellPathService({
249
+ workingDirectory: runtime.workingDirectory,
250
+ homeDirectory: runtime.homeDirectory,
251
+ });
252
+ const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
253
+ const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
254
+ const operatorTokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
255
+ return {
256
+ userStorePath,
257
+ userStorePresent: existsSync(userStorePath),
258
+ bootstrapCredentialPath,
259
+ bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
260
+ operatorTokenPath,
261
+ operatorTokenPresent: existsSync(operatorTokenPath),
262
+ };
263
+ }
264
+
265
+ export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
266
+ const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
267
+ if (!prompt) {
268
+ console.error('Usage: goodvibes run|exec [prompt]');
269
+ return 2;
270
+ }
271
+
272
+ const outputFormat = runtime.cli.flags.outputFormat;
273
+ const ctx = await bootstrapRuntime(process.stdout, {
274
+ configManager: runtime.configManager,
275
+ workingDir: runtime.workingDirectory,
276
+ homeDirectory: runtime.homeDirectory,
277
+ });
278
+
279
+ const events: TurnEvent[] = [];
280
+ let finalResponse = '';
281
+ let finalError = '';
282
+ let finalStopReason = '';
283
+ let exitCode = 0;
284
+
285
+ const done = new Promise<void>((resolve) => {
286
+ const unsubs = [
287
+ ctx.runtimeBus.on<Extract<TurnEvent, { type: 'STREAM_DELTA' }>>('STREAM_DELTA', ({ payload }) => {
288
+ events.push(payload);
289
+ if (outputFormat === 'stream-json') {
290
+ process.stdout.write(JSON.stringify({ type: payload.type, content: payload.content, accumulated: payload.accumulated }) + '\n');
291
+ }
292
+ }),
293
+ ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_COMPLETED' }>>('TURN_COMPLETED', ({ payload }) => {
294
+ events.push(payload);
295
+ finalResponse = payload.response;
296
+ finalStopReason = payload.stopReason;
297
+ for (const unsub of unsubs) unsub();
298
+ resolve();
299
+ }),
300
+ ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_ERROR' }>>('TURN_ERROR', ({ payload }) => {
301
+ events.push(payload);
302
+ finalError = payload.error;
303
+ finalStopReason = payload.stopReason;
304
+ exitCode = 1;
305
+ for (const unsub of unsubs) unsub();
306
+ resolve();
307
+ }),
308
+ ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_CANCEL' }>>('TURN_CANCEL', ({ payload }) => {
309
+ events.push(payload);
310
+ finalError = payload.reason ?? 'cancelled';
311
+ finalStopReason = payload.stopReason;
312
+ exitCode = 130;
313
+ for (const unsub of unsubs) unsub();
314
+ resolve();
315
+ }),
316
+ ];
317
+ });
318
+
319
+ try {
320
+ await ctx.orchestrator.handleUserInput(prompt);
321
+ await done;
322
+ if (outputFormat === 'json') {
323
+ process.stdout.write(JSON.stringify({
324
+ ok: exitCode === 0,
325
+ response: finalResponse,
326
+ error: finalError || undefined,
327
+ stopReason: finalStopReason,
328
+ sessionId: ctx.runtime.sessionId,
329
+ model: ctx.runtime.model,
330
+ provider: ctx.runtime.provider,
331
+ events: events.length,
332
+ }, null, 2) + '\n');
333
+ } else if (outputFormat !== 'stream-json') {
334
+ process.stdout.write((exitCode === 0 ? finalResponse : finalError) + '\n');
335
+ } else {
336
+ process.stdout.write(JSON.stringify({
337
+ type: exitCode === 0 ? 'TURN_COMPLETED' : 'TURN_ERROR',
338
+ ok: exitCode === 0,
339
+ response: finalResponse,
340
+ error: finalError || undefined,
341
+ stopReason: finalStopReason,
342
+ }) + '\n');
343
+ }
344
+ } finally {
345
+ const snapshot = ctx.conversation.toJSON() as Parameters<typeof ctx.shutdown>[0];
346
+ await ctx.shutdown(snapshot);
347
+ }
348
+ return exitCode;
349
+ }
350
+
351
+ /** @deprecated Use ConfigManager directly. Kept for backward compat with management.ts usages. */
352
+ export type { GoodVibesConfig };