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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/auth.schema.json +63 -0
  4. package/cron.schema.json +96 -0
  5. package/package.json +72 -0
  6. package/scripts/emit-base-dockerfile.ts +5 -0
  7. package/scripts/generate-schema.ts +34 -0
  8. package/secrets.schema.json +63 -0
  9. package/src/agent/auth.ts +119 -0
  10. package/src/agent/compaction.ts +35 -0
  11. package/src/agent/git-nudge.ts +95 -0
  12. package/src/agent/index.ts +451 -0
  13. package/src/agent/plugin-tools.ts +269 -0
  14. package/src/agent/reload-tool.ts +71 -0
  15. package/src/agent/self.ts +45 -0
  16. package/src/agent/session-origin.ts +288 -0
  17. package/src/agent/subagents.ts +253 -0
  18. package/src/agent/system-prompt.ts +68 -0
  19. package/src/agent/tools/channel-fetch-attachment.ts +118 -0
  20. package/src/agent/tools/channel-history.ts +119 -0
  21. package/src/agent/tools/channel-reply.ts +182 -0
  22. package/src/agent/tools/channel-send.ts +212 -0
  23. package/src/agent/tools/ddg.ts +218 -0
  24. package/src/agent/tools/restart.ts +122 -0
  25. package/src/agent/tools/stream-snapshot.ts +181 -0
  26. package/src/agent/tools/webfetch/fetch.ts +102 -0
  27. package/src/agent/tools/webfetch/index.ts +1 -0
  28. package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
  29. package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
  30. package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
  31. package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
  32. package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
  33. package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
  34. package/src/agent/tools/webfetch/tool.ts +281 -0
  35. package/src/agent/tools/webfetch/types.ts +33 -0
  36. package/src/agent/tools/websearch.ts +96 -0
  37. package/src/agent/tools/wikipedia.ts +52 -0
  38. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
  39. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
  40. package/src/bundled-plugins/agent-browser/index.ts +179 -0
  41. package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
  42. package/src/bundled-plugins/agent-browser/shim.ts +152 -0
  43. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
  44. package/src/bundled-plugins/guard/index.ts +26 -0
  45. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
  46. package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
  47. package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
  48. package/src/bundled-plugins/guard/policy.ts +18 -0
  49. package/src/bundled-plugins/memory/README.md +71 -0
  50. package/src/bundled-plugins/memory/append-tool.ts +84 -0
  51. package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
  52. package/src/bundled-plugins/memory/dreaming.ts +470 -0
  53. package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
  54. package/src/bundled-plugins/memory/index.ts +238 -0
  55. package/src/bundled-plugins/memory/load-memory.ts +122 -0
  56. package/src/bundled-plugins/memory/memory-logger.ts +257 -0
  57. package/src/bundled-plugins/memory/secret-detector.ts +49 -0
  58. package/src/bundled-plugins/memory/watermark.ts +15 -0
  59. package/src/bundled-plugins/security/index.ts +35 -0
  60. package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
  61. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
  62. package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
  63. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
  64. package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
  65. package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
  66. package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
  67. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
  68. package/src/bundled-plugins/security/policy.ts +9 -0
  69. package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
  70. package/src/channels/adapters/discord-bot-classify.ts +148 -0
  71. package/src/channels/adapters/discord-bot.ts +640 -0
  72. package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
  73. package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
  74. package/src/channels/adapters/kakaotalk-classify.ts +77 -0
  75. package/src/channels/adapters/kakaotalk.ts +622 -0
  76. package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
  77. package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
  78. package/src/channels/adapters/slack-bot-classify.ts +213 -0
  79. package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
  80. package/src/channels/adapters/slack-bot-time.ts +10 -0
  81. package/src/channels/adapters/slack-bot.ts +881 -0
  82. package/src/channels/adapters/telegram-bot-classify.ts +155 -0
  83. package/src/channels/adapters/telegram-bot-format.ts +309 -0
  84. package/src/channels/adapters/telegram-bot.ts +604 -0
  85. package/src/channels/engagement.ts +227 -0
  86. package/src/channels/index.ts +21 -0
  87. package/src/channels/manager.ts +292 -0
  88. package/src/channels/membership-cache.ts +116 -0
  89. package/src/channels/membership-from-history.ts +53 -0
  90. package/src/channels/membership.ts +30 -0
  91. package/src/channels/participants.ts +47 -0
  92. package/src/channels/persistence.ts +209 -0
  93. package/src/channels/reloadable.ts +28 -0
  94. package/src/channels/router.ts +1570 -0
  95. package/src/channels/schema.ts +273 -0
  96. package/src/channels/types.ts +160 -0
  97. package/src/cli/channel.ts +403 -0
  98. package/src/cli/compose-status.ts +95 -0
  99. package/src/cli/compose.ts +240 -0
  100. package/src/cli/hostd.ts +163 -0
  101. package/src/cli/index.ts +27 -0
  102. package/src/cli/init.ts +592 -0
  103. package/src/cli/logs.ts +38 -0
  104. package/src/cli/reload.ts +68 -0
  105. package/src/cli/restart.ts +66 -0
  106. package/src/cli/run.ts +77 -0
  107. package/src/cli/shell.ts +33 -0
  108. package/src/cli/start.ts +57 -0
  109. package/src/cli/status.ts +178 -0
  110. package/src/cli/stop.ts +31 -0
  111. package/src/cli/tui.ts +35 -0
  112. package/src/cli/ui.ts +110 -0
  113. package/src/commands/index.ts +74 -0
  114. package/src/compose/discover.ts +43 -0
  115. package/src/compose/index.ts +25 -0
  116. package/src/compose/logs.ts +162 -0
  117. package/src/compose/restart.ts +69 -0
  118. package/src/compose/start.ts +62 -0
  119. package/src/compose/status.ts +28 -0
  120. package/src/compose/stop.ts +43 -0
  121. package/src/config/config.ts +424 -0
  122. package/src/config/index.ts +25 -0
  123. package/src/config/providers.ts +234 -0
  124. package/src/config/reloadable.ts +47 -0
  125. package/src/container/index.ts +27 -0
  126. package/src/container/logs.ts +37 -0
  127. package/src/container/port.ts +137 -0
  128. package/src/container/shared.ts +290 -0
  129. package/src/container/shell.ts +58 -0
  130. package/src/container/start.ts +670 -0
  131. package/src/container/status.ts +76 -0
  132. package/src/container/stop.ts +120 -0
  133. package/src/container/verify-running.ts +149 -0
  134. package/src/cron/consumer.ts +138 -0
  135. package/src/cron/index.ts +54 -0
  136. package/src/cron/reloadable.ts +64 -0
  137. package/src/cron/scheduler.ts +200 -0
  138. package/src/cron/schema.ts +96 -0
  139. package/src/hostd/client.ts +113 -0
  140. package/src/hostd/daemon.ts +587 -0
  141. package/src/hostd/index.ts +25 -0
  142. package/src/hostd/paths.ts +82 -0
  143. package/src/hostd/portbroker-manager.ts +101 -0
  144. package/src/hostd/protocol.ts +48 -0
  145. package/src/hostd/spawn.ts +224 -0
  146. package/src/hostd/supervisor.ts +60 -0
  147. package/src/hostd/tailscale.ts +172 -0
  148. package/src/hostd/version.ts +115 -0
  149. package/src/init/dockerfile.ts +327 -0
  150. package/src/init/ensure-deps.ts +152 -0
  151. package/src/init/gitignore.ts +46 -0
  152. package/src/init/hatching.ts +60 -0
  153. package/src/init/index.ts +786 -0
  154. package/src/init/kakaotalk-auth.ts +114 -0
  155. package/src/init/models-dev.ts +130 -0
  156. package/src/init/oauth-login.ts +74 -0
  157. package/src/init/packagejson.ts +94 -0
  158. package/src/init/paths.ts +2 -0
  159. package/src/init/run-bun-install.ts +20 -0
  160. package/src/markdown/chunk.ts +299 -0
  161. package/src/markdown/index.ts +1 -0
  162. package/src/plugin/context.ts +40 -0
  163. package/src/plugin/define.ts +35 -0
  164. package/src/plugin/hooks.ts +204 -0
  165. package/src/plugin/index.ts +63 -0
  166. package/src/plugin/loader.ts +111 -0
  167. package/src/plugin/manager.ts +136 -0
  168. package/src/plugin/registry.ts +145 -0
  169. package/src/plugin/skills.ts +62 -0
  170. package/src/plugin/types.ts +172 -0
  171. package/src/portbroker/bind-with-forward.ts +102 -0
  172. package/src/portbroker/container-server.ts +305 -0
  173. package/src/portbroker/forward-result-bus.ts +36 -0
  174. package/src/portbroker/hostd-client.ts +443 -0
  175. package/src/portbroker/index.ts +33 -0
  176. package/src/portbroker/policy.ts +24 -0
  177. package/src/portbroker/proc-net-tcp.ts +72 -0
  178. package/src/portbroker/protocol.ts +39 -0
  179. package/src/reload/client.ts +59 -0
  180. package/src/reload/index.ts +3 -0
  181. package/src/reload/registry.ts +60 -0
  182. package/src/reload/types.ts +13 -0
  183. package/src/run/bundled-plugins.ts +24 -0
  184. package/src/run/channel-session-factory.ts +105 -0
  185. package/src/run/index.ts +432 -0
  186. package/src/run/plugin-runtime.ts +43 -0
  187. package/src/run/schema-with-plugins.ts +14 -0
  188. package/src/secrets/index.ts +13 -0
  189. package/src/secrets/migrate.ts +95 -0
  190. package/src/secrets/schema.ts +75 -0
  191. package/src/secrets/storage.ts +231 -0
  192. package/src/server/index.ts +436 -0
  193. package/src/sessions/index.ts +23 -0
  194. package/src/shared/index.ts +9 -0
  195. package/src/shared/local-time.ts +21 -0
  196. package/src/shared/protocol.ts +25 -0
  197. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
  198. package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
  199. package/src/skills/typeclaw-config/SKILL.md +643 -0
  200. package/src/skills/typeclaw-cron/SKILL.md +159 -0
  201. package/src/skills/typeclaw-git/SKILL.md +89 -0
  202. package/src/skills/typeclaw-memory/SKILL.md +174 -0
  203. package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
  204. package/src/skills/typeclaw-plugins/SKILL.md +594 -0
  205. package/src/skills/typeclaw-skills/SKILL.md +246 -0
  206. package/src/stream/broker.ts +161 -0
  207. package/src/stream/index.ts +16 -0
  208. package/src/stream/types.ts +69 -0
  209. package/src/tui/client.ts +45 -0
  210. package/src/tui/format.ts +317 -0
  211. package/src/tui/index.ts +225 -0
  212. package/src/tui/theme.ts +41 -0
  213. package/typeclaw.schema.json +826 -0
