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,592 @@
1
+ import { cancel, confirm, intro, isCancel, log, note, password, select, spinner, text } from '@clack/prompts'
2
+ import { defineCommand } from 'citty'
3
+
4
+ import {
5
+ KNOWN_PROVIDERS,
6
+ supportsApiKey as providerSupportsApiKey,
7
+ supportsOAuth as providerSupportsOAuth,
8
+ type KnownModelRef,
9
+ type KnownProviderId,
10
+ } from '@/config/providers'
11
+ import type { DockerAvailability } from '@/container'
12
+ import {
13
+ findAgentDir,
14
+ isDirectoryNonEmpty,
15
+ isHatched,
16
+ runInit,
17
+ type InitStep,
18
+ type InitStepEvent,
19
+ type KakaotalkAuthResult,
20
+ type LLMAuth,
21
+ } from '@/init'
22
+ import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
23
+ import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
24
+ import { makeOAuthLoginRunner } from '@/init/oauth-login'
25
+
26
+ import { c, done, errorLine } from './ui'
27
+
28
+ export const init = defineCommand({
29
+ meta: {
30
+ name: 'init',
31
+ description: 'initialize a new typeclaw agent in the current directory',
32
+ },
33
+ async run() {
34
+ const cwd = process.cwd()
35
+
36
+ const existingAgent = findAgentDir(cwd)
37
+ if (existingAgent !== null && existingAgent !== cwd) {
38
+ console.error(
39
+ errorLine(
40
+ `Refusing to init: a TypeClaw agent already exists at ${existingAgent}. Nested agents are not supported.`,
41
+ ),
42
+ )
43
+ process.exit(1)
44
+ }
45
+
46
+ if (await isHatched(cwd)) {
47
+ console.error(errorLine(`TypeClaw has already hatched in ${cwd}.`))
48
+ process.exit(1)
49
+ }
50
+
51
+ if (isDirectoryNonEmpty(cwd)) {
52
+ const proceed = await confirm({
53
+ message: `You're at ${cwd}. The directory is not empty. Do you want to proceed?`,
54
+ initialValue: false,
55
+ })
56
+ if (isCancel(proceed) || !proceed) {
57
+ cancel('Aborted.')
58
+ process.exit(0)
59
+ }
60
+ }
61
+
62
+ intro('Initializing TypeClaw...')
63
+
64
+ const selectedModel = await pickModel()
65
+ const provider = KNOWN_PROVIDERS[selectedModel.providerId]
66
+
67
+ const llmAuth = await collectLLMAuth(provider)
68
+
69
+ const channelChoice = await select({
70
+ message: 'Pick a channel to wire (you can add more later by editing typeclaw.json + .env)',
71
+ options: [
72
+ { value: 'slack', label: 'Slack' },
73
+ { value: 'discord', label: 'Discord' },
74
+ { value: 'telegram', label: 'Telegram' },
75
+ { value: 'kakaotalk', label: 'KakaoTalk' },
76
+ { value: 'none', label: 'Skip — no channel right now' },
77
+ ],
78
+ initialValue: 'slack' as const,
79
+ })
80
+ if (isCancel(channelChoice)) {
81
+ cancel('Aborted.')
82
+ process.exit(0)
83
+ }
84
+
85
+ let discordBotToken: string | undefined
86
+ let slackBotToken: string | undefined
87
+ let slackAppToken: string | undefined
88
+ let telegramBotToken: string | undefined
89
+ let kakaotalkEmail: string | undefined
90
+ let kakaotalkPassword: string | undefined
91
+
92
+ if (channelChoice === 'discord') {
93
+ note(
94
+ [
95
+ 'https://discord.com/developers/applications',
96
+ 'New Application → Bot tab → Reset Token.',
97
+ 'Enable the MESSAGE CONTENT intent.',
98
+ ].join('\n'),
99
+ 'Get a Discord bot token',
100
+ )
101
+ const token = await password({
102
+ message: 'Discord bot token',
103
+ validate: (value) => (value && value.length > 0 ? undefined : 'Token is required'),
104
+ })
105
+ if (isCancel(token)) {
106
+ cancel('Aborted.')
107
+ process.exit(0)
108
+ }
109
+ discordBotToken = token
110
+ }
111
+
112
+ if (channelChoice === 'kakaotalk') {
113
+ note(
114
+ [
115
+ 'KakaoTalk authentication uses a personal account, registered as a',
116
+ 'tablet sub-device. Messages will be sent and received under this',
117
+ 'account. Use a non-primary account if possible.',
118
+ '',
119
+ 'After you submit the password, KakaoTalk may ask you to confirm a',
120
+ 'passcode on your phone. Watch the screen for the code.',
121
+ ].join('\n'),
122
+ 'About to log in to KakaoTalk',
123
+ )
124
+ const email = await text({
125
+ message: 'KakaoTalk email',
126
+ validate: (value) => (value && value.length > 0 ? undefined : 'Email is required'),
127
+ })
128
+ if (isCancel(email)) {
129
+ cancel('Aborted.')
130
+ process.exit(0)
131
+ }
132
+ const pwd = await password({
133
+ message: 'KakaoTalk password',
134
+ validate: (value) => (value && value.length > 0 ? undefined : 'Password is required'),
135
+ })
136
+ if (isCancel(pwd)) {
137
+ cancel('Aborted.')
138
+ process.exit(0)
139
+ }
140
+ kakaotalkEmail = email
141
+ kakaotalkPassword = pwd
142
+ }
143
+
144
+ if (channelChoice === 'slack') {
145
+ note(
146
+ [
147
+ '1. https://api.slack.com/apps → Create New App → From a manifest.',
148
+ ' Pick your workspace, then paste this JSON manifest:',
149
+ '',
150
+ ' {',
151
+ ' "display_information": { "name": "TypeClaw" },',
152
+ ' "features": {',
153
+ ' "bot_user": { "display_name": "TypeClaw", "always_online": true }',
154
+ ' },',
155
+ ' "oauth_config": {',
156
+ ' "scopes": {',
157
+ ' "bot": [',
158
+ ' "app_mentions:read", "chat:write", "users:read", "files:read",',
159
+ ' "channels:history", "channels:read",',
160
+ ' "groups:history", "groups:read",',
161
+ ' "im:history", "im:read",',
162
+ ' "mpim:history", "mpim:read"',
163
+ ' ]',
164
+ ' }',
165
+ ' },',
166
+ ' "settings": {',
167
+ ' "event_subscriptions": {',
168
+ ' "bot_events": [',
169
+ ' "app_mention",',
170
+ ' "message.channels", "message.groups",',
171
+ ' "message.im", "message.mpim"',
172
+ ' ]',
173
+ ' },',
174
+ ' "socket_mode_enabled": true',
175
+ ' }',
176
+ ' }',
177
+ '',
178
+ '2. Install to Workspace, then OAuth & Permissions →',
179
+ ' copy the Bot User OAuth Token (xoxb-...).',
180
+ '3. Basic Information → App-Level Tokens → Generate Token and',
181
+ ' Scopes, add the connections:write scope, and copy the',
182
+ ' token (xapp-...). Socket Mode needs this; the manifest',
183
+ ' cannot grant it.',
184
+ '4. Invite the bot to any private channel or DM you want it in:',
185
+ ' /invite @TypeClaw',
186
+ ].join('\n'),
187
+ 'Get a Slack bot',
188
+ )
189
+ const botToken = await password({
190
+ message: 'Slack bot token (xoxb-...)',
191
+ validate: (value) =>
192
+ value && value.length > 0
193
+ ? value.startsWith('xoxb-')
194
+ ? undefined
195
+ : 'Bot token must start with "xoxb-"'
196
+ : 'Token is required',
197
+ })
198
+ if (isCancel(botToken)) {
199
+ cancel('Aborted.')
200
+ process.exit(0)
201
+ }
202
+ slackBotToken = botToken
203
+ note(
204
+ [
205
+ 'Slack does not accept connections:write inside the manifest, so',
206
+ 'this token has to be generated by hand:',
207
+ '',
208
+ '1. Basic Information → App-Level Tokens → Generate Token and Scopes.',
209
+ '2. Token Name: anything (e.g. "socket-mode").',
210
+ '3. Add Scope → connections:write → Generate.',
211
+ '4. Copy the xapp-... token shown once on screen.',
212
+ ' (You cannot retrieve it later — only revoke and regenerate.)',
213
+ ].join('\n'),
214
+ 'Generate the Slack app-level token',
215
+ )
216
+ const appToken = await password({
217
+ message: 'Slack app-level token (xapp-...) — Socket Mode requires this',
218
+ validate: (value) =>
219
+ value && value.length > 0
220
+ ? value.startsWith('xapp-')
221
+ ? undefined
222
+ : 'App-level token must start with "xapp-"'
223
+ : 'Token is required',
224
+ })
225
+ if (isCancel(appToken)) {
226
+ cancel('Aborted.')
227
+ process.exit(0)
228
+ }
229
+ slackAppToken = appToken
230
+ }
231
+
232
+ if (channelChoice === 'telegram') {
233
+ note(
234
+ [
235
+ 'Open Telegram and message @BotFather.',
236
+ '/newbot → pick a name and username, copy the HTTP API token',
237
+ ' (looks like 1234567890:ABCdef...).',
238
+ 'In @BotFather: /setprivacy → Disable, so the bot can see group messages.',
239
+ ].join('\n'),
240
+ 'Get a Telegram bot token',
241
+ )
242
+ const token = await password({
243
+ message: 'Telegram bot token',
244
+ validate: (value) =>
245
+ value && value.length > 0
246
+ ? /^\d+:/.test(value)
247
+ ? undefined
248
+ : 'Bot token must look like "<digits>:<secret>" (from @BotFather)'
249
+ : 'Token is required',
250
+ })
251
+ if (isCancel(token)) {
252
+ cancel('Aborted.')
253
+ process.exit(0)
254
+ }
255
+ telegramBotToken = token
256
+ note(
257
+ [
258
+ 'Open https://t.me/<your_bot_username> (the username you picked in /newbot, ends in "bot").',
259
+ 'Tap Start in the chat — the agent will reply once it hatches.',
260
+ 'For groups: add the bot to the group, then @mention it or reply to its messages.',
261
+ ].join('\n'),
262
+ 'Send your first message',
263
+ )
264
+ }
265
+
266
+ // TODO: add remaining wizard steps from TypeClaw.md once their runtime lands:
267
+ // - git backup (url + PAT) — Phase 10
268
+ // - cron.json scaffolding — Phase 9
269
+ // - compose.yml registration in $HOME/.typeclaw — Phase 12
270
+ const wantsKakaotalk = kakaotalkEmail !== undefined && kakaotalkPassword !== undefined
271
+ let hatchingOk = false
272
+ let preflightFailure: Extract<DockerAvailability, { ok: false }> | null = null
273
+ try {
274
+ await runInit({
275
+ cwd,
276
+ llmAuth,
277
+ model: selectedModel.ref,
278
+ ...(discordBotToken !== undefined ? { discordBotToken } : {}),
279
+ ...(slackBotToken !== undefined ? { slackBotToken, slackAppToken } : {}),
280
+ ...(telegramBotToken !== undefined ? { telegramBotToken } : {}),
281
+ ...(wantsKakaotalk
282
+ ? {
283
+ withKakaotalk: true,
284
+ runKakaotalkAuth: ({ cwd: agentDir }) =>
285
+ runKakaotalkBootstrap({
286
+ email: kakaotalkEmail!,
287
+ password: kakaotalkPassword!,
288
+ agentDir,
289
+ callbacks: {
290
+ onPasscode: (code) => log.info(`Confirm this passcode on your phone: ${code}`),
291
+ },
292
+ }),
293
+ }
294
+ : {}),
295
+ onProgress: reportProgress(
296
+ (ok) => {
297
+ hatchingOk = ok
298
+ },
299
+ (result) => {
300
+ preflightFailure = result
301
+ },
302
+ ),
303
+ })
304
+ } catch (error) {
305
+ console.error(errorLine(error instanceof Error ? error.message : String(error)))
306
+ process.exit(1)
307
+ }
308
+
309
+ if (preflightFailure !== null) {
310
+ note(preflightFailureGuidance(preflightFailure).join('\n'), 'Docker check failed')
311
+ process.exit(1)
312
+ }
313
+
314
+ if (hatchingOk) {
315
+ done({
316
+ title: c.green('Hatched. Your agent is ready.'),
317
+ hints: [
318
+ { label: 'Attach TUI:', command: 'typeclaw tui' },
319
+ { label: 'Follow logs:', command: 'typeclaw logs -f' },
320
+ { label: 'Stop:', command: 'typeclaw stop' },
321
+ ],
322
+ })
323
+ }
324
+ },
325
+ })
326
+
327
+ function reportProgress(
328
+ onHatchingDone: (ok: boolean) => void,
329
+ onPreflightFail: (result: Extract<DockerAvailability, { ok: false }>) => void,
330
+ ): (event: InitStepEvent) => void {
331
+ const spinners: Partial<Record<InitStepEvent['step'], ReturnType<typeof spinner>>> = {}
332
+
333
+ return (event) => {
334
+ if (event.step === 'hatching') {
335
+ reportHatching(event)
336
+ if (event.phase === 'done') onHatchingDone(event.result.ok)
337
+ return
338
+ }
339
+
340
+ if (event.phase === 'start') {
341
+ const s = spinner()
342
+ s.start(START_MESSAGES[event.step])
343
+ spinners[event.step] = s
344
+ return
345
+ }
346
+
347
+ const s = spinners[event.step]
348
+ if (!s) return
349
+
350
+ switch (event.step) {
351
+ case 'preflight':
352
+ if (event.result.ok) {
353
+ s.stop('Docker is reachable.')
354
+ } else {
355
+ s.error(preflightFailureSummary(event.result))
356
+ onPreflightFail(event.result)
357
+ }
358
+ break
359
+ case 'scaffold':
360
+ s.stop('Egg laid. 🥚')
361
+ break
362
+ case 'kakaotalk-auth':
363
+ s.stop(reportKakaotalkAuth(event.result))
364
+ break
365
+ case 'oauth-login':
366
+ s.stop(event.result.ok ? 'Logged in.' : `OAuth login failed: ${event.result.reason}`)
367
+ break
368
+ case 'install':
369
+ s.stop(event.result.ok ? 'Dependencies installed.' : `Skipped bun install: ${event.result.reason}`)
370
+ break
371
+ case 'dockerfile':
372
+ if (event.result.ok) {
373
+ s.stop(event.result.devMode ? 'Dockerfile written (dev mode).' : 'Dockerfile written.')
374
+ } else {
375
+ s.stop(`Skipped Dockerfile: ${event.result.reason}`)
376
+ }
377
+ break
378
+ case 'git':
379
+ if (event.result.ok) {
380
+ s.stop(event.result.skipped ? 'Git repository already exists.' : 'Git repository initialized.')
381
+ } else {
382
+ s.stop(`Skipped git init: ${event.result.reason}`)
383
+ }
384
+ break
385
+ }
386
+ }
387
+ }
388
+
389
+ function preflightFailureSummary(result: Extract<DockerAvailability, { ok: false }>): string {
390
+ if (result.reason === 'binary-missing') return 'Docker is not installed.'
391
+ return 'Docker is installed but the daemon is not reachable.'
392
+ }
393
+
394
+ function preflightFailureGuidance(result: Extract<DockerAvailability, { ok: false }>): string[] {
395
+ if (result.reason === 'binary-missing') {
396
+ return [
397
+ 'TypeClaw runs every agent inside its own Docker container, so Docker is required.',
398
+ '',
399
+ 'Install one of:',
400
+ ' • Docker Desktop — https://docs.docker.com/get-docker/',
401
+ ' • OrbStack (macOS, lighter) — https://orbstack.dev',
402
+ '',
403
+ 'Then re-run `typeclaw init`.',
404
+ ]
405
+ }
406
+ return [
407
+ 'The docker CLI is on $PATH, but the daemon refused the connection:',
408
+ '',
409
+ ` ${result.detail}`,
410
+ '',
411
+ 'Start Docker Desktop / OrbStack (or `sudo systemctl start docker` on Linux),',
412
+ 'then re-run `typeclaw init`.',
413
+ ]
414
+ }
415
+
416
+ function reportKakaotalkAuth(result: KakaotalkAuthResult): string {
417
+ if (result.ok) return 'KakaoTalk credentials saved to workspace/.agent-messenger/.'
418
+ return `KakaoTalk login failed: ${result.reason}`
419
+ }
420
+
421
+ // Hatching launches the container and foregrounds the TUI, so it steals stdin
422
+ // and cannot share the spinner lifecycle with the other steps. Print plain
423
+ // lines instead.
424
+ function reportHatching(event: Extract<InitStepEvent, { step: 'hatching' }>): void {
425
+ if (event.phase === 'start') {
426
+ console.log('Hatching...')
427
+ return
428
+ }
429
+ if (event.result.ok) {
430
+ console.log('Hatched. 🐣')
431
+ } else {
432
+ console.error(`Hatching failed: ${event.result.reason}`)
433
+ }
434
+ }
435
+
436
+ // Resolves how the user wants to authenticate to the chosen provider:
437
+ // - api-key only (e.g. Fireworks): prompt for the key, write to .env.
438
+ // - oauth only (e.g. openai-codex): run the browser flow inline, write
439
+ // secrets.json. No API key prompt at all.
440
+ // - both supported (no providers ship this today, but Anthropic will when
441
+ // wired): ask "API key or OAuth?" first, then dispatch to the chosen path.
442
+ async function collectLLMAuth(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): Promise<LLMAuth> {
443
+ const supportsApiKey = providerSupportsApiKey(provider)
444
+ const supportsOAuth = providerSupportsOAuth(provider)
445
+
446
+ let method: 'api-key' | 'oauth'
447
+ if (supportsApiKey && supportsOAuth) {
448
+ const choice = await select<'api-key' | 'oauth'>({
449
+ message: `How do you want to authenticate to ${provider.name}?`,
450
+ options: [
451
+ { value: 'api-key', label: 'API key', hint: `saved to .env as ${provider.apiKeyEnv}` },
452
+ { value: 'oauth', label: 'OAuth (browser login)', hint: 'saved to secrets.json' },
453
+ ],
454
+ initialValue: 'api-key',
455
+ })
456
+ if (isCancel(choice)) {
457
+ cancel('Aborted.')
458
+ process.exit(0)
459
+ }
460
+ method = choice
461
+ } else if (supportsOAuth) {
462
+ method = 'oauth'
463
+ } else {
464
+ method = 'api-key'
465
+ }
466
+
467
+ if (method === 'api-key') {
468
+ const apiKey = await password({
469
+ message: `Put your ${provider.name} API key (will be saved to .env as ${provider.apiKeyEnv})`,
470
+ validate: (value) => (value && value.length > 0 ? undefined : 'API key is required'),
471
+ })
472
+ if (isCancel(apiKey)) {
473
+ cancel('Aborted.')
474
+ process.exit(0)
475
+ }
476
+ return { kind: 'api-key', apiKey }
477
+ }
478
+
479
+ return { kind: 'oauth', runLogin: makeOAuthLoginRunner(buildOAuthCallbacks(provider.name)) }
480
+ }
481
+
482
+ // Wraps the OAuth lifecycle into the same clack idiom the rest of the wizard
483
+ // uses: a spinner over the "waiting for login" period, with onAuth printing
484
+ // the URL the user needs to open and onPrompt falling back to a `text`
485
+ // prompt for the manual code path. The spinner is started by onAuth and
486
+ // stopped by the caller (runInit) — we don't try to manage it here because
487
+ // the spinner lifecycle has to span emit('start') -> emit('done').
488
+ function buildOAuthCallbacks(providerName: string) {
489
+ return {
490
+ onAuth: (url: string, instructions?: string) => {
491
+ // Don't put the URL inside note(): clack wraps long lines with the box
492
+ // border `│` on each wrapped segment, which corrupts the URL when the
493
+ // user copy-pastes it. Keep instructional text in the box, but print
494
+ // the URL itself as a bare console.log line that any terminal will
495
+ // hyperlink intact.
496
+ const preamble = [`Open this URL in your browser to authorize ${providerName}.`]
497
+ if (instructions) preamble.push('', instructions)
498
+ note(preamble.join('\n'), 'Browser login')
499
+ console.log(url)
500
+ console.log('')
501
+ },
502
+ onProgress: (message: string) => {
503
+ log.info(message)
504
+ },
505
+ onPrompt: async (message: string, placeholder?: string): Promise<string | null> => {
506
+ const value = await text({ message, ...(placeholder !== undefined ? { placeholder } : {}) })
507
+ if (isCancel(value)) return null
508
+ return value
509
+ },
510
+ }
511
+ }
512
+
513
+ // Two-step provider+model picker. We split it because most users have a key
514
+ // for exactly one provider — asking them to scroll through a flat list of
515
+ // every (provider, model) pair would surface options they can't use.
516
+ async function pickModel(): Promise<ModelOption> {
517
+ const s = spinner()
518
+ s.start('Loading model catalog from models.dev...')
519
+ const { options, source, warning } = await fetchModelOptions()
520
+ if (source === 'curated') {
521
+ s.stop(`Using built-in catalog (models.dev unavailable: ${warning ?? 'unknown'})`)
522
+ } else {
523
+ s.stop('Loaded model catalog.')
524
+ }
525
+
526
+ const providers = uniqueProviders(options)
527
+ const providerChoice = await select({
528
+ message: 'Pick an LLM provider',
529
+ options: providers.map((id) => ({ value: id, label: KNOWN_PROVIDERS[id].name, hint: providerAuthHint(id) })),
530
+ initialValue: providers[0],
531
+ })
532
+ if (isCancel(providerChoice)) {
533
+ cancel('Aborted.')
534
+ process.exit(0)
535
+ }
536
+
537
+ const candidates = options.filter((o) => o.providerId === providerChoice)
538
+ const modelChoice = await select<KnownModelRef>({
539
+ message: `Pick a ${KNOWN_PROVIDERS[providerChoice].name} model`,
540
+ options: candidates.map((o) => ({
541
+ value: o.ref,
542
+ label: o.modelName,
543
+ hint: formatModelHint(o),
544
+ })),
545
+ initialValue: candidates[0]?.ref,
546
+ })
547
+ if (isCancel(modelChoice)) {
548
+ cancel('Aborted.')
549
+ process.exit(0)
550
+ }
551
+
552
+ const picked = candidates.find((o) => o.ref === modelChoice)
553
+ if (!picked) throw new Error(`Internal error: picked model ${modelChoice} not in candidates`)
554
+ return picked
555
+ }
556
+
557
+ function uniqueProviders(options: ModelOption[]): KnownProviderId[] {
558
+ const seen = new Set<KnownProviderId>()
559
+ const out: KnownProviderId[] = []
560
+ for (const o of options) {
561
+ if (seen.has(o.providerId)) continue
562
+ seen.add(o.providerId)
563
+ out.push(o.providerId)
564
+ }
565
+ return out
566
+ }
567
+
568
+ function formatModelHint(o: ModelOption): string {
569
+ const parts: string[] = []
570
+ if (o.contextWindow !== null) parts.push(`${(o.contextWindow / 1000).toFixed(0)}K ctx`)
571
+ if (o.reasoning) parts.push('reasoning')
572
+ return parts.join(' · ')
573
+ }
574
+
575
+ function providerAuthHint(id: KnownProviderId): string {
576
+ const provider = KNOWN_PROVIDERS[id]
577
+ const apiKey = providerSupportsApiKey(provider)
578
+ const oauth = providerSupportsOAuth(provider)
579
+ if (apiKey && oauth) return 'API key or OAuth'
580
+ if (oauth) return 'OAuth login'
581
+ return 'API key'
582
+ }
583
+
584
+ const START_MESSAGES: Record<Exclude<InitStep, 'hatching'>, string> = {
585
+ preflight: 'Checking Docker...',
586
+ 'oauth-login': 'Waiting for browser login...',
587
+ scaffold: 'Laying the egg...',
588
+ 'kakaotalk-auth': 'Logging in to KakaoTalk...',
589
+ install: 'Installing dependencies with bun...',
590
+ dockerfile: 'Writing Dockerfile...',
591
+ git: 'Initializing git repository...',
592
+ }
@@ -0,0 +1,38 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { logs } from '@/container'
4
+ import { findAgentDir } from '@/init'
5
+
6
+ import { c, errorLine } from './ui'
7
+
8
+ export const logsCommand = defineCommand({
9
+ meta: {
10
+ name: 'logs',
11
+ description: 'show the agent container logs (host stage)',
12
+ },
13
+ args: {
14
+ follow: {
15
+ type: 'boolean',
16
+ alias: 'f',
17
+ description: 'stream new log output as it arrives',
18
+ default: false,
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const cwd = findAgentDir(process.cwd()) ?? process.cwd()
23
+
24
+ if (args.follow) {
25
+ console.log(c.cyan('Streaming container logs...'))
26
+ } else {
27
+ console.log(c.dim('Showing container logs.'))
28
+ }
29
+
30
+ const result = await logs({ cwd, follow: args.follow })
31
+ if (!result.ok) {
32
+ console.error(errorLine(result.reason))
33
+ process.exit(1)
34
+ }
35
+
36
+ process.exit(result.exitCode)
37
+ },
38
+ })
@@ -0,0 +1,68 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { resolveHostPort } from '@/container'
4
+ import { findAgentDir } from '@/init'
5
+ import { requestReload, type ReloadResult } from '@/reload'
6
+
7
+ import { c, errorLine, spinner } from './ui'
8
+
9
+ export const reload = defineCommand({
10
+ meta: {
11
+ name: 'reload',
12
+ description: "reload the running agent's reloadable subsystems (cron, ...)",
13
+ },
14
+ args: {
15
+ url: {
16
+ type: 'string',
17
+ description:
18
+ "agent websocket url (defaults to ws://localhost:<host port> discovered from the running container's published port)",
19
+ },
20
+ timeout: {
21
+ type: 'string',
22
+ description: 'milliseconds to wait for the agent to respond',
23
+ default: '30000',
24
+ },
25
+ },
26
+ async run({ args }) {
27
+ const url = args.url ?? (await defaultUrl())
28
+
29
+ const s = spinner()
30
+ s.start('Reloading...')
31
+ let results: ReloadResult[]
32
+ try {
33
+ results = await requestReload({ url, timeoutMs: Number(args.timeout) })
34
+ } catch (err) {
35
+ s.error(`reload failed: ${err instanceof Error ? err.message : String(err)}`)
36
+ process.exit(1)
37
+ }
38
+
39
+ if (results.length === 0) {
40
+ s.stop(c.dim('Nothing to reload.'))
41
+ return
42
+ }
43
+
44
+ let failed = 0
45
+ for (const r of results) {
46
+ if (!r.ok) failed++
47
+ }
48
+ s.stop(failed === 0 ? `Reloaded ${results.length} scope(s).` : `Reloaded with ${failed} failure(s).`)
49
+
50
+ for (const r of results) {
51
+ if (r.ok) {
52
+ console.log(`${c.green('●')} ${c.bold(`[${r.scope}]`)} ${r.summary}`)
53
+ } else {
54
+ console.error(`${c.red('●')} ${c.bold(`[${r.scope}]`)} ${errorLine(r.reason)}`)
55
+ }
56
+ }
57
+
58
+ if (failed > 0) {
59
+ process.exit(1)
60
+ }
61
+ },
62
+ })
63
+
64
+ async function defaultUrl(): Promise<string> {
65
+ const cwd = findAgentDir(process.cwd()) ?? process.cwd()
66
+ const port = await resolveHostPort({ cwd })
67
+ return `ws://localhost:${port}`
68
+ }