@pellux/goodvibes-tui 1.18.0 → 1.19.1
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 +164 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1421 -896
- package/package.json +7 -7
- package/src/cli/endpoints.ts +50 -4
- package/src/cli/help.ts +29 -2
- package/src/cli/management-commands.ts +17 -8
- package/src/cli/service-posture.ts +5 -3
- package/src/cli/status.ts +27 -6
- package/src/cli/surface-command.ts +29 -15
- package/src/core/memory-diagnostics-gateway.ts +99 -0
- package/src/core/memory-status.ts +118 -0
- package/src/core/voice-provision-gateway.ts +202 -0
- package/src/core/voice-provision-status.ts +197 -0
- package/src/daemon/cli.ts +80 -4
- package/src/input/commands/experience-runtime.ts +25 -4
- package/src/input/commands/health-runtime.ts +15 -1
- package/src/input/tts-settings-actions.ts +27 -8
- package/src/input/voice-local-settings.ts +52 -0
- package/src/runtime/code-index-services.ts +15 -1
- package/src/runtime/knowledge-services.ts +94 -0
- package/src/runtime/legacy-daemon-migration.ts +38 -6
- package/src/runtime/legacy-daemon-reconcile.ts +439 -0
- package/src/runtime/memory-governance-services.ts +79 -0
- package/src/runtime/notification-dispatch.ts +28 -0
- package/src/runtime/services.ts +70 -73
- package/src/runtime/voice-setup-services.ts +52 -0
- package/src/version.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
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",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
105
105
|
"@ast-grep/napi": "^0.42.0",
|
|
106
106
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
107
|
-
"@pellux/goodvibes-sdk": "1.
|
|
108
|
-
"@pellux/goodvibes-terminal-shell": "1.
|
|
107
|
+
"@pellux/goodvibes-sdk": "1.10.1",
|
|
108
|
+
"@pellux/goodvibes-terminal-shell": "1.10.1",
|
|
109
109
|
"bash-language-server": "^5.6.0",
|
|
110
110
|
"fuse.js": "^7.1.0",
|
|
111
111
|
"graphql": "^16.13.2",
|
|
@@ -131,10 +131,10 @@
|
|
|
131
131
|
"typescript": "^5.9.3"
|
|
132
132
|
},
|
|
133
133
|
"optionalDependencies": {
|
|
134
|
-
"@pellux/goodvibes-tui-linux-x64": "1.
|
|
135
|
-
"@pellux/goodvibes-tui-linux-arm64": "1.
|
|
136
|
-
"@pellux/goodvibes-tui-darwin-x64": "1.
|
|
137
|
-
"@pellux/goodvibes-tui-darwin-arm64": "1.
|
|
134
|
+
"@pellux/goodvibes-tui-linux-x64": "1.19.1",
|
|
135
|
+
"@pellux/goodvibes-tui-linux-arm64": "1.19.1",
|
|
136
|
+
"@pellux/goodvibes-tui-darwin-x64": "1.19.1",
|
|
137
|
+
"@pellux/goodvibes-tui-darwin-arm64": "1.19.1"
|
|
138
138
|
},
|
|
139
139
|
"trustedDependencies": [],
|
|
140
140
|
"overrides": {
|
package/src/cli/endpoints.ts
CHANGED
|
@@ -36,6 +36,30 @@ export interface RuntimeEndpointBinding {
|
|
|
36
36
|
readonly configuredHost: string;
|
|
37
37
|
readonly host: string;
|
|
38
38
|
readonly port: number;
|
|
39
|
+
/**
|
|
40
|
+
* False when the stored hostMode is not one of the SDK's recognized modes
|
|
41
|
+
* ('local' | 'network' | 'custom'). The SDK's resolveHostBinding is a switch
|
|
42
|
+
* with NO default case: an unrecognized mode yields an undefined binding and
|
|
43
|
+
* the daemon throws in its constructor before ever binding. Displays must
|
|
44
|
+
* therefore never present the host/port below as a definite binding when
|
|
45
|
+
* this is false — the fallback values exist only so display code has
|
|
46
|
+
* something structured to show alongside the warning.
|
|
47
|
+
*/
|
|
48
|
+
readonly recognized: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* THE one display seam for endpoint bindings: every surface that renders a
|
|
53
|
+
* binding as text goes through this, so no surface can present the resolver's
|
|
54
|
+
* loopback fallback as a definite bind for a hostMode the SDK cannot handle
|
|
55
|
+
* (its bind resolver has no default case — the daemon throws before binding).
|
|
56
|
+
* The recognized case renders the familiar `<mode> <host>:<port>`.
|
|
57
|
+
*/
|
|
58
|
+
export function formatRuntimeEndpointBinding(binding: RuntimeEndpointBinding): string {
|
|
59
|
+
if (binding.recognized) {
|
|
60
|
+
return `${binding.hostMode} ${binding.host}:${binding.port}`;
|
|
61
|
+
}
|
|
62
|
+
return `'${binding.hostMode}' — not a recognized host mode (expected local|network|custom); the daemon cannot bind this endpoint until it is corrected`;
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export function hostModeForHostname(hostname: string): RuntimeHostMode {
|
|
@@ -52,12 +76,34 @@ export function resolveRuntimeEndpointBinding(
|
|
|
52
76
|
const keys = RUNTIME_ENDPOINT_CONFIG_KEYS[endpoint];
|
|
53
77
|
const hostMode = String(config.get(keys.hostMode) ?? 'local');
|
|
54
78
|
const configuredHost = String(config.get(keys.host) ?? '127.0.0.1');
|
|
55
|
-
|
|
79
|
+
// Port coercion is anchored PER ENDPOINT to what the SDK actually does with
|
|
80
|
+
// the stored value, so a display never disagrees with the machinery:
|
|
81
|
+
// - controlPlane / httpListener bind via resolveHostBinding, which is fed
|
|
82
|
+
// `Number(raw ?? default)` and applies `customPort || DEFAULT` — a
|
|
83
|
+
// stored 0 or non-numeric value collapses to the endpoint default.
|
|
84
|
+
// - web has NO resolveHostBinding consumer in the SDK at all; its closest
|
|
85
|
+
// machinery (resolveWebPort, which drives tailscale-serve, plus the
|
|
86
|
+
// surface registry/announcements) uses bare `Number(raw ?? default)`
|
|
87
|
+
// with no || collapse — 0 stays 0 and a non-numeric value stays NaN.
|
|
88
|
+
// Collapsing here would make the display assert a port the tailscale/
|
|
89
|
+
// announcement machinery is not actually using. (The SDK's web-port
|
|
90
|
+
// handling is inconsistent-by-absence — a proper resolveHostBinding-style
|
|
91
|
+
// web resolver is an SDK-side fix; until then this mirrors resolveWebPort.)
|
|
92
|
+
const rawPort = Number(config.get(keys.port) ?? RUNTIME_ENDPOINT_DEFAULT_PORTS[endpoint]);
|
|
93
|
+
const port = endpoint === 'web' ? rawPort : (rawPort || RUNTIME_ENDPOINT_DEFAULT_PORTS[endpoint]);
|
|
56
94
|
if (hostMode === 'network') {
|
|
57
|
-
return { hostMode, configuredHost, host: '0.0.0.0', port };
|
|
95
|
+
return { hostMode, configuredHost, host: '0.0.0.0', port, recognized: true };
|
|
58
96
|
}
|
|
59
97
|
if (hostMode === 'custom') {
|
|
60
|
-
return { hostMode, configuredHost, host: configuredHost || '127.0.0.1', port };
|
|
98
|
+
return { hostMode, configuredHost, host: configuredHost || '127.0.0.1', port, recognized: true };
|
|
99
|
+
}
|
|
100
|
+
if (hostMode === 'local') {
|
|
101
|
+
return { hostMode, configuredHost, host: '127.0.0.1', port, recognized: true };
|
|
61
102
|
}
|
|
62
|
-
|
|
103
|
+
// Unrecognized hostMode ('LAN', 'Network', '', a trimmed variant, …): the
|
|
104
|
+
// SDK bind path has NO default case for this — resolveHostBinding returns
|
|
105
|
+
// undefined and the daemon throws before binding. There IS no real binding
|
|
106
|
+
// to display; recognized:false tells callers to warn instead of asserting
|
|
107
|
+
// the loopback fallback below as fact.
|
|
108
|
+
return { hostMode, configuredHost, host: '127.0.0.1', port, recognized: false };
|
|
63
109
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -6,8 +6,13 @@ import { VERSION } from '../version.ts';
|
|
|
6
6
|
function readJsonVersion(path: string): string | null {
|
|
7
7
|
try {
|
|
8
8
|
if (!existsSync(path)) return null;
|
|
9
|
-
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as { version?: unknown };
|
|
10
|
-
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as { name?: unknown; version?: unknown };
|
|
10
|
+
// Only trust OUR package.json — a compiled single-file binary can resolve
|
|
11
|
+
// this path to a different package.json (a bundled dependency's) that
|
|
12
|
+
// reports a placeholder like "0.0.0". Fall through to the baked VERSION in
|
|
13
|
+
// that case rather than rendering a stray version in `--version`/banners.
|
|
14
|
+
if (parsed.name !== '@pellux/goodvibes-tui') return null;
|
|
15
|
+
return typeof parsed.version === 'string' && parsed.version.length > 0 ? parsed.version : null;
|
|
11
16
|
} catch {
|
|
12
17
|
return null;
|
|
13
18
|
}
|
|
@@ -23,6 +28,28 @@ export function renderGoodVibesVersion(binary = 'goodvibes'): string {
|
|
|
23
28
|
return `${binary} ${getPackageVersion()}`;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Honest one-line startup identity for the daemon binary, emitted right as it
|
|
33
|
+
* begins serving — including on a bare (no-arg) systemd launch. It states the
|
|
34
|
+
* RESOLVED version (never a placeholder), the home/host/port it actually bound,
|
|
35
|
+
* and points at the real service-setup command. This replaces the field
|
|
36
|
+
* behavior where a bare launch showed a wrong "v0.0.0" banner and gave an
|
|
37
|
+
* operator nothing to act on. `version` is passed in (never read from the live
|
|
38
|
+
* build here) so callers/tests can pin a sentinel and never compare the live
|
|
39
|
+
* VERSION.
|
|
40
|
+
*/
|
|
41
|
+
export function renderDaemonStartupBanner(
|
|
42
|
+
version: string,
|
|
43
|
+
binding: { readonly homeDir: string; readonly host: string; readonly port: number },
|
|
44
|
+
binary = 'goodvibes-daemon',
|
|
45
|
+
): string {
|
|
46
|
+
return (
|
|
47
|
+
`${binary} ${version} starting — ` +
|
|
48
|
+
`home=${binding.homeDir} host=${binding.host} port=${binding.port} ` +
|
|
49
|
+
`(manage as a service: ${binary} install-service)`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
27
54
|
return [
|
|
28
55
|
`Usage: ${binary} [OPTIONS] [PROMPT]`,
|
|
@@ -10,7 +10,7 @@ import { ensurePublicBaseUrl } from '../core/pairing-origin.ts';
|
|
|
10
10
|
import { formatPairingOffers, formatPostureCapabilities, pairingPostureNotice } from '../core/pairing-offers.ts';
|
|
11
11
|
import { defaultPairingTokenName } from '../core/pairing-handoff.ts';
|
|
12
12
|
import { stableUrlHostForBindHost } from '../core/stable-host.ts';
|
|
13
|
-
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
13
|
+
import { formatRuntimeEndpointBinding, resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
14
14
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
15
15
|
import type { CliCommandRuntime } from './types.ts';
|
|
16
16
|
import type { RuntimeServices } from '../runtime/services.ts';
|
|
@@ -313,8 +313,10 @@ export interface ControlPlaneStatusResult {
|
|
|
313
313
|
readonly configuredHost: string;
|
|
314
314
|
readonly host: string;
|
|
315
315
|
readonly port: number;
|
|
316
|
+
readonly recognized: boolean;
|
|
316
317
|
readonly posture: ReturnType<typeof classifyBindPosture>;
|
|
317
|
-
|
|
318
|
+
/** undefined = NOT PROBED (unrecognized host mode) — a tri-state, never coerced to false. */
|
|
319
|
+
readonly reachable: boolean | undefined;
|
|
318
320
|
readonly auth: ReturnType<typeof readAuthPaths>;
|
|
319
321
|
readonly service: {
|
|
320
322
|
readonly enabled: unknown;
|
|
@@ -327,7 +329,13 @@ export interface ControlPlaneStatusResult {
|
|
|
327
329
|
export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime): Promise<ControlPlaneStatusResult> {
|
|
328
330
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
329
331
|
const enabled = runtime.configManager.get('controlPlane.enabled');
|
|
330
|
-
|
|
332
|
+
// Never TCP-probe the fallback endpoint of an unrecognized hostMode — a
|
|
333
|
+
// probe asserts a binding that does not exist. Not-probed is its own state
|
|
334
|
+
// (undefined), NEVER coerced to a definite false: a daemon launched with
|
|
335
|
+
// flag overrides can be serving healthily regardless of the stored mode.
|
|
336
|
+
const reachable = enabled === true
|
|
337
|
+
? (binding.recognized ? await probeTcp(binding.host, binding.port) : undefined)
|
|
338
|
+
: false;
|
|
331
339
|
const auth = readAuthPaths(runtime);
|
|
332
340
|
const service = {
|
|
333
341
|
enabled: runtime.configManager.get('service.enabled'),
|
|
@@ -335,7 +343,8 @@ export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime):
|
|
|
335
343
|
restartOnFailure: runtime.configManager.get('service.restartOnFailure'),
|
|
336
344
|
};
|
|
337
345
|
const issues: string[] = [];
|
|
338
|
-
if (
|
|
346
|
+
if (!binding.recognized) issues.push(`controlPlane.hostMode '${binding.hostMode}' is not a recognized mode (local|network|custom) — the daemon cannot bind until it is corrected.`);
|
|
347
|
+
if (enabled === true && binding.recognized && !reachable) issues.push(`Control plane is enabled but not reachable on ${binding.host}:${binding.port}.`);
|
|
339
348
|
if (enabled === true && service.enabled !== true) issues.push('Control plane is enabled but service mode is off.');
|
|
340
349
|
if (enabled === true && service.autostart !== true) issues.push('Control plane is enabled but service autostart is off.');
|
|
341
350
|
if (enabled === true && service.restartOnFailure !== true) issues.push('Control plane is enabled but service restart-on-failure is off.');
|
|
@@ -356,9 +365,9 @@ export function formatControlPlaneStatus(runtime: CliCommandRuntime, value: Cont
|
|
|
356
365
|
return formatJsonOrText(runtime.cli)(value, [
|
|
357
366
|
'GoodVibes control-plane status',
|
|
358
367
|
` enabled: ${yesNo(value.enabled)}`,
|
|
359
|
-
` bind: ${value
|
|
360
|
-
` bind posture: ${value.posture.label}`,
|
|
361
|
-
` reachable: ${yesNo(value.reachable)}`,
|
|
368
|
+
` bind: ${formatRuntimeEndpointBinding(value)}`,
|
|
369
|
+
` bind posture: ${value.recognized ? value.posture.label : 'unknown (unrecognized host mode)'}`,
|
|
370
|
+
` reachable: ${value.reachable === undefined ? 'not probed (unrecognized host mode)' : yesNo(value.reachable)}`,
|
|
362
371
|
` service: enabled=${yesNo(value.service.enabled)} autostart=${yesNo(value.service.autostart)} restartOnFailure=${yesNo(value.service.restartOnFailure)}`,
|
|
363
372
|
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
364
373
|
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
@@ -486,7 +495,7 @@ export function renderWeb(runtime: CliCommandRuntime): string {
|
|
|
486
495
|
return formatJsonOrText(runtime.cli)(value, [
|
|
487
496
|
'GoodVibes web',
|
|
488
497
|
` enabled: ${yesNo(value.enabled)}`,
|
|
489
|
-
` bind: ${value
|
|
498
|
+
` bind: ${formatRuntimeEndpointBinding(value)}`,
|
|
490
499
|
` url: ${value.url}`,
|
|
491
500
|
...(runtime.cli.flags.open ? [` open: ${openBrowser(value.url)}`] : []),
|
|
492
501
|
].join('\n'));
|
|
@@ -7,7 +7,7 @@ import { PlatformServiceManager } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
|
7
7
|
import type { ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
8
8
|
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
9
9
|
import type { ConfigManager } from '../config/index.ts';
|
|
10
|
-
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
10
|
+
import { formatRuntimeEndpointBinding, resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
11
|
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
12
12
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
13
13
|
import { redactText } from './redaction.ts';
|
|
@@ -395,7 +395,9 @@ export async function buildCliServicePosture(
|
|
|
395
395
|
binding,
|
|
396
396
|
bindPosture: classifyBindPosture(binding),
|
|
397
397
|
networkFacing: isNetworkFacing(enabled, binding),
|
|
398
|
-
|
|
398
|
+
// Never TCP-probe the fallback endpoint of an unrecognized hostMode —
|
|
399
|
+
// a probe asserts a binding that does not exist.
|
|
400
|
+
...(options.probe && enabled && binding.recognized ? { reachable: await probeTcp(binding.host, binding.port) } : {}),
|
|
399
401
|
};
|
|
400
402
|
}));
|
|
401
403
|
|
|
@@ -485,7 +487,7 @@ export function formatCliServicePosture(posture: CliServicePosture, json = false
|
|
|
485
487
|
'',
|
|
486
488
|
'Endpoints:',
|
|
487
489
|
...posture.endpoints.map((endpoint) =>
|
|
488
|
-
` ${endpoint.label}: enabled=${yesNo(endpoint.enabled)} ${endpoint.binding
|
|
490
|
+
` ${endpoint.label}: enabled=${yesNo(endpoint.enabled)} ${formatRuntimeEndpointBinding(endpoint.binding)} posture=${endpoint.binding.recognized ? endpoint.bindPosture.label : 'unknown'}${endpoint.reachable === undefined ? '' : ` reachable=${yesNo(endpoint.reachable)}`}`,
|
|
489
491
|
),
|
|
490
492
|
'',
|
|
491
493
|
posture.issues.length === 0 ? 'Readiness: ready' : 'Readiness: needs attention',
|
package/src/cli/status.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
3
|
import type { OnboardingCheckMarkersState } from '../runtime/onboarding/index.ts';
|
|
4
|
-
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
5
|
-
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
4
|
+
import { formatRuntimeEndpointBinding, resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
5
|
+
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
6
6
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
7
7
|
import type { GoodVibesCliOutputFormat } from './types.ts';
|
|
8
8
|
import type { CliServicePosture } from './service-posture.ts';
|
|
@@ -165,8 +165,8 @@ function secretPolicyLabel(policy: unknown): string {
|
|
|
165
165
|
return String(policy ?? 'unknown');
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function bindLine(label: string, enabled: unknown, binding:
|
|
169
|
-
return ` ${label}: ${yesNo(enabled)} (${binding
|
|
168
|
+
function bindLine(label: string, enabled: unknown, binding: RuntimeEndpointBinding): string {
|
|
169
|
+
return ` ${label}: ${yesNo(enabled)} (${formatRuntimeEndpointBinding(binding)})`;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/** Describe registration coverage for the Workspace status section. */
|
|
@@ -300,8 +300,8 @@ export function buildCliExposureReport(options: CliStatusOptions): readonly CliE
|
|
|
300
300
|
id: surface.id,
|
|
301
301
|
label: surface.label,
|
|
302
302
|
enabled,
|
|
303
|
-
bind:
|
|
304
|
-
reach: classifyBindPosture(binding).label,
|
|
303
|
+
bind: formatRuntimeEndpointBinding(binding),
|
|
304
|
+
reach: binding.recognized ? classifyBindPosture(binding).label : 'Unknown (unrecognized host mode)',
|
|
305
305
|
networkFacing,
|
|
306
306
|
authMode: surfaceAuthMode(networkFacing, options.auth),
|
|
307
307
|
originAllowlist: surfaceOriginAllowlist(surface.id, config),
|
|
@@ -333,6 +333,27 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
333
333
|
|
|
334
334
|
const findings: CliDoctorFinding[] = [];
|
|
335
335
|
|
|
336
|
+
// An unrecognized hostMode has NO binding the SDK can produce (its bind
|
|
337
|
+
// resolver has no default case — the daemon throws before binding), so it
|
|
338
|
+
// is surfaced as a doctor finding on every endpoint that carries one,
|
|
339
|
+
// instead of the fallback loopback binding being presented as fact.
|
|
340
|
+
for (const [endpointId, endpointLabel, endpointBinding] of [
|
|
341
|
+
['controlPlane', 'control plane', controlPlaneBinding],
|
|
342
|
+
['httpListener', 'HTTP listener', httpListenerBinding],
|
|
343
|
+
['web', 'web surface', webBinding],
|
|
344
|
+
] as const) {
|
|
345
|
+
if (endpointBinding.recognized) continue;
|
|
346
|
+
findings.push({
|
|
347
|
+
id: `unrecognized-host-mode-${endpointId}`,
|
|
348
|
+
area: 'service',
|
|
349
|
+
severity: 'warning',
|
|
350
|
+
summary: `${endpointLabel} hostMode '${endpointBinding.hostMode}' is not a recognized mode.`,
|
|
351
|
+
cause: `${endpointId}.hostMode is set to '${endpointBinding.hostMode}', but only local|network|custom are recognized.`,
|
|
352
|
+
impact: 'The daemon cannot resolve a binding for this endpoint and will fail to start it; displays cannot show a real bind address.',
|
|
353
|
+
action: `Set ${endpointId}.hostMode to local, network, or custom.`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
336
357
|
if (serverBackedEnabled && !serviceEnabled) {
|
|
337
358
|
findings.push({
|
|
338
359
|
id: 'service-disabled-for-server-surfaces',
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolveGoodVibesNtfyTopics,
|
|
7
7
|
} from '@pellux/goodvibes-sdk/platform/integrations';
|
|
8
8
|
import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags, surfaceFeatureGateSettingsKeys } from '../runtime/surface-feature-flags.ts';
|
|
9
|
-
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
9
|
+
import { formatRuntimeEndpointBinding, resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
10
10
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
11
11
|
import type { CliCommandRuntime } from './types.ts';
|
|
12
12
|
import {
|
|
@@ -103,9 +103,9 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
103
103
|
const shouldProbeListener = includeProbe && (!target || targetExternalSurface);
|
|
104
104
|
const [controlPlaneReachable, webReachable, listenerReachable] = includeProbe
|
|
105
105
|
? await Promise.all([
|
|
106
|
-
shouldProbeControlPlane ? probeTcp(controlPlane.host, controlPlane.port) : Promise.resolve(undefined),
|
|
107
|
-
shouldProbeWeb ? probeTcp(web.host, web.port) : Promise.resolve(undefined),
|
|
108
|
-
shouldProbeListener ? probeTcp(httpListener.host, httpListener.port) : Promise.resolve(undefined),
|
|
106
|
+
shouldProbeControlPlane && controlPlane.recognized ? probeTcp(controlPlane.host, controlPlane.port) : Promise.resolve(undefined),
|
|
107
|
+
shouldProbeWeb && web.recognized ? probeTcp(web.host, web.port) : Promise.resolve(undefined),
|
|
108
|
+
shouldProbeListener && httpListener.recognized ? probeTcp(httpListener.host, httpListener.port) : Promise.resolve(undefined),
|
|
109
109
|
])
|
|
110
110
|
: [undefined, undefined, undefined];
|
|
111
111
|
const externalSurfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
|
|
@@ -129,13 +129,18 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
129
129
|
remoteTopic: String(config.get('surfaces.ntfy.remoteTopic' as ConfigKey) || GOODVIBES_NTFY_REMOTE_TOPIC),
|
|
130
130
|
});
|
|
131
131
|
const readinessIssues: string[] = [];
|
|
132
|
-
|
|
132
|
+
for (const [endpointLabel, endpointBinding] of [['controlPlane', controlPlane], ['web', web], ['httpListener', httpListener]] as const) {
|
|
133
|
+
if (!endpointBinding.recognized) {
|
|
134
|
+
readinessIssues.push(`${endpointLabel}.hostMode '${endpointBinding.hostMode}' is not a recognized mode (local|network|custom) — the daemon cannot bind this endpoint until it is corrected.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (shouldProbeControlPlane && controlPlane.recognized && config.get('controlPlane.enabled') === true && !controlPlaneReachable) {
|
|
133
138
|
readinessIssues.push(`Control plane is enabled but not reachable on ${controlPlane.host}:${controlPlane.port}.`);
|
|
134
139
|
}
|
|
135
|
-
if (shouldProbeWeb && config.get('web.enabled') === true && !webReachable) {
|
|
140
|
+
if (shouldProbeWeb && web.recognized && config.get('web.enabled') === true && !webReachable) {
|
|
136
141
|
readinessIssues.push(`Web surface is enabled but not reachable on ${web.host}:${web.port}.`);
|
|
137
142
|
}
|
|
138
|
-
if (shouldProbeListener && config.get('danger.httpListener') === true && !listenerReachable) {
|
|
143
|
+
if (shouldProbeListener && httpListener.recognized && config.get('danger.httpListener') === true && !listenerReachable) {
|
|
139
144
|
readinessIssues.push(`HTTP listener is enabled but not reachable on ${httpListener.host}:${httpListener.port}.`);
|
|
140
145
|
}
|
|
141
146
|
for (const surface of filteredSurfaces) {
|
|
@@ -157,6 +162,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
157
162
|
configuredHost: controlPlane.configuredHost,
|
|
158
163
|
host: controlPlane.host,
|
|
159
164
|
port: controlPlane.port,
|
|
165
|
+
recognized: controlPlane.recognized,
|
|
160
166
|
reachable: controlPlaneReachable,
|
|
161
167
|
},
|
|
162
168
|
web: {
|
|
@@ -165,6 +171,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
165
171
|
configuredHost: web.configuredHost,
|
|
166
172
|
host: web.host,
|
|
167
173
|
port: web.port,
|
|
174
|
+
recognized: web.recognized,
|
|
168
175
|
reachable: webReachable,
|
|
169
176
|
},
|
|
170
177
|
httpListener: {
|
|
@@ -173,6 +180,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
173
180
|
configuredHost: httpListener.configuredHost,
|
|
174
181
|
host: httpListener.host,
|
|
175
182
|
port: httpListener.port,
|
|
183
|
+
recognized: httpListener.recognized,
|
|
176
184
|
reachable: listenerReachable,
|
|
177
185
|
},
|
|
178
186
|
surfaces: filteredSurfaces,
|
|
@@ -180,9 +188,9 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
180
188
|
};
|
|
181
189
|
const output = formatJsonOrText(runtime.cli)(value, [
|
|
182
190
|
'GoodVibes surfaces',
|
|
183
|
-
` control-plane: ${yesNo(value.controlPlane.enabled)} (${value.controlPlane
|
|
184
|
-
` web: ${yesNo(value.web.enabled)} (${value.web
|
|
185
|
-
` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener
|
|
191
|
+
` control-plane: ${yesNo(value.controlPlane.enabled)} (${formatRuntimeEndpointBinding(value.controlPlane)})${includeProbe && value.controlPlane.reachable !== undefined ? ` reachable=${yesNo(value.controlPlane.reachable)}` : ''}`,
|
|
192
|
+
` web: ${yesNo(value.web.enabled)} (${formatRuntimeEndpointBinding(value.web)})${includeProbe && value.web.reachable !== undefined ? ` reachable=${yesNo(value.web.reachable)}` : ''}`,
|
|
193
|
+
` http-listener: ${yesNo(value.httpListener.enabled)} (${formatRuntimeEndpointBinding(value.httpListener)})${includeProbe && value.httpListener.reachable !== undefined ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
|
|
186
194
|
'',
|
|
187
195
|
'External surfaces:',
|
|
188
196
|
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
|
|
@@ -208,8 +216,10 @@ export interface ListenerTestResult {
|
|
|
208
216
|
readonly configuredHost: string;
|
|
209
217
|
readonly host: string;
|
|
210
218
|
readonly port: number;
|
|
219
|
+
readonly recognized: boolean;
|
|
211
220
|
readonly posture: ReturnType<typeof classifyBindPosture>;
|
|
212
|
-
|
|
221
|
+
/** undefined = NOT PROBED (unrecognized host mode) — a tri-state, never coerced to false. */
|
|
222
|
+
readonly reachable: boolean | undefined;
|
|
213
223
|
readonly service: {
|
|
214
224
|
readonly enabled: unknown;
|
|
215
225
|
readonly autostart: unknown;
|
|
@@ -231,7 +241,10 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
|
|
|
231
241
|
const enabled = runtime.configManager.get('danger.httpListener');
|
|
232
242
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'httpListener');
|
|
233
243
|
const posture = classifyBindPosture(binding);
|
|
234
|
-
|
|
244
|
+
// Not-probed (unrecognized host mode) stays undefined — never a definite false.
|
|
245
|
+
const reachable = enabled === true
|
|
246
|
+
? (binding.recognized ? await probeTcp(binding.host, binding.port) : undefined)
|
|
247
|
+
: false;
|
|
235
248
|
const auth = readAuthPaths(runtime);
|
|
236
249
|
const service = {
|
|
237
250
|
enabled: runtime.configManager.get('service.enabled'),
|
|
@@ -252,6 +265,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
|
|
|
252
265
|
};
|
|
253
266
|
}).filter((surface) => surface.enabled === true);
|
|
254
267
|
const issues: string[] = [];
|
|
268
|
+
if (!binding.recognized) issues.push(`httpListener.hostMode '${binding.hostMode}' is not a recognized mode (local|network|custom) — the daemon cannot bind until it is corrected.`);
|
|
255
269
|
if (enabled !== true) issues.push('HTTP listener is disabled.');
|
|
256
270
|
if (enabled === true && service.enabled !== true) issues.push('HTTP listener is enabled but service mode is off.');
|
|
257
271
|
if (enabled === true && service.autostart !== true) issues.push('HTTP listener is enabled but service autostart is off.');
|
|
@@ -269,9 +283,9 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
|
|
|
269
283
|
return formatJsonOrText(runtime.cli)(value, [
|
|
270
284
|
'GoodVibes listener test',
|
|
271
285
|
` enabled: ${yesNo(value.enabled)}`,
|
|
272
|
-
` endpoint: ${value
|
|
273
|
-
` bind posture: ${value.posture.label}`,
|
|
274
|
-
` reachable: ${yesNo(value.reachable)}`,
|
|
286
|
+
` endpoint: ${formatRuntimeEndpointBinding(value)}`,
|
|
287
|
+
` bind posture: ${value.recognized ? value.posture.label : 'unknown (unrecognized host mode)'}`,
|
|
288
|
+
` reachable: ${value.reachable === undefined ? 'not probed (unrecognized host mode)' : yesNo(value.reachable)}`,
|
|
275
289
|
` service: enabled=${yesNo(value.service.enabled)} autostart=${yesNo(value.service.autostart)} restartOnFailure=${yesNo(value.service.restartOnFailure)}`,
|
|
276
290
|
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
277
291
|
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// memory-diagnostics-gateway.ts
|
|
3
|
+
//
|
|
4
|
+
// The daemon-backed verb the /health memory (doctor) surface reads:
|
|
5
|
+
// ops.memory.get (GET /api/ops/memory) — the read-only MemoryGovernor snapshot.
|
|
6
|
+
// No named facade exists on the in-process OperatorClient, so — exactly like
|
|
7
|
+
// the memory-consolidation and voice-provisioning gateways — it goes over the
|
|
8
|
+
// generic operator invoke path (operator-rpc.ts's resolveOperatorRpc ->
|
|
9
|
+
// sdk.operator.invoke), reaching the SAME daemon the command layer does.
|
|
10
|
+
//
|
|
11
|
+
// A daemon that predates the memory governor answers an honest 501 (the verb is
|
|
12
|
+
// cataloged but has no handler when no live governor is composed) or a 404 (an
|
|
13
|
+
// older daemon without the route) — both mean "this daemon does not serve
|
|
14
|
+
// memory diagnostics", NOT a fabricated all-zero snapshot (see
|
|
15
|
+
// classifyMemoryDiagnosticsError). The interface is injectable so the command
|
|
16
|
+
// and its tests round-trip against a mocked daemon.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
20
|
+
import { GoodVibesSdkError } from '@pellux/goodvibes-sdk';
|
|
21
|
+
import { resolveOperatorRpc, describeOperatorRpcError } from '../input/commands/operator-rpc.ts';
|
|
22
|
+
import { memoryStatusLines, type MemoryGovernorSnapshotResult } from './memory-status.ts';
|
|
23
|
+
|
|
24
|
+
/** The narrow async verb surface the /health memory (doctor) surface drives. */
|
|
25
|
+
export interface MemoryDiagnosticsGateway {
|
|
26
|
+
/** Read the MemoryGovernor snapshot (ops.memory.get). */
|
|
27
|
+
fetchSnapshot(): Promise<MemoryGovernorSnapshotResult>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Why the gateway could not be built (daemon disabled / no control-plane URL),
|
|
32
|
+
* surfaced verbatim so the surface prints an honest "unavailable" line rather
|
|
33
|
+
* than guessing — mirrors the memory-consolidation / voice gateways.
|
|
34
|
+
*/
|
|
35
|
+
export type MemoryDiagnosticsGatewayResolution =
|
|
36
|
+
| { readonly available: true; readonly gateway: MemoryDiagnosticsGateway }
|
|
37
|
+
| { readonly available: false; readonly reason: string };
|
|
38
|
+
|
|
39
|
+
export interface MemoryDiagnosticsGatewayDeps {
|
|
40
|
+
readonly configManager: ConfigManager;
|
|
41
|
+
readonly homeDirectory: string | (() => string);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the live memory-diagnostics gateway over the generic operator invoke
|
|
46
|
+
* path — the same daemon resolution the command layer uses. Returns an honest
|
|
47
|
+
* unavailable reason when no daemon is reachable.
|
|
48
|
+
*/
|
|
49
|
+
export function createMemoryDiagnosticsGateway(deps: MemoryDiagnosticsGatewayDeps): MemoryDiagnosticsGatewayResolution {
|
|
50
|
+
const rpc = resolveOperatorRpc({ configManager: deps.configManager, homeDirectory: deps.homeDirectory });
|
|
51
|
+
if (!rpc.available) return { available: false, reason: rpc.reason };
|
|
52
|
+
const { sdk } = rpc;
|
|
53
|
+
return {
|
|
54
|
+
available: true,
|
|
55
|
+
gateway: { fetchSnapshot: () => sdk.operator.invoke('ops.memory.get', {}) },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** How an ops.memory.get rejection should render. */
|
|
60
|
+
export type MemoryDiagnosticsFailure =
|
|
61
|
+
| { readonly kind: 'unavailable'; readonly reason: string }
|
|
62
|
+
| { readonly kind: 'error'; readonly message: string };
|
|
63
|
+
|
|
64
|
+
/** The honest "no memory diagnostics" reason for a daemon that predates the governor verb. */
|
|
65
|
+
export const MEMORY_DIAGNOSTICS_UNAVAILABLE = 'this daemon does not serve memory diagnostics.';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Classify an ops.memory.get rejection: a 501 (a daemon composed without a live
|
|
69
|
+
* governor — the verb is cataloged but unhandled) or a 404 (an older daemon
|
|
70
|
+
* without the route) are both "verb unavailable"; anything else (network,
|
|
71
|
+
* 401/403, 500) is a generic error.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyMemoryDiagnosticsError(error: unknown): MemoryDiagnosticsFailure {
|
|
74
|
+
if (error instanceof GoodVibesSdkError && (error.status === 501 || error.status === 404)) {
|
|
75
|
+
return { kind: 'unavailable', reason: MEMORY_DIAGNOSTICS_UNAVAILABLE };
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'error', message: describeOperatorRpcError(error) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render the /health memory (doctor) block from an already-resolved gateway
|
|
82
|
+
* resolution — the testable core the command wraps. Kept separate from the live
|
|
83
|
+
* gateway construction so a wire test injects a fake resolution (available /
|
|
84
|
+
* unavailable / failing) with no HTTP.
|
|
85
|
+
*/
|
|
86
|
+
export async function renderMemoryDiagnostics(resolution: MemoryDiagnosticsGatewayResolution): Promise<string> {
|
|
87
|
+
if (!resolution.available) {
|
|
88
|
+
return `Health Review: Memory\n memory diagnostics unavailable: ${resolution.reason}`;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const snapshot = await resolution.gateway.fetchSnapshot();
|
|
92
|
+
return ['Health Review: Memory', ...memoryStatusLines(snapshot)].join('\n');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const failure = classifyMemoryDiagnosticsError(error);
|
|
95
|
+
return failure.kind === 'unavailable'
|
|
96
|
+
? `Health Review: Memory\n ${failure.reason}`
|
|
97
|
+
: `Health Review: Memory\n could not read memory diagnostics: ${failure.message}`;
|
|
98
|
+
}
|
|
99
|
+
}
|