@pellux/goodvibes-tui 1.16.0 → 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 +95 -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/onboarding/onboarding-wizard.ts +22 -8
- 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 +102 -24
- 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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification dispatch — the production wiring that finally gives the
|
|
3
|
+
* panel_only notification target a live producer.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's NotificationRouter decides where each domain notification goes
|
|
6
|
+
* (conversation / status_bar / panel_only) and collapses bursts and batches.
|
|
7
|
+
* Nothing in the running app routed real notifications before this: the
|
|
8
|
+
* panel_only feed (panels/notifications-feed.ts) and its panel existed with no
|
|
9
|
+
* upstream. This module builds the router, records every panel_only decision
|
|
10
|
+
* (including burst-collapsed ones) into the shared feed, and bridges the
|
|
11
|
+
* runtime event bus so real domain events become notifications.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createNotificationRouter, type NotificationRouter } from '@pellux/goodvibes-sdk/platform/runtime/ui';
|
|
15
|
+
import type { Notification, RoutingDecision, RuntimeEventBus, RuntimeEventDomain } from '@/runtime/index.ts';
|
|
16
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
17
|
+
import { getSharedNotificationFeed, type PanelNotificationFeed } from '../panels/notifications-feed.ts';
|
|
18
|
+
|
|
19
|
+
export interface NotificationDispatcher {
|
|
20
|
+
/** Route a notification; a panel_only (or burst-collapsed) decision lands in the feed. Returns the decision. */
|
|
21
|
+
dispatch(notification: Notification): RoutingDecision;
|
|
22
|
+
/** Surface batch-held notifications (call on a timer / when quiet-typing ends). */
|
|
23
|
+
flush(): void;
|
|
24
|
+
readonly router: NotificationRouter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The runtime event domains whose events surface as operational notifications.
|
|
29
|
+
* Deliberately a curated set of user-relevant, completion/attention-shaped
|
|
30
|
+
* domains — not every domain — so the panel reflects meaningful operational
|
|
31
|
+
* activity rather than raw event churn. The router's per-domain verbosity and
|
|
32
|
+
* burst/batch policies still collapse floods within these.
|
|
33
|
+
*/
|
|
34
|
+
export const NOTIFICATION_BRIDGE_DOMAINS: readonly RuntimeEventDomain[] = [
|
|
35
|
+
'agents',
|
|
36
|
+
'tasks',
|
|
37
|
+
'workflows',
|
|
38
|
+
'automation',
|
|
39
|
+
'deliveries',
|
|
40
|
+
'security',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Turn an UPPER_SNAKE event type into a short human title ("AGENT_COMPLETED" → "Agent completed"). */
|
|
44
|
+
export function humanizeEventType(type: string): string {
|
|
45
|
+
const words = type.toLowerCase().split(/[_\s]+/).filter(Boolean);
|
|
46
|
+
if (words.length === 0) return type;
|
|
47
|
+
return words.map((word, index) => (index === 0 ? word[0]!.toUpperCase() + word.slice(1) : word)).join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Derive a notification severity from an event type — errors/failures warn, the rest are informational. */
|
|
51
|
+
export function levelForEventType(type: string): Notification['level'] {
|
|
52
|
+
const upper = type.toUpperCase();
|
|
53
|
+
if (/(FAILED|ERROR|CRASH|DENIED|BLOCKED)/.test(upper)) return 'warning';
|
|
54
|
+
if (/(CRITICAL|FATAL|BREACH)/.test(upper)) return 'critical';
|
|
55
|
+
return 'info';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createNotificationDispatcher(
|
|
59
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
60
|
+
feed: PanelNotificationFeed = getSharedNotificationFeed(),
|
|
61
|
+
): NotificationDispatcher {
|
|
62
|
+
const router = createNotificationRouter(undefined, undefined, configManager);
|
|
63
|
+
const recordIfPanel = (notification: Notification, decision: RoutingDecision): void => {
|
|
64
|
+
if (decision.suppressed) return;
|
|
65
|
+
if (decision.target === 'panel_only') feed.record(notification, decision);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
router,
|
|
69
|
+
dispatch(notification) {
|
|
70
|
+
const decision = router.route(notification);
|
|
71
|
+
recordIfPanel(notification, decision);
|
|
72
|
+
return decision;
|
|
73
|
+
},
|
|
74
|
+
flush() {
|
|
75
|
+
for (const { notification } of router.flush()) {
|
|
76
|
+
// A flushed batch head surfaces as a batch-collapsed panel entry; the
|
|
77
|
+
// feed folds all sharing this batch key into one running-count row.
|
|
78
|
+
feed.record(notification, {
|
|
79
|
+
target: 'panel_only',
|
|
80
|
+
reasonCode: 'batch_window_collapsed',
|
|
81
|
+
batchKey: `${notification.domain}:${notification.level}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bridge the runtime event bus to the dispatcher: each event in a curated
|
|
90
|
+
* domain becomes a notification and is routed. Returns an unsubscribe function
|
|
91
|
+
* that detaches every domain listener.
|
|
92
|
+
*/
|
|
93
|
+
export function wireRuntimeNotificationBridge(
|
|
94
|
+
runtimeBus: RuntimeEventBus,
|
|
95
|
+
dispatcher: Pick<NotificationDispatcher, 'dispatch'>,
|
|
96
|
+
domains: readonly RuntimeEventDomain[] = NOTIFICATION_BRIDGE_DOMAINS,
|
|
97
|
+
): () => void {
|
|
98
|
+
const unsubscribes = domains.map((domain) =>
|
|
99
|
+
runtimeBus.onDomain(domain, (envelope) => {
|
|
100
|
+
dispatcher.dispatch({
|
|
101
|
+
id: envelope.traceId ?? `${domain}-${envelope.type}-${envelope.ts}`,
|
|
102
|
+
domain,
|
|
103
|
+
level: levelForEventType(envelope.type),
|
|
104
|
+
title: humanizeEventType(envelope.type),
|
|
105
|
+
timestamp: envelope.ts,
|
|
106
|
+
});
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
return () => { for (const unsubscribe of unsubscribes) unsubscribe(); };
|
|
110
|
+
}
|
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
|
2
2
|
import { atomicWriteFileSync } from '../../config/atomic-write.ts';
|
|
3
3
|
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
4
|
import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
|
|
5
|
-
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
6
5
|
import {
|
|
7
6
|
getOnboardingRuntimeStatePath,
|
|
8
7
|
readOnboardingRuntimeState,
|
|
@@ -133,39 +132,14 @@ function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
|
|
|
133
132
|
return normalized.startsWith('goodvibes://') && !isGoodVibesSecretReferenceValue(normalized);
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
function isFeatureFlagConfigKey(key: string): key is FeatureFlagConfigKey {
|
|
137
|
-
return key === 'featureFlags' || key.startsWith('featureFlags.');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function validateFeatureFlagConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): boolean {
|
|
141
|
-
if (!isFeatureFlagConfigKey(operation.key)) return false;
|
|
142
|
-
|
|
143
|
-
if (operation.key === 'featureFlags') {
|
|
144
|
-
if (!isPlainObject(operation.value)) throw new Error('featureFlags expects an object value.');
|
|
145
|
-
for (const [flagId, state] of Object.entries(operation.value)) {
|
|
146
|
-
if (flagId.trim().length === 0) throw new Error('featureFlags cannot contain an empty feature id.');
|
|
147
|
-
if (state !== 'enabled' && state !== 'disabled') {
|
|
148
|
-
throw new Error(`featureFlags.${flagId} expects enabled or disabled.`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const flagId = operation.key.slice('featureFlags.'.length);
|
|
155
|
-
if (flagId.trim().length === 0) throw new Error('featureFlags requires a feature id.');
|
|
156
|
-
if (operation.value !== 'enabled' && operation.value !== 'disabled') {
|
|
157
|
-
throw new Error(`Config key ${operation.key} expects enabled or disabled.`);
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
135
|
function validateConfigValue(operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>): void {
|
|
163
136
|
if (typeof operation.value === 'string' && isMalformedGoodVibesSecretReferenceValue(operation.value)) {
|
|
164
137
|
throw new Error(`Config key ${operation.key} only accepts goodvibes://secrets/... secret references.`);
|
|
165
138
|
}
|
|
166
139
|
|
|
167
|
-
|
|
168
|
-
|
|
140
|
+
// Feature enablement arrives as plain domain settings writes (e.g.
|
|
141
|
+
// sandbox.enabled, controlPlane.gateway), validated by the schema path below
|
|
142
|
+
// like any other key — there is no separate enablement namespace.
|
|
169
143
|
const schema = CONFIG_SCHEMA.find((entry) => entry.key === operation.key);
|
|
170
144
|
if (!schema) {
|
|
171
145
|
const defaultValue = operation.key.split('.').reduce<unknown>((cursor, part) => (
|
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
* - If a provider API key is already configured (env / secrets), skip
|
|
8
8
|
* onboarding entirely — write the check marker and drop straight into a
|
|
9
9
|
* working session. ZERO steps.
|
|
10
|
-
* - Otherwise ask exactly one question: start now on the
|
|
11
|
-
*
|
|
12
|
-
* always skippable.
|
|
10
|
+
* - Otherwise ask exactly one question: start now on the default model, or
|
|
11
|
+
* run the full guided setup. Dismissing (Esc) starts now — so it is
|
|
12
|
+
* always skippable. The start-now copy is GENERATED from the default
|
|
13
|
+
* model's registered auth state (buildOnboardingModelCopy), so a
|
|
14
|
+
* "no API key needed" promise can only render when the provider is
|
|
15
|
+
* genuinely keyless-ready.
|
|
13
16
|
*
|
|
14
17
|
* The full wizard remains one keystroke away (the "Full guided setup" choice,
|
|
15
18
|
* or `/onboarding` at any time). If the command context can't answer whether a
|
|
16
19
|
* provider is configured (e.g. a headless/test surface), we fall back to the
|
|
17
20
|
* classic behavior of opening the wizard, so nothing regresses.
|
|
18
21
|
*/
|
|
22
|
+
import {
|
|
23
|
+
buildOnboardingModelCopy,
|
|
24
|
+
resolveDefaultModelReadiness,
|
|
25
|
+
type ModelProviderSource,
|
|
26
|
+
type OnboardingModelCopy,
|
|
27
|
+
} from '@pellux/goodvibes-sdk/platform/providers';
|
|
19
28
|
import type { CommandContext } from '../../input/command-registry.ts';
|
|
20
29
|
import type { SelectionItem } from '../../input/selection-modal.ts';
|
|
21
30
|
import { writeOnboardingCheckMarker } from './markers.ts';
|
|
@@ -43,6 +52,27 @@ function readConfiguredProviderIds(commandContext: CommandContext): string[] | n
|
|
|
43
52
|
|
|
44
53
|
const MARKER_OPTIONS: WriteOnboardingCheckMarkerOptions = { source: 'command' };
|
|
45
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Copy for the "start now" choice, GENERATED from the default model's live
|
|
57
|
+
* registration state (never hand-written): a "no API key needed" promise can
|
|
58
|
+
* only appear when the provider's own auth state proves keyless readiness.
|
|
59
|
+
* Surfaces that cannot answer (no registry / no getForModel) resolve to the
|
|
60
|
+
* honest 'unresolvable' copy rather than a fabricated keyless claim.
|
|
61
|
+
*/
|
|
62
|
+
function resolveStartCopy(commandContext: CommandContext): OnboardingModelCopy {
|
|
63
|
+
const modelKey = String(commandContext.platform?.config?.provider?.model ?? '').trim();
|
|
64
|
+
const registry = commandContext.provider?.providerRegistry;
|
|
65
|
+
const source: ModelProviderSource =
|
|
66
|
+
registry && typeof (registry as Partial<ModelProviderSource>).getForModel === 'function'
|
|
67
|
+
? registry
|
|
68
|
+
: {
|
|
69
|
+
getForModel: () => {
|
|
70
|
+
throw new Error('provider registry unavailable on this surface');
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return buildOnboardingModelCopy(resolveDefaultModelReadiness(source, modelKey));
|
|
74
|
+
}
|
|
75
|
+
|
|
46
76
|
/**
|
|
47
77
|
* Run the fast-path first-run flow. Returns nothing; side effects (wizard open,
|
|
48
78
|
* marker write, modal) happen through the injected deps.
|
|
@@ -68,8 +98,9 @@ export function startOnboardingFastPath(deps: OnboardingFastPathDeps): void {
|
|
|
68
98
|
return;
|
|
69
99
|
}
|
|
70
100
|
|
|
101
|
+
const startCopy = resolveStartCopy(commandContext);
|
|
71
102
|
const items: SelectionItem[] = [
|
|
72
|
-
{ id: 'start-now', label:
|
|
103
|
+
{ id: 'start-now', label: startCopy.headline, detail: startCopy.detail, primaryAction: 'select' },
|
|
73
104
|
{ id: 'full-setup', label: 'Full guided setup', detail: 'Choose provider, model, channels, and more', primaryAction: 'select' },
|
|
74
105
|
];
|
|
75
106
|
commandContext.openSelection(
|
|
@@ -82,10 +113,12 @@ export function startOnboardingFastPath(deps: OnboardingFastPathDeps): void {
|
|
|
82
113
|
return;
|
|
83
114
|
}
|
|
84
115
|
// Start now (or dismissed): mark first-run done and drop into a working
|
|
85
|
-
// session on the
|
|
116
|
+
// session on the default model. The status line reuses the generated
|
|
117
|
+
// copy so it can never promise keyless readiness the registration
|
|
118
|
+
// state does not prove.
|
|
86
119
|
writeOnboardingCheckMarker(shellPaths, MARKER_OPTIONS);
|
|
87
120
|
commandContext.print?.(
|
|
88
|
-
|
|
121
|
+
`${startCopy.detail}. Add a provider key anytime with /provider, or run /onboarding for full setup.`,
|
|
89
122
|
);
|
|
90
123
|
render();
|
|
91
124
|
},
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic key capture for onboarding.
|
|
3
|
+
*
|
|
4
|
+
* The onboarding wizard offers an API-key field for every provider the registry
|
|
5
|
+
* knows accepts a key — derived from each provider's registration-time auth
|
|
6
|
+
* state (describeAuthState), never a hardcoded vendor list. This module holds
|
|
7
|
+
* the two shared pieces that keep the wizard step and the apply step in lockstep:
|
|
8
|
+
*
|
|
9
|
+
* 1. enrichProviderAccountsSnapshot — stamps each provider-account record with
|
|
10
|
+
* the secrets-store key it reads its API key from (its declared auth env
|
|
11
|
+
* var), read live from the provider registry.
|
|
12
|
+
* 2. deriveProviderKeyCaptureTargets / providerKeyFieldId — the single source
|
|
13
|
+
* of truth for which providers get a key field and how those fields are
|
|
14
|
+
* named, used by both the field builder and the apply writer.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { LLMProvider } from '@pellux/goodvibes-sdk/platform/providers';
|
|
18
|
+
import type {
|
|
19
|
+
OnboardingProviderAccountRecord,
|
|
20
|
+
OnboardingProviderAccountsSnapshot,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
|
|
23
|
+
/** Minimal provider lookup the enrichment needs (structural for testing). */
|
|
24
|
+
export interface ProviderKeyRegistryLike {
|
|
25
|
+
tryGet(name: string): LLMProvider | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read a provider's declared API-key env var, or undefined when it accepts none. */
|
|
29
|
+
export function readProviderApiKeyEnvVar(
|
|
30
|
+
registry: ProviderKeyRegistryLike,
|
|
31
|
+
providerId: string,
|
|
32
|
+
): string | undefined {
|
|
33
|
+
let provider: LLMProvider | undefined;
|
|
34
|
+
try {
|
|
35
|
+
provider = registry.tryGet(providerId);
|
|
36
|
+
} catch {
|
|
37
|
+
provider = undefined;
|
|
38
|
+
}
|
|
39
|
+
const envVars = provider?.describeAuthState?.().authEnvVars;
|
|
40
|
+
return envVars && envVars.length > 0 ? envVars[0] : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return an equivalent snapshot with each provider record stamped with the
|
|
45
|
+
* secrets-store key it accepts an API key under (from live registration truth).
|
|
46
|
+
* Records for keyless / subscription-only providers keep apiKeyEnvVar absent.
|
|
47
|
+
*/
|
|
48
|
+
export function enrichProviderAccountsSnapshot(
|
|
49
|
+
snapshot: OnboardingProviderAccountsSnapshot,
|
|
50
|
+
registry: ProviderKeyRegistryLike,
|
|
51
|
+
): OnboardingProviderAccountsSnapshot {
|
|
52
|
+
return {
|
|
53
|
+
...snapshot,
|
|
54
|
+
providers: snapshot.providers.map((record) => {
|
|
55
|
+
const apiKeyEnvVar = readProviderApiKeyEnvVar(registry, record.providerId);
|
|
56
|
+
return apiKeyEnvVar ? { ...record, apiKeyEnvVar } : record;
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A provider that onboarding can offer an API-key field for. */
|
|
62
|
+
export interface ProviderKeyCaptureTarget {
|
|
63
|
+
readonly providerId: string;
|
|
64
|
+
readonly apiKeyEnvVar: string;
|
|
65
|
+
readonly configured: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The providers onboarding offers an API-key field for: every registered
|
|
70
|
+
* provider that declares an api-key route AND carries a resolved auth env var.
|
|
71
|
+
* Ordered by provider id so the wizard renders deterministically.
|
|
72
|
+
*/
|
|
73
|
+
export function deriveProviderKeyCaptureTargets(
|
|
74
|
+
providers: readonly OnboardingProviderAccountRecord[] | undefined,
|
|
75
|
+
): ProviderKeyCaptureTarget[] {
|
|
76
|
+
if (!providers) return [];
|
|
77
|
+
return providers
|
|
78
|
+
.filter((record) => record.apiKeyEnvVar && record.availableRoutes.includes('api-key'))
|
|
79
|
+
.map((record) => ({
|
|
80
|
+
providerId: record.providerId,
|
|
81
|
+
apiKeyEnvVar: record.apiKeyEnvVar as string,
|
|
82
|
+
configured: record.configured,
|
|
83
|
+
}))
|
|
84
|
+
.sort((a, b) => a.providerId.localeCompare(b.providerId));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The wizard field id that captures a given provider's API key. */
|
|
88
|
+
export function providerKeyFieldId(providerId: string): string {
|
|
89
|
+
return `providers.api-key.${providerId}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A subscription-capable provider and its current subscription posture. */
|
|
93
|
+
export interface SubscriptionStatusTarget {
|
|
94
|
+
readonly providerId: string;
|
|
95
|
+
readonly active: boolean;
|
|
96
|
+
readonly pending: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The providers onboarding shows a subscription-status row for: every provider
|
|
101
|
+
* that declares a subscription route (registration truth), plus any provider
|
|
102
|
+
* with an active or pending subscription session — so a live session is never
|
|
103
|
+
* hidden just because its account record was absent. Ordered by provider id.
|
|
104
|
+
*/
|
|
105
|
+
export function deriveSubscriptionStatusTargets(
|
|
106
|
+
providers: readonly OnboardingProviderAccountRecord[] | undefined,
|
|
107
|
+
activeProviderIds: readonly string[],
|
|
108
|
+
pendingProviderIds: readonly string[],
|
|
109
|
+
): SubscriptionStatusTarget[] {
|
|
110
|
+
const active = new Set(activeProviderIds);
|
|
111
|
+
const pending = new Set(pendingProviderIds);
|
|
112
|
+
const ids = new Set<string>();
|
|
113
|
+
for (const record of providers ?? []) {
|
|
114
|
+
if (record.availableRoutes.includes('subscription')) ids.add(record.providerId);
|
|
115
|
+
}
|
|
116
|
+
for (const id of active) ids.add(id);
|
|
117
|
+
for (const id of pending) ids.add(id);
|
|
118
|
+
return [...ids]
|
|
119
|
+
.sort((a, b) => a.localeCompare(b))
|
|
120
|
+
.map((providerId) => ({ providerId, active: active.has(providerId), pending: pending.has(providerId) }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The wizard field id of a provider's subscription-status row. */
|
|
124
|
+
export function subscriptionStatusFieldId(providerId: string): string {
|
|
125
|
+
return `providers.subscription.${providerId}`;
|
|
126
|
+
}
|
|
@@ -43,7 +43,6 @@ function buildConfigSnapshot(
|
|
|
43
43
|
network: config.getCategory('network'),
|
|
44
44
|
surfaces: config.getCategory('surfaces'),
|
|
45
45
|
service: config.getCategory('service'),
|
|
46
|
-
featureFlags: config.getCategory('featureFlags'),
|
|
47
46
|
batch: config.getCategory('batch'),
|
|
48
47
|
cloudflare: config.getCategory('cloudflare'),
|
|
49
48
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
|
|
2
2
|
import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
|
|
3
|
-
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
4
3
|
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
5
4
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
6
5
|
import type {
|
|
@@ -46,7 +45,6 @@ export interface OnboardingConfigSnapshot {
|
|
|
46
45
|
readonly network: GoodVibesConfig['network'];
|
|
47
46
|
readonly surfaces: GoodVibesConfig['surfaces'];
|
|
48
47
|
readonly service: GoodVibesConfig['service'];
|
|
49
|
-
readonly featureFlags: GoodVibesConfig['featureFlags'];
|
|
50
48
|
readonly batch: GoodVibesConfig['batch'];
|
|
51
49
|
readonly cloudflare: GoodVibesConfig['cloudflare'];
|
|
52
50
|
}
|
|
@@ -157,6 +155,14 @@ export interface OnboardingProviderAccountRecord {
|
|
|
157
155
|
readonly availableRoutes: readonly string[];
|
|
158
156
|
readonly activeRoute: string;
|
|
159
157
|
readonly authFreshness: string;
|
|
158
|
+
/**
|
|
159
|
+
* The secrets-store key this provider reads its API key from — its declared
|
|
160
|
+
* auth env var, read from the live provider's registration-time auth state.
|
|
161
|
+
* Present only for providers that accept an API key; absent for keyless /
|
|
162
|
+
* subscription-only providers. Used to offer provider-agnostic key entry in
|
|
163
|
+
* onboarding, and as the storage key when the entered value is saved.
|
|
164
|
+
*/
|
|
165
|
+
readonly apiKeyEnvVar?: string;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
export interface OnboardingProviderAccountsSnapshot {
|
|
@@ -267,7 +273,7 @@ export interface OnboardingStepDerivationState {
|
|
|
267
273
|
export type OnboardingApplyOperation =
|
|
268
274
|
| {
|
|
269
275
|
readonly kind: 'set-config';
|
|
270
|
-
readonly key: ConfigKey
|
|
276
|
+
readonly key: ConfigKey;
|
|
271
277
|
readonly value: unknown;
|
|
272
278
|
readonly scope?: 'global' | 'project';
|
|
273
279
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-fallback.ts — the pre-catalog fallback registration for the
|
|
3
|
+
* configured model, extracted verbatim from services.ts (file-size hygiene):
|
|
4
|
+
* before the model catalog cache has loaded, the configured provider:model is
|
|
5
|
+
* registered with SDK family-aware context-window inference so the meter and
|
|
6
|
+
* compaction denominator agree with the post-catalog window.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
9
|
+
import { inferFallbackContextWindow, type ModelDefinition, type ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
10
|
+
|
|
11
|
+
function buildFallbackModelDefinition(provider: string, modelId: string): ModelDefinition {
|
|
12
|
+
const providerLower = provider.toLowerCase();
|
|
13
|
+
const isReasoningProvider = providerLower.includes('openai')
|
|
14
|
+
|| providerLower.includes('anthropic')
|
|
15
|
+
|| providerLower.includes('gemini')
|
|
16
|
+
|| providerLower.includes('google');
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
id: modelId,
|
|
20
|
+
provider,
|
|
21
|
+
registryKey: `${provider}:${modelId}`,
|
|
22
|
+
displayName: modelId,
|
|
23
|
+
description: 'Configured model available before the model catalog cache has loaded.',
|
|
24
|
+
capabilities: {
|
|
25
|
+
toolCalling: true,
|
|
26
|
+
codeEditing: true,
|
|
27
|
+
reasoning: isReasoningProvider,
|
|
28
|
+
multimodal: isReasoningProvider,
|
|
29
|
+
},
|
|
30
|
+
contextWindow: inferFallbackContextWindow(provider, modelId),
|
|
31
|
+
contextWindowProvenance: 'fallback',
|
|
32
|
+
selectable: true,
|
|
33
|
+
tier: 'standard',
|
|
34
|
+
...(isReasoningProvider ? { reasoningEffort: ['instant', 'low', 'medium', 'high'] } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ensureConfiguredModelIsRoutable(providerRegistry: ProviderRegistry, configManager: ConfigManager): void {
|
|
39
|
+
const configuredModel = String(configManager.get('provider.model') ?? '').trim();
|
|
40
|
+
if (!configuredModel.includes(':')) return;
|
|
41
|
+
if (providerRegistry.listModels().some((model) => model.registryKey === configuredModel)) return;
|
|
42
|
+
|
|
43
|
+
const [providerId, ...modelParts] = configuredModel.split(':');
|
|
44
|
+
const modelId = modelParts.join(':').trim();
|
|
45
|
+
if (!providerId || !modelId) return;
|
|
46
|
+
|
|
47
|
+
const provider = providerRegistry.tryGet(providerId);
|
|
48
|
+
if (!provider) return;
|
|
49
|
+
|
|
50
|
+
providerRegistry.registerRuntimeProvider({
|
|
51
|
+
provider,
|
|
52
|
+
replace: true,
|
|
53
|
+
models: [buildFallbackModelDefinition(providerId, modelId)],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-optimizer-wiring — bind the provider optimizer to its capability
|
|
3
|
+
* gate and seed its persisted routing mode at startup.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the SDK composition root's provider-optimizer-wiring module (which
|
|
6
|
+
* the SDK does not export through a public seam) so this repo's composition
|
|
7
|
+
* fork cannot drift from it silently: the optimizer is constructed dark, the
|
|
8
|
+
* gate subscription flips it live, and provider.optimizerMode seeds the mode.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
|
+
import type { ProviderOptimizer } from '@pellux/goodvibes-sdk/platform/providers';
|
|
13
|
+
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
14
|
+
|
|
15
|
+
export function bindProviderOptimizerFeatureFlag(
|
|
16
|
+
featureFlags: Pick<FeatureFlagManager, 'isEnabled' | 'subscribe'>,
|
|
17
|
+
providerOptimizer: Pick<ProviderOptimizer, 'setEnabled'>,
|
|
18
|
+
): () => void {
|
|
19
|
+
providerOptimizer.setEnabled(featureFlags.isEnabled('provider-optimizer'));
|
|
20
|
+
return featureFlags.subscribe((flagId, state) => {
|
|
21
|
+
if (flagId === 'provider-optimizer') {
|
|
22
|
+
providerOptimizer.setEnabled(state === 'enabled');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Apply the persistent provider-optimizer routing mode from config at startup.
|
|
29
|
+
* provider.optimizerMode 'off' keeps the optimizer inactive (its gate derives
|
|
30
|
+
* from the same key); this only seeds the mode/pin so an operator can persist
|
|
31
|
+
* "auto" or a pinned model without re-issuing a /provider command each
|
|
32
|
+
* session. Runtime pin/unpin/setMode still override for the live session.
|
|
33
|
+
*/
|
|
34
|
+
export function applyProviderOptimizerConfigMode(
|
|
35
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
36
|
+
providerOptimizer: Pick<ProviderOptimizer, 'setMode' | 'pin'>,
|
|
37
|
+
): void {
|
|
38
|
+
const mode = configManager.get('provider.optimizerMode');
|
|
39
|
+
if (mode === 'off') {
|
|
40
|
+
providerOptimizer.setMode('manual'); // inert baseline while inactive
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (mode === 'pinned') {
|
|
44
|
+
const pinned = configManager.get('provider.optimizerPinnedModel').trim();
|
|
45
|
+
const sep = pinned.indexOf(':');
|
|
46
|
+
if (sep > 0 && sep < pinned.length - 1) {
|
|
47
|
+
providerOptimizer.pin(pinned.slice(0, sep), pinned.slice(sep + 1));
|
|
48
|
+
} else {
|
|
49
|
+
// Pinned mode requested without a valid provider-qualified model — stay manual.
|
|
50
|
+
providerOptimizer.setMode('manual');
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
providerOptimizer.setMode(mode);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,109 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (`goodvibes[-daemon]-{platform}-{arch}`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Release-artifact naming and checksum verification — re-exported from the
|
|
3
|
+
* SDK's canonical update policy module (platform/runtime/self-update), which
|
|
4
|
+
* was hoisted from this file's semantics. One mechanism everywhere: the
|
|
5
|
+
* asset naming convention (`goodvibes[-daemon]-{platform}-{arch}`), the
|
|
6
|
+
* sqlite-vec addon naming, and the "no manifest entry is as fatal as a
|
|
7
|
+
* mismatch" verification rule now have a single owner. Consumers that must
|
|
8
|
+
* stay in lockstep:
|
|
8
9
|
* - scripts/postinstall.js (npm install-time binary download)
|
|
9
|
-
* - src/input/commands/update-runtime.ts (the /update
|
|
10
|
-
*
|
|
11
|
-
* A downloaded artifact with NO entry in the checksum manifest is just as
|
|
12
|
-
* unverified as one with a mismatching entry — verifyChecksum() throws in
|
|
13
|
-
* both cases, never silently skips.
|
|
10
|
+
* - src/input/commands/update-runtime.ts (the /update self-update)
|
|
11
|
+
* - scripts/install.sh (the shell installer; mirrors the same names)
|
|
14
12
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export {
|
|
14
|
+
CHECKSUM_MANIFEST_NAME,
|
|
15
|
+
parseChecksumFile,
|
|
16
|
+
resolveArtifactNames,
|
|
17
|
+
resolveSqliteVecAsset,
|
|
18
|
+
sha256,
|
|
19
|
+
verifyChecksum,
|
|
20
|
+
type ReleaseArtifactNames,
|
|
21
|
+
type SqliteVecAsset,
|
|
22
|
+
} from '@pellux/goodvibes-sdk/platform/runtime/self-update';
|
|
18
23
|
|
|
19
24
|
export type SupportedPlatform = 'linux' | 'darwin';
|
|
20
25
|
export type SupportedArch = 'x64' | 'arm64';
|
|
21
|
-
|
|
22
|
-
export interface ReleaseArtifactNames {
|
|
23
|
-
readonly app: string;
|
|
24
|
-
readonly daemon: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Release-asset platform tag as used in artifact filenames ("linux" | "macos"). */
|
|
28
|
-
const PLATFORM_TAGS: Record<SupportedPlatform, string> = {
|
|
29
|
-
linux: 'linux',
|
|
30
|
-
darwin: 'macos',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function resolveArtifactNames(platform: string, arch: string): ReleaseArtifactNames | null {
|
|
34
|
-
const platformTag = PLATFORM_TAGS[platform as SupportedPlatform];
|
|
35
|
-
if (!platformTag || (arch !== 'x64' && arch !== 'arm64')) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const suffix = `${platformTag}-${arch}`;
|
|
39
|
-
return {
|
|
40
|
-
app: `goodvibes-${suffix}`,
|
|
41
|
-
daemon: `goodvibes-daemon-${suffix}`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SqliteVecAsset {
|
|
46
|
-
/** Release asset filename, e.g. `sqlite-vec-linux-x64.so`. */
|
|
47
|
-
readonly assetName: string;
|
|
48
|
-
/** Directory name the SDK loader resolves, e.g. `sqlite-vec-linux-x64`. */
|
|
49
|
-
readonly dirName: string;
|
|
50
|
-
/** File the loader opens inside that directory, e.g. `vec0.so`. */
|
|
51
|
-
readonly fileName: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Names the sqlite-vec native addon for a platform/arch. Unlike the binaries
|
|
56
|
-
* (whose release tag maps darwin→"macos"), the addon keeps the Node-style
|
|
57
|
-
* platform tag ("linux" | "darwin") because that is exactly what the SDK's
|
|
58
|
-
* loader resolves at `<execDir>/lib/sqlite-vec-<process.platform>-<process.arch>/vec0.<suffix>`.
|
|
59
|
-
* The scripts/install.sh installer and the release workflow name the asset the
|
|
60
|
-
* same way; all three must stay in lockstep.
|
|
61
|
-
*/
|
|
62
|
-
export function resolveSqliteVecAsset(platform: string, arch: string): SqliteVecAsset | null {
|
|
63
|
-
if ((platform !== 'linux' && platform !== 'darwin') || (arch !== 'x64' && arch !== 'arm64')) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
const suffix = platform === 'darwin' ? 'dylib' : 'so';
|
|
67
|
-
const dirName = `sqlite-vec-${platform}-${arch}`;
|
|
68
|
-
return {
|
|
69
|
-
assetName: `${dirName}.${suffix}`,
|
|
70
|
-
dirName,
|
|
71
|
-
fileName: `vec0.${suffix}`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function sha256(buffer: Buffer | Uint8Array): string {
|
|
76
|
-
return createHash('sha256').update(buffer).digest('hex');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function parseChecksumFile(contents: string): Map<string, string> {
|
|
80
|
-
const checksums = new Map<string, string>();
|
|
81
|
-
for (const rawLine of contents.split(/\r?\n/)) {
|
|
82
|
-
const line = rawLine.trim();
|
|
83
|
-
if (!line) continue;
|
|
84
|
-
const match = line.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
|
|
85
|
-
if (!match) continue;
|
|
86
|
-
checksums.set(match[2], match[1].toLowerCase());
|
|
87
|
-
}
|
|
88
|
-
return checksums;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Verify a downloaded artifact's checksum against the parsed manifest.
|
|
93
|
-
* An artifact with no entry in the manifest is a hard failure, identical
|
|
94
|
-
* in severity to a mismatching entry — never treated as "unverifiable, so
|
|
95
|
-
* skip the check". Throws naming the artifact and the manifest.
|
|
96
|
-
*/
|
|
97
|
-
export function verifyChecksum(
|
|
98
|
-
artifactName: string,
|
|
99
|
-
actual: string,
|
|
100
|
-
expected: string | undefined,
|
|
101
|
-
manifestName: string = CHECKSUM_MANIFEST_NAME,
|
|
102
|
-
): void {
|
|
103
|
-
if (expected === undefined) {
|
|
104
|
-
throw new Error(`no checksum entry for ${artifactName} in ${manifestName} — refusing to install an unverified binary`);
|
|
105
|
-
}
|
|
106
|
-
if (expected !== actual) {
|
|
107
|
-
throw new Error(`checksum mismatch for ${artifactName}: expected ${expected}, got ${actual}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* resume-notice.ts — the boot-time "previous session found" transcript notice.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* item 1: a supervision-journey audit of 1.7.0 found that the TUI
|
|
5
5
|
* accumulates rich resumable state on disk (a saved conversation, workspace
|
|
6
6
|
* checkpoints, WRFC chain history) but never surfaces any of it at startup —
|
|
7
7
|
* an operator has no way to know it exists short of already knowing the
|