@pellux/goodvibes-tui 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +208 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5025 -1636
- 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 +99 -23
- package/src/cli/management.ts +0 -2
- 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/consolidation-receipt.ts +41 -0
- package/src/core/conversation-rendering.ts +14 -5
- package/src/core/memory-diagnostics-gateway.ts +99 -0
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/memory-status.ts +118 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +21 -0
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +39 -0
- package/src/core/turn-event-wiring.ts +6 -0
- package/src/core/voice-provision-gateway.ts +202 -0
- package/src/core/voice-provision-status.ts +197 -0
- package/src/daemon/cli.ts +127 -60
- package/src/input/command-registry.ts +21 -0
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/experience-runtime.ts +25 -4
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +15 -1
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +44 -25
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -0
- package/src/input/config-modal-types.ts +11 -0
- package/src/input/config-modal.ts +65 -2
- package/src/input/handler-shortcuts.ts +23 -0
- package/src/input/keybindings.ts +19 -1
- package/src/input/settings-modal-data.ts +31 -37
- package/src/input/settings-modal-types.ts +5 -2
- package/src/input/tts-settings-actions.ts +27 -8
- package/src/input/voice-local-settings.ts +52 -0
- package/src/main.ts +42 -42
- package/src/panels/builtin/operations.ts +54 -1
- package/src/panels/builtin/shared.ts +12 -0
- package/src/panels/builtin-modals.ts +49 -32
- package/src/panels/diff-panel.ts +34 -1
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +120 -4
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +60 -44
- package/src/panels/fleet-read-model.ts +34 -3
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-stop.ts +38 -2
- package/src/panels/fleet-tabs.ts +2 -2
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/memory-modal.ts +149 -5
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/security-modal.ts +6 -0
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +4 -2
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/tool-call.ts +12 -6
- package/src/renderer/ui-factory.ts +32 -0
- package/src/runtime/bootstrap-command-context.ts +3 -1
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +6 -6
- package/src/runtime/bootstrap-shell.ts +7 -0
- package/src/runtime/code-index-services.ts +15 -1
- package/src/runtime/durability-services.ts +36 -1
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/interaction-seams.ts +109 -0
- 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/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +105 -107
- package/src/runtime/ui-services.ts +3 -0
- package/src/runtime/voice-setup-services.ts +104 -0
- package/src/shell/blocking-input.ts +21 -141
- package/src/shell/recovery-input-helpers.ts +53 -166
- 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.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -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.0",
|
|
108
|
+
"@pellux/goodvibes-terminal-shell": "1.10.0",
|
|
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.0",
|
|
135
|
+
"@pellux/goodvibes-tui-linux-arm64": "1.19.0",
|
|
136
|
+
"@pellux/goodvibes-tui-darwin-x64": "1.19.0",
|
|
137
|
+
"@pellux/goodvibes-tui-darwin-arm64": "1.19.0"
|
|
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]`,
|
|
@@ -5,12 +5,16 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
|
|
|
5
5
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
6
6
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
7
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { generateQrMatrix, renderQrToString, PAIRING_HANDOFF_OFFER_KINDS, type PairingHandoffOfferKind } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
9
|
+
import { ensurePublicBaseUrl } from '../core/pairing-origin.ts';
|
|
10
|
+
import { formatPairingOffers, formatPostureCapabilities, pairingPostureNotice } from '../core/pairing-offers.ts';
|
|
11
|
+
import { defaultPairingTokenName } from '../core/pairing-handoff.ts';
|
|
12
|
+
import { stableUrlHostForBindHost } from '../core/stable-host.ts';
|
|
13
|
+
import { formatRuntimeEndpointBinding, resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
14
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
15
|
import type { CliCommandRuntime } from './types.ts';
|
|
13
|
-
import {
|
|
16
|
+
import type { RuntimeServices } from '../runtime/services.ts';
|
|
17
|
+
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, readOptionValue, runNonInteractiveAgent, withRuntimeServices, yesNo } from './management-utils.ts';
|
|
14
18
|
|
|
15
19
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
20
|
return await withRuntimeServices(runtime, async (services) => {
|
|
@@ -309,8 +313,10 @@ export interface ControlPlaneStatusResult {
|
|
|
309
313
|
readonly configuredHost: string;
|
|
310
314
|
readonly host: string;
|
|
311
315
|
readonly port: number;
|
|
316
|
+
readonly recognized: boolean;
|
|
312
317
|
readonly posture: ReturnType<typeof classifyBindPosture>;
|
|
313
|
-
|
|
318
|
+
/** undefined = NOT PROBED (unrecognized host mode) — a tri-state, never coerced to false. */
|
|
319
|
+
readonly reachable: boolean | undefined;
|
|
314
320
|
readonly auth: ReturnType<typeof readAuthPaths>;
|
|
315
321
|
readonly service: {
|
|
316
322
|
readonly enabled: unknown;
|
|
@@ -323,7 +329,13 @@ export interface ControlPlaneStatusResult {
|
|
|
323
329
|
export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime): Promise<ControlPlaneStatusResult> {
|
|
324
330
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
325
331
|
const enabled = runtime.configManager.get('controlPlane.enabled');
|
|
326
|
-
|
|
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;
|
|
327
339
|
const auth = readAuthPaths(runtime);
|
|
328
340
|
const service = {
|
|
329
341
|
enabled: runtime.configManager.get('service.enabled'),
|
|
@@ -331,7 +343,8 @@ export async function buildControlPlaneStatusResult(runtime: CliCommandRuntime):
|
|
|
331
343
|
restartOnFailure: runtime.configManager.get('service.restartOnFailure'),
|
|
332
344
|
};
|
|
333
345
|
const issues: string[] = [];
|
|
334
|
-
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}.`);
|
|
335
348
|
if (enabled === true && service.enabled !== true) issues.push('Control plane is enabled but service mode is off.');
|
|
336
349
|
if (enabled === true && service.autostart !== true) issues.push('Control plane is enabled but service autostart is off.');
|
|
337
350
|
if (enabled === true && service.restartOnFailure !== true) issues.push('Control plane is enabled but service restart-on-failure is off.');
|
|
@@ -352,9 +365,9 @@ export function formatControlPlaneStatus(runtime: CliCommandRuntime, value: Cont
|
|
|
352
365
|
return formatJsonOrText(runtime.cli)(value, [
|
|
353
366
|
'GoodVibes control-plane status',
|
|
354
367
|
` enabled: ${yesNo(value.enabled)}`,
|
|
355
|
-
` bind: ${value
|
|
356
|
-
` bind posture: ${value.posture.label}`,
|
|
357
|
-
` 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)}`,
|
|
358
371
|
` service: enabled=${yesNo(value.service.enabled)} autostart=${yesNo(value.service.autostart)} restartOnFailure=${yesNo(value.service.restartOnFailure)}`,
|
|
359
372
|
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
360
373
|
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
@@ -368,19 +381,80 @@ export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Prom
|
|
|
368
381
|
return formatControlPlaneStatus(runtime, await buildControlPlaneStatusResult(runtime));
|
|
369
382
|
}
|
|
370
383
|
|
|
384
|
+
interface PairingHandoffCreateResult {
|
|
385
|
+
readonly token: { readonly id: string; readonly name: string; readonly token: string };
|
|
386
|
+
readonly offers: readonly { readonly kind: PairingHandoffOfferKind }[];
|
|
387
|
+
readonly fragment: string;
|
|
388
|
+
readonly deepLink?: string | undefined;
|
|
389
|
+
/** The honest TLS/capability posture of the origin the QR points at (SDK-computed). */
|
|
390
|
+
readonly posture?: import('@pellux/goodvibes-sdk/platform/pairing').OriginPosture | undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
371
393
|
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
394
|
+
return await withRuntimeServices(runtime, async (services) => {
|
|
395
|
+
// Freeze a stable web origin once (never clobbering a user-set value) so the
|
|
396
|
+
// printed link and the daemon's own handoff origin agree and survive a DHCP
|
|
397
|
+
// lease change where a stable name exists. The verb re-derives the same origin
|
|
398
|
+
// and returns its honest posture, so this call is a persistence side effect.
|
|
399
|
+
ensurePublicBaseUrl(runtime.configManager);
|
|
400
|
+
const name = readOptionValue(runtime.cli.commandArgs, 'name') ?? defaultPairingTokenName();
|
|
401
|
+
// Mint through the canonical hand-off verb: it mints a fresh per-device token
|
|
402
|
+
// and returns the exact `#pair=<token>` deep link the web app consumes. The
|
|
403
|
+
// daemon filters the requested offer set down to what it can actually satisfy.
|
|
404
|
+
const result = (await services.gatewayMethods.invoke('pairing.handoff.create', {
|
|
405
|
+
body: { name, offers: [...PAIRING_HANDOFF_OFFER_KINDS] },
|
|
406
|
+
context: { principalId: 'admin', principalKind: 'user', admin: true },
|
|
407
|
+
})) as PairingHandoffCreateResult;
|
|
408
|
+
const link = result.deepLink ?? result.fragment;
|
|
409
|
+
const qr = renderQrToString(generateQrMatrix(link));
|
|
410
|
+
const lines: string[] = [
|
|
411
|
+
'Scan to pair a device — the QR opens the web app already signed in:',
|
|
412
|
+
'',
|
|
413
|
+
` ${link}`,
|
|
414
|
+
` Token name: ${result.token.name} (rename or revoke later in /settings → security → devices)`,
|
|
415
|
+
];
|
|
416
|
+
if (result.offers.length > 0) {
|
|
417
|
+
lines.push('', 'Offers carried in this pairing (each declinable in the web app):', ...formatPairingOffers(result.offers.map((o) => o.kind)));
|
|
418
|
+
}
|
|
419
|
+
// The labeled browser-capability list and the one honest LAN line both render
|
|
420
|
+
// from the SDK posture the verb returned — never a locally-authored string.
|
|
421
|
+
const capabilities = formatPostureCapabilities(result.posture);
|
|
422
|
+
if (capabilities.length > 0) lines.push('', 'This device will get:', ...capabilities);
|
|
423
|
+
const notice = pairingPostureNotice(result.posture);
|
|
424
|
+
if (notice) lines.push('', notice);
|
|
425
|
+
// Tailscale detection is read-only here (the interactive serve lives in the
|
|
426
|
+
// pairing modal); when detected with an https MagicDNS URL, name the encrypted
|
|
427
|
+
// path. Absence stays quiet.
|
|
428
|
+
lines.push(...(await renderPairingTailscaleLines(services)));
|
|
429
|
+
lines.push('', qr);
|
|
430
|
+
return lines.join('\n');
|
|
380
431
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
interface TailscaleGetResult {
|
|
435
|
+
readonly available: boolean;
|
|
436
|
+
readonly httpsUrl?: string | undefined;
|
|
437
|
+
readonly detail: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Read-only tailscale detection for the `gv pair` block: when tailscale is
|
|
442
|
+
* available with a served https MagicDNS URL, name the encrypted path (the
|
|
443
|
+
* interactive one-action serve lives in the pairing modal). Quiet — an empty
|
|
444
|
+
* array — when tailscale is absent or the probe fails.
|
|
445
|
+
*/
|
|
446
|
+
async function renderPairingTailscaleLines(services: RuntimeServices): Promise<string[]> {
|
|
447
|
+
try {
|
|
448
|
+
const ts = (await services.gatewayMethods.invoke('tailscale.get', {
|
|
449
|
+
body: {},
|
|
450
|
+
context: { principalId: 'admin', principalKind: 'user', admin: true },
|
|
451
|
+
})) as TailscaleGetResult;
|
|
452
|
+
if (!ts.available) return [];
|
|
453
|
+
if (ts.httpsUrl) return ['', `Encrypted access (Tailscale): ${ts.httpsUrl}`];
|
|
454
|
+
return ['', 'Tailscale detected — run it as a serve target for encrypted https access (see the pairing modal).'];
|
|
455
|
+
} catch {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
384
458
|
}
|
|
385
459
|
|
|
386
460
|
export async function renderRemote(runtime: CliCommandRuntime, label: 'remote' | 'bridge'): Promise<string> {
|
|
@@ -408,9 +482,11 @@ export function renderWeb(runtime: CliCommandRuntime): string {
|
|
|
408
482
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'web');
|
|
409
483
|
const publicBaseUrl = String(runtime.configManager.get('web.publicBaseUrl') ?? '');
|
|
410
484
|
const hasEndpointOverride = runtime.cli.flags.hostname !== undefined || runtime.cli.flags.port !== undefined;
|
|
485
|
+
// Prefer the stable-name resolution over the raw first-non-internal IPv4 so the
|
|
486
|
+
// printed URL survives a DHCP lease change where a stable name exists.
|
|
411
487
|
const url = !hasEndpointOverride && publicBaseUrl
|
|
412
488
|
? publicBaseUrl
|
|
413
|
-
: `http://${
|
|
489
|
+
: `http://${stableUrlHostForBindHost(binding.host).host}:${binding.port}`;
|
|
414
490
|
const value = {
|
|
415
491
|
enabled: runtime.configManager.get('web.enabled'),
|
|
416
492
|
...binding,
|
|
@@ -419,7 +495,7 @@ export function renderWeb(runtime: CliCommandRuntime): string {
|
|
|
419
495
|
return formatJsonOrText(runtime.cli)(value, [
|
|
420
496
|
'GoodVibes web',
|
|
421
497
|
` enabled: ${yesNo(value.enabled)}`,
|
|
422
|
-
` bind: ${value
|
|
498
|
+
` bind: ${formatRuntimeEndpointBinding(value)}`,
|
|
423
499
|
` url: ${value.url}`,
|
|
424
500
|
...(runtime.cli.flags.open ? [` open: ${openBrowser(value.url)}`] : []),
|
|
425
501
|
].join('\n'));
|
package/src/cli/management.ts
CHANGED
|
@@ -11,8 +11,6 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
|
|
|
11
11
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
13
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
14
|
-
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
15
|
-
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
16
14
|
import type { CliCommandRuntime, GoodVibesCliParseResult } from './types.ts';
|
|
17
15
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
18
16
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
@@ -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,41 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// consolidation-receipt.ts — the one-line notice for an idle-time memory
|
|
3
|
+
// consolidation run.
|
|
4
|
+
//
|
|
5
|
+
// The SDK's MemoryConsolidationScheduler runs at idle and produces a
|
|
6
|
+
// MemoryConsolidationRunReceipt (merged / archived / decayed / proposed). This
|
|
7
|
+
// formats it into the same one-line shape every other attach-time receipt
|
|
8
|
+
// uses, so a run that changed something surfaces on the next attach like a
|
|
9
|
+
// crash/update/migration receipt. A pure no-op run yields null — no noise.
|
|
10
|
+
//
|
|
11
|
+
// A run that PROPOSED something (a contradiction or cross-scope-duplicate
|
|
12
|
+
// awaiting human judgment — never auto-applied) names where to actually look:
|
|
13
|
+
// the Memory modal's Proposals tab (`/memory`, then the Proposals tab —
|
|
14
|
+
// memory-modal.ts's proposalsTab()), which lists each proposal's kind,
|
|
15
|
+
// reason, and affected record ids and jumps to them in the Review Queue.
|
|
16
|
+
// Without this pointer the receipt line was the only trace a proposal ever
|
|
17
|
+
// existed — a count with nowhere to go.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
import type { MemoryConsolidationRunReceipt } from '@pellux/goodvibes-sdk/platform/state';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A one-line receipt for a consolidation run that changed something, or null
|
|
24
|
+
* when the run merged/archived/decayed/proposed nothing (a quiet idle pass —
|
|
25
|
+
* no notice, matching the check-in "stayed quiet" discipline).
|
|
26
|
+
*/
|
|
27
|
+
export function formatConsolidationReceipt(receipt: MemoryConsolidationRunReceipt): string | null {
|
|
28
|
+
const merged = receipt.merged.length;
|
|
29
|
+
const archived = receipt.archived.length;
|
|
30
|
+
const decayed = receipt.decayed.length;
|
|
31
|
+
const proposed = receipt.proposed.length;
|
|
32
|
+
if (merged + archived + decayed + proposed === 0) return null;
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
if (merged) parts.push(`${merged} merged`);
|
|
35
|
+
if (archived) parts.push(`${archived} archived`);
|
|
36
|
+
if (decayed) parts.push(`${decayed} decayed`);
|
|
37
|
+
if (proposed) parts.push(`${proposed} proposed`);
|
|
38
|
+
const base = `Memory consolidation: ${parts.join(', ')} (scanned ${receipt.scanned}).`;
|
|
39
|
+
if (proposed === 0) return base;
|
|
40
|
+
return `${base} Review the ${proposed} proposed change${proposed === 1 ? '' : 's'} with /memory (Proposals tab).`;
|
|
41
|
+
}
|