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,115 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
4
|
+
|
|
5
|
+
// Identifies the typeclaw source the daemon was loaded from. Computed by
|
|
6
|
+
// hashing the bytes of every .ts file under `src/` (excluding tests).
|
|
7
|
+
//
|
|
8
|
+
// Why: the host daemon (`typeclaw _hostd`) is the only long-lived host-stage
|
|
9
|
+
// process. Bun reads source files once at process start; subsequent edits on
|
|
10
|
+
// disk do not propagate. A short-lived CLI invocation (`typeclaw start`) sees
|
|
11
|
+
// the new code immediately, but if it reuses an existing daemon, the daemon
|
|
12
|
+
// keeps serving old in-memory daemon logic indefinitely. The
|
|
13
|
+
// observable effect is that bug fixes "don't apply" until the user manually
|
|
14
|
+
// kills `_hostd` — which is invisible footgun territory.
|
|
15
|
+
//
|
|
16
|
+
// This module produces a deterministic fingerprint of the source the running
|
|
17
|
+
// daemon represents, so the CLI can detect drift over the control socket and
|
|
18
|
+
// transparently respawn. Two implementation guarantees matter:
|
|
19
|
+
//
|
|
20
|
+
// 1. Determinism. The same source tree must always produce the same hash
|
|
21
|
+
// across processes/machines. We sort entries by path and hash bytes only,
|
|
22
|
+
// not metadata (mtime/size/inode), so a clean checkout of HEAD always
|
|
23
|
+
// matches.
|
|
24
|
+
// 2. Right scope. Hash only files the daemon actually loads. Test files and
|
|
25
|
+
// non-typescript assets are out — changes to them must not trigger a
|
|
26
|
+
// daemon respawn. The current scheme covers all of `src/**/*.ts` because
|
|
27
|
+
// the daemon's behavior depends on transitive imports (e.g. the supervisor
|
|
28
|
+
// callback in `src/cli/hostd.ts` reaches into `src/container/` and
|
|
29
|
+
// `src/config/`). Over-respawning on `src/agent/` changes is acceptable
|
|
30
|
+
// cost: a daemon spawn is < 100ms.
|
|
31
|
+
|
|
32
|
+
export type SourceVersion = string
|
|
33
|
+
|
|
34
|
+
export type ComputeOptions = {
|
|
35
|
+
// Absolute path to the project's `src/` directory. The hash covers every
|
|
36
|
+
// *.ts file rooted here, recursively.
|
|
37
|
+
srcRoot: string
|
|
38
|
+
// Test seam. Tests inject an in-memory file map to keep the unit tests
|
|
39
|
+
// hermetic; production reads the real filesystem.
|
|
40
|
+
fs?: VersionFs
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type VersionFs = {
|
|
44
|
+
readdir: (path: string) => Promise<Array<{ name: string; isDirectory: boolean }>>
|
|
45
|
+
readFile: (path: string) => Promise<Buffer>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const realFs: VersionFs = {
|
|
49
|
+
readdir: async (path) => {
|
|
50
|
+
const entries = await readdir(path, { withFileTypes: true })
|
|
51
|
+
return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }))
|
|
52
|
+
},
|
|
53
|
+
readFile: (path) => readFile(path),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function computeSourceVersion(opts: ComputeOptions): Promise<SourceVersion> {
|
|
57
|
+
const fs = opts.fs ?? realFs
|
|
58
|
+
const root = resolve(opts.srcRoot)
|
|
59
|
+
const files = await collectSourceFiles(root, root, fs)
|
|
60
|
+
files.sort()
|
|
61
|
+
|
|
62
|
+
const hash = createHash('sha256')
|
|
63
|
+
for (const rel of files) {
|
|
64
|
+
const abs = join(root, rel)
|
|
65
|
+
const bytes = await fs.readFile(abs)
|
|
66
|
+
// Path separator is normalized so a hash computed on macOS matches one
|
|
67
|
+
// computed on Linux for the same checkout. Tree fingerprints should not
|
|
68
|
+
// depend on the host OS's path conventions.
|
|
69
|
+
const normalizedRel = rel.split(sep).join('/')
|
|
70
|
+
hash.update(`${normalizedRel}\u0000`)
|
|
71
|
+
hash.update(bytes)
|
|
72
|
+
hash.update('\u0000')
|
|
73
|
+
}
|
|
74
|
+
return hash.digest('hex').slice(0, 32)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function collectSourceFiles(root: string, dir: string, fs: VersionFs): Promise<string[]> {
|
|
78
|
+
const out: string[] = []
|
|
79
|
+
const entries = await fs.readdir(dir)
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.isDirectory) {
|
|
82
|
+
const sub = await collectSourceFiles(root, join(dir, entry.name), fs)
|
|
83
|
+
out.push(...sub)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (!entry.name.endsWith('.ts')) continue
|
|
87
|
+
if (entry.name.endsWith('.test.ts')) continue
|
|
88
|
+
const absPath = join(dir, entry.name)
|
|
89
|
+
const relPath = absPath.slice(root.length + 1)
|
|
90
|
+
out.push(relPath)
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Resolves the project's `src/` directory from a CLI entry path
|
|
96
|
+
// (typically `process.argv[1]`, which points at `src/cli/index.ts` in dev
|
|
97
|
+
// stage or a bundled JS entry in published builds). Returns `null` if no
|
|
98
|
+
// `src/` ancestor is found, in which case versioning falls back to a
|
|
99
|
+
// constant — disabling drift detection rather than crashing.
|
|
100
|
+
export function resolveSrcRoot(cliEntry: string): string | null {
|
|
101
|
+
let current = resolve(cliEntry)
|
|
102
|
+
while (true) {
|
|
103
|
+
const parent = dirname(current)
|
|
104
|
+
if (parent === current) return null
|
|
105
|
+
if (parent.endsWith(`${sep}src`) || parent === 'src') return parent
|
|
106
|
+
current = parent
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Sentinel used when the source root cannot be resolved (e.g. published
|
|
111
|
+
// bundle that lives outside a `src/` tree). Both daemon and CLI compute the
|
|
112
|
+
// same fallback, so they will appear in-sync and skip the respawn path. This
|
|
113
|
+
// preserves correctness for non-dev installs at the cost of disabling drift
|
|
114
|
+
// detection there.
|
|
115
|
+
export const UNVERSIONED_SENTINEL: SourceVersion = 'unversioned'
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { DockerfileConfig, DockerfileFeatureToggle } from '@/config/config'
|
|
2
|
+
|
|
3
|
+
export const DOCKERFILE = 'Dockerfile'
|
|
4
|
+
|
|
5
|
+
// Apt packages that EVERY image must have — git for the agent runtime,
|
|
6
|
+
// curl/ca-certificates/gnupg for HTTPS and key fetches that downstream layers
|
|
7
|
+
// (e.g. the gh keyring) depend on. Listed first in the apt-get install line so
|
|
8
|
+
// the package set is self-documenting at a glance.
|
|
9
|
+
const BASELINE_APT_PACKAGES = ['git', 'ca-certificates', 'curl', 'gnupg'] as const
|
|
10
|
+
|
|
11
|
+
// curl-impersonate is the only currently-working way to query DuckDuckGo from
|
|
12
|
+
// a non-browser client on residential IPs in 2026. DDG fingerprints incoming
|
|
13
|
+
// requests at the TLS handshake (JA3/JA4) and HTTP/2 SETTINGS-frame layer
|
|
14
|
+
// before any HTTP headers are read; Bun's native fetch cannot match Chrome's
|
|
15
|
+
// fingerprint (upstream Bun issue #11368, open) so requests get gated behind
|
|
16
|
+
// 202 anomaly-modal responses, escalating to interactive duck-picker
|
|
17
|
+
// challenges. See `src/agent/tools/ddg.ts` for the runtime invocation.
|
|
18
|
+
//
|
|
19
|
+
// Pinned to lexiforest's actively-maintained fork (Chrome 136+ profiles in
|
|
20
|
+
// v1.5.6, May 2026), NOT the original `lwthiker/curl-impersonate` whose last
|
|
21
|
+
// release v0.6.1 (March 2024) carries Chrome ≤116 profiles — two years stale
|
|
22
|
+
// and useless against current DDG fingerprinting. Bumping: replace the
|
|
23
|
+
// version + sha256 constants below and run `typeclaw start --build` in any
|
|
24
|
+
// agent folder per the AGENTS.md "owns the Dockerfile" rule. Verify the new
|
|
25
|
+
// release ships the wrapper named in CURL_IMPERSONATE_PROFILE; lexiforest
|
|
26
|
+
// regenerates the bundled wrappers on Chrome major bumps and occasionally
|
|
27
|
+
// drops older ones.
|
|
28
|
+
export const CURL_IMPERSONATE_VERSION = 'v1.5.6'
|
|
29
|
+
export const CURL_IMPERSONATE_SHA256_AMD64 = 'b60344f63b9ed8806f0e9f7fd357d9f6c9a82aca279ed1e9e257d544885dcbde'
|
|
30
|
+
export const CURL_IMPERSONATE_SHA256_ARM64 = '6766bc67fd3e8e2313875f32b36b5a3fab02beffe77e5f1cf7fc5da99731d403'
|
|
31
|
+
// Wrapper symlink shipped in the v1.5.6 tarball. The tarball lays out
|
|
32
|
+
// curl_chrome136 → curl_chrome alongside the canonical `curl-impersonate`
|
|
33
|
+
// binary; we invoke the version-pinned wrapper so a future release that
|
|
34
|
+
// drops chrome136 fails loudly at search time instead of silently regressing
|
|
35
|
+
// the impersonation to whatever `curl_chrome` resolves to.
|
|
36
|
+
export const CURL_IMPERSONATE_PROFILE = 'chrome136'
|
|
37
|
+
|
|
38
|
+
// Shared-library runtime deps Chrome for Testing needs to launch on amd64
|
|
39
|
+
// Debian trixie (base of `oven/bun:1-slim`). `agent-browser install
|
|
40
|
+
// --with-deps` (v0.27.0) is supposed to install these but silently no-ops:
|
|
41
|
+
// its hardcoded list omits `libglib2.0-0t64`, so Chrome dies on launch
|
|
42
|
+
// with `libglib-2.0.so.0: cannot open shared object file` even though the
|
|
43
|
+
// binary download and `--with-deps` both exit 0. We install the full
|
|
44
|
+
// Playwright-tested chromium dep set here in Layer 2 so the bug doesn't
|
|
45
|
+
// recur if upstream omits another package; Layer 5 still calls
|
|
46
|
+
// `--with-deps` as a no-op-on-cache-hit backstop for future deps.
|
|
47
|
+
//
|
|
48
|
+
// Package list mirrors Playwright's `debian13-x64` chromium deps in
|
|
49
|
+
// nativeDeps.ts (https://github.com/microsoft/playwright). t64-suffixed
|
|
50
|
+
// names are the trixie-renamed variants from the 64-bit time_t ABI
|
|
51
|
+
// transition; SONAMEs (libglib-2.0.so.0 etc.) are unchanged. Packages
|
|
52
|
+
// without t64 here have no t64 sibling on trixie — verified against
|
|
53
|
+
// packages.debian.org/trixie. Fonts are intentionally omitted: the
|
|
54
|
+
// reported failure is launch-time linker errors, not rendering glyphs;
|
|
55
|
+
// font packages (esp. fonts-noto-cjk) cost ~50MB+ for no launch impact.
|
|
56
|
+
export const CHROME_RUNTIME_APT_PACKAGES_AMD64 = [
|
|
57
|
+
'libasound2t64',
|
|
58
|
+
'libatk-bridge2.0-0t64',
|
|
59
|
+
'libatk1.0-0t64',
|
|
60
|
+
'libatspi2.0-0t64',
|
|
61
|
+
'libcairo2',
|
|
62
|
+
'libcups2t64',
|
|
63
|
+
'libdbus-1-3',
|
|
64
|
+
'libdrm2',
|
|
65
|
+
'libgbm1',
|
|
66
|
+
'libglib2.0-0t64',
|
|
67
|
+
'libnspr4',
|
|
68
|
+
'libnss3',
|
|
69
|
+
'libpango-1.0-0',
|
|
70
|
+
'libx11-6',
|
|
71
|
+
'libxcb1',
|
|
72
|
+
'libxcomposite1',
|
|
73
|
+
'libxdamage1',
|
|
74
|
+
'libxext6',
|
|
75
|
+
'libxfixes3',
|
|
76
|
+
'libxkbcommon0',
|
|
77
|
+
'libxrandr2',
|
|
78
|
+
] as const
|
|
79
|
+
|
|
80
|
+
type AptFeature = {
|
|
81
|
+
toAptArgs: (toggle: DockerfileFeatureToggle) => string[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python', AptFeature> = {
|
|
85
|
+
ffmpeg: { toAptArgs: (v) => singlePackageArgs('ffmpeg', v) },
|
|
86
|
+
gh: { toAptArgs: (v) => singlePackageArgs('gh', v) },
|
|
87
|
+
tmux: { toAptArgs: (v) => singlePackageArgs('tmux', v) },
|
|
88
|
+
python: {
|
|
89
|
+
toAptArgs: (v) => (v === true ? ['python3', 'python3-pip', 'python3-venv', 'python-is-python3'] : []),
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildDockerfile(config: DockerfileConfig = defaultConfig()): string {
|
|
94
|
+
const aptArgs = collectAptArgs(config)
|
|
95
|
+
const ghKeyringLayer = renderGhKeyringLayer(config.gh)
|
|
96
|
+
const customLines = renderCustomDockerfileLines(config.append)
|
|
97
|
+
|
|
98
|
+
return `${BUILDKIT_HEADER}
|
|
99
|
+
# AUTOGENERATED by typeclaw — do not edit.
|
|
100
|
+
# This file is rewritten on every \`typeclaw start\` from src/init/dockerfile.ts
|
|
101
|
+
# in the typeclaw repo. Local edits will be overwritten (and committed away if
|
|
102
|
+
# the working tree is dirty). To change the template, edit dockerfile.ts there.
|
|
103
|
+
|
|
104
|
+
${FROM_AND_WORKDIR}
|
|
105
|
+
|
|
106
|
+
# Layers are ordered most-stable first to maximize Docker layer cache hits on
|
|
107
|
+
# rebuilds. Anything that pulls from npm (volatile) sits below anything that
|
|
108
|
+
# pulls from apt (stable, version-pinned by the base image's debian release),
|
|
109
|
+
# and the heavy Chrome-for-Testing download on amd64 is isolated in its own
|
|
110
|
+
# final layer so unrelated changes do not invalidate it.
|
|
111
|
+
|
|
112
|
+
${LAYER_0_APT_KEEP_CACHE}
|
|
113
|
+
|
|
114
|
+
${ghKeyringLayer}# Layer 2 (changes when the package list changes): the actual apt install.
|
|
115
|
+
# Cache mounts make a re-install nearly free when this layer is invalidated:
|
|
116
|
+
# .deb files come straight from the host's BuildKit cache instead of being
|
|
117
|
+
# refetched from Debian/GitHub mirrors. Package set is composed from the
|
|
118
|
+
# \`dockerfile\` config block in typeclaw.json — toggles for tmux/python/gh/
|
|
119
|
+
# ffmpeg fan out into the args below. Baseline (git/ca-certificates/curl/
|
|
120
|
+
# gnupg) is always installed because downstream layers depend on it.
|
|
121
|
+
#
|
|
122
|
+
# No \`rm -rf /var/lib/apt/lists/*\` because the lists live on a cache mount
|
|
123
|
+
# that is excluded from the image layer by definition.
|
|
124
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
125
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
126
|
+
apt-get update \\
|
|
127
|
+
&& apt-get install -y --no-install-recommends \\
|
|
128
|
+
${aptArgs.join(' ')} \\
|
|
129
|
+
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
130
|
+
apt-get install -y --no-install-recommends chromium; \\
|
|
131
|
+
else \\
|
|
132
|
+
apt-get install -y --no-install-recommends \\
|
|
133
|
+
${CHROME_RUNTIME_APT_PACKAGES_AMD64.join(' ')}; \\
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
${LAYER_2_5_CURL_IMPERSONATE}
|
|
137
|
+
|
|
138
|
+
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
139
|
+
|
|
140
|
+
${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
141
|
+
|
|
142
|
+
${LAYER_5_CHROME_FOR_TESTING}
|
|
143
|
+
|
|
144
|
+
# The agent folder (including node_modules) is bind-mounted at runtime by
|
|
145
|
+
# \`typeclaw start\`, so we do not COPY or install here. This keeps the image
|
|
146
|
+
# tiny and lets edits on the host take effect without rebuilds.
|
|
147
|
+
|
|
148
|
+
ENV NODE_ENV=production
|
|
149
|
+
|
|
150
|
+
# Pin agent-messenger's config dir into the agent's workspace/ so KakaoTalk
|
|
151
|
+
# (and any future agent-messenger-backed channel) reads/writes credentials
|
|
152
|
+
# inside the bind-mounted agent folder. Without this, the SDK would default
|
|
153
|
+
# to /root/.config/agent-messenger inside the container, which doesn't
|
|
154
|
+
# survive container restarts and isn't visible from the host. The agent
|
|
155
|
+
# folder's bind-mount maps /agent → host's agent dir, so the credentials
|
|
156
|
+
# end up at <agentDir>/workspace/.agent-messenger/ on the host.
|
|
157
|
+
ENV AGENT_MESSENGER_CONFIG_DIR=/agent/workspace/.agent-messenger
|
|
158
|
+
|
|
159
|
+
${customLines}ENTRYPOINT ["bun", "run", "typeclaw"]
|
|
160
|
+
CMD ["run"]
|
|
161
|
+
`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Recipe for the prebuilt typeclaw-base image published to
|
|
165
|
+
// ghcr.io/typeclaw/typeclaw-base by .github/workflows/base-image.yml. Built
|
|
166
|
+
// from the same constants and layer templates as buildDockerfile() so the
|
|
167
|
+
// two cannot drift — the published image is a function of this source, not
|
|
168
|
+
// a checked-in Dockerfile that needs hand-syncing. The base intentionally
|
|
169
|
+
// stops before the per-agent layers (gh keyring, apt feature toggles,
|
|
170
|
+
// dockerfile.append, ENV, ENTRYPOINT) so users can still toggle them via
|
|
171
|
+
// typeclaw.json without forcing a base-image rebuild.
|
|
172
|
+
//
|
|
173
|
+
// Layer 2's apt-get install line installs only the baseline packages, NOT
|
|
174
|
+
// the gh/python/tmux/ffmpeg toggles — those layer onto the base in the
|
|
175
|
+
// per-agent Dockerfile.
|
|
176
|
+
export function buildBaseDockerfile(): string {
|
|
177
|
+
return `${BUILDKIT_HEADER}
|
|
178
|
+
# AUTOGENERATED by scripts/emit-base-dockerfile.ts from src/init/dockerfile.ts.
|
|
179
|
+
# Do not edit by hand — your changes will be lost on the next CI run.
|
|
180
|
+
|
|
181
|
+
${FROM_AND_WORKDIR}
|
|
182
|
+
|
|
183
|
+
${LAYER_0_APT_KEEP_CACHE}
|
|
184
|
+
|
|
185
|
+
# Layer 2 (baseline only): apt baseline + Chrome runtime libs. Toggle-driven
|
|
186
|
+
# packages (gh/python/tmux/ffmpeg) are intentionally NOT installed here —
|
|
187
|
+
# they layer onto the base in the per-agent Dockerfile so users can opt in/
|
|
188
|
+
# out via typeclaw.json without forcing a base-image rebuild.
|
|
189
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
190
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
191
|
+
apt-get update \\
|
|
192
|
+
&& apt-get install -y --no-install-recommends \\
|
|
193
|
+
${BASELINE_APT_PACKAGES.join(' ')} \\
|
|
194
|
+
&& if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
195
|
+
apt-get install -y --no-install-recommends chromium; \\
|
|
196
|
+
else \\
|
|
197
|
+
apt-get install -y --no-install-recommends \\
|
|
198
|
+
${CHROME_RUNTIME_APT_PACKAGES_AMD64.join(' ')}; \\
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
${LAYER_2_5_CURL_IMPERSONATE}
|
|
202
|
+
|
|
203
|
+
${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
204
|
+
|
|
205
|
+
${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
206
|
+
|
|
207
|
+
${LAYER_5_CHROME_FOR_TESTING}
|
|
208
|
+
`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Shared layer templates. Both buildDockerfile() (per-agent) and
|
|
212
|
+
// buildBaseDockerfile() (prebuilt base image) compose from these so the
|
|
213
|
+
// two outputs cannot drift on Chrome runtime libs, curl-impersonate
|
|
214
|
+
// version, or the agent-browser install path.
|
|
215
|
+
|
|
216
|
+
const BUILDKIT_HEADER = `# syntax=docker/dockerfile:1.7`
|
|
217
|
+
|
|
218
|
+
const FROM_AND_WORKDIR = `FROM oven/bun:1-slim
|
|
219
|
+
|
|
220
|
+
WORKDIR /agent
|
|
221
|
+
|
|
222
|
+
ARG TARGETARCH`
|
|
223
|
+
|
|
224
|
+
// Layer 0: defeat Debian's apt auto-clean so \`--mount=type=cache\` below
|
|
225
|
+
// actually retains downloaded .debs across builds. The default
|
|
226
|
+
// /etc/apt/apt.conf.d/docker-clean (inherited from debian:slim) deletes
|
|
227
|
+
// /var/cache/apt/archives at the end of every apt invocation, which would
|
|
228
|
+
// nullify our cache mount. Also pre-create the keyring dir so the gh repo
|
|
229
|
+
// layer in the per-agent Dockerfile is one cheap cp/echo with no mkdir.
|
|
230
|
+
const LAYER_0_APT_KEEP_CACHE = `# Layer 0 (rarely changes): defeat Debian's apt auto-clean so cache mounts
|
|
231
|
+
# below actually retain downloaded .debs across builds.
|
|
232
|
+
RUN rm -f /etc/apt/apt.conf.d/docker-clean \\
|
|
233
|
+
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \\
|
|
234
|
+
&& mkdir -p -m 755 /etc/apt/keyrings`
|
|
235
|
+
|
|
236
|
+
// Layer 2.5: install pinned curl-impersonate (lexiforest fork) for the
|
|
237
|
+
// websearch tool's DDG scraper. Required to evade DDG's TLS/HTTP2
|
|
238
|
+
// fingerprinting on residential IPs — see src/agent/tools/ddg.ts for the
|
|
239
|
+
// full rationale. Placed after Layer 2 so curl + ca-certificates + tar
|
|
240
|
+
// (already in baseline) are present, and before agent-browser so a version
|
|
241
|
+
// bump there doesn't invalidate this layer.
|
|
242
|
+
const LAYER_2_5_CURL_IMPERSONATE = `# Layer 2.5 (stable): pinned curl-impersonate (lexiforest fork) for DDG.
|
|
243
|
+
RUN ARCH_TARBALL="$(if [ "$TARGETARCH" = "arm64" ]; then echo aarch64-linux-gnu; else echo x86_64-linux-gnu; fi)" \\
|
|
244
|
+
&& ARCH_SHA="$(if [ "$TARGETARCH" = "arm64" ]; then echo ${CURL_IMPERSONATE_SHA256_ARM64}; else echo ${CURL_IMPERSONATE_SHA256_AMD64}; fi)" \\
|
|
245
|
+
&& cd /tmp \\
|
|
246
|
+
&& curl -fsSL -o curl-impersonate.tar.gz \\
|
|
247
|
+
"https://github.com/lexiforest/curl-impersonate/releases/download/${CURL_IMPERSONATE_VERSION}/curl-impersonate-${CURL_IMPERSONATE_VERSION}.\${ARCH_TARBALL}.tar.gz" \\
|
|
248
|
+
&& echo "\${ARCH_SHA} curl-impersonate.tar.gz" | sha256sum -c - \\
|
|
249
|
+
&& tar -xzf curl-impersonate.tar.gz -C /usr/local/bin/ \\
|
|
250
|
+
&& rm curl-impersonate.tar.gz \\
|
|
251
|
+
&& /usr/local/bin/curl_${CURL_IMPERSONATE_PROFILE} --version > /dev/null`
|
|
252
|
+
|
|
253
|
+
const LAYER_3_AGENT_BROWSER_ARM64_CONFIG = `# Layer 3 (stable, arm64 only): point agent-browser at the apt-installed
|
|
254
|
+
# chromium. Independent of the npm install below so it stays cached across
|
|
255
|
+
# agent-browser version bumps.
|
|
256
|
+
RUN if [ "$TARGETARCH" = "arm64" ]; then \\
|
|
257
|
+
mkdir -p /root/.agent-browser \\
|
|
258
|
+
&& printf '%s\\n' '{"executablePath":"/usr/bin/chromium"}' > /root/.agent-browser/config.json; \\
|
|
259
|
+
fi`
|
|
260
|
+
|
|
261
|
+
const LAYER_4_AGENT_BROWSER_INSTALL = `# Layer 4 (volatile): install agent-browser globally so it survives the
|
|
262
|
+
# runtime bind-mount over /agent/node_modules.
|
|
263
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\
|
|
264
|
+
bun install -g agent-browser`
|
|
265
|
+
|
|
266
|
+
// Layer 5: download the pinned Chrome for Testing build into
|
|
267
|
+
// ~/.agent-browser/browsers/. NO cache mount on that path because the
|
|
268
|
+
// runtime needs the binary in the image. System shared libraries are
|
|
269
|
+
// already installed in Layer 2; --with-deps is a defense-in-depth backstop
|
|
270
|
+
// so a future agent-browser bump that adds new deps installs them
|
|
271
|
+
// automatically (near-no-op when Layer 2 already covers them).
|
|
272
|
+
const LAYER_5_CHROME_FOR_TESTING = `# Layer 5 (heavy, amd64 only): Chrome for Testing download.
|
|
273
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
274
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
275
|
+
if [ "$TARGETARCH" != "arm64" ]; then \\
|
|
276
|
+
agent-browser install --with-deps; \\
|
|
277
|
+
fi`
|
|
278
|
+
|
|
279
|
+
function defaultConfig(): DockerfileConfig {
|
|
280
|
+
return { ffmpeg: false, gh: true, python: true, tmux: true, append: [] }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function collectAptArgs(config: DockerfileConfig): string[] {
|
|
284
|
+
const args: string[] = [...BASELINE_APT_PACKAGES]
|
|
285
|
+
for (const key of ['ffmpeg', 'gh', 'python', 'tmux'] as const) {
|
|
286
|
+
args.push(...APT_FEATURES[key].toAptArgs(config[key]))
|
|
287
|
+
}
|
|
288
|
+
return args
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function singlePackageArgs(name: string, toggle: DockerfileFeatureToggle): string[] {
|
|
292
|
+
if (toggle === false) return []
|
|
293
|
+
if (toggle === true) return [name]
|
|
294
|
+
return [`${name}=${toggle}`]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The gh keyring bootstrap is a separate layer so editing the package list
|
|
298
|
+
// (the most frequent change) does not re-fetch the GPG key over the network.
|
|
299
|
+
// When `gh` is disabled, omit the layer entirely — both to skip the network
|
|
300
|
+
// roundtrip on cold builds and to keep the package source registry clean.
|
|
301
|
+
function renderGhKeyringLayer(toggle: DockerfileFeatureToggle): string {
|
|
302
|
+
if (toggle === false) return ''
|
|
303
|
+
return `# Layer 1 (rarely changes): register the GitHub CLI apt repository and trust
|
|
304
|
+
# its keyring. Split from the package install below so editing the package
|
|
305
|
+
# list (the most frequent change to this Dockerfile) does NOT re-fetch the
|
|
306
|
+
# GPG key over the network. The cache mount on /var/cache/apt covers the
|
|
307
|
+
# tiny gnupg/curl install we need to bootstrap the key fetch.
|
|
308
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
309
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
310
|
+
apt-get update \\
|
|
311
|
+
&& apt-get install -y --no-install-recommends curl ca-certificates gnupg \\
|
|
312
|
+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \\
|
|
313
|
+
| gpg --dearmor -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \\
|
|
314
|
+
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \\
|
|
315
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \\
|
|
316
|
+
> /etc/apt/sources.list.d/github-cli.list
|
|
317
|
+
|
|
318
|
+
`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function renderCustomDockerfileLines(lines: string[]): string {
|
|
322
|
+
if (lines.length === 0) return ''
|
|
323
|
+
return `# Custom lines from typeclaw.json#dockerfile.append.
|
|
324
|
+
${lines.join('\n')}
|
|
325
|
+
|
|
326
|
+
`
|
|
327
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, parse as parsePath } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { runBunInstall } from './run-bun-install'
|
|
6
|
+
|
|
7
|
+
const PACKAGE_FILE = 'package.json'
|
|
8
|
+
const NODE_MODULES = 'node_modules'
|
|
9
|
+
|
|
10
|
+
export type EnsureDepsResult =
|
|
11
|
+
| { ok: true; installed: boolean }
|
|
12
|
+
| { ok: false; reason: string; missing?: readonly string[] }
|
|
13
|
+
|
|
14
|
+
export type EnsureDepsOptions = {
|
|
15
|
+
cwd: string
|
|
16
|
+
install?: (cwd: string) => Promise<{ ok: true } | { ok: false; reason: string }>
|
|
17
|
+
detect?: (cwd: string) => Promise<readonly string[]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function ensureDepsInstalled(options: EnsureDepsOptions): Promise<EnsureDepsResult> {
|
|
21
|
+
const { cwd } = options
|
|
22
|
+
const install = options.install ?? runBunInstall
|
|
23
|
+
const detect = options.detect ?? detectMissingDeps
|
|
24
|
+
|
|
25
|
+
const missing = await detect(cwd)
|
|
26
|
+
if (missing.length === 0) return { ok: true, installed: false }
|
|
27
|
+
|
|
28
|
+
const result = await install(cwd)
|
|
29
|
+
if (!result.ok) return { ok: false, reason: result.reason, missing }
|
|
30
|
+
|
|
31
|
+
// Re-probe: `bun install` returns 0 even when a file:-linked dep's own
|
|
32
|
+
// package.json is unreachable (it silently no-ops on the target). Without
|
|
33
|
+
// this check, we'd proceed to `docker run` with a known-broken
|
|
34
|
+
// node_modules/ and the agent would crash with a confusing in-container
|
|
35
|
+
// `Cannot find package 'x'`.
|
|
36
|
+
const stillMissing = await detect(cwd)
|
|
37
|
+
if (stillMissing.length > 0) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: `bun install completed but these deps are still missing from ${cwd}/node_modules/: ${stillMissing.join(', ')}`,
|
|
41
|
+
missing: stillMissing,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, installed: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Walks the agent's package.json plus one level of transitive deps. The
|
|
48
|
+
// canonical failure we guard against: typeclaw is installed, but its own
|
|
49
|
+
// deps (e.g. agent-messenger, zod) aren't reachable from the agent folder
|
|
50
|
+
// after a CLI upgrade added them. Direct deps alone wouldn't catch that —
|
|
51
|
+
// typeclaw IS present, but its dependencies field has grown. Deeper drift
|
|
52
|
+
// (dep-of-dep-of-dep missing) is rare and surfaces during `bun install`.
|
|
53
|
+
//
|
|
54
|
+
// Root deps are checked against <cwd>/node_modules/<dep> ONLY (no walk-up):
|
|
55
|
+
// the agent folder is what gets bind-mounted into the container, so a dep
|
|
56
|
+
// satisfied by some ancestor host folder's node_modules would be invisible
|
|
57
|
+
// inside the container. Walking the host filesystem upward here would
|
|
58
|
+
// silently pass the gate and let `docker run` crash later with "Cannot find
|
|
59
|
+
// package", which is the exact failure mode this whole module was added to
|
|
60
|
+
// prevent.
|
|
61
|
+
//
|
|
62
|
+
// Transitive deps are different: they MUST be resolved via Node's algorithm
|
|
63
|
+
// from the parent package's realpath, not lexical-joined against <cwd>.
|
|
64
|
+
// Bun's isolated linker (used for new workspace projects with
|
|
65
|
+
// `configVersion = 1`, which is TypeClaw's scaffold) symlinks
|
|
66
|
+
// node_modules/<dep> into node_modules/.bun/<dep>@<ver>/node_modules/<dep>/
|
|
67
|
+
// and stores that package's own deps as siblings inside the same nested
|
|
68
|
+
// node_modules. A lexical probe at <cwd>/node_modules/<transitive> finds
|
|
69
|
+
// nothing even though the dep is correctly installed and reachable from
|
|
70
|
+
// the parent — that was the original false-positive that aborted
|
|
71
|
+
// `typeclaw start`.
|
|
72
|
+
export async function detectMissingDeps(cwd: string): Promise<readonly string[]> {
|
|
73
|
+
const rootDeps = await readDeclaredDeps(join(cwd, PACKAGE_FILE))
|
|
74
|
+
if (rootDeps.length === 0) return []
|
|
75
|
+
|
|
76
|
+
const missing = new Set<string>()
|
|
77
|
+
const installedRootDirs = new Map<string, string>()
|
|
78
|
+
for (const dep of rootDeps) {
|
|
79
|
+
const dir = resolveDirectDep(cwd, dep)
|
|
80
|
+
if (dir === null) {
|
|
81
|
+
missing.add(dep)
|
|
82
|
+
} else {
|
|
83
|
+
installedRootDirs.set(dep, dir)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [dep, parentDir] of installedRootDirs) {
|
|
88
|
+
if (missing.has(dep)) continue
|
|
89
|
+
const transitive = await readDeclaredDeps(join(parentDir, PACKAGE_FILE))
|
|
90
|
+
for (const t of transitive) {
|
|
91
|
+
if (!resolveTransitiveDep(parentDir, t)) {
|
|
92
|
+
missing.add(t)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [...missing].sort()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readDeclaredDeps(packageJsonPath: string): Promise<readonly string[]> {
|
|
101
|
+
if (!existsSync(packageJsonPath)) return []
|
|
102
|
+
let raw: string
|
|
103
|
+
try {
|
|
104
|
+
raw = await readFile(packageJsonPath, 'utf8')
|
|
105
|
+
} catch {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
let parsed: unknown
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(raw)
|
|
111
|
+
} catch {
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
if (parsed === null || typeof parsed !== 'object') return []
|
|
115
|
+
const deps = (parsed as { dependencies?: unknown }).dependencies
|
|
116
|
+
if (deps === null || typeof deps !== 'object') return []
|
|
117
|
+
return Object.keys(deps as Record<string, unknown>)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveDirectDep(cwd: string, depName: string): string | null {
|
|
121
|
+
const candidate = join(cwd, NODE_MODULES, depName)
|
|
122
|
+
if (!existsSync(join(candidate, PACKAGE_FILE))) return null
|
|
123
|
+
try {
|
|
124
|
+
return realpathSync(candidate)
|
|
125
|
+
} catch {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveTransitiveDep(fromDir: string, depName: string): string | null {
|
|
131
|
+
let dir: string
|
|
132
|
+
try {
|
|
133
|
+
dir = realpathSync(fromDir)
|
|
134
|
+
} catch {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
const fsRoot = parsePath(dir).root
|
|
138
|
+
while (true) {
|
|
139
|
+
const candidate = join(dir, NODE_MODULES, depName)
|
|
140
|
+
if (existsSync(join(candidate, PACKAGE_FILE))) {
|
|
141
|
+
try {
|
|
142
|
+
return realpathSync(candidate)
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (dir === fsRoot) return null
|
|
148
|
+
const parent = dirname(dir)
|
|
149
|
+
if (parent === dir) return null
|
|
150
|
+
dir = parent
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GitignoreConfig } from '@/config/config'
|
|
2
|
+
|
|
3
|
+
export const GITIGNORE_FILE = '.gitignore'
|
|
4
|
+
|
|
5
|
+
export function buildGitignore(config: GitignoreConfig = { append: [] }): string {
|
|
6
|
+
const customEntries = renderCustomGitignoreEntries(config.append)
|
|
7
|
+
|
|
8
|
+
return `${customEntries}# Truly ignored: secrets, runtime junk, the agent's free-write zone, and
|
|
9
|
+
# regenerated-on-every-start system files. Never enter git history.
|
|
10
|
+
#
|
|
11
|
+
# Dockerfile is rewritten from the typeclaw CLI template on every \`typeclaw
|
|
12
|
+
# start\` (see src/init/dockerfile.ts), so tracking it would only produce
|
|
13
|
+
# noisy "Update Dockerfile" commits whenever the template changes. Treat it
|
|
14
|
+
# like node_modules/ — reproducible from source, not part of agent state.
|
|
15
|
+
#
|
|
16
|
+
# auth.json is the pre-rename name for secrets.json; kept here permanently
|
|
17
|
+
# as a safety net so an agent folder cloned from a pre-rename machine never
|
|
18
|
+
# stages credentials by accident, even if its agent boot hasn't yet run the
|
|
19
|
+
# auth.json -> secrets.json migration.
|
|
20
|
+
.env
|
|
21
|
+
.env.local
|
|
22
|
+
secrets.json
|
|
23
|
+
auth.json
|
|
24
|
+
node_modules/
|
|
25
|
+
packages/*/node_modules/
|
|
26
|
+
workspace/
|
|
27
|
+
mounts/
|
|
28
|
+
Dockerfile
|
|
29
|
+
.DS_Store
|
|
30
|
+
|
|
31
|
+
# System-managed: gitignored by default so the agent never stages them by hand,
|
|
32
|
+
# but TypeClaw force-commits them on its own schedule (sessions/ via auto-backup,
|
|
33
|
+
# memory/ via the dreaming subagent). Treat them as runtime-owned, not agent-owned.
|
|
34
|
+
sessions/
|
|
35
|
+
memory/
|
|
36
|
+
channels/
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderCustomGitignoreEntries(entries: string[]): string {
|
|
41
|
+
if (entries.length === 0) return ''
|
|
42
|
+
return `# Custom entries from typeclaw.json#gitignore.append.
|
|
43
|
+
${entries.join('\n')}
|
|
44
|
+
|
|
45
|
+
`
|
|
46
|
+
}
|