typeclaw 0.28.2 → 0.30.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/package.json +1 -1
- package/src/agent/index.ts +43 -5
- package/src/agent/live-subagents.ts +5 -0
- package/src/agent/loop-guard.ts +112 -26
- package/src/agent/plugin-tools.ts +167 -50
- package/src/agent/session-origin.ts +3 -3
- package/src/agent/subagent-drain.ts +150 -0
- package/src/agent/subagents.ts +41 -3
- package/src/agent/system-prompt.ts +29 -4
- package/src/agent/tools/channel-send.ts +1 -1
- package/src/agent/tools/spawn-subagent.ts +34 -1
- package/src/agent/tools/subagent-output.ts +7 -3
- package/src/agent/tools/wikipedia.ts +1 -1
- package/src/bundled-plugins/bun-hygiene/README.md +12 -11
- package/src/bundled-plugins/bun-hygiene/policy.ts +8 -3
- package/src/bundled-plugins/explorer/explorer.ts +2 -0
- package/src/bundled-plugins/github-cli-auth/approve-idempotency.ts +94 -0
- package/src/bundled-plugins/github-cli-auth/effective-approval.ts +98 -0
- package/src/bundled-plugins/github-cli-auth/gh-review-inline-detect.ts +130 -0
- package/src/bundled-plugins/github-cli-auth/index.ts +27 -2
- package/src/bundled-plugins/github-cli-auth/review-recorder.ts +12 -4
- package/src/bundled-plugins/memory/memory-logger.ts +3 -3
- package/src/bundled-plugins/operator/operator.ts +2 -0
- package/src/bundled-plugins/planner/index.ts +11 -0
- package/src/bundled-plugins/planner/planner.ts +283 -0
- package/src/bundled-plugins/planner/skills/general.ts +65 -0
- package/src/bundled-plugins/planner/skills/project.ts +69 -0
- package/src/bundled-plugins/researcher/index.ts +11 -0
- package/src/bundled-plugins/researcher/researcher.ts +233 -0
- package/src/bundled-plugins/researcher/skills/general.ts +105 -0
- package/src/bundled-plugins/researcher/write-report.ts +107 -0
- package/src/bundled-plugins/reviewer/reviewer.ts +28 -9
- package/src/bundled-plugins/reviewer/skills/data-review.ts +77 -0
- package/src/bundled-plugins/reviewer/skills/doc-review.ts +79 -0
- package/src/bundled-plugins/reviewer/skills/plan-review.ts +64 -0
- package/src/bundled-plugins/reviewer/skills/security-audit.ts +70 -0
- package/src/bundled-plugins/reviewer/skills/writing-review.ts +63 -0
- package/src/bundled-plugins/scout/scout.ts +2 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +8 -4
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +3 -2
- package/src/channels/adapters/discord-bot.ts +38 -11
- package/src/channels/adapters/github/inbound.ts +68 -4
- package/src/channels/adapters/kakaotalk-classify.ts +2 -2
- package/src/channels/adapters/kakaotalk.ts +2 -2
- package/src/channels/adapters/slack-bot-classify.ts +1 -1
- package/src/channels/adapters/slack-bot.ts +3 -0
- package/src/channels/adapters/telegram-bot.ts +3 -0
- package/src/channels/engagement.ts +12 -7
- package/src/channels/github-review-claim.ts +15 -3
- package/src/channels/router.ts +85 -9
- package/src/channels/schema.ts +1 -1
- package/src/channels/types.ts +6 -0
- package/src/cli/init.ts +13 -2
- package/src/cli/ui.ts +64 -0
- package/src/config/config.ts +21 -15
- package/src/container/start.ts +5 -1
- package/src/init/dockerfile.ts +19 -56
- package/src/init/hatching.ts +1 -1
- package/src/init/index.ts +5 -1
- package/src/migrations/index.ts +35 -0
- package/src/migrations/secrets-v1-to-v2.ts +344 -0
- package/src/run/bundled-plugins.ts +4 -0
- package/src/run/index.ts +13 -0
- package/src/sandbox/availability.ts +12 -0
- package/src/sandbox/build.ts +12 -0
- package/src/sandbox/index.ts +1 -1
- package/src/sandbox/policy.ts +8 -0
- package/src/server/index.ts +24 -5
- package/src/shared/host-locale.ts +27 -0
- package/src/shared/protocol.ts +1 -1
- package/src/shared/wordmark.ts +19 -0
- package/src/skills/typeclaw-config/SKILL.md +32 -32
- package/src/skills/typeclaw-kaomoji/SKILL.md +3 -3
- package/src/skills/typeclaw-tunnels/SKILL.md +3 -1
- package/src/tui/banner.ts +19 -0
- package/src/tui/format.ts +34 -0
- package/src/tui/index.ts +121 -22
- package/src/tui/theme.ts +26 -1
- package/src/tunnels/providers/cloudflare-named.ts +15 -4
- package/src/tunnels/providers/cloudflare-quick.ts +15 -4
- package/src/tunnels/providers/cloudflared-binary.ts +11 -0
- package/typeclaw.schema.json +15 -7
package/src/tui/index.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Editor,
|
|
3
|
+
Key,
|
|
4
|
+
Loader,
|
|
5
|
+
Markdown,
|
|
6
|
+
matchesKey,
|
|
7
|
+
ProcessTerminal,
|
|
8
|
+
type Terminal,
|
|
9
|
+
Text,
|
|
10
|
+
TUI,
|
|
11
|
+
} from '@mariozechner/pi-tui'
|
|
2
12
|
|
|
3
13
|
import { parseCommand } from '@/commands'
|
|
4
14
|
|
|
15
|
+
import { formatBanner } from './banner'
|
|
5
16
|
import { createClient as createClientDefault, type Client } from './client'
|
|
6
17
|
import {
|
|
18
|
+
formatAssistantHeader,
|
|
7
19
|
formatQueuePanel,
|
|
8
|
-
formatTimestamp,
|
|
9
20
|
formatToolEnd,
|
|
10
21
|
formatToolStart,
|
|
22
|
+
formatUsageSummary,
|
|
11
23
|
formatUserPromptHistory,
|
|
12
24
|
withTimestamp,
|
|
13
25
|
} from './format'
|
|
@@ -118,29 +130,58 @@ export function createTui({
|
|
|
118
130
|
if (handshake === null) return { reason: 'connectFailed' }
|
|
119
131
|
|
|
120
132
|
const { sessionId, serverVersion } = handshake
|
|
121
|
-
|
|
133
|
+
// The banner card already carries session id, version, and url, so it
|
|
134
|
+
// supersedes the old one-line session status in place (status is child[0],
|
|
135
|
+
// pinned above all scrollback).
|
|
136
|
+
status.setText(formatBanner({ sessionId, displayUrl, ...(serverVersion !== undefined ? { serverVersion } : {}) }))
|
|
122
137
|
tui.requestRender()
|
|
123
138
|
|
|
124
139
|
const editor = new Editor(tui, editorTheme, { paddingX: 0 })
|
|
140
|
+
const statusBar = new Text('', 0, 0)
|
|
125
141
|
let replyInFlight = false
|
|
126
142
|
let onReplyDone: (() => void) | null = null
|
|
127
143
|
let currentAssistant: Markdown | null = null
|
|
128
144
|
let currentAssistantText = ''
|
|
129
145
|
let queuePanel: Text | null = null
|
|
146
|
+
let thinkingLoader: Loader | null = null
|
|
147
|
+
let reloadLoader: Loader | null = null
|
|
148
|
+
let restartLoader: Loader | null = null
|
|
149
|
+
let usageLabel: string | null = null
|
|
150
|
+
let connectionLabel = 'connected'
|
|
151
|
+
|
|
152
|
+
const shortSessionId = sessionId.length > 14 ? `${sessionId.slice(0, 14)}…` : sessionId
|
|
153
|
+
const refreshStatusBar = () => {
|
|
154
|
+
const parts = [
|
|
155
|
+
colors.dim(connectionLabel),
|
|
156
|
+
colors.dim(`session ${shortSessionId}`),
|
|
157
|
+
serverVersion !== undefined ? colors.dim(`v${serverVersion}`) : null,
|
|
158
|
+
usageLabel !== null ? colors.accent(usageLabel) : null,
|
|
159
|
+
].filter((part): part is string => part !== null)
|
|
160
|
+
statusBar.setText(parts.join(colors.dim(' · ')))
|
|
161
|
+
}
|
|
162
|
+
refreshStatusBar()
|
|
130
163
|
|
|
131
164
|
// Pi-tui's Container.addChild appends to the end of the children array.
|
|
132
|
-
// The
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
// The bottom tail is pinned as [...history, queuePanel?, editor, statusBar]:
|
|
166
|
+
// the editor stays above the persistent status bar, and the queue panel,
|
|
167
|
+
// when present, sits just above the editor. Any new history entry is
|
|
168
|
+
// inserted by stripping that tail, appending the entry, then re-appending
|
|
169
|
+
// the tail in order so nothing ever renders below the status bar.
|
|
170
|
+
const reattachTail = () => {
|
|
171
|
+
if (queuePanel) tui.addChild(queuePanel)
|
|
172
|
+
tui.addChild(editor)
|
|
173
|
+
tui.addChild(statusBar)
|
|
174
|
+
}
|
|
175
|
+
const detachTail = () => {
|
|
139
176
|
if (queuePanel) tui.removeChild(queuePanel)
|
|
140
177
|
tui.removeChild(editor)
|
|
178
|
+
tui.removeChild(statusBar)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const appendHistory = (component: Text | Markdown) => {
|
|
182
|
+
detachTail()
|
|
141
183
|
tui.addChild(component)
|
|
142
|
-
|
|
143
|
-
tui.addChild(editor)
|
|
184
|
+
reattachTail()
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
const updateQueuePanel = (pending: ReadonlyArray<{ id: string; text: string; ts: number }>) => {
|
|
@@ -156,14 +197,36 @@ export function createTui({
|
|
|
156
197
|
if (queuePanel) {
|
|
157
198
|
queuePanel.setText(text)
|
|
158
199
|
} else {
|
|
159
|
-
queuePanel = new Text(text, 0, 0)
|
|
160
200
|
tui.removeChild(editor)
|
|
201
|
+
tui.removeChild(statusBar)
|
|
202
|
+
queuePanel = new Text(text, 0, 0)
|
|
161
203
|
tui.addChild(queuePanel)
|
|
162
204
|
tui.addChild(editor)
|
|
205
|
+
tui.addChild(statusBar)
|
|
163
206
|
}
|
|
164
207
|
tui.requestRender()
|
|
165
208
|
}
|
|
166
209
|
|
|
210
|
+
const showThinking = () => {
|
|
211
|
+
if (thinkingLoader !== null) return
|
|
212
|
+
const loader = new Loader(tui, colors.accent, colors.dim, 'thinking…')
|
|
213
|
+
thinkingLoader = loader
|
|
214
|
+
appendHistory(loader)
|
|
215
|
+
loader.start()
|
|
216
|
+
tui.requestRender()
|
|
217
|
+
}
|
|
218
|
+
const hideThinking = () => {
|
|
219
|
+
if (thinkingLoader === null) return
|
|
220
|
+
thinkingLoader.stop()
|
|
221
|
+
tui.removeChild(thinkingLoader)
|
|
222
|
+
thinkingLoader = null
|
|
223
|
+
}
|
|
224
|
+
const stopAllLoaders = () => {
|
|
225
|
+
thinkingLoader?.stop()
|
|
226
|
+
reloadLoader?.stop()
|
|
227
|
+
restartLoader?.stop()
|
|
228
|
+
}
|
|
229
|
+
|
|
167
230
|
// Reset between text segments so a new Markdown block is created after
|
|
168
231
|
// any non-text event (tool calls). Otherwise text_delta after a tool call
|
|
169
232
|
// would append to the previous Markdown and visually push the tool lines
|
|
@@ -174,19 +237,20 @@ export function createTui({
|
|
|
174
237
|
}
|
|
175
238
|
|
|
176
239
|
const finishAssistantTurn = () => {
|
|
240
|
+
hideThinking()
|
|
177
241
|
sealAssistantBlock()
|
|
178
242
|
replyInFlight = false
|
|
179
243
|
onReplyDone?.()
|
|
180
244
|
onReplyDone = null
|
|
181
245
|
}
|
|
182
246
|
|
|
183
|
-
// A Markdown block can't carry an ANSI
|
|
184
|
-
// markdown), so the assistant turn's
|
|
185
|
-
// emitted just above the block when it's first created —
|
|
186
|
-
// first delta's server `ts`.
|
|
247
|
+
// A Markdown block can't carry an ANSI header prefix (it'd be parsed as
|
|
248
|
+
// markdown), so the assistant turn's boxed header (label + timestamp) is a
|
|
249
|
+
// separate Text line emitted just above the block when it's first created —
|
|
250
|
+
// stamped with the first delta's server `ts`.
|
|
187
251
|
const ensureAssistantBlock = (ts: number | undefined): Markdown => {
|
|
188
252
|
if (currentAssistant) return currentAssistant
|
|
189
|
-
appendHistory(new Text(
|
|
253
|
+
appendHistory(new Text(formatAssistantHeader(ts), 0, 0))
|
|
190
254
|
const md = new Markdown('', 0, 0, markdownTheme)
|
|
191
255
|
currentAssistant = md
|
|
192
256
|
currentAssistantText = ''
|
|
@@ -198,10 +262,12 @@ export function createTui({
|
|
|
198
262
|
switch (msg.type) {
|
|
199
263
|
case 'prompt_started': {
|
|
200
264
|
appendHistory(new Text(withTimestamp(msg.ts, formatUserPromptHistory(msg.text)), 0, 0))
|
|
265
|
+
if (replyInFlight) showThinking()
|
|
201
266
|
tui.requestRender()
|
|
202
267
|
break
|
|
203
268
|
}
|
|
204
269
|
case 'text_delta': {
|
|
270
|
+
hideThinking()
|
|
205
271
|
const block = ensureAssistantBlock(msg.ts)
|
|
206
272
|
currentAssistantText += msg.delta
|
|
207
273
|
block.setText(currentAssistantText)
|
|
@@ -209,6 +275,7 @@ export function createTui({
|
|
|
209
275
|
break
|
|
210
276
|
}
|
|
211
277
|
case 'tool_start': {
|
|
278
|
+
hideThinking()
|
|
212
279
|
sealAssistantBlock()
|
|
213
280
|
appendHistory(new Text(withTimestamp(msg.ts, formatToolStart(msg.name, msg.args)), 0, 0))
|
|
214
281
|
tui.requestRender()
|
|
@@ -223,6 +290,10 @@ export function createTui({
|
|
|
223
290
|
break
|
|
224
291
|
}
|
|
225
292
|
case 'done': {
|
|
293
|
+
if (msg.usage !== undefined) {
|
|
294
|
+
usageLabel = formatUsageSummary(msg.usage)
|
|
295
|
+
refreshStatusBar()
|
|
296
|
+
}
|
|
226
297
|
finishAssistantTurn()
|
|
227
298
|
tui.requestRender()
|
|
228
299
|
break
|
|
@@ -238,6 +309,11 @@ export function createTui({
|
|
|
238
309
|
break
|
|
239
310
|
}
|
|
240
311
|
case 'reload_result': {
|
|
312
|
+
if (reloadLoader !== null) {
|
|
313
|
+
reloadLoader.stop()
|
|
314
|
+
tui.removeChild(reloadLoader)
|
|
315
|
+
reloadLoader = null
|
|
316
|
+
}
|
|
241
317
|
for (const result of msg.results) {
|
|
242
318
|
const text = result.ok
|
|
243
319
|
? `${colors.green('●')} ${colors.bold(`[${result.scope}]`)} ${result.summary}`
|
|
@@ -248,6 +324,11 @@ export function createTui({
|
|
|
248
324
|
break
|
|
249
325
|
}
|
|
250
326
|
case 'restart_result': {
|
|
327
|
+
if (restartLoader !== null) {
|
|
328
|
+
restartLoader.stop()
|
|
329
|
+
tui.removeChild(restartLoader)
|
|
330
|
+
restartLoader = null
|
|
331
|
+
}
|
|
251
332
|
const text =
|
|
252
333
|
msg.status === 'accepted'
|
|
253
334
|
? colors.green(colors.dim(msg.message ?? 'restart scheduled; reconnecting when the new container is up'))
|
|
@@ -271,6 +352,9 @@ export function createTui({
|
|
|
271
352
|
}
|
|
272
353
|
|
|
273
354
|
client.onClose(() => {
|
|
355
|
+
stopAllLoaders()
|
|
356
|
+
connectionLabel = 'disconnected'
|
|
357
|
+
refreshStatusBar()
|
|
274
358
|
appendHistory(new Text(colors.dim('disconnected'), 0, 0))
|
|
275
359
|
tui.requestRender()
|
|
276
360
|
// A user-initiated detach/exit already closed the WS deliberately and
|
|
@@ -293,14 +377,27 @@ export function createTui({
|
|
|
293
377
|
}
|
|
294
378
|
if (command === 'reload') {
|
|
295
379
|
client.send({ type: 'reload' })
|
|
296
|
-
|
|
380
|
+
if (reloadLoader === null) {
|
|
381
|
+
const loader = new Loader(tui, colors.accent, colors.dim, 'reloading…')
|
|
382
|
+
reloadLoader = loader
|
|
383
|
+
appendHistory(loader)
|
|
384
|
+
loader.start()
|
|
385
|
+
}
|
|
297
386
|
tui.requestRender()
|
|
298
387
|
return true
|
|
299
388
|
}
|
|
300
389
|
client.send({ type: 'restart' })
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
390
|
+
if (restartLoader === null) {
|
|
391
|
+
const loader = new Loader(
|
|
392
|
+
tui,
|
|
393
|
+
colors.yellow,
|
|
394
|
+
colors.dim,
|
|
395
|
+
'restart requested… reconnecting when the new container is up',
|
|
396
|
+
)
|
|
397
|
+
restartLoader = loader
|
|
398
|
+
appendHistory(loader)
|
|
399
|
+
loader.start()
|
|
400
|
+
}
|
|
304
401
|
tui.requestRender()
|
|
305
402
|
return true
|
|
306
403
|
}
|
|
@@ -322,6 +419,7 @@ export function createTui({
|
|
|
322
419
|
// settles 'lostConnection'. settle() is idempotent, so the first call wins —
|
|
323
420
|
// settling the deliberate outcome first keeps the later onClose a no-op.
|
|
324
421
|
const teardown = (): void => {
|
|
422
|
+
stopAllLoaders()
|
|
325
423
|
tui.stop()
|
|
326
424
|
client.close()
|
|
327
425
|
}
|
|
@@ -365,6 +463,7 @@ export function createTui({
|
|
|
365
463
|
void send(text)
|
|
366
464
|
}
|
|
367
465
|
tui.addChild(editor)
|
|
466
|
+
tui.addChild(statusBar)
|
|
368
467
|
tui.setFocus(editor)
|
|
369
468
|
tui.requestRender()
|
|
370
469
|
|
package/src/tui/theme.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EditorTheme, MarkdownTheme } from '@mariozechner/pi-tui'
|
|
2
2
|
|
|
3
3
|
const wrap = (code: string) => (text: string) => `\x1b[${code}m${text}\x1b[0m`
|
|
4
|
+
const wrapRgb = (r: number, g: number, b: number) => (text: string) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`
|
|
4
5
|
|
|
5
6
|
const dim = wrap('2')
|
|
6
7
|
const bold = wrap('1')
|
|
@@ -9,8 +10,32 @@ const green = wrap('32')
|
|
|
9
10
|
const yellow = wrap('33')
|
|
10
11
|
const cyan = wrap('36')
|
|
11
12
|
const gray = wrap('90')
|
|
13
|
+
const brightGreen = wrap('92')
|
|
14
|
+
const brightCyan = wrap('96')
|
|
15
|
+
const brightMagenta = wrap('95')
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
// Sampled from the typeey mascot. True navy (#182A5B) is too dark to read on
|
|
18
|
+
// dark terminals, so `accent` is a lifted cornflower of the same hue; amber is
|
|
19
|
+
// the mascot's beak/feet highlight.
|
|
20
|
+
const cornflower = wrapRgb(0x5b, 0x7f, 0xd4)
|
|
21
|
+
const amber = wrapRgb(0xe7, 0x8f, 0x37)
|
|
22
|
+
const accent = cornflower
|
|
23
|
+
|
|
24
|
+
export const colors = {
|
|
25
|
+
dim,
|
|
26
|
+
bold,
|
|
27
|
+
red,
|
|
28
|
+
green,
|
|
29
|
+
yellow,
|
|
30
|
+
cyan,
|
|
31
|
+
gray,
|
|
32
|
+
brightGreen,
|
|
33
|
+
brightCyan,
|
|
34
|
+
brightMagenta,
|
|
35
|
+
cornflower,
|
|
36
|
+
amber,
|
|
37
|
+
accent,
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
export const editorTheme: EditorTheme = {
|
|
16
41
|
borderColor: dim,
|
|
@@ -2,6 +2,7 @@ import type { Unsubscribe } from '@/stream'
|
|
|
2
2
|
|
|
3
3
|
import { createLogRing, type LogLineSubscriber, type LogRing } from '../log-ring'
|
|
4
4
|
import type { TunnelConfig, TunnelProviderHandle, TunnelState } from '../types'
|
|
5
|
+
import { isBinaryNotFound, MISSING_BINARY_DETAIL } from './cloudflared-binary'
|
|
5
6
|
|
|
6
7
|
const DEFAULT_BINARY = 'cloudflared'
|
|
7
8
|
const DEFAULT_RESTART_BACKOFF_MS = [1_000, 2_000, 4_000, 10_000, 30_000]
|
|
@@ -79,10 +80,20 @@ export function createCloudflareNamedProvider(options: CloudflareNamedProviderOp
|
|
|
79
80
|
|
|
80
81
|
state.status = 'starting'
|
|
81
82
|
state.detail = 'starting cloudflared'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
let spawned: Bun.Subprocess<'ignore', 'ignore', 'pipe'>
|
|
84
|
+
try {
|
|
85
|
+
spawned = Bun.spawn([binary, 'tunnel', '--no-autoupdate', 'run', '--token', token], {
|
|
86
|
+
stdout: 'ignore',
|
|
87
|
+
stderr: 'pipe',
|
|
88
|
+
})
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (isBinaryNotFound(err)) {
|
|
91
|
+
state.status = 'permanently-failed'
|
|
92
|
+
state.detail = MISSING_BINARY_DETAIL
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
throw err
|
|
96
|
+
}
|
|
86
97
|
proc = spawned
|
|
87
98
|
|
|
88
99
|
// Mark healthy on the FIRST stderr line. cloudflared with a valid token
|
|
@@ -3,6 +3,7 @@ import type { Unsubscribe } from '@/stream'
|
|
|
3
3
|
import { createLogRing, type LogLineSubscriber, type LogRing } from '../log-ring'
|
|
4
4
|
import { extractQuickTunnelUrl } from '../quick-url-parser'
|
|
5
5
|
import type { TunnelConfig, TunnelProviderHandle, TunnelState } from '../types'
|
|
6
|
+
import { isBinaryNotFound, MISSING_BINARY_DETAIL } from './cloudflared-binary'
|
|
6
7
|
|
|
7
8
|
const DEFAULT_BINARY = 'cloudflared'
|
|
8
9
|
const DEFAULT_RESTART_BACKOFF_MS = [1_000, 2_000, 4_000, 10_000, 30_000]
|
|
@@ -61,10 +62,20 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
61
62
|
attemptEmittedUrl = false
|
|
62
63
|
state.status = 'starting'
|
|
63
64
|
state.detail = 'starting cloudflared'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
let spawned: Bun.Subprocess<'ignore', 'ignore', 'pipe'>
|
|
66
|
+
try {
|
|
67
|
+
spawned = Bun.spawn(
|
|
68
|
+
[binary, 'tunnel', '--url', `http://127.0.0.1:${upstreamPort}`, '--no-autoupdate', '--metrics', '127.0.0.1:0'],
|
|
69
|
+
{ stdout: 'ignore', stderr: 'pipe' },
|
|
70
|
+
)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (isBinaryNotFound(err)) {
|
|
73
|
+
state.status = 'permanently-failed'
|
|
74
|
+
state.detail = MISSING_BINARY_DETAIL
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
68
79
|
proc = spawned
|
|
69
80
|
|
|
70
81
|
void pumpStderr(spawned.stderr, logs, (line) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Bun.spawn throws synchronously with `code === 'ENOENT'` when the binary is
|
|
2
|
+
// absent from $PATH. Both Cloudflare providers translate that into a
|
|
3
|
+
// permanently-failed state instead of letting the raw spawn error bubble up
|
|
4
|
+
// as a generic "start failed" log — the actionable fix is enabling
|
|
5
|
+
// docker.file.cloudflared and rebuilding, not waiting through restart backoff.
|
|
6
|
+
export const MISSING_BINARY_DETAIL =
|
|
7
|
+
'cloudflared binary not found in image; set docker.file.cloudflared: true in typeclaw.json and run typeclaw restart'
|
|
8
|
+
|
|
9
|
+
export function isBinaryNotFound(err: unknown): boolean {
|
|
10
|
+
return err instanceof Error && 'code' in err && (err as { code?: unknown }).code === 'ENOENT'
|
|
11
|
+
}
|
package/typeclaw.schema.json
CHANGED
|
@@ -1121,8 +1121,8 @@
|
|
|
1121
1121
|
"gh": true,
|
|
1122
1122
|
"python": true,
|
|
1123
1123
|
"tmux": true,
|
|
1124
|
-
"cjkFonts":
|
|
1125
|
-
"cloudflared":
|
|
1124
|
+
"cjkFonts": "auto",
|
|
1125
|
+
"cloudflared": false,
|
|
1126
1126
|
"xvfb": true,
|
|
1127
1127
|
"claudeCode": false,
|
|
1128
1128
|
"codexCli": false,
|
|
@@ -1137,8 +1137,8 @@
|
|
|
1137
1137
|
"gh": true,
|
|
1138
1138
|
"python": true,
|
|
1139
1139
|
"tmux": true,
|
|
1140
|
-
"cjkFonts":
|
|
1141
|
-
"cloudflared":
|
|
1140
|
+
"cjkFonts": "auto",
|
|
1141
|
+
"cloudflared": false,
|
|
1142
1142
|
"xvfb": true,
|
|
1143
1143
|
"claudeCode": false,
|
|
1144
1144
|
"codexCli": false,
|
|
@@ -1187,11 +1187,19 @@
|
|
|
1187
1187
|
]
|
|
1188
1188
|
},
|
|
1189
1189
|
"cjkFonts": {
|
|
1190
|
-
"default":
|
|
1191
|
-
"
|
|
1190
|
+
"default": "auto",
|
|
1191
|
+
"anyOf": [
|
|
1192
|
+
{
|
|
1193
|
+
"type": "boolean"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
"type": "string",
|
|
1197
|
+
"const": "auto"
|
|
1198
|
+
}
|
|
1199
|
+
]
|
|
1192
1200
|
},
|
|
1193
1201
|
"cloudflared": {
|
|
1194
|
-
"default":
|
|
1202
|
+
"default": false,
|
|
1195
1203
|
"type": "boolean"
|
|
1196
1204
|
},
|
|
1197
1205
|
"xvfb": {
|