zooid 0.7.1 → 0.7.3
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/dist/bin.js +59 -6
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-SUPTPSN3.js → chunk-3IKBBKGI.js} +178 -24
- package/dist/chunk-3IKBBKGI.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/web/assets/{index-Cz5fEZBc.js → index-BqOX0Pv4.js} +85 -75
- package/dist/web/assets/index-C-ZtBp7U.css +1 -0
- package/dist/web/assets/{index-CqfWXqvO.js → index-Dc8BJYf_.js} +1 -1
- package/dist/web/assets/{reaction-picker-emoji-cME0vOsX.js → reaction-picker-emoji-xD2HkULN.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +8 -8
- package/src/build-registry.context.test.ts +1 -1
- package/src/build-registry.zod043.test.ts +1 -1
- package/src/build-registry.zod044.test.ts +2 -2
- package/src/commands/dev.ts +5 -1
- package/src/daemon/start-daemon.ts +17 -2
- package/src/services/tuwunel.ts +68 -1
- package/dist/chunk-SUPTPSN3.js.map +0 -1
- package/dist/web/assets/index-C2MAIew3.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>Zoon</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BqOX0Pv4.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C-ZtBp7U.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Open-source tool for defining and running AI agents alongside you and your team. Any model, any CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"marked": "^18.0.4",
|
|
38
38
|
"sanitize-html": "^2.17.4",
|
|
39
39
|
"yaml": "^2.5.0",
|
|
40
|
-
"@zooid/
|
|
41
|
-
"@zooid/
|
|
42
|
-
"@zooid/
|
|
43
|
-
"@zooid/runtime-docker": "^0.7.
|
|
44
|
-
"@zooid/
|
|
45
|
-
"@zooid/
|
|
46
|
-
"@zooid/
|
|
40
|
+
"@zooid/context-mcp": "^0.7.3",
|
|
41
|
+
"@zooid/acp-client": "^0.7.3",
|
|
42
|
+
"@zooid/transport-http": "^0.7.3",
|
|
43
|
+
"@zooid/runtime-docker": "^0.7.3",
|
|
44
|
+
"@zooid/transport-matrix": "^0.7.3",
|
|
45
|
+
"@zooid/runtime-local": "^0.7.3",
|
|
46
|
+
"@zooid/core": "^0.7.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "^0.21.0",
|
|
@@ -28,7 +28,7 @@ function makeCfg(over: Partial<ZooidConfig> = {}): ZooidConfig {
|
|
|
28
28
|
matrix: {
|
|
29
29
|
transport: 'm1',
|
|
30
30
|
user_id: '@alice:localhost',
|
|
31
|
-
rooms: ['!r:localhost'],
|
|
31
|
+
rooms: [{ alias: '!r:localhost' }],
|
|
32
32
|
trigger: 'mention',
|
|
33
33
|
},
|
|
34
34
|
container: { env: { LOG_LEVEL: 'info' } },
|
|
@@ -27,7 +27,7 @@ function mkCfg(over: Partial<ZooidConfig['agents']['alice']> = {}): ZooidConfig
|
|
|
27
27
|
matrix: {
|
|
28
28
|
transport: 'm1',
|
|
29
29
|
user_id: '@alice:localhost',
|
|
30
|
-
rooms: ['!r:localhost'],
|
|
30
|
+
rooms: [{ alias: '!r:localhost' }],
|
|
31
31
|
trigger: 'mention',
|
|
32
32
|
},
|
|
33
33
|
...over,
|
|
@@ -207,7 +207,7 @@ describe('buildAcpRegistry — image resolution (ZOD044)', () => {
|
|
|
207
207
|
matrix: {
|
|
208
208
|
transport: 'm1',
|
|
209
209
|
user_id: '@bob:localhost',
|
|
210
|
-
rooms: ['!r:localhost'],
|
|
210
|
+
rooms: [{ alias: '!r:localhost' }],
|
|
211
211
|
trigger: 'mention',
|
|
212
212
|
},
|
|
213
213
|
},
|
package/src/commands/dev.ts
CHANGED
|
@@ -161,7 +161,11 @@ export async function runDev(flags: DevFlags): Promise<DevHandle> {
|
|
|
161
161
|
title: 'Wait for Tuwunel /_matrix/client/versions',
|
|
162
162
|
task: async () => {
|
|
163
163
|
if (!ctx.svc) throw new Error('service not started')
|
|
164
|
-
|
|
164
|
+
// 180s covers Docker Desktop's first-run bind-mount setup on macOS
|
|
165
|
+
// (which can sit in `created` for 30–90s before transitioning to
|
|
166
|
+
// `running`) plus Tuwunel's RocksDB init. Repeated runs against an
|
|
167
|
+
// existing DB are far quicker.
|
|
168
|
+
await ctx.svc.waitHealthy({ url: homeserver, timeoutMs: 180_000 })
|
|
165
169
|
},
|
|
166
170
|
},
|
|
167
171
|
{
|
|
@@ -18,6 +18,7 @@ import { createApp } from '@zooid/transport-http'
|
|
|
18
18
|
import {
|
|
19
19
|
MatrixClient,
|
|
20
20
|
createMatrixTransport,
|
|
21
|
+
ensureDefaultChannel,
|
|
21
22
|
ensureWorkforceSpace,
|
|
22
23
|
startWorkforcePublisher,
|
|
23
24
|
type AgentBinding,
|
|
@@ -174,7 +175,7 @@ export async function startDaemon(opts: StartDaemonOpts = {}): Promise<DaemonHan
|
|
|
174
175
|
hsToken: matrix.transport.hs_token,
|
|
175
176
|
adminUserId: opts.adminUserId,
|
|
176
177
|
})
|
|
177
|
-
const requestedPort = matrix.transport.port ??
|
|
178
|
+
const requestedPort = matrix.transport.port ?? 9000
|
|
178
179
|
// Bind 0.0.0.0 explicitly — @hono/node-server defaults to IPv6-only on
|
|
179
180
|
// macOS, which Docker's NAT bridge can't reach when Tuwunel pushes AS
|
|
180
181
|
// events back to host.docker.internal:<port>.
|
|
@@ -189,6 +190,7 @@ export async function startDaemon(opts: StartDaemonOpts = {}): Promise<DaemonHan
|
|
|
189
190
|
new URL(matrix.transport.homeserver).hostname
|
|
190
191
|
const asUserId = `@${matrix.transport.sender_localpart}:${serverName}`
|
|
191
192
|
const spaceLocalpart = matrix.transport.space ?? 'dev'
|
|
193
|
+
const adminUserIds = opts.adminUserId ? [opts.adminUserId] : []
|
|
192
194
|
let spaceRoomId: string | undefined
|
|
193
195
|
try {
|
|
194
196
|
spaceRoomId = await ensureWorkforceSpace({
|
|
@@ -197,6 +199,7 @@ export async function startDaemon(opts: StartDaemonOpts = {}): Promise<DaemonHan
|
|
|
197
199
|
serverName,
|
|
198
200
|
spaceLocalpart,
|
|
199
201
|
preset: 'public_chat',
|
|
202
|
+
admins: adminUserIds,
|
|
200
203
|
})
|
|
201
204
|
console.log(`[matrix] ensured workforce space #${spaceLocalpart}:${serverName} → ${spaceRoomId}`)
|
|
202
205
|
} catch (err) {
|
|
@@ -209,9 +212,21 @@ export async function startDaemon(opts: StartDaemonOpts = {}): Promise<DaemonHan
|
|
|
209
212
|
// those pushes don't hit a connection-refused. Bootstrap also runs
|
|
210
213
|
// *after* the space exists so BotPool can attach each agent room as
|
|
211
214
|
// m.space.child while joining it.
|
|
212
|
-
await transport.bootstrap({ spaceRoomId, asUserId })
|
|
215
|
+
await transport.bootstrap({ spaceRoomId, asUserId, adminUserIds })
|
|
213
216
|
|
|
214
217
|
if (spaceRoomId) {
|
|
218
|
+
try {
|
|
219
|
+
const generalRoomId = await ensureDefaultChannel({
|
|
220
|
+
client,
|
|
221
|
+
asUserId,
|
|
222
|
+
serverName,
|
|
223
|
+
spaceId: spaceRoomId,
|
|
224
|
+
admins: adminUserIds,
|
|
225
|
+
})
|
|
226
|
+
console.log(`[matrix] ensured default channel #general:${serverName} → ${generalRoomId}`)
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.warn('[matrix] default channel provisioning failed:', err)
|
|
229
|
+
}
|
|
215
230
|
try {
|
|
216
231
|
const publisher: PublisherHandle = await startWorkforcePublisher({
|
|
217
232
|
client,
|
package/src/services/tuwunel.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
2
4
|
import type { Paths } from '../bootstrap/paths.js'
|
|
3
5
|
|
|
6
|
+
const execFileAsync = promisify(execFile)
|
|
7
|
+
|
|
4
8
|
export interface TuwunelOpts {
|
|
5
9
|
name: string
|
|
6
10
|
image?: string
|
|
@@ -61,7 +65,32 @@ export class TuwunelService {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
async waitHealthy(opts: { url: string; timeoutMs: number }): Promise<void> {
|
|
68
|
+
// Two-phase wait: first the container has to transition from `created` to
|
|
69
|
+
// `running` (Docker Desktop on macOS can take 30–90s on first-run bind-mount
|
|
70
|
+
// setup), then Tuwunel has to open its DB and start serving HTTP. Polling
|
|
71
|
+
// only HTTP loses both signals: a long create looks identical to a long
|
|
72
|
+
// boot, and a crashed container looks identical to a slow one.
|
|
64
73
|
const deadline = Date.now() + opts.timeoutMs
|
|
74
|
+
|
|
75
|
+
// Phase 1: wait for the container to be running. Surface the actual
|
|
76
|
+
// docker error if it transitions to `exited` instead — that's the
|
|
77
|
+
// case where today we just hang for 60s and say "did not become healthy"
|
|
78
|
+
// with no clue what really broke.
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
const state = await this.inspectState().catch(() => null)
|
|
81
|
+
if (state?.status === 'running') break
|
|
82
|
+
if (state?.status === 'exited') {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Tuwunel container exited before serving HTTP ` +
|
|
85
|
+
`(exit=${state.exitCode}${state.error ? `, error=${state.error}` : ''}). ` +
|
|
86
|
+
`Check the engine logs (\`${this.opts.engine} logs ${this.opts.name}\`).`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Phase 2: container is running (or we ran out of time). Poll the HTTP
|
|
93
|
+
// endpoint until either it answers or the deadline passes.
|
|
65
94
|
while (Date.now() < deadline) {
|
|
66
95
|
try {
|
|
67
96
|
const r = await fetch(`${opts.url}/_matrix/client/versions`)
|
|
@@ -71,10 +100,48 @@ export class TuwunelService {
|
|
|
71
100
|
}
|
|
72
101
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
73
102
|
}
|
|
103
|
+
|
|
104
|
+
// Last-ditch diagnostic before throwing the generic timeout — surface the
|
|
105
|
+
// current container state so the user knows whether to blame slow start,
|
|
106
|
+
// slow boot, or a misconfiguration.
|
|
107
|
+
const finalState = await this.inspectState().catch(() => null)
|
|
108
|
+
const detail = finalState
|
|
109
|
+
? ` (container status=${finalState.status}${
|
|
110
|
+
finalState.exitCode !== undefined ? `, exit=${finalState.exitCode}` : ''
|
|
111
|
+
})`
|
|
112
|
+
: ''
|
|
74
113
|
throw new Error(
|
|
75
|
-
`Tuwunel did not become healthy at ${opts.url} in ${opts.timeoutMs}ms`,
|
|
114
|
+
`Tuwunel did not become healthy at ${opts.url} in ${opts.timeoutMs}ms${detail}`,
|
|
76
115
|
)
|
|
77
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the engine's view of the container. Returns null if the container
|
|
120
|
+
* isn't known to the engine (e.g. before spawn completed or after `--rm`).
|
|
121
|
+
*/
|
|
122
|
+
private async inspectState(): Promise<{
|
|
123
|
+
status: string
|
|
124
|
+
exitCode?: number
|
|
125
|
+
error?: string
|
|
126
|
+
} | null> {
|
|
127
|
+
try {
|
|
128
|
+
const { stdout } = await execFileAsync(this.opts.engine, [
|
|
129
|
+
'inspect',
|
|
130
|
+
this.opts.name,
|
|
131
|
+
'--format',
|
|
132
|
+
'{{.State.Status}}|{{.State.ExitCode}}|{{.State.Error}}',
|
|
133
|
+
])
|
|
134
|
+
const [status, exitCodeRaw, error] = stdout.trim().split('|')
|
|
135
|
+
const exitCode = exitCodeRaw && exitCodeRaw !== '<no value>' ? Number(exitCodeRaw) : undefined
|
|
136
|
+
return {
|
|
137
|
+
status: status ?? 'unknown',
|
|
138
|
+
...(Number.isFinite(exitCode) ? { exitCode: exitCode as number } : {}),
|
|
139
|
+
...(error ? { error } : {}),
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
78
145
|
}
|
|
79
146
|
|
|
80
147
|
function execEngine(engine: string, args: string[]): Promise<void> {
|