@pellux/goodvibes-tui 1.16.1 → 1.18.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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -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 +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- 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 +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
package/src/daemon/cli.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import { homedir
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { ConfigManager, resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
5
4
|
import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
|
|
6
5
|
import { RuntimeEventBus, GlobalNetworkTransportInstaller } from '@/runtime/index.ts';
|
|
7
|
-
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
8
|
-
import type { FlagState } from '@/runtime/index.ts';
|
|
6
|
+
import { bindFeatureSettingsBridge, createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
|
|
9
7
|
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
10
8
|
import { createRuntimeServices } from '../runtime/services.ts';
|
|
11
9
|
import { DaemonServer, HttpListener } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
10
|
+
import { createHostPowerSeam } from '@pellux/goodvibes-sdk/platform/power';
|
|
12
11
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
14
13
|
import {
|
|
15
14
|
getOrCreateCompanionToken,
|
|
16
15
|
pruneStaleOperatorTokens,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
formatConnectionBlock,
|
|
16
|
+
buildPairingHandoffLink,
|
|
17
|
+
describeOriginPosture,
|
|
20
18
|
generateQrMatrix,
|
|
21
19
|
renderQrToString,
|
|
22
20
|
} from '@pellux/goodvibes-sdk/platform/pairing';
|
|
21
|
+
import { ensurePublicBaseUrl } from '../core/pairing-origin.ts';
|
|
22
|
+
import { availablePairingOffers } from '../core/pairing-handoff.ts';
|
|
23
|
+
import { formatPairingOffers, formatPostureCapabilities, pairingPostureNotice } from '../core/pairing-offers.ts';
|
|
23
24
|
import { workspaceOperatorTokenCandidates } from '../runtime/operator-token-cleanup.ts';
|
|
24
25
|
import {
|
|
25
26
|
scan,
|
|
@@ -28,6 +29,8 @@ import {
|
|
|
28
29
|
} from '@pellux/goodvibes-sdk/platform/discovery';
|
|
29
30
|
import { createSafeHostServeFactory } from './safe-serve.ts';
|
|
30
31
|
import { isDaemonServiceSubcommand, resolveInstalledDaemonBinary, runDaemonServiceCli } from './service-commands.ts';
|
|
32
|
+
import { resolveDaemonUpdateArtifact } from './lifecycle.ts';
|
|
33
|
+
import { VERSION } from '../version.ts';
|
|
31
34
|
|
|
32
35
|
import {
|
|
33
36
|
parseGoodVibesCli,
|
|
@@ -50,36 +53,6 @@ type DaemonCliTokens = {
|
|
|
50
53
|
readonly httpToken: string | undefined;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
|
-
function getLocalNetworkIp(): string {
|
|
54
|
-
try {
|
|
55
|
-
const nets = networkInterfaces();
|
|
56
|
-
for (const name of Object.keys(nets)) {
|
|
57
|
-
for (const net of nets[name] ?? []) {
|
|
58
|
-
if (net.family === 'IPv4' && !net.internal) {
|
|
59
|
-
return net.address;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
return 'localhost';
|
|
65
|
-
}
|
|
66
|
-
return 'localhost';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readBootstrapPassword(credentialPath: string): string | undefined {
|
|
70
|
-
try {
|
|
71
|
-
const content = readFileSync(credentialPath, 'utf-8');
|
|
72
|
-
for (const line of content.split('\n')) {
|
|
73
|
-
if (line.startsWith('password=')) {
|
|
74
|
-
return line.slice('password='.length).trim();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// credential file may not exist yet
|
|
79
|
-
}
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
56
|
function resolveDaemonCliOwnership(): DaemonCliOwnership {
|
|
84
57
|
return {
|
|
85
58
|
workingDirectory: process.env['GOODVIBES_WORKING_DIR'] ?? process.cwd(),
|
|
@@ -190,10 +163,11 @@ async function main(): Promise<void> {
|
|
|
190
163
|
|
|
191
164
|
const runtimeBus = new RuntimeEventBus();
|
|
192
165
|
const runtimeStore = createRuntimeStore();
|
|
166
|
+
// Gate states derive from the domain settings keys; the bridge keeps live
|
|
167
|
+
// config changes flowing into the manager for the daemon's lifetime.
|
|
193
168
|
const featureFlags = createFeatureFlagManager();
|
|
194
|
-
featureFlags.loadFromConfig({
|
|
195
|
-
|
|
196
|
-
});
|
|
169
|
+
featureFlags.loadFromConfig({ flags: deriveFeatureStates(config) });
|
|
170
|
+
bindFeatureSettingsBridge(config, featureFlags);
|
|
197
171
|
const runtimeServices = createRuntimeServices({
|
|
198
172
|
configManager: config,
|
|
199
173
|
featureFlags,
|
|
@@ -202,6 +176,17 @@ async function main(): Promise<void> {
|
|
|
202
176
|
getConversationTitle: () => 'goodvibes daemon',
|
|
203
177
|
workingDir,
|
|
204
178
|
homeDirectory,
|
|
179
|
+
// The standalone daemon observes externally-launched coding-agent sessions
|
|
180
|
+
// on the host read-only (fleet visibility + steer; never counted, never
|
|
181
|
+
// stopped). Daemon-side only — the interactive process reads this snapshot
|
|
182
|
+
// rather than double-detecting. Mirrors the SDK daemon cli.
|
|
183
|
+
observeExternalAgents: true,
|
|
184
|
+
// Opt into the REAL host power seam (Linux logind: systemd-inhibit children
|
|
185
|
+
// + the dbus-monitor sleep-edge watcher) so the standalone daemon holds live
|
|
186
|
+
// keep-awake/idle-inhibit. SDK 1.9.0's runtime-services factory defaults to
|
|
187
|
+
// the non-spawning unavailable seam; only daemon compositions opt in. Mirrors
|
|
188
|
+
// the SDK daemon cli's createHostPowerSeam() (sdk commit 3a5ea26d).
|
|
189
|
+
powerSeam: createHostPowerSeam(),
|
|
205
190
|
});
|
|
206
191
|
|
|
207
192
|
// Load persisted providers from disk so the provider registry is pre-populated
|
|
@@ -233,6 +218,11 @@ async function main(): Promise<void> {
|
|
|
233
218
|
userAuth,
|
|
234
219
|
runtimeServices,
|
|
235
220
|
serveFactory: createSafeHostServeFactory('Standalone daemon'),
|
|
221
|
+
// Hand the facade this binary's real version + exec path so its self-update
|
|
222
|
+
// lifecycle compares the HOST's version (not the sdk package's). Resolves to
|
|
223
|
+
// undefined for a non-binary install, so a dev run stays host-managed and
|
|
224
|
+
// never swaps the interpreter. See lifecycle.ts.
|
|
225
|
+
updateArtifact: resolveDaemonUpdateArtifact({ version: VERSION }),
|
|
236
226
|
});
|
|
237
227
|
const listener = new HttpListener({
|
|
238
228
|
hookDispatcher: runtimeServices.hookDispatcher,
|
|
@@ -268,6 +258,11 @@ async function main(): Promise<void> {
|
|
|
268
258
|
config.get('danger.httpListener') ? listener.start() : Promise.resolve(),
|
|
269
259
|
]);
|
|
270
260
|
|
|
261
|
+
// The DaemonServer facade owns the hourly self-update loop now (fed this
|
|
262
|
+
// binary's version + exec path via updateArtifact above); it checks hourly,
|
|
263
|
+
// swaps only at an idle moment, keeps the outgoing binary at `<path>.previous`,
|
|
264
|
+
// and stops with the daemon. No separate host loop.
|
|
265
|
+
|
|
271
266
|
const abortController = new AbortController();
|
|
272
267
|
|
|
273
268
|
const shutdown = async (): Promise<void> => {
|
|
@@ -300,26 +295,37 @@ async function main(): Promise<void> {
|
|
|
300
295
|
httpListener: config.get('danger.httpListener'),
|
|
301
296
|
});
|
|
302
297
|
|
|
303
|
-
// Print
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
token: companionTokenRecord.token,
|
|
315
|
-
password: bootstrapPassword,
|
|
316
|
-
surface: 'tui',
|
|
298
|
+
// Print a device-pairing QR to stdout. The QR encodes the canonical
|
|
299
|
+
// `#pair=<token>` deep link the web app consumes — a camera scan opens it
|
|
300
|
+
// already signed in. No raw JSON connection blob is printed. This is also the
|
|
301
|
+
// one place web.publicBaseUrl is frozen from the stable-name resolution (never
|
|
302
|
+
// clobbering a user-set value), so the printed origin survives a DHCP change.
|
|
303
|
+
// The daemon's shared companion token rides as the pairing token; a device can
|
|
304
|
+
// later migrate to its own per-device token from /settings → security → devices.
|
|
305
|
+
const webOrigin = ensurePublicBaseUrl(config);
|
|
306
|
+
const offers = availablePairingOffers({
|
|
307
|
+
relayEnabled: config.get('relay.enabled') === true,
|
|
308
|
+
stepUpAvailable: true,
|
|
317
309
|
});
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
310
|
+
const deepLink = buildPairingHandoffLink({ webOrigin: webOrigin.origin, token: companionTokenRecord.token, offers });
|
|
311
|
+
const qrString = renderQrToString(generateQrMatrix(deepLink));
|
|
312
|
+
// The banner renders the SAME SDK posture the pairing verb carries: the labeled
|
|
313
|
+
// capability list, and the one honest LAN line only when the posture holds it.
|
|
314
|
+
const posture = describeOriginPosture(webOrigin.origin);
|
|
315
|
+
const capabilities = formatPostureCapabilities(posture);
|
|
316
|
+
const notice = pairingPostureNotice(posture);
|
|
317
|
+
const bannerLines = [
|
|
318
|
+
`GoodVibes daemon ${VERSION} — scan to pair a device (opens the web app signed in):`,
|
|
319
|
+
'',
|
|
320
|
+
` ${webOrigin.origin}`,
|
|
321
|
+
'',
|
|
322
|
+
...(offers.length > 0 ? ['Offers (each declinable in the web app):', ...formatPairingOffers(offers), ''] : []),
|
|
323
|
+
...(capabilities.length > 0 ? ['This device will get:', ...capabilities, ''] : []),
|
|
324
|
+
...(notice ? [notice, ''] : []),
|
|
325
|
+
qrString,
|
|
326
|
+
];
|
|
321
327
|
// eslint-disable-next-line no-console
|
|
322
|
-
console.log(
|
|
328
|
+
console.log(bannerLines.join('\n'));
|
|
323
329
|
}
|
|
324
330
|
|
|
325
331
|
void main().catch(async (error) => {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle.ts — resolve THIS binary's update-artifact identity for the SDK
|
|
3
|
+
* DaemonServer facade's auto-update lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* The SDK facade now runs the entire self-update loop itself when handed a
|
|
6
|
+
* DaemonUpdateArtifact ({version, execPath}): it compares the HOST binary's
|
|
7
|
+
* version against the release tags (no longer the version-blind sdk-package
|
|
8
|
+
* comparison that used to restart-loop), swaps only at an idle moment, keeps
|
|
9
|
+
* the outgoing file at `<path>.previous`, and leaves a receipt in the store it
|
|
10
|
+
* serves on /status. When the artifact is ABSENT, updates are host-managed —
|
|
11
|
+
* the facade runs no loop (the safe embedded default).
|
|
12
|
+
*
|
|
13
|
+
* The one guard the facade does NOT apply is install-kind: it must never swap a
|
|
14
|
+
* dev `bun run daemon` interpreter or a bun-global package install. This helper
|
|
15
|
+
* is that guard — it hands the facade an artifact ONLY for a compiled binary
|
|
16
|
+
* install, so a dev run resolves to `undefined` (host-managed, no loop) and
|
|
17
|
+
* only a real self-contained binary self-updates.
|
|
18
|
+
*/
|
|
19
|
+
import type { DaemonUpdateArtifact } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
20
|
+
import { VERSION } from '../version.ts';
|
|
21
|
+
import { detectInstallKind } from '../runtime/update-check.ts';
|
|
22
|
+
|
|
23
|
+
export interface ResolveDaemonUpdateArtifactOptions {
|
|
24
|
+
/** The executable to identify; defaults to process.execPath. */
|
|
25
|
+
readonly execPath?: string;
|
|
26
|
+
/** Injectable so tests pin a fixture version — never the live build VERSION. */
|
|
27
|
+
readonly version?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The update-artifact identity to hand the DaemonServer facade, or `undefined`
|
|
32
|
+
* for a non-binary install (dev/source or bun-global package) — in which case
|
|
33
|
+
* the facade keeps updates host-managed and runs no swap loop.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveDaemonUpdateArtifact(
|
|
36
|
+
options: ResolveDaemonUpdateArtifactOptions = {},
|
|
37
|
+
): DaemonUpdateArtifact | undefined {
|
|
38
|
+
const execPath = options.execPath ?? process.execPath;
|
|
39
|
+
if (detectInstallKind(execPath) !== 'binary') return undefined;
|
|
40
|
+
return { version: options.version ?? VERSION, execPath };
|
|
41
|
+
}
|
|
@@ -290,7 +290,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
290
290
|
ok: false,
|
|
291
291
|
exitCode: 1,
|
|
292
292
|
lines: [
|
|
293
|
-
`service install refused: a service is already installed
|
|
293
|
+
`service install refused: a service is already installed as ${LEGACY_SERVICE_UNIT_NAME}.service (the unit name the goodvibes install script manages).`,
|
|
294
294
|
legacyUnitNote(legacy, resolvedName),
|
|
295
295
|
`Installing this tool's ${resolvedName}.service alongside it risks two daemons competing for the same port.`,
|
|
296
296
|
],
|
|
@@ -313,10 +313,10 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
313
313
|
if (uninstalled.actionError) return failed('uninstall', uninstalled);
|
|
314
314
|
const extra: string[] = [];
|
|
315
315
|
if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
316
|
+
// This command only ever touches the TRACKED unit (whatever name/path
|
|
317
|
+
// actually resolved — not necessarily the SERVICE_NAME constant) above —
|
|
318
|
+
// say so explicitly when an install-script unit also exists, so its
|
|
319
|
+
// continued presence is never a silent surprise.
|
|
320
320
|
if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
|
|
321
321
|
return ok('uninstall', uninstalled, extra);
|
|
322
322
|
}
|
package/src/export/cost-utils.ts
CHANGED
|
@@ -15,18 +15,30 @@
|
|
|
15
15
|
// honest "unpriced" state instead of a silent $0.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
|
-
import type
|
|
18
|
+
import { computeUsageCostUsd, type CatalogModel, type ResolvedModelPricing } from '@pellux/goodvibes-sdk/platform/providers';
|
|
19
19
|
|
|
20
20
|
export interface ModelPricing {
|
|
21
21
|
input: number;
|
|
22
22
|
output: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Where a resolved price came from, for the render-side source distinction:
|
|
27
|
+
* 'user' = the manual pricing.modelPrices entry ("your price"), 'provider' =
|
|
28
|
+
* served by the provider API, 'catalog' = the live model catalog (dated),
|
|
29
|
+
* 'fallback' = this module's small built-in safety net.
|
|
30
|
+
*/
|
|
31
|
+
export type PricingSourceKind = 'user' | 'provider' | 'catalog' | 'fallback';
|
|
32
|
+
|
|
25
33
|
/** Result of a pricing lookup: the resolved (possibly zero) price, and whether it's real. */
|
|
26
34
|
export interface PricingResult {
|
|
27
35
|
pricing: ModelPricing;
|
|
28
|
-
/** False when no source
|
|
36
|
+
/** False when no source recognized the model — the zero pricing is a placeholder, not a real price. */
|
|
29
37
|
priced: boolean;
|
|
38
|
+
/** Which source priced the model (present only when priced). */
|
|
39
|
+
source?: PricingSourceKind;
|
|
40
|
+
/** ISO date (YYYY-MM-DD) of the catalog/provider snapshot the price came from, when known. */
|
|
41
|
+
asOf?: string;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
// Hand-maintained safety net, used only when the live catalog has no source
|
|
@@ -61,7 +73,7 @@ const STATIC_FALLBACK_PRICING: Record<string, ModelPricing> = {
|
|
|
61
73
|
|
|
62
74
|
// Module-level injection point. Kept as a bare singleton (not threaded through
|
|
63
75
|
// call sites) because the 9 existing consumers are pure functions with no
|
|
64
|
-
// ProviderRegistry in scope; see
|
|
76
|
+
// ProviderRegistry in scope; see brief for the tradeoff. Tests that
|
|
65
77
|
// never call setPricingSource() fall through to the static-fallback path,
|
|
66
78
|
// which keeps them isolated from catalog state by default.
|
|
67
79
|
let pricingSource: (() => readonly CatalogModel[]) | null = null;
|
|
@@ -71,6 +83,34 @@ export function setPricingSource(source: (() => readonly CatalogModel[]) | null)
|
|
|
71
83
|
pricingSource = source;
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
// The ONE model pricing resolver (manual pricing.modelPrices -> registration
|
|
87
|
+
// -> provider-served -> catalog -> honest unknown), wired from
|
|
88
|
+
// bootstrap-core.ts as ProviderRegistry.resolveModelPricing once the runtime
|
|
89
|
+
// exists. When set, it is consulted FIRST so a manual price the user sets is
|
|
90
|
+
// visible to every cost surface immediately; the legacy catalog/static chain
|
|
91
|
+
// below remains the fallback for resolver-unknown models and for surfaces
|
|
92
|
+
// (tests, headless) that never wire a registry.
|
|
93
|
+
let modelPricingResolver: ((modelId: string) => ResolvedModelPricing) | null = null;
|
|
94
|
+
|
|
95
|
+
/** Wire (or clear, with null) the registry's resolveModelPricing as the primary pricing source. */
|
|
96
|
+
export function setModelPricingResolver(resolver: ((modelId: string) => ResolvedModelPricing) | null): void {
|
|
97
|
+
modelPricingResolver = resolver;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* One-call bootstrap wiring: the live catalog as the legacy source AND the
|
|
102
|
+
* registry's ONE pricing resolver (manual pricing.modelPrices -> registration
|
|
103
|
+
* -> provider-served -> catalog -> honest unknown) as the primary, so every
|
|
104
|
+
* cost surface sees a manually-set price immediately and can name its source.
|
|
105
|
+
*/
|
|
106
|
+
export function wireCostPricing(registry: {
|
|
107
|
+
getRawCatalogModels(): readonly CatalogModel[];
|
|
108
|
+
resolveModelPricing(modelRef: string): ResolvedModelPricing;
|
|
109
|
+
}): void {
|
|
110
|
+
setPricingSource(() => registry.getRawCatalogModels());
|
|
111
|
+
setModelPricingResolver((modelId) => registry.resolveModelPricing(modelId));
|
|
112
|
+
}
|
|
113
|
+
|
|
74
114
|
function findInCatalog(modelId: string, models: readonly CatalogModel[]): ModelPricing | null {
|
|
75
115
|
const exact = models.find((m) => m.id === modelId);
|
|
76
116
|
if (exact) return exact.tier === 'free' ? { input: 0, output: 0 } : exact.pricing;
|
|
@@ -97,20 +137,60 @@ function findInStaticFallback(modelId: string): ModelPricing | null {
|
|
|
97
137
|
* header for the full resolution order.
|
|
98
138
|
*/
|
|
99
139
|
export function resolvePricing(modelId: string): PricingResult {
|
|
100
|
-
if (modelId.endsWith(':free')) return { pricing: { input: 0, output: 0 }, priced: true };
|
|
140
|
+
if (modelId.endsWith(':free')) return { pricing: { input: 0, output: 0 }, priced: true, source: 'catalog' };
|
|
141
|
+
|
|
142
|
+
// The ONE resolver first: manual price -> registration -> provider-served
|
|
143
|
+
// -> catalog. A 'priced' answer carries its source (and snapshot date when
|
|
144
|
+
// known) so render surfaces can say "your price" vs "catalog price, as of
|
|
145
|
+
// <date>". Unknown/subscription answers fall through to the legacy chain
|
|
146
|
+
// so nothing regresses while the catalog is still loading.
|
|
147
|
+
if (modelPricingResolver) {
|
|
148
|
+
const resolved = modelPricingResolver(modelId);
|
|
149
|
+
if (resolved.status === 'priced') {
|
|
150
|
+
return {
|
|
151
|
+
pricing: { input: resolved.rates.inputPerMTok, output: resolved.rates.outputPerMTok },
|
|
152
|
+
priced: true,
|
|
153
|
+
source: resolved.source,
|
|
154
|
+
...(resolved.asOf ? { asOf: resolved.asOf } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
101
158
|
|
|
102
159
|
const models = pricingSource?.() ?? [];
|
|
103
160
|
const catalogHit = findInCatalog(modelId, models);
|
|
104
|
-
if (catalogHit) return { pricing: catalogHit, priced: true };
|
|
161
|
+
if (catalogHit) return { pricing: catalogHit, priced: true, source: 'catalog' };
|
|
105
162
|
|
|
106
163
|
const fallbackHit = findInStaticFallback(modelId);
|
|
107
|
-
if (fallbackHit) return { pricing: fallbackHit, priced: true };
|
|
164
|
+
if (fallbackHit) return { pricing: fallbackHit, priced: true, source: 'fallback' };
|
|
108
165
|
|
|
109
166
|
// Genuinely unknown — no source recognizes this model. Report honestly
|
|
110
167
|
// rather than collapsing into the same zero a free model would return.
|
|
111
168
|
return { pricing: { input: 0, output: 0 }, priced: false };
|
|
112
169
|
}
|
|
113
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The render-side source distinction for a priced model: "your price" for a
|
|
173
|
+
* manual pricing.modelPrices entry, "catalog price, as of <date>" (or
|
|
174
|
+
* provider-served/built-in equivalents) otherwise. Null when the model is
|
|
175
|
+
* unpriced — callers render the explicit "price unknown" marker instead.
|
|
176
|
+
*/
|
|
177
|
+
export function describePricingSource(modelId: string): string | null {
|
|
178
|
+
const result = resolvePricing(modelId);
|
|
179
|
+
if (!result.priced) return null;
|
|
180
|
+
switch (result.source) {
|
|
181
|
+
case 'user':
|
|
182
|
+
return 'your price';
|
|
183
|
+
case 'provider':
|
|
184
|
+
return result.asOf ? `provider price, as of ${result.asOf}` : 'provider price';
|
|
185
|
+
case 'catalog':
|
|
186
|
+
return result.asOf ? `catalog price, as of ${result.asOf}` : 'catalog price';
|
|
187
|
+
case 'fallback':
|
|
188
|
+
return 'built-in price';
|
|
189
|
+
default:
|
|
190
|
+
return 'catalog price';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
114
194
|
/**
|
|
115
195
|
* getPricing — resolve USD-per-1M-token pricing for a model ID.
|
|
116
196
|
* Back-compat wrapper over resolvePricing(); returns zero for both genuinely
|
|
@@ -146,6 +226,20 @@ export function calcSessionCost(
|
|
|
146
226
|
cacheWrite: number,
|
|
147
227
|
modelId: string,
|
|
148
228
|
): number {
|
|
229
|
+
// Through the ONE resolver when wired: computeUsageCostUsd applies the
|
|
230
|
+
// source's explicit cache rates (or the published per-provider ratio)
|
|
231
|
+
// instead of billing cache traffic at the full input rate.
|
|
232
|
+
if (modelPricingResolver) {
|
|
233
|
+
const resolved = modelPricingResolver(modelId);
|
|
234
|
+
if (resolved.status === 'priced') {
|
|
235
|
+
return computeUsageCostUsd(resolved, {
|
|
236
|
+
inputTokens,
|
|
237
|
+
outputTokens,
|
|
238
|
+
cacheReadTokens: cacheRead,
|
|
239
|
+
cacheWriteTokens: cacheWrite,
|
|
240
|
+
}) ?? 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
149
243
|
const pricing = getPricing(modelId);
|
|
150
244
|
// Cache reads/writes count toward billable input side (same convention as panel)
|
|
151
245
|
const billableInput = inputTokens + cacheRead + cacheWrite;
|
|
@@ -195,3 +289,31 @@ export function readBudgetAlertUsd(configGet: (key: string) => unknown): number
|
|
|
195
289
|
const parsed = typeof raw === 'number' ? raw : Number(raw);
|
|
196
290
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : BUDGET_ALERT_USD_DEFAULT;
|
|
197
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The SDK settings key holding the user's manual per-model prices — the
|
|
295
|
+
* highest-precedence source in the ONE pricing resolver. Object map of
|
|
296
|
+
* `provider:model` -> { input, output, cacheRead?, cacheWrite? } in USD per
|
|
297
|
+
* 1M tokens.
|
|
298
|
+
*/
|
|
299
|
+
export const MODEL_PRICES_CONFIG_KEY = 'pricing.modelPrices';
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Persist a manual price for one `provider:model` key into
|
|
303
|
+
* pricing.modelPrices, preserving every other entry. The registry's resolver
|
|
304
|
+
* reads the key live per call, so the new price is visible to every cost
|
|
305
|
+
* surface on the next render — no restart.
|
|
306
|
+
*/
|
|
307
|
+
export function writeManualModelPrice(
|
|
308
|
+
config: BudgetAlertConfigAccess,
|
|
309
|
+
modelKey: string,
|
|
310
|
+
price: { input: number; output: number },
|
|
311
|
+
): void {
|
|
312
|
+
const current = config.get(MODEL_PRICES_CONFIG_KEY);
|
|
313
|
+
const map: Record<string, unknown> =
|
|
314
|
+
current && typeof current === 'object' && !Array.isArray(current)
|
|
315
|
+
? { ...(current as Record<string, unknown>) }
|
|
316
|
+
: {};
|
|
317
|
+
map[modelKey] = { input: price.input, output: price.output };
|
|
318
|
+
config.set(MODEL_PRICES_CONFIG_KEY, map);
|
|
319
|
+
}
|
|
@@ -19,7 +19,7 @@ export interface AutocompleteState {
|
|
|
19
19
|
results: AutocompleteResult[];
|
|
20
20
|
selectedIndex: number;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* (item 4): how many leading entries of `results` belong to the
|
|
23
23
|
* curated "common" tier — only meaningful when `query === ''` (0
|
|
24
24
|
* otherwise, since a typed filter searches everything and the common/rest
|
|
25
25
|
* split no longer applies). The renderer uses this to draw a separator
|
|
@@ -56,7 +56,7 @@ export class AutocompleteEngine {
|
|
|
56
56
|
this.state.query = query;
|
|
57
57
|
this.state.results = this.registry.fuzzyMatch(query);
|
|
58
58
|
this.state.active = true;
|
|
59
|
-
//
|
|
59
|
+
// fuzzyMatch('') sorts the curated common tier first (score 2),
|
|
60
60
|
// then the alphabetical rest (score 1) — both tiers alphabetical within
|
|
61
61
|
// themselves. Count the leading common run to tell the renderer where to
|
|
62
62
|
// draw the separator. A non-empty query means every command was scored on
|
|
@@ -56,7 +56,7 @@ export interface CommandRuntimeState {
|
|
|
56
56
|
* Cumulative count of direct terminal writes the output guard intercepted
|
|
57
57
|
* this session — surfaced by /debug. Optional so the SDK's MutableRuntimeState
|
|
58
58
|
* (which has no such field) stays assignable; the guard sets it lazily on the
|
|
59
|
-
* shared runtime object. (
|
|
59
|
+
* shared runtime object. (item 1a.)
|
|
60
60
|
*/
|
|
61
61
|
terminalWritesIntercepted?: number;
|
|
62
62
|
}
|
|
@@ -81,7 +81,28 @@ export interface CommandUiActions {
|
|
|
81
81
|
marker?: string;
|
|
82
82
|
};
|
|
83
83
|
executeCommand?: (name: string, args: string[]) => Promise<boolean>;
|
|
84
|
+
/** Arm the one-key jump to a spawned CI fix-session (the surface attaches on the jump key; the user never retypes an id). */
|
|
85
|
+
armFixSessionAttach?: (fixSessionId: string) => void;
|
|
84
86
|
cancelGeneration?: () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Cancel JUST the currently-running tool call (the live transcript row),
|
|
89
|
+
* leaving the turn to continue — the local-session equivalent of the
|
|
90
|
+
* sessions.toolCalls.cancel wire verb over the in-process orchestrator.
|
|
91
|
+
* Returns true when a running call was found and cancelled.
|
|
92
|
+
*/
|
|
93
|
+
cancelToolCall?: () => boolean;
|
|
94
|
+
/** The still-undelivered mid-turn messages, in delivery order (the /queue editable list). */
|
|
95
|
+
listQueuedMessages?: () => ReadonlyArray<{ id: string; queuedAt: number; text: string }>;
|
|
96
|
+
/** Replace a still-queued message's text; false once delivered (immutable). */
|
|
97
|
+
editQueuedMessage?: (id: string, text: string) => boolean;
|
|
98
|
+
/** Remove a still-queued message before delivery; false once delivered. */
|
|
99
|
+
deleteQueuedMessage?: (id: string) => boolean;
|
|
100
|
+
/** Toggle the drill-in of the "used N memories" provenance chip (memory.showProvenance). */
|
|
101
|
+
toggleMemoryProvenance?: () => void;
|
|
102
|
+
/** The current host sleep-ownership projection (power.status.get, flattened) — chip, held-because, lid-split. */
|
|
103
|
+
getPowerState?: () => import('../core/power-status.ts').PowerSurfaceState;
|
|
104
|
+
/** Set the daemon-held keep-awake toggle (power.keepAwake.set), acquiring/releasing the inhibitor live; returns the new state. */
|
|
105
|
+
setKeepAwake?: (enabled: boolean) => Promise<import('../core/power-status.ts').PowerSurfaceState>;
|
|
85
106
|
/** True while an LLM turn is actively streaming. Used to give Escape
|
|
86
107
|
* cancel-turn precedence over a focused panel's own escape handling. */
|
|
87
108
|
isGenerating?: () => boolean;
|
|
@@ -139,7 +160,7 @@ export interface CommandShellUiOpeners {
|
|
|
139
160
|
) => void;
|
|
140
161
|
openSettingsModal?: (target?: string) => void;
|
|
141
162
|
/**
|
|
142
|
-
* Open a MIGRATE-TO-MODAL surface by name (
|
|
163
|
+
* Open a MIGRATE-TO-MODAL surface by name (purge skeleton — B
|
|
143
164
|
* command runtimes call this instead of openCommandPanel once their panel
|
|
144
165
|
* is converted to a ModalFactory config). Threaded from ui-openers.ts the
|
|
145
166
|
* same way openSettingsModal is.
|
|
@@ -157,12 +178,21 @@ export interface CommandShellUiOpeners {
|
|
|
157
178
|
*/
|
|
158
179
|
openCommandPalette?: () => void;
|
|
159
180
|
/**
|
|
160
|
-
*
|
|
181
|
+
* Command name -> reference-category label, from the same single source of
|
|
182
|
+
* truth as the generated command reference (categorizeBuiltinCommands),
|
|
183
|
+
* memoized by the shell. Lets registry-driven surfaces built inside command
|
|
184
|
+
* handlers (/help) group by category without importing the aggregation
|
|
185
|
+
* module — a static import there would cycle (commands.ts registers those
|
|
186
|
+
* very handlers).
|
|
187
|
+
*/
|
|
188
|
+
getCommandCategories?: () => ReadonlyMap<string, string>;
|
|
189
|
+
/**
|
|
190
|
+
* Open (and optionally focus) a panel. focus rule: the command path is
|
|
161
191
|
* "the user is mid-command-flow" — opening a panel this way leaves keyboard
|
|
162
192
|
* focus in the composer by default. Pass `{ focus: true }` for a caller that
|
|
163
193
|
* genuinely wants to grab focus (chords use panelManager.focusPanels()
|
|
164
194
|
* directly instead of this method, so no current call site needs it — but
|
|
165
|
-
* the intent is explicit rather than implicit here). `target`
|
|
195
|
+
* the intent is explicit rather than implicit here). `target` is a
|
|
166
196
|
* fleet deep-link jump target forwarded to PanelManager.open.
|
|
167
197
|
*/
|
|
168
198
|
showPanel?: (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget, opts?: { focus?: boolean }) => void;
|
|
@@ -207,7 +237,7 @@ export interface CommandSessionServices {
|
|
|
207
237
|
* conversation's current message history. Call after a session resume
|
|
208
238
|
* replays historical messages, before the next render, so the footer's
|
|
209
239
|
* token counter reflects the resumed session's real usage instead of a
|
|
210
|
-
* fresh Orchestrator's zeroed default
|
|
240
|
+
* fresh Orchestrator's zeroed default.
|
|
211
241
|
*/
|
|
212
242
|
readonly hydrateSessionUsage?: () => void;
|
|
213
243
|
/** The orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
|
|
@@ -221,7 +251,7 @@ export interface CommandSessionServices {
|
|
|
221
251
|
/**
|
|
222
252
|
* The MAIN interactive session's per-turn passive-injection
|
|
223
253
|
* honesty ring — `Orchestrator.getTurnInjections()`, the main-session
|
|
224
|
-
* counterpart to `AgentRecord.turnInjections
|
|
254
|
+
* counterpart to `AgentRecord.turnInjections`. `/recall injections`
|
|
225
255
|
* with no agent id reads this. Optional so command contexts built without an
|
|
226
256
|
* orchestrator (headless, tests) fall back to the honest empty state.
|
|
227
257
|
*/
|
|
@@ -262,7 +292,9 @@ export interface CommandWorkspaceServices
|
|
|
262
292
|
export interface CommandPlatformConfigServices {
|
|
263
293
|
readonly config: DeepReadonly<GoodVibesConfig>;
|
|
264
294
|
readonly configManager: ConfigManager;
|
|
265
|
-
/**
|
|
295
|
+
/** Per-device pairing tokens — backs the /devices command family. */
|
|
296
|
+
readonly pairingTokens?: import('@pellux/goodvibes-sdk/platform/pairing').PairingTokenManager;
|
|
297
|
+
/** Capability gate runtime — feature states derive from domain settings keys; surfaces read live/pending state here. */
|
|
266
298
|
readonly featureFlagManager?: FeatureFlagManager;
|
|
267
299
|
readonly voiceProviderRegistry?: VoiceProviderRegistry;
|
|
268
300
|
readonly voiceService?: VoiceService;
|
|
@@ -325,11 +357,18 @@ export interface CommandContext
|
|
|
325
357
|
readonly mcpApi?: McpApi;
|
|
326
358
|
readonly opsApi?: OpsApi;
|
|
327
359
|
readonly transport?: DirectTransport;
|
|
360
|
+
/**
|
|
361
|
+
* Durable user-origin permission rules (the persistent form of remembered
|
|
362
|
+
* approvals). The same in-process store the permission evaluator reads, so
|
|
363
|
+
* /permissions rules lists and revokes rules that take effect live. Served
|
|
364
|
+
* by the daemon's permissions.rules.list/.delete verbs to other surfaces.
|
|
365
|
+
*/
|
|
366
|
+
readonly userPermissionRuleStore?: import('@pellux/goodvibes-sdk/platform/permissions').UserPermissionRuleStore;
|
|
328
367
|
};
|
|
329
368
|
}
|
|
330
369
|
|
|
331
370
|
/**
|
|
332
|
-
*
|
|
371
|
+
* palette curation (item 4): the "common" first tier the slash-command
|
|
333
372
|
* autocomplete dropdown shows before the alphabetical rest when it opens with
|
|
334
373
|
* no filter typed yet (bare '/') — the "132-command palette unranked"
|
|
335
374
|
* evaluator finding. Typed filtering (any non-empty query) is completely
|
|
@@ -379,6 +418,13 @@ export class CommandRegistry {
|
|
|
379
418
|
* statically by the alias lint test).
|
|
380
419
|
*/
|
|
381
420
|
register(command: SlashCommand): void {
|
|
421
|
+
// Every registered command is rendered with its description in /help, the
|
|
422
|
+
// command palette, and the generated reference (docs/commands-reference.md).
|
|
423
|
+
// A blank description would ship an unexplained row in all three, so it
|
|
424
|
+
// fails fast here — the release gate red-tests this invariant.
|
|
425
|
+
if (!command.description || command.description.trim().length === 0) {
|
|
426
|
+
throw new Error(`Command registration for "${command.name}" has no description — every command must describe itself.`);
|
|
427
|
+
}
|
|
382
428
|
const existingByName = this.commands.get(command.name) ?? this.aliasIndex.get(command.name);
|
|
383
429
|
if (existingByName) {
|
|
384
430
|
throw new Error(
|
|
@@ -447,7 +493,7 @@ export class CommandRegistry {
|
|
|
447
493
|
}
|
|
448
494
|
|
|
449
495
|
if (bestScore > 0 || q === '') {
|
|
450
|
-
//
|
|
496
|
+
// with no query yet, rank the curated common tier (score 2)
|
|
451
497
|
// ahead of everything else (score 1) — the tie-break below then sorts
|
|
452
498
|
// each tier alphabetically, so the result is "common tier, then the
|
|
453
499
|
// alphabetical rest" rather than one flat alphabetical list.
|