@pellux/goodvibes-tui 0.19.48 → 0.19.50

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.50] — 2026-04-28
8
+
9
+ ### Changed
10
+ - Updated `@pellux/goodvibes-sdk` to `0.26.5`.
11
+ - TUI external-service inspection now uses SDK `daemonStatus` and `httpListenerStatus` startup modes to distinguish embedded services, verified external daemons, blocked ports, disabled services, and unavailable services.
12
+
13
+ ### Fixed
14
+ - Treats a verified external GoodVibes daemon on the configured host/port as active instead of reporting daemon activation failure during onboarding.
15
+ - Reports SDK-provided blocked/unavailable startup reasons in onboarding runtime verification warnings.
16
+
17
+ ---
18
+
19
+ ## [0.19.49] — 2026-04-28
20
+
21
+ ### Changed
22
+ - Updated `@pellux/goodvibes-sdk` to `0.26.4` for SDK-owned multipart/raw artifact uploads across artifact storage, knowledge ingest, and Home Assistant Home Graph ingest.
23
+ - Exposed the SDK `storage.artifacts.maxBytes` setting through the normal storage configuration surface.
24
+ - Regenerated foundation operator contract artifacts from the SDK 0.26.4 contract.
25
+
26
+ ### Fixed
27
+ - Picked up SDK fixes for large multipart upload cap handling and oversized-upload rejection behavior.
28
+ - Removed the temporary TUI upload-stream normalization shim after SDK 0.26.4 fixed Bun request-reader cleanup failures in raw artifact uploads.
29
+
30
+ ---
31
+
7
32
  ## [0.19.48] — 2026-04-28
8
33
 
9
34
  ### Changed
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.19.48-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.50-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
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.26.1"
6
+ "version": "0.26.5"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
@@ -2319,7 +2319,7 @@
2319
2319
  {
2320
2320
  "id": "artifacts.create",
2321
2321
  "title": "Create Artifact",
2322
- "description": "Store a file or attachment artifact for later delivery or analysis.",
2322
+ "description": "Store a file or attachment artifact for later delivery, analysis, or knowledge ingest. JSON supports small inline bodies and daemon-local path/URI references; HTTP callers can also send multipart/form-data or a raw binary body for large uploads.",
2323
2323
  "category": "artifacts",
2324
2324
  "source": "builtin",
2325
2325
  "access": "authenticated",
@@ -2486,7 +2486,16 @@
2486
2486
  ],
2487
2487
  "additionalProperties": false
2488
2488
  },
2489
- "invokable": true
2489
+ "invokable": true,
2490
+ "metadata": {
2491
+ "uploadModes": [
2492
+ "json-inline",
2493
+ "json-path-or-uri",
2494
+ "multipart-file",
2495
+ "raw-body"
2496
+ ],
2497
+ "largeUploadConfigKey": "storage.artifacts.maxBytes"
2498
+ }
2490
2499
  },
2491
2500
  {
2492
2501
  "id": "artifacts.get",
@@ -26572,7 +26581,7 @@
26572
26581
  {
26573
26582
  "id": "homeassistant.homeGraph.ingestHomeGraphArtifact",
26574
26583
  "title": "Ingest Home Graph Artifact",
26575
- "description": "Index an artifact, document, receipt, warranty, manual, or photo into a Home Graph space.",
26584
+ "description": "Index an existing artifact reference, JSON path/URI reference, multipart file upload, or raw binary upload as a Home Graph document, receipt, warranty, manual, or photo.",
26576
26585
  "category": "knowledge",
26577
26586
  "source": "builtin",
26578
26587
  "access": "admin",
@@ -27020,7 +27029,16 @@
27020
27029
  ],
27021
27030
  "additionalProperties": true
27022
27031
  },
27023
- "invokable": true
27032
+ "invokable": true,
27033
+ "metadata": {
27034
+ "uploadModes": [
27035
+ "json-artifact-reference",
27036
+ "json-path-or-uri",
27037
+ "multipart-file",
27038
+ "raw-body"
27039
+ ],
27040
+ "largeUploadConfigKey": "storage.artifacts.maxBytes"
27041
+ }
27024
27042
  },
