@pellux/goodvibes-tui 0.22.0 → 0.24.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 +47 -0
- package/README.md +17 -8
- 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 +116 -344
- 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/long-task-notifier.ts +145 -0
- package/src/core/session-recovery.ts +147 -0
- package/src/core/stream-event-wiring.ts +199 -7
- package/src/core/transcript-journal.ts +339 -0
- package/src/core/turn-event-wiring.ts +67 -4
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +0 -2
- package/src/input/commands/diff-runtime.ts +1 -1
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +23 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-runtime.ts +1 -2
- package/src/input/commands/memory-product-runtime.ts +2 -2
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/onboarding-runtime.ts +0 -1
- package/src/input/commands/policy.ts +1 -1
- package/src/input/commands/profile-sync-runtime.ts +4 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +0 -1
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/session-workflow.ts +32 -2
- package/src/input/commands/session.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +9 -9
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +56 -6
- package/src/input/commands/work-plan-runtime.ts +8 -8
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +6 -0
- package/src/input/handler-feed-routes.ts +19 -1
- package/src/input/handler-feed.ts +11 -0
- package/src/input/handler-prompt-buffer.ts +28 -0
- package/src/input/handler-shortcuts.ts +88 -2
- package/src/input/handler-ui-state.ts +2 -2
- package/src/input/handler.ts +39 -3
- package/src/input/keybindings.ts +33 -3
- package/src/input/kill-ring.ts +134 -0
- package/src/input/model-picker.ts +18 -1
- package/src/input/search.ts +18 -6
- package/src/input/settings-modal-activation.ts +134 -0
- package/src/input/settings-modal-adjustment.ts +124 -0
- package/src/input/settings-modal-data.ts +53 -0
- package/src/input/settings-modal.ts +48 -145
- package/src/main.ts +50 -50
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/panels/provider-health-panel.ts +13 -9
- package/src/panels/provider-health-tracker.ts +7 -4
- package/src/panels/settings-sync-panel.ts +3 -3
- package/src/panels/work-plan-panel.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/diff-view.ts +2 -2
- package/src/renderer/help-overlay.ts +1 -0
- package/src/renderer/model-picker-overlay.ts +23 -11
- package/src/renderer/progress.ts +3 -3
- package/src/renderer/search-overlay.ts +8 -5
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +1 -1
- package/src/renderer/ui-factory.ts +11 -0
- package/src/runtime/bootstrap-core.ts +92 -0
- package/src/runtime/bootstrap-hook-bridge.ts +18 -0
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/shell/blocking-input.ts +32 -0
- package/src/shell/recovery-input-helpers.ts +71 -0
- package/src/utils/browser.ts +29 -0
- package/src/utils/terminal-width.ts +10 -3
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.24.0] — 2026-06-12
|
|
8
|
+
|
|
9
|
+
Fourth best-in-class program release: the engineering-task backlog closed, crash durability made real, and the composer brought to readline parity. Every change passed independent review before commit.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
- Added composer editing at readline parity: coalesced undo/redo (Ctrl+Z / Ctrl+Shift+Z with cursor restoration), a 32-entry kill ring (Ctrl+K/U, Alt+D, Ctrl+Shift+Y yank, Alt+Y yank-pop with emacs invalidation semantics), Unicode word navigation (Alt+B/F), and a line-count indicator rendered inside the prompt border so footer height never jumps.
|
|
13
|
+
- Added transcript search navigation: n/N next/previous in locked search mode, an honest (wrap) marker on the match count, and the bindings documented in both help overlays.
|
|
14
|
+
- Added a SIGKILL-proof transcript journal: fsync-per-record append-only log between snapshots, with replay wired into every resume seam — command resume, Ctrl+R crash recovery, and panel resume — proven by real-seam tests.
|
|
15
|
+
- Added push notifications for long-running turns (behavior.notifyAfterSeconds): desktop and webhook delivery with metadata only — conversation text can never leak, pinned by literal and compile-time assertions.
|
|
16
|
+
- Added one-key retry from turn errors: r re-submits through the shared failover snapshot path (never a duplicate user message), m opens the model picker; the exhausted-chain notice says plainly that r reuses the same provider.
|
|
17
|
+
- Added /health term: real terminal-capability posture in the doctor surface.
|
|
18
|
+
- Added command grammar conventions enforced by a live-registry lint; settingssync/profilesync/workplan renamed to kebab-case primaries with the old names kept as aliases.
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
- Fixed wide-character padding in diff headers, progress rows, and panel spinners (ANSI-aware width everywhere); width-utility docs state the per-code-point contract honestly.
|
|
22
|
+
- Fixed the /pin collision by naming the session-memory command /keep; model favorites keep /pin.
|
|
23
|
+
- Deflaked the websocket reconnect test with a deadline poll.
|
|
24
|
+
|
|
25
|
+
### Internal
|
|
26
|
+
- Architecture gate detects import cycles (Tarjan SCC, proven against an injected scratch cycle) and enforces 8 directional layer rules; the conversation/system-message-router cycle is genuinely broken.
|
|
27
|
+
- Golden-frame harness pins four renderer surfaces with regen-stable committed snapshots; CI fails on a missing golden.
|
|
28
|
+
- Release pipeline: live verifier runs warn-only in validation, a release-blocking macOS smoke job covers the darwin-arm64 binaries, and release.ts runs all six gates pre-tag.
|
|
29
|
+
- Auth listener behavior pinned by 18 live tests; whole-suite single-process run verified clean (8978 tests); settings-modal split into activation/adjustment modules; CLI management helpers deduplicated.
|
|
30
|
+
- Inline terminal images (Kitty/iTerm2/sixel) explicitly canned by owner decision — images stay as [image N] slugs for identical behavior in every terminal.
|
|
31
|
+
|
|
32
|
+
## [0.23.0] — 2026-06-12
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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.
|
|
40
|
+
- 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).
|
|
41
|
+
- Added /channel: routes, delivery, status, and policy snapshots for the omnichannel substrate, with --json on every subcommand.
|
|
42
|
+
- 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.
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
- Fixed a command registration collision between the new session-memory pin and the model-favorites /pin by naming the new command /keep.
|
|
46
|
+
- Deflaked the websocket reconnect test with a deadline poll in place of a fixed sleep.
|
|
47
|
+
|
|
48
|
+
### Internal
|
|
49
|
+
- 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.
|
|
50
|
+
- Auth listener behavior pinned by 18 live tests: default login rate limit (five 401s then 429), proxy-spoofing posture, and empty-password handling.
|
|
51
|
+
- 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.
|
|
52
|
+
- CLI management helpers deduplicated into management-utils with openBrowser extracted to a shared utility.
|
|
53
|
+
|
|
7
54
|
## [0.22.0] — 2026-06-12
|
|
8
55
|
|
|
9
56
|
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
|
|
|
@@ -803,7 +803,7 @@ Key commands:
|
|
|
803
803
|
- `/update`
|
|
804
804
|
- `/trust`
|
|
805
805
|
- `/bridge`
|
|
806
|
-
- `/
|
|
806
|
+
- `/profile-sync`
|
|
807
807
|
|
|
808
808
|
The setup surface is also broader than a single readiness screen:
|
|
809
809
|
|
|
@@ -848,7 +848,7 @@ GoodVibes includes an automation layer with:
|
|
|
848
848
|
- cron-like scheduled agent tasks with timezone-aware schedules, missed-run tracking, run history, and manual trigger support
|
|
849
849
|
- TUI-owned project planning with readiness gaps, one-question-at-a-time clarification, project language, decision records, task/dependency/verification metadata, and explicit execution approval
|
|
850
850
|
- planning commands with project-planning inspection, active-plan review, and mode/explain/override/status controls
|
|
851
|
-
- persistent work-plan tracking for concrete task checklists via `/
|
|
851
|
+
- persistent work-plan tracking for concrete task checklists via `/work-plan`
|
|
852
852
|
|
|
853
853
|
Key commands:
|
|
854
854
|
|
|
@@ -874,7 +874,7 @@ Key commands:
|
|
|
874
874
|
|
|
875
875
|
- `/services inspect|test|resolve|auth|auth-review|doctor|export|import`
|
|
876
876
|
- `/profiles`
|
|
877
|
-
- `/
|
|
877
|
+
- `/profile-sync`
|
|
878
878
|
- `/setup transfer export|inspect|import`
|
|
879
879
|
|
|
880
880
|
Service entries can use existing `tokenKey` fields, a SecretRef in the key field, or explicit `tokenRef` / `passwordRef` / `webhookUrlRef` / `signingSecretRef` / `publicKeyRef` / `appTokenRef` fields:
|
|
@@ -1281,7 +1281,7 @@ Those pieces cover conversation-noise routing, panel-health/performance budgets,
|
|
|
1281
1281
|
| `/git [action]` | `/g` | Git commands: status, log, diff. Opens git panel if no action given |
|
|
1282
1282
|
| `/scan` | — | Scan for local LLM servers |
|
|
1283
1283
|
| `/plan [goal]` | — | Inspect or seed TUI-owned project planning state; `panel`, `approve`, `list`, and `show <id>` are supported |
|
|
1284
|
-
| `/
|
|
1284
|
+
| `/work-plan [action]` | `/wp`, `/todo` | Open or update the persistent workspace work-plan checklist |
|
|
1285
1285
|
| `/panel [action]` | `/panels` | Panel management: open, close, list, toggle, move, focus, split, width, height |
|
|
1286
1286
|
| `/plugin [action]` | — | Manage plugins (enable/disable/reload/list) |
|
|
1287
1287
|
| `/marketplace [action]` | — | Browse curated plugin, skill, hook-pack, and policy-pack surfaces |
|
|
@@ -1333,14 +1333,23 @@ All shortcuts are customizable via `~/.goodvibes/tui/keybindings.json`. Use `/ke
|
|
|
1333
1333
|
| `Enter` | Send message |
|
|
1334
1334
|
| `Shift+Enter` | Insert newline |
|
|
1335
1335
|
| `Tab` | Toggle block collapse / path completion |
|
|
1336
|
-
| `Ctrl+U` |
|
|
1337
|
-
| `
|
|
1338
|
-
| `Ctrl+
|
|
1336
|
+
| `Ctrl+U` | Kill to start of line (push to kill ring) |
|
|
1337
|
+
| `Alt+U` | Clear entire prompt |
|
|
1338
|
+
| `Ctrl+W` | Kill word backward (push to kill ring) |
|
|
1339
|
+
| `Ctrl+K` | Kill to end of line (push to kill ring) |
|
|
1340
|
+
| `Alt+D` | Kill word forward (push to kill ring) |
|
|
1341
|
+
| `Ctrl+Shift+Y` | Yank from kill ring |
|
|
1342
|
+
| `Alt+Y` | Yank-pop (rotate kill ring, replace last yank) |
|
|
1343
|
+
| `Alt+B` | Move word backward |
|
|
1344
|
+
| `Alt+F` | Move word forward |
|
|
1339
1345
|
| `Ctrl+Z` | Undo prompt edit |
|
|
1340
1346
|
| `Ctrl+Shift+Z` | Redo prompt edit |
|
|
1341
1347
|
| `Ctrl+V` | Paste (image or text) |
|
|
1342
1348
|
| `@` | Open file picker (insert file path) |
|
|
1343
1349
|
| `?` | Open help/command picker (empty prompt) |
|
|
1350
|
+
| `r` / `m` | After a turn error: retry the turn / open the model picker (any other key dismisses) |
|
|
1351
|
+
|
|
1352
|
+
> **Note:** `Ctrl+W` uses whitespace-delimited word boundaries (readline/unix-word-rubout semantics), while `Alt+D`, `Alt+B`, and `Alt+F` use Unicode word boundaries (letters, digits, underscore).
|
|
1344
1353
|
|
|
1345
1354
|
### Navigation
|
|
1346
1355
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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 };
|