@pellux/goodvibes-tui 0.19.32 → 0.19.34
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 +23 -0
- package/README.md +4 -2
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/audio/spoken-turn-model-routing.ts +117 -0
- package/src/input/command-registry.ts +2 -0
- package/src/input/commands/cloudflare-runtime.ts +343 -0
- package/src/input/commands/tts-runtime.ts +288 -7
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +1 -0
- package/src/input/handler-feed.ts +6 -0
- package/src/input/handler-modal-routes.ts +23 -10
- package/src/input/handler-modal-token-routes.ts +9 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +33 -0
- package/src/input/handler-picker-routes.ts +1 -1
- package/src/input/handler.ts +4 -1
- package/src/input/model-picker-types.ts +125 -0
- package/src/input/model-picker.ts +144 -134
- package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +30 -8
- package/src/main.ts +12 -1
- package/src/renderer/buffer.ts +40 -2
- package/src/renderer/compositor.ts +25 -17
- package/src/renderer/model-picker-overlay.ts +70 -0
- package/src/renderer/settings-modal-helpers.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/cloudflare-control-plane.ts +328 -0
- package/src/runtime/onboarding/derivation.ts +25 -0
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +5 -1
- package/src/shell/ui-openers.ts +21 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloudflareDaemonRouteError,
|
|
3
|
+
createCloudflareDaemonClient,
|
|
4
|
+
type CloudflareComponentSelection,
|
|
5
|
+
type CloudflareDaemonClient,
|
|
6
|
+
type CloudflareDiscoverResult,
|
|
7
|
+
type CloudflareOperationalTokenResult,
|
|
8
|
+
type CloudflareProvisionRequest,
|
|
9
|
+
type CloudflareProvisionResult,
|
|
10
|
+
type CloudflareTokenRequirementsResult,
|
|
11
|
+
type CloudflareValidateResult,
|
|
12
|
+
type CloudflareVerifyResult,
|
|
13
|
+
} from '../runtime/cloudflare-control-plane.ts';
|
|
14
|
+
import type { OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
15
|
+
import type { InputHandler } from './handler.ts';
|
|
16
|
+
import type { OnboardingWizardAction, OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
17
|
+
import {
|
|
18
|
+
buildCloudflareApiTokenRef,
|
|
19
|
+
buildCloudflareProvisionRequest,
|
|
20
|
+
getCloudflareBatchMode,
|
|
21
|
+
getCloudflareComponentSelection,
|
|
22
|
+
getCloudflareSetupSource,
|
|
23
|
+
shouldShowCloudflareStep,
|
|
24
|
+
} from './onboarding/onboarding-wizard-cloudflare.ts';
|
|
25
|
+
|
|
26
|
+
type CloudflareOnboardingAction = Extract<OnboardingWizardAction,
|
|
27
|
+
| 'cloudflare-token-requirements'
|
|
28
|
+
| 'cloudflare-create-operational-token'
|
|
29
|
+
| 'cloudflare-discover'
|
|
30
|
+
| 'cloudflare-validate'
|
|
31
|
+
| 'cloudflare-provision'
|
|
32
|
+
| 'cloudflare-verify'
|
|
33
|
+
| 'cloudflare-disable'
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
function getCloudflareDaemonClientForHandler(handler: InputHandler): CloudflareDaemonClient {
|
|
37
|
+
return createCloudflareDaemonClient({
|
|
38
|
+
configManager: handler.uiServices.platform.configManager,
|
|
39
|
+
homeDirectory: handler.uiServices.environment.homeDirectory,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeCloudflareActionError(error: unknown): string {
|
|
44
|
+
if (error instanceof CloudflareDaemonRouteError) {
|
|
45
|
+
return `${error.message} (HTTP ${error.status}, ${error.code})`;
|
|
46
|
+
}
|
|
47
|
+
return error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setCloudflareWizardStatusForHandler(
|
|
51
|
+
handler: InputHandler,
|
|
52
|
+
title: string,
|
|
53
|
+
lines: readonly string[],
|
|
54
|
+
severity: OnboardingWizardApplyFeedback['severity'] = 'info',
|
|
55
|
+
): void {
|
|
56
|
+
const message = [title, ...lines].filter((line) => line.length > 0).join('\n');
|
|
57
|
+
handler.onboardingWizard.textState.set('cloudflare.action-status', message);
|
|
58
|
+
handler.onboardingWizard.setApplyFeedback({
|
|
59
|
+
severity,
|
|
60
|
+
title,
|
|
61
|
+
summary: lines[0] ?? title,
|
|
62
|
+
messages: lines.length > 0 ? lines : [title],
|
|
63
|
+
});
|
|
64
|
+
const targetIndex = handler.onboardingWizard.steps.findIndex((step) => step.id === 'cloudflare');
|
|
65
|
+
if (targetIndex >= 0) handler.onboardingWizard.setStep(targetIndex);
|
|
66
|
+
handler.commandContext?.print?.(message);
|
|
67
|
+
handler.requestRender();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatCloudflareComponents(components: CloudflareComponentSelection): string {
|
|
71
|
+
const enabled = Object.entries(components)
|
|
72
|
+
.filter(([, selected]) => selected === true)
|
|
73
|
+
.map(([component]) => component);
|
|
74
|
+
return enabled.length > 0 ? enabled.join(', ') : 'none';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatCloudflareRequirements(result: CloudflareTokenRequirementsResult): string[] {
|
|
78
|
+
const permissionLines = result.permissions.length > 0
|
|
79
|
+
? result.permissions.map((permission) => ` ${permission.scope}: ${permission.permission} (${permission.component}) - ${permission.reason}`)
|
|
80
|
+
: [' No permissions returned for the selected components.'];
|
|
81
|
+
return [
|
|
82
|
+
`components: ${formatCloudflareComponents(result.components)}`,
|
|
83
|
+
'required permissions:',
|
|
84
|
+
...permissionLines,
|
|
85
|
+
...(result.bootstrapToken.instructions.length > 0
|
|
86
|
+
? ['', 'bootstrap token instructions:', ...result.bootstrapToken.instructions.map((line) => ` ${line}`)]
|
|
87
|
+
: []),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatCloudflareValidation(result: CloudflareValidateResult): string[] {
|
|
92
|
+
return [
|
|
93
|
+
`token: ${result.ok ? 'valid' : 'not valid'}`,
|
|
94
|
+
`source: ${result.tokenSource}`,
|
|
95
|
+
result.account
|
|
96
|
+
? `account: ${result.account.name} (${result.account.id})`
|
|
97
|
+
: 'account: not resolved',
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatCloudflareDiscovery(result: CloudflareDiscoverResult): string[] {
|
|
102
|
+
return [
|
|
103
|
+
`token source: ${result.tokenSource}`,
|
|
104
|
+
`accounts: ${result.accounts.length}`,
|
|
105
|
+
`zones: ${result.zones.length}`,
|
|
106
|
+
`worker subdomain: ${result.workerSubdomain || 'not detected'}`,
|
|
107
|
+
`queues: ${result.queues?.length ?? 0}`,
|
|
108
|
+
`KV namespaces: ${result.kvNamespaces?.length ?? 0}`,
|
|
109
|
+
`R2 buckets: ${result.r2Buckets?.length ?? 0}`,
|
|
110
|
+
...(result.selectedAccount ? [`selected account: ${result.selectedAccount.name} (${result.selectedAccount.id})`] : []),
|
|
111
|
+
...(result.selectedZone ? [`selected zone: ${result.selectedZone.name} (${result.selectedZone.id})`] : []),
|
|
112
|
+
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatCloudflareTokenCreate(result: CloudflareOperationalTokenResult): string[] {
|
|
117
|
+
return [
|
|
118
|
+
`token: ${result.tokenName}${result.tokenId ? ` (${result.tokenId})` : ''}`,
|
|
119
|
+
`account: ${result.accountId}`,
|
|
120
|
+
`stored ref: ${result.apiTokenRef ?? 'not stored'}`,
|
|
121
|
+
`permissions: ${result.permissions.length}`,
|
|
122
|
+
'Delete or expire the temporary bootstrap token in Cloudflare after confirming the operational token works.',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatCloudflareProvision(result: CloudflareProvisionResult): string[] {
|
|
127
|
+
return [
|
|
128
|
+
`result: ${result.ok ? 'ok' : 'needs attention'}`,
|
|
129
|
+
...(result.worker ? [`worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
|
|
130
|
+
...(result.queues ? [`queue: ${result.queues.queueName}; DLQ: ${result.queues.deadLetterQueueName}`] : []),
|
|
131
|
+
...result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
|
|
132
|
+
...(result.verification ? formatCloudflareVerify(result.verification).map((line) => `verify ${line}`) : []),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatCloudflareVerify(result: CloudflareVerifyResult): string[] {
|
|
137
|
+
return [
|
|
138
|
+
`worker health: ${result.workerHealth.ok ? 'ok' : 'failed'} (HTTP ${result.workerHealth.status})${result.workerHealth.error ? ` - ${result.workerHealth.error}` : ''}`,
|
|
139
|
+
...(result.daemonBatchProxy
|
|
140
|
+
? [`daemon batch proxy: ${result.daemonBatchProxy.ok ? 'ok' : 'failed'} (HTTP ${result.daemonBatchProxy.status})${result.daemonBatchProxy.error ? ` - ${result.daemonBatchProxy.error}` : ''}`]
|
|
141
|
+
: []),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getCloudflareBootstrapTokenFromWizard(handler: InputHandler): string {
|
|
146
|
+
const wizard = handler.onboardingWizard;
|
|
147
|
+
const setupSource = getCloudflareSetupSource(wizard);
|
|
148
|
+
if (setupSource === 'bootstrap-token') {
|
|
149
|
+
return wizard.getStringFieldValue('cloudflare.bootstrap-token', '');
|
|
150
|
+
}
|
|
151
|
+
if (setupSource === 'bootstrap-env') {
|
|
152
|
+
const envName = wizard.getStringFieldValue('cloudflare.bootstrap-env-name', 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN');
|
|
153
|
+
return process.env[envName] ?? '';
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getCloudflareOperationalTokenFromWizard(handler: InputHandler): string {
|
|
159
|
+
const wizard = handler.onboardingWizard;
|
|
160
|
+
return getCloudflareSetupSource(wizard) === 'operational-token'
|
|
161
|
+
? wizard.getStringFieldValue('cloudflare.operational-token', '')
|
|
162
|
+
: '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getCloudflareApiTokenRefFromWizard(handler: InputHandler): string {
|
|
166
|
+
const wizard = handler.onboardingWizard;
|
|
167
|
+
const setupSource = getCloudflareSetupSource(wizard);
|
|
168
|
+
if (setupSource === 'operational-env') {
|
|
169
|
+
return buildCloudflareApiTokenRef(wizard.getStringFieldValue('cloudflare.operational-env-name', 'CLOUDFLARE_API_TOKEN'));
|
|
170
|
+
}
|
|
171
|
+
return wizard.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function createCloudflareOperationalTokenForHandler(handler: InputHandler): Promise<CloudflareOperationalTokenResult> {
|
|
175
|
+
const wizard = handler.onboardingWizard;
|
|
176
|
+
const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
|
|
177
|
+
if (!bootstrapToken) {
|
|
178
|
+
throw new Error('A bootstrap token is required. Paste it in the wizard or select an environment variable that is set in this TUI process.');
|
|
179
|
+
}
|
|
180
|
+
const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
|
|
181
|
+
const zoneId = wizard.getStringFieldValue('cloudflare.zone-id', wizard.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
|
|
182
|
+
const zoneName = wizard.getStringFieldValue('cloudflare.zone-name', wizard.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
|
|
183
|
+
return await getCloudflareDaemonClientForHandler(handler).createOperationalToken({
|
|
184
|
+
components: getCloudflareComponentSelection(wizard),
|
|
185
|
+
bootstrapToken,
|
|
186
|
+
...(accountId ? { accountId } : {}),
|
|
187
|
+
...(zoneId ? { zoneId } : {}),
|
|
188
|
+
...(zoneName ? { zoneName } : {}),
|
|
189
|
+
storeApiToken: true,
|
|
190
|
+
persistConfig: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function buildCloudflareProvisionInputForHandler(handler: InputHandler): Promise<CloudflareProvisionRequest> {
|
|
195
|
+
const input = buildCloudflareProvisionRequest(handler.onboardingWizard, { includeTransientSecrets: true });
|
|
196
|
+
const setupSource = getCloudflareSetupSource(handler.onboardingWizard);
|
|
197
|
+
if (setupSource === 'bootstrap-token' || setupSource === 'bootstrap-env') {
|
|
198
|
+
const tokenResult = await createCloudflareOperationalTokenForHandler(handler);
|
|
199
|
+
if (tokenResult.apiTokenRef) {
|
|
200
|
+
const withoutInlineToken = { ...input };
|
|
201
|
+
delete withoutInlineToken.apiToken;
|
|
202
|
+
return { ...withoutInlineToken, apiTokenRef: tokenResult.apiTokenRef };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return input;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildCloudflareDiscoveryInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['discover']>[0] {
|
|
209
|
+
const wizard = handler.onboardingWizard;
|
|
210
|
+
const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
|
|
211
|
+
const zoneId = wizard.getStringFieldValue('cloudflare.zone-id', wizard.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
|
|
212
|
+
const zoneName = wizard.getStringFieldValue('cloudflare.zone-name', wizard.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
|
|
213
|
+
const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
|
|
214
|
+
const apiToken = getCloudflareOperationalTokenFromWizard(handler) || bootstrapToken;
|
|
215
|
+
const apiTokenRef = getCloudflareApiTokenRefFromWizard(handler);
|
|
216
|
+
return {
|
|
217
|
+
components: getCloudflareComponentSelection(wizard),
|
|
218
|
+
includeResources: true,
|
|
219
|
+
...(accountId ? { accountId } : {}),
|
|
220
|
+
...(zoneId ? { zoneId } : {}),
|
|
221
|
+
...(zoneName ? { zoneName } : {}),
|
|
222
|
+
...(apiToken ? { apiToken } : apiTokenRef ? { apiTokenRef } : {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildCloudflareValidateInputForHandler(handler: InputHandler): Parameters<CloudflareDaemonClient['validate']>[0] {
|
|
227
|
+
const wizard = handler.onboardingWizard;
|
|
228
|
+
const accountId = wizard.getStringFieldValue('cloudflare.account-id', wizard.runtimeSnapshot?.config.cloudflare.accountId ?? '');
|
|
229
|
+
const bootstrapToken = getCloudflareBootstrapTokenFromWizard(handler);
|
|
230
|
+
const apiToken = getCloudflareOperationalTokenFromWizard(handler) || bootstrapToken;
|
|
231
|
+
const apiTokenRef = getCloudflareApiTokenRefFromWizard(handler);
|
|
232
|
+
return {
|
|
233
|
+
...(accountId ? { accountId } : {}),
|
|
234
|
+
...(apiToken ? { apiToken } : apiTokenRef ? { apiTokenRef } : {}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function handleCloudflareOnboardingActionForHandler(
|
|
239
|
+
handler: InputHandler,
|
|
240
|
+
action: CloudflareOnboardingAction,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
if (handler.onboardingApplyPending) return;
|
|
243
|
+
handler.onboardingApplyPending = true;
|
|
244
|
+
handler.onboardingWizard.clearApplyFeedback();
|
|
245
|
+
handler.requestRender();
|
|
246
|
+
try {
|
|
247
|
+
const client = getCloudflareDaemonClientForHandler(handler);
|
|
248
|
+
if (action === 'cloudflare-token-requirements') {
|
|
249
|
+
const result = await client.tokenRequirements({
|
|
250
|
+
components: getCloudflareComponentSelection(handler.onboardingWizard),
|
|
251
|
+
includeBootstrap: true,
|
|
252
|
+
});
|
|
253
|
+
setCloudflareWizardStatusForHandler(handler, 'Cloudflare token requirements', formatCloudflareRequirements(result));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (action === 'cloudflare-create-operational-token') {
|
|
258
|
+
const result = await createCloudflareOperationalTokenForHandler(handler);
|
|
259
|
+
setCloudflareWizardStatusForHandler(handler, 'Cloudflare operational token created', formatCloudflareTokenCreate(result));
|
|
260
|
+
await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (action === 'cloudflare-discover') {
|
|
265
|
+
const result = await client.discover(buildCloudflareDiscoveryInputForHandler(handler));
|
|
266
|
+
if (result.selectedAccount && !handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', '')) {
|
|
267
|
+
handler.onboardingWizard.setFieldValue('cloudflare.account-id', result.selectedAccount.id);
|
|
268
|
+
} else if (result.accounts.length === 1 && !handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', '')) {
|
|
269
|
+
handler.onboardingWizard.setFieldValue('cloudflare.account-id', result.accounts[0]!.id);
|
|
270
|
+
}
|
|
271
|
+
if (result.selectedZone && !handler.onboardingWizard.getStringFieldValue('cloudflare.zone-id', '')) {
|
|
272
|
+
handler.onboardingWizard.setFieldValue('cloudflare.zone-id', result.selectedZone.id);
|
|
273
|
+
handler.onboardingWizard.setFieldValue('cloudflare.zone-name', result.selectedZone.name);
|
|
274
|
+
} else if (result.zones.length === 1 && !handler.onboardingWizard.getStringFieldValue('cloudflare.zone-id', '')) {
|
|
275
|
+
handler.onboardingWizard.setFieldValue('cloudflare.zone-id', result.zones[0]!.id);
|
|
276
|
+
handler.onboardingWizard.setFieldValue('cloudflare.zone-name', result.zones[0]!.name);
|
|
277
|
+
}
|
|
278
|
+
if (result.workerSubdomain && !handler.onboardingWizard.getStringFieldValue('cloudflare.worker-subdomain', '')) {
|
|
279
|
+
handler.onboardingWizard.setFieldValue('cloudflare.worker-subdomain', result.workerSubdomain);
|
|
280
|
+
}
|
|
281
|
+
setCloudflareWizardStatusForHandler(handler, 'Cloudflare discovery completed', formatCloudflareDiscovery(result));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (action === 'cloudflare-validate') {
|
|
286
|
+
const result = await client.validate(buildCloudflareValidateInputForHandler(handler));
|
|
287
|
+
setCloudflareWizardStatusForHandler(
|
|
288
|
+
handler,
|
|
289
|
+
result.ok ? 'Cloudflare token validated' : 'Cloudflare token validation needs attention',
|
|
290
|
+
formatCloudflareValidation(result),
|
|
291
|
+
result.ok ? 'info' : 'warning',
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (action === 'cloudflare-provision') {
|
|
297
|
+
const input = await buildCloudflareProvisionInputForHandler(handler);
|
|
298
|
+
const result = await client.provision(input);
|
|
299
|
+
setCloudflareWizardStatusForHandler(
|
|
300
|
+
handler,
|
|
301
|
+
result.ok ? 'Cloudflare provisioning completed' : 'Cloudflare provisioning needs attention',
|
|
302
|
+
formatCloudflareProvision(result),
|
|
303
|
+
result.ok ? 'info' : 'warning',
|
|
304
|
+
);
|
|
305
|
+
await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (action === 'cloudflare-verify') {
|
|
310
|
+
const result = await client.verify({
|
|
311
|
+
workerBaseUrl: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-base-url', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerBaseUrl ?? ''),
|
|
312
|
+
workerClientTokenRef: handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerClientTokenRef ?? '',
|
|
313
|
+
});
|
|
314
|
+
setCloudflareWizardStatusForHandler(
|
|
315
|
+
handler,
|
|
316
|
+
result.ok ? 'Cloudflare Worker verified' : 'Cloudflare Worker verification needs attention',
|
|
317
|
+
formatCloudflareVerify(result),
|
|
318
|
+
result.ok ? 'info' : 'warning',
|
|
319
|
+
);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (action === 'cloudflare-disable') {
|
|
324
|
+
const result = await client.disable({
|
|
325
|
+
accountId: handler.onboardingWizard.getStringFieldValue('cloudflare.account-id', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.accountId ?? ''),
|
|
326
|
+
apiTokenRef: getCloudflareApiTokenRefFromWizard(handler),
|
|
327
|
+
workerName: handler.onboardingWizard.getStringFieldValue('cloudflare.worker-name', handler.onboardingWizard.runtimeSnapshot?.config.cloudflare.workerName ?? 'goodvibes-batch-worker'),
|
|
328
|
+
persistConfig: true,
|
|
329
|
+
});
|
|
330
|
+
setCloudflareWizardStatusForHandler(
|
|
331
|
+
handler,
|
|
332
|
+
result.ok ? 'Cloudflare integration disabled' : 'Cloudflare disable needs attention',
|
|
333
|
+
result.steps.map((step) => `${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`),
|
|
334
|
+
result.ok ? 'info' : 'warning',
|
|
335
|
+
);
|
|
336
|
+
await handler.refreshOnboardingHydration({ preserveValues: true, targetStepId: 'cloudflare' });
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
setCloudflareWizardStatusForHandler(handler, 'Cloudflare action failed', [normalizeCloudflareActionError(error)], 'error');
|
|
340
|
+
} finally {
|
|
341
|
+
handler.onboardingApplyPending = false;
|
|
342
|
+
handler.requestRender();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function maybeProvisionCloudflareOnFinalApplyForHandler(handler: InputHandler): Promise<readonly OnboardingVerificationItem[]> {
|
|
347
|
+
const wizard = handler.onboardingWizard;
|
|
348
|
+
if (!shouldShowCloudflareStep(wizard)) return [];
|
|
349
|
+
const cloudflareEnabled = wizard.getBooleanFieldValue('cloudflare.enabled', wizard.isCapabilitySelected('cloudflare-batch') || wizard.runtimeSnapshot?.config.cloudflare.enabled === true);
|
|
350
|
+
if (!cloudflareEnabled) {
|
|
351
|
+
return [{
|
|
352
|
+
id: 'cloudflare:disabled',
|
|
353
|
+
status: 'pass',
|
|
354
|
+
message: 'Cloudflare integration is disabled; local daemon behavior remains active.',
|
|
355
|
+
target: 'cloudflare',
|
|
356
|
+
}];
|
|
357
|
+
}
|
|
358
|
+
const provisionOnApply = wizard.getStringFieldValue('cloudflare.provision-on-apply', 'no') === 'yes';
|
|
359
|
+
if (!provisionOnApply) {
|
|
360
|
+
return [{
|
|
361
|
+
id: 'cloudflare:configuration-saved',
|
|
362
|
+
status: 'pass',
|
|
363
|
+
message: `Cloudflare settings were saved. Batch mode is ${getCloudflareBatchMode(wizard)}; provisioning was not requested on final apply.`,
|
|
364
|
+
target: 'cloudflare',
|
|
365
|
+
}];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const client = getCloudflareDaemonClientForHandler(handler);
|
|
370
|
+
const result = await client.provision(await buildCloudflareProvisionInputForHandler(handler));
|
|
371
|
+
handler.onboardingWizard.textState.set('cloudflare.action-status', [
|
|
372
|
+
result.ok ? 'Cloudflare provisioning completed during final apply.' : 'Cloudflare provisioning needs attention after final apply.',
|
|
373
|
+
...formatCloudflareProvision(result),
|
|
374
|
+
].join('\n'));
|
|
375
|
+
return [{
|
|
376
|
+
id: 'cloudflare:provision',
|
|
377
|
+
status: result.ok ? 'pass' : 'warn',
|
|
378
|
+
message: result.ok
|
|
379
|
+
? 'Cloudflare resources were provisioned and verified through the daemon SDK route.'
|
|
380
|
+
: 'Cloudflare provisioning returned warnings or failed verification. Settings were saved; rerun the Cloudflare wizard action after correcting token/resource issues.',
|
|
381
|
+
target: 'cloudflare',
|
|
382
|
+
}];
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return [{
|
|
385
|
+
id: 'cloudflare:provision',
|
|
386
|
+
status: 'warn',
|
|
387
|
+
message: `Cloudflare provisioning did not complete: ${normalizeCloudflareActionError(error)} Settings were saved; retry from the Cloudflare wizard or /cloudflare command.`,
|
|
388
|
+
target: 'cloudflare',
|
|
389
|
+
}];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -3,6 +3,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
3
3
|
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
|
|
4
4
|
import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
5
5
|
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
6
|
+
import { handleCloudflareOnboardingActionForHandler, maybeProvisionCloudflareOnFinalApplyForHandler } from './handler-onboarding-cloudflare.ts';
|
|
6
7
|
import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
|
|
7
8
|
import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
8
9
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
@@ -186,6 +187,24 @@ export function openModelPickerWithTargetForHandler(
|
|
|
186
187
|
return true;
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
export function openProviderModelPickerWithTargetForHandler(
|
|
191
|
+
handler: InputHandler,
|
|
192
|
+
target: ModelPickerTarget,
|
|
193
|
+
source: 'settings' | 'onboarding' = 'settings',
|
|
194
|
+
): boolean {
|
|
195
|
+
const openProviderPicker = handler.commandContext?.openProviderPicker;
|
|
196
|
+
if (!openProviderPicker) return false;
|
|
197
|
+
if (source === 'onboarding' && handler.onboardingWizard.active) {
|
|
198
|
+
handler.onboardingModelPickerCancelSnapshot = captureOnboardingWizardSnapshot(handler.onboardingWizard);
|
|
199
|
+
} else {
|
|
200
|
+
handler.clearOnboardingModelPickerCancelState();
|
|
201
|
+
}
|
|
202
|
+
handler.clearOnboardingPendingModelPickerTarget();
|
|
203
|
+
handler.modelPicker.target = target;
|
|
204
|
+
openProviderPicker();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
189
208
|
export function handleModelPickerCommitForHandler(handler: InputHandler): boolean {
|
|
190
209
|
if (handler.onboardingModelPickerCancelSnapshot && handler.onboardingWizard.active) {
|
|
191
210
|
const selected = handler.modelPicker.mode === 'effort'
|
|
@@ -227,6 +246,18 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
227
246
|
continueOnboardingSection(handler);
|
|
228
247
|
return;
|
|
229
248
|
}
|
|
249
|
+
if (action.startsWith('cloudflare-')) {
|
|
250
|
+
await handleCloudflareOnboardingActionForHandler(handler, action as Extract<OnboardingWizardAction,
|
|
251
|
+
| 'cloudflare-token-requirements'
|
|
252
|
+
| 'cloudflare-create-operational-token'
|
|
253
|
+
| 'cloudflare-discover'
|
|
254
|
+
| 'cloudflare-validate'
|
|
255
|
+
| 'cloudflare-provision'
|
|
256
|
+
| 'cloudflare-verify'
|
|
257
|
+
| 'cloudflare-disable'
|
|
258
|
+
>);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
230
261
|
if (action !== 'apply') return;
|
|
231
262
|
if (handler.onboardingApplyPending) return;
|
|
232
263
|
const blockers = handler.onboardingWizard.getBlockingFieldLabels();
|
|
@@ -272,6 +303,8 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
272
303
|
? { ...item, status: 'warn' }
|
|
273
304
|
: item));
|
|
274
305
|
verificationItems = dedupeOnboardingVerificationItems([...verificationItems, ...runtimeWarnings]);
|
|
306
|
+
const cloudflareItems = await maybeProvisionCloudflareOnFinalApplyForHandler(handler);
|
|
307
|
+
verificationItems = dedupeOnboardingVerificationItems([...verificationItems, ...cloudflareItems]);
|
|
275
308
|
}
|
|
276
309
|
} catch (error) {
|
|
277
310
|
showOnboardingApplyFeedbackForHandler(handler, {
|
|
@@ -55,7 +55,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
55
55
|
const selected = state.modelPicker.getSelected();
|
|
56
56
|
if (selected) {
|
|
57
57
|
const currentEffort = state.commandContext?.session.runtime.reasoningEffort ?? 'medium';
|
|
58
|
-
if (selected.reasoningEffort && selected.reasoningEffort.length > 0) {
|
|
58
|
+
if (state.modelPicker.target === 'main' && selected.reasoningEffort && selected.reasoningEffort.length > 0) {
|
|
59
59
|
state.modelPicker.showEffortPicker(selected, currentEffort);
|
|
60
60
|
} else {
|
|
61
61
|
const target = state.modelPicker.target;
|
package/src/input/handler.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
|
|
4
4
|
import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config/oauth-local-listener';
|
|
5
5
|
import { clearModalStackForHandler, cleanupMarkerRegistryForHandler, executeBlockActionForHandler, expandPromptForHandler, findMarkerAtPosForHandler, getImageAttachmentsForHandler, handleBlockCopyForHandler, handleBlockRerunForHandler, handleBlockSaveForHandler, handleBlockToggleForHandler, handleBookmarkForHandler, handleCopyForHandler, handleCtrlCForHandler, handleDiffApplyForHandler, handleEscapeForHandler, hydrateOnboardingWizardFromRuntimeForHandler, modalOpenedForHandler, openOnboardingWizardForHandler, registerPasteForHandler } from './handler-interactions.ts';
|
|
6
|
-
import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, type OnboardingRuntimePosture } from './handler-onboarding.ts';
|
|
6
|
+
import { clearOnboardingModelPickerCancelStateForHandler, clearOnboardingPendingModelPickerTargetForHandler, completeOpenAiSubscriptionFromListenerForHandler, getOnboardingConfigValueForHandler, getOnboardingRuntimePostureForHandler, handleModelPickerCommitForHandler, handleOnboardingActionForHandler, handleOpenAiSubscriptionFinishForHandler, handleOpenAiSubscriptionStartForHandler, openModelPickerWithTargetForHandler, openProviderModelPickerWithTargetForHandler, refreshOnboardingHydrationForHandler, restartOnboardingExternalServicesIfNeededForHandler, restoreOnboardingModelPickerCancelStateForHandler, syncRuntimeFromOnboardingRequestForHandler, verifyOnboardingRuntimePostureForHandler, type OnboardingRuntimePosture } from './handler-onboarding.ts';
|
|
7
7
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
8
8
|
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
|
|
9
9
|
import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
@@ -325,6 +325,8 @@ export class InputHandler {
|
|
|
325
325
|
expandPrompt: (text: string) => this.expandPrompt(text),
|
|
326
326
|
openModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') =>
|
|
327
327
|
this.openModelPickerWithTarget(target, source),
|
|
328
|
+
openProviderModelPickerWithTarget: (target: ModelPickerTarget, source?: 'settings' | 'onboarding') =>
|
|
329
|
+
this.openProviderModelPickerWithTarget(target, source),
|
|
328
330
|
onModelPickerCommit: () => this.handleModelPickerCommit(),
|
|
329
331
|
onOnboardingAction: (action: OnboardingWizardAction) => { void this.handleOnboardingAction(action); },
|
|
330
332
|
},
|
|
@@ -404,6 +406,7 @@ export class InputHandler {
|
|
|
404
406
|
public clearOnboardingModelPickerCancelState(): void { clearOnboardingModelPickerCancelStateForHandler(this); }
|
|
405
407
|
public restoreOnboardingModelPickerCancelState(): void { restoreOnboardingModelPickerCancelStateForHandler(this); }
|
|
406
408
|
public openModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openModelPickerWithTargetForHandler(this, target, source); }
|
|
409
|
+
public openProviderModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openProviderModelPickerWithTargetForHandler(this, target, source); }
|
|
407
410
|
public handleModelPickerCommit(): boolean { return handleModelPickerCommitForHandler(this); }
|
|
408
411
|
public async handleOnboardingAction(action: OnboardingWizardAction): Promise<void> { await handleOnboardingActionForHandler(this, action); }
|
|
409
412
|
public async refreshOnboardingHydration(options: { readonly preserveValues?: boolean; readonly targetStepId?: string } = {}): Promise<void> { await refreshOnboardingHydrationForHandler(this, options); }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers/registry';
|
|
2
|
+
|
|
3
|
+
export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which config keys the model picker writes to on commit.
|
|
7
|
+
* 'main' -> provider.provider + provider.model (default)
|
|
8
|
+
* 'helper' -> helper.globalProvider + helper.globalModel (+ helper.enabled: true)
|
|
9
|
+
* 'tool' -> tools.llmProvider + tools.llmModel (+ tools.llmEnabled: true)
|
|
10
|
+
* 'tts' -> tts.llmProvider + tts.llmModel
|
|
11
|
+
*/
|
|
12
|
+
export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pricing tier filter.
|
|
16
|
+
* 'paid' matches ModelDefinition tiers 'standard' and 'premium' for forward-compat
|
|
17
|
+
* with future CatalogModel tiers ('free' | 'paid' | 'subscription').
|
|
18
|
+
*/
|
|
19
|
+
export type CategoryFilter = 'all' | 'free' | 'paid' | 'subscription';
|
|
20
|
+
|
|
21
|
+
export type ModelFamily =
|
|
22
|
+
| 'GPT'
|
|
23
|
+
| 'Claude'
|
|
24
|
+
| 'Gemini'
|
|
25
|
+
| 'Llama'
|
|
26
|
+
| 'Qwen'
|
|
27
|
+
| 'GLM'
|
|
28
|
+
| 'MiniMax'
|
|
29
|
+
| 'DeepSeek'
|
|
30
|
+
| 'Mistral'
|
|
31
|
+
| 'Command'
|
|
32
|
+
| 'Grok'
|
|
33
|
+
| 'Kimi'
|
|
34
|
+
| 'Other';
|
|
35
|
+
|
|
36
|
+
export type CapabilityFilter = 'reasoning' | 'toolUse' | 'multimodal' | 'none';
|
|
37
|
+
export type BenchmarkSort = 'none' | 'composite' | 'swe' | 'gpqa';
|
|
38
|
+
export type GroupByMode = 'provider' | 'family' | 'pricingTier' | 'qualityTier';
|
|
39
|
+
|
|
40
|
+
const FAMILY_PATTERNS: Array<{ pattern: RegExp; family: ModelFamily }> = [
|
|
41
|
+
{ pattern: /claude/i, family: 'Claude' },
|
|
42
|
+
{ pattern: /gpt|\bo1\b|\bo3\b|\bo4\b/i, family: 'GPT' },
|
|
43
|
+
{ pattern: /gemini/i, family: 'Gemini' },
|
|
44
|
+
{ pattern: /llama/i, family: 'Llama' },
|
|
45
|
+
{ pattern: /qwen/i, family: 'Qwen' },
|
|
46
|
+
{ pattern: /glm|chatglm/i, family: 'GLM' },
|
|
47
|
+
{ pattern: /minimax|abab/i, family: 'MiniMax' },
|
|
48
|
+
{ pattern: /deepseek/i, family: 'DeepSeek' },
|
|
49
|
+
{ pattern: /mistral|mixtral/i, family: 'Mistral' },
|
|
50
|
+
{ pattern: /command|cohere/i, family: 'Command' },
|
|
51
|
+
{ pattern: /grok/i, family: 'Grok' },
|
|
52
|
+
{ pattern: /kimi|moonshot/i, family: 'Kimi' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export function detectFamily(model: ModelDefinition): ModelFamily {
|
|
56
|
+
const haystack = `${model.id} ${model.displayName}`;
|
|
57
|
+
for (const { pattern, family } of FAMILY_PATTERNS) {
|
|
58
|
+
if (pattern.test(haystack)) return family;
|
|
59
|
+
}
|
|
60
|
+
return 'Other';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function tierToCategoryFilter(tier: string | undefined): CategoryFilter {
|
|
64
|
+
if (tier === 'free') return 'free';
|
|
65
|
+
if (tier === 'subscription') return 'subscription';
|
|
66
|
+
return 'paid';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PickerItem {
|
|
70
|
+
id: string;
|
|
71
|
+
label: string;
|
|
72
|
+
detail?: string;
|
|
73
|
+
isGroupHeader?: boolean;
|
|
74
|
+
qualityTier?: string;
|
|
75
|
+
isPinned?: boolean;
|
|
76
|
+
isFree?: boolean;
|
|
77
|
+
isConfigured?: boolean;
|
|
78
|
+
configuredVia?: 'env' | 'secrets' | 'subscription' | 'anonymous';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const POPULAR_PROVIDERS: ReadonlySet<string> = new Set([
|
|
82
|
+
'anthropic',
|
|
83
|
+
'google',
|
|
84
|
+
'groq',
|
|
85
|
+
'mistral',
|
|
86
|
+
'nvidia',
|
|
87
|
+
'ollama',
|
|
88
|
+
'openai',
|
|
89
|
+
'openrouter',
|
|
90
|
+
'synthetic',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
export interface FilteredModelsCache {
|
|
94
|
+
readonly modelsRef: ModelDefinition[];
|
|
95
|
+
readonly configuredProvidersKey: string;
|
|
96
|
+
readonly pinnedIdsKey: string;
|
|
97
|
+
readonly recentIdsKey: string;
|
|
98
|
+
readonly query: string;
|
|
99
|
+
readonly categoryFilter: CategoryFilter;
|
|
100
|
+
readonly capabilityFilter: CapabilityFilter;
|
|
101
|
+
readonly availableOnly: boolean;
|
|
102
|
+
readonly benchmarkSort: BenchmarkSort;
|
|
103
|
+
readonly groupBy: GroupByMode;
|
|
104
|
+
readonly result: ModelDefinition[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface FilteredProvidersCache {
|
|
108
|
+
readonly providersRef: string[];
|
|
109
|
+
readonly query: string;
|
|
110
|
+
readonly result: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ModelItemsCache {
|
|
114
|
+
readonly filteredModelsRef: ModelDefinition[];
|
|
115
|
+
readonly pinnedIdsKey: string;
|
|
116
|
+
readonly groupBy: GroupByMode;
|
|
117
|
+
readonly result: PickerItem[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ProviderItemsCache {
|
|
121
|
+
readonly filteredProvidersRef: string[];
|
|
122
|
+
readonly configuredProvidersKey: string;
|
|
123
|
+
readonly configuredViaKey: string;
|
|
124
|
+
readonly result: PickerItem[];
|
|
125
|
+
}
|