27025
27043
  {
27026
27044
  "id": "homeassistant.homeGraph.ingestHomeGraphNote",
@@ -31622,7 +31640,7 @@
31622
31640
  {
31623
31641
  "id": "knowledge.ingest.artifact",
31624
31642
  "title": "Ingest Artifact Into Knowledge",
31625
- "description": "Snapshot a local path, remote URI, or existing artifact into the structured knowledge store and run structured extraction.",
31643
+ "description": "Snapshot an existing artifact, daemon-local path, remote URI, multipart file upload, or raw binary upload into the structured knowledge store and run structured extraction.",
31626
31644
  "category": "knowledge",
31627
31645
  "source": "builtin",
31628
31646
  "access": "admin",
@@ -31925,7 +31943,16 @@
31925
31943
  ],
31926
31944
  "additionalProperties": true
31927
31945
  },
31928
- "invokable": true
31946
+ "invokable": true,
31947
+ "metadata": {
31948
+ "uploadModes": [
31949
+ "json-artifact-reference",
31950
+ "json-path-or-uri",
31951
+ "multipart-file",
31952
+ "raw-body"
31953
+ ],
31954
+ "largeUploadConfigKey": "storage.artifacts.maxBytes"
31955
+ }
31929
31956
  },
31930
31957
  {
31931
31958
  "id": "knowledge.ingest.bookmarks",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.48",
3
+ "version": "0.19.50",
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",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "0.26.1",
94
+ "@pellux/goodvibes-sdk": "0.26.5",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -9,6 +9,15 @@ import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runt
9
9
  import type { ModelPickerTarget } from './model-picker.ts';
10
10
  import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
11
11
  import type { InputHandler } from './handler.ts';
12
+ import {
13
+ formatRuntimeActiveSuccessMessage,
14
+ getRuntimeEndpointStatus,
15
+ isRuntimeEndpointActive,
16
+ isRuntimeEndpointOccupyingConfiguredPort,
17
+ runtimePortDiagnostic,
18
+ type OnboardingExternalServiceState,
19
+ type OnboardingRuntimeEndpoint,
20
+ } from './onboarding/onboarding-runtime-status.ts';
12
21
 
13
22
  export interface OnboardingRuntimePosture {
14
23
  readonly serviceEnabled: boolean;
@@ -20,15 +29,6 @@ export interface OnboardingRuntimePosture {
20
29
  readonly remoteExposure: boolean;
21
30
  }
22
31
 
23
- interface OnboardingExternalServiceState {
24
- readonly daemonRunning?: boolean;
25
- readonly daemonPortInUse?: boolean;
26
- readonly httpListenerRunning?: boolean;
27
- readonly httpListenerPortInUse?: boolean;
28
- }
29
-
30
- type OnboardingRuntimeEndpoint = 'daemon' | 'httpListener';
31
-
32
32
  function extractAuthorizationCode(input: string): string | null {
33
33
  const trimmed = input.trim();
34
34
  if (!trimmed) return null;
@@ -105,16 +105,6 @@ function getRuntimeEndpointBinding(
105
105
  };
106
106
  }
107
107
 
108
- function runtimePortDiagnostic(
109
- binding: { readonly label: string; readonly host: string; readonly port: number },
110
- portInUse: boolean | undefined,
111
- ): string {
112
- if (portInUse) {
113
- return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
114
- }
115
- return `No process is listening on ${binding.host}:${binding.port} after restart.`;
116
- }
117
-
118
108
  function formatRuntimeActiveFailureMessage(
119
109
  handler: InputHandler,
120
110
  request: OnboardingApplyRequest,
@@ -123,20 +113,24 @@ function formatRuntimeActiveFailureMessage(
123
113
  ): string {
124
114
  const binding = getRuntimeEndpointBinding(handler, request, endpoint);
125
115
  const portInUse = endpoint === 'daemon' ? state?.daemonPortInUse : state?.httpListenerPortInUse;
116
+ const status = getRuntimeEndpointStatus(state, endpoint);
126
117
  const impact = endpoint === 'daemon'
127
118
  ? 'browser, LAN, and service-backed GoodVibes surfaces may be unavailable until the daemon is running there.'
128
119
  : 'incoming webhooks and event surfaces will not receive traffic until the listener is running there.';
129
- return `${binding.label} is enabled for ${binding.host}:${binding.port}, but onboarding could not confirm it is running in this TUI instance after restart. ${runtimePortDiagnostic(binding, portInUse)} Settings were saved; ${impact}`;
120
+ return `${binding.label} is enabled for ${binding.host}:${binding.port}, but onboarding could not confirm an embedded or verified external service after restart. ${runtimePortDiagnostic(binding, portInUse, status)} Settings were saved; ${impact}`;
130
121
  }
131
122
 
132
123
  function formatRuntimeStoppedFailureMessage(
133
124
  handler: InputHandler,
134
125
  request: OnboardingApplyRequest,
135
126
  endpoint: OnboardingRuntimeEndpoint,
127
+ state?: OnboardingExternalServiceState,
136
128
  ): string {
137
129
  const binding = getRuntimeEndpointBinding(handler, request, endpoint);
130
+ const status = getRuntimeEndpointStatus(state, endpoint);
138
131
  const disabledSurface = endpoint === 'daemon' ? 'server-backed surfaces' : 'incoming event surfaces';
139
- return `${binding.label} was disabled for ${disabledSurface}, but ${binding.host}:${binding.port} is still occupied. Settings were saved; another GoodVibes process or external service may still be running on that port.`;
132
+ const statusDetail = status ? ` ${runtimePortDiagnostic(binding, undefined, status)}` : '';
133
+ return `${binding.label} was disabled for ${disabledSurface}, but ${binding.host}:${binding.port} is still occupied. Settings were saved; another GoodVibes process or external service may still be running on that port.${statusDetail}`;
140
134
  }
141
135
 
142
136
  function showOnboardingApplyFeedbackForHandler(handler: InputHandler, feedback: OnboardingWizardApplyFeedback): void {
@@ -611,16 +605,14 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
611
605
  }
612
606
 
613
607
  const currentState = externalServices.inspect();
614
- const hasLiveExternalServices = currentState.daemonRunning === true
615
- || currentState.daemonPortInUse === true
616
- || currentState.httpListenerRunning === true
617
- || currentState.httpListenerPortInUse === true;
608
+ const hasLiveExternalServices = isRuntimeEndpointOccupyingConfiguredPort(currentState, 'daemon')
609
+ || isRuntimeEndpointOccupyingConfiguredPort(currentState, 'httpListener');
618
610
  if (!posture.serverBacked && !hasLiveExternalServices) return [];
619
611
 
620
612
  try {
621
613
  const state = await externalServices.restart();
622
614
  const failures: OnboardingVerificationItem[] = [];
623
- if (posture.expectedDaemon && !state.daemonRunning) {
615
+ if (posture.expectedDaemon && !isRuntimeEndpointActive(state, 'daemon')) {
624
616
  failures.push({
625
617
  id: 'runtime:daemon-active',
626
618
  status: 'fail',
@@ -628,15 +620,15 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
628
620
  target: 'service',
629
621
  });
630
622
  }
631
- if (!posture.expectedDaemon && (state.daemonRunning || state.daemonPortInUse)) {
623
+ if (!posture.expectedDaemon && isRuntimeEndpointOccupyingConfiguredPort(state, 'daemon')) {
632
624
  failures.push({
633
625
  id: 'runtime:daemon-stopped',
634
626
  status: 'fail',
635
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
627
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', state),
636
628
  target: 'service',
637
629
  });
638
630
  }
639
- if (posture.expectedHttpListener && !state.httpListenerRunning) {
631
+ if (posture.expectedHttpListener && !isRuntimeEndpointActive(state, 'httpListener')) {
640
632
  failures.push({
641
633
  id: 'runtime:http-listener-active',
642
634
  status: 'fail',
@@ -644,11 +636,11 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
644
636
  target: 'service',
645
637
  });
646
638
  }
647
- if (!posture.expectedHttpListener && (state.httpListenerRunning || state.httpListenerPortInUse)) {
639
+ if (!posture.expectedHttpListener && isRuntimeEndpointOccupyingConfiguredPort(state, 'httpListener')) {
648
640
  failures.push({
649
641
  id: 'runtime:http-listener-stopped',
650
642
  status: 'fail',
651
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
643
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', state),
652
644
  target: 'service',
653
645
  });
654
646
  }
@@ -685,19 +677,19 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
685
677
  }
686
678
 
687
679
  const stoppedItems: OnboardingVerificationItem[] = [];
688
- if (externalState?.daemonRunning || externalState?.daemonPortInUse) {
680
+ if (isRuntimeEndpointOccupyingConfiguredPort(externalState, 'daemon')) {
689
681
  stoppedItems.push({
690
682
  id: 'runtime:daemon-stopped',
691
683
  status: 'fail',
692
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
684
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', externalState),
693
685
  target: 'service',
694
686
  });
695
687
  }
696
- if (externalState?.httpListenerRunning || externalState?.httpListenerPortInUse) {
688
+ if (isRuntimeEndpointOccupyingConfiguredPort(externalState, 'httpListener')) {
697
689
  stoppedItems.push({
698
690
  id: 'runtime:http-listener-stopped',
699
691
  status: 'fail',
700
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
692
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', externalState),
701
693
  target: 'service',
702
694
  });
703
695
  }
@@ -744,38 +736,40 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
744
736
  }
745
737
 
746
738
  if (posture.expectedDaemon) {
739
+ const daemonActive = isRuntimeEndpointActive(externalState, 'daemon');
747
740
  items.push({
748
741
  id: 'runtime:daemon-active',
749
- status: externalState?.daemonRunning ? 'pass' : 'fail',
750
- message: externalState?.daemonRunning
751
- ? 'The GoodVibes daemon is running with the applied onboarding settings.'
742
+ status: daemonActive ? 'pass' : 'fail',
743
+ message: daemonActive
744
+ ? formatRuntimeActiveSuccessMessage('daemon', externalState)
752
745
  : formatRuntimeActiveFailureMessage(handler, request, 'daemon', externalState),
753
746
  target: 'service',
754
747
  });
755
748
  }
756
- if (!posture.expectedDaemon && (externalState?.daemonRunning || externalState?.daemonPortInUse)) {
749
+ if (!posture.expectedDaemon && isRuntimeEndpointOccupyingConfiguredPort(externalState, 'daemon')) {
757
750
  items.push({
758
751
  id: 'runtime:daemon-stopped',
759
752
  status: 'fail',
760
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
753
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', externalState),
761
754
  target: 'service',
762
755
  });
763
756
  }
764
757
  if (posture.expectedHttpListener) {
758
+ const httpListenerActive = isRuntimeEndpointActive(externalState, 'httpListener');
765
759
  items.push({
766
760
  id: 'runtime:http-listener-active',
767
- status: externalState?.httpListenerRunning ? 'pass' : 'fail',
768
- message: externalState?.httpListenerRunning
769
- ? 'The HTTP listener is running with the applied onboarding settings.'
761
+ status: httpListenerActive ? 'pass' : 'fail',
762
+ message: httpListenerActive
763
+ ? formatRuntimeActiveSuccessMessage('httpListener', externalState)
770
764
  : formatRuntimeActiveFailureMessage(handler, request, 'httpListener', externalState),
771
765
  target: 'service',
772
766
  });
773
767
  }
774
- if (!posture.expectedHttpListener && (externalState?.httpListenerRunning || externalState?.httpListenerPortInUse)) {
768
+ if (!posture.expectedHttpListener && isRuntimeEndpointOccupyingConfiguredPort(externalState, 'httpListener')) {
775
769
  items.push({
776
770
  id: 'runtime:http-listener-stopped',
777
771
  status: 'fail',
778
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
772
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', externalState),
779
773
  target: 'service',
780
774
  });
781
775
  }
@@ -0,0 +1,87 @@
1
+ import type { HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
2
+
3
+ export interface OnboardingExternalServiceState {
4
+ readonly daemonRunning?: boolean;
5
+ readonly daemonPortInUse?: boolean;
6
+ readonly httpListenerRunning?: boolean;
7
+ readonly httpListenerPortInUse?: boolean;
8
+ readonly daemonStatus?: HostServiceStatus;
9
+ readonly httpListenerStatus?: HostServiceStatus;
10
+ }
11
+
12
+ export type OnboardingRuntimeEndpoint = 'daemon' | 'httpListener';
13
+
14
+ export function runtimePortDiagnostic(
15
+ binding: { readonly label: string; readonly host: string; readonly port: number },
16
+ portInUse: boolean | undefined,
17
+ status?: HostServiceStatus,
18
+ ): string {
19
+ if (status) {
20
+ const reason = status.reason ? ` ${status.reason}` : '';
21
+ if (status.mode === 'blocked') {
22
+ return `The configured endpoint ${status.baseUrl} is occupied but was not usable by this TUI instance.${reason}`;
23
+ }
24
+ if (status.mode === 'disabled') {
25
+ return `The configured endpoint ${status.baseUrl} is disabled in the runtime service configuration.${reason}`;
26
+ }
27
+ if (status.mode === 'unavailable') {
28
+ return `The configured endpoint ${status.baseUrl} is unavailable after startup or restart.${reason}`;
29
+ }
30
+ if (status.mode === 'external') {
31
+ const version = status.version ? ` version ${status.version}` : '';
32
+ return `An existing GoodVibes service was verified at ${status.baseUrl}${version}.`;
33
+ }
34
+ return `An embedded GoodVibes service is running at ${status.baseUrl}.`;
35
+ }
36
+ if (portInUse) {
37
+ return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
38
+ }
39
+ return `No process is listening on ${binding.host}:${binding.port} after restart.`;
40
+ }
41
+
42
+ export function getRuntimeEndpointStatus(
43
+ state: OnboardingExternalServiceState | undefined,
44
+ endpoint: OnboardingRuntimeEndpoint,
45
+ ): HostServiceStatus | undefined {
46
+ return endpoint === 'daemon' ? state?.daemonStatus : state?.httpListenerStatus;
47
+ }
48
+
49
+ export function isRuntimeEndpointActive(
50
+ state: OnboardingExternalServiceState | undefined,
51
+ endpoint: OnboardingRuntimeEndpoint,
52
+ ): boolean {
53
+ const status = getRuntimeEndpointStatus(state, endpoint);
54
+ if (status) return status.mode === 'embedded' || status.mode === 'external';
55
+ return endpoint === 'daemon'
56
+ ? state?.daemonRunning === true
57
+ : state?.httpListenerRunning === true;
58
+ }
59
+
60
+ export function isRuntimeEndpointOccupyingConfiguredPort(
61
+ state: OnboardingExternalServiceState | undefined,
62
+ endpoint: OnboardingRuntimeEndpoint,
63
+ ): boolean {
64
+ const status = getRuntimeEndpointStatus(state, endpoint);
65
+ if (status) return status.mode === 'embedded' || status.mode === 'external' || status.mode === 'blocked';
66
+ return endpoint === 'daemon'
67
+ ? state?.daemonRunning === true || state?.daemonPortInUse === true
68
+ : state?.httpListenerRunning === true || state?.httpListenerPortInUse === true;
69
+ }
70
+
71
+ export function formatRuntimeActiveSuccessMessage(
72
+ endpoint: OnboardingRuntimeEndpoint,
73
+ state: OnboardingExternalServiceState | undefined,
74
+ ): string {
75
+ const status = getRuntimeEndpointStatus(state, endpoint);
76
+ const label = endpoint === 'daemon' ? 'GoodVibes daemon' : 'HTTP listener';
77
+ if (status?.mode === 'external') {
78
+ const version = status.version ? ` version ${status.version}` : '';
79
+ return `${label} is already running as a verified external GoodVibes service at ${status.baseUrl}${version}.`;
80
+ }
81
+ if (status?.mode === 'embedded') {
82
+ return `${label} is running as an embedded service at ${status.baseUrl}.`;
83
+ }
84
+ return endpoint === 'daemon'
85
+ ? 'The GoodVibes daemon is running with the applied onboarding settings.'
86
+ : 'The HTTP listener is running with the applied onboarding settings.';
87
+ }
@@ -10,7 +10,6 @@
10
10
  * - lifecycle.ts: save/shutdown helpers
11
11
  */
12
12
  import { join } from 'node:path';
13
- import net from 'node:net';
14
13
  import { Orchestrator, type OrchestratorUserInputOptions } from '../core/orchestrator.ts';
15
14
  import { AcpManager } from '@pellux/goodvibes-sdk/platform/acp/manager';
16
15
  import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers/tier-prompts';
@@ -36,7 +35,7 @@ import {
36
35
  } from '@pellux/goodvibes-sdk/platform/runtime/session-persistence';
37
36
  import { startBackgroundProviderRegistration } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-background';
38
37
  import { restoreSavedModel } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-helpers';
39
- import { startExternalServices, type ExternalServicesHandle } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
38
+ import { startExternalServices, type ExternalServicesHandle, type HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
40
39
  import { getOrCreateCompanionToken, pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing/companion-token';
41
40
  import { workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
42
41
  import type { UiRuntimeServices } from './ui-services.ts';
@@ -295,80 +294,45 @@ export async function bootstrapRuntime(
295
294
 
296
295
  const deferredStartup = createDeferredStartupCoordinator();
297
296
 
298
- interface ExternalServiceBindingSnapshot {
299
- readonly daemon: {
300
- readonly host: string;
301
- readonly port: number;
302
- };
303
- readonly httpListener: {
304
- readonly host: string;
305
- readonly port: number;
306
- };
307
- }
308
-
309
- interface ExternalServicePortState {
310
- readonly daemonPortInUse: boolean;
311
- readonly httpListenerPortInUse: boolean;
312
- }
313
-
314
- const readExternalServiceBindings = (): ExternalServiceBindingSnapshot => ({
315
- daemon: {
316
- host: String(configManager.get('controlPlane.host') ?? '127.0.0.1'),
317
- port: Number(configManager.get('controlPlane.port') ?? 3421),
318
- },
319
- httpListener: {
320
- host: String(configManager.get('httpListener.host') ?? '127.0.0.1'),
321
- port: Number(configManager.get('httpListener.port') ?? 3422),
322
- },
323
- });
324
-
325
- const getProbeHosts = (host: string): readonly string[] => {
297
+ const formatHostServiceBaseUrl = (host: string, port: number): string => {
326
298
  const normalized = host.trim().toLowerCase();
327
- if (normalized === '0.0.0.0') return ['127.0.0.1'];
328
- if (normalized === '::' || normalized === '[::]') return ['::1'];
329
- if (normalized.length === 0) return ['127.0.0.1'];
330
- return [host];
299
+ const probeHost = normalized === '0.0.0.0'
300
+ ? '127.0.0.1'
301
+ : normalized === '::' || normalized === '[::]'
302
+ ? '::1'
303
+ : host;
304
+ const urlHost = probeHost.includes(':') && !probeHost.startsWith('[') ? `[${probeHost}]` : probeHost;
305
+ return `http://${urlHost}:${port}`;
331
306
  };
332
307
 
333
- const isTcpPortInUse = async (host: string, port: number): Promise<boolean> => new Promise((resolve) => {
334
- const socket = new net.Socket();
335
- let settled = false;
336
- const finish = (result: boolean): void => {
337
- if (settled) return;
338
- settled = true;
339
- socket.destroy();
340
- resolve(result);
308
+ const createPendingServiceStatus = (
309
+ service: 'daemon' | 'httpListener',
310
+ ): HostServiceStatus => {
311
+ const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
312
+ const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
313
+ return {
314
+ mode: 'unavailable',
315
+ host,
316
+ port,
317
+ baseUrl: formatHostServiceBaseUrl(host, port),
318
+ reason: 'Background service startup has not completed yet',
341
319
  };
320
+ };
342
321
 
343
- socket.setTimeout(250);
344
- socket.once('connect', () => finish(true));
345
- socket.once('timeout', () => finish(false));
346
- socket.once('error', () => finish(false));
347
- socket.connect(port, host);
348
- });
322
+ const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
349
323
 
350
- const inspectExternalPorts = async (
351
- bindings: readonly ExternalServiceBindingSnapshot[],
352
- ): Promise<ExternalServicePortState> => {
353
- const daemonTargets = new Map<string, { readonly host: string; readonly port: number }>();
354
- const listenerTargets = new Map<string, { readonly host: string; readonly port: number }>();
355
- for (const binding of bindings) {
356
- for (const host of getProbeHosts(binding.daemon.host)) {
357
- daemonTargets.set(`${host}:${binding.daemon.port}`, { host, port: binding.daemon.port });
358
- }
359
- for (const host of getProbeHosts(binding.httpListener.host)) {
360
- listenerTargets.set(`${host}:${binding.httpListener.port}`, { host, port: binding.httpListener.port });
361
- }
362
- }
363
-
364
- const [daemonResults, listenerResults] = await Promise.all([
365
- Promise.all([...daemonTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
366
- Promise.all([...listenerTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
367
- ]);
324
+ const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked';
368
325
 
326
+ const inspectExternalServices = () => {
327
+ const daemonStatus = externalServices.daemonStatus;
328
+ const httpListenerStatus = externalServices.httpListenerStatus;
369
329
  return {
370
- daemonPortInUse: daemonResults.some(Boolean),
371
- httpListenerPortInUse: listenerResults.some(Boolean),
330
+ daemonRunning: hostServiceIsActive(daemonStatus),
331
+ daemonPortInUse: hostServiceIsBlocked(daemonStatus),
332
+ httpListenerRunning: hostServiceIsActive(httpListenerStatus),
333
+ httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
334
+ daemonStatus,
335
+ httpListenerStatus,
372
336
  };
373
337
  };
374
338
 
@@ -389,21 +353,12 @@ export async function bootstrapRuntime(
389
353
  let externalServices: ExternalServicesHandle = {
390
354
  daemonServer: null,
391
355
  httpListener: null,
356
+ daemonStatus: createPendingServiceStatus('daemon'),
357
+ httpListenerStatus: createPendingServiceStatus('httpListener'),
392
358
  listRecentControlPlaneEvents: () => [],
393
359
  async stop(): Promise<void> {},
394
360
  };
395
361
  let externalServicesPromise: Promise<ExternalServicesHandle> | null = null;
396
- let externalServiceBindings = readExternalServiceBindings();
397
- let externalServicePortState: ExternalServicePortState = {
398
- daemonPortInUse: false,
399
- httpListenerPortInUse: false,
400
- };
401
- const inspectExternalServices = () => ({
402
- daemonRunning: externalServices.daemonServer !== null,
403
- daemonPortInUse: externalServicePortState.daemonPortInUse,
404
- httpListenerRunning: externalServices.httpListener !== null,
405
- httpListenerPortInUse: externalServicePortState.httpListenerPortInUse,
406
- });
407
362
  const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
408
363
  externalServices: NonNullable<typeof uiServices.platform.externalServices>;
409
364
  };
@@ -419,8 +374,6 @@ export async function bootstrapRuntime(
419
374
  }
420
375
  await waitForConfigDrivenRestarts(externalServices);
421
376
  await externalServices.stop();
422
- const previousBindings = externalServiceBindings;
423
- externalServiceBindings = readExternalServiceBindings();
424
377
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
425
378
  const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
426
379
  externalServicesPromise = startExternalServices(
@@ -432,7 +385,6 @@ export async function bootstrapRuntime(
432
385
  );
433
386
  externalServices = await externalServicesPromise;
434
387
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
435
- externalServicePortState = await inspectExternalPorts([previousBindings, externalServiceBindings]);
436
388
  requestRender();
437
389
  return inspectExternalServices();
438
390
  },
@@ -487,7 +439,6 @@ export async function bootstrapRuntime(
487
439
  if (prune.failedPaths.length > 0) {
488
440
  logger.warn(`[bootstrap] Failed to prune ${prune.failedPaths.length} stale operator-token file(s) (permission/race): ${prune.failedPaths.join(', ')}`);
489
441
  }
490
- externalServiceBindings = readExternalServiceBindings();
491
442
  externalServicesPromise = startExternalServices(
492
443
  configManager,
493
444
  runtimeBus,
@@ -497,7 +448,6 @@ export async function bootstrapRuntime(
497
448
  );
498
449
  externalServices = await externalServicesPromise;
499
450
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
500
- externalServicePortState = await inspectExternalPorts([externalServiceBindings]);
501
451
  requestRender();
502
452
  },
503
453
  onError: (error) => {
@@ -8,6 +8,7 @@ import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/con
8
8
  import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane/approval-broker';
9
9
  import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane/session-broker';
10
10
  import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
11
+ import type { HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
11
12
  import type { SecretsManager } from '../config/secrets.ts';
12
13
 
13
14
  export interface UiEnvironmentServices {
@@ -61,12 +62,16 @@ export interface UiPlatformServices {
61
62
  readonly daemonPortInUse?: boolean;
62
63
  readonly httpListenerRunning: boolean;
63
64
  readonly httpListenerPortInUse?: boolean;
65
+ readonly daemonStatus?: HostServiceStatus;
66
+ readonly httpListenerStatus?: HostServiceStatus;
64
67
  };
65
68
  restart(): Promise<{
66
69
  readonly daemonRunning: boolean;
67
70
  readonly daemonPortInUse?: boolean;
68
71
  readonly httpListenerRunning: boolean;
69
72
  readonly httpListenerPortInUse?: boolean;
73
+ readonly daemonStatus?: HostServiceStatus;
74
+ readonly httpListenerStatus?: HostServiceStatus;
70
75
  }>;
71
76
  };
72
77
  }
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.48';
9
+ let _version = '0.19.50';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;