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,43 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ import { containerNameFromCwd } from '@/container'
5
+ import { isInitialized } from '@/init'
6
+
7
+ export type AgentEntry = {
8
+ name: string
9
+ cwd: string
10
+ containerName: string
11
+ }
12
+
13
+ // One-depth scan: lists immediate subdirectories of `rootCwd`, keeps the ones
14
+ // that contain a typeclaw.json, and skips dot-prefixed names (.git, .vscode,
15
+ // node_modules-style hidden dirs are not skipped because they don't match the
16
+ // dot-prefix rule, but they also won't pass the typeclaw.json filter).
17
+ //
18
+ // Returns an empty array when rootCwd doesn't exist or is empty — discovery is
19
+ // not the place to fail; the caller decides what to do with zero agents.
20
+ //
21
+ // Sort by name so output across operations (up/down/ps/restart/logs) is
22
+ // deterministic regardless of filesystem readdir order.
23
+ export function discoverAgents(rootCwd: string): AgentEntry[] {
24
+ const root = resolve(rootCwd)
25
+ let entries: { name: string; isDir: boolean }[]
26
+ try {
27
+ entries = readdirSync(root, { withFileTypes: true }).map((d) => ({ name: d.name, isDir: d.isDirectory() }))
28
+ } catch {
29
+ return []
30
+ }
31
+
32
+ const agents: AgentEntry[] = []
33
+ for (const entry of entries) {
34
+ if (!entry.isDir) continue
35
+ if (entry.name.startsWith('.')) continue
36
+ const cwd = join(root, entry.name)
37
+ if (!isInitialized(cwd)) continue
38
+ agents.push({ name: entry.name, cwd, containerName: containerNameFromCwd(cwd) })
39
+ }
40
+
41
+ agents.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
42
+ return agents
43
+ }
@@ -0,0 +1,25 @@
1
+ export { discoverAgents, type AgentEntry } from './discover'
2
+ export { colorFor, composeLogs, makeLinePrefixer, type ComposeLogsOptions, type ComposeLogsResult } from './logs'
3
+ export { composeStatus, type AgentRuntimeState, type AgentStatusEntry, type ComposeStatusResult } from './status'
4
+ export {
5
+ composeRestart,
6
+ type ComposeRestartEvent,
7
+ type ComposeRestartOptions,
8
+ type ComposeRestartResult,
9
+ type RestartData,
10
+ } from './restart'
11
+ export {
12
+ composeStart,
13
+ type AgentResult,
14
+ type ComposeStartEvent,
15
+ type ComposeStartOptions,
16
+ type ComposeStartResult,
17
+ type StartSuccess,
18
+ } from './start'
19
+ export {
20
+ composeStop,
21
+ type ComposeStopEvent,
22
+ type ComposeStopOptions,
23
+ type ComposeStopResult,
24
+ type StopSuccess,
25
+ } from './stop'
@@ -0,0 +1,162 @@
1
+ import { containerExists } from '@/container'
2
+ import { getBun } from '@/container/shared'
3
+
4
+ import { discoverAgents, type AgentEntry } from './discover'
5
+
6
+ export type ComposeLogsOptions = {
7
+ rootCwd: string
8
+ follow: boolean
9
+ out?: NodeJS.WritableStream
10
+ err?: NodeJS.WritableStream
11
+ signal?: AbortSignal
12
+ }
13
+
14
+ export type ComposeLogsResult = {
15
+ agents: AgentEntry[]
16
+ attached: AgentEntry[]
17
+ missing: AgentEntry[]
18
+ exitCode: number
19
+ }
20
+
21
+ const COLORS = ['36', '33', '32', '35', '34', '31', '96', '93', '92', '95'] as const
22
+
23
+ export function colorFor(name: string, palette: readonly string[] = COLORS): string {
24
+ let h = 0
25
+ for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0
26
+ return palette[h % palette.length] ?? '0'
27
+ }
28
+
29
+ // Stateful chunker that buffers partial lines across chunks: only emits
30
+ // newline-terminated lines (each prefixed with the agent name + bar), and
31
+ // flushes the un-terminated tail on EOF. Without this, interleaved chunks
32
+ // from multiple agents would shred lines mid-character.
33
+ export function makeLinePrefixer(
34
+ name: string,
35
+ width: number,
36
+ color: string,
37
+ useColor: boolean,
38
+ ): { write: (chunk: string) => string; flush: () => string } {
39
+ const padded = name.padEnd(width)
40
+ const prefix = useColor ? `\x1b[${color}m${padded}\x1b[0m | ` : `${padded} | `
41
+ let buffer = ''
42
+ return {
43
+ write(chunk: string): string {
44
+ buffer += chunk
45
+ const nl = buffer.lastIndexOf('\n')
46
+ if (nl < 0) return ''
47
+ const complete = buffer.slice(0, nl + 1)
48
+ buffer = buffer.slice(nl + 1)
49
+ return complete
50
+ .split('\n')
51
+ .slice(0, -1)
52
+ .map((l) => `${prefix}${l}\n`)
53
+ .join('')
54
+ },
55
+ flush(): string {
56
+ if (buffer.length === 0) return ''
57
+ const out = `${prefix}${buffer}\n`
58
+ buffer = ''
59
+ return out
60
+ },
61
+ }
62
+ }
63
+
64
+ export async function composeLogs({
65
+ rootCwd,
66
+ follow,
67
+ out = process.stdout,
68
+ err = process.stderr,
69
+ signal,
70
+ }: ComposeLogsOptions): Promise<ComposeLogsResult> {
71
+ const agents = discoverAgents(rootCwd)
72
+
73
+ const liveness = await Promise.all(
74
+ agents.map(async (a) => ({ agent: a, exists: await containerExists(a.containerName) })),
75
+ )
76
+ const attached = liveness.filter((l) => l.exists).map((l) => l.agent)
77
+ const missing = liveness.filter((l) => !l.exists).map((l) => l.agent)
78
+
79
+ for (const a of missing) {
80
+ err.write(`compose: skipping ${a.name} (container not running)\n`)
81
+ }
82
+ if (attached.length === 0) return { agents, attached, missing, exitCode: 0 }
83
+
84
+ const bun = getBun()
85
+ if (!bun) {
86
+ err.write('compose: bun runtime not available\n')
87
+ return { agents, attached, missing, exitCode: 1 }
88
+ }
89
+
90
+ const width = attached.reduce((w, a) => Math.max(w, a.name.length), 0)
91
+ const useColor = supportsColor(out)
92
+
93
+ const procs = attached.map((agent) => {
94
+ const cmd = follow ? ['docker', 'logs', '-f', agent.containerName] : ['docker', 'logs', agent.containerName]
95
+ const proc = bun.spawn({ cmd, stdout: 'pipe', stderr: 'pipe' })
96
+ return { agent, proc }
97
+ })
98
+
99
+ const onAbort = (): void => {
100
+ for (const { proc } of procs) {
101
+ try {
102
+ proc.kill('SIGTERM')
103
+ } catch {
104
+ // already exited
105
+ }
106
+ }
107
+ }
108
+ signal?.addEventListener('abort', onAbort, { once: true })
109
+
110
+ const pumps = procs.flatMap(({ agent, proc }) => {
111
+ const color = colorFor(agent.name)
112
+ return [
113
+ pumpStream(proc.stdout, makeLinePrefixer(agent.name, width, color, useColor), out),
114
+ pumpStream(proc.stderr, makeLinePrefixer(agent.name, width, color, useColor), err),
115
+ ]
116
+ })
117
+
118
+ await Promise.all(pumps)
119
+ const exits = await Promise.all(procs.map((p) => p.proc.exited))
120
+ signal?.removeEventListener('abort', onAbort)
121
+
122
+ // 143 = 128 + SIGTERM(15). When we cancel via signal, every child exits 143;
123
+ // that's expected, not failure. Surface only the first non-OK, non-cancelled
124
+ // exit so a real `docker logs` failure (e.g. "no such container") still bubbles.
125
+ const exitCode = exits.find((c) => c !== 0 && c !== 143) ?? 0
126
+ return { agents, attached, missing, exitCode }
127
+ }
128
+
129
+ async function pumpStream(
130
+ stream: ReadableStream<Uint8Array>,
131
+ prefixer: { write: (s: string) => string; flush: () => string },
132
+ sink: NodeJS.WritableStream,
133
+ ): Promise<void> {
134
+ const decoder = new TextDecoder()
135
+ const reader = stream.getReader()
136
+ try {
137
+ while (true) {
138
+ const { done, value } = await reader.read()
139
+ if (done) break
140
+ if (value && value.byteLength > 0) {
141
+ const out = prefixer.write(decoder.decode(value, { stream: true }))
142
+ if (out.length > 0) sink.write(out)
143
+ }
144
+ }
145
+ const tail = decoder.decode()
146
+ if (tail.length > 0) {
147
+ const out = prefixer.write(tail)
148
+ if (out.length > 0) sink.write(out)
149
+ }
150
+ const flushed = prefixer.flush()
151
+ if (flushed.length > 0) sink.write(flushed)
152
+ } finally {
153
+ reader.releaseLock()
154
+ }
155
+ }
156
+
157
+ function supportsColor(stream: NodeJS.WritableStream): boolean {
158
+ const tty = (stream as unknown as { isTTY?: boolean }).isTTY === true
159
+ if (!tty) return false
160
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false
161
+ return true
162
+ }
@@ -0,0 +1,69 @@
1
+ import { validateConfig } from '@/config'
2
+ import { start, stop } from '@/container'
3
+
4
+ import { discoverAgents, type AgentEntry } from './discover'
5
+ import type { AgentResult, StartSuccess } from './start'
6
+ import type { StopSuccess } from './stop'
7
+
8
+ export type RestartData = { stop: StopSuccess; start: StartSuccess }
9
+
10
+ export type ComposeRestartEvent =
11
+ | { kind: 'agent-start'; name: string }
12
+ | { kind: 'agent-stopped'; name: string }
13
+ | { kind: 'agent-done'; name: string; result: AgentResult<RestartData> }
14
+
15
+ export type ComposeRestartOptions = {
16
+ rootCwd: string
17
+ preferredHostPort: number
18
+ forceBuild?: boolean
19
+ cliEntry?: string
20
+ onProgress?: (event: ComposeRestartEvent) => void
21
+ }
22
+
23
+ export type ComposeRestartResult = {
24
+ agents: AgentEntry[]
25
+ results: AgentResult<RestartData>[]
26
+ }
27
+
28
+ export async function composeRestart({
29
+ rootCwd,
30
+ preferredHostPort,
31
+ forceBuild = false,
32
+ cliEntry,
33
+ onProgress,
34
+ }: ComposeRestartOptions): Promise<ComposeRestartResult> {
35
+ const agents = discoverAgents(rootCwd)
36
+ const results = await Promise.all(
37
+ agents.map(async (agent): Promise<AgentResult<RestartData>> => {
38
+ onProgress?.({ kind: 'agent-start', name: agent.name })
39
+ const result = await runOne(agent.name, agent.cwd, preferredHostPort, forceBuild, cliEntry, () => {
40
+ onProgress?.({ kind: 'agent-stopped', name: agent.name })
41
+ })
42
+ onProgress?.({ kind: 'agent-done', name: agent.name, result })
43
+ return result
44
+ }),
45
+ )
46
+ return { agents, results }
47
+ }
48
+
49
+ async function runOne(
50
+ name: string,
51
+ cwd: string,
52
+ preferredHostPort: number,
53
+ forceBuild: boolean,
54
+ cliEntry: string | undefined,
55
+ onStopped: () => void,
56
+ ): Promise<AgentResult<RestartData>> {
57
+ const validated = validateConfig(cwd)
58
+ if (!validated.ok) return { name, ok: false, reason: validated.reason }
59
+ try {
60
+ const stopped = await stop({ cwd })
61
+ if (!stopped.ok) return { name, ok: false, reason: stopped.reason }
62
+ onStopped()
63
+ const started = await start({ cwd, preferredHostPort, forceBuild, cliEntry })
64
+ if (!started.ok) return { name, ok: false, reason: started.reason }
65
+ return { name, ok: true, data: { stop: stopped, start: started } }
66
+ } catch (error) {
67
+ return { name, ok: false, reason: error instanceof Error ? error.message : String(error) }
68
+ }
69
+ }
@@ -0,0 +1,62 @@
1
+ import { validateConfig } from '@/config'
2
+ import { start, type StartResult } from '@/container'
3
+
4
+ import { discoverAgents, type AgentEntry } from './discover'
5
+
6
+ export type AgentResult<T> = { name: string; ok: true; data: T } | { name: string; ok: false; reason: string }
7
+
8
+ export type StartSuccess = Extract<StartResult, { ok: true }>
9
+
10
+ export type ComposeStartEvent =
11
+ | { kind: 'agent-start'; name: string }
12
+ | { kind: 'agent-done'; name: string; result: AgentResult<StartSuccess> }
13
+
14
+ export type ComposeStartOptions = {
15
+ rootCwd: string
16
+ preferredHostPort: number
17
+ forceBuild?: boolean
18
+ cliEntry?: string
19
+ onProgress?: (event: ComposeStartEvent) => void
20
+ }
21
+
22
+ export type ComposeStartResult = {
23
+ agents: AgentEntry[]
24
+ results: AgentResult<StartSuccess>[]
25
+ }
26
+
27
+ export async function composeStart({
28
+ rootCwd,
29
+ preferredHostPort,
30
+ forceBuild = false,
31
+ cliEntry,
32
+ onProgress,
33
+ }: ComposeStartOptions): Promise<ComposeStartResult> {
34
+ const agents = discoverAgents(rootCwd)
35
+ const results = await Promise.all(
36
+ agents.map(async (agent): Promise<AgentResult<StartSuccess>> => {
37
+ onProgress?.({ kind: 'agent-start', name: agent.name })
38
+ const result = await runOne(agent.name, agent.cwd, preferredHostPort, forceBuild, cliEntry)
39
+ onProgress?.({ kind: 'agent-done', name: agent.name, result })
40
+ return result
41
+ }),
42
+ )
43
+ return { agents, results }
44
+ }
45
+
46
+ async function runOne(
47
+ name: string,
48
+ cwd: string,
49
+ preferredHostPort: number,
50
+ forceBuild: boolean,
51
+ cliEntry: string | undefined,
52
+ ): Promise<AgentResult<StartSuccess>> {
53
+ const validated = validateConfig(cwd)
54
+ if (!validated.ok) return { name, ok: false, reason: validated.reason }
55
+ try {
56
+ const data = await start({ cwd, preferredHostPort, forceBuild, cliEntry })
57
+ if (!data.ok) return { name, ok: false, reason: data.reason }
58
+ return { name, ok: true, data }
59
+ } catch (error) {
60
+ return { name, ok: false, reason: error instanceof Error ? error.message : String(error) }
61
+ }
62
+ }
@@ -0,0 +1,28 @@
1
+ import { inspectContainer, resolveHostPort } from '@/container'
2
+
3
+ import { discoverAgents, type AgentEntry } from './discover'
4
+
5
+ export type AgentRuntimeState = 'running' | 'stopped' | 'absent'
6
+
7
+ export type AgentStatusEntry = AgentEntry & {
8
+ state: AgentRuntimeState
9
+ hostPort: number | null
10
+ }
11
+
12
+ export type ComposeStatusResult = {
13
+ rootCwd: string
14
+ entries: AgentStatusEntry[]
15
+ }
16
+
17
+ export async function composeStatus(rootCwd: string): Promise<ComposeStatusResult> {
18
+ const agents = discoverAgents(rootCwd)
19
+ const entries = await Promise.all(
20
+ agents.map(async (agent): Promise<AgentStatusEntry> => {
21
+ const container = await inspectContainer(agent.containerName)
22
+ const state: AgentRuntimeState = !container.exists ? 'absent' : container.running ? 'running' : 'stopped'
23
+ const hostPort = state === 'running' ? await resolveHostPort({ cwd: agent.cwd }).catch(() => null) : null
24
+ return { ...agent, state, hostPort }
25
+ }),
26
+ )
27
+ return { rootCwd, entries }
28
+ }
@@ -0,0 +1,43 @@
1
+ import { stop, type StopResult } from '@/container'
2
+
3
+ import { discoverAgents, type AgentEntry } from './discover'
4
+ import type { AgentResult } from './start'
5
+
6
+ export type StopSuccess = Extract<StopResult, { ok: true }>
7
+
8
+ export type ComposeStopEvent =
9
+ | { kind: 'agent-start'; name: string }
10
+ | { kind: 'agent-done'; name: string; result: AgentResult<StopSuccess> }
11
+
12
+ export type ComposeStopOptions = {
13
+ rootCwd: string
14
+ onProgress?: (event: ComposeStopEvent) => void
15
+ }
16
+
17
+ export type ComposeStopResult = {
18
+ agents: AgentEntry[]
19
+ results: AgentResult<StopSuccess>[]
20
+ }
21
+
22
+ export async function composeStop({ rootCwd, onProgress }: ComposeStopOptions): Promise<ComposeStopResult> {
23
+ const agents = discoverAgents(rootCwd)
24
+ const results = await Promise.all(
25
+ agents.map(async (agent): Promise<AgentResult<StopSuccess>> => {
26
+ onProgress?.({ kind: 'agent-start', name: agent.name })
27
+ const result = await runOne(agent.name, agent.cwd)
28
+ onProgress?.({ kind: 'agent-done', name: agent.name, result })
29
+ return result
30
+ }),
31
+ )
32
+ return { agents, results }
33
+ }
34
+
35
+ async function runOne(name: string, cwd: string): Promise<AgentResult<StopSuccess>> {
36
+ try {
37
+ const data = await stop({ cwd })
38
+ if (!data.ok) return { name, ok: false, reason: data.reason }
39
+ return { name, ok: true, data }
40
+ } catch (error) {
41
+ return { name, ok: false, reason: error instanceof Error ? error.message : String(error) }
42
+ }
43
+ }