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/provider.ts
CHANGED
|
@@ -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
|
|
274
|
-
|
|
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:
|
|
287
|
+
options: vendorIds.map((id) => ({
|
|
277
288
|
value: id,
|
|
278
|
-
label:
|
|
279
|
-
hint:
|
|
289
|
+
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
290
|
+
hint: vendorAuthHint(id),
|
|
280
291
|
})),
|
|
281
|
-
initialValue:
|
|
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
|
|
382
|
-
const
|
|
383
|
-
const apiKey = providerSupportsApiKey(
|
|
384
|
-
const oauth = providerSupportsOAuth(
|
|
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'
|
package/src/config/config.ts
CHANGED
|
@@ -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
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
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),
|
package/src/config/providers.ts
CHANGED
|
@@ -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`.
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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]
|
package/src/container/start.ts
CHANGED
|
@@ -514,16 +514,21 @@ export async function planStart({
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
// sandbox.realProc opts the per-tool bwrap sandbox
|
|
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).
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
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
|
-
|
|
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> {
|
package/src/init/models-dev.ts
CHANGED
|
@@ -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
|
}
|