typeclaw 0.6.0 → 0.7.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/README.md +32 -86
- package/package.json +1 -1
- package/src/agent/system-prompt.ts +3 -1
- package/src/cli/init.ts +8 -1
- package/src/cli/oauth-callbacks.ts +64 -34
- package/src/cli/provider.ts +9 -4
- package/src/config/config.ts +28 -4
- package/src/config/providers.ts +106 -0
- package/src/init/models-dev.ts +1 -0
- package/typeclaw.schema.json +6 -0
package/README.md
CHANGED
|
@@ -2,51 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
> A TypeScript-native, Bun-powered, Docker-friendly general-purpose agent runtime.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
There are great agents out there. None of them were quite the shape I wanted:
|
|
5
|
+
Full docs: **[typeclaw.dev](https://typeclaw.dev)**.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
- **NanoClaw** — simple, but no plugin system
|
|
11
|
-
- **PicoClaw** — fast, but Go (so plugins live outside the runtime)
|
|
12
|
-
- **ZeroClaw** — light, but Rust (same problem, different ecosystem)
|
|
13
|
-
- **Hermes Agent** — awesome, but Python
|
|
7
|
+
## Why?
|
|
14
8
|
|
|
15
|
-
None of
|
|
9
|
+
There are great agents out there. None of them were quite the shape I wanted — most are written in Go, Rust, or Python, which means plugins live outside the runtime (IPC, FFI, or a separate process). The ones in TypeScript are either too heavy or too bare.
|
|
16
10
|
|
|
17
11
|
TypeClaw is the agent I wanted to use:
|
|
18
12
|
|
|
19
13
|
- **TypeScript end to end** — agent core, plugins, channel adapters, CLI, TUI all in one language
|
|
20
14
|
- **Bun-native plugins** — plugins are just TS modules; no IPC, no FFI, hot-reloadable config
|
|
21
15
|
- **Docker-friendly by default** — every agent runs in its own container; the host CLI is purely a launcher
|
|
22
|
-
- **Multi-channel out of the box** — Slack, Discord, TUI, websocket — all routed through one in-process stream
|
|
23
16
|
- **Self-improving** — the agent observes its own work, distills it into long-term memory and reusable skills, and gets sharper over time without you writing prompts for it
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
If you're like me, TypeClaw is the right choice. If not, that's fine too.
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
20
|
+
## What you'd expect
|
|
21
|
+
|
|
22
|
+
- 🐳 **Sandboxed by default** — every agent runs in its own Docker container with `.env` injection and bind-mounted host folders
|
|
23
|
+
- 🔌 **Plugin system** — plain TypeScript modules contribute tools, skills, subagents, channels, commands, and typed config
|
|
24
|
+
- 💬 **Multi-channel** — Slack, Discord, Telegram, KakaoTalk, GitHub webhooks, and a websocket TUI; one agent, many inboxes
|
|
31
25
|
- ⏰ **Cron** — schedule prompts or shell commands; per-job coalescing so slow jobs don't pile up
|
|
32
26
|
- 📚 **Skills on demand** — markdown procedures the agent loads only when relevant; zero token cost until used
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- 🔁 **Self-restart** — the agent can bounce its own container when it updates itself
|
|
37
|
-
- 🌐 **Auto port-forward** — dev servers inside the container appear on `localhost`, even loopback-only ones
|
|
38
|
-
- 🌍 **Public tunnels** — Cloudflare Quick (zero signup) or bring-your-own external URL; the agent self-registers GitHub webhooks at the resulting public URL
|
|
39
|
-
- 🎼 **Compose** — orchestrate multiple agents across multiple folders
|
|
40
|
-
|
|
41
|
-
### 🌱 Self-improving, in detail
|
|
27
|
+
- 🔎 **Web research** — bundled `scout` subagent plus first-class `websearch` and `webfetch` tools (DuckDuckGo via curl-impersonate, Wikipedia)
|
|
28
|
+
- 🛡 **Security guards** — bundled `tool.before` policies catch secret exfil, SSRF, prompt injection, and tainted git remotes before they fire
|
|
29
|
+
- 📊 **Usage and doctor** — `typeclaw usage` reports token/$ spend per session, model, or day; `typeclaw doctor` diagnoses host, agent folder, and plugin state
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
## Where it goes further
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
- 🌱 **Self-improving** — bundled `memory` plugin distills sessions into long-term `MEMORY.md` without you writing prompts for it
|
|
34
|
+
- 🧠 **Muscle memory** — repeated procedures get distilled into reusable skills the agent writes for itself and loads on later runs
|
|
35
|
+
- 💾 **Auto-backup** — the bundled `backup` plugin commits session logs and memory on every idle window with an LLM-generated commit subject
|
|
36
|
+
- 🪄 **Subagents** — first-class child sessions with their own system prompt, payload schema, and per-payload coalescing; cron and the main agent fire them through one in-process Stream
|
|
37
|
+
- 🪪 **Roles and permissions** — `owner` / `trusted` / `member` / `guest` with first-message match rules per channel; gates `channel.respond`, cron scheduling, and security bypasses, so a Slack stranger can't tell the agent to push to main
|
|
38
|
+
- 👥 **Group chat awareness** — knows who's in the room, distinguishes humans from bots, and stays engaged after a reply without re-mentioning
|
|
39
|
+
- 🧱 **Managed-file guards** — `typeclaw.json`, `cron.json`, `MEMORY.md`, and bundled skills are protected from accidental rewrites; invalid config writes are rejected at the tool boundary
|
|
40
|
+
- 🌐 **Headed browser inside the container** — bundled `agent-browser` plugin ships Chrome under Xvfb so the agent can drive real web pages past bot fingerprinting
|
|
41
|
+
- 🌍 **Tunnels and auto port-forward** — dev servers inside the container appear on `localhost` (even loopback-only ones); public URLs via Cloudflare Quick (zero signup) or your own external URL, with GitHub webhooks self-registered at the resulting URL
|
|
42
|
+
- 🔄 **Hot reload** — change `typeclaw.json`, run `typeclaw reload` — no restart for most fields
|
|
43
|
+
- 🔁 **Self-restart** — the agent can bounce its own container when it updates itself
|
|
44
|
+
- 🎼 **Compose** — orchestrate multiple agents across multiple folders
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
Memory loop and subagent architecture are covered in detail in [AGENTS.md](./AGENTS.md) and [`src/bundled-plugins/memory/README.md`](./src/bundled-plugins/memory/README.md).
|
|
50
47
|
|
|
51
48
|
## Install
|
|
52
49
|
|
|
@@ -67,59 +64,7 @@ typeclaw tui # attach a terminal UI to the running agent
|
|
|
67
64
|
|
|
68
65
|
That's it. The agent is now alive, listening on a websocket, ready to receive prompts from the TUI or any wired channel.
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
| Command | Purpose |
|
|
73
|
-
| ----------------------------------- | ----------------------------------------------------------------------------------- |
|
|
74
|
-
| `typeclaw init` | Scaffold a new agent folder |
|
|
75
|
-
| `typeclaw start` | Build and run the container |
|
|
76
|
-
| `typeclaw stop` | Stop the container |
|
|
77
|
-
| `typeclaw restart` | `stop` then `start` |
|
|
78
|
-
| `typeclaw status` | Show container + daemon registration state |
|
|
79
|
-
| `typeclaw logs` | Stream container stdout/stderr with local timestamps; `-f` to follow |
|
|
80
|
-
| `typeclaw tui` | Attach a terminal UI over the agent's websocket |
|
|
81
|
-
| `typeclaw shell` | Open a shell inside the running container |
|
|
82
|
-
| `typeclaw reload` | Push a live config reload to the running agent |
|
|
83
|
-
| `typeclaw compose` | Orchestrate multiple agents |
|
|
84
|
-
| `typeclaw cron list` | List every cron job registered in the running agent (user `cron.json` + plugins) |
|
|
85
|
-
| `typeclaw channel add <kind>` | Wire a new channel adapter (Slack, Discord, Telegram, KakaoTalk, GitHub) |
|
|
86
|
-
| `typeclaw channel set <kind>` | Rotate the credentials of an already-configured channel (bot/app tokens, PAT, etc.) |
|
|
87
|
-
| `typeclaw channel reauth kakaotalk` | Re-authenticate KakaoTalk after a stale-token 401 or to rotate the stored password |
|
|
88
|
-
| `typeclaw tunnel ...` | Add/list/status/remove public tunnels and inspect tunnel logs |
|
|
89
|
-
|
|
90
|
-
## Configuration
|
|
91
|
-
|
|
92
|
-
Agent folder layout after `init`:
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
my-agent/
|
|
96
|
-
├── typeclaw.json # main config (schema-validated)
|
|
97
|
-
├── cron.json # scheduled jobs (optional)
|
|
98
|
-
├── .env # secrets, injected via --env-file
|
|
99
|
-
├── Dockerfile # auto-managed by typeclaw, refreshed every `start`
|
|
100
|
-
├── package.json # `typeclaw` as a dependency
|
|
101
|
-
├── .gitignore # auto-managed
|
|
102
|
-
├── workspace/ # agent's free-write zone (gitignored)
|
|
103
|
-
├── sessions/ # JSONL session logs (gitignored, force-committed by auto-backup)
|
|
104
|
-
└── memory/ # MEMORY.md + muscle-memory skills (gitignored, force-committed by dreaming)
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
`typeclaw.json` is JSON Schema–validated (see `typeclaw.schema.json`). Highlights:
|
|
108
|
-
|
|
109
|
-
- `port` — preferred host port (CLI falls back to ephemeral on conflict)
|
|
110
|
-
- `mounts` — host directories to expose inside the container
|
|
111
|
-
- `plugins` — list of plugin module specifiers
|
|
112
|
-
- `channels` — `slack-bot` / `discord-bot` config
|
|
113
|
-
- `portForward` — allow/deny list for auto port forwarding (default: `*`)
|
|
114
|
-
- `tunnels` — declare public URLs for inbound webhooks and ad-hoc exposure (`cloudflare-quick` or `external`)
|
|
115
|
-
- `dockerfile` — toggles for `gh`, `python`, `tmux`, `ffmpeg`, `cjkFonts`, plus `append` lines
|
|
116
|
-
- `memory` — idle window and dreaming schedule for the memory plugin
|
|
117
|
-
|
|
118
|
-
`Dockerfile` and `.gitignore` are owned by TypeClaw and rewritten on every `start` — edit `src/init/dockerfile.ts` and re-run `start --build` to ship template changes.
|
|
119
|
-
|
|
120
|
-
### Secrets
|
|
121
|
-
|
|
122
|
-
Credentials live in two gitignored files: `.env` (plain `KEY=value` lines, injected into the container via `--env-file`) and `secrets.json` (a structured store managed by TypeClaw). **Env-wins**: when a credential's canonical env var (e.g. `FIREWORKS_API_KEY`, `SLACK_BOT_TOKEN`) is set, that value is used at runtime — `secrets.json` is never auto-mutated to capture it. Every secret-bearing field in `secrets.json` is a `Secret` (`string | { value?, env? }`), so the file can rebind a credential to a custom env-var name on demand. See [AGENTS.md § Secrets](./AGENTS.md#secrets) for the full contract.
|
|
67
|
+
See `typeclaw --help` for the full command surface, or [typeclaw.dev](https://typeclaw.dev) for guides and configuration reference.
|
|
123
68
|
|
|
124
69
|
## Development
|
|
125
70
|
|
|
@@ -130,7 +75,7 @@ bun install
|
|
|
130
75
|
bun test
|
|
131
76
|
```
|
|
132
77
|
|
|
133
|
-
Pre-commit checks (must
|
|
78
|
+
Pre-commit checks (all must pass — no exceptions):
|
|
134
79
|
|
|
135
80
|
```sh
|
|
136
81
|
bun run typecheck
|
|
@@ -138,11 +83,12 @@ bun run lint
|
|
|
138
83
|
bun run format
|
|
139
84
|
```
|
|
140
85
|
|
|
141
|
-
See [AGENTS.md](./AGENTS.md) for the long-form architecture notes — stages, hostd internals, message stream, plugin contracts, and the testing philosophy.
|
|
86
|
+
See [AGENTS.md](./AGENTS.md) for the long-form architecture notes — stages, hostd internals, message stream, plugin contracts, and the testing philosophy. The docs site at [typeclaw.dev](https://typeclaw.dev) lives in [`docs/`](./docs/).
|
|
142
87
|
|
|
143
|
-
##
|
|
88
|
+
## Acknowledgments
|
|
144
89
|
|
|
145
|
-
|
|
90
|
+
- **Multi-channel** is powered by [agent-messenger](https://github.com/agent-messenger/agent-messenger) — every non-GitHub adapter (`slack-bot`, `discord-bot`, `telegram-bot`, `kakaotalk`) is built on its SDK. Thanks to the maintainers for the credential extraction, listener protocols, and platform coverage that made multi-channel a feature instead of a year-long project.
|
|
91
|
+
- **Subagent architecture** is inspired by [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) by [@code-yeongyu](https://github.com/code-yeongyu). Thanks for the shape that made this clean.
|
|
146
92
|
|
|
147
93
|
## License
|
|
148
94
|
|
package/package.json
CHANGED
|
@@ -68,7 +68,9 @@ The bundled \`scout\` subagent is its external counterpart — web research only
|
|
|
68
68
|
|
|
69
69
|
When the user hands you a task that will take minutes (a multi-step browser session, a long build, a complex external operation), acknowledge in plain language ("Alright, running that in the background — I'll let you know when it's done"), spawn one subagent with \`run_in_background: true\`, then KEEP TALKING. Stay available for follow-ups, related questions, parallel small tasks. When the completion reminder lands, weave the result into your next reply naturally. If the conversation has gone idle, proactively message the user with the result rather than waiting.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Before you start an inline operation you expect to take more than ~30 seconds — a chain of \`webfetch\` calls, a \`websearch\` round you'll iterate on, a \`bash\` command that hits a slow API or scrapes a site, an \`agent-browser\` session, any "fetch N things in a loop" — pause and ask whether a subagent should run it instead. Inline long calls block the user from talking to you and pollute your context window with intermediate output; \`scout\` (for research) or \`operator\` (for actions with side effects) keeps the conversation responsive and returns a clean summary. The exception is a single quick call (one \`webfetch\` of a known URL, one \`websearch\` query you already know the shape of) — do those inline.
|
|
72
|
+
|
|
73
|
+
The bundled \`operator\` subagent is the right tool for this mode. It is write-capable (read, write, edit, bash with side effects) and runs on the default model. Use it for: browser sessions, multi-file refactors, deploys, batch API calls, anything that involves taking action on behalf of the user over multiple steps. The operator returns a structured final report (outcome, what changed, what was observed); surface it naturally rather than copy-pasting. Operator is gated by a separate permission (\`subagent.spawn.operator\`) so write-capable spawns are restricted to owner-tier and trusted-tier callers — if the gate denies, fall back to doing the work in your own session rather than reporting failure to the user.
|
|
72
74
|
|
|
73
75
|
**Status queries**
|
|
74
76
|
|
package/src/cli/init.ts
CHANGED
|
@@ -374,7 +374,14 @@ export const defaultWizardPrompts: WizardPrompts = {
|
|
|
374
374
|
hasExistingChannelSecrets,
|
|
375
375
|
askReuseExistingChannel,
|
|
376
376
|
runChannelFlow,
|
|
377
|
-
runOAuthLogin: (provider, cwd, model) =>
|
|
377
|
+
runOAuthLogin: async (provider, cwd, model) => {
|
|
378
|
+
const { callbacks, dispose } = buildOAuthCallbacks(provider.name)
|
|
379
|
+
try {
|
|
380
|
+
return await makeOAuthLoginRunner(callbacks)({ cwd, model })
|
|
381
|
+
} finally {
|
|
382
|
+
dispose()
|
|
383
|
+
}
|
|
384
|
+
},
|
|
378
385
|
askOAuthFailureRecovery,
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -9,41 +9,71 @@ import type { OAuthCallbacks } from '@/init/oauth-login'
|
|
|
9
9
|
// concurrent `onManualCodeInput` prompt for users whose browser is on a
|
|
10
10
|
// different host than the CLI. See src/init/oauth-login.ts for the contract
|
|
11
11
|
// on each callback and why onManualCodeInput is required for cross-device.
|
|
12
|
-
|
|
12
|
+
//
|
|
13
|
+
// Returns `{ callbacks, dispose }` rather than bare callbacks because of a
|
|
14
|
+
// pi-ai contract gap: pi-ai races `onManualCodeInput()` against the local
|
|
15
|
+
// callback server (packages/ai/src/utils/oauth/anthropic.ts:210-253). When
|
|
16
|
+
// the browser wins the race, pi-ai sets `result.code` and falls through to
|
|
17
|
+
// token exchange WITHOUT calling `server.cancelWait()` on the manual side —
|
|
18
|
+
// the manual `text()` prompt is left dangling in clack's render pipeline,
|
|
19
|
+
// re-appearing after every subsequent log line. Without the dispose hook,
|
|
20
|
+
// the user sees "Logged in to {Provider}" immediately followed by the stale
|
|
21
|
+
// "paste the redirect URL here" prompt that's now meaningless. Each call
|
|
22
|
+
// site (init/provider) MUST call `dispose()` in a finally after the OAuth
|
|
23
|
+
// runner returns so the orphaned prompt aborts cleanly; clack honors the
|
|
24
|
+
// signal by resolving the prompt with cancel state, the cancel branch
|
|
25
|
+
// throws inside our callback, and pi-ai's outer `.catch()` swallows it
|
|
26
|
+
// (since it stops awaiting the manual promise on the winning-browser path).
|
|
27
|
+
export type OAuthCallbackHandle = {
|
|
28
|
+
callbacks: OAuthCallbacks
|
|
29
|
+
dispose: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildOAuthCallbacks(providerName: string): OAuthCallbackHandle {
|
|
33
|
+
const controller = new AbortController()
|
|
34
|
+
const { signal } = controller
|
|
13
35
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
dispose: () => controller.abort(),
|
|
37
|
+
callbacks: {
|
|
38
|
+
onAuth: (url, instructions) => {
|
|
39
|
+
// Don't put the URL inside note(): clack wraps long lines with the box
|
|
40
|
+
// border `│` on each wrapped segment, which corrupts the URL when the
|
|
41
|
+
// user copy-pastes it. Keep instructional text in the box, but print
|
|
42
|
+
// the URL itself as a bare console.log line that any terminal will
|
|
43
|
+
// hyperlink intact.
|
|
44
|
+
const preamble = [
|
|
45
|
+
`Open this URL in your browser to sign in to ${providerName}.`,
|
|
46
|
+
'',
|
|
47
|
+
'If your browser shows "this site can\'t be reached" after you sign in,',
|
|
48
|
+
'copy the full address from the top of the browser and paste it below.',
|
|
49
|
+
]
|
|
50
|
+
if (instructions) preamble.push('', instructions)
|
|
51
|
+
note(preamble.join('\n'), 'Browser login')
|
|
52
|
+
console.log(url)
|
|
53
|
+
console.log('')
|
|
54
|
+
},
|
|
55
|
+
onProgress: (message) => {
|
|
56
|
+
log.info(message)
|
|
57
|
+
},
|
|
58
|
+
onPrompt: async (message, placeholder) => {
|
|
59
|
+
const value = await text({
|
|
60
|
+
message,
|
|
61
|
+
signal,
|
|
62
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
63
|
+
})
|
|
64
|
+
if (isCancel(value)) return null
|
|
65
|
+
return value
|
|
66
|
+
},
|
|
67
|
+
onManualCodeInput: async () => {
|
|
68
|
+
const value = await text({
|
|
69
|
+
message:
|
|
70
|
+
'If your browser shows "this site can\'t be reached" after you sign in, copy the full address from the top of the browser and paste it here:',
|
|
71
|
+
placeholder: 'http://localhost:1455/auth/callback?code=...&state=...',
|
|
72
|
+
signal,
|
|
73
|
+
})
|
|
74
|
+
if (isCancel(value)) throw new Error('Login cancelled by user')
|
|
75
|
+
return value
|
|
76
|
+
},
|
|
47
77
|
},
|
|
48
78
|
}
|
|
49
79
|
}
|
package/src/cli/provider.ts
CHANGED
|
@@ -367,10 +367,15 @@ async function runOAuthLogin(cwd: string, providerId: KnownProviderId): Promise<
|
|
|
367
367
|
}
|
|
368
368
|
const modelRef = `${providerId}/${ref}` as const
|
|
369
369
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
const { callbacks, dispose } = buildOAuthCallbacks(provider.name)
|
|
371
|
+
try {
|
|
372
|
+
const runner = makeOAuthLoginRunner(callbacks)
|
|
373
|
+
const result = await runner({ cwd, model: modelRef as Parameters<typeof runner>[0]['model'] })
|
|
374
|
+
if (!result.ok) return { ok: false, reason: result.reason }
|
|
375
|
+
return { ok: true }
|
|
376
|
+
} finally {
|
|
377
|
+
dispose()
|
|
378
|
+
}
|
|
374
379
|
}
|
|
375
380
|
|
|
376
381
|
function authHint(id: KnownProviderId): string {
|
package/src/config/config.ts
CHANGED
|
@@ -420,15 +420,39 @@ export function expandMountPath(input: string, cwd: string): string {
|
|
|
420
420
|
|
|
421
421
|
// Loaded eagerly from process.cwd()/typeclaw.json at module-import time so
|
|
422
422
|
// citty arg defaults (e.g. config.port in src/cli/*.ts) see real values, not
|
|
423
|
-
// hardcoded fallbacks. Missing file → schema defaults; malformed file →
|
|
424
|
-
//
|
|
425
|
-
//
|
|
423
|
+
// hardcoded fallbacks. Missing file → schema defaults; malformed file → ALSO
|
|
424
|
+
// schema defaults plus a stderr warning.
|
|
425
|
+
//
|
|
426
|
+
// Why soft-fail and not throw: every CLI command — including diagnostic ones
|
|
427
|
+
// (`typeclaw status`, `typeclaw doctor`, `typeclaw logs`, `typeclaw stop`,
|
|
428
|
+
// `typeclaw usage`, `typeclaw tui`) — pays this eager-load cost through its
|
|
429
|
+
// import graph, regardless of whether the command actually reads config. A
|
|
430
|
+
// hard throw here turns every read-only diagnostic into a crash exactly when
|
|
431
|
+
// the user needs the diagnostic to figure out what's wrong with their config.
|
|
432
|
+
// `validateConfig` (called by `start`/`restart`/`reload`/host-side mutations)
|
|
433
|
+
// is the strict gate for destructive paths; that's where malformed-config
|
|
434
|
+
// errors should surface, not at module-import time.
|
|
426
435
|
//
|
|
427
436
|
// `config` is a module-import-time snapshot. Container-stage code that must
|
|
428
437
|
// observe `typeclaw run` reloads should call `getConfig()` instead, which
|
|
429
438
|
// returns the current swapped-in value. Host-stage CLI processes are
|
|
430
439
|
// short-lived, so they keep using `config` directly.
|
|
431
|
-
export const config: Config =
|
|
440
|
+
export const config: Config = loadConfigSyncOrDefaults(process.cwd())
|
|
441
|
+
|
|
442
|
+
export function loadConfigSyncOrDefaults(cwd: string, options: { warn?: (message: string) => void } = {}): Config {
|
|
443
|
+
try {
|
|
444
|
+
return loadConfigSync(cwd)
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
447
|
+
const warn = options.warn ?? ((message: string) => process.stderr.write(message))
|
|
448
|
+
warn(
|
|
449
|
+
`warning: ${detail}\n` +
|
|
450
|
+
`warning: continuing with default config so diagnostic commands still work; ` +
|
|
451
|
+
`run \`typeclaw doctor\` or fix ${CONFIG_FILE} before \`typeclaw start\`/\`restart\`/\`reload\`.\n`,
|
|
452
|
+
)
|
|
453
|
+
return configSchema.parse({})
|
|
454
|
+
}
|
|
455
|
+
}
|
|
432
456
|
|
|
433
457
|
let current: Config = config
|
|
434
458
|
|
package/src/config/providers.ts
CHANGED
|
@@ -108,6 +108,112 @@ export const KNOWN_PROVIDERS = {
|
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
|
+
// Anthropic Claude — both the Anthropic Console API (ANTHROPIC_API_KEY)
|
|
112
|
+
// and Claude Pro/Max/Team/Enterprise subscriptions (OAuth) reach the same
|
|
113
|
+
// /v1/messages endpoint and share one provider id. Auth path determines
|
|
114
|
+
// which headers pi-ai's `anthropic-messages` transport injects: API key
|
|
115
|
+
// sends a plain `x-api-key`; OAuth sends Bearer + Claude Code identity
|
|
116
|
+
// (anthropic-beta: claude-code-20250219,oauth-2025-04-20 +
|
|
117
|
+
// user-agent: claude-cli/<version>), which is exactly the surface a
|
|
118
|
+
// subscriber's `claude setup-token` credential authorizes. The OAuth dance
|
|
119
|
+
// itself is authorization-code + PKCE against `claude.ai/oauth/authorize`
|
|
120
|
+
// with a localhost callback server (not device-code); the existing
|
|
121
|
+
// `typeclaw-claude-code` skill documents the user-side flow for getting
|
|
122
|
+
// a subscription credential onto the agent when the in-container browser
|
|
123
|
+
// callback can't reach the user's machine.
|
|
124
|
+
//
|
|
125
|
+
// anthropic is the FIRST provider in the registry where both auth modes
|
|
126
|
+
// coexist on one entry. The runtime in src/agent/auth.ts has a load-bearing
|
|
127
|
+
// resolution rule: when secrets.json#providers.anthropic carries an OAuth
|
|
128
|
+
// credential, `ANTHROPIC_API_KEY` in .env is IGNORED (OAuth-on-disk wins
|
|
129
|
+
// because env-wins only applies to api-key-shaped credentials). For
|
|
130
|
+
// api-key-only providers this is invisible; for anthropic it surfaces as
|
|
131
|
+
// "I added the env var but the agent still uses OAuth." The mitigation is
|
|
132
|
+
// to remove the OAuth credential explicitly (`typeclaw provider remove
|
|
133
|
+
// anthropic`) before relying on the env-var path. Same rule applies to any
|
|
134
|
+
// future dual-auth provider — keep the surprise in mind when expanding.
|
|
135
|
+
//
|
|
136
|
+
// Model lineup is the current GA tier as of 2026-04-16: Opus 4.7 (top,
|
|
137
|
+
// released Apr 16 2026), Sonnet 4.6 (mid, Feb 5 2026), Haiku 4.5 (fast,
|
|
138
|
+
// Oct 1 2025). Anthropic's own model overview lists these three as the
|
|
139
|
+
// current recommended set and flags earlier Opus/Sonnet variants with
|
|
140
|
+
// "Consider migrating to current models." Opus 4 / Sonnet 4 are deprecated
|
|
141
|
+
// (retirement: Jun 15 2026); the 4.5/4.6 alternates remain Active but are
|
|
142
|
+
// not the recommended path.
|
|
143
|
+
//
|
|
144
|
+
// ID semantics differ across the lineup and matter for forward-compat:
|
|
145
|
+
// - `claude-haiku-4-5` is a 4.5-generation CONVENIENCE ALIAS that
|
|
146
|
+
// resolves to the latest dated snapshot (currently `-20251001`). Per
|
|
147
|
+
// Anthropic's model-id docs, pre-4.6 dateless ids are evergreen
|
|
148
|
+
// pointers — Anthropic can ship a new dated snapshot under the same
|
|
149
|
+
// alias and we pick it up automatically.
|
|
150
|
+
// - `claude-sonnet-4-6` and `claude-opus-4-7` are 4.6+-generation PINNED
|
|
151
|
+
// SNAPSHOTS, not aliases. Anthropic explicitly says "the dateless ID is
|
|
152
|
+
// the canonical model ID for that release. It maps to a single, fixed
|
|
153
|
+
// model snapshot." A future Sonnet 4.6.1 (if it ever exists) would ship
|
|
154
|
+
// under a new id, NOT silently replace `claude-sonnet-4-6`.
|
|
155
|
+
// Consequence for refresh discipline: bumping Haiku is a no-op (alias
|
|
156
|
+
// catches the latest); bumping Sonnet/Opus to a future 4.7+ family is a
|
|
157
|
+
// real edit here. Don't assume `claude-opus-4-7` will silently advance.
|
|
158
|
+
//
|
|
159
|
+
// Opus 4.7 specifics that affect cost accounting:
|
|
160
|
+
// - New tokenizer: same input maps to 1.0-1.3x more tokens than prior
|
|
161
|
+
// generations depending on content type. Per-token price is unchanged
|
|
162
|
+
// vs Opus 4.6, but total cost on identical workloads can rise meaningfully.
|
|
163
|
+
// - 1M token context window (vs 200k on Haiku) and 128k max output (vs
|
|
164
|
+
// 64k on Sonnet/Haiku). 1M context is at standard pricing — no surcharge.
|
|
165
|
+
// - New `xhigh` effort level between `high` and `max` (pi-ai 0.67.x may
|
|
166
|
+
// not surface this knob yet; check before relying on it).
|
|
167
|
+
//
|
|
168
|
+
// Pricing mirrors Anthropic's official table as of 2026-05; cacheWrite is
|
|
169
|
+
// the 5m-TTL rate (1.25x input). 1h TTL is ~2x input (not modeled here —
|
|
170
|
+
// pi-ai's `cacheWrite` field captures the default 5m rate only).
|
|
171
|
+
anthropic: {
|
|
172
|
+
id: 'anthropic',
|
|
173
|
+
name: 'Anthropic',
|
|
174
|
+
baseUrl: 'https://api.anthropic.com',
|
|
175
|
+
auth: ['api-key', 'oauth'],
|
|
176
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
177
|
+
oauthProviderId: 'anthropic',
|
|
178
|
+
models: {
|
|
179
|
+
'claude-haiku-4-5': {
|
|
180
|
+
id: 'claude-haiku-4-5',
|
|
181
|
+
name: 'Claude Haiku 4.5',
|
|
182
|
+
api: 'anthropic-messages',
|
|
183
|
+
provider: 'anthropic',
|
|
184
|
+
baseUrl: 'https://api.anthropic.com',
|
|
185
|
+
reasoning: true,
|
|
186
|
+
input: ['text', 'image'],
|
|
187
|
+
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
188
|
+
contextWindow: 200000,
|
|
189
|
+
maxTokens: 64000,
|
|
190
|
+
},
|
|
191
|
+
'claude-sonnet-4-6': {
|
|
192
|
+
id: 'claude-sonnet-4-6',
|
|
193
|
+
name: 'Claude Sonnet 4.6',
|
|
194
|
+
api: 'anthropic-messages',
|
|
195
|
+
provider: 'anthropic',
|
|
196
|
+
baseUrl: 'https://api.anthropic.com',
|
|
197
|
+
reasoning: true,
|
|
198
|
+
input: ['text', 'image'],
|
|
199
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
200
|
+
contextWindow: 1000000,
|
|
201
|
+
maxTokens: 64000,
|
|
202
|
+
},
|
|
203
|
+
'claude-opus-4-7': {
|
|
204
|
+
id: 'claude-opus-4-7',
|
|
205
|
+
name: 'Claude Opus 4.7',
|
|
206
|
+
api: 'anthropic-messages',
|
|
207
|
+
provider: 'anthropic',
|
|
208
|
+
baseUrl: 'https://api.anthropic.com',
|
|
209
|
+
reasoning: true,
|
|
210
|
+
input: ['text', 'image'],
|
|
211
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
212
|
+
contextWindow: 1000000,
|
|
213
|
+
maxTokens: 128000,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
111
217
|
// ChatGPT Plus/Pro subscription via the OAuth Codex backend. No API key
|
|
112
218
|
// path here on purpose — the Codex backend is OAuth-only upstream.
|
|
113
219
|
//
|
package/src/init/models-dev.ts
CHANGED
|
@@ -13,6 +13,7 @@ const PROVIDER_TO_MODELS_DEV: Record<KnownProviderId, string> = {
|
|
|
13
13
|
// (Codex is a backend, not a separate provider in their taxonomy). Curated
|
|
14
14
|
// entries are surfaced regardless of upstream membership.
|
|
15
15
|
'openai-codex': 'openai',
|
|
16
|
+
anthropic: 'anthropic',
|
|
16
17
|
fireworks: 'fireworks-ai',
|
|
17
18
|
zai: 'zai',
|
|
18
19
|
// zai-coding (GLM Coding Plan) is a billing surface, not a separate model
|
package/typeclaw.schema.json
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"openai/gpt-5.4-mini",
|
|
27
27
|
"openai/gpt-5.4",
|
|
28
28
|
"openai/gpt-5.5",
|
|
29
|
+
"anthropic/claude-haiku-4-5",
|
|
30
|
+
"anthropic/claude-sonnet-4-6",
|
|
31
|
+
"anthropic/claude-opus-4-7",
|
|
29
32
|
"openai-codex/gpt-5.4-mini",
|
|
30
33
|
"openai-codex/gpt-5.4",
|
|
31
34
|
"openai-codex/gpt-5.5",
|
|
@@ -50,6 +53,9 @@
|
|
|
50
53
|
"openai/gpt-5.4-mini",
|
|
51
54
|
"openai/gpt-5.4",
|
|
52
55
|
"openai/gpt-5.5",
|
|
56
|
+
"anthropic/claude-haiku-4-5",
|
|
57
|
+
"anthropic/claude-sonnet-4-6",
|
|
58
|
+
"anthropic/claude-opus-4-7",
|
|
53
59
|
"openai-codex/gpt-5.4-mini",
|
|
54
60
|
"openai-codex/gpt-5.4",
|
|
55
61
|
"openai-codex/gpt-5.5",
|