typeclaw 0.32.0 → 0.33.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/package.json +1 -1
- package/scripts/verify-procbind-sandbox.sh +61 -0
- package/src/agent/multimodal/look-at.ts +7 -5
- package/src/agent/plugin-tools.ts +47 -12
- package/src/agent/session-origin.ts +15 -9
- package/src/agent/system-prompt.ts +6 -0
- package/src/agent/tools/channel-fetch-attachment.ts +8 -7
- package/src/agent/tools/channel-history.ts +2 -0
- package/src/bundled-plugins/github-cli-auth/gh-command.ts +267 -13
- package/src/bundled-plugins/reviewer/skills/code-review.ts +11 -9
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +1 -0
- package/src/channels/adapters/slack-bot-reference.ts +9 -10
- package/src/channels/adapters/slack-bot.ts +29 -7
- package/src/channels/router.ts +89 -21
- package/src/cli/index.ts +42 -2
- package/src/cli/init.ts +267 -82
- package/src/cli/inspect.ts +5 -2
- package/src/cli/model.ts +5 -1
- package/src/cli/provider.ts +41 -10
- package/src/config/config.ts +23 -11
- package/src/config/providers.ts +304 -7
- package/src/container/start.ts +12 -7
- package/src/init/find-agent-dir.ts +44 -0
- package/src/init/index.ts +3 -34
- package/src/init/models-dev.ts +2 -0
- package/src/init/validate-api-key.ts +13 -0
- package/src/inspect/transcript-view.ts +33 -7
- package/src/sandbox/availability.ts +354 -2
- package/src/sandbox/build.ts +17 -7
- package/src/sandbox/index.ts +10 -1
- package/src/sandbox/policy.ts +27 -9
- package/src/secrets/oauth-xai.ts +342 -0
- package/src/secrets/storage.ts +2 -0
- package/src/skills/typeclaw-markdown-pdf/SKILL.md +64 -5
- package/typeclaw.schema.json +20 -2
package/src/cli/init.ts
CHANGED
|
@@ -4,11 +4,17 @@ import { cancel, confirm, intro, isCancel, log, note, password, select, spinner,
|
|
|
4
4
|
import { defineCommand } from 'citty'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
KNOWN_PROVIDER_VENDORS,
|
|
7
8
|
KNOWN_PROVIDERS,
|
|
9
|
+
listKnownProviderVendorIds,
|
|
10
|
+
providerIdsForVendor,
|
|
8
11
|
supportsApiKey as providerSupportsApiKey,
|
|
9
12
|
supportsOAuth as providerSupportsOAuth,
|
|
13
|
+
variantHint,
|
|
14
|
+
variantLabel,
|
|
10
15
|
type KnownModelRef,
|
|
11
16
|
type KnownProviderId,
|
|
17
|
+
type KnownProviderVendorId,
|
|
12
18
|
} from '@/config/providers'
|
|
13
19
|
import { checkDockerAvailable, type DockerAvailability } from '@/container'
|
|
14
20
|
import {
|
|
@@ -32,7 +38,12 @@ import {
|
|
|
32
38
|
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
33
39
|
import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
34
40
|
import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
|
|
35
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
API_KEY_DASHBOARD_URL,
|
|
43
|
+
MINIMAX_TOKEN_PLAN_DASHBOARD_URL,
|
|
44
|
+
validateApiKey,
|
|
45
|
+
type KeyValidationResult,
|
|
46
|
+
} from '@/init/validate-api-key'
|
|
36
47
|
|
|
37
48
|
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
38
49
|
import { CANCEL_SYMBOL, promptPrivateKeyPem } from './prompt-pem'
|
|
@@ -290,11 +301,13 @@ export const init = defineCommand({
|
|
|
290
301
|
|
|
291
302
|
interface WizardState {
|
|
292
303
|
catalog?: { options: ModelOption[]; source: 'models.dev' | 'curated'; warning?: string }
|
|
304
|
+
vendorId?: KnownProviderVendorId
|
|
293
305
|
providerId?: KnownProviderId
|
|
294
306
|
model?: ModelOption
|
|
295
307
|
reuseExisting?: boolean
|
|
296
308
|
authMethod?: 'api-key' | 'oauth'
|
|
297
309
|
llmAuth?: LLMAuth
|
|
310
|
+
visionVendorId?: KnownProviderVendorId
|
|
298
311
|
visionProviderId?: KnownProviderId
|
|
299
312
|
visionModel?: ModelOption
|
|
300
313
|
visionReuseExisting?: boolean
|
|
@@ -336,14 +349,16 @@ interface CollectedInputs {
|
|
|
336
349
|
}
|
|
337
350
|
|
|
338
351
|
type StepId =
|
|
339
|
-
| 'pick-
|
|
340
|
-
| 'pick-
|
|
352
|
+
| 'pick-vendor'
|
|
353
|
+
| 'pick-provider-variant'
|
|
341
354
|
| 'reuse-existing-key'
|
|
342
355
|
| 'pick-auth-method'
|
|
356
|
+
| 'pick-model'
|
|
343
357
|
| 'enter-api-key'
|
|
344
|
-
| 'pick-vision-
|
|
345
|
-
| 'pick-vision-
|
|
358
|
+
| 'pick-vision-vendor'
|
|
359
|
+
| 'pick-vision-provider-variant'
|
|
346
360
|
| 'pick-vision-auth-method'
|
|
361
|
+
| 'pick-vision-model'
|
|
347
362
|
| 'enter-vision-api-key'
|
|
348
363
|
| 'pick-channel'
|
|
349
364
|
| 'reuse-existing-channel'
|
|
@@ -353,7 +368,15 @@ export interface WizardPrompts {
|
|
|
353
368
|
loadCatalog: () => Promise<NonNullable<WizardState['catalog']>>
|
|
354
369
|
readExistingApiKey: (cwd: string, providerId: KnownProviderId) => Promise<string | null>
|
|
355
370
|
hasExistingOAuthCredentials: (cwd: string, providerId: KnownProviderId) => Promise<boolean>
|
|
356
|
-
|
|
371
|
+
pickVendor: (
|
|
372
|
+
options: ModelOption[],
|
|
373
|
+
initial: KnownProviderVendorId | undefined,
|
|
374
|
+
) => Promise<StepResult<KnownProviderVendorId>>
|
|
375
|
+
pickProviderVariant: (
|
|
376
|
+
vendorId: KnownProviderVendorId,
|
|
377
|
+
options: ModelOption[],
|
|
378
|
+
initial: KnownProviderId | undefined,
|
|
379
|
+
) => Promise<StepResult<KnownProviderId>>
|
|
357
380
|
pickModel: (
|
|
358
381
|
options: ModelOption[],
|
|
359
382
|
providerId: KnownProviderId,
|
|
@@ -365,10 +388,15 @@ export interface WizardPrompts {
|
|
|
365
388
|
) => Promise<StepResult<'api-key' | 'oauth'>>
|
|
366
389
|
askApiKey: (provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]) => Promise<StepResult<string>>
|
|
367
390
|
validateApiKey: (providerId: KnownProviderId, key: string) => Promise<KeyValidationResult>
|
|
368
|
-
|
|
391
|
+
pickVisionVendor: (
|
|
392
|
+
options: ModelOption[],
|
|
393
|
+
initial: KnownProviderVendorId | undefined,
|
|
394
|
+
) => Promise<StepResult<KnownProviderVendorId | 'skip'>>
|
|
395
|
+
pickVisionProviderVariant: (
|
|
396
|
+
vendorId: KnownProviderVendorId,
|
|
369
397
|
options: ModelOption[],
|
|
370
398
|
initial: KnownProviderId | undefined,
|
|
371
|
-
) => Promise<StepResult<KnownProviderId
|
|
399
|
+
) => Promise<StepResult<KnownProviderId>>
|
|
372
400
|
pickVisionModel: (
|
|
373
401
|
options: ModelOption[],
|
|
374
402
|
providerId: KnownProviderId,
|
|
@@ -399,12 +427,14 @@ export const defaultWizardPrompts: WizardPrompts = {
|
|
|
399
427
|
loadCatalog,
|
|
400
428
|
readExistingApiKey: readExistingProviderApiKey,
|
|
401
429
|
hasExistingOAuthCredentials,
|
|
402
|
-
|
|
430
|
+
pickVendor,
|
|
431
|
+
pickProviderVariant,
|
|
403
432
|
pickModel: pickModelForProvider,
|
|
404
433
|
pickAuthMethod,
|
|
405
434
|
askApiKey,
|
|
406
435
|
validateApiKey,
|
|
407
|
-
|
|
436
|
+
pickVisionVendor,
|
|
437
|
+
pickVisionProviderVariant,
|
|
408
438
|
pickVisionModel,
|
|
409
439
|
pickChannel,
|
|
410
440
|
hasExistingChannelSecrets,
|
|
@@ -428,7 +458,7 @@ export async function collectWizardInputs(
|
|
|
428
458
|
const reset = options.reset === true
|
|
429
459
|
const catalog = await prompts.loadCatalog()
|
|
430
460
|
const state: WizardState = { catalog }
|
|
431
|
-
let step: StepId = 'pick-
|
|
461
|
+
let step: StepId = 'pick-vendor'
|
|
432
462
|
let pendingBackOrigin: StepId | null = null
|
|
433
463
|
let oauthCredentialsSaved = false
|
|
434
464
|
|
|
@@ -463,29 +493,39 @@ export async function collectWizardInputs(
|
|
|
463
493
|
|
|
464
494
|
while (true) {
|
|
465
495
|
switch (step) {
|
|
466
|
-
case 'pick-
|
|
467
|
-
const result = onResult(step, await prompts.
|
|
496
|
+
case 'pick-vendor': {
|
|
497
|
+
const result = onResult(step, await prompts.pickVendor(catalog.options, state.vendorId))
|
|
468
498
|
if (result.kind === 'back') {
|
|
469
499
|
break
|
|
470
500
|
}
|
|
471
|
-
if (state.
|
|
501
|
+
if (state.vendorId !== result.value) {
|
|
502
|
+
state.providerId = undefined
|
|
472
503
|
state.model = undefined
|
|
473
504
|
state.reuseExisting = undefined
|
|
474
505
|
state.authMethod = undefined
|
|
475
506
|
state.llmAuth = undefined
|
|
476
507
|
}
|
|
477
|
-
state.
|
|
478
|
-
step = 'pick-
|
|
508
|
+
state.vendorId = result.value
|
|
509
|
+
step = 'pick-provider-variant'
|
|
479
510
|
break
|
|
480
511
|
}
|
|
481
512
|
|
|
482
|
-
case 'pick-
|
|
483
|
-
const result = onResult(
|
|
513
|
+
case 'pick-provider-variant': {
|
|
514
|
+
const result = onResult(
|
|
515
|
+
step,
|
|
516
|
+
await prompts.pickProviderVariant(state.vendorId!, catalog.options, state.providerId),
|
|
517
|
+
)
|
|
484
518
|
if (result.kind === 'back') {
|
|
485
|
-
step = 'pick-
|
|
519
|
+
step = 'pick-vendor'
|
|
486
520
|
break
|
|
487
521
|
}
|
|
488
|
-
state.
|
|
522
|
+
if (state.providerId !== result.value) {
|
|
523
|
+
state.model = undefined
|
|
524
|
+
state.reuseExisting = undefined
|
|
525
|
+
state.authMethod = undefined
|
|
526
|
+
state.llmAuth = undefined
|
|
527
|
+
}
|
|
528
|
+
state.providerId = result.value
|
|
489
529
|
step = 'reuse-existing-key'
|
|
490
530
|
break
|
|
491
531
|
}
|
|
@@ -503,7 +543,7 @@ export async function collectWizardInputs(
|
|
|
503
543
|
log.info(`Reusing existing ${provider.name} API key from secrets.json.`)
|
|
504
544
|
state.llmAuth = { kind: 'api-key', apiKey: existingApiKey }
|
|
505
545
|
state.reuseExisting = true
|
|
506
|
-
step =
|
|
546
|
+
step = 'pick-model'
|
|
507
547
|
break
|
|
508
548
|
}
|
|
509
549
|
state.reuseExisting = false
|
|
@@ -517,12 +557,12 @@ export async function collectWizardInputs(
|
|
|
517
557
|
const result = onResult(step, await prompts.pickAuthMethod(provider, state.authMethod))
|
|
518
558
|
if (result.kind === 'back') {
|
|
519
559
|
// Skip past `reuse-existing-key` — it is a silent auto-resume
|
|
520
|
-
// step with no user prompt, so unwind directly to
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
// back branch.
|
|
525
|
-
step =
|
|
560
|
+
// step with no user prompt, so unwind directly to the prior
|
|
561
|
+
// user-visible step (the variant picker when it was interactive,
|
|
562
|
+
// else the vendor picker). This only fires when pickAuthMethod was
|
|
563
|
+
// an interactive choice (dual-auth providers); single-method
|
|
564
|
+
// providers return autoValue and never reach the back branch.
|
|
565
|
+
step = stepBeforeAuthMethod(state)
|
|
526
566
|
break
|
|
527
567
|
}
|
|
528
568
|
state.authMethod = result.value
|
|
@@ -530,21 +570,24 @@ export async function collectWizardInputs(
|
|
|
530
570
|
// Auto-resume: skip the browser flow when OAuth credentials are
|
|
531
571
|
// already on disk from a prior partial run. The fresh-tokens path
|
|
532
572
|
// and the resume path both leave `state.llmAuth = oauth-completed`,
|
|
533
|
-
// so downstream steps (vision, channel, scaffold) can't tell
|
|
534
|
-
// difference. `--reset` short-circuits this by making
|
|
573
|
+
// so downstream steps (model, vision, channel, scaffold) can't tell
|
|
574
|
+
// the difference. `--reset` short-circuits this by making
|
|
535
575
|
// `hasExistingOAuth` return false.
|
|
536
576
|
if (await hasExistingOAuth(state.providerId!)) {
|
|
537
577
|
log.info(`Reusing existing ${provider.name} OAuth credentials from secrets.json.`)
|
|
538
578
|
state.llmAuth = { kind: 'oauth-completed' }
|
|
539
|
-
step =
|
|
579
|
+
step = 'pick-model'
|
|
540
580
|
break
|
|
541
581
|
}
|
|
542
582
|
// Run the browser login eagerly so the user sees the OAuth URL the
|
|
543
583
|
// moment they pick "OAuth (browser login)" — not at the end of the
|
|
544
|
-
// wizard.
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
|
|
584
|
+
// wizard. The model isn't picked yet at this point, so we hand the
|
|
585
|
+
// login the provider's first model ref purely to resolve its
|
|
586
|
+
// `oauthProviderId` (login ignores the model otherwise). On failure
|
|
587
|
+
// we ask the user how to recover (retry / fall back to API key /
|
|
588
|
+
// abort) instead of dumping them back into the auth method picker
|
|
589
|
+
// with no guidance.
|
|
590
|
+
const login = await runOAuthLoginSafely(prompts, provider, cwd, oauthDiscoveryRef(state.providerId!))
|
|
548
591
|
if (!login.ok) {
|
|
549
592
|
const recovery = await prompts.askOAuthFailureRecovery(
|
|
550
593
|
provider,
|
|
@@ -560,24 +603,38 @@ export async function collectWizardInputs(
|
|
|
560
603
|
if (recovery === 'abort') abort()
|
|
561
604
|
state.authMethod = recovery === 'api-key' ? 'api-key' : undefined
|
|
562
605
|
state.llmAuth = undefined
|
|
563
|
-
step = recovery === 'api-key' ? '
|
|
606
|
+
step = recovery === 'api-key' ? 'pick-model' : 'pick-auth-method'
|
|
564
607
|
break
|
|
565
608
|
}
|
|
566
609
|
oauthCredentialsSaved = true
|
|
567
610
|
state.llmAuth = { kind: 'oauth-completed' }
|
|
568
|
-
step =
|
|
611
|
+
step = 'pick-model'
|
|
569
612
|
} else {
|
|
570
|
-
step = '
|
|
613
|
+
step = 'pick-model'
|
|
571
614
|
}
|
|
572
615
|
break
|
|
573
616
|
}
|
|
574
617
|
|
|
618
|
+
case 'pick-model': {
|
|
619
|
+
const result = onResult(step, await prompts.pickModel(catalog.options, state.providerId!, state.model?.ref))
|
|
620
|
+
if (result.kind === 'back') {
|
|
621
|
+
step = stepBeforeModel(state)
|
|
622
|
+
break
|
|
623
|
+
}
|
|
624
|
+
state.model = result.value
|
|
625
|
+
// OAuth and reused api-key already minted `state.llmAuth`; only a
|
|
626
|
+
// freshly-chosen api-key still needs the key prompt.
|
|
627
|
+
step =
|
|
628
|
+
state.authMethod === 'api-key' && state.reuseExisting !== true ? 'enter-api-key' : stepAfterDefaultAuth(state)
|
|
629
|
+
break
|
|
630
|
+
}
|
|
631
|
+
|
|
575
632
|
case 'enter-api-key': {
|
|
576
633
|
const providerId = state.providerId!
|
|
577
634
|
const provider = KNOWN_PROVIDERS[providerId]
|
|
578
635
|
const result = onResult(step, await prompts.askApiKey(provider))
|
|
579
636
|
if (result.kind === 'back') {
|
|
580
|
-
step = 'pick-
|
|
637
|
+
step = 'pick-model'
|
|
581
638
|
break
|
|
582
639
|
}
|
|
583
640
|
const verdict = await runApiKeyValidation(prompts, providerId, result.value)
|
|
@@ -590,14 +647,15 @@ export async function collectWizardInputs(
|
|
|
590
647
|
break
|
|
591
648
|
}
|
|
592
649
|
|
|
593
|
-
case 'pick-vision-
|
|
650
|
+
case 'pick-vision-vendor': {
|
|
594
651
|
const visionOptions = catalog.options.filter((o) => o.supportsVision)
|
|
595
|
-
const result = onResult(step, await prompts.
|
|
652
|
+
const result = onResult(step, await prompts.pickVisionVendor(visionOptions, state.visionVendorId))
|
|
596
653
|
if (result.kind === 'back') {
|
|
597
654
|
step = stepBeforeVision(state)
|
|
598
655
|
break
|
|
599
656
|
}
|
|
600
657
|
if (result.value === 'skip') {
|
|
658
|
+
state.visionVendorId = undefined
|
|
601
659
|
state.visionProviderId = undefined
|
|
602
660
|
state.visionModel = undefined
|
|
603
661
|
state.visionLlmAuth = undefined
|
|
@@ -606,6 +664,28 @@ export async function collectWizardInputs(
|
|
|
606
664
|
step = 'pick-channel'
|
|
607
665
|
break
|
|
608
666
|
}
|
|
667
|
+
if (state.visionVendorId !== result.value) {
|
|
668
|
+
state.visionProviderId = undefined
|
|
669
|
+
state.visionModel = undefined
|
|
670
|
+
state.visionReuseExisting = undefined
|
|
671
|
+
state.visionAuthMethod = undefined
|
|
672
|
+
state.visionLlmAuth = undefined
|
|
673
|
+
}
|
|
674
|
+
state.visionVendorId = result.value
|
|
675
|
+
step = 'pick-vision-provider-variant'
|
|
676
|
+
break
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
case 'pick-vision-provider-variant': {
|
|
680
|
+
const visionOptions = catalog.options.filter((o) => o.supportsVision)
|
|
681
|
+
const result = onResult(
|
|
682
|
+
step,
|
|
683
|
+
await prompts.pickVisionProviderVariant(state.visionVendorId!, visionOptions, state.visionProviderId),
|
|
684
|
+
)
|
|
685
|
+
if (result.kind === 'back') {
|
|
686
|
+
step = 'pick-vision-vendor'
|
|
687
|
+
break
|
|
688
|
+
}
|
|
609
689
|
if (state.visionProviderId !== result.value) {
|
|
610
690
|
state.visionModel = undefined
|
|
611
691
|
state.visionReuseExisting = undefined
|
|
@@ -624,7 +704,7 @@ export async function collectWizardInputs(
|
|
|
624
704
|
await prompts.pickVisionModel(visionOptions, state.visionProviderId!, state.visionModel?.ref),
|
|
625
705
|
)
|
|
626
706
|
if (result.kind === 'back') {
|
|
627
|
-
step = 'pick-vision-provider'
|
|
707
|
+
step = 'pick-vision-provider-variant'
|
|
628
708
|
break
|
|
629
709
|
}
|
|
630
710
|
state.visionModel = result.value
|
|
@@ -806,14 +886,33 @@ function channelDisplayName(choice: Exclude<ChannelChoice, 'none'>): string {
|
|
|
806
886
|
}
|
|
807
887
|
}
|
|
808
888
|
|
|
889
|
+
// Model is the last default-track step before the vision/channel branch, so
|
|
890
|
+
// vendor/variant/auth all sit upstream of it. Reached after `pick-model`
|
|
891
|
+
// (api-key path also passes through `enter-api-key` first).
|
|
809
892
|
function stepAfterDefaultAuth(state: WizardState): StepId {
|
|
810
|
-
return state.model?.supportsVision === false ? 'pick-vision-
|
|
893
|
+
return state.model?.supportsVision === false ? 'pick-vision-vendor' : 'pick-channel'
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Back-target when leaving `pick-auth-method`: the variant picker when it was
|
|
897
|
+
// interactive (multi-provider vendor), else the vendor picker. The
|
|
898
|
+
// `reuse-existing-key` step in between is a silent auto-resume with no prompt.
|
|
899
|
+
function stepBeforeAuthMethod(state: WizardState): StepId {
|
|
900
|
+
return providerIdsForVendor(state.vendorId!).length > 1 ? 'pick-provider-variant' : 'pick-vendor'
|
|
811
901
|
}
|
|
812
902
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
903
|
+
// Back-target when leaving `pick-model`: the auth picker when it was
|
|
904
|
+
// interactive (dual-auth provider), else fall back past the silent
|
|
905
|
+
// auto-resume/auth steps to the prior user-visible picker.
|
|
906
|
+
function stepBeforeModel(state: WizardState): StepId {
|
|
907
|
+
const provider = KNOWN_PROVIDERS[state.providerId!]
|
|
908
|
+
if (providerSupportsApiKey(provider) && providerSupportsOAuth(provider)) return 'pick-auth-method'
|
|
909
|
+
return stepBeforeAuthMethod(state)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Back-target when leaving the vision track to the default track. With model
|
|
913
|
+
// now the final default-track step, that is always `pick-model`.
|
|
914
|
+
function stepBeforeVision(_state: WizardState): StepId {
|
|
915
|
+
return 'pick-model'
|
|
817
916
|
}
|
|
818
917
|
|
|
819
918
|
function stepBeforePickChannel(state: WizardState): StepId {
|
|
@@ -824,10 +923,18 @@ function stepBeforePickChannel(state: WizardState): StepId {
|
|
|
824
923
|
if (state.visionAuthMethod === 'oauth') return 'pick-vision-auth-method'
|
|
825
924
|
return 'pick-vision-model'
|
|
826
925
|
}
|
|
827
|
-
if (state.model?.supportsVision === false) return 'pick-vision-
|
|
926
|
+
if (state.model?.supportsVision === false) return 'pick-vision-vendor'
|
|
828
927
|
return stepBeforeVision(state)
|
|
829
928
|
}
|
|
830
929
|
|
|
930
|
+
function oauthDiscoveryRef(providerId: KnownProviderId): KnownModelRef {
|
|
931
|
+
// OAuth login only reads the provider's `oauthProviderId` from the ref, so
|
|
932
|
+
// any registered model for the provider works as the discovery handle.
|
|
933
|
+
const modelId = Object.keys(KNOWN_PROVIDERS[providerId].models)[0]
|
|
934
|
+
if (modelId === undefined) throw new Error(`Provider ${providerId} has no registered models for OAuth discovery`)
|
|
935
|
+
return `${providerId}/${modelId}` as KnownModelRef
|
|
936
|
+
}
|
|
937
|
+
|
|
831
938
|
async function loadCatalog(): Promise<NonNullable<WizardState['catalog']>> {
|
|
832
939
|
const s = spinner()
|
|
833
940
|
s.start('Loading model catalog from models.dev...')
|
|
@@ -840,15 +947,41 @@ async function loadCatalog(): Promise<NonNullable<WizardState['catalog']>> {
|
|
|
840
947
|
return warning !== undefined ? { options, source, warning } : { options, source }
|
|
841
948
|
}
|
|
842
949
|
|
|
843
|
-
async function
|
|
950
|
+
async function pickVendor(
|
|
844
951
|
options: ModelOption[],
|
|
845
|
-
initial:
|
|
846
|
-
): Promise<StepResult<
|
|
847
|
-
const
|
|
952
|
+
initial: KnownProviderVendorId | undefined,
|
|
953
|
+
): Promise<StepResult<KnownProviderVendorId>> {
|
|
954
|
+
const vendors = uniqueVendors(options)
|
|
848
955
|
const choice = await select({
|
|
849
956
|
message: 'Pick an LLM provider',
|
|
850
|
-
options:
|
|
851
|
-
|
|
957
|
+
options: vendors.map((id) => ({
|
|
958
|
+
value: id,
|
|
959
|
+
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
960
|
+
hint: vendorHint(id, options),
|
|
961
|
+
})),
|
|
962
|
+
initialValue: initial ?? vendors[0],
|
|
963
|
+
})
|
|
964
|
+
if (isCancel(choice)) return back()
|
|
965
|
+
return value(choice)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function pickProviderVariant(
|
|
969
|
+
vendorId: KnownProviderVendorId,
|
|
970
|
+
options: ModelOption[],
|
|
971
|
+
initial: KnownProviderId | undefined,
|
|
972
|
+
): Promise<StepResult<KnownProviderId>> {
|
|
973
|
+
const variants = providersForVendorInCatalog(vendorId, options)
|
|
974
|
+
if (variants.length === 0) throw new Error(`Internal error: vendor ${vendorId} has no providers in the catalog`)
|
|
975
|
+
if (variants.length === 1) return autoValue(variants[0]!)
|
|
976
|
+
const choice = await select<KnownProviderId>({
|
|
977
|
+
message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
|
|
978
|
+
options: variants.map((id) => {
|
|
979
|
+
const hint = variantHint(vendorId, id)
|
|
980
|
+
return hint !== undefined
|
|
981
|
+
? { value: id, label: variantLabel(vendorId, id), hint }
|
|
982
|
+
: { value: id, label: variantLabel(vendorId, id) }
|
|
983
|
+
}),
|
|
984
|
+
initialValue: initial ?? variants[0],
|
|
852
985
|
})
|
|
853
986
|
if (isCancel(choice)) return back()
|
|
854
987
|
return value(choice)
|
|
@@ -860,7 +993,11 @@ async function pickModelForProvider(
|
|
|
860
993
|
initial: KnownModelRef | undefined,
|
|
861
994
|
): Promise<StepResult<ModelOption>> {
|
|
862
995
|
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
863
|
-
|
|
996
|
+
// select<string>, not select<KnownModelRef>: clack's Option<Value> is a
|
|
997
|
+
// distributive conditional type, so a large KnownModelRef union explodes into
|
|
998
|
+
// a per-literal option union that no longer accepts `value: ref`. The runtime
|
|
999
|
+
// value is the ref string and is re-narrowed via `candidates.find` below.
|
|
1000
|
+
const choice = await select<string>({
|
|
864
1001
|
message: `Pick a ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
865
1002
|
options: candidates.map((o) => ({
|
|
866
1003
|
value: o.ref,
|
|
@@ -896,38 +1033,47 @@ async function pickAuthMethod(
|
|
|
896
1033
|
return autoValue(supportsOAuth ? 'oauth' : 'api-key')
|
|
897
1034
|
}
|
|
898
1035
|
|
|
899
|
-
async function
|
|
1036
|
+
async function pickVisionVendor(
|
|
900
1037
|
options: ModelOption[],
|
|
901
|
-
initial:
|
|
902
|
-
): Promise<StepResult<
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
1038
|
+
initial: KnownProviderVendorId | undefined,
|
|
1039
|
+
): Promise<StepResult<KnownProviderVendorId | 'skip'>> {
|
|
1040
|
+
const vendors = uniqueVendors(options)
|
|
1041
|
+
if (vendors.length === 0) {
|
|
905
1042
|
log.warn('No vision-capable models available; skipping vision profile.')
|
|
906
1043
|
return autoValue('skip')
|
|
907
1044
|
}
|
|
908
|
-
const choice = await select<
|
|
1045
|
+
const choice = await select<KnownProviderVendorId | 'skip'>({
|
|
909
1046
|
message: 'Your model is text-only. Pick a provider for the `vision` profile (used for image input)',
|
|
910
1047
|
options: [
|
|
911
|
-
...
|
|
912
|
-
value: id as
|
|
913
|
-
label:
|
|
914
|
-
hint:
|
|
1048
|
+
...vendors.map((id) => ({
|
|
1049
|
+
value: id as KnownProviderVendorId | 'skip',
|
|
1050
|
+
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
1051
|
+
hint: vendorHint(id, options),
|
|
915
1052
|
})),
|
|
916
1053
|
{ value: 'skip', label: 'Skip — no vision support', hint: 'add later with `typeclaw model set vision <ref>`' },
|
|
917
1054
|
],
|
|
918
|
-
initialValue: initial ??
|
|
1055
|
+
initialValue: initial ?? vendors[0],
|
|
919
1056
|
})
|
|
920
1057
|
if (isCancel(choice)) return back()
|
|
921
1058
|
return value(choice)
|
|
922
1059
|
}
|
|
923
1060
|
|
|
1061
|
+
async function pickVisionProviderVariant(
|
|
1062
|
+
vendorId: KnownProviderVendorId,
|
|
1063
|
+
options: ModelOption[],
|
|
1064
|
+
initial: KnownProviderId | undefined,
|
|
1065
|
+
): Promise<StepResult<KnownProviderId>> {
|
|
1066
|
+
return pickProviderVariant(vendorId, options, initial)
|
|
1067
|
+
}
|
|
1068
|
+
|
|
924
1069
|
async function pickVisionModel(
|
|
925
1070
|
options: ModelOption[],
|
|
926
1071
|
providerId: KnownProviderId,
|
|
927
1072
|
initial: KnownModelRef | undefined,
|
|
928
1073
|
): Promise<StepResult<ModelOption>> {
|
|
929
1074
|
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
930
|
-
|
|
1075
|
+
// select<string> for the same distributive-Option reason as pickModelForProvider.
|
|
1076
|
+
const choice = await select<string>({
|
|
931
1077
|
message: `Pick a vision-capable ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
932
1078
|
options: candidates.map((o) => ({
|
|
933
1079
|
value: o.ref,
|
|
@@ -989,21 +1135,52 @@ async function runApiKeyValidation(
|
|
|
989
1135
|
|
|
990
1136
|
async function askApiKey(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): Promise<StepResult<string>> {
|
|
991
1137
|
const providerId = provider.id as KnownProviderId
|
|
992
|
-
const
|
|
1138
|
+
const keySurface = await pickMinimaxKeySurface(providerId)
|
|
1139
|
+
if (keySurface.kind === 'back') return back()
|
|
1140
|
+
const label = keySurface.kind === 'chosen' ? keySurface.label : `${provider.name} API`
|
|
1141
|
+
const dashboardUrl = keySurface.kind === 'chosen' ? keySurface.dashboardUrl : API_KEY_DASHBOARD_URL[providerId]
|
|
993
1142
|
if (dashboardUrl) {
|
|
994
1143
|
note(
|
|
995
1144
|
[`Don't have a key yet?`, `Get one at ${dashboardUrl}`, `Then come back and paste it below.`].join('\n'),
|
|
996
|
-
`Get a ${
|
|
1145
|
+
`Get a ${label} key`,
|
|
997
1146
|
)
|
|
998
1147
|
}
|
|
999
1148
|
const apiKey = await password({
|
|
1000
|
-
message: `Put your ${
|
|
1149
|
+
message: `Put your ${label} key (will be saved to secrets.json)`,
|
|
1001
1150
|
validate: (v) => (v && v.length > 0 ? undefined : 'API key is required'),
|
|
1002
1151
|
})
|
|
1003
1152
|
if (isCancel(apiKey)) return back()
|
|
1004
1153
|
return value(apiKey)
|
|
1005
1154
|
}
|
|
1006
1155
|
|
|
1156
|
+
type MinimaxKeySurface =
|
|
1157
|
+
| { kind: 'back' }
|
|
1158
|
+
| { kind: 'default' }
|
|
1159
|
+
| { kind: 'chosen'; label: string; dashboardUrl: string }
|
|
1160
|
+
|
|
1161
|
+
// MiniMax issues keys on two billing surfaces (pay-as-you-go vs Token Plan
|
|
1162
|
+
// subscription) that store in the same provider slot but live on different
|
|
1163
|
+
// dashboard pages. For the `minimax` provider, let the user say which one they
|
|
1164
|
+
// have so the deep-link points at the right page; every other provider skips
|
|
1165
|
+
// straight to the paste prompt. The picked surface only changes the label and
|
|
1166
|
+
// dashboard URL — the pasted key is stored identically either way.
|
|
1167
|
+
async function pickMinimaxKeySurface(providerId: KnownProviderId): Promise<MinimaxKeySurface> {
|
|
1168
|
+
if (providerId !== 'minimax') return { kind: 'default' }
|
|
1169
|
+
const choice = await select<'paygo' | 'token-plan'>({
|
|
1170
|
+
message: 'Which MiniMax key do you have?',
|
|
1171
|
+
options: [
|
|
1172
|
+
{ value: 'paygo', label: 'Pay-as-you-go API key', hint: 'billed per token' },
|
|
1173
|
+
{ value: 'token-plan', label: 'Token Plan API key', hint: 'subscription / Credits (sk-cp-…)' },
|
|
1174
|
+
],
|
|
1175
|
+
initialValue: 'paygo',
|
|
1176
|
+
})
|
|
1177
|
+
if (isCancel(choice)) return { kind: 'back' }
|
|
1178
|
+
if (choice === 'token-plan') {
|
|
1179
|
+
return { kind: 'chosen', label: 'MiniMax Token Plan', dashboardUrl: MINIMAX_TOKEN_PLAN_DASHBOARD_URL }
|
|
1180
|
+
}
|
|
1181
|
+
return { kind: 'chosen', label: 'MiniMax pay-as-you-go API', dashboardUrl: API_KEY_DASHBOARD_URL.minimax! }
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1007
1184
|
async function askOAuthFailureRecovery(
|
|
1008
1185
|
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
1009
1186
|
reason: string,
|
|
@@ -1486,15 +1663,23 @@ function reportHatching(event: Extract<InitStepEvent, { step: 'hatching' }>): vo
|
|
|
1486
1663
|
}
|
|
1487
1664
|
}
|
|
1488
1665
|
|
|
1489
|
-
function
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1666
|
+
function providersInCatalog(options: ModelOption[]): Set<KnownProviderId> {
|
|
1667
|
+
return new Set(options.map((o) => o.providerId))
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Vendors with at least one provider present in the catalog, ordered by the
|
|
1671
|
+
// product priority encoded in `KNOWN_PROVIDER_VENDORS` declaration order (not
|
|
1672
|
+
// catalog iteration order).
|
|
1673
|
+
function uniqueVendors(options: ModelOption[]): KnownProviderVendorId[] {
|
|
1674
|
+
const present = providersInCatalog(options)
|
|
1675
|
+
return listKnownProviderVendorIds().filter((vendorId) =>
|
|
1676
|
+
providerIdsForVendor(vendorId).some((providerId) => present.has(providerId)),
|
|
1677
|
+
)
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function providersForVendorInCatalog(vendorId: KnownProviderVendorId, options: ModelOption[]): KnownProviderId[] {
|
|
1681
|
+
const present = providersInCatalog(options)
|
|
1682
|
+
return providerIdsForVendor(vendorId).filter((providerId) => present.has(providerId))
|
|
1498
1683
|
}
|
|
1499
1684
|
|
|
1500
1685
|
// Per-provider recommended model refs. Surfaces a "(Recommended)" suffix in
|
|
@@ -1528,10 +1713,10 @@ function formatModelHint(o: ModelOption): string {
|
|
|
1528
1713
|
return parts.join(' · ')
|
|
1529
1714
|
}
|
|
1530
1715
|
|
|
1531
|
-
function
|
|
1532
|
-
const
|
|
1533
|
-
const apiKey = providerSupportsApiKey(
|
|
1534
|
-
const oauth = providerSupportsOAuth(
|
|
1716
|
+
function vendorHint(vendorId: KnownProviderVendorId, options: ModelOption[]): string {
|
|
1717
|
+
const providers = providersForVendorInCatalog(vendorId, options)
|
|
1718
|
+
const apiKey = providers.some((id) => providerSupportsApiKey(KNOWN_PROVIDERS[id]))
|
|
1719
|
+
const oauth = providers.some((id) => providerSupportsOAuth(KNOWN_PROVIDERS[id]))
|
|
1535
1720
|
if (apiKey && oauth) return 'API key or OAuth'
|
|
1536
1721
|
if (oauth) return 'OAuth login'
|
|
1537
1722
|
return 'API key'
|
package/src/cli/inspect.ts
CHANGED
|
@@ -299,9 +299,12 @@ async function clackSelectSession(
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
function itemLabel(item: ViewerItem): string {
|
|
302
|
+
// clack's select already draws a radio dot (●/○) per option; a leading status
|
|
303
|
+
// dot here doubled it into a confusing "● ○". Keep rows glyph-free; the live
|
|
304
|
+
// row uses ▸ (not a dot) to stay distinct from the radio cursor.
|
|
302
305
|
if (item.kind === 'logs') return `${c.dim('▤')} container logs`
|
|
303
|
-
if (item.kind === 'tui') return `${c.green('
|
|
304
|
-
return
|
|
306
|
+
if (item.kind === 'tui') return `${c.green('▸')} ${c.bold('live TUI')} ${sessionRowLabel(item.summary)}`
|
|
307
|
+
return sessionRowLabel(item.summary)
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
function itemHint(item: ViewerItem): { hint: string } {
|
package/src/cli/model.ts
CHANGED
|
@@ -230,7 +230,11 @@ async function pickModelRef(cwd: string): Promise<string> {
|
|
|
230
230
|
}
|
|
231
231
|
continue
|
|
232
232
|
}
|
|
233
|
-
|
|
233
|
+
// select<string>, not the KnownModelRef union: clack's Option<Value> is a
|
|
234
|
+
// distributive conditional type and a large ref union breaks `value: ref`
|
|
235
|
+
// assignability. Values are ref strings (+ the sentinel) and stay correct
|
|
236
|
+
// at runtime — the sentinel check and `return choice` below are unaffected.
|
|
237
|
+
const choice = await select<string>({
|
|
234
238
|
message: 'Pick a model',
|
|
235
239
|
options: [
|
|
236
240
|
...refs.map((ref) => ({
|