@pellux/goodvibes-tui 0.22.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.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +36 -334
- package/src/cli/surface-command.ts +1 -1
- package/src/core/context-auto-compact.ts +43 -10
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/stream-event-wiring.ts +125 -7
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +54 -4
- package/src/input/commands.ts +2 -0
- package/src/main.ts +26 -26
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/runtime/bootstrap-core.ts +92 -0
- package/src/utils/browser.ts +29 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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
|
+
|
|
7
29
|
## [0.22.0] — 2026-06-12
|
|
8
30
|
|
|
9
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.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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.
|
|
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",
|
|
@@ -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 };
|