typeclaw 0.1.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/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { accessSync, constants as fsConstants, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { Model } from '@mariozechner/pi-ai'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
import { channelsSchema } from '@/channels/schema'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_MODEL_REF,
|
|
12
|
+
KNOWN_PROVIDERS,
|
|
13
|
+
listKnownModelRefs,
|
|
14
|
+
type KnownModelRef,
|
|
15
|
+
type KnownProviderId,
|
|
16
|
+
} from './providers'
|
|
17
|
+
|
|
18
|
+
const CONFIG_FILE = 'typeclaw.json'
|
|
19
|
+
|
|
20
|
+
const knownModelRefs = listKnownModelRefs() as [KnownModelRef, ...KnownModelRef[]]
|
|
21
|
+
|
|
22
|
+
// T9 keypad: T=8, Y=9, P=7, E=3
|
|
23
|
+
const DEFAULT_PORT = 8973
|
|
24
|
+
|
|
25
|
+
// Mount names land on disk as `mounts/<name>` inside the agent folder, so they
|
|
26
|
+
// share a namespace with regular filenames. Restricting to lowercase
|
|
27
|
+
// alphanumerics + `-`/`_` keeps them shell-safe and avoids accidental shadowing
|
|
28
|
+
// of files like `mounts/.git` or `mounts/Hello`.
|
|
29
|
+
const MOUNT_NAME_PATTERN = /^[a-z0-9][a-z0-9-_]*$/
|
|
30
|
+
|
|
31
|
+
export const mountSchema = z.object({
|
|
32
|
+
name: z.string().regex(MOUNT_NAME_PATTERN, 'mount name must be lowercase alphanumeric with - or _'),
|
|
33
|
+
path: z.string().min(1),
|
|
34
|
+
readOnly: z.boolean().default(false),
|
|
35
|
+
description: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type Mount = z.infer<typeof mountSchema>
|
|
39
|
+
|
|
40
|
+
const portNumber = z.number().int().min(1).max(65535)
|
|
41
|
+
|
|
42
|
+
// `allow` is the discriminator between "forward everything" ('*') and a fixed
|
|
43
|
+
// allowlist (number[]). `deny` is only meaningful when allow === '*'; combining
|
|
44
|
+
// it with a number[] allow is rejected at parse time so a typo doesn't silently
|
|
45
|
+
// drop the deny rule. An empty allowlist (`allow: []`) is the off switch.
|
|
46
|
+
export const portForwardSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
allow: z.union([z.literal('*'), z.array(portNumber)]),
|
|
49
|
+
deny: z.array(portNumber).optional(),
|
|
50
|
+
})
|
|
51
|
+
.refine((v) => !(Array.isArray(v.allow) && v.deny !== undefined && v.deny.length > 0), {
|
|
52
|
+
message: 'portForward.deny is only meaningful when allow is "*"; remove deny or set allow to "*"',
|
|
53
|
+
path: ['deny'],
|
|
54
|
+
})
|
|
55
|
+
.default({ allow: '*' })
|
|
56
|
+
|
|
57
|
+
export type PortForward = z.infer<typeof portForwardSchema>
|
|
58
|
+
|
|
59
|
+
const dockerfileLineSchema = z.string().refine((line) => !/[\r\n]/.test(line), {
|
|
60
|
+
message: 'dockerfile.append entries must be single Dockerfile lines; split multiline instructions into array entries',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// A feature toggle is either a boolean (install latest / don't install) or a
|
|
64
|
+
// version string that becomes an apt pin (`pkg=<version>`). The string form
|
|
65
|
+
// rejects whitespace and `=` so the `pkg=<version>` invocation we pass to
|
|
66
|
+
// apt-get cannot be smuggled into a separate package or option flag.
|
|
67
|
+
const dockerfileFeatureSchema = z.union([
|
|
68
|
+
z.boolean(),
|
|
69
|
+
z
|
|
70
|
+
.string()
|
|
71
|
+
.min(1)
|
|
72
|
+
.refine((v) => !/[\s=]/.test(v), {
|
|
73
|
+
message: 'dockerfile feature version strings must not contain whitespace or "="',
|
|
74
|
+
}),
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
// `default(() => ({}))` paired with field-level defaults is the idiom that
|
|
78
|
+
// makes both `dockerfile: {}` and an omitted `dockerfile` key resolve to the
|
|
79
|
+
// SAME fully-populated object. A plain `.default({})` would short-circuit the
|
|
80
|
+
// inner field defaults when the key is omitted, leaving downstream code with
|
|
81
|
+
// `{ append: undefined, tmux: undefined, ... }` and a `lines.length` crash.
|
|
82
|
+
const dockerfileObjectSchema = z.object({
|
|
83
|
+
ffmpeg: dockerfileFeatureSchema.default(false),
|
|
84
|
+
gh: dockerfileFeatureSchema.default(true),
|
|
85
|
+
python: z.boolean().default(true),
|
|
86
|
+
tmux: dockerfileFeatureSchema.default(true),
|
|
87
|
+
append: z.array(dockerfileLineSchema).default([]),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const dockerfileSchema = dockerfileObjectSchema.default(() => dockerfileObjectSchema.parse({}))
|
|
91
|
+
|
|
92
|
+
export type DockerfileConfig = z.infer<typeof dockerfileSchema>
|
|
93
|
+
export type DockerfileFeatureToggle = z.infer<typeof dockerfileFeatureSchema>
|
|
94
|
+
|
|
95
|
+
const gitignoreLineSchema = z.string().refine((line) => !/[\r\n]/.test(line), {
|
|
96
|
+
message: 'gitignore.append entries must be single gitignore lines; split multiline patterns into array entries',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const gitignoreSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
append: z.array(gitignoreLineSchema).default([]),
|
|
102
|
+
})
|
|
103
|
+
.default({ append: [] })
|
|
104
|
+
|
|
105
|
+
export type GitignoreConfig = z.infer<typeof gitignoreSchema>
|
|
106
|
+
|
|
107
|
+
export const configSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
$schema: z.string().optional(),
|
|
110
|
+
port: z.number().int().min(1).max(65535).default(DEFAULT_PORT),
|
|
111
|
+
model: z.enum(knownModelRefs).default(DEFAULT_MODEL_REF),
|
|
112
|
+
// Defaults to `[]` so the field can be omitted from `typeclaw.json` (no
|
|
113
|
+
// host paths exposed) without failing the whole config load. `typeclaw
|
|
114
|
+
// init` omits this field so users don't see noise for the empty case.
|
|
115
|
+
mounts: z.array(mountSchema).default([]),
|
|
116
|
+
plugins: z.array(z.string().min(1)).default([]),
|
|
117
|
+
// Additional names the agent answers to in channel engagement, on top
|
|
118
|
+
// of `basename(agentDir)` which is always implicit. Each entry is a
|
|
119
|
+
// plain string matched case-insensitively as a substring of the
|
|
120
|
+
// inbound text. Empty/whitespace-only entries are rejected at parse
|
|
121
|
+
// time. Defaults to `[]`. Hatching appends the agent's chosen name
|
|
122
|
+
// here, so a freshly-hatched bot already has its identity wired up.
|
|
123
|
+
alias: z.array(z.string().trim().min(1)).default([]),
|
|
124
|
+
channels: channelsSchema,
|
|
125
|
+
portForward: portForwardSchema,
|
|
126
|
+
dockerfile: dockerfileSchema,
|
|
127
|
+
gitignore: gitignoreSchema,
|
|
128
|
+
})
|
|
129
|
+
.catchall(z.unknown())
|
|
130
|
+
|
|
131
|
+
export type Config = z.infer<typeof configSchema>
|
|
132
|
+
|
|
133
|
+
export function resolveModel(ref: KnownModelRef): Model<'openai-completions'> | Model<'openai-responses'> {
|
|
134
|
+
// Model IDs can contain '/', so split only on the first separator.
|
|
135
|
+
const slash = ref.indexOf('/')
|
|
136
|
+
const providerId = ref.slice(0, slash) as KnownProviderId
|
|
137
|
+
const modelId = ref.slice(slash + 1)
|
|
138
|
+
return KNOWN_PROVIDERS[providerId].models[modelId as never]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Resolves a mount's `path` field to an absolute host path, mirroring shell
|
|
142
|
+
// expansion rules: `~`/`~/...` → home dir, relative → resolved against `cwd`,
|
|
143
|
+
// absolute → unchanged. Single source of truth so validation and Docker arg
|
|
144
|
+
// building agree on the resolved path.
|
|
145
|
+
export function expandMountPath(input: string, cwd: string): string {
|
|
146
|
+
if (input === '~' || input.startsWith('~/')) {
|
|
147
|
+
return join(homedir(), input.slice(1))
|
|
148
|
+
}
|
|
149
|
+
return isAbsolute(input) ? input : resolve(cwd, input)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Loaded eagerly from process.cwd()/typeclaw.json at module-import time so
|
|
153
|
+
// citty arg defaults (e.g. config.port in src/cli/*.ts) see real values, not
|
|
154
|
+
// hardcoded fallbacks. Missing file → schema defaults; malformed file → throw,
|
|
155
|
+
// which surfaces during CLI startup instead of silently reverting to defaults
|
|
156
|
+
// and confusing the user.
|
|
157
|
+
//
|
|
158
|
+
// `config` is a module-import-time snapshot. Container-stage code that must
|
|
159
|
+
// observe `typeclaw run` reloads should call `getConfig()` instead, which
|
|
160
|
+
// returns the current swapped-in value. Host-stage CLI processes are
|
|
161
|
+
// short-lived, so they keep using `config` directly.
|
|
162
|
+
export const config: Config = loadConfigSync(process.cwd())
|
|
163
|
+
|
|
164
|
+
let current: Config = config
|
|
165
|
+
|
|
166
|
+
export function getConfig(): Config {
|
|
167
|
+
return current
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Test-only: restore the live pointer to the module-import-time snapshot. Lets
|
|
171
|
+
// reload-aware tests run without leaking a swapped pointer into other test
|
|
172
|
+
// files that still mutate the eager `config` export directly.
|
|
173
|
+
export function __resetConfigForTesting(): void {
|
|
174
|
+
current = config
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type ConfigChange = {
|
|
178
|
+
path: string
|
|
179
|
+
before: unknown
|
|
180
|
+
after: unknown
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type ConfigReloadDiff = {
|
|
184
|
+
applied: ConfigChange[]
|
|
185
|
+
restartRequired: ConfigChange[]
|
|
186
|
+
ignored: ConfigChange[]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Reloads typeclaw.json from disk and atomically swaps the live config pointer
|
|
190
|
+
// on success. Throws (and leaves `current` untouched) when the file is
|
|
191
|
+
// malformed or schema-invalid — callers translate that into a `Reloadable`
|
|
192
|
+
// failure result.
|
|
193
|
+
export function reloadConfig(cwd: string): ConfigReloadDiff {
|
|
194
|
+
const next = loadConfigSync(cwd)
|
|
195
|
+
const diff = diffConfig(current, next)
|
|
196
|
+
current = next
|
|
197
|
+
return diff
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Field classification. The fence is intentional: only fields that are read
|
|
201
|
+
// fresh on each session/subagent/cron-reload land in `applied`. Boot-only
|
|
202
|
+
// fields (port, mounts, container/server bind) are reported as
|
|
203
|
+
// `restartRequired` so the user knows the reload landed but the change won't
|
|
204
|
+
// take effect until restart.
|
|
205
|
+
export type FieldEffect = 'applied' | 'restart-required' | 'ignored'
|
|
206
|
+
|
|
207
|
+
export const FIELD_EFFECTS: Record<string, FieldEffect> = {
|
|
208
|
+
$schema: 'ignored',
|
|
209
|
+
model: 'applied',
|
|
210
|
+
port: 'restart-required',
|
|
211
|
+
mounts: 'restart-required',
|
|
212
|
+
plugins: 'restart-required',
|
|
213
|
+
alias: 'applied',
|
|
214
|
+
channels: 'applied',
|
|
215
|
+
portForward: 'restart-required',
|
|
216
|
+
dockerfile: 'restart-required',
|
|
217
|
+
gitignore: 'restart-required',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Stable JSON for value comparison. Fields are small JSON-shaped objects, so
|
|
221
|
+
// JSON.stringify with sorted keys is sufficient and avoids a deep-equal dep.
|
|
222
|
+
function stableStringify(value: unknown): string {
|
|
223
|
+
if (value === undefined) return 'undefined'
|
|
224
|
+
return JSON.stringify(value, (_key, v: unknown) => {
|
|
225
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
226
|
+
const sorted: Record<string, unknown> = {}
|
|
227
|
+
for (const k of Object.keys(v as Record<string, unknown>).sort()) {
|
|
228
|
+
sorted[k] = (v as Record<string, unknown>)[k]
|
|
229
|
+
}
|
|
230
|
+
return sorted
|
|
231
|
+
}
|
|
232
|
+
return v
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function diffConfig(before: Config, after: Config): ConfigReloadDiff {
|
|
237
|
+
const diff: ConfigReloadDiff = { applied: [], restartRequired: [], ignored: [] }
|
|
238
|
+
const keys = new Set<string>(Object.keys(FIELD_EFFECTS))
|
|
239
|
+
|
|
240
|
+
for (const path of keys) {
|
|
241
|
+
const b = readPath(before, path)
|
|
242
|
+
const a = readPath(after, path)
|
|
243
|
+
if (stableStringify(b) === stableStringify(a)) continue
|
|
244
|
+
|
|
245
|
+
const change: ConfigChange = { path, before: b, after: a }
|
|
246
|
+
const effect = FIELD_EFFECTS[path] ?? 'applied'
|
|
247
|
+
if (effect === 'applied') diff.applied.push(change)
|
|
248
|
+
else if (effect === 'restart-required') diff.restartRequired.push(change)
|
|
249
|
+
else diff.ignored.push(change)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return diff
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readPath(obj: unknown, path: string): unknown {
|
|
256
|
+
let cur: unknown = obj
|
|
257
|
+
for (const part of path.split('.')) {
|
|
258
|
+
if (cur === null || cur === undefined) return undefined
|
|
259
|
+
cur = (cur as Record<string, unknown>)[part]
|
|
260
|
+
}
|
|
261
|
+
return cur
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Plugin configs live at the top level of typeclaw.json keyed by plugin name
|
|
265
|
+
// (e.g. "standup-log": { ... }). They are preserved by configSchema.catchall(z.unknown())
|
|
266
|
+
// because the schema does not predeclare these keys. This helper returns the
|
|
267
|
+
// raw map of unknown values keyed by plugin name; the plugin loader re-validates
|
|
268
|
+
// each block against its plugin's `configSchema`.
|
|
269
|
+
export function extractPluginConfigs(raw: unknown): Record<string, unknown> {
|
|
270
|
+
if (typeof raw !== 'object' || raw === null) return {}
|
|
271
|
+
const known = new Set([
|
|
272
|
+
'$schema',
|
|
273
|
+
'port',
|
|
274
|
+
'model',
|
|
275
|
+
'mounts',
|
|
276
|
+
'plugins',
|
|
277
|
+
'channels',
|
|
278
|
+
'portForward',
|
|
279
|
+
'dockerfile',
|
|
280
|
+
'gitignore',
|
|
281
|
+
])
|
|
282
|
+
const result: Record<string, unknown> = {}
|
|
283
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
284
|
+
if (!known.has(key)) result[key] = value
|
|
285
|
+
}
|
|
286
|
+
return result
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function loadPluginConfigsSync(cwd: string): Record<string, unknown> {
|
|
290
|
+
let raw: string
|
|
291
|
+
try {
|
|
292
|
+
raw = readFileSync(join(cwd, CONFIG_FILE), 'utf8')
|
|
293
|
+
} catch {
|
|
294
|
+
return {}
|
|
295
|
+
}
|
|
296
|
+
let json: unknown
|
|
297
|
+
try {
|
|
298
|
+
json = JSON.parse(raw)
|
|
299
|
+
} catch {
|
|
300
|
+
return {}
|
|
301
|
+
}
|
|
302
|
+
return extractPluginConfigs(json)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function loadConfigSync(cwd: string): Config {
|
|
306
|
+
let raw: string
|
|
307
|
+
try {
|
|
308
|
+
raw = readFileSync(join(cwd, CONFIG_FILE), 'utf8')
|
|
309
|
+
} catch {
|
|
310
|
+
return configSchema.parse({})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let json: unknown
|
|
314
|
+
try {
|
|
315
|
+
json = JSON.parse(raw)
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
318
|
+
throw new Error(`${CONFIG_FILE} is not valid JSON: ${detail}`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = configSchema.safeParse(json)
|
|
322
|
+
if (!result.success) {
|
|
323
|
+
throw new Error(`${CONFIG_FILE} is invalid: ${formatZodError(result.error)}`)
|
|
324
|
+
}
|
|
325
|
+
return result.data
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export type ValidateConfigResult = { ok: true } | { ok: false; reason: string }
|
|
329
|
+
|
|
330
|
+
// Missing file → ok (matches `loadMounts` in src/container/up.ts; `isInitialized`
|
|
331
|
+
// is the dedicated check for "not initialized"). Present but invalid → fail, so
|
|
332
|
+
// `restart` doesn't stop the container before discovering the config is broken.
|
|
333
|
+
//
|
|
334
|
+
// Mount accessibility is checked here (after schema parse succeeds) so every
|
|
335
|
+
// caller — `typeclaw start`, `restart`, `reload`, hostd's restart RPC — fails
|
|
336
|
+
// fast with a clear, mount-named error instead of letting Docker surface a
|
|
337
|
+
// confusing path-sharing error (or, on some Linux setups, silently bind-mount
|
|
338
|
+
// an empty auto-created directory). First-failure reporting matches the
|
|
339
|
+
// schema-error path's shape; users fix one and re-run.
|
|
340
|
+
export function validateConfig(cwd: string): ValidateConfigResult {
|
|
341
|
+
let raw: string
|
|
342
|
+
try {
|
|
343
|
+
raw = readFileSync(join(cwd, CONFIG_FILE), 'utf8')
|
|
344
|
+
} catch {
|
|
345
|
+
return { ok: true }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let json: unknown
|
|
349
|
+
try {
|
|
350
|
+
json = JSON.parse(raw)
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
353
|
+
return { ok: false, reason: `${CONFIG_FILE} is not valid JSON: ${detail}` }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const result = configSchema.safeParse(json)
|
|
357
|
+
if (!result.success) {
|
|
358
|
+
return { ok: false, reason: `${CONFIG_FILE} is invalid: ${formatZodError(result.error)}` }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const mount of result.data.mounts) {
|
|
362
|
+
const check = validateMount(mount, cwd)
|
|
363
|
+
if (!check.ok) return check
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { ok: true }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Verifies a mount's host path: exists, is a directory, is readable, and is
|
|
370
|
+
// writable when not declared `readOnly`. Symlinks are followed (statSync's
|
|
371
|
+
// default) so a broken symlink reads as "does not exist". Permission checks
|
|
372
|
+
// are skipped when running as root (uid 0) — euidaccess returns success
|
|
373
|
+
// regardless, so the test would be vacuous and inconsistent with non-root.
|
|
374
|
+
export function validateMount(mount: Mount, cwd: string): ValidateConfigResult {
|
|
375
|
+
const resolved = expandMountPath(mount.path, cwd)
|
|
376
|
+
const label = `mount "${mount.name}"`
|
|
377
|
+
|
|
378
|
+
let stats: ReturnType<typeof statSync>
|
|
379
|
+
try {
|
|
380
|
+
stats = statSync(resolved)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
383
|
+
if (code === 'ENOENT') {
|
|
384
|
+
return { ok: false, reason: `${label}: path ${resolved} does not exist` }
|
|
385
|
+
}
|
|
386
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
387
|
+
return { ok: false, reason: `${label}: cannot stat ${resolved}: ${detail}` }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!stats.isDirectory()) {
|
|
391
|
+
return { ok: false, reason: `${label}: path ${resolved} is not a directory` }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0
|
|
395
|
+
if (isRoot) return { ok: true }
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
accessSync(resolved, fsConstants.R_OK)
|
|
399
|
+
} catch {
|
|
400
|
+
return { ok: false, reason: `${label}: path ${resolved} is not readable` }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!mount.readOnly) {
|
|
404
|
+
try {
|
|
405
|
+
accessSync(resolved, fsConstants.W_OK)
|
|
406
|
+
} catch {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
reason: `${label}: path ${resolved} is not writable (declare readOnly: true if read-only access is intended)`,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { ok: true }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function formatZodError(error: z.ZodError): string {
|
|
418
|
+
return error.issues
|
|
419
|
+
.map((issue) => {
|
|
420
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>'
|
|
421
|
+
return `${path}: ${issue.message}`
|
|
422
|
+
})
|
|
423
|
+
.join('; ')
|
|
424
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export {
|
|
2
|
+
config,
|
|
3
|
+
configSchema,
|
|
4
|
+
expandMountPath,
|
|
5
|
+
extractPluginConfigs,
|
|
6
|
+
getConfig,
|
|
7
|
+
loadConfigSync,
|
|
8
|
+
loadPluginConfigsSync,
|
|
9
|
+
mountSchema,
|
|
10
|
+
dockerfileSchema,
|
|
11
|
+
portForwardSchema,
|
|
12
|
+
reloadConfig,
|
|
13
|
+
resolveModel,
|
|
14
|
+
validateConfig,
|
|
15
|
+
validateMount,
|
|
16
|
+
type Config,
|
|
17
|
+
type ConfigChange,
|
|
18
|
+
type ConfigReloadDiff,
|
|
19
|
+
type DockerfileConfig,
|
|
20
|
+
type Mount,
|
|
21
|
+
type PortForward,
|
|
22
|
+
type ValidateConfigResult,
|
|
23
|
+
} from './config'
|
|
24
|
+
export { type KnownModelRef, type KnownProviderId } from './providers'
|
|
25
|
+
export { createConfigReloadable, type CreateConfigReloadableOptions } from './reloadable'
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { Api, Model } from '@mariozechner/pi-ai'
|
|
2
|
+
|
|
3
|
+
// Authentication mechanism a provider supports. `api-key` reads a static key
|
|
4
|
+
// from .env (the original path); `oauth` runs a browser flow at init time and
|
|
5
|
+
// stores rotating credentials in secrets.json. The CLI picker uses this to ask
|
|
6
|
+
// "API key or OAuth?" only when both are wired up.
|
|
7
|
+
export type AuthMethod = 'api-key' | 'oauth'
|
|
8
|
+
|
|
9
|
+
// `apiKeyEnv` and `oauthProviderId` are both always present on the literal
|
|
10
|
+
// to keep `as const satisfies` narrowing easy on the consumer side; entries
|
|
11
|
+
// that don't apply to a given provider are set to `null` rather than omitted.
|
|
12
|
+
// Consumers check `auth.includes('api-key')` / `auth.includes('oauth')` to
|
|
13
|
+
// decide which field to consult.
|
|
14
|
+
type KnownProvider = {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
baseUrl: string
|
|
18
|
+
auth: ReadonlyArray<AuthMethod>
|
|
19
|
+
apiKeyEnv: string | null
|
|
20
|
+
oauthProviderId: string | null
|
|
21
|
+
models: Record<string, Model<Api>>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Curated allowlist of providers + models that are wired into the agent
|
|
25
|
+
// runtime. The values here back the Zod enum on `configSchema.model`, so any
|
|
26
|
+
// model the user can put in `typeclaw.json` MUST appear here verbatim. The
|
|
27
|
+
// init-time picker may surface additional models from models.dev, but it
|
|
28
|
+
// resolves them through this list before scaffolding (anything missing falls
|
|
29
|
+
// back to a curated default).
|
|
30
|
+
//
|
|
31
|
+
// Adding a new model: append it to the matching provider's `models` map. Each
|
|
32
|
+
// model object is the literal `Model<...>` that pi-ai consumes — keep it
|
|
33
|
+
// faithful to https://github.com/mariozechner/pi-ai (the readme's "Custom
|
|
34
|
+
// Models" section). `setRuntimeApiKey(provider, key)` keys off the `provider`
|
|
35
|
+
// field, so it MUST match the outer provider id.
|
|
36
|
+
//
|
|
37
|
+
// Adding a new provider: add a top-level entry. Set `auth` to the supported
|
|
38
|
+
// methods. For `api-key` providers, `apiKeyEnv` is the .env var typeclaw
|
|
39
|
+
// writes at init and reads at boot (match the upstream provider's standard,
|
|
40
|
+
// e.g. `OPENAI_API_KEY`). For `oauth` providers, `oauthProviderId` MUST match
|
|
41
|
+
// a pi-ai OAuth provider id exactly, otherwise `authStorage.login()` will
|
|
42
|
+
// throw "Unknown OAuth provider".
|
|
43
|
+
export const KNOWN_PROVIDERS = {
|
|
44
|
+
openai: {
|
|
45
|
+
id: 'openai',
|
|
46
|
+
name: 'OpenAI',
|
|
47
|
+
// OpenAI's library auto-detects this from `provider: 'openai'`, but we
|
|
48
|
+
// store it explicitly so the init wizard can show users which endpoint
|
|
49
|
+
// their key will hit.
|
|
50
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
51
|
+
auth: ['api-key'],
|
|
52
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
53
|
+
oauthProviderId: null,
|
|
54
|
+
// Costs and context windows mirror models.dev as of 2026-05-10. When
|
|
55
|
+
// refreshing, also rerun `scripts/generate-schema.ts` so typeclaw.schema.json
|
|
56
|
+
// picks up new enum values.
|
|
57
|
+
models: {
|
|
58
|
+
// Default. Cheapest tool-calling reasoning model in the family;
|
|
59
|
+
// available on every paid OpenAI account tier.
|
|
60
|
+
'gpt-5.4-nano': {
|
|
61
|
+
id: 'gpt-5.4-nano',
|
|
62
|
+
name: 'GPT-5.4 nano',
|
|
63
|
+
api: 'openai-responses',
|
|
64
|
+
provider: 'openai',
|
|
65
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
66
|
+
reasoning: true,
|
|
67
|
+
input: ['text', 'image'],
|
|
68
|
+
cost: { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 },
|
|
69
|
+
contextWindow: 400000,
|
|
70
|
+
maxTokens: 128000,
|
|
71
|
+
},
|
|
72
|
+
'gpt-5.4-mini': {
|
|
73
|
+
id: 'gpt-5.4-mini',
|
|
74
|
+
name: 'GPT-5.4 mini',
|
|
75
|
+
api: 'openai-responses',
|
|
76
|
+
provider: 'openai',
|
|
77
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
78
|
+
reasoning: true,
|
|
79
|
+
input: ['text', 'image'],
|
|
80
|
+
cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
|
81
|
+
contextWindow: 400000,
|
|
82
|
+
maxTokens: 128000,
|
|
83
|
+
},
|
|
84
|
+
'gpt-5.4': {
|
|
85
|
+
id: 'gpt-5.4',
|
|
86
|
+
name: 'GPT-5.4',
|
|
87
|
+
api: 'openai-responses',
|
|
88
|
+
provider: 'openai',
|
|
89
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
90
|
+
reasoning: true,
|
|
91
|
+
input: ['text', 'image'],
|
|
92
|
+
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
93
|
+
contextWindow: 1050000,
|
|
94
|
+
maxTokens: 128000,
|
|
95
|
+
},
|
|
96
|
+
'gpt-5.5': {
|
|
97
|
+
id: 'gpt-5.5',
|
|
98
|
+
name: 'GPT-5.5',
|
|
99
|
+
api: 'openai-responses',
|
|
100
|
+
provider: 'openai',
|
|
101
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
102
|
+
reasoning: true,
|
|
103
|
+
input: ['text', 'image'],
|
|
104
|
+
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
|
105
|
+
contextWindow: 1050000,
|
|
106
|
+
maxTokens: 128000,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
// ChatGPT Plus/Pro subscription via the OAuth Codex backend. No API key
|
|
111
|
+
// path here on purpose — the Codex backend is OAuth-only upstream.
|
|
112
|
+
//
|
|
113
|
+
// pi-ai 0.73.1's `openai-codex` bucket carries gpt-5.5 (and 5.4) against
|
|
114
|
+
// chatgpt.com/backend-api. We pin pi-coding-agent ^0.67.3 today, which
|
|
115
|
+
// ships pi-ai 0.67.3 and lacks those entries — but we hand pi-ai a
|
|
116
|
+
// freshly-constructed `Model<>` literal via resolveModel(), bypassing its
|
|
117
|
+
// built-in catalog entirely (same trick we use for kimi-k2p6-turbo). So
|
|
118
|
+
// these ids work end-to-end as long as the Codex backend itself accepts
|
|
119
|
+
// them, which it does for ChatGPT Plus/Pro accounts as of 2026-05-10.
|
|
120
|
+
'openai-codex': {
|
|
121
|
+
id: 'openai-codex',
|
|
122
|
+
name: 'OpenAI Codex (ChatGPT Plus/Pro)',
|
|
123
|
+
baseUrl: 'https://chatgpt.com/backend-api',
|
|
124
|
+
auth: ['oauth'],
|
|
125
|
+
apiKeyEnv: null,
|
|
126
|
+
oauthProviderId: 'openai-codex',
|
|
127
|
+
models: {
|
|
128
|
+
'gpt-5.4-mini': {
|
|
129
|
+
id: 'gpt-5.4-mini',
|
|
130
|
+
name: 'GPT-5.4 mini',
|
|
131
|
+
api: 'openai-codex-responses',
|
|
132
|
+
provider: 'openai-codex',
|
|
133
|
+
baseUrl: 'https://chatgpt.com/backend-api',
|
|
134
|
+
reasoning: true,
|
|
135
|
+
input: ['text', 'image'],
|
|
136
|
+
cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
|
137
|
+
contextWindow: 272000,
|
|
138
|
+
maxTokens: 128000,
|
|
139
|
+
},
|
|
140
|
+
'gpt-5.4': {
|
|
141
|
+
id: 'gpt-5.4',
|
|
142
|
+
name: 'GPT-5.4',
|
|
143
|
+
api: 'openai-codex-responses',
|
|
144
|
+
provider: 'openai-codex',
|
|
145
|
+
baseUrl: 'https://chatgpt.com/backend-api',
|
|
146
|
+
reasoning: true,
|
|
147
|
+
input: ['text', 'image'],
|
|
148
|
+
cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
149
|
+
contextWindow: 272000,
|
|
150
|
+
maxTokens: 128000,
|
|
151
|
+
},
|
|
152
|
+
'gpt-5.5': {
|
|
153
|
+
id: 'gpt-5.5',
|
|
154
|
+
name: 'GPT-5.5',
|
|
155
|
+
api: 'openai-codex-responses',
|
|
156
|
+
provider: 'openai-codex',
|
|
157
|
+
baseUrl: 'https://chatgpt.com/backend-api',
|
|
158
|
+
reasoning: true,
|
|
159
|
+
input: ['text', 'image'],
|
|
160
|
+
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
|
161
|
+
contextWindow: 272000,
|
|
162
|
+
maxTokens: 128000,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
fireworks: {
|
|
167
|
+
id: 'fireworks',
|
|
168
|
+
name: 'Fireworks',
|
|
169
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
170
|
+
auth: ['api-key'],
|
|
171
|
+
apiKeyEnv: 'FIREWORKS_API_KEY',
|
|
172
|
+
oauthProviderId: null,
|
|
173
|
+
models: {
|
|
174
|
+
// Kept available even though models.dev hasn't indexed it yet —
|
|
175
|
+
// Fireworks ships this router as an alias to the latest k2.6 weights.
|
|
176
|
+
'accounts/fireworks/routers/kimi-k2p6-turbo': {
|
|
177
|
+
id: 'accounts/fireworks/routers/kimi-k2p6-turbo',
|
|
178
|
+
name: 'Kimi K2.6 Turbo',
|
|
179
|
+
api: 'openai-completions',
|
|
180
|
+
provider: 'fireworks',
|
|
181
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
182
|
+
reasoning: true,
|
|
183
|
+
input: ['text', 'image'],
|
|
184
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
185
|
+
contextWindow: 256000,
|
|
186
|
+
maxTokens: 256000,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
} as const satisfies Record<string, KnownProvider>
|
|
191
|
+
|
|
192
|
+
export type KnownProviderId = keyof typeof KNOWN_PROVIDERS
|
|
193
|
+
|
|
194
|
+
export type KnownModelRef = {
|
|
195
|
+
[P in KnownProviderId]: `${P}/${Extract<keyof (typeof KNOWN_PROVIDERS)[P]['models'], string>}`
|
|
196
|
+
}[KnownProviderId]
|
|
197
|
+
|
|
198
|
+
export function listKnownModelRefs(): KnownModelRef[] {
|
|
199
|
+
const refs: string[] = []
|
|
200
|
+
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
201
|
+
for (const modelId of Object.keys(KNOWN_PROVIDERS[providerId].models)) {
|
|
202
|
+
refs.push(`${providerId}/${modelId}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return refs as KnownModelRef[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The default we hand to scaffolded `typeclaw.json` and the schema's
|
|
209
|
+
// `model.default`. Lives here (next to the provider table) so adding a model
|
|
210
|
+
// can't drift from the field default — both come from the same module.
|
|
211
|
+
export const DEFAULT_MODEL_REF: KnownModelRef = 'openai/gpt-5.4-nano'
|
|
212
|
+
|
|
213
|
+
export function providerForModelRef(ref: KnownModelRef): KnownProviderId {
|
|
214
|
+
// KnownModelRef is `${provider}/${modelId}`, but provider IDs themselves can
|
|
215
|
+
// contain '-' and model IDs can contain '/' (Fireworks). We split on the
|
|
216
|
+
// first slash that follows a registered provider id.
|
|
217
|
+
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
218
|
+
if (ref.startsWith(`${providerId}/`)) return providerId
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`Unknown provider in model ref: ${ref}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// `as const satisfies` narrows each entry's `auth` to a tuple of its specific
|
|
224
|
+
// literal values, which makes `provider.auth.includes('oauth')` fail to
|
|
225
|
+
// compile on api-key-only entries (because TS thinks the array can never
|
|
226
|
+
// contain 'oauth'). These accessors widen the membership check back to
|
|
227
|
+
// AuthMethod so consumers can branch without per-provider casts.
|
|
228
|
+
export function supportsApiKey(provider: { auth: ReadonlyArray<AuthMethod> }): boolean {
|
|
229
|
+
return (provider.auth as ReadonlyArray<AuthMethod>).includes('api-key')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function supportsOAuth(provider: { auth: ReadonlyArray<AuthMethod> }): boolean {
|
|
233
|
+
return (provider.auth as ReadonlyArray<AuthMethod>).includes('oauth')
|
|
234
|
+
}
|