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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/scripts/verify-procbind-sandbox.sh +61 -0
  3. package/src/agent/multimodal/look-at.ts +7 -5
  4. package/src/agent/plugin-tools.ts +47 -12
  5. package/src/agent/session-origin.ts +15 -9
  6. package/src/agent/system-prompt.ts +6 -0
  7. package/src/agent/tools/channel-fetch-attachment.ts +8 -7
  8. package/src/agent/tools/channel-history.ts +2 -0
  9. package/src/bundled-plugins/github-cli-auth/gh-command.ts +267 -13
  10. package/src/bundled-plugins/reviewer/skills/code-review.ts +11 -9
  11. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +1 -0
  12. package/src/channels/adapters/slack-bot-reference.ts +9 -10
  13. package/src/channels/adapters/slack-bot.ts +29 -7
  14. package/src/channels/router.ts +89 -21
  15. package/src/cli/index.ts +42 -2
  16. package/src/cli/init.ts +267 -82
  17. package/src/cli/inspect.ts +5 -2
  18. package/src/cli/model.ts +5 -1
  19. package/src/cli/provider.ts +41 -10
  20. package/src/config/config.ts +23 -11
  21. package/src/config/providers.ts +304 -7
  22. package/src/container/start.ts +12 -7
  23. package/src/init/find-agent-dir.ts +44 -0
  24. package/src/init/index.ts +3 -34
  25. package/src/init/models-dev.ts +2 -0
  26. package/src/init/validate-api-key.ts +13 -0
  27. package/src/inspect/transcript-view.ts +33 -7
  28. package/src/sandbox/availability.ts +354 -2
  29. package/src/sandbox/build.ts +17 -7
  30. package/src/sandbox/index.ts +10 -1
  31. package/src/sandbox/policy.ts +27 -9
  32. package/src/secrets/oauth-xai.ts +342 -0
  33. package/src/secrets/storage.ts +2 -0
  34. package/src/skills/typeclaw-markdown-pdf/SKILL.md +64 -5
  35. package/typeclaw.schema.json +20 -2
@@ -2,10 +2,16 @@ import { cancel, intro, isCancel, log, password, select } from '@clack/prompts'
2
2
  import { defineCommand } from 'citty'
3
3
 
4
4
  import {
5
+ KNOWN_PROVIDER_VENDORS,
5
6
  KNOWN_PROVIDERS,
7
+ listKnownProviderVendorIds,
8
+ providerIdsForVendor,
6
9
  supportsApiKey as providerSupportsApiKey,
7
10
  supportsOAuth as providerSupportsOAuth,
11
+ variantHint,
12
+ variantLabel,
8
13
  type KnownProviderId,
14
+ type KnownProviderVendorId,
9
15
  } from '@/config/providers'
