@pellux/goodvibes-tui 0.19.10 → 0.19.12
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 +62 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1169 -1121
- package/package.json +7 -6
- package/src/cli-flags.ts +12 -2
- package/src/daemon/cli.ts +45 -5
- package/src/panels/builtin/session.ts +4 -2
- package/src/panels/builtin/shared.ts +5 -0
- package/src/renderer/qr-renderer.ts +8 -5
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/services.ts +12 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.12",
|
|
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",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"build:linux-arm64": "bun build src/main.ts --compile --target=bun-linux-arm64 --outfile dist/goodvibes-linux-arm64",
|
|
33
33
|
"build:macos-x64": "bun build src/main.ts --compile --target=bun-darwin-x64 --outfile dist/goodvibes-macos-x64",
|
|
34
34
|
"build:macos-arm64": "bun build src/main.ts --compile --target=bun-darwin-arm64 --outfile dist/goodvibes-macos-arm64",
|
|
35
|
-
"build:daemon:linux-x64": "bun
|
|
36
|
-
"build:daemon:linux-arm64": "bun
|
|
37
|
-
"build:daemon:macos-x64": "bun
|
|
38
|
-
"build:daemon:macos-arm64": "bun
|
|
35
|
+
"build:daemon:linux-x64": "bun run scripts/build.ts --target daemon-linux-x64",
|
|
36
|
+
"build:daemon:linux-arm64": "bun run scripts/build.ts --target daemon-linux-arm64",
|
|
37
|
+
"build:daemon:macos-x64": "bun run scripts/build.ts --target daemon-macos-x64",
|
|
38
|
+
"build:daemon:macos-arm64": "bun run scripts/build.ts --target daemon-macos-arm64",
|
|
39
|
+
"smoke:daemon": "bun run scripts/post-build-smoke.ts",
|
|
39
40
|
"build:windows": "bun build src/main.ts --compile --target=bun-windows-x64 --outfile dist/goodvibes-windows.exe",
|
|
40
41
|
"build:all-shell": "bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:macos-x64 && bun run build:macos-arm64 && bun run build:windows",
|
|
41
42
|
"test": "bun run scripts/run-tests.ts",
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
90
91
|
"@ast-grep/napi": "^0.42.0",
|
|
91
92
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
92
|
-
"@pellux/goodvibes-sdk": "0.21.
|
|
93
|
+
"@pellux/goodvibes-sdk": "0.21.23",
|
|
93
94
|
"bash-language-server": "^5.6.0",
|
|
94
95
|
"fuse.js": "^7.1.0",
|
|
95
96
|
"graphql": "^16.13.2",
|
package/src/cli-flags.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
export type CliFlags = {
|
|
6
6
|
readonly provider: string | undefined;
|
|
7
7
|
readonly model: string | undefined;
|
|
8
|
+
readonly daemonHome: string | undefined;
|
|
9
|
+
readonly workingDir: string | undefined;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Parse `--provider` / `--model` / `--help` flags from an argv slice.
|
|
13
|
+
* Parse `--provider` / `--model` / `--daemon-home` / `--working-dir` / `--help` flags from an argv slice.
|
|
12
14
|
*
|
|
13
15
|
* @param argv - argv array (pass `process.argv.slice(2)`)
|
|
14
16
|
* @param binary - binary name shown in the --help usage line (e.g. "goodvibes" or "goodvibes-daemon")
|
|
@@ -16,6 +18,8 @@ export type CliFlags = {
|
|
|
16
18
|
export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): CliFlags {
|
|
17
19
|
let provider: string | undefined;
|
|
18
20
|
let model: string | undefined;
|
|
21
|
+
let daemonHome: string | undefined;
|
|
22
|
+
let workingDir: string | undefined;
|
|
19
23
|
|
|
20
24
|
for (let i = 0; i < argv.length; i++) {
|
|
21
25
|
const arg = argv[i];
|
|
@@ -29,6 +33,8 @@ export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): Cl
|
|
|
29
33
|
' --model <registryKey> Override the model from settings.json at startup',
|
|
30
34
|
' Format: provider:modelId (e.g. inception:mercury-2)',
|
|
31
35
|
' If provider:modelId format is used, --provider is inferred',
|
|
36
|
+
' --daemon-home=<path> Override daemon home (precedence: flag > GOODVIBES_DAEMON_HOME env > ~/.goodvibes/daemon)',
|
|
37
|
+
' --working-dir=<path> Override working directory (precedence: flag > GOODVIBES_WORKING_DIR env > <cwd>)',
|
|
32
38
|
' --help, -h Show this help message',
|
|
33
39
|
].join('\n'));
|
|
34
40
|
process.exit(0);
|
|
@@ -41,8 +47,12 @@ export function parseCliFlags(argv: readonly string[], binary = 'goodvibes'): Cl
|
|
|
41
47
|
if (typeof model === 'string' && model.includes(':') && provider === undefined) {
|
|
42
48
|
provider = model.split(':')[0];
|
|
43
49
|
}
|
|
50
|
+
} else if (arg.startsWith('--daemon-home=')) {
|
|
51
|
+
daemonHome = arg.slice('--daemon-home='.length);
|
|
52
|
+
} else if (arg.startsWith('--working-dir=')) {
|
|
53
|
+
workingDir = arg.slice('--working-dir='.length);
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
return { provider, model };
|
|
57
|
+
return { provider, model, daemonHome, workingDir };
|
|
48
58
|
}
|
package/src/daemon/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { homedir, networkInterfaces } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
|
|
4
5
|
import { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
|
|
@@ -16,6 +17,11 @@ import {
|
|
|
16
17
|
formatConnectionBlock,
|
|
17
18
|
} from '@pellux/goodvibes-sdk/platform/pairing/index';
|
|
18
19
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
|
|
20
|
+
import {
|
|
21
|
+
scan,
|
|
22
|
+
loadPersistedProviders,
|
|
23
|
+
persistProviders,
|
|
24
|
+
} from '@pellux/goodvibes-sdk/platform/discovery/index';
|
|
19
25
|
|
|
20
26
|
import { parseCliFlags } from '../cli-flags.ts';
|
|
21
27
|
type DaemonCliOwnership = {
|
|
@@ -58,8 +64,8 @@ function readBootstrapPassword(credentialPath: string): string | undefined {
|
|
|
58
64
|
|
|
59
65
|
function resolveDaemonCliOwnership(): DaemonCliOwnership {
|
|
60
66
|
return {
|
|
61
|
-
workingDirectory: process.cwd(),
|
|
62
|
-
homeDirectory: homedir(),
|
|
67
|
+
workingDirectory: process.env['GOODVIBES_WORKING_DIR'] ?? process.cwd(),
|
|
68
|
+
homeDirectory: process.env['GOODVIBES_DAEMON_HOME'] ?? homedir(),
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -72,12 +78,23 @@ function readDaemonCliTokens(env: NodeJS.ProcessEnv): DaemonCliTokens {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
async function main(): Promise<void> {
|
|
81
|
+
// Parse CLI flags first so --daemon-home and --working-dir env vars are set
|
|
82
|
+
// before resolveDaemonCliOwnership() reads them.
|
|
83
|
+
const cliFlags = parseCliFlags(process.argv.slice(2), 'goodvibes-daemon');
|
|
84
|
+
if (cliFlags.daemonHome !== undefined) {
|
|
85
|
+
process.env['GOODVIBES_DAEMON_HOME'] = cliFlags.daemonHome;
|
|
86
|
+
logger.info('daemon: --daemon-home flag applied', { daemonHome: cliFlags.daemonHome });
|
|
87
|
+
}
|
|
88
|
+
if (cliFlags.workingDir !== undefined) {
|
|
89
|
+
process.env['GOODVIBES_WORKING_DIR'] = cliFlags.workingDir;
|
|
90
|
+
logger.info('daemon: --working-dir flag applied', { workingDir: cliFlags.workingDir });
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
const { workingDirectory: workingDir, homeDirectory } = resolveDaemonCliOwnership();
|
|
76
94
|
const config = new ConfigManager({ workingDir, homeDir: homeDirectory, surfaceRoot: 'tui' });
|
|
77
95
|
new GlobalNetworkTransportInstaller().install(config);
|
|
78
96
|
|
|
79
|
-
// Apply CLI flags — override settings.json before the provider registry is constructed
|
|
80
|
-
const cliFlags = parseCliFlags(process.argv.slice(2), 'goodvibes-daemon');
|
|
97
|
+
// Apply remaining CLI flags — override settings.json before the provider registry is constructed
|
|
81
98
|
if (cliFlags.provider !== undefined) {
|
|
82
99
|
config.set('provider.provider', cliFlags.provider);
|
|
83
100
|
logger.info('daemon: --provider flag applied', { provider: cliFlags.provider });
|
|
@@ -97,6 +114,29 @@ async function main(): Promise<void> {
|
|
|
97
114
|
homeDirectory,
|
|
98
115
|
});
|
|
99
116
|
|
|
117
|
+
// F2: Load persisted providers from disk so the provider registry is pre-populated
|
|
118
|
+
// on standalone daemon startup (same machinery the TUI uses after /scan).
|
|
119
|
+
const discoveryRoots = { homeDirectory, surfaceRoot: 'tui' };
|
|
120
|
+
const persistedProviders = loadPersistedProviders(discoveryRoots);
|
|
121
|
+
if (persistedProviders.length > 0) {
|
|
122
|
+
runtimeServices.providerRegistry.registerDiscoveredProviders(persistedProviders);
|
|
123
|
+
logger.info('daemon: loaded persisted providers', { count: persistedProviders.length });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// F2: Run a background LAN scan (non-blocking). Discovered servers are registered
|
|
127
|
+
// and persisted so subsequent daemon restarts benefit from the warm cache.
|
|
128
|
+
void scan().then((result) => {
|
|
129
|
+
if (result.servers.length > 0) {
|
|
130
|
+
runtimeServices.providerRegistry.registerDiscoveredProviders(result.servers);
|
|
131
|
+
persistProviders(discoveryRoots, result.servers);
|
|
132
|
+
logger.info('daemon: LAN scan complete', { found: result.servers.length });
|
|
133
|
+
} else {
|
|
134
|
+
logger.info('daemon: LAN scan found no servers');
|
|
135
|
+
}
|
|
136
|
+
}).catch((err: unknown) => {
|
|
137
|
+
logger.warn('daemon: LAN scan failed', { error: summarizeError(err) });
|
|
138
|
+
});
|
|
139
|
+
|
|
100
140
|
const userAuth = runtimeServices.localUserAuthManager;
|
|
101
141
|
const daemon = new DaemonServer({ runtimeBus, userAuth, runtimeServices });
|
|
102
142
|
const listener = new HttpListener({
|
|
@@ -107,7 +147,7 @@ async function main(): Promise<void> {
|
|
|
107
147
|
const { daemonToken, httpToken } = readDaemonCliTokens(process.env);
|
|
108
148
|
|
|
109
149
|
// If no explicit daemon token is set, use the companion token so mobile apps can connect.
|
|
110
|
-
const companionTokenRecord = getOrCreateCompanionToken('tui');
|
|
150
|
+
const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir: join(homeDirectory, '.goodvibes', 'daemon') });
|
|
111
151
|
const effectiveDaemonToken = daemonToken ?? companionTokenRecord.token;
|
|
112
152
|
const effectiveHttpToken = httpToken ?? effectiveDaemonToken;
|
|
113
153
|
|
|
@@ -49,7 +49,9 @@ export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuilt
|
|
|
49
49
|
category: 'session',
|
|
50
50
|
description: 'QR code for companion app pairing — scan to connect a mobile or desktop companion',
|
|
51
51
|
factory: () => {
|
|
52
|
-
|
|
52
|
+
if (!deps.daemonHomeDir) throw new Error('daemonHomeDir must be provided to the session panel factory via BuiltinPanelDeps');
|
|
53
|
+
const daemonHomeDir = deps.daemonHomeDir;
|
|
54
|
+
const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
53
55
|
const daemonPort = deps.configManager.get('controlPlane.port');
|
|
54
56
|
const daemonHost = String(process.env['GOODVIBES_DAEMON_HOST'] ?? getLocalNetworkIp());
|
|
55
57
|
const daemonUrl = `http://${daemonHost}:${daemonPort}`;
|
|
@@ -61,7 +63,7 @@ export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuilt
|
|
|
61
63
|
surface: 'tui',
|
|
62
64
|
});
|
|
63
65
|
const regenerate = (): typeof connectionInfo => {
|
|
64
|
-
const newRecord = regenerateCompanionToken('tui');
|
|
66
|
+
const newRecord = regenerateCompanionToken('tui', { daemonHomeDir });
|
|
65
67
|
return buildCompanionConnectionInfo({
|
|
66
68
|
daemonUrl,
|
|
67
69
|
token: newRecord.token,
|
|
@@ -78,6 +78,11 @@ export interface BuiltinPanelDeps {
|
|
|
78
78
|
worktreeRegistry: WorktreeRegistry;
|
|
79
79
|
/** Shared sandbox session registry for sandbox surfaces and tools. */
|
|
80
80
|
sandboxSessionRegistry: SandboxSessionRegistry;
|
|
81
|
+
/**
|
|
82
|
+
* Resolved daemon home directory (e.g. `~/.goodvibes/daemon`) — owned by the composition root
|
|
83
|
+
* and passed explicitly so panel factories do not discover cwd/home implicitly.
|
|
84
|
+
*/
|
|
85
|
+
daemonHomeDir?: string;
|
|
81
86
|
/** Session memory store for context and token budget panels. */
|
|
82
87
|
sessionMemoryStore?: SessionMemoryStore;
|
|
83
88
|
/** Execution plan manager for plan dashboard panels. */
|
|
@@ -39,15 +39,18 @@ export function renderQrMatrix(
|
|
|
39
39
|
|
|
40
40
|
const lines: Line[] = [];
|
|
41
41
|
|
|
42
|
-
// Prepend a
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
42
|
+
// Prepend a half-height top quiet band: the BOTTOM half of this terminal row
|
|
43
|
+
// is white (QR bg) flush against the QR's first module row below; the TOP half
|
|
44
|
+
// is the terminal's default background (chrome). Using '▄' (LOWER HALF BLOCK)
|
|
45
|
+
// with fg = QR bg and no bg override achieves the half-height effect.
|
|
46
|
+
// Combined with the leftPad=1 on the horizontal axis, this keeps the
|
|
47
|
+
// finder-pattern square margin consistent on both axes without stealing a
|
|
48
|
+
// full row of vertical space.
|
|
46
49
|
{
|
|
47
50
|
const topBand = createEmptyLine(width);
|
|
48
51
|
const endCol = Math.min(leftPad + cols + 1, width);
|
|
49
52
|
for (let col = 0; col < endCol; col++) {
|
|
50
|
-
topBand[col] = createStyledCell('
|
|
53
|
+
topBand[col] = createStyledCell('▄', { fg: bg });
|
|
51
54
|
}
|
|
52
55
|
lines.push(topBand);
|
|
53
56
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import type { ConversationManager } from '../core/conversation';
|
|
2
3
|
import type { Orchestrator } from '../core/orchestrator';
|
|
3
4
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
|
|
@@ -130,6 +131,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
130
131
|
hookActivityTracker: services.hookActivityTracker,
|
|
131
132
|
hookWorkbench: services.hookWorkbench,
|
|
132
133
|
mcpRegistry: services.mcpRegistry,
|
|
134
|
+
daemonHomeDir: join(services.homeDirectory, '.goodvibes', 'daemon'),
|
|
133
135
|
});
|
|
134
136
|
services.panelManager.prewarmRegistered();
|
|
135
137
|
|
package/src/runtime/services.ts
CHANGED
|
@@ -166,6 +166,13 @@ export interface RuntimeServices {
|
|
|
166
166
|
readonly modeManager: ModeManager;
|
|
167
167
|
readonly fileUndoManager: FileUndoManager;
|
|
168
168
|
readonly integrationHelpers: IntegrationHelperService;
|
|
169
|
+
/**
|
|
170
|
+
* Re-root path-bound stores (MemoryStore, ProjectIndex) to a new working directory.
|
|
171
|
+
* Called by WorkspaceSwapManager after the new directory has been verified.
|
|
172
|
+
* Stores that require a process restart emit a warn-level log; they continue serving
|
|
173
|
+
* the old path until the daemon restarts with the new --working-dir.
|
|
174
|
+
*/
|
|
175
|
+
rerootStores(newWorkingDir: string): Promise<void>;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeServices {
|
|
@@ -544,5 +551,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
544
551
|
modeManager,
|
|
545
552
|
fileUndoManager,
|
|
546
553
|
integrationHelpers,
|
|
554
|
+
async rerootStores(newWorkingDir: string): Promise<void> {
|
|
555
|
+
const newMemoryDbPath = join(newWorkingDir, '.goodvibes', 'tui', 'memory.sqlite');
|
|
556
|
+
await memoryStore.reroot(newMemoryDbPath);
|
|
557
|
+
await projectIndex.reroot(newWorkingDir);
|
|
558
|
+
},
|
|
547
559
|
};
|
|
548
560
|
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.12';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|