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,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
|
+
}
|
package/src/cli/hostd.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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)
|