10
16
  import {
11
17
  addProvider,
@@ -270,15 +276,40 @@ function validateKnownProvider(input: string): KnownProviderId {
270
276
 
271
277
  async function resolveProviderForAdd(input: string | undefined): Promise<KnownProviderId> {
272
278
  if (input !== undefined) return validateKnownProvider(input)
273
- const ids = Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]
274
- const choice = await select<KnownProviderId>({
279
+ const vendorId = await pickVendorToAdd()
280
+ return await pickVariantToAdd(vendorId)
281
+ }
282
+
283
+ async function pickVendorToAdd(): Promise<KnownProviderVendorId> {
284
+ const vendorIds = listKnownProviderVendorIds()
285
+ const choice = await select<KnownProviderVendorId>({
275
286
  message: 'Pick a provider to add',
276
- options: ids.map((id) => ({
287
+ options: vendorIds.map((id) => ({
277
288
  value: id,
278
- label: KNOWN_PROVIDERS[id].name,
279
- hint: authHint(id),
289
+ label: KNOWN_PROVIDER_VENDORS[id].name,
290
+ hint: vendorAuthHint(id),
280
291
  })),
281
- initialValue: ids[0],
292
+ initialValue: vendorIds[0],
293
+ })
294
+ if (isCancel(choice)) {
295
+ cancel('Aborted.')
296
+ process.exit(0)
297
+ }
298
+ return choice
299
+ }
300
+
301
+ async function pickVariantToAdd(vendorId: KnownProviderVendorId): Promise<KnownProviderId> {
302
+ const variants = providerIdsForVendor(vendorId)
303
+ if (variants.length === 1) return variants[0]!
304
+ const choice = await select<KnownProviderId>({
305
+ message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
306
+ options: variants.map((id) => {
307
+ const hint = variantHint(vendorId, id)
308
+ return hint !== undefined
309
+ ? { value: id, label: variantLabel(vendorId, id), hint }
310
+ : { value: id, label: variantLabel(vendorId, id) }
311
+ }),
312
+ initialValue: variants[0],
282
313
  })
283
314
  if (isCancel(choice)) {
284
315
  cancel('Aborted.')
@@ -378,10 +409,10 @@ async function runOAuthLogin(cwd: string, providerId: KnownProviderId): Promise<
378
409
  }
379
410
  }
380
411
 
381
- function authHint(id: KnownProviderId): string {
382
- const provider = KNOWN_PROVIDERS[id]
383
- const apiKey = providerSupportsApiKey(provider)
384
- const oauth = providerSupportsOAuth(provider)
412
+ function vendorAuthHint(vendorId: KnownProviderVendorId): string {
413
+ const providers = providerIdsForVendor(vendorId)
414
+ const apiKey = providers.some((id) => providerSupportsApiKey(KNOWN_PROVIDERS[id]))
415
+ const oauth = providers.some((id) => providerSupportsOAuth(KNOWN_PROVIDERS[id]))
385
416
  if (apiKey && oauth) return 'API key or OAuth'
386
417
  if (oauth) return 'OAuth only'
387
418
  return 'API key'
@@ -338,17 +338,29 @@ export const networkSchema = z
338
338
 
339
339
  export type NetworkConfig = z.infer<typeof networkSchema>
340
340
 
341
- // `realProc` opts the per-tool bwrap sandbox into the 'real-proc' strategy
342
- // (src/sandbox/build.ts): a fresh procfs scoped to a new PID namespace so
343
- // external-package runners (`bunx`, `bun add <pkg>`, `bun run <pkg-bin>`) get a
344
- // working /proc/self/{fd,maps} and stop aborting with Bun's "NotDir". Default
345
- // `false` keeps the universally-portable '--tmpfs /proc' profile, under which
346
- // sandboxed external-package execution is unsupported by design. Turning it on
347
- // makes `typeclaw start` grant the container CAP_SYS_ADMIN (required to mount
348
- // proc for the new PID namespace), which is a deliberate posture change on the
349
- // single-tenant outer boundarysee docs/internals/sandbox.mdx. PID isolation
350
- // and the /proc/N/environ leak guard are both preserved; the trade is the
351
- // CAP_SYS_ADMIN grant, not sandbox strength.
341
+ // `realProc` opts the per-tool bwrap sandbox (src/sandbox/build.ts) into the
342
+ // stricter 'real-proc' /proc strategy: a fresh procfs scoped to a NEW PID
343
+ // namespace via `unshare --pid --fork --mount --mount-proc`. It adds full PID
344
+ // isolation (the agent runtime's pids are absent from the sandbox namespace),
345
+ // but needs CAP_SYS_ADMIN to mount proc so `typeclaw start` grants the
346
+ // container `--cap-add=SYS_ADMIN` only when this is set.
347
+ //
348
+ // Default `false`, because external-package execution (`bunx agent-*`, `bun add
349
+ // <pkg>`, `bun run <pkg-bin>` the core subagent workflow) no longer needs it:
350
+ // the default 'proc-bind' strategy `--ro-bind`s the container's already-real
351
+ // procfs into the sandbox with NO CAP_SYS_ADMIN, giving the runner's child a
352
+ // working /proc/self/{fd,maps} so it stops aborting with Bun's "NotDir". The
353
+ // agent runtime's /proc/N/environ (FIREWORKS_API_KEY) stays unreadable because
354
+ // bwrap's --unshare-user puts the sandbox in a child user namespace the kernel
355
+ // won't let read a parent-userns process's environ — verified at runtime by a
356
+ // probe before the strategy is selected (src/sandbox/availability.ts). Avoiding
357
+ // the broad CAP_SYS_ADMIN grant by default is a smaller blast radius than the
358
+ // non-secret PID metadata 'proc-bind' exposes — see docs/internals/sandbox.mdx.
359
+ //
360
+ // Set `true` only to add the PID-isolation posture on a host where the proc
361
+ // mount actually works (bare-metal Linux, Docker Desktop — NOT OrbStack, which
362
+ // rejects the mount even with the cap; there the runtime falls back to
363
+ // 'proc-bind' regardless). The cost is the CAP_SYS_ADMIN grant on the container.
352
364
  export const sandboxSchema = z
353
365
  .object({
354
366
  realProc: z.boolean().default(false),
@@ -41,6 +41,17 @@ type KnownProvider = {
41
41
  // e.g. `OPENAI_API_KEY`). For `oauth` providers, `oauthProviderId` MUST match
42
42
  // a pi-ai OAuth provider id exactly, otherwise `authStorage.login()` will
43
43
  // throw "Unknown OAuth provider".
44
+ //
45
+ // Granularity rule (split vs merge): a provider id is the runtime API surface,
46
+ // not the brand. Different API call => different provider id; same API call =>
47
+ // same provider id. "Same API call" means same endpoint + same wire transport.
48
+ // So `anthropic` is ONE id because api-key and oauth hit the same
49
+ // /v1/messages endpoint (only the auth header differs), while `openai` /
50
+ // `openai-codex` and `zai` / `zai-coding` are SEPARATE ids because each pair
51
+ // targets different endpoints (and env vars). The user-facing brand grouping
52
+ // lives in `KNOWN_PROVIDER_VENDORS` below — keep it out of this decision.
53
+ // Renaming an id is a breaking change to secrets.json keys and typeclaw.json
54
+ // model refs; only do it behind a migration in a dedicated major-version PR.
44
55
  export const KNOWN_PROVIDERS = {
45
56
  openai: {
46
57
  id: 'openai',
@@ -119,13 +130,12 @@ export const KNOWN_PROVIDERS = {
119
130
  // these ids work end-to-end as long as the Codex backend itself accepts
120
131
  // them, which it does for ChatGPT Plus/Pro accounts as of 2026-05-10.
121
132
  //
122
- // Position-load-bearing: must stay adjacent to `openai`. The init wizard's
123
- // provider picker, `provider --help`'s `id | id | ...` listing, and the
124
- // generated JSON schema's model-ref enum all derive their ordering from
125
- // Object.keys() iteration on this literal. Alphabetizing the registry
126
- // would scatter `openai-codex` after `fireworks` and re-introduce the
127
- // "OpenAI ... Anthropic ... OpenAI" picker order this comment exists to
128
- // prevent.
133
+ // Position-load-bearing: must stay adjacent to `openai`. `provider --help`'s
134
+ // `id | id | ...` listing and the generated JSON schema's model-ref enum
135
+ // derive their ordering from Object.keys() iteration on this literal, so
136
+ // alphabetizing the registry would scatter `openai-codex` after `fireworks`.
137
+ // (The init wizard's picker no longer depends on this order — it groups by
138
+ // `KNOWN_PROVIDER_VENDORS` below.)
129
139
  'openai-codex': {
130
140
  id: 'openai-codex',
131
141
  name: 'OpenAI Codex (ChatGPT Plus/Pro)',
@@ -453,10 +463,297 @@ export const KNOWN_PROVIDERS = {
453
463
  },
454
464
  },
455
465
  },
466
+ // xAI (Grok). The native developer API at api.x.ai/v1 is OpenAI-compatible
467
+ // (Bearer auth + /chat/completions shape), so models go through pi-ai's
468
+ // `openai-completions` adapter with a custom baseUrl — same trick as
469
+ // Fireworks and Z.AI. This is a DUAL-AUTH provider like `anthropic`:
470
+ // * api-key — XAI_API_KEY (the standard xAI env var), a plain Bearer token.
471
+ // * oauth — Grok subscription login via xAI's OIDC server (auth.x.ai),
472
+ // authorization-code + PKCE. pi-ai ships no built-in xAI OAuth provider,
473
+ // so `oauthProviderId: 'xai'` resolves to the custom provider registered
474
+ // in src/secrets/oauth-xai.ts (registered from createSecretsStoreForAgent
475
+ // so both init-login and runtime-refresh see it). The dual-auth runtime
476
+ // rule in src/agent/auth.ts applies: an OAuth credential on disk wins over
477
+ // XAI_API_KEY in .env — remove it (`typeclaw provider remove xai`) to fall
478
+ // back to the key.
479
+ //
480
+ // Costs and context windows mirror docs.x.ai/developers/models and the raw
481
+ // /v1/models price fields as of 2026-06-08 (xAI quotes prices in cents per
482
+ // 100M tokens; e.g. grok-4.3 prompt 12500 = $1.25/1M). grok-4.3 is the
483
+ // flagship default; grok-build-0.1 is the coding-tuned model. The
484
+ // grok-4.20-0309 snapshots are pinned weights for reproducible runs.
485
+ //
486
+ // The earlier grok-4 / grok-4-fast / grok-code-fast-1 ids were RETIRED on
487
+ // 2026-05-15 — they still resolve but silently redirect (and bill) at the
488
+ // grok-4.3 / grok-build-0.1 rates, so they are intentionally NOT listed.
489
+ //
490
+ // cacheWrite is 0: xAI publishes no cache-write price (caching is implicit,
491
+ // billed only at the cacheRead rate). Models 1-4 also carry a long-context
492
+ // tier (2x rates above a 200k-token request); pi-ai's Model shape can't
493
+ // express tiered pricing, so the standard rate is used and the breakpoint is
494
+ // noted here. When refreshing, rerun `scripts/generate-schema.ts`.
495
+ xai: {
496
+ id: 'xai',
497
+ name: 'xAI (Grok)',
498
+ baseUrl: 'https://api.x.ai/v1',
499
+ auth: ['api-key', 'oauth'],
500
+ apiKeyEnv: 'XAI_API_KEY',
501
+ oauthProviderId: 'xai',
502
+ models: {
503
+ 'grok-4.3': {
504
+ id: 'grok-4.3',
505
+ name: 'Grok 4.3',
506
+ api: 'openai-completions',
507
+ provider: 'xai',
508
+ baseUrl: 'https://api.x.ai/v1',
509
+ reasoning: true,
510
+ input: ['text', 'image'],
511
+ cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
512
+ contextWindow: 1000000,
513
+ maxTokens: 64000,
514
+ },
515
+ 'grok-4.20-0309-reasoning': {
516
+ id: 'grok-4.20-0309-reasoning',
517
+ name: 'Grok 4.20 (Reasoning)',
518
+ api: 'openai-completions',
519
+ provider: 'xai',
520
+ baseUrl: 'https://api.x.ai/v1',
521
+ reasoning: true,
522
+ input: ['text', 'image'],
523
+ cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
524
+ contextWindow: 1000000,
525
+ maxTokens: 64000,
526
+ },
527
+ 'grok-4.20-0309-non-reasoning': {
528
+ id: 'grok-4.20-0309-non-reasoning',
529
+ name: 'Grok 4.20 (Non-Reasoning)',
530
+ api: 'openai-completions',
531
+ provider: 'xai',
532
+ baseUrl: 'https://api.x.ai/v1',
533
+ reasoning: false,
534
+ input: ['text', 'image'],
535
+ cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
536
+ contextWindow: 1000000,
537
+ maxTokens: 64000,
538
+ },
539
+ 'grok-build-0.1': {
540
+ id: 'grok-build-0.1',
541
+ name: 'Grok Build 0.1',
542
+ api: 'openai-completions',
543
+ provider: 'xai',
544
+ baseUrl: 'https://api.x.ai/v1',
545
+ reasoning: true,
546
+ input: ['text', 'image'],
547
+ cost: { input: 1.0, output: 2.0, cacheRead: 0.2, cacheWrite: 0 },
548
+ contextWindow: 256000,
549
+ maxTokens: 64000,
550
+ },
551
+ },
552
+ },
553
+ // MiniMax (minimax.io) pay-as-you-go API. OpenAI-compatible (Bearer auth +
554
+ // /chat/completions shape), so models go through pi-ai's `openai-completions`
555
+ // adapter with a custom baseUrl — same trick as Fireworks and Z.AI.
556
+ //
557
+ // Endpoint choice: the international endpoint (api.minimax.io) is the global
558
+ // surface; the China endpoint (api.minimaxi.com) is a regional alternative on
559
+ // the same protocol. We pin the international one here — operators who need the
560
+ // China gateway can front it with an OpenAI-compatible proxy.
561
+ //
562
+ // Model lineup mirrors the OpenAI-compatible model enum on
563
+ // platform.minimax.io as of 2026-06-08: MiniMax-M3 (flagship, 1M context,
564
+ // image input, controllable reasoning) plus the M2 reasoning series
565
+ // (204,800 context, reasoning always on, text-only). The `-highspeed`
566
+ // billing-tier variants and the native-only `M2-her` / `MiniMax-Text-01` /
567
+ // `abab*` models are intentionally omitted — the first are duplicate weights
568
+ // on a pricier surface, the rest don't serve the OpenAI-compatible
569
+ // /chat/completions route this adapter speaks. Costs are USD per 1M tokens
570
+ // from docs/guides/pricing-paygo (standard tier): M3 reflects the permanent
571
+ // 50%-off ≤512K-input rate (input 0.30 / output 1.20 / cacheRead 0.06; M3 has
572
+ // no separate cache-write rate). M2.7 caches at 0.06 read / 0.375 write; the
573
+ // M2.5/M2.1/M2 series at 0.03 read / 0.375 write.
574
+ //
575
+ // M3 tiered pricing is NOT modeled: this single cost record only encodes the
576
+ // ≤512K-input tier. The >512K tier (input 0.60 / output 2.40 / cacheRead 0.12)
577
+ // is a limited-availability surcharge that pi-ai's flat per-model cost shape
578
+ // can't represent, so long-context M3 sessions above 512K under-report cost.
579
+ minimax: {
580
+ id: 'minimax',
581
+ name: 'MiniMax',
582
+ baseUrl: 'https://api.minimax.io/v1',
583
+ auth: ['api-key'],
584
+ apiKeyEnv: 'MINIMAX_API_KEY',
585
+ oauthProviderId: null,
586
+ models: {
587
+ 'MiniMax-M3': {
588
+ id: 'MiniMax-M3',
589
+ name: 'MiniMax M3',
590
+ api: 'openai-completions',
591
+ provider: 'minimax',
592
+ baseUrl: 'https://api.minimax.io/v1',
593
+ reasoning: true,
594
+ input: ['text', 'image'],
595
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
596
+ contextWindow: 1000000,
597
+ maxTokens: 524288,
598
+ },
599
+ 'MiniMax-M2.7': {
600
+ id: 'MiniMax-M2.7',
601
+ name: 'MiniMax M2.7',
602
+ api: 'openai-completions',
603
+ provider: 'minimax',
604
+ baseUrl: 'https://api.minimax.io/v1',
605
+ reasoning: true,
606
+ input: ['text'],
607
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
608
+ contextWindow: 204800,
609
+ maxTokens: 204800,
610
+ },
611
+ 'MiniMax-M2.5': {
612
+ id: 'MiniMax-M2.5',
613
+ name: 'MiniMax M2.5',
614
+ api: 'openai-completions',
615
+ provider: 'minimax',
616
+ baseUrl: 'https://api.minimax.io/v1',
617
+ reasoning: true,
618
+ input: ['text'],
619
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
620
+ contextWindow: 204800,
621
+ maxTokens: 204800,
622
+ },
623
+ 'MiniMax-M2.1': {
624
+ id: 'MiniMax-M2.1',
625
+ name: 'MiniMax M2.1',
626
+ api: 'openai-completions',
627
+ provider: 'minimax',
628
+ baseUrl: 'https://api.minimax.io/v1',
629
+ reasoning: true,
630
+ input: ['text'],
631
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
632
+ contextWindow: 204800,
633
+ maxTokens: 204800,
634
+ },
635
+ 'MiniMax-M2': {
636
+ id: 'MiniMax-M2',
637
+ name: 'MiniMax M2',
638
+ api: 'openai-completions',
639
+ provider: 'minimax',
640
+ baseUrl: 'https://api.minimax.io/v1',
641
+ reasoning: true,
642
+ input: ['text'],
643
+ cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
644
+ contextWindow: 204800,
645
+ maxTokens: 204800,
646
+ },
647
+ },
648
+ },
456
649
  } as const satisfies Record<string, KnownProvider>
457
650
 
458
651
  export type KnownProviderId = keyof typeof KNOWN_PROVIDERS
459
652
 
653
+ // UX-only grouping of provider ids under one vendor for the init/`provider
654
+ // add` pickers. Deliberately does NOT touch the runtime contract:
655
+ // `KnownProviderId`, `KnownModelRef`, secrets.json keys, auth resolution, and
656
+ // the generated schema all stay keyed on the flat ids in `KNOWN_PROVIDERS`.
657
+ // The follow-up "variant" prompt resolves a concrete provider id, then
658
+ // `pickAuthMethod` runs as before; it is auto-resolved for single-provider
659
+ // vendors (Fireworks, Anthropic). `variants` copy lets the prompt read as an
660
+ // auth choice for OpenAI but a plan choice for Z.AI (both api-key, different
661
+ // billing surfaces).
662
+ type KnownProviderVendor = {
663
+ id: string
664
+ name: string
665
+ providers: ReadonlyArray<KnownProviderId>
666
+ variants?: Partial<Record<KnownProviderId, { label: string; hint?: string }>>
667
+ }
668
+
669
+ // Ordered by product priority for the picker — independent of the
670
+ // `KNOWN_PROVIDERS` declaration order (which stays load-bearing for the schema
671
+ // enum and `provider --help` listing). Every provider id below MUST appear in
672
+ // exactly one vendor; `providers.test.ts` enforces the partition.
673
+ export const KNOWN_PROVIDER_VENDORS = {
674
+ openai: {
675
+ id: 'openai',
676
+ name: 'OpenAI',
677
+ providers: ['openai', 'openai-codex'],
678
+ variants: {
679
+ openai: { label: 'API key', hint: 'OpenAI API platform' },
680
+ 'openai-codex': { label: 'OAuth (ChatGPT Plus/Pro)', hint: 'ChatGPT subscription' },
681
+ },
682
+ },
683
+ anthropic: {
684
+ id: 'anthropic',
685
+ name: 'Anthropic',
686
+ providers: ['anthropic'],
687
+ },
688
+ fireworks: {
689
+ id: 'fireworks',
690
+ name: 'Fireworks',
691
+ providers: ['fireworks'],
692
+ },
693
+ zai: {
694
+ id: 'zai',
695
+ name: 'Z.AI',
696
+ providers: ['zai', 'zai-coding'],
697
+ variants: {
698
+ zai: { label: 'Pay-as-you-go', hint: 'standard API billing' },
699
+ 'zai-coding': { label: 'Coding Plan', hint: 'GLM Coding Plan subscription' },
700
+ },
701
+ },
702
+ xai: {
703
+ id: 'xai',
704
+ name: 'xAI (Grok)',
705
+ providers: ['xai'],
706
+ },
707
+ // Single-provider vendor: pay-as-you-go and Token Plan are the SAME provider
708
+ // id (same /v1 endpoint, same Bearer transport — only the key prefix differs),
709
+ // so per the granularity rule above MiniMax is one id like Anthropic, not a
710
+ // zai-style split. The picker shows one row and skips the variant prompt; the
711
+ // pay-as-you-go-vs-Token-Plan dashboard hint is handled in the key-entry step.
712
+ minimax: {
713
+ id: 'minimax',
714
+ name: 'MiniMax',
715
+ providers: ['minimax'],
716
+ },
717
+ } as const satisfies Record<string, KnownProviderVendor>
718
+
719
+ export type KnownProviderVendorId = keyof typeof KNOWN_PROVIDER_VENDORS
720
+
721
+ export function listKnownProviderVendorIds(): KnownProviderVendorId[] {
722
+ return Object.keys(KNOWN_PROVIDER_VENDORS) as KnownProviderVendorId[]
723
+ }
724
+
725
+ export function providerIdsForVendor(vendorId: KnownProviderVendorId): ReadonlyArray<KnownProviderId> {
726
+ return KNOWN_PROVIDER_VENDORS[vendorId].providers
727
+ }
728
+
729
+ export function vendorForProviderId(providerId: KnownProviderId): KnownProviderVendorId {
730
+ for (const vendorId of listKnownProviderVendorIds()) {
731
+ if ((KNOWN_PROVIDER_VENDORS[vendorId].providers as ReadonlyArray<KnownProviderId>).includes(providerId)) {
732
+ return vendorId
733
+ }
734
+ }
735
+ throw new Error(`Provider ${providerId} is not assigned to any vendor in KNOWN_PROVIDER_VENDORS`)
736
+ }
737
+
738
+ function variantCopy(
739
+ vendorId: KnownProviderVendorId,
740
+ providerId: KnownProviderId,
741
+ ): { label: string; hint?: string } | undefined {
742
+ const vendor: KnownProviderVendor = KNOWN_PROVIDER_VENDORS[vendorId]
743
+ return vendor.variants?.[providerId]
744
+ }
745
+
746
+ // Falls back to the provider's own name when a vendor supplies no variant copy
747
+ // (single-provider vendors never render this prompt, so the fallback only
748
+ // guards against an incomplete `variants` map on a multi-provider vendor).
749
+ export function variantLabel(vendorId: KnownProviderVendorId, providerId: KnownProviderId): string {
750
+ return variantCopy(vendorId, providerId)?.label ?? KNOWN_PROVIDERS[providerId].name
751
+ }
752
+
753
+ export function variantHint(vendorId: KnownProviderVendorId, providerId: KnownProviderId): string | undefined {
754
+ return variantCopy(vendorId, providerId)?.hint
755
+ }
756
+
460
757
  export type KnownModelRef = {
461
758
  [P in KnownProviderId]: `${P}/${Extract<keyof (typeof KNOWN_PROVIDERS)[P]['models'], string>}`
462
759
  }[KnownProviderId]
@@ -514,16 +514,21 @@ export async function planStart({
514
514
  }
515
515
  }
516
516
 
517
- // sandbox.realProc opts the per-tool bwrap sandbox into the 'real-proc'
518
- // strategy (src/sandbox/build.ts), which prefixes the sandbox with
517
+ // sandbox.realProc (default FALSE) opts into the per-tool bwrap sandbox's
518
+ // 'real-proc' strategy (src/sandbox/build.ts), which prefixes the sandbox with
519
519
  // `unshare --pid --fork --mount --mount-proc`. Mounting a fresh procfs for the
520
520
  // new PID namespace needs real CAP_SYS_ADMIN — seccomp=unconfined alone is not
521
521
  // enough (it only unblocks the unshare/clone SYSCALLS; the kernel still
522
- // rejects mount(2) of proc without the capability). This is the deliberate
523
- // posture change documented in docs/internals/sandbox.mdx: the default keeps
524
- // the narrower seccomp-only profile, and the operator grants the broad
525
- // "new root" capability ONLY by opting into real-proc. Placed before the
526
- // image tag (like --cap-add=NET_ADMIN) so docker applies it at run time.
522
+ // rejects mount(2) of proc without the capability). So the grant is gated on
523
+ // the flag and is OFF by default: external-package execution (`bunx agent-*`)
524
+ // no longer needs it the default 'proc-bind' strategy gives the runner real
525
+ // /proc without any outer capability (see docs/internals/sandbox.mdx). Setting
526
+ // realProc:true adds the stricter PID-isolation posture at the cost of this
527
+ // broad "new root" grant. The container-side strategy resolution still probes
528
+ // whether the mount actually works (canMountRealProc) and falls back to
529
+ // proc-bind on runtimes where the cap is a no-op (e.g. OrbStack), so this grant
530
+ // is necessary-but-not-sufficient by design. Placed before the image tag (like
531
+ // --cap-add=NET_ADMIN) so docker applies it at run time.
527
532
  if (cfg.sandbox.realProc) {
528
533
  runArgs.push('--cap-add=SYS_ADMIN')
529
534
  }
@@ -0,0 +1,44 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ // Dependency-free agent-folder resolution. Kept out of `src/init/index.ts` so
5
+ // the host CLI entry (`src/cli/index.ts`) can locate the agent folder at the
6
+ // dispatch boundary WITHOUT pulling in the heavy init barrel (which statically
7
+ // imports @/config, @/config/providers, @/container, @/secrets, @/tui — a
8
+ // ~190ms module graph). This module MUST NOT import from the init barrel,
9
+ // config, container, or plugin modules; keep the dependency direction one-way.
10
+
11
+ export const CONFIG_FILE = 'typeclaw.json'
12
+
13
+ export function isInitialized(dir: string): boolean {
14
+ return existsSync(join(dir, CONFIG_FILE))
15
+ }
16
+
17
+ // Walks upward from `start` looking for the agent folder (the dir containing
18
+ // typeclaw.json). Returns the found dir, or null if nothing is found before
19
+ // the walk hits a stop boundary.
20
+ //
21
+ // Stop boundaries (whichever comes first, checked at every level):
22
+ // 1. The current dir contains typeclaw.json — return it.
23
+ // 2. The current dir contains .git — return null. A .git boundary marks a
24
+ // project root; refusing to cross it prevents accidentally picking up an
25
+ // unrelated parent project, and matches how typeclaw itself initializes
26
+ // one .git per agent folder.
27
+ // 3. We've reached the filesystem root — return null.
28
+ //
29
+ // The `.git` check fires AFTER the typeclaw.json check at the same level so
30
+ // that walking up from a subdir of the agent (e.g. `<agent>/workspace/`) still
31
+ // resolves to the agent root, even though the agent root itself contains both
32
+ // typeclaw.json and .git.
33
+ export function findAgentDir(start: string): string | null {
34
+ let dir = resolve(start)
35
+ const root = resolve(dir, '/')
36
+ while (true) {
37
+ if (existsSync(join(dir, CONFIG_FILE))) return dir
38
+ if (existsSync(join(dir, '.git'))) return null
39
+ if (dir === root) return null
40
+ const parent = dirname(dir)
41
+ if (parent === dir) return null
42
+ dir = parent
43
+ }
44
+ }
package/src/init/index.ts CHANGED
@@ -19,6 +19,7 @@ import { createTui } from '@/tui'
19
19
 
20
20
  import { resolveBaseImageVersion, resolveScaffoldVersion } from './cli-version'
21
21
  import { buildDockerfile, DOCKERFILE } from './dockerfile'
22
+ import { CONFIG_FILE, findAgentDir, isInitialized } from './find-agent-dir'
22
23
  import { installGithubWebhooksEagerly, type EagerGithubWebhookInstallResult } from './github-webhook-install'
23
24
  import { buildGitignore, GITIGNORE_FILE } from './gitignore'
24
25
  import { buildHatchingPrompt } from './hatching'
@@ -35,7 +36,8 @@ export { GITKEEP_FILE, PACKAGES_DIR, PUBLIC_DIR } from './paths'
35
36
 
36
37
  export { appendOrReplaceEnvKey, hasEnvKey, readEnvFile } from './env-file'
37
38
 
38
- const CONFIG_FILE = 'typeclaw.json'
39
+ export { CONFIG_FILE, findAgentDir, isInitialized }
40
+
39
41
  const CRON_FILE = 'cron.json'
40
42
  const PACKAGE_FILE = 'package.json'
41
43
 
@@ -491,39 +493,6 @@ export function isDirectoryNonEmpty(dir: string): boolean {
491
493
  }
492
494
  }
493
495
 
494
- export function isInitialized(dir: string): boolean {
495
- return existsSync(join(dir, CONFIG_FILE))
496
- }
497
-
498
- // Walks upward from `start` looking for the agent folder (the dir containing
499
- // typeclaw.json). Returns the found dir, or null if nothing is found before
500
- // the walk hits a stop boundary.
501
- //
502
- // Stop boundaries (whichever comes first, checked at every level):
503
- // 1. The current dir contains typeclaw.json — return it.
504
- // 2. The current dir contains .git — return null. A .git boundary marks a
505
- // project root; refusing to cross it prevents accidentally picking up an
506
- // unrelated parent project, and matches how typeclaw itself initializes
507
- // one .git per agent folder.
508
- // 3. We've reached the filesystem root — return null.
509
- //
510
- // The `.git` check fires AFTER the typeclaw.json check at the same level so
511
- // that walking up from a subdir of the agent (e.g. `<agent>/workspace/`) still
512
- // resolves to the agent root, even though the agent root itself contains both
513
- // typeclaw.json and .git.
514
- export function findAgentDir(start: string): string | null {
515
- let dir = resolve(start)
516
- const root = resolve(dir, '/')
517
- while (true) {
518
- if (existsSync(join(dir, CONFIG_FILE))) return dir
519
- if (existsSync(join(dir, '.git'))) return null
520
- if (dir === root) return null
521
- const parent = dirname(dir)
522
- if (parent === dir) return null
523
- dir = parent
524
- }
525
- }
526
-
527
496
  const HATCHED_COMMIT_SUBJECT = 'Hatched 🐣'
528
497
 
529
498
  export async function isHatched(dir: string): Promise<boolean> {
@@ -20,6 +20,8 @@ const PROVIDER_TO_MODELS_DEV: Record<KnownProviderId, string> = {
20
20
  // catalog. models.dev tracks the underlying model metadata under `zai`,
21
21
  // so we route lookups there. The curated entries still get surfaced.
22
22
  'zai-coding': 'zai',
23
+ xai: 'xai',
24
+ minimax: 'minimax',
23
25
  }
24
26
 
25
27
  export type ModelOption = {
@@ -7,6 +7,8 @@ const PROVIDER_PROBE: Partial<Record<KnownProviderId, { url: string; authHeader:
7
7
  fireworks: { url: 'https://api.fireworks.ai/inference/v1/models', authHeader: 'bearer' },
8
8
  zai: { url: 'https://api.z.ai/api/paas/v4/models', authHeader: 'bearer' },
9
9
  'zai-coding': { url: 'https://api.z.ai/api/coding/paas/v4/models', authHeader: 'bearer' },
10
+ xai: { url: 'https://api.x.ai/v1/models', authHeader: 'bearer' },
11
+ minimax: { url: 'https://api.minimax.io/v1/models', authHeader: 'bearer' },
10
12
  }
11
13
 
12
14
  // When a base-URL override (ANTHROPIC_BASE_URL / OPENAI_BASE_URL) points at a
@@ -159,8 +161,19 @@ export const API_KEY_DASHBOARD_URL: Partial<Record<KnownProviderId, string>> = {
159
161
  fireworks: 'https://fireworks.ai/account/api-keys',
160
162
  zai: 'https://docs.z.ai/devpack/tool/claude#api-key',
161
163
  'zai-coding': 'https://docs.z.ai/devpack/tool/claude#api-key',
164
+ xai: 'https://console.x.ai',
165
+ minimax: 'https://platform.minimax.io/user-center/basic-information/interface-key',
162
166
  }
163
167
 
168
+ // MiniMax sells the same `minimax` provider under two billing surfaces that
169
+ // each hand out a key on a DIFFERENT dashboard page: pay-as-you-go API keys
170
+ // (the API_KEY_DASHBOARD_URL above) and Token Plan "Subscription Keys"
171
+ // (sk-cp-…, this URL). Both keys are Bearer tokens for the same api.minimax.io
172
+ // endpoint and store in the same MINIMAX_API_KEY slot — the runtime doesn't
173
+ // care which. The init wizard surfaces the choice only to deep-link the
174
+ // correct dashboard, so a Token Plan subscriber isn't sent to the paygo page.
175
+ export const MINIMAX_TOKEN_PLAN_DASHBOARD_URL = 'https://platform.minimax.io/user-center/payment/token-plan'
176
+
164
177
  export function providersWithApiKeyProbe(): KnownProviderId[] {
165
178
  return Object.keys(PROVIDER_PROBE) as KnownProviderId[]
166
179
  }