@pellux/goodvibes-tui 0.19.10 → 0.19.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.10",
3
+ "version": "0.19.13",
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 build src/daemon/cli.ts --compile --target=bun-linux-x64 --outfile dist/goodvibes-daemon-linux-x64",
36
- "build:daemon:linux-arm64": "bun build src/daemon/cli.ts --compile --target=bun-linux-arm64 --outfile dist/goodvibes-daemon-linux-arm64",
37
- "build:daemon:macos-x64": "bun build src/daemon/cli.ts --compile --target=bun-darwin-x64 --outfile dist/goodvibes-daemon-macos-x64",
38
- "build:daemon:macos-arm64": "bun build src/daemon/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/goodvibes-daemon-macos-arm64",
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.16",
93
+ "@pellux/goodvibes-sdk": "0.21.25",
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
- const tokenRecord = getOrCreateCompanionToken('tui');
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 single-row top quiet band of bg so the QR's first module row
43
- // does not butt up against whatever chrome precedes it. Combined with the
44
- // leftPad=1 on the horizontal axis, this keeps the finder-pattern square
45
- // margin consistent on both axes.
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(' ', { fg, bg });
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
 
@@ -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.10';
9
+ let _version = '0.19.13';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;