@pellux/goodvibes-tui 1.16.1 → 1.17.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 +89 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1091 -271
- 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/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/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +4 -4
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/stream-event-wiring.ts +8 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +8 -8
- package/src/daemon/cli.ts +20 -5
- 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 +34 -9
- 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/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/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/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- 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-runtime.ts +2 -2
- package/src/input/commands.ts +0 -2
- package/src/input/config-modal-types.ts +3 -3
- package/src/input/config-modal.ts +10 -11
- 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 +6 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +2 -2
- 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 +22 -21
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +23 -16
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +48 -48
- 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 +29 -10
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/builtin-modals.ts +10 -10
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +5 -5
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-panel-format.ts +44 -8
- package/src/panels/fleet-panel.ts +16 -16
- package/src/panels/fleet-read-model.ts +52 -13
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +3 -3
- package/src/panels/fleet-tabs.ts +13 -14
- 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/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 +4 -4
- 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 +2 -2
- 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 +1 -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/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/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 +14 -5
- package/src/renderer/settings-modal.ts +127 -39
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +6 -1
- package/src/runtime/bootstrap-command-context.ts +4 -1
- package/src/runtime/bootstrap-command-parts.ts +6 -4
- package/src/runtime/bootstrap-core.ts +35 -35
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +6 -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 +55 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/index.ts +4 -21
- 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/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 +81 -82
- 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 +3 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +102 -6
- 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
|
@@ -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,6 +81,8 @@ 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;
|
|
85
87
|
/** True while an LLM turn is actively streaming. Used to give Escape
|
|
86
88
|
* cancel-turn precedence over a focused panel's own escape handling. */
|
|
@@ -139,7 +141,7 @@ export interface CommandShellUiOpeners {
|
|
|
139
141
|
) => void;
|
|
140
142
|
openSettingsModal?: (target?: string) => void;
|
|
141
143
|
/**
|
|
142
|
-
* Open a MIGRATE-TO-MODAL surface by name (
|
|
144
|
+
* Open a MIGRATE-TO-MODAL surface by name (purge skeleton — B
|
|
143
145
|
* command runtimes call this instead of openCommandPanel once their panel
|
|
144
146
|
* is converted to a ModalFactory config). Threaded from ui-openers.ts the
|
|
145
147
|
* same way openSettingsModal is.
|
|
@@ -157,12 +159,21 @@ export interface CommandShellUiOpeners {
|
|
|
157
159
|
*/
|
|
158
160
|
openCommandPalette?: () => void;
|
|
159
161
|
/**
|
|
160
|
-
*
|
|
162
|
+
* Command name -> reference-category label, from the same single source of
|
|
163
|
+
* truth as the generated command reference (categorizeBuiltinCommands),
|
|
164
|
+
* memoized by the shell. Lets registry-driven surfaces built inside command
|
|
165
|
+
* handlers (/help) group by category without importing the aggregation
|
|
166
|
+
* module — a static import there would cycle (commands.ts registers those
|
|
167
|
+
* very handlers).
|
|
168
|
+
*/
|
|
169
|
+
getCommandCategories?: () => ReadonlyMap<string, string>;
|
|
170
|
+
/**
|
|
171
|
+
* Open (and optionally focus) a panel. focus rule: the command path is
|
|
161
172
|
* "the user is mid-command-flow" — opening a panel this way leaves keyboard
|
|
162
173
|
* focus in the composer by default. Pass `{ focus: true }` for a caller that
|
|
163
174
|
* genuinely wants to grab focus (chords use panelManager.focusPanels()
|
|
164
175
|
* directly instead of this method, so no current call site needs it — but
|
|
165
|
-
* the intent is explicit rather than implicit here). `target`
|
|
176
|
+
* the intent is explicit rather than implicit here). `target` is a
|
|
166
177
|
* fleet deep-link jump target forwarded to PanelManager.open.
|
|
167
178
|
*/
|
|
168
179
|
showPanel?: (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget, opts?: { focus?: boolean }) => void;
|
|
@@ -207,7 +218,7 @@ export interface CommandSessionServices {
|
|
|
207
218
|
* conversation's current message history. Call after a session resume
|
|
208
219
|
* replays historical messages, before the next render, so the footer's
|
|
209
220
|
* token counter reflects the resumed session's real usage instead of a
|
|
210
|
-
* fresh Orchestrator's zeroed default
|
|
221
|
+
* fresh Orchestrator's zeroed default.
|
|
211
222
|
*/
|
|
212
223
|
readonly hydrateSessionUsage?: () => void;
|
|
213
224
|
/** The orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
|
|
@@ -221,7 +232,7 @@ export interface CommandSessionServices {
|
|
|
221
232
|
/**
|
|
222
233
|
* The MAIN interactive session's per-turn passive-injection
|
|
223
234
|
* honesty ring — `Orchestrator.getTurnInjections()`, the main-session
|
|
224
|
-
* counterpart to `AgentRecord.turnInjections
|
|
235
|
+
* counterpart to `AgentRecord.turnInjections`. `/recall injections`
|
|
225
236
|
* with no agent id reads this. Optional so command contexts built without an
|
|
226
237
|
* orchestrator (headless, tests) fall back to the honest empty state.
|
|
227
238
|
*/
|
|
@@ -262,7 +273,7 @@ export interface CommandWorkspaceServices
|
|
|
262
273
|
export interface CommandPlatformConfigServices {
|
|
263
274
|
readonly config: DeepReadonly<GoodVibesConfig>;
|
|
264
275
|
readonly configManager: ConfigManager;
|
|
265
|
-
/**
|
|
276
|
+
/** Capability gate runtime — feature states derive from domain settings keys; surfaces read live/pending state here. */
|
|
266
277
|
readonly featureFlagManager?: FeatureFlagManager;
|
|
267
278
|
readonly voiceProviderRegistry?: VoiceProviderRegistry;
|
|
268
279
|
readonly voiceService?: VoiceService;
|
|
@@ -325,11 +336,18 @@ export interface CommandContext
|
|
|
325
336
|
readonly mcpApi?: McpApi;
|
|
326
337
|
readonly opsApi?: OpsApi;
|
|
327
338
|
readonly transport?: DirectTransport;
|
|
339
|
+
/**
|
|
340
|
+
* Durable user-origin permission rules (the persistent form of remembered
|
|
341
|
+
* approvals). The same in-process store the permission evaluator reads, so
|
|
342
|
+
* /permissions rules lists and revokes rules that take effect live. Served
|
|
343
|
+
* by the daemon's permissions.rules.list/.delete verbs to other surfaces.
|
|
344
|
+
*/
|
|
345
|
+
readonly userPermissionRuleStore?: import('@pellux/goodvibes-sdk/platform/permissions').UserPermissionRuleStore;
|
|
328
346
|
};
|
|
329
347
|
}
|
|
330
348
|
|
|
331
349
|
/**
|
|
332
|
-
*
|
|
350
|
+
* palette curation (item 4): the "common" first tier the slash-command
|
|
333
351
|
* autocomplete dropdown shows before the alphabetical rest when it opens with
|
|
334
352
|
* no filter typed yet (bare '/') — the "132-command palette unranked"
|
|
335
353
|
* evaluator finding. Typed filtering (any non-empty query) is completely
|
|
@@ -379,6 +397,13 @@ export class CommandRegistry {
|
|
|
379
397
|
* statically by the alias lint test).
|
|
380
398
|
*/
|
|
381
399
|
register(command: SlashCommand): void {
|
|
400
|
+
// Every registered command is rendered with its description in /help, the
|
|
401
|
+
// command palette, and the generated reference (docs/commands-reference.md).
|
|
402
|
+
// A blank description would ship an unexplained row in all three, so it
|
|
403
|
+
// fails fast here — the release gate red-tests this invariant.
|
|
404
|
+
if (!command.description || command.description.trim().length === 0) {
|
|
405
|
+
throw new Error(`Command registration for "${command.name}" has no description — every command must describe itself.`);
|
|
406
|
+
}
|
|
382
407
|
const existingByName = this.commands.get(command.name) ?? this.aliasIndex.get(command.name);
|
|
383
408
|
if (existingByName) {
|
|
384
409
|
throw new Error(
|
|
@@ -447,7 +472,7 @@ export class CommandRegistry {
|
|
|
447
472
|
}
|
|
448
473
|
|
|
449
474
|
if (bestScore > 0 || q === '') {
|
|
450
|
-
//
|
|
475
|
+
// with no query yet, rank the curated common tier (score 2)
|
|
451
476
|
// ahead of everything else (score 1) — the tie-break below then sorts
|
|
452
477
|
// each tier alphabetically, so the result is "common tier, then the
|
|
453
478
|
// alphabetical rest" rather than one flat alphabetical list.
|
|
@@ -155,8 +155,12 @@ export function registerCiRuntimeCommands(registry: CommandRegistry): void {
|
|
|
155
155
|
const result = await rpc.sdk.operator.invoke('ci.watches.run', { watchId: runId });
|
|
156
156
|
const lines = [renderReport(result.report)];
|
|
157
157
|
lines.push(`notified: ${result.notified ? 'yes' : 'no'}${result.notificationId ? ` (${result.notificationId})` : ''}`);
|
|
158
|
-
lines.push(`fix-session triggered: ${result.fixSessionTriggered ? 'yes' : 'no'}
|
|
158
|
+
lines.push(`fix-session triggered: ${result.fixSessionTriggered ? 'yes' : 'no'}`);
|
|
159
159
|
ctx.print(lines.join('\n'));
|
|
160
|
+
// Route the spawned session through the shared one-key jump affordance
|
|
161
|
+
// instead of printing the id for the user to retype — same attach flow
|
|
162
|
+
// as the accepted fix-this card.
|
|
163
|
+
if (result.fixSessionId) ctx.armFixSessionAttach?.(result.fixSessionId);
|
|
160
164
|
} catch (error) {
|
|
161
165
|
ctx.print(`[ci watches run] ${describeOperatorRpcError(error)}`);
|
|
162
166
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Explicit-query + transparency surface over the repo source-tree code index
|
|
5
5
|
// (CodeIndexStore, @pellux/goodvibes-sdk/platform/state, landed on SDK main
|
|
6
|
-
//
|
|
6
|
+
// with explicit-query build/status/search) via the TUI's own wiring in
|
|
7
7
|
// src/runtime/code-index-services.ts, threaded through
|
|
8
8
|
// CommandContext.session.codeIndexStore exactly like wrfcController/
|
|
9
9
|
// workstreamEngine already are (bootstrap-command-context.ts).
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// node while it runs), status (counts, skips, degradation state, last
|
|
15
15
|
// build), search <query> (explicit retrieval, results labeled
|
|
16
16
|
// 'lexical'|'semantic' honestly — never implied as more precise than they
|
|
17
|
-
// are).
|
|
18
|
-
//
|
|
17
|
+
// are). Passive auto-injection into coding turns and tool-site incremental
|
|
18
|
+
// reindex followed later; `status` surfaces both honestly (the
|
|
19
19
|
// auto-injection flag+setting gate, and the last reindex activity).
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
@@ -35,7 +35,7 @@ function formatBytes(bytes: number): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Honest one-line auto-injection state
|
|
38
|
+
* Honest one-line auto-injection state: both the (default-off)
|
|
39
39
|
* `agent-passive-code-injection` feature flag AND storage.codeIndexEnabled must
|
|
40
40
|
* be on for passive code injection to run — state which is off, and why.
|
|
41
41
|
*/
|
|
@@ -49,7 +49,7 @@ function renderAutoInjectionState(flagEnabled: boolean, settingEnabled: boolean)
|
|
|
49
49
|
return ` auto-injection: off (${reasons.join('; ')})`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Honest last-reindex activity line (
|
|
52
|
+
/** Honest last-reindex activity line (tool-site incremental reindex). */
|
|
53
53
|
function renderReindexActivity(activity: CodeIndexReindexActivity | null, now: number): string {
|
|
54
54
|
if (!activity) return ' last reindex: none this session (writes/edits reindex touched files once the index is built)';
|
|
55
55
|
const agoSec = Math.max(0, Math.round((now - activity.at) / 1000));
|
|
@@ -82,7 +82,7 @@ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<Config
|
|
|
82
82
|
lines.push(
|
|
83
83
|
` auto-build on startup: ${autoStart ? 'on' : 'off'} (storage.codeIndexEnabled, default off — /config to change)`,
|
|
84
84
|
);
|
|
85
|
-
//
|
|
85
|
+
// Passive code auto-injection state (flag + setting) and the last tool-site reindex.
|
|
86
86
|
lines.push(renderAutoInjectionState(extras.flagEnabled, autoStart));
|
|
87
87
|
lines.push(renderReindexActivity(extras.reindexActivity, extras.now ?? Date.now()));
|
|
88
88
|
lines.push(
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from './cost-attribution-format.ts';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* /cost budget <usd> — makes the CostTrackerPanel's budget alert real
|
|
14
|
+
* /cost budget <usd> — makes the CostTrackerPanel's budget alert real.
|
|
15
15
|
* Opens the cost panel (creating it via the registered factory if not already
|
|
16
16
|
* open) and sets its budget threshold directly, so the meter+alert already
|
|
17
17
|
* built into CostTrackerPanel.render() (:266-290) actually fires. 0 disables
|
|
@@ -48,7 +48,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
48
48
|
const readModels = requireReadModels(ctx);
|
|
49
49
|
|
|
50
50
|
if (sub === 'open' || sub === 'panel' || sub === 'provider') {
|
|
51
|
-
ctx.openModal?.('providers-modal'); //
|
|
51
|
+
ctx.openModal?.('providers-modal'); // provider-health panel -> config modal
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -477,7 +477,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
|
|
|
477
477
|
|
|
478
478
|
ctx.print([
|
|
479
479
|
'Health Review',
|
|
480
|
-
//
|
|
480
|
+
// item 3: /health stays a cross-domain transcript report (it
|
|
481
481
|
// genuinely spans settings/accounts/auth/sandbox/etc — no single
|
|
482
482
|
// modal owns this data), but the providers domain DOES have one, so
|
|
483
483
|
// point at it honestly. "/provider" (no domain) is a DIFFERENT
|
|
@@ -55,7 +55,7 @@ export function registerIntelligenceRuntimeCommands(registry: CommandRegistry):
|
|
|
55
55
|
async handler(args, ctx) {
|
|
56
56
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
57
57
|
if (sub === 'panel' || sub === 'open') {
|
|
58
|
-
//
|
|
58
|
+
// (the purge): 'intelligence'/IntelligencePanel was
|
|
59
59
|
// DELETE-disposition (no surviving human surface — its read model
|
|
60
60
|
// stays and still backs every subcommand below). There is no alias
|
|
61
61
|
// to resolve through, so this prints an honest notice and opens
|
|
@@ -10,7 +10,7 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
|
|
|
10
10
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
11
11
|
const auth = requireLocalUserAuthManager(ctx);
|
|
12
12
|
if (sub === 'panel' || sub === 'open') {
|
|
13
|
-
//
|
|
13
|
+
// browse view moved to local-auth-modal. The LocalAuthPanel itself is
|
|
14
14
|
// kept (masked password-entry host) but is no longer the /local-auth panel
|
|
15
15
|
// destination — masked entry is reached via add-user/rotate-password below.
|
|
16
16
|
ctx.openModal?.('local-auth-modal');
|
|
@@ -4,29 +4,35 @@ import { writeFile, unlink } from 'node:fs/promises';
|
|
|
4
4
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
5
5
|
import { fetchModelContextWindows } from '@pellux/goodvibes-sdk/platform/discovery';
|
|
6
6
|
import type { CustomProviderConfig } from '@pellux/goodvibes-sdk/platform/providers';
|
|
7
|
-
import { requireProviderApi, requireShellPaths } from './runtime-services.ts';
|
|
7
|
+
import { requireProviderApi, requireSecretsManager, requireShellPaths } from './runtime-services.ts';
|
|
8
|
+
import { ensureProviderKeyThenSelect } from '../provider-key-intake.ts';
|
|
8
9
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
10
|
|
|
10
11
|
function isValidProviderName(name: string): boolean {
|
|
11
12
|
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Conventional secrets-store key a custom provider reads its API key from. */
|
|
16
|
+
function customProviderKeyName(providerName: string): string {
|
|
17
|
+
return `${providerName.replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}_API_KEY`;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export function registerLocalProviderRuntimeCommands(registry: CommandRegistry): void {
|
|
15
21
|
registry.register({
|
|
16
22
|
name: 'provider',
|
|
17
23
|
aliases: ['p'],
|
|
18
24
|
description: 'Switch provider or manage custom providers (add/remove)',
|
|
19
|
-
usage: '[add <name> <baseURL>
|
|
25
|
+
usage: '[add <name> <baseURL> | remove <name> | <provider-name>]',
|
|
20
26
|
argsHint: '[add|remove|name]',
|
|
21
27
|
async handler(args, ctx) {
|
|
22
28
|
const shellPaths = requireShellPaths(ctx);
|
|
23
29
|
if (args[0] === 'add') {
|
|
24
30
|
const addArgs = args.slice(1);
|
|
25
31
|
if (addArgs.length < 2) {
|
|
26
|
-
ctx.print('Usage: /provider add <name> <baseURL
|
|
32
|
+
ctx.print('Usage: /provider add <name> <baseURL>\nExample: /provider add my-server http://192.168.0.85:8001/v1\nAfter the provider is created you can enter its API key with the keystrokes masked (or skip it for keyless endpoints).');
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
|
-
const [name, baseURL
|
|
35
|
+
const [name, baseURL] = addArgs;
|
|
30
36
|
if (!isValidProviderName(name)) {
|
|
31
37
|
ctx.print('Error: Provider name must contain only letters, numbers, hyphens, and underscores.');
|
|
32
38
|
return;
|
|
@@ -51,7 +57,6 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
|
|
|
51
57
|
const controller = new AbortController();
|
|
52
58
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
53
59
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
54
|
-
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
55
60
|
const res = await fetch(`${baseURL}/models`, { signal: controller.signal, headers });
|
|
56
61
|
clearTimeout(timeoutId);
|
|
57
62
|
if (res.ok) {
|
|
@@ -91,12 +96,16 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
|
|
|
91
96
|
contextWindow: contextWindows[id] ?? 8192,
|
|
92
97
|
capabilities: { toolCalling: true, codeEditing: true, reasoning: false, multimodal: false },
|
|
93
98
|
}));
|
|
99
|
+
// The key is never embedded in the provider JSON. The config references
|
|
100
|
+
// it by name (apiKeyEnv) and the actual value lands in the secrets
|
|
101
|
+
// manager below, resolved live at request time.
|
|
102
|
+
const keyName = customProviderKeyName(name);
|
|
94
103
|
const config: CustomProviderConfig = {
|
|
95
104
|
name,
|
|
96
105
|
displayName: name,
|
|
97
106
|
type: 'openai-compat',
|
|
98
107
|
baseURL,
|
|
99
|
-
|
|
108
|
+
apiKeyEnv: keyName,
|
|
100
109
|
models,
|
|
101
110
|
};
|
|
102
111
|
try {
|
|
@@ -107,6 +116,32 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
|
|
|
107
116
|
return;
|
|
108
117
|
}
|
|
109
118
|
ctx.print(`Provider '${name}' added with ${models.length} model(s):\n${discoveredModelIds.length > 0 ? discoveredModelIds.map((id) => ` • ${id} (${(contextWindows[id] ?? 8192).toLocaleString()} ctx)`).join('\n') : ` • ${defaultModel} (starter entry)`}\nThe file watcher will auto-register it shortly.`);
|
|
119
|
+
// Capture the key with the keystrokes masked, straight into the secrets
|
|
120
|
+
// manager — never a cleartext argument, never written into the JSON.
|
|
121
|
+
if (ctx.beginConcealedInput) {
|
|
122
|
+
const secretsManager = requireSecretsManager(ctx);
|
|
123
|
+
ctx.print(`Enter the API key for provider '${name}' (input is masked; press Enter to store, Esc to skip if the endpoint needs no key).`);
|
|
124
|
+
ctx.beginConcealedInput({
|
|
125
|
+
label: name,
|
|
126
|
+
onSubmit: (value) => {
|
|
127
|
+
if (value.length === 0) {
|
|
128
|
+
ctx.print(`No key stored for '${name}' (endpoint used without a key).`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
void (async () => {
|
|
132
|
+
try {
|
|
133
|
+
await secretsManager.set(keyName, value, { scope: 'user', medium: 'secure' });
|
|
134
|
+
ctx.print(`Stored the API key for '${name}' (value hidden). It resolves live for this provider — no restart needed.`);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
ctx.print(`Could not store the key for '${name}': ${summarizeError(error)}`);
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
},
|
|
140
|
+
onCancel: () => {
|
|
141
|
+
ctx.print(`No key stored for '${name}' (endpoint used without a key).`);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
110
145
|
return;
|
|
111
146
|
}
|
|
112
147
|
|
|
@@ -157,10 +192,14 @@ export function registerLocalProviderRuntimeCommands(registry: CommandRegistry):
|
|
|
157
192
|
}
|
|
158
193
|
try {
|
|
159
194
|
const selected = await providerApi.selectModel(match.registryKey);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
ctx
|
|
163
|
-
|
|
195
|
+
// Unconfigured provider selected → capture its key inline, then finish
|
|
196
|
+
// the switch so it lands ready to chat.
|
|
197
|
+
ensureProviderKeyThenSelect(ctx, selected.providerId, () => {
|
|
198
|
+
ctx.session.runtime.model = selected.registryKey;
|
|
199
|
+
ctx.session.runtime.provider = selected.providerId;
|
|
200
|
+
ctx.platform.configManager.set('provider.model', selected.registryKey);
|
|
201
|
+
ctx.print(`Switched to provider: ${selected.providerId} (model: ${selected.modelId})`);
|
|
202
|
+
});
|
|
164
203
|
} catch (e) {
|
|
165
204
|
ctx.print(`Error: ${summarizeError(e)}`);
|
|
166
205
|
}
|