typeclaw 0.36.8 → 0.37.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 +2 -2
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +11 -2
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +121 -34
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +41 -7
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
KNOWN_PROVIDERS,
|
|
6
|
+
listKnownModelRefs,
|
|
7
|
+
listKnownProviderVendorIds,
|
|
8
|
+
providerIdsForVendor,
|
|
9
|
+
type KnownProviderId,
|
|
10
|
+
type KnownProviderVendorId,
|
|
11
|
+
type ModelRef,
|
|
12
|
+
} from '@/config/providers'
|
|
13
|
+
|
|
14
|
+
// In-folder scratch for an in-progress `typeclaw init`, co-located with the
|
|
15
|
+
// agent it describes. Lives under the agent's `.typeclaw/` (the same gitignored
|
|
16
|
+
// local-scratch dir as the persistent-$HOME overlay), so it self-cleans when
|
|
17
|
+
// the half-init folder is deleted, survives a folder rename mid-init, and is
|
|
18
|
+
// inspectable right next to the thing being resumed. Gitignored via
|
|
19
|
+
// `TRULY_IGNORED_PATTERNS`; never committed.
|
|
20
|
+
export const INIT_CHECKPOINT_PATH = join('.typeclaw', 'init-progress.json')
|
|
21
|
+
|
|
22
|
+
export const WIZARD_CHECKPOINT_VERSION = 1
|
|
23
|
+
|
|
24
|
+
export type WizardChannelChoice = 'slack' | 'discord' | 'telegram' | 'kakaotalk' | 'github' | 'none'
|
|
25
|
+
|
|
26
|
+
export type AuthMethod = 'api-key' | 'oauth'
|
|
27
|
+
|
|
28
|
+
// Only stable selection IDs — never `llmAuth`, tokens, OAuth data, channel
|
|
29
|
+
// secrets, the volatile models.dev catalog, or full `ModelOption` objects. The
|
|
30
|
+
// projection in `checkpointFromSelections` is the single seam that enforces
|
|
31
|
+
// this, so a secret can never leak into host state by accident.
|
|
32
|
+
export interface WizardAnswerCheckpointV1 {
|
|
33
|
+
version: typeof WIZARD_CHECKPOINT_VERSION
|
|
34
|
+
cwd: string
|
|
35
|
+
updatedAt: string
|
|
36
|
+
vendorId?: KnownProviderVendorId
|
|
37
|
+
providerId?: KnownProviderId
|
|
38
|
+
modelRef?: ModelRef | string
|
|
39
|
+
authMethod?: AuthMethod
|
|
40
|
+
visionVendorId?: KnownProviderVendorId
|
|
41
|
+
visionProviderId?: KnownProviderId
|
|
42
|
+
visionModelRef?: ModelRef | string
|
|
43
|
+
visionAuthMethod?: AuthMethod
|
|
44
|
+
channelChoice?: WizardChannelChoice
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WizardCheckpointStore {
|
|
48
|
+
load(cwd: string): Promise<WizardAnswerCheckpointV1 | undefined>
|
|
49
|
+
save(cwd: string, checkpoint: WizardAnswerCheckpointV1): Promise<void>
|
|
50
|
+
clear(cwd: string): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Selections the wizard already holds, projected to the persisted shape. Keep
|
|
54
|
+
// this the ONLY place that reads `WizardState` so secret fields are physically
|
|
55
|
+
// unable to reach the checkpoint file.
|
|
56
|
+
export interface WizardCheckpointSelections {
|
|
57
|
+
cwd: string
|
|
58
|
+
vendorId?: KnownProviderVendorId
|
|
59
|
+
providerId?: KnownProviderId
|
|
60
|
+
modelRef?: ModelRef | string
|
|
61
|
+
authMethod?: AuthMethod
|
|
62
|
+
visionVendorId?: KnownProviderVendorId
|
|
63
|
+
visionProviderId?: KnownProviderId
|
|
64
|
+
visionModelRef?: ModelRef | string
|
|
65
|
+
visionAuthMethod?: AuthMethod
|
|
66
|
+
channelChoice?: WizardChannelChoice
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function checkpointFromSelections(selections: WizardCheckpointSelections): WizardAnswerCheckpointV1 {
|
|
70
|
+
return {
|
|
71
|
+
version: WIZARD_CHECKPOINT_VERSION,
|
|
72
|
+
cwd: selections.cwd,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
...(selections.vendorId !== undefined ? { vendorId: selections.vendorId } : {}),
|
|
75
|
+
...(selections.providerId !== undefined ? { providerId: selections.providerId } : {}),
|
|
76
|
+
...(selections.modelRef !== undefined ? { modelRef: selections.modelRef } : {}),
|
|
77
|
+
...(selections.authMethod !== undefined ? { authMethod: selections.authMethod } : {}),
|
|
78
|
+
...(selections.visionVendorId !== undefined ? { visionVendorId: selections.visionVendorId } : {}),
|
|
79
|
+
...(selections.visionProviderId !== undefined ? { visionProviderId: selections.visionProviderId } : {}),
|
|
80
|
+
...(selections.visionModelRef !== undefined ? { visionModelRef: selections.visionModelRef } : {}),
|
|
81
|
+
...(selections.visionAuthMethod !== undefined ? { visionAuthMethod: selections.visionAuthMethod } : {}),
|
|
82
|
+
...(selections.channelChoice !== undefined ? { channelChoice: selections.channelChoice } : {}),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Drop any saved field that no longer references a real vendor/provider/model.
|
|
87
|
+
// Stale values cascade downward: an unknown provider invalidates its model and
|
|
88
|
+
// auth-method too, since those were chosen for a provider that no longer
|
|
89
|
+
// exists. Returns a sanitized copy; never throws on drift.
|
|
90
|
+
export function sanitizeCheckpointAgainstCatalog(
|
|
91
|
+
checkpoint: WizardAnswerCheckpointV1,
|
|
92
|
+
validModelRefs: ReadonlySet<string> = new Set(listKnownModelRefs()),
|
|
93
|
+
): WizardAnswerCheckpointV1 {
|
|
94
|
+
const sanitized: WizardAnswerCheckpointV1 = {
|
|
95
|
+
version: WIZARD_CHECKPOINT_VERSION,
|
|
96
|
+
cwd: checkpoint.cwd,
|
|
97
|
+
updatedAt: checkpoint.updatedAt,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const vendor = pruneVendor(checkpoint.vendorId)
|
|
101
|
+
const provider = pruneProvider(vendor, checkpoint.providerId)
|
|
102
|
+
if (vendor !== undefined) sanitized.vendorId = vendor
|
|
103
|
+
if (provider !== undefined) {
|
|
104
|
+
sanitized.providerId = provider
|
|
105
|
+
if (checkpoint.modelRef !== undefined && validModelRefs.has(checkpoint.modelRef)) {
|
|
106
|
+
sanitized.modelRef = checkpoint.modelRef
|
|
107
|
+
}
|
|
108
|
+
if (checkpoint.authMethod !== undefined) sanitized.authMethod = checkpoint.authMethod
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const visionVendor = pruneVendor(checkpoint.visionVendorId)
|
|
112
|
+
const visionProvider = pruneProvider(visionVendor, checkpoint.visionProviderId)
|
|
113
|
+
if (visionVendor !== undefined) sanitized.visionVendorId = visionVendor
|
|
114
|
+
if (visionProvider !== undefined) {
|
|
115
|
+
sanitized.visionProviderId = visionProvider
|
|
116
|
+
if (checkpoint.visionModelRef !== undefined && validModelRefs.has(checkpoint.visionModelRef)) {
|
|
117
|
+
sanitized.visionModelRef = checkpoint.visionModelRef
|
|
118
|
+
}
|
|
119
|
+
if (checkpoint.visionAuthMethod !== undefined) sanitized.visionAuthMethod = checkpoint.visionAuthMethod
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (checkpoint.channelChoice !== undefined) sanitized.channelChoice = checkpoint.channelChoice
|
|
123
|
+
|
|
124
|
+
return sanitized
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pruneVendor(vendorId: KnownProviderVendorId | undefined): KnownProviderVendorId | undefined {
|
|
128
|
+
if (vendorId === undefined) return undefined
|
|
129
|
+
return listKnownProviderVendorIds().includes(vendorId) ? vendorId : undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pruneProvider(
|
|
133
|
+
vendorId: KnownProviderVendorId | undefined,
|
|
134
|
+
providerId: KnownProviderId | undefined,
|
|
135
|
+
): KnownProviderId | undefined {
|
|
136
|
+
if (vendorId === undefined || providerId === undefined) return undefined
|
|
137
|
+
if (!(providerId in KNOWN_PROVIDERS)) return undefined
|
|
138
|
+
return providerIdsForVendor(vendorId).includes(providerId) ? providerId : undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkpointFilePath(cwd: string): string {
|
|
142
|
+
return join(cwd, INIT_CHECKPOINT_PATH)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createLocalWizardCheckpointStore(): WizardCheckpointStore {
|
|
146
|
+
return {
|
|
147
|
+
async load(cwd) {
|
|
148
|
+
const path = checkpointFilePath(cwd)
|
|
149
|
+
let parsed: unknown
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(await readFile(path, 'utf8'))
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
if (!isValidCheckpoint(parsed)) return undefined
|
|
156
|
+
return parsed
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Atomic write: temp + rename within the agent's .typeclaw/ so a crash
|
|
160
|
+
// mid-write never leaves a half-written file that `load` would misparse and
|
|
161
|
+
// discard.
|
|
162
|
+
async save(cwd, checkpoint) {
|
|
163
|
+
const final = checkpointFilePath(cwd)
|
|
164
|
+
const tmp = `${final}.${process.pid}.tmp`
|
|
165
|
+
await mkdir(dirname(final), { recursive: true })
|
|
166
|
+
await writeFile(tmp, JSON.stringify(checkpoint), { mode: 0o600 })
|
|
167
|
+
await rename(tmp, final)
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
async clear(cwd) {
|
|
171
|
+
try {
|
|
172
|
+
await unlink(checkpointFilePath(cwd))
|
|
173
|
+
} catch {}
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isValidCheckpoint(value: unknown): value is WizardAnswerCheckpointV1 {
|
|
179
|
+
if (typeof value !== 'object' || value === null) return false
|
|
180
|
+
const v = value as Record<string, unknown>
|
|
181
|
+
if (v.version !== WIZARD_CHECKPOINT_VERSION) return false
|
|
182
|
+
if (typeof v.cwd !== 'string') return false
|
|
183
|
+
if (typeof v.updatedAt !== 'string') return false
|
|
184
|
+
// Every optional selection field must be a string when present. Membership
|
|
185
|
+
// (is this a real provider/model?) is the sanitizer's job, but a non-string
|
|
186
|
+
// here is structurally corrupt and would later index KNOWN_PROVIDERS or join
|
|
187
|
+
// into prompt text with a wrong shape — reject it at load.
|
|
188
|
+
return OPTIONAL_STRING_FIELDS.every((field) => v[field] === undefined || typeof v[field] === 'string')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const OPTIONAL_STRING_FIELDS = [
|
|
192
|
+
'vendorId',
|
|
193
|
+
'providerId',
|
|
194
|
+
'modelRef',
|
|
195
|
+
'authMethod',
|
|
196
|
+
'visionVendorId',
|
|
197
|
+
'visionProviderId',
|
|
198
|
+
'visionModelRef',
|
|
199
|
+
'visionAuthMethod',
|
|
200
|
+
'channelChoice',
|
|
201
|
+
] as const satisfies ReadonlyArray<keyof WizardAnswerCheckpointV1>
|
package/src/init/dockerfile.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type BuildDockerfileOptions = {
|
|
|
18
18
|
// host signal — e.g. a bare `buildDockerfile()` in tests — stays small and
|
|
19
19
|
// deterministic.
|
|
20
20
|
cjkFontsAuto?: boolean
|
|
21
|
+
// Emit a BuildKit Dockerfile (default) or strip BuildKit-only directives for
|
|
22
|
+
// hosts without buildx. `start` sets this to false when buildx is absent so
|
|
23
|
+
// the legacy `docker build` can still build the agent image.
|
|
24
|
+
buildKit?: boolean
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// Apt packages that EVERY image must have — git for the agent runtime,
|
|
@@ -75,6 +79,7 @@ const BASELINE_APT_PACKAGES = [
|
|
|
75
79
|
'util-linux',
|
|
76
80
|
'bubblewrap',
|
|
77
81
|
'jq',
|
|
82
|
+
'libgomp1',
|
|
78
83
|
] as const
|
|
79
84
|
|
|
80
85
|
// curl-impersonate is the only currently-working way to query DuckDuckGo from
|
|
@@ -573,6 +578,72 @@ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
|
|
|
573
578
|
&& chmod +x ${TYPECLAW_ENTRYPOINT_PATH}`
|
|
574
579
|
}
|
|
575
580
|
|
|
581
|
+
// Layer 7: install @huggingface/transformers with its linux-native binaries.
|
|
582
|
+
//
|
|
583
|
+
// The host node_modules is bind-mounted over /agent at runtime carrying
|
|
584
|
+
// whatever binaries the host (typically macOS) resolved, so this layer seeds
|
|
585
|
+
// LINUX binaries into an image path that survives the bind mount and still
|
|
586
|
+
// wins Node/Bun resolution. Two native-dep mechanisms, fixed differently:
|
|
587
|
+
//
|
|
588
|
+
// 1. onnxruntime-node ships its addon via a POSTINSTALL that materializes the
|
|
589
|
+
// binary inside the package dir, so a plain `bun add` covers it.
|
|
590
|
+
// 2. sharp resolves its native code from SEPARATE `@img/sharp-*` optional
|
|
591
|
+
// PACKAGES, not a postinstall addon. sharp is a MANDATORY dep of
|
|
592
|
+
// transformers, loaded eagerly because the package main chains
|
|
593
|
+
// transformers.js -> utils/image.js (top-level `import sharp`) even though
|
|
594
|
+
// typeclaw only does text feature-extraction. `bun add
|
|
595
|
+
// @huggingface/transformers` alone does NOT pull the linux `@img/*` when
|
|
596
|
+
// the bind-mounted host tree already satisfies sharp with the macOS ones —
|
|
597
|
+
// so `sharp.js` throws at import ("Could not load the sharp module using
|
|
598
|
+
// the linux-<arch> runtime") and the container exits at startup. We
|
|
599
|
+
// install the arch-matched linux platform packages EXPLICITLY to fix it.
|
|
600
|
+
//
|
|
601
|
+
// CRITICAL — install location. `typeclaw start` bind-mounts the host agent
|
|
602
|
+
// folder over /agent at runtime (`-v <cwd>:/agent`), so anything written under
|
|
603
|
+
// /agent/node_modules at BUILD time is MASKED at runtime and can never win.
|
|
604
|
+
// The prior fix ran this `bun add` under `WORKDIR /agent`, populating
|
|
605
|
+
// /agent/node_modules — invisible behind the bind mount, so the sharp crash
|
|
606
|
+
// persisted. We install from `WORKDIR /` instead: the linux packages land in
|
|
607
|
+
// the IMAGE's /node_modules, which is NOT masked by the /agent mount. Node/Bun
|
|
608
|
+
// resolution for `require('@img/sharp-linux-<arch>')` ascends from
|
|
609
|
+
// /agent/node_modules/sharp up the directory chain and finds /node_modules,
|
|
610
|
+
// so the linux binary resolves. WORKDIR is restored to /agent afterwards so
|
|
611
|
+
// later layers and the runtime CWD are unchanged.
|
|
612
|
+
//
|
|
613
|
+
// EXACT transformers version this image installs. Must stay in lockstep with
|
|
614
|
+
// `@huggingface/transformers` in the repo package.json (a guard test in
|
|
615
|
+
// dockerfile.test.ts asserts the two agree). The pin is EXACT — not a caret —
|
|
616
|
+
// because this layer's `bun add` runs at `WORKDIR /` with NO project lockfile,
|
|
617
|
+
// so the repo `bun.lock` does NOT constrain it. A bare `@huggingface/
|
|
618
|
+
// transformers` (no version) would resolve npm `latest` at BUILD time: the day
|
|
619
|
+
// a newer transformers ships, the container would install it while the
|
|
620
|
+
// sharp/libvips pins below stay fixed, and the mismatched sharp platform
|
|
621
|
+
// packages crash at container import ("Could not load the sharp module using
|
|
622
|
+
// the linux-<arch> runtime"). Pinning the version closes that drift.
|
|
623
|
+
export const TRANSFORMERS_VERSION = '4.2.0'
|
|
624
|
+
|
|
625
|
+
// sharp + libvips versions are pinned to what @huggingface/transformers@4.2.0
|
|
626
|
+
// resolves. A future transformers bump that moves sharp must bump
|
|
627
|
+
// TRANSFORMERS_VERSION, `sharp@`, `@img/sharp-linux-*`, and
|
|
628
|
+
// `@img/sharp-libvips-linux-*` together — mismatched sharp/libvips platform
|
|
629
|
+
// packages are a known failure mode. `$TARGETARCH` is `arm64` or `amd64`; an
|
|
630
|
+
// empty value (bare `docker build` without buildx) falls back to x64 for
|
|
631
|
+
// determinism.
|
|
632
|
+
const LAYER_TRANSFORMERS_INSTALL = `# Layer 7: install @huggingface/transformers with its linux-native binaries.
|
|
633
|
+
# Installs the linux onnxruntime-node addon AND sharp's linux platform packages
|
|
634
|
+
# (@img/sharp-linux-*) into the image's /node_modules so they survive the
|
|
635
|
+
# runtime /agent bind mount and still win Node/Bun resolution from
|
|
636
|
+
# /agent/node_modules/sharp. The transformers version is pinned EXACT (this
|
|
637
|
+
# bun add has no lockfile) — see src/init/dockerfile.ts for the rationale.
|
|
638
|
+
WORKDIR /
|
|
639
|
+
RUN SHARP_ARCH="$(if [ "\${TARGETARCH:-amd64}" = "arm64" ]; then echo arm64; else echo x64; fi)" \\
|
|
640
|
+
&& bun add \\
|
|
641
|
+
@huggingface/transformers@${TRANSFORMERS_VERSION} \\
|
|
642
|
+
sharp@0.34.5 \\
|
|
643
|
+
"@img/sharp-linux-\${SHARP_ARCH}@0.34.5" \\
|
|
644
|
+
"@img/sharp-libvips-linux-\${SHARP_ARCH}@1.2.4"
|
|
645
|
+
WORKDIR /agent`
|
|
646
|
+
|
|
576
647
|
// Claude Code's official installer is `curl | bash`, not apt — can't live
|
|
577
648
|
// in APT_FEATURES. Layer placed after the toggle apt install (so curl + ca-
|
|
578
649
|
// certificates from the baseline are guaranteed present) and before the
|
|
@@ -1059,7 +1130,7 @@ const TYPECLAW_CX_GLOBAL_HOOKS = JSON.stringify({
|
|
|
1059
1130
|
},
|
|
1060
1131
|
})
|
|
1061
1132
|
|
|
1062
|
-
function renderCodexCliInstallLayer(enabled: boolean): string {
|
|
1133
|
+
function renderCodexCliInstallLayer(enabled: boolean, buildKit: boolean): string {
|
|
1063
1134
|
if (!enabled) return ''
|
|
1064
1135
|
return `# Layer 5.7 (toggle): install OpenAI's Codex CLI. Opt-in via
|
|
1065
1136
|
# typeclaw.json#docker.file.codexCli. The skill \`typeclaw-codex-cli\`
|
|
@@ -1069,8 +1140,7 @@ function renderCodexCliInstallLayer(enabled: boolean): string {
|
|
|
1069
1140
|
# are pre-written at build time so the operator subagent never has to
|
|
1070
1141
|
# construct that JSON itself — same load-bearing reason as the Claude
|
|
1071
1142
|
# Code layer above.
|
|
1072
|
-
RUN
|
|
1073
|
-
bun install -g @openai/codex \\
|
|
1143
|
+
RUN ${bunCacheMount(buildKit)}bun install -g @openai/codex \\
|
|
1074
1144
|
&& codex --version > /dev/null \\
|
|
1075
1145
|
&& cat > ${TYPECLAW_CX_SESSION_START_HOOK_PATH} <<'TYPECLAW_CX_SESSION_START_HOOK_EOF'
|
|
1076
1146
|
${TYPECLAW_CX_SESSION_START_HOOK_SCRIPT}TYPECLAW_CX_SESSION_START_HOOK_EOF
|
|
@@ -1155,15 +1225,19 @@ export function buildDockerfile(
|
|
|
1155
1225
|
config: DockerfileConfig = defaultConfig(),
|
|
1156
1226
|
options: BuildDockerfileOptions = {},
|
|
1157
1227
|
): string {
|
|
1228
|
+
// buildKit is the default; `start` passes false on hosts without buildx so the
|
|
1229
|
+
// generated Dockerfile omits the `# syntax=` pragma and every cache mount —
|
|
1230
|
+
// the legacy builder accepts the result as-is, no text post-processing needed.
|
|
1231
|
+
const buildKit = options.buildKit !== false
|
|
1158
1232
|
const cjkFonts = resolveCjkFonts(config.cjkFonts, options.cjkFontsAuto ?? false)
|
|
1159
1233
|
const toggleAptArgs = collectToggleAptArgs(config, cjkFonts)
|
|
1160
|
-
const ghKeyringLayer = renderGhKeyringLayer(config.gh)
|
|
1234
|
+
const ghKeyringLayer = renderGhKeyringLayer(config.gh, buildKit)
|
|
1161
1235
|
const cloudflaredLayer = renderCloudflaredLayer(config.cloudflared)
|
|
1162
1236
|
const customLines = renderCustomDockerfileLines(config.append)
|
|
1163
1237
|
const baseImageVersion = options.baseImageVersion ?? null
|
|
1164
1238
|
|
|
1165
1239
|
const claudeCodeLayer = renderClaudeCodeInstallLayer(config.claudeCode)
|
|
1166
|
-
const codexCliLayer = renderCodexCliInstallLayer(config.codexCli)
|
|
1240
|
+
const codexCliLayer = renderCodexCliInstallLayer(config.codexCli, buildKit)
|
|
1167
1241
|
const fromAndHeavyLayers =
|
|
1168
1242
|
baseImageVersion !== null
|
|
1169
1243
|
? renderVersionedHead(
|
|
@@ -1173,11 +1247,12 @@ export function buildDockerfile(
|
|
|
1173
1247
|
cloudflaredLayer,
|
|
1174
1248
|
claudeCodeLayer,
|
|
1175
1249
|
codexCliLayer,
|
|
1250
|
+
buildKit,
|
|
1176
1251
|
)
|
|
1177
|
-
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer, claudeCodeLayer, codexCliLayer)
|
|
1252
|
+
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer, claudeCodeLayer, codexCliLayer, buildKit)
|
|
1178
1253
|
|
|
1179
|
-
|
|
1180
|
-
# AUTOGENERATED by typeclaw — do not edit.
|
|
1254
|
+
const header = buildKit ? `${BUILDKIT_HEADER}\n` : ''
|
|
1255
|
+
return `${header}# AUTOGENERATED by typeclaw — do not edit.
|
|
1181
1256
|
# This file is rewritten on every \`typeclaw start\` from src/init/dockerfile.ts
|
|
1182
1257
|
# in the typeclaw repo. Local edits will be overwritten (and committed away if
|
|
1183
1258
|
# the working tree is dirty). To change the template, edit dockerfile.ts there.
|
|
@@ -1223,8 +1298,9 @@ function renderVersionedHead(
|
|
|
1223
1298
|
cloudflaredLayer: string,
|
|
1224
1299
|
claudeCodeLayer: string,
|
|
1225
1300
|
codexCliLayer: string,
|
|
1301
|
+
buildKit: boolean,
|
|
1226
1302
|
): string {
|
|
1227
|
-
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
|
|
1303
|
+
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs, buildKit)}\n\n`
|
|
1228
1304
|
const cloudflaredBlock = cloudflaredLayer === '' ? '' : `${cloudflaredLayer}\n\n`
|
|
1229
1305
|
const claudeCodeBlock = claudeCodeLayer === '' ? '' : `${claudeCodeLayer}\n\n`
|
|
1230
1306
|
const codexCliBlock = codexCliLayer === '' ? '' : `${codexCliLayer}\n\n`
|
|
@@ -1236,6 +1312,8 @@ ARG TARGETARCH
|
|
|
1236
1312
|
|
|
1237
1313
|
${ghKeyringLayer}${toggleAptLayer}${cloudflaredBlock}${claudeCodeBlock}${codexCliBlock}${renderEntrypointShimLayer()}
|
|
1238
1314
|
|
|
1315
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1316
|
+
|
|
1239
1317
|
`
|
|
1240
1318
|
}
|
|
1241
1319
|
|
|
@@ -1249,6 +1327,7 @@ function renderInlineHead(
|
|
|
1249
1327
|
cloudflaredLayer: string,
|
|
1250
1328
|
claudeCodeLayer: string,
|
|
1251
1329
|
codexCliLayer: string,
|
|
1330
|
+
buildKit: boolean,
|
|
1252
1331
|
): string {
|
|
1253
1332
|
const baselineAndToggleArgs = [...BASELINE_APT_PACKAGES, ...toggleAptArgs]
|
|
1254
1333
|
const cloudflaredBlock = cloudflaredLayer === '' ? '' : `${cloudflaredLayer}\n\n`
|
|
@@ -1274,9 +1353,7 @@ ${ghKeyringLayer}# Layer 2 (changes when the package list changes): the actual a
|
|
|
1274
1353
|
#
|
|
1275
1354
|
# No \`rm -rf /var/lib/apt/lists/*\` because the lists live on a cache mount
|
|
1276
1355
|
# that is excluded from the image layer by definition.
|
|
1277
|
-
RUN
|
|
1278
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1279
|
-
apt-get update \\
|
|
1356
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1280
1357
|
&& apt-get install -y --no-install-recommends \\
|
|
1281
1358
|
${baselineAndToggleArgs.join(' ')} \\
|
|
1282
1359
|
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
@@ -1290,14 +1367,16 @@ ${LAYER_2_5_CURL_IMPERSONATE}
|
|
|
1290
1367
|
|
|
1291
1368
|
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
1292
1369
|
|
|
1293
|
-
${
|
|
1370
|
+
${renderAgentBrowserInstallLayer(buildKit)}
|
|
1294
1371
|
|
|
1295
1372
|
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
1296
1373
|
|
|
1297
|
-
${
|
|
1374
|
+
${renderChromeForTestingLayer(buildKit)}
|
|
1298
1375
|
|
|
1299
1376
|
${cloudflaredBlock}${claudeCodeBlock}${codexCliBlock}${renderEntrypointShimLayer()}
|
|
1300
1377
|
|
|
1378
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1379
|
+
|
|
1301
1380
|
`
|
|
1302
1381
|
}
|
|
1303
1382
|
|
|
@@ -1317,13 +1396,11 @@ RUN ARCH_BIN="$(if [ "$TARGETARCH" = "arm64" ]; then echo arm64; else echo amd64
|
|
|
1317
1396
|
`
|
|
1318
1397
|
}
|
|
1319
1398
|
|
|
1320
|
-
function renderToggleAptInstallLayer(toggleAptArgs: string[]): string {
|
|
1399
|
+
function renderToggleAptInstallLayer(toggleAptArgs: string[], buildKit: boolean): string {
|
|
1321
1400
|
return `# Layer 1 (toggle apt install): packages requested via typeclaw.json
|
|
1322
1401
|
# #docker.file toggles. Baseline + Chrome runtime libs are already in the
|
|
1323
1402
|
# base image; this layer only adds gh/tmux/python/ffmpeg/cjkFonts if enabled.
|
|
1324
|
-
RUN
|
|
1325
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1326
|
-
apt-get update \\
|
|
1403
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1327
1404
|
&& apt-get install -y --no-install-recommends \\
|
|
1328
1405
|
${toggleAptArgs.join(' ')}`
|
|
1329
1406
|
}
|
|
@@ -1353,9 +1430,7 @@ ${LAYER_0_APT_KEEP_CACHE}
|
|
|
1353
1430
|
# packages (gh/python/tmux/ffmpeg) are intentionally NOT installed here —
|
|
1354
1431
|
# they layer onto the base in the per-agent Dockerfile so users can opt in/
|
|
1355
1432
|
# out via typeclaw.json without forcing a base-image rebuild.
|
|
1356
|
-
RUN
|
|
1357
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1358
|
-
apt-get update \\
|
|
1433
|
+
RUN ${aptCacheMount(true)}apt-get update \\
|
|
1359
1434
|
&& apt-get install -y --no-install-recommends \\
|
|
1360
1435
|
${BASELINE_APT_PACKAGES.join(' ')} \\
|
|
1361
1436
|
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
@@ -1369,13 +1444,15 @@ ${LAYER_2_5_CURL_IMPERSONATE}
|
|
|
1369
1444
|
|
|
1370
1445
|
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
1371
1446
|
|
|
1372
|
-
${
|
|
1447
|
+
${renderAgentBrowserInstallLayer(true)}
|
|
1373
1448
|
|
|
1374
1449
|
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
1375
1450
|
|
|
1376
|
-
${
|
|
1451
|
+
${renderChromeForTestingLayer(true)}
|
|
1377
1452
|
|
|
1378
1453
|
${renderEntrypointShimLayer()}
|
|
1454
|
+
|
|
1455
|
+
${LAYER_TRANSFORMERS_INSTALL}
|
|
1379
1456
|
`
|
|
1380
1457
|
}
|
|
1381
1458
|
|
|
@@ -1386,6 +1463,19 @@ ${renderEntrypointShimLayer()}
|
|
|
1386
1463
|
|
|
1387
1464
|
const BUILDKIT_HEADER = `# syntax=docker/dockerfile:1.7`
|
|
1388
1465
|
|
|
1466
|
+
// Cache-mount prefixes spliced between `RUN ` and the command. BuildKit honors
|
|
1467
|
+
// `--mount=type=cache` (apt .debs / bun packages reused across builds); the
|
|
1468
|
+
// legacy builder (hosts without buildx) has no equivalent, so legacy mode emits
|
|
1469
|
+
// an empty prefix — same image, just no cross-build cache. Generating the two
|
|
1470
|
+
// modes structurally is why typeclaw needs no Dockerfile text post-processing.
|
|
1471
|
+
const APT_CACHE_MOUNT =
|
|
1472
|
+
'--mount=type=cache,target=/var/cache/apt,sharing=locked \\\n' +
|
|
1473
|
+
' --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\\n '
|
|
1474
|
+
const BUN_CACHE_MOUNT = '--mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\\n '
|
|
1475
|
+
|
|
1476
|
+
const aptCacheMount = (buildKit: boolean): string => (buildKit ? APT_CACHE_MOUNT : '')
|
|
1477
|
+
const bunCacheMount = (buildKit: boolean): string => (buildKit ? BUN_CACHE_MOUNT : '')
|
|
1478
|
+
|
|
1389
1479
|
const FROM_AND_WORKDIR = `FROM oven/bun:1-slim
|
|
1390
1480
|
|
|
1391
1481
|
WORKDIR /agent
|
|
@@ -1429,10 +1519,10 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
|
1429
1519
|
&& printf '%s\\n' '{"executablePath":"/usr/bin/chromium"}' > /root/.agent-browser/config.json; \\
|
|
1430
1520
|
fi`
|
|
1431
1521
|
|
|
1432
|
-
const
|
|
1522
|
+
const renderAgentBrowserInstallLayer = (buildKit: boolean): string =>
|
|
1523
|
+
`# Layer 4 (volatile): install agent-browser globally so it survives the
|
|
1433
1524
|
# runtime bind-mount over /agent/node_modules.
|
|
1434
|
-
RUN
|
|
1435
|
-
bun install -g agent-browser`
|
|
1525
|
+
RUN ${bunCacheMount(buildKit)}bun install -g agent-browser@^0.27.0`
|
|
1436
1526
|
|
|
1437
1527
|
// Layer 4.5: shim the agent-browser binary with a wrapper that calls
|
|
1438
1528
|
// \`agent-browser close\` before \`open\`/\`goto\`/\`navigate\` when headed
|
|
@@ -1548,10 +1638,9 @@ TYPECLAW_AGENT_BROWSER_WRAPPER_EOF`
|
|
|
1548
1638
|
// already installed in Layer 2; --with-deps is a defense-in-depth backstop
|
|
1549
1639
|
// so a future agent-browser bump that adds new deps installs them
|
|
1550
1640
|
// automatically (near-no-op when Layer 2 already covers them).
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
if [ "$TARGETARCH" != "arm64" ]; then \\
|
|
1641
|
+
const renderChromeForTestingLayer = (buildKit: boolean): string =>
|
|
1642
|
+
`# Layer 5 (heavy, amd64 only): Chrome for Testing download.
|
|
1643
|
+
RUN ${aptCacheMount(buildKit)}if [ "$TARGETARCH" != "arm64" ]; then \\
|
|
1555
1644
|
agent-browser install --with-deps; \\
|
|
1556
1645
|
fi`
|
|
1557
1646
|
|
|
@@ -1590,16 +1679,14 @@ function singlePackageArgs(name: string, toggle: DockerfileFeatureToggle): strin
|
|
|
1590
1679
|
// (the most frequent change) does not re-fetch the GPG key over the network.
|
|
1591
1680
|
// When `gh` is disabled, omit the layer entirely — both to skip the network
|
|
1592
1681
|
// roundtrip on cold builds and to keep the package source registry clean.
|
|
1593
|
-
function renderGhKeyringLayer(toggle: DockerfileFeatureToggle): string {
|
|
1682
|
+
function renderGhKeyringLayer(toggle: DockerfileFeatureToggle, buildKit: boolean): string {
|
|
1594
1683
|
if (toggle === false) return ''
|
|
1595
1684
|
return `# Layer 1 (rarely changes): register the GitHub CLI apt repository and trust
|
|
1596
1685
|
# its keyring. Split from the package install below so editing the package
|
|
1597
1686
|
# list (the most frequent change to this Dockerfile) does NOT re-fetch the
|
|
1598
1687
|
# GPG key over the network. The cache mount on /var/cache/apt covers the
|
|
1599
1688
|
# tiny gnupg/curl install we need to bootstrap the key fetch.
|
|
1600
|
-
RUN
|
|
1601
|
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
1602
|
-
apt-get update \\
|
|
1689
|
+
RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
1603
1690
|
&& apt-get install -y --no-install-recommends curl ca-certificates gnupg \\
|
|
1604
1691
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\
|
|
1605
1692
|
| gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \\
|
package/src/init/gitignore.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const TRULY_IGNORED_PATTERNS = [
|
|
|
9
9
|
'.env.local',
|
|
10
10
|
'secrets.json',
|
|
11
11
|
'auth.json',
|
|
12
|
-
'.typeclaw/
|
|
12
|
+
'.typeclaw/',
|
|
13
13
|
'node_modules/',
|
|
14
14
|
'packages/*/node_modules/',
|
|
15
15
|
'workspace/',
|
|
@@ -40,12 +40,12 @@ export function buildGitignore(config: GitignoreConfig = { append: [] }): string
|
|
|
40
40
|
# as a safety net so an agent folder cloned from a pre-rename machine never
|
|
41
41
|
# stages credentials by accident.
|
|
42
42
|
#
|
|
43
|
-
# .typeclaw/
|
|
44
|
-
# entrypoint shim's
|
|
45
|
-
# src/init/dockerfile.ts
|
|
46
|
-
# $HOME
|
|
47
|
-
#
|
|
48
|
-
# commit.
|
|
43
|
+
# .typeclaw/ is the agent's local-scratch dir. It holds the persistent-$HOME
|
|
44
|
+
# overlay (.typeclaw/home/, populated by the entrypoint shim's
|
|
45
|
+
# \`link_persistent_home_files\` — see src/init/dockerfile.ts, mirrors container
|
|
46
|
+
# $HOME files like ~/.codex/auth.json so tool credentials survive restarts) and
|
|
47
|
+
# the in-progress init checkpoint (.typeclaw/init-progress.json, non-secret
|
|
48
|
+
# wizard selections for resume). Local-only; never commit.
|
|
49
49
|
${TRULY_IGNORED_PATTERNS.join('\n')}
|
|
50
50
|
|
|
51
51
|
# System-managed: gitignored by default so the agent never stages them by hand,
|
package/src/init/index.ts
CHANGED
|
@@ -10,13 +10,15 @@ import {
|
|
|
10
10
|
GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
11
11
|
migrateLegacyConfigShape,
|
|
12
12
|
type Config,
|
|
13
|
+
type CustomModelMeta,
|
|
13
14
|
} from '@/config'
|
|
14
15
|
import {
|
|
15
16
|
DEFAULT_MODEL_REF,
|
|
16
17
|
KNOWN_PROVIDERS,
|
|
18
|
+
isKnownModelRef,
|
|
17
19
|
providerForModelRef,
|
|
18
|
-
type KnownModelRef,
|
|
19
20
|
type KnownProviderId,
|
|
21
|
+
type ModelRef,
|
|
20
22
|
} from '@/config/providers'
|
|
21
23
|
import { checkDockerAvailable, type DockerAvailability, type DockerExec, start } from '@/container'
|
|
22
24
|
import { commitSystemFile } from '@/git/system-commit'
|
|
@@ -171,7 +173,8 @@ export type InitOptions = {
|
|
|
171
173
|
cwd: string
|
|
172
174
|
// Selected `provider/model` ref written into typeclaw.json. Defaults to
|
|
173
175
|
// DEFAULT_MODEL_REF when callers (or older test fixtures) omit it.
|
|
174
|
-
model?:
|
|
176
|
+
model?: ModelRef | string
|
|
177
|
+
modelMeta?: CustomModelMeta
|
|
175
178
|
// How the agent will authenticate to the LLM provider. When omitted,
|
|
176
179
|
// defaults to the api-key path with `apiKey` (legacy field, still
|
|
177
180
|
// supported for backwards compat with the old `runInit` signature).
|
|
@@ -181,7 +184,8 @@ export type InitOptions = {
|
|
|
181
184
|
// when both refer to the same provider; the wizard enforces this
|
|
182
185
|
// pairing rule, so by the time we get here `visionAuth` is either
|
|
183
186
|
// (a) absent, or (b) the right auth for `visionModel`'s provider.
|
|
184
|
-
visionModel?:
|
|
187
|
+
visionModel?: ModelRef | string
|
|
188
|
+
visionModelMeta?: CustomModelMeta
|
|
185
189
|
visionAuth?: LLMAuth
|
|
186
190
|
apiKey?: string
|
|
187
191
|
discordBotToken?: string
|
|
@@ -224,7 +228,9 @@ export async function runInit({
|
|
|
224
228
|
apiKey,
|
|
225
229
|
llmAuth,
|
|
226
230
|
model = DEFAULT_MODEL_REF,
|
|
231
|
+
modelMeta,
|
|
227
232
|
visionModel,
|
|
233
|
+
visionModelMeta,
|
|
228
234
|
visionAuth,
|
|
229
235
|
discordBotToken,
|
|
230
236
|
slackBotToken,
|
|
@@ -304,7 +310,9 @@ export async function runInit({
|
|
|
304
310
|
emit({ step: 'scaffold', phase: 'start' })
|
|
305
311
|
await scaffold(cwd, {
|
|
306
312
|
model,
|
|
313
|
+
...(modelMeta !== undefined ? { modelMeta } : {}),
|
|
307
314
|
...(visionModel !== undefined ? { visionModel } : {}),
|
|
315
|
+
...(visionModelMeta !== undefined ? { visionModelMeta } : {}),
|
|
308
316
|
withDiscord: wantsDiscord,
|
|
309
317
|
withSlack: wantsSlack,
|
|
310
318
|
withTelegram: wantsTelegram,
|
|
@@ -520,8 +528,10 @@ export async function isHatched(dir: string): Promise<boolean> {
|
|
|
520
528
|
}
|
|
521
529
|
|
|
522
530
|
export type ScaffoldOptions = {
|
|
523
|
-
model?:
|
|
524
|
-
|
|
531
|
+
model?: ModelRef | string
|
|
532
|
+
modelMeta?: CustomModelMeta
|
|
533
|
+
visionModel?: ModelRef | string
|
|
534
|
+
visionModelMeta?: CustomModelMeta
|
|
525
535
|
withDiscord?: boolean
|
|
526
536
|
withSlack?: boolean
|
|
527
537
|
withTelegram?: boolean
|
|
@@ -545,12 +555,14 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
545
555
|
// `memory.*`) is omitted to keep the scaffold minimal — duplicating defaults
|
|
546
556
|
// here would mean every schema change has to be mirrored in two places, and
|
|
547
557
|
// users would feel obligated to maintain values they never set.
|
|
548
|
-
const models: Record<string,
|
|
558
|
+
const models: Record<string, string> = { default: options.model ?? DEFAULT_MODEL_REF }
|
|
549
559
|
if (options.visionModel !== undefined) models.vision = options.visionModel
|
|
550
560
|
const config: Record<string, unknown> = {
|
|
551
561
|
$schema: './node_modules/typeclaw/typeclaw.schema.json',
|
|
552
562
|
models,
|
|
553
563
|
}
|
|
564
|
+
const customModels = collectCustomModels(options)
|
|
565
|
+
if (Object.keys(customModels).length > 0) config.customModels = customModels
|
|
554
566
|
const channels: Record<string, Record<string, never>> = {}
|
|
555
567
|
if (options.withDiscord) channels['discord-bot'] = {}
|
|
556
568
|
if (options.withSlack) channels['slack-bot'] = {}
|
|
@@ -578,12 +590,32 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
578
590
|
await writeFile(join(root, GITIGNORE_FILE), buildGitignore(), { flag: 'wx' }).catch(ignoreExists)
|
|
579
591
|
}
|
|
580
592
|
|
|
593
|
+
function collectCustomModels(options: ScaffoldOptions): Record<string, CustomModelMeta> {
|
|
594
|
+
const customModels: Record<string, CustomModelMeta> = {}
|
|
595
|
+
addCustomModel(customModels, options.model ?? DEFAULT_MODEL_REF, options.modelMeta)
|
|
596
|
+
if (options.visionModel !== undefined) addCustomModel(customModels, options.visionModel, options.visionModelMeta)
|
|
597
|
+
return customModels
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function addCustomModel(
|
|
601
|
+
customModels: Record<string, CustomModelMeta>,
|
|
602
|
+
ref: string,
|
|
603
|
+
meta: CustomModelMeta | undefined,
|
|
604
|
+
): void {
|
|
605
|
+
if (isKnownModelRef(ref)) return
|
|
606
|
+
customModels[ref] = meta ?? {}
|
|
607
|
+
}
|
|
608
|
+
|
|
581
609
|
// agent-browser ships in every agent: the bundled SKILL.md (src/skills/
|
|
582
610
|
// agent-browser/SKILL.md) is a discovery stub that calls `agent-browser
|
|
583
611
|
// skills get core` at runtime, so the CLI must be installed for the skill
|
|
584
612
|
// to function. The Dockerfile pre-downloads Chromium too, so the agent
|
|
585
613
|
// can drive a browser without any first-run setup.
|
|
586
|
-
|
|
614
|
+
//
|
|
615
|
+
// Must match the Dockerfile Layer 4 global install (dockerfile.ts); they are
|
|
616
|
+
// two installs of the same CLI and a skew is silent. Enforced by a guard test
|
|
617
|
+
// in packagejson.test.ts.
|
|
618
|
+
export const AGENT_BROWSER_VERSION = '^0.27.0'
|
|
587
619
|
function buildPackageJson(root: string, name: string): Record<string, unknown> {
|
|
588
620
|
return {
|
|
589
621
|
name,
|
|
@@ -738,10 +770,10 @@ export async function writeSecrets(
|
|
|
738
770
|
slackAppToken,
|
|
739
771
|
telegramBotToken,
|
|
740
772
|
}: {
|
|
741
|
-
model?:
|
|
773
|
+
model?: ModelRef | string
|
|
742
774
|
// Omitted on the OAuth path — credentials live in secrets.json via the OAuth runner.
|
|
743
775
|
apiKey?: string
|
|
744
|
-
visionModel?:
|
|
776
|
+
visionModel?: ModelRef | string
|
|
745
777
|
visionApiKey?: string
|
|
746
778
|
discordBotToken?: string
|
|
747
779
|
slackBotToken?: string
|