@@ -0,0 +1,240 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { composeLogs, composeRestart, composeStart, composeStatus, composeStop, type AgentResult } from '@/compose'
4
+ import { config } from '@/config'
5
+
6
+ import { formatComposeStatus } from './compose-status'
7
+ import { c, spinner } from './ui'
8
+
9
+ const startSub = defineCommand({
10
+ meta: { name: 'start', description: 'start every agent in immediate subdirectories of cwd' },
11
+ args: {
12
+ port: {
13
+ type: 'string',
14
+ description: 'preferred host port for each agent; collisions fall back to ephemeral per-agent',
15
+ default: String(config.port),
16
+ },
17
+ build: {
18
+ type: 'boolean',
19
+ description: 'regenerate each Dockerfile from the template and rebuild',
20
+ default: false,
21
+ },
22
+ },
23
+ async run({ args }) {
24
+ const board = makeBoard('Starting agents')
25
+ const s = spinner()
26
+ const { agents, results } = await composeStart({
27
+ rootCwd: process.cwd(),
28
+ preferredHostPort: Number(args.port),
29
+ forceBuild: args.build,
30
+ cliEntry: process.argv[1],
31
+ onProgress: (event) => {
32
+ if (event.kind === 'agent-start') {
33
+ board.add(s, event.name, 'starting')
34
+ } else {
35
+ board.set(s, event.name, formatStartDone(event.result))
36
+ }
37
+ },
38
+ })
39
+ if (agents.length === 0) {
40
+ console.log(c.dim('No typeclaw agents found in immediate subdirectories of cwd.'))
41
+ return
42
+ }
43
+ const failed = results.reduce((n, r) => (r.ok ? n : n + 1), 0)
44
+ board.finish(s, results, 'started', failed)
45
+ if (failed > 0) process.exit(1)
46
+ },
47
+ })
48
+
49
+ const stopSub = defineCommand({
50
+ meta: { name: 'stop', description: 'stop every agent in immediate subdirectories of cwd' },
51
+ async run() {
52
+ const board = makeBoard('Stopping agents')
53
+ const s = spinner()
54
+ const { agents, results } = await composeStop({
55
+ rootCwd: process.cwd(),
56
+ onProgress: (event) => {
57
+ if (event.kind === 'agent-start') {
58
+ board.add(s, event.name, 'stopping')
59
+ } else {
60
+ board.set(s, event.name, formatStopDone(event.result))
61
+ }
62
+ },
63
+ })
64
+ if (agents.length === 0) {
65
+ console.log(c.dim('No typeclaw agents found in immediate subdirectories of cwd.'))
66
+ return
67
+ }
68
+ const failed = results.reduce((n, r) => (r.ok ? n : n + 1), 0)
69
+ board.finish(s, results, 'stopped', failed)
70
+ if (failed > 0) process.exit(1)
71
+ },
72
+ })
73
+
74
+ const restartSub = defineCommand({
75
+ meta: { name: 'restart', description: 'stop and relaunch every agent in immediate subdirectories of cwd' },
76
+ args: {
77
+ port: {
78
+ type: 'string',
79
+ description: 'preferred host port for each agent; collisions fall back to ephemeral per-agent',
80
+ default: String(config.port),
81
+ },
82
+ build: {
83
+ type: 'boolean',
84
+ description: 'regenerate each Dockerfile from the template and rebuild',
85
+ default: false,
86
+ },
87
+ },
88
+ async run({ args }) {
89
+ const board = makeBoard('Restarting agents')
90
+ const s = spinner()
91
+ const { agents, results } = await composeRestart({
92
+ rootCwd: process.cwd(),
93
+ preferredHostPort: Number(args.port),
94
+ forceBuild: args.build,
95
+ cliEntry: process.argv[1],
96
+ onProgress: (event) => {
97
+ if (event.kind === 'agent-start') {
98
+ board.add(s, event.name, 'stopping')
99
+ } else if (event.kind === 'agent-stopped') {
100
+ board.set(s, event.name, c.dim('starting...'))
101
+ } else {
102
+ board.set(s, event.name, formatRestartDone(event.result))
103
+ }
104
+ },
105
+ })
106
+ if (agents.length === 0) {
107
+ console.log(c.dim('No typeclaw agents found in immediate subdirectories of cwd.'))
108
+ return
109
+ }
110
+ const failed = results.reduce((n, r) => (r.ok ? n : n + 1), 0)
111
+ board.finish(s, results, 'restarted', failed)
112
+ if (failed > 0) process.exit(1)
113
+ },
114
+ })
115
+
116
+ const statusSub = defineCommand({
117
+ meta: { name: 'status', description: 'show status of every agent in immediate subdirectories of cwd' },
118
+ async run() {
119
+ const result = await composeStatus(process.cwd())
120
+ const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined
121
+ process.stdout.write(`${formatComposeStatus(result, { useColor })}\n`)
122
+ },
123
+ })
124
+
125
+ const logsSub = defineCommand({
126
+ meta: { name: 'logs', description: 'multiplex docker logs for every running agent in immediate subdirectories' },
127
+ args: {
128
+ follow: {
129
+ type: 'boolean',
130
+ alias: 'f',
131
+ description: 'stream new log output as it arrives',
132
+ default: false,
133
+ },
134
+ },
135
+ async run({ args }) {
136
+ const controller = new AbortController()
137
+ const onSig = (): void => controller.abort()
138
+ process.once('SIGINT', onSig)
139
+ process.once('SIGTERM', onSig)
140
+ try {
141
+ if (args.follow) {
142
+ console.log(c.cyan('Streaming logs for all agents...'))
143
+ } else {
144
+ console.log(c.dim('Showing logs for all agents.'))
145
+ }
146
+ const result = await composeLogs({ rootCwd: process.cwd(), follow: args.follow, signal: controller.signal })
147
+ if (result.agents.length === 0) {
148
+ console.log(c.dim('No typeclaw agents found in immediate subdirectories of cwd.'))
149
+ return
150
+ }
151
+ if (result.exitCode !== 0) process.exit(result.exitCode)
152
+ } finally {
153
+ process.off('SIGINT', onSig)
154
+ process.off('SIGTERM', onSig)
155
+ }
156
+ },
157
+ })
158
+
159
+ export const composeCommand = defineCommand({
160
+ meta: {
161
+ name: 'compose',
162
+ description: 'orchestrate every typeclaw agent in immediate subdirectories of cwd',
163
+ },
164
+ subCommands: {
165
+ start: startSub,
166
+ stop: stopSub,
167
+ restart: restartSub,
168
+ status: statusSub,
169
+ logs: logsSub,
170
+ },
171
+ })
172
+
173
+ // Single clack spinner with a multi-line message body, one line per agent.
174
+ // Concurrent clack spinners can't coexist: each one's render loop writes
175
+ // cursor.to(0) + erase.down() to process.stdout, so they trample each other.
176
+ // Multi-line redraw is safe — clack counts newlines in the previous message
177
+ // and walks the cursor up before erasing (see @clack/prompts spinner.ts).
178
+ type Board = {
179
+ add: (s: ReturnType<typeof spinner>, name: string, state: string) => void
180
+ set: (s: ReturnType<typeof spinner>, name: string, state: string) => void
181
+ finish: <T>(s: ReturnType<typeof spinner>, results: AgentResult<T>[], verb: string, failed: number) => void
182
+ }
183
+
184
+ function makeBoard(header: string): Board {
185
+ const order: string[] = []
186
+ const states = new Map<string, string>()
187
+ let started = false
188
+
189
+ const renderLines = (): string => {
190
+ const width = order.reduce((w, name) => Math.max(w, name.length), 0)
191
+ return order.map((name) => ` ${c.bold(name.padEnd(width))} ${states.get(name) ?? ''}`).join('\n')
192
+ }
193
+
194
+ const paint = (s: ReturnType<typeof spinner>): void => {
195
+ const body = `${header}\n${renderLines()}`
196
+ if (!started) {
197
+ started = true
198
+ s.start(body)
199
+ } else {
200
+ s.message(body)
201
+ }
202
+ }
203
+
204
+ return {
205
+ add(s, name, state) {
206
+ order.push(name)
207
+ states.set(name, c.dim(`${state}...`))
208
+ paint(s)
209
+ },
210
+ set(s, name, state) {
211
+ states.set(name, state)
212
+ paint(s)
213
+ },
214
+ finish(s, results, verb, failed) {
215
+ const total = results.length
216
+ const ok = total - failed
217
+ const summary = failed === 0 ? `${verb} ${ok}/${total}` : `${verb} ${ok}/${total} (${failed} failed)`
218
+ const body = `${failed === 0 ? c.green(summary) : c.red(summary)}\n${renderLines()}`
219
+ if (failed === 0) s.stop(body)
220
+ else s.error(body)
221
+ },
222
+ }
223
+ }
224
+
225
+ function formatStartDone<T extends { alreadyRunning?: boolean; hostPort: number }>(result: AgentResult<T>): string {
226
+ if (!result.ok) return `${c.red('✖')} ${c.red('failed:')} ${result.reason}`
227
+ const verb = result.data.alreadyRunning ? 'already running' : 'started'
228
+ return `${c.green('✔')} ${verb} on host port ${c.cyan(String(result.data.hostPort))}`
229
+ }
230
+
231
+ function formatStopDone<T extends { running: boolean }>(result: AgentResult<T>): string {
232
+ if (!result.ok) return `${c.red('✖')} ${c.red('failed:')} ${result.reason}`
233
+ if (result.data.running) return `${c.green('✔')} stopped`
234
+ return `${c.dim('○')} ${c.dim('not running')}`
235
+ }
236
+
237
+ function formatRestartDone<T extends { start: { hostPort: number } }>(result: AgentResult<T>): string {
238
+ if (!result.ok) return `${c.red('✖')} ${c.red('failed:')} ${result.reason}`
239
+ return `${c.green('✔')} restarted on host port ${c.cyan(String(result.data.start.hostPort))}`
240
+ }
@@ -0,0 +1,163 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { loadConfigSync, validateConfig, type Config, type ValidateConfigResult } from '@/config'
4
+ import { start, stop, type StartOptions, type StartResult, type StopResult } from '@/container'
5
+ import { startDaemon, type DaemonLogEvent, type RestartPreflight } from '@/hostd/daemon'
6
+ import { createPortbrokerManager } from '@/hostd/portbroker-manager'
7
+ import type { SupervisorLogEvent, SupervisorRestart } from '@/hostd/supervisor'
8
+ import { computeSourceVersion, resolveSrcRoot, UNVERSIONED_SENTINEL } from '@/hostd/version'
9
+
10
+ export const hostdCommand = defineCommand({
11
+ meta: {
12
+ name: '_hostd',
13
+ description: 'internal: host-side typeclaw daemon (do not invoke directly)',
14
+ hidden: true,
15
+ },
16
+ async run() {
17
+ const cliEntry = process.argv[1] ?? ''
18
+ const srcRoot = resolveSrcRoot(cliEntry)
19
+ const version = srcRoot === null ? UNVERSIONED_SENTINEL : await computeSourceVersion({ srcRoot })
20
+
21
+ const portbroker = createPortbrokerManager({
22
+ onLog: (msg) => writeLogLine(msg),
23
+ })
24
+
25
+ const daemon = await startDaemon({
26
+ onLog: (e) => writeLogLine(formatLog(e)),
27
+ version,
28
+ onShutdown: () => process.exit(0),
29
+ portbroker,
30
+ restartPreflight: buildHostdRestartPreflight(cliEntry, version),
31
+ restart: buildHostdRestart(cliEntry, defaultRestartDeps, version),
32
+ })
33
+
34
+ const shutdown = (): void => {
35
+ void daemon
36
+ .stop()
37
+ .then(() => portbroker.drain())
38
+ .then(() => process.exit(0))
39
+ }
40
+ process.on('SIGTERM', shutdown)
41
+ process.on('SIGINT', shutdown)
42
+
43
+ await new Promise<void>(() => {})
44
+ },
45
+ })
46
+
47
+ export type HostdRestartDeps = {
48
+ validateConfig: (cwd: string) => ValidateConfigResult
49
+ stop: (opts: { cwd: string }) => Promise<StopResult>
50
+ loadConfigSync: (cwd: string) => Config
51
+ start: (opts: StartOptions) => Promise<StartResult>
52
+ }
53
+
54
+ const defaultRestartDeps: HostdRestartDeps = {
55
+ validateConfig,
56
+ stop,
57
+ loadConfigSync,
58
+ start,
59
+ }
60
+
61
+ export function buildHostdRestart(
62
+ cliEntry: string,
63
+ deps: HostdRestartDeps = defaultRestartDeps,
64
+ daemonVersion?: string,
65
+ ): SupervisorRestart {
66
+ return async ({ containerName, cwd, build = false }) => {
67
+ const drift = await detectSourceDrift(cliEntry, daemonVersion)
68
+ if (drift) return { ok: false, reason: drift }
69
+
70
+ const validated = deps.validateConfig(cwd)
71
+ if (!validated.ok) {
72
+ return { ok: false, reason: `invalid config for ${containerName}: ${validated.reason}` }
73
+ }
74
+ const stopResult = await deps.stop({ cwd })
75
+ if (!stopResult.ok) return { ok: false, reason: `stop failed: ${stopResult.reason}` }
76
+
77
+ const cfg = deps.loadConfigSync(cwd)
78
+ const startResult = await deps.start({
79
+ cwd,
80
+ preferredHostPort: cfg.port,
81
+ forceBuild: build,
82
+ cliEntry,
83
+ reuseCurrentHostDaemon: true,
84
+ })
85
+ if (!startResult.ok) return { ok: false, reason: `start failed: ${startResult.reason}` }
86
+ return { ok: true }
87
+ }
88
+ }
89
+
90
+ export function buildHostdRestartPreflight(cliEntry: string, daemonVersion: string): RestartPreflight {
91
+ return async () => {
92
+ const drift = await detectSourceDrift(cliEntry, daemonVersion)
93
+ return drift ? { ok: false, reason: drift } : null
94
+ }
95
+ }
96
+
97
+ async function detectSourceDrift(cliEntry: string, daemonVersion: string | undefined): Promise<string | null> {
98
+ if (!daemonVersion || daemonVersion === UNVERSIONED_SENTINEL) return null
99
+ const srcRoot = resolveSrcRoot(cliEntry)
100
+ if (srcRoot === null) return null
101
+ const currentVersion = await computeSourceVersion({ srcRoot })
102
+ if (currentVersion === daemonVersion) return null
103
+ return 'host daemon source has drifted from the current typeclaw source; run `typeclaw restart --build` from the host-stage agent folder so the daemon respawns before rebuilding the Docker image'
104
+ }
105
+
106
+ function writeLogLine(msg: string): void {
107
+ console.log(`${new Date().toISOString()} ${msg}`)
108
+ }
109
+
110
+ function formatLog(event: DaemonLogEvent | SupervisorLogEvent): string {
111
+ switch (event.kind) {
112
+ case 'daemon-listening':
113
+ return `[hostd] listening on ${event.socket}`
114
+ case 'daemon-http-listening':
115
+ return `[hostd] HTTP control listening on ${event.host}:${event.port}`
116
+ case 'daemon-http-port-fallback':
117
+ return `[hostd] HTTP preferred port ${event.preferred} busy; fell back to ${event.actual} (containers started on ${event.preferred} will see stale TYPECLAW_HOSTD_URL until restarted)`
118
+ case 'daemon-stopping':
119
+ return `[hostd] stopping`
120
+ case 'shutdown-requested':
121
+ return `[hostd] shutdown requested (version drift); exiting so the next CLI call respawns`
122
+ case 'register':
123
+ return `[hostd] registered ${event.containerName}`
124
+ case 'deregister':
125
+ return `[hostd] deregistered ${event.containerName} (${event.reason})`
126
+ case 'registration-skipped':
127
+ return `[hostd] skipped persisted registration ${event.containerName}: ${event.reason}`
128
+ case 'restart-scheduled':
129
+ return `[hostd] restart scheduled for ${event.containerName}${event.build ? ' (with rebuild)' : ''}`
130
+ case 'restart-completed':
131
+ return `[hostd] restart completed for ${event.containerName}`
132
+ case 'restart-failed':
133
+ return `[hostd] restart failed for ${event.containerName}: ${event.reason}`
134
+ case 'port-forward-event':
135
+ return formatPortForwardEvent(event.event)
136
+ case 'tailscale-serve-event':
137
+ return formatTailscaleServeEvent(event.event)
138
+ }
139
+ }
140
+
141
+ function formatPortForwardEvent(event: import('@/portbroker').PortForwardEvent): string {
142
+ switch (event.kind) {
143
+ case 'port-forward-opened':
144
+ return `[hostd] port-forward opened ${event.containerName}:${event.port} (${event.bindAddr}) → localhost:${event.port}`
145
+ case 'port-forward-closed':
146
+ return `[hostd] port-forward closed ${event.containerName}:${event.port} (${event.reason})`
147
+ case 'port-forward-failed':
148
+ return `[hostd] port-forward FAILED ${event.containerName}:${event.port} — ${event.reason}`
149
+ }
150
+ }
151
+
152
+ function formatTailscaleServeEvent(event: import('@/hostd/tailscale').TailscaleServeEvent): string {
153
+ switch (event.kind) {
154
+ case 'tailscale-serve-opened':
155
+ return `[hostd] tailscale serve opened ${event.containerName}:${event.port}`
156
+ case 'tailscale-serve-closed':
157
+ return `[hostd] tailscale serve closed ${event.containerName}:${event.port}`
158
+ case 'tailscale-serve-skipped':
159
+ return `[hostd] tailscale serve skipped ${event.containerName}:${event.port} — ${event.reason}`
160
+ case 'tailscale-serve-failed':
161
+ return `[hostd] tailscale serve FAILED ${event.containerName}:${event.port} (${event.command}) — ${event.reason}`
162
+ }
163
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { defineCommand, runMain } from 'citty'
4
+
5
+ const main = defineCommand({
6
+ meta: {
7
+ name: 'typeclaw',
8
+ description: 'TypeClaw agent runtime',
9
+ },
10
+ subCommands: {
11
+ init: () => import('./init').then((m) => m.init),
12
+ run: () => import('./run').then((m) => m.run),
13
+ tui: () => import('./tui').then((m) => m.tui),
14
+ start: () => import('./start').then((m) => m.startCommand),
15
+ stop: () => import('./stop').then((m) => m.stopCommand),
16
+ restart: () => import('./restart').then((m) => m.restartCommand),
17
+ status: () => import('./status').then((m) => m.statusCommand),
18
+ reload: () => import('./reload').then((m) => m.reload),
19
+ logs: () => import('./logs').then((m) => m.logsCommand),
20
+ shell: () => import('./shell').then((m) => m.shellCommand),
21
+ compose: () => import('./compose').then((m) => m.composeCommand),
22
+ channel: () => import('./channel').then((m) => m.channelCommand),
23
+ _hostd: () => import('./hostd').then((m) => m.hostdCommand),
24
+ },
25
+ })
26
+
27
+ runMain(main)