typeclaw 0.1.2 → 0.1.3
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 +4 -0
- package/auth.schema.json +238 -7
- package/package.json +1 -1
- package/secrets.schema.json +238 -7
- package/src/agent/auth.ts +19 -38
- package/src/agent/tools/channel-fetch-attachment.ts +6 -0
- package/src/agent/tools/channel-history.ts +10 -1
- package/src/agent/tools/channel-log.ts +32 -0
- package/src/agent/tools/channel-reply.ts +18 -1
- package/src/agent/tools/channel-send.ts +13 -1
- package/src/bundled-plugins/tool-result-cap/README.md +67 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +56 -0
- package/src/bundled-plugins/tool-result-cap/index.ts +51 -0
- package/src/channels/adapters/kakaotalk.ts +25 -16
- package/src/channels/manager.ts +47 -38
- package/src/cli/channel.ts +3 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/init.ts +2 -1
- package/src/cli/ui.ts +11 -0
- package/src/config/config.ts +15 -4
- package/src/container/start.ts +90 -1
- package/src/hostd/daemon.ts +28 -3
- package/src/hostd/protocol.ts +7 -0
- package/src/init/auto-upgrade.ts +368 -0
- package/src/init/dockerfile.ts +25 -14
- package/src/init/index.ts +123 -77
- package/src/init/kakaotalk-auth.ts +9 -3
- package/src/init/run-bun-install.ts +34 -0
- package/src/run/bundled-plugins.ts +7 -0
- package/src/run/index.ts +9 -0
- package/src/secrets/defaults.ts +67 -0
- package/src/secrets/hydrate.ts +99 -0
- package/src/secrets/index.ts +6 -12
- package/src/secrets/kakao-store.ts +129 -0
- package/src/secrets/migrate-kakaotalk.ts +82 -0
- package/src/secrets/migrate.ts +5 -4
- package/src/secrets/resolve.ts +57 -0
- package/src/secrets/schema.ts +162 -42
- package/src/secrets/storage.ts +253 -47
- package/src/skills/typeclaw-config/SKILL.md +47 -8
- package/typeclaw.schema.json +36 -2
- package/src/secrets/env.ts +0 -43
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { resolveScaffoldVersion } from './cli-version'
|
|
6
|
+
|
|
7
|
+
const PACKAGE_FILE = 'package.json'
|
|
8
|
+
const TYPECLAW = 'typeclaw'
|
|
9
|
+
|
|
10
|
+
// Two semver quirks drive every branch in this module:
|
|
11
|
+
//
|
|
12
|
+
// 1. Pre-1.0 caret: `^0.1.0` resolves to `>=0.1.0 <0.2.0`. A CLI bump from
|
|
13
|
+
// 0.1.x to 0.2.0 falls OUT of the agent's range, so we must rewrite the
|
|
14
|
+
// spec for those crossings.
|
|
15
|
+
//
|
|
16
|
+
// 2. `bun install` honors the lockfile: when the lockfile entry already
|
|
17
|
+
// satisfies the declared spec, `bun install` is a no-op even if a newer
|
|
18
|
+
// in-range version exists upstream. To actually upgrade an in-range dep
|
|
19
|
+
// we MUST use `bun update <pkg> --latest`. See src/init/run-bun-install.ts.
|
|
20
|
+
//
|
|
21
|
+
// The decision matrix anchors on the INSTALLED version (the truth), not the
|
|
22
|
+
// declared range floor (a promise the agent may not yet have kept).
|
|
23
|
+
|
|
24
|
+
export type AutoUpgradeOutcome =
|
|
25
|
+
| { kind: 'skipped-dev-mode' }
|
|
26
|
+
| { kind: 'skipped-no-dep' }
|
|
27
|
+
| { kind: 'skipped-non-release-spec'; declared: string }
|
|
28
|
+
| { kind: 'skipped-already-running' }
|
|
29
|
+
| { kind: 'up-to-date'; installedVersion: string }
|
|
30
|
+
| { kind: 'exact-pin-respected'; declared: string; cliVersion: string }
|
|
31
|
+
| { kind: 'spec-rewritten'; from: string; to: string; cliVersion: string }
|
|
32
|
+
| { kind: 'reinstall-needed'; from: string; to: string }
|
|
33
|
+
|
|
34
|
+
export type AutoUpgradeOptions = {
|
|
35
|
+
cwd: string
|
|
36
|
+
// Test seam: lets tests simulate dev-mode (null) and arbitrary release
|
|
37
|
+
// versions without depending on the test runner's actual CLI version.
|
|
38
|
+
scaffoldVersion?: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function autoUpgradeTypeclawDep(options: AutoUpgradeOptions): Promise<AutoUpgradeOutcome> {
|
|
42
|
+
const { cwd } = options
|
|
43
|
+
const scaffold = options.scaffoldVersion !== undefined ? options.scaffoldVersion : resolveScaffoldVersion()
|
|
44
|
+
if (scaffold === null) return { kind: 'skipped-dev-mode' }
|
|
45
|
+
|
|
46
|
+
const cliVersion = stripCaret(scaffold)
|
|
47
|
+
if (cliVersion === null) return { kind: 'skipped-dev-mode' }
|
|
48
|
+
|
|
49
|
+
const pkg = await readAgentPackageJson(cwd)
|
|
50
|
+
if (pkg === null) return { kind: 'skipped-no-dep' }
|
|
51
|
+
|
|
52
|
+
const declared = pkg.parsed.dependencies?.[TYPECLAW]
|
|
53
|
+
if (typeof declared !== 'string') return { kind: 'skipped-no-dep' }
|
|
54
|
+
|
|
55
|
+
const declaredKind = classifyDepSpec(declared)
|
|
56
|
+
if (declaredKind.kind === 'non-release') {
|
|
57
|
+
return { kind: 'skipped-non-release-spec', declared }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const installed = readInstalledTypeclawVersion(cwd)
|
|
61
|
+
|
|
62
|
+
// "Upgrade only, never downgrade" — anchored on the INSTALLED version
|
|
63
|
+
// (the truth), with the declared range floor used ONLY when nothing is
|
|
64
|
+
// installed yet (best proxy for "what would land if bun install ran").
|
|
65
|
+
//
|
|
66
|
+
// Anchoring on `installed` first closes the half-applied-rewrite hole:
|
|
67
|
+
// a previous start may have written ^0.2.0 to package.json but failed
|
|
68
|
+
// its install, leaving node_modules at 0.1.x. The declared floor would
|
|
69
|
+
// wrongly say "up-to-date"; the installed version correctly says "retry."
|
|
70
|
+
if (installed !== null && compareReleaseVersions(installed, cliVersion) >= 0) {
|
|
71
|
+
return { kind: 'up-to-date', installedVersion: installed }
|
|
72
|
+
}
|
|
73
|
+
if (installed === null && declaredKind.kind !== 'exact') {
|
|
74
|
+
const declaredFloor = formatTriple(declaredKind.version)
|
|
75
|
+
if (compareReleaseVersions(declaredFloor, cliVersion) >= 0) {
|
|
76
|
+
return { kind: 'up-to-date', installedVersion: declaredFloor }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (declaredKind.kind === 'exact') {
|
|
81
|
+
const declaredVersion = formatTriple(declaredKind.version)
|
|
82
|
+
// Exact pin matches CLI but installed is stale (or missing): we still
|
|
83
|
+
// need to install. The user wrote the right spec — they just haven't
|
|
84
|
+
// materialized it yet. Return reinstall-needed; caller will run
|
|
85
|
+
// `bun update typeclaw --latest` against that exact spec.
|
|
86
|
+
if (declaredVersion === cliVersion) {
|
|
87
|
+
return { kind: 'reinstall-needed', from: installed ?? '<missing>', to: cliVersion }
|
|
88
|
+
}
|
|
89
|
+
// Exact pin diverges from CLI. User intent wins; we warn but never
|
|
90
|
+
// rewrite. If installed is ALSO ahead of CLI (e.g. exact pin 0.1.5,
|
|
91
|
+
// CLI 0.1.2), the up-to-date check above already returned.
|
|
92
|
+
return { kind: 'exact-pin-respected', declared, cliVersion }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!rangeSatisfies(declaredKind, cliVersion)) {
|
|
96
|
+
const newSpec = `^${cliVersion}`
|
|
97
|
+
await writeDepSpec(cwd, pkg.raw, pkg.parsed, newSpec)
|
|
98
|
+
return { kind: 'spec-rewritten', from: declared, to: newSpec, cliVersion }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Declared range includes CLI. Three sub-cases:
|
|
102
|
+
// - installed === null: fresh agent, nothing on disk yet. ensureDeps
|
|
103
|
+
// will install for the missing-dep reason; nothing for us to add.
|
|
104
|
+
// - installed > CLI but in range: we already returned up-to-date above.
|
|
105
|
+
// - installed < CLI: force an upgrade via `bun update typeclaw --latest`.
|
|
106
|
+
if (installed === null) {
|
|
107
|
+
return { kind: 'up-to-date', installedVersion: cliVersion }
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'reinstall-needed', from: installed, to: cliVersion }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function outcomeForcesInstall(outcome: AutoUpgradeOutcome): boolean {
|
|
113
|
+
return outcome.kind === 'spec-rewritten' || outcome.kind === 'reinstall-needed'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The version we expect to find in node_modules/typeclaw after the
|
|
117
|
+
// auto-upgrade-triggered install completes. Callers use this to verify
|
|
118
|
+
// the install actually moved the on-disk version (not just resolved the
|
|
119
|
+
// lockfile). Returns null when no install was forced — verification is
|
|
120
|
+
// skipped on no-op outcomes.
|
|
121
|
+
export function expectedInstalledAfterUpgrade(outcome: AutoUpgradeOutcome): string | null {
|
|
122
|
+
if (outcome.kind === 'spec-rewritten') return outcome.cliVersion
|
|
123
|
+
if (outcome.kind === 'reinstall-needed') return outcome.to
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function describeAutoUpgrade(outcome: AutoUpgradeOutcome): string {
|
|
128
|
+
switch (outcome.kind) {
|
|
129
|
+
case 'spec-rewritten':
|
|
130
|
+
return `Upgrading agent typeclaw ${outcome.from} → ${outcome.to} to match CLI`
|
|
131
|
+
case 'reinstall-needed':
|
|
132
|
+
return `Upgrading agent typeclaw ${outcome.from} → ${outcome.to} to match CLI`
|
|
133
|
+
case 'exact-pin-respected':
|
|
134
|
+
return `Agent typeclaw is exact-pinned to ${outcome.declared}; CLI is ${outcome.cliVersion}. Not upgrading (remove the exact pin to allow auto-upgrade).`
|
|
135
|
+
default:
|
|
136
|
+
return ''
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function readInstalledTypeclawVersionFromAgent(cwd: string): string | null {
|
|
141
|
+
return readInstalledTypeclawVersion(cwd)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type ParsedPackage = {
|
|
145
|
+
raw: string
|
|
146
|
+
parsed: { dependencies?: Record<string, string> } & Record<string, unknown>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function readAgentPackageJson(cwd: string): Promise<ParsedPackage | null> {
|
|
150
|
+
const path = join(cwd, PACKAGE_FILE)
|
|
151
|
+
if (!existsSync(path)) return null
|
|
152
|
+
let raw: string
|
|
153
|
+
try {
|
|
154
|
+
raw = await readFile(path, 'utf8')
|
|
155
|
+
} catch {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
let parsed: unknown
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw)
|
|
161
|
+
} catch {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
165
|
+
return { raw, parsed: parsed as ParsedPackage['parsed'] }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function readInstalledTypeclawVersion(cwd: string): string | null {
|
|
169
|
+
const path = join(cwd, 'node_modules', TYPECLAW, PACKAGE_FILE)
|
|
170
|
+
if (!existsSync(path)) return null
|
|
171
|
+
let raw: string
|
|
172
|
+
try {
|
|
173
|
+
raw = readFileSync(path, 'utf8')
|
|
174
|
+
} catch {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(raw) as { version?: string }
|
|
179
|
+
if (typeof parsed.version === 'string' && isReleaseVersion(parsed.version)) return parsed.version
|
|
180
|
+
} catch {}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type DepSpecKind =
|
|
185
|
+
| { kind: 'exact'; version: [number, number, number]; raw: string }
|
|
186
|
+
| { kind: 'caret'; version: [number, number, number] }
|
|
187
|
+
| { kind: 'tilde'; version: [number, number, number] }
|
|
188
|
+
| { kind: 'non-release' }
|
|
189
|
+
|
|
190
|
+
function classifyDepSpec(spec: string): DepSpecKind {
|
|
191
|
+
const trimmed = spec.trim()
|
|
192
|
+
const exactMatch = trimmed.match(/^=?(\d+)\.(\d+)\.(\d+)$/)
|
|
193
|
+
if (exactMatch) {
|
|
194
|
+
const [, a, b, c] = exactMatch
|
|
195
|
+
return { kind: 'exact', version: parseTriple(a!, b!, c!), raw: trimmed }
|
|
196
|
+
}
|
|
197
|
+
const caretMatch = trimmed.match(/^\^(\d+)\.(\d+)\.(\d+)$/)
|
|
198
|
+
if (caretMatch) {
|
|
199
|
+
const [, a, b, c] = caretMatch
|
|
200
|
+
return { kind: 'caret', version: parseTriple(a!, b!, c!) }
|
|
201
|
+
}
|
|
202
|
+
const tildeMatch = trimmed.match(/^~(\d+)\.(\d+)\.(\d+)$/)
|
|
203
|
+
if (tildeMatch) {
|
|
204
|
+
const [, a, b, c] = tildeMatch
|
|
205
|
+
return { kind: 'tilde', version: parseTriple(a!, b!, c!) }
|
|
206
|
+
}
|
|
207
|
+
return { kind: 'non-release' }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseTriple(a: string, b: string, c: string): [number, number, number] {
|
|
211
|
+
return [Number.parseInt(a, 10), Number.parseInt(b, 10), Number.parseInt(c, 10)]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatTriple(v: [number, number, number]): string {
|
|
215
|
+
return `${v[0]}.${v[1]}.${v[2]}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// npm/bun caret+tilde semantics, narrowed to plain X.Y.Z bases:
|
|
219
|
+
// ^0.1.2 → >=0.1.2 <0.2.0 (pre-1.0 caret pins minor — the bug we fix)
|
|
220
|
+
// ^1.2.3 → >=1.2.3 <2.0.0
|
|
221
|
+
// ~0.1.2 → >=0.1.2 <0.2.0
|
|
222
|
+
// ~1.2.3 → >=1.2.3 <1.3.0
|
|
223
|
+
function rangeSatisfies(range: Exclude<DepSpecKind, { kind: 'exact' | 'non-release' }>, version: string): boolean {
|
|
224
|
+
const v = parseVersion(version)
|
|
225
|
+
if (v === null) return false
|
|
226
|
+
const [base, ceiling] = rangeBounds(range)
|
|
227
|
+
return compareTriples(v, base) >= 0 && compareTriples(v, ceiling) < 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rangeBounds(
|
|
231
|
+
range: Exclude<DepSpecKind, { kind: 'exact' | 'non-release' }>,
|
|
232
|
+
): [[number, number, number], [number, number, number]] {
|
|
233
|
+
const [maj, min, pat] = range.version
|
|
234
|
+
if (range.kind === 'caret') {
|
|
235
|
+
if (maj > 0)
|
|
236
|
+
return [
|
|
237
|
+
[maj, min, pat],
|
|
238
|
+
[maj + 1, 0, 0],
|
|
239
|
+
]
|
|
240
|
+
if (min > 0)
|
|
241
|
+
return [
|
|
242
|
+
[maj, min, pat],
|
|
243
|
+
[maj, min + 1, 0],
|
|
244
|
+
]
|
|
245
|
+
return [
|
|
246
|
+
[maj, min, pat],
|
|
247
|
+
[maj, min, pat + 1],
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
return [
|
|
251
|
+
[maj, min, pat],
|
|
252
|
+
[maj, min + 1, 0],
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseVersion(version: string): [number, number, number] | null {
|
|
257
|
+
const m = version.match(/^(\d+)\.(\d+)\.(\d+)$/)
|
|
258
|
+
if (!m) return null
|
|
259
|
+
const [, a, b, c] = m
|
|
260
|
+
return parseTriple(a!, b!, c!)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function compareTriples(a: [number, number, number], b: [number, number, number]): number {
|
|
264
|
+
for (let i = 0; i < 3; i++) {
|
|
265
|
+
const ai = a[i]!
|
|
266
|
+
const bi = b[i]!
|
|
267
|
+
if (ai !== bi) return ai - bi
|
|
268
|
+
}
|
|
269
|
+
return 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function compareReleaseVersions(a: string, b: string): number {
|
|
273
|
+
const av = parseVersion(a)
|
|
274
|
+
const bv = parseVersion(b)
|
|
275
|
+
if (av === null || bv === null) return 0
|
|
276
|
+
return compareTriples(av, bv)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function stripCaret(scaffold: string): string | null {
|
|
280
|
+
const m = scaffold.match(/^\^?(\d+\.\d+\.\d+)$/)
|
|
281
|
+
return m ? (m[1] ?? null) : null
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isReleaseVersion(version: string): boolean {
|
|
285
|
+
return /^\d+\.\d+\.\d+$/.test(version)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function writeDepSpec(cwd: string, raw: string, parsed: ParsedPackage['parsed'], newSpec: string): Promise<void> {
|
|
289
|
+
// Scoped edit: replace the typeclaw spec ONLY inside the dependencies
|
|
290
|
+
// object. The previous implementation used `raw.replace(/"typeclaw":.../)`
|
|
291
|
+
// unscoped, which would silently rewrite devDependencies.typeclaw if it
|
|
292
|
+
// appeared before dependencies.typeclaw in the file (the original spec
|
|
293
|
+
// never moves). We slice the dependencies object's textual range, edit
|
|
294
|
+
// inside it, then splice back to preserve whitespace, key order, and
|
|
295
|
+
// trailing newline. If the slice fails (unusual JSON shape), fall back
|
|
296
|
+
// to a full JSON round-trip — formatting churn is acceptable; silently
|
|
297
|
+
// updating the wrong key is not.
|
|
298
|
+
const scoped = sliceDependenciesRange(raw, parsed)
|
|
299
|
+
if (scoped !== null) {
|
|
300
|
+
const { start, end } = scoped
|
|
301
|
+
const block = raw.slice(start, end)
|
|
302
|
+
const replaced = block.replace(
|
|
303
|
+
/("typeclaw"\s*:\s*)"[^"]+"/,
|
|
304
|
+
(_m, prefix: string) => `${prefix}${JSON.stringify(newSpec)}`,
|
|
305
|
+
)
|
|
306
|
+
if (replaced !== block) {
|
|
307
|
+
await writeFile(join(cwd, PACKAGE_FILE), `${raw.slice(0, start)}${replaced}${raw.slice(end)}`)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const deps = { ...parsed.dependencies, [TYPECLAW]: newSpec }
|
|
312
|
+
const next = { ...parsed, dependencies: deps }
|
|
313
|
+
const indent = detectIndent(raw)
|
|
314
|
+
await writeFile(join(cwd, PACKAGE_FILE), `${JSON.stringify(next, null, indent)}\n`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Returns the [start, end) byte range of the "dependencies" object's value
|
|
318
|
+
// in `raw`, or null if it can't be located unambiguously. Uses a brace-
|
|
319
|
+
// counting tokenizer that respects string literals so a `dependencies` key
|
|
320
|
+
// inside a string value (e.g. inside a `description`) cannot fool it.
|
|
321
|
+
function sliceDependenciesRange(raw: string, parsed: ParsedPackage['parsed']): { start: number; end: number } | null {
|
|
322
|
+
if (parsed.dependencies === undefined || parsed.dependencies === null) return null
|
|
323
|
+
const keyMatch = raw.match(/"dependencies"\s*:\s*\{/)
|
|
324
|
+
if (!keyMatch || keyMatch.index === undefined) return null
|
|
325
|
+
const startOfOpenBrace = keyMatch.index + keyMatch[0].length - 1
|
|
326
|
+
const closeBrace = findMatchingCloseBrace(raw, startOfOpenBrace)
|
|
327
|
+
if (closeBrace === null) return null
|
|
328
|
+
return { start: startOfOpenBrace, end: closeBrace + 1 }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findMatchingCloseBrace(raw: string, openIndex: number): number | null {
|
|
332
|
+
let depth = 0
|
|
333
|
+
let inString = false
|
|
334
|
+
let escape = false
|
|
335
|
+
for (let i = openIndex; i < raw.length; i++) {
|
|
336
|
+
const ch = raw[i]
|
|
337
|
+
if (escape) {
|
|
338
|
+
escape = false
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
if (inString) {
|
|
342
|
+
if (ch === '\\') escape = true
|
|
343
|
+
else if (ch === '"') inString = false
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
if (ch === '"') {
|
|
347
|
+
inString = true
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
if (ch === '{') depth++
|
|
351
|
+
else if (ch === '}') {
|
|
352
|
+
depth--
|
|
353
|
+
if (depth === 0) return i
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function detectIndent(raw: string): number | string {
|
|
360
|
+
// Default to 2 — matches `JSON.stringify(_, _, 2)` behavior and the
|
|
361
|
+
// project's existing scaffold style. Only override when we can see a
|
|
362
|
+
// clear non-2 indent on the first indented line.
|
|
363
|
+
const match = raw.match(/\n([\t ]+)\S/)
|
|
364
|
+
if (!match) return 2
|
|
365
|
+
const sample = match[1]!
|
|
366
|
+
if (sample.startsWith('\t')) return '\t'
|
|
367
|
+
return sample.length
|
|
368
|
+
}
|
package/src/init/dockerfile.ts
CHANGED
|
@@ -89,11 +89,13 @@ export const NETWORK_BLOCK_IPV6_NETS = ['fc00::/7', 'fe80::/10', 'ff00::/8', '::
|
|
|
89
89
|
// Renders the shell script that runs as PID 1 inside the container. Two
|
|
90
90
|
// modes, picked at boot time from `$TYPECLAW_NETWORK_BLOCK_INTERNAL`:
|
|
91
91
|
//
|
|
92
|
-
// off (
|
|
93
|
-
//
|
|
94
|
-
//
|
|
92
|
+
// off (env unset or != "1"): no rules installed, no setpriv. Just exec
|
|
93
|
+
// `bun run typeclaw "$@"`. Identical observable behavior to a container
|
|
94
|
+
// without this feature. This is the opt-out path for users who set
|
|
95
|
+
// `network.blockInternal: false` in their `typeclaw.json`.
|
|
95
96
|
//
|
|
96
|
-
// on (blockInternal
|
|
97
|
+
// on (default, env = "1" via `network.blockInternal: true`): walks IPv4 +
|
|
98
|
+
// IPv6 block lists and installs
|
|
97
99
|
// REJECT rules in the OUTPUT chain. Loopback (`-o lo`) is ACCEPT'd first
|
|
98
100
|
// so dev-server dogfooding still works. The hostd HTTP control port on
|
|
99
101
|
// `host.docker.internal` is re-allowed at runtime — narrowly, single
|
|
@@ -264,13 +266,10 @@ ${fromAndHeavyLayers}
|
|
|
264
266
|
|
|
265
267
|
ENV NODE_ENV=production
|
|
266
268
|
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
# survive container restarts and isn't visible from the host. The agent
|
|
272
|
-
# folder's bind-mount maps /agent → host's agent dir, so the credentials
|
|
273
|
-
# end up at <agentDir>/workspace/.agent-messenger/ on the host.
|
|
269
|
+
# Keep agent-messenger's fallback config dir inside workspace/ for any future
|
|
270
|
+
# SDK fallback paths. TypeClaw's KakaoTalk adapter does not write there:
|
|
271
|
+
# credentials live in secrets.json#channels.kakaotalk and container writes go
|
|
272
|
+
# through hostd's secrets-patch RPC.
|
|
274
273
|
ENV AGENT_MESSENGER_CONFIG_DIR=/agent/workspace/.agent-messenger
|
|
275
274
|
|
|
276
275
|
${customLines}ENTRYPOINT ["${TYPECLAW_ENTRYPOINT_PATH}"]
|
|
@@ -282,8 +281,18 @@ CMD ["run"]
|
|
|
282
281
|
// layers (apt baseline, Chrome runtime libs, curl-impersonate, agent-browser,
|
|
283
282
|
// Chrome for Testing) are already in that image, so the per-agent head only
|
|
284
283
|
// re-runs the toggle apt install and (optionally) the gh keyring bootstrap.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
284
|
+
//
|
|
285
|
+
// The entrypoint shim is ALSO re-emitted here, even though the base image
|
|
286
|
+
// already carries it. Two reasons: (1) older base images published before
|
|
287
|
+
// the shim landed (or before a shim source edit) don't have the up-to-date
|
|
288
|
+
// binary at TYPECLAW_ENTRYPOINT_PATH, and the per-agent ENTRYPOINT line
|
|
289
|
+
// would crash on startup with `stat: no such file or directory`. Re-emitting
|
|
290
|
+
// is ~1KB of image and keeps the contract local: whatever per-agent
|
|
291
|
+
// Dockerfile we emit guarantees the shim path exists, regardless of which
|
|
292
|
+
// base-image vintage we FROM. (2) Edits to `buildEntrypointShim()` ship via
|
|
293
|
+
// npm + `typeclaw start --build` immediately, instead of being blocked on a
|
|
294
|
+
// fresh base-image release. The base image's copy is harmlessly overwritten
|
|
295
|
+
// by this RUN — same path, same chmod.
|
|
287
296
|
function renderVersionedHead(baseImageVersion: string, ghKeyringLayer: string, toggleAptArgs: string[]): string {
|
|
288
297
|
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
|
|
289
298
|
return `FROM ${GHCR_BASE_IMAGE_REPO}:${baseImageVersion}
|
|
@@ -292,7 +301,9 @@ WORKDIR /agent
|
|
|
292
301
|
|
|
293
302
|
ARG TARGETARCH
|
|
294
303
|
|
|
295
|
-
${ghKeyringLayer}${toggleAptLayer}
|
|
304
|
+
${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
|
|
305
|
+
|
|
306
|
+
`
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
// FROMs oven/bun:1-slim and rebuilds the full heavy stack inline. Used by
|