typeclaw 0.10.0 → 0.11.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 +37 -4
- package/src/agent/restart-handoff/index.ts +91 -0
- package/src/agent/restart-handoff/paths.ts +11 -0
- package/src/agent/session-origin.ts +30 -10
- package/src/agent/subagent-completion-reminder.ts +4 -2
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tools/restart.ts +42 -1
- package/src/agent/tools/skip-response.ts +157 -0
- package/src/bundled-plugins/memory/README.md +18 -2
- package/src/bundled-plugins/memory/index.ts +108 -6
- package/src/bundled-plugins/memory/memory-logger.ts +33 -24
- package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
- package/src/channels/adapters/github/auth-app.ts +53 -9
- package/src/channels/adapters/github/auth-pat.ts +4 -1
- package/src/channels/adapters/github/auth.ts +10 -0
- package/src/channels/adapters/github/event-permissions.ts +83 -0
- package/src/channels/adapters/github/inbound.ts +126 -1
- package/src/channels/adapters/github/index.ts +60 -66
- package/src/channels/adapters/github/outbound.ts +65 -17
- package/src/channels/adapters/github/permission-guidance.ts +169 -0
- package/src/channels/adapters/github/team-membership.ts +56 -0
- package/src/channels/router.ts +213 -32
- package/src/channels/schema.ts +8 -7
- package/src/channels/types.ts +1 -1
- package/src/cli/channel.ts +135 -38
- package/src/cli/init.ts +133 -86
- package/src/cli/inspect-controller.ts +66 -0
- package/src/cli/inspect.ts +24 -32
- package/src/cli/run.ts +24 -5
- package/src/cli/tui.ts +34 -10
- package/src/cli/tunnel.ts +453 -14
- package/src/config/config.ts +35 -7
- package/src/config/providers.ts +64 -56
- package/src/init/env-file.ts +66 -0
- package/src/init/hatching.ts +32 -5
- package/src/init/index.ts +131 -39
- package/src/init/validate-api-key.ts +31 -0
- package/src/inspect/index.ts +5 -1
- package/src/inspect/loop.ts +12 -1
- package/src/inspect/replay.ts +15 -1
- package/src/run/codex-fetch-observer.ts +377 -0
- package/src/run/index.ts +12 -2
- package/src/server/index.ts +59 -1
- package/src/shared/protocol.ts +1 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +45 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
- package/src/tui/index.ts +17 -5
- package/src/tunnels/index.ts +1 -0
- package/src/tunnels/manager.ts +18 -0
- package/src/tunnels/providers/cloudflare-named.ts +224 -0
- package/src/tunnels/types.ts +17 -1
- package/typeclaw.schema.json +25 -7
package/src/tui/index.ts
CHANGED
|
@@ -44,6 +44,14 @@ export type TuiOptions = {
|
|
|
44
44
|
onVersionMismatch?: (info: VersionMismatch) => void
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Outcome of a single `run()` cycle. The CLI's reconnect loop reads this to
|
|
48
|
+
// decide whether to spin again or exit. `lostConnection` is true when the
|
|
49
|
+
// WS closed AFTER the connected handshake without a deliberate /quit or
|
|
50
|
+
// Ctrl+C — exactly the case a self-restart produces, and the only one
|
|
51
|
+
// where a fresh connect can recover the session. Quit / Ctrl+C / pre-
|
|
52
|
+
// handshake errors all resolve with `lostConnection: false`.
|
|
53
|
+
export type TuiRunOutcome = { lostConnection: boolean }
|
|
54
|
+
|
|
47
55
|
export function createTui({
|
|
48
56
|
url,
|
|
49
57
|
initialPrompt,
|
|
@@ -54,7 +62,7 @@ export function createTui({
|
|
|
54
62
|
expectedVersion,
|
|
55
63
|
onVersionMismatch,
|
|
56
64
|
}: TuiOptions) {
|
|
57
|
-
async function run(): Promise<
|
|
65
|
+
async function run(): Promise<TuiRunOutcome> {
|
|
58
66
|
const terminal = createTerminal()
|
|
59
67
|
const tui = new TUI(terminal)
|
|
60
68
|
const displayUrl = redactUrl(url)
|
|
@@ -80,6 +88,8 @@ export function createTui({
|
|
|
80
88
|
exit(1)
|
|
81
89
|
throw err
|
|
82
90
|
})
|
|
91
|
+
|
|
92
|
+
let userInitiatedShutdown = false
|
|
83
93
|
const { sessionId, serverVersion } = handshake
|
|
84
94
|
status.setText(colors.dim(`session: ${sessionId}`))
|
|
85
95
|
tui.requestRender()
|
|
@@ -196,11 +206,11 @@ export function createTui({
|
|
|
196
206
|
}
|
|
197
207
|
})
|
|
198
208
|
|
|
199
|
-
const closed = new Promise<
|
|
209
|
+
const closed = new Promise<boolean>((resolve) => {
|
|
200
210
|
client.onClose(() => {
|
|
201
211
|
appendHistory(new Text(colors.dim('disconnected'), 0, 0))
|
|
202
212
|
tui.requestRender()
|
|
203
|
-
resolve()
|
|
213
|
+
resolve(!userInitiatedShutdown)
|
|
204
214
|
})
|
|
205
215
|
})
|
|
206
216
|
|
|
@@ -223,6 +233,7 @@ export function createTui({
|
|
|
223
233
|
})
|
|
224
234
|
|
|
225
235
|
const shutdown = (code: number) => {
|
|
236
|
+
userInitiatedShutdown = true
|
|
226
237
|
tui.stop()
|
|
227
238
|
client.close()
|
|
228
239
|
exit(code)
|
|
@@ -268,13 +279,14 @@ export function createTui({
|
|
|
268
279
|
// instead of leaking the command into the agent's chat context.
|
|
269
280
|
if (isQuitCommand(initialPrompt)) {
|
|
270
281
|
shutdown(0)
|
|
271
|
-
return
|
|
282
|
+
return { lostConnection: false }
|
|
272
283
|
}
|
|
273
284
|
await send(initialPrompt)
|
|
274
285
|
}
|
|
275
286
|
|
|
276
|
-
await closed
|
|
287
|
+
const lostConnection = await closed
|
|
277
288
|
tui.stop()
|
|
289
|
+
return { lostConnection }
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
return { run }
|
package/src/tunnels/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createTunnelManager, type TunnelManager, type TunnelManagerOptions, type TunnelManagerLogger } from './manager'
|
|
2
|
+
export { createCloudflareNamedProvider, type CloudflareNamedProviderOptions } from './providers/cloudflare-named'
|
|
2
3
|
export { createCloudflareQuickProvider, type CloudflareQuickProviderOptions } from './providers/cloudflare-quick'
|
|
3
4
|
export {
|
|
4
5
|
type TunnelConfig,
|
package/src/tunnels/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Stream } from '@/stream'
|
|
2
2
|
|
|
3
|
+
import { createCloudflareNamedProvider } from './providers/cloudflare-named'
|
|
3
4
|
import { createCloudflareQuickProvider } from './providers/cloudflare-quick'
|
|
4
5
|
import { createExternalProvider } from './providers/external'
|
|
5
6
|
import type { TunnelConfig, TunnelProviderHandle, TunnelState, TunnelUrlChangedPayload } from './types'
|
|
@@ -15,6 +16,11 @@ export type TunnelManagerOptions = {
|
|
|
15
16
|
stream: Stream
|
|
16
17
|
resolveChannelUpstreamPort?: (channelName: string) => number | null
|
|
17
18
|
cloudflareQuickBinary?: string
|
|
19
|
+
cloudflareNamedBinary?: string
|
|
20
|
+
// Reads an env var by name. Defaults to `process.env[name]` in production.
|
|
21
|
+
// Parameterized so tests can drive the named-provider token path without
|
|
22
|
+
// poking global env and so the manager stays a pure function of its inputs.
|
|
23
|
+
resolveEnv?: (name: string) => string | undefined
|
|
18
24
|
logger?: TunnelManagerLogger
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -36,6 +42,7 @@ const consoleLogger: TunnelManagerLogger = {
|
|
|
36
42
|
export function createTunnelManager(options: TunnelManagerOptions): TunnelManager {
|
|
37
43
|
const logger = options.logger ?? consoleLogger
|
|
38
44
|
const handles = new Map<string, TunnelProviderHandle>()
|
|
45
|
+
const resolveEnv = options.resolveEnv ?? ((name: string) => process.env[name])
|
|
39
46
|
|
|
40
47
|
for (const config of options.tunnels) {
|
|
41
48
|
const handle = buildProvider(
|
|
@@ -43,6 +50,8 @@ export function createTunnelManager(options: TunnelManagerOptions): TunnelManage
|
|
|
43
50
|
options.resolveChannelUpstreamPort,
|
|
44
51
|
(url) => publishUrlChange(options.stream, config, url, logger),
|
|
45
52
|
options.cloudflareQuickBinary,
|
|
53
|
+
options.cloudflareNamedBinary,
|
|
54
|
+
resolveEnv,
|
|
46
55
|
)
|
|
47
56
|
handles.set(config.name, handle)
|
|
48
57
|
}
|
|
@@ -92,6 +101,8 @@ function buildProvider(
|
|
|
92
101
|
resolveChannelUpstreamPort: TunnelManagerOptions['resolveChannelUpstreamPort'],
|
|
93
102
|
onUrlChange: (url: string) => void,
|
|
94
103
|
cloudflareQuickBinary: string | undefined,
|
|
104
|
+
cloudflareNamedBinary: string | undefined,
|
|
105
|
+
resolveEnv: (name: string) => string | undefined,
|
|
95
106
|
): TunnelProviderHandle {
|
|
96
107
|
switch (config.provider) {
|
|
97
108
|
case 'external':
|
|
@@ -103,6 +114,13 @@ function buildProvider(
|
|
|
103
114
|
onUrlChange,
|
|
104
115
|
binary: cloudflareQuickBinary,
|
|
105
116
|
})
|
|
117
|
+
case 'cloudflare-named':
|
|
118
|
+
return createCloudflareNamedProvider({
|
|
119
|
+
config,
|
|
120
|
+
onUrlChange,
|
|
121
|
+
resolveToken: () => (config.tokenEnv !== undefined ? resolveEnv(config.tokenEnv) : undefined),
|
|
122
|
+
binary: cloudflareNamedBinary,
|
|
123
|
+
})
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Unsubscribe } from '@/stream'
|
|
2
|
+
|
|
3
|
+
import { createLogRing, type LogLineSubscriber, type LogRing } from '../log-ring'
|
|
4
|
+
import type { TunnelConfig, TunnelProviderHandle, TunnelState } from '../types'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BINARY = 'cloudflared'
|
|
7
|
+
const DEFAULT_RESTART_BACKOFF_MS = [1_000, 2_000, 4_000, 10_000, 30_000]
|
|
8
|
+
const DEFAULT_MAX_CONSECUTIVE_CRASHES = 10
|
|
9
|
+
const DEFAULT_STOP_GRACE_MS = 5_000
|
|
10
|
+
|
|
11
|
+
export type CloudflareNamedProviderOptions = {
|
|
12
|
+
config: TunnelConfig
|
|
13
|
+
onUrlChange: (url: string) => void
|
|
14
|
+
// Token resolver. Production wiring reads `process.env[config.tokenEnv]`;
|
|
15
|
+
// the resolver is parameterized so tests can inject a value without poking
|
|
16
|
+
// global env. Returning `undefined` (or empty string) at any call fails the
|
|
17
|
+
// start with a clear error pointing at the env-var name.
|
|
18
|
+
resolveToken: () => string | undefined
|
|
19
|
+
binary?: string
|
|
20
|
+
restartBackoffMs?: number[]
|
|
21
|
+
maxConsecutiveCrashes?: number
|
|
22
|
+
stopGraceMs?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CloudflareNamedProviderHandle = TunnelProviderHandle & {
|
|
26
|
+
tail: () => string[]
|
|
27
|
+
subscribeToLogs: (cb: LogLineSubscriber) => Unsubscribe
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createCloudflareNamedProvider(options: CloudflareNamedProviderOptions): CloudflareNamedProviderHandle {
|
|
31
|
+
const { config, onUrlChange, resolveToken } = options
|
|
32
|
+
if (config.provider !== 'cloudflare-named') {
|
|
33
|
+
throw new Error(`createCloudflareNamedProvider: provider must be 'cloudflare-named', got '${config.provider}'`)
|
|
34
|
+
}
|
|
35
|
+
const hostname = config.hostname
|
|
36
|
+
if (hostname === undefined || hostname.trim() === '') {
|
|
37
|
+
throw new Error(`tunnel '${config.name}' (cloudflare-named): hostname is required`)
|
|
38
|
+
}
|
|
39
|
+
const tokenEnv = config.tokenEnv
|
|
40
|
+
if (tokenEnv === undefined || tokenEnv.trim() === '') {
|
|
41
|
+
throw new Error(`tunnel '${config.name}' (cloudflare-named): tokenEnv is required`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const binary = options.binary ?? DEFAULT_BINARY
|
|
45
|
+
const restartBackoffMs = options.restartBackoffMs ?? DEFAULT_RESTART_BACKOFF_MS
|
|
46
|
+
const maxConsecutiveCrashes = options.maxConsecutiveCrashes ?? DEFAULT_MAX_CONSECUTIVE_CRASHES
|
|
47
|
+
const stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS
|
|
48
|
+
const logs = createLogRing()
|
|
49
|
+
const state: TunnelState = {
|
|
50
|
+
name: config.name,
|
|
51
|
+
provider: 'cloudflare-named',
|
|
52
|
+
for: config.for,
|
|
53
|
+
url: null,
|
|
54
|
+
status: 'stopped',
|
|
55
|
+
lastUrlAt: null,
|
|
56
|
+
detail: '',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let started = false
|
|
60
|
+
let stopping = false
|
|
61
|
+
let proc: ReturnType<typeof Bun.spawn> | null = null
|
|
62
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null
|
|
63
|
+
let consecutiveCrashes = 0
|
|
64
|
+
|
|
65
|
+
async function launch(): Promise<void> {
|
|
66
|
+
if (!started || stopping) return
|
|
67
|
+
|
|
68
|
+
const token = resolveToken()
|
|
69
|
+
if (token === undefined || token.trim() === '') {
|
|
70
|
+
// Bad config rather than a transient process crash: the user-facing fix
|
|
71
|
+
// is editing `.env`, not waiting for backoff. Flip straight to
|
|
72
|
+
// permanently-failed so `tunnel status` makes the cause obvious and we
|
|
73
|
+
// don't waste retries spawning a cloudflared we know will reject the
|
|
74
|
+
// missing token.
|
|
75
|
+
state.status = 'permanently-failed'
|
|
76
|
+
state.detail = `env var ${tokenEnv} is unset or empty; set it in .env and restart`
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
state.status = 'starting'
|
|
81
|
+
state.detail = 'starting cloudflared'
|
|
82
|
+
const spawned = Bun.spawn([binary, 'tunnel', '--no-autoupdate', 'run', '--token', token], {
|
|
83
|
+
stdout: 'ignore',
|
|
84
|
+
stderr: 'pipe',
|
|
85
|
+
})
|
|
86
|
+
proc = spawned
|
|
87
|
+
|
|
88
|
+
// Mark healthy on the FIRST stderr line. cloudflared with a valid token
|
|
89
|
+
// prints registration progress to stderr within ~1s of start; a process
|
|
90
|
+
// that exits before printing anything is almost certainly a token/network
|
|
91
|
+
// failure. Healthy != "traffic flowing" — only Cloudflare's edge knows
|
|
92
|
+
// that — but it's the strongest signal available locally and matches the
|
|
93
|
+
// quick provider's "saw something on stderr" health model.
|
|
94
|
+
//
|
|
95
|
+
// Deliberately does NOT reset `consecutiveCrashes`. A process that prints
|
|
96
|
+
// one line of stderr then crashes is a tight crash loop (bad token,
|
|
97
|
+
// network down, cloudflared bug); the counter must trip the cap. The
|
|
98
|
+
// counter resets on operator action (`stop()` then `start()` again) or
|
|
99
|
+
// on `typeclaw restart`, not on stderr noise.
|
|
100
|
+
let sawFirstLine = false
|
|
101
|
+
void pumpStderr(spawned.stderr, logs, () => {
|
|
102
|
+
if (sawFirstLine) return
|
|
103
|
+
sawFirstLine = true
|
|
104
|
+
state.status = 'healthy'
|
|
105
|
+
state.detail = 'cloudflared started'
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
void spawned.exited.then((code) => {
|
|
109
|
+
if (proc !== spawned) return
|
|
110
|
+
proc = null
|
|
111
|
+
if (!started || stopping) return
|
|
112
|
+
handleExit(code)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleExit(code: number): void {
|
|
117
|
+
consecutiveCrashes += 1
|
|
118
|
+
if (consecutiveCrashes >= maxConsecutiveCrashes) {
|
|
119
|
+
state.status = 'permanently-failed'
|
|
120
|
+
state.detail = `cloudflared exited ${code}; retry cap reached after ${consecutiveCrashes} consecutive crashes`
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
state.status = 'unhealthy'
|
|
125
|
+
state.detail = `cloudflared exited ${code}; restarting`
|
|
126
|
+
const delay = restartBackoffMs[Math.min(consecutiveCrashes - 1, restartBackoffMs.length - 1)] ?? 30_000
|
|
127
|
+
retryTimer = setTimeout(() => {
|
|
128
|
+
retryTimer = null
|
|
129
|
+
void launch()
|
|
130
|
+
}, delay)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
async start(): Promise<void> {
|
|
135
|
+
if (started) return
|
|
136
|
+
started = true
|
|
137
|
+
stopping = false
|
|
138
|
+
consecutiveCrashes = 0
|
|
139
|
+
// The URL is known from config, not from cloudflared. Emit it
|
|
140
|
+
// synchronously so subscribers (channel adapters, tunnel-bridge) wire
|
|
141
|
+
// up immediately, regardless of whether cloudflared comes up healthy.
|
|
142
|
+
// For named tunnels, the URL is bound to the dashboard config — even
|
|
143
|
+
// if the local process is unhealthy, the hostname is the right value
|
|
144
|
+
// to surface in `tunnel-url-changed` events.
|
|
145
|
+
state.url = hostname
|
|
146
|
+
state.lastUrlAt = Date.now()
|
|
147
|
+
onUrlChange(hostname)
|
|
148
|
+
await launch()
|
|
149
|
+
},
|
|
150
|
+
async stop(): Promise<void> {
|
|
151
|
+
if (!started && proc === null) return
|
|
152
|
+
started = false
|
|
153
|
+
stopping = true
|
|
154
|
+
if (retryTimer !== null) {
|
|
155
|
+
clearTimeout(retryTimer)
|
|
156
|
+
retryTimer = null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const running = proc
|
|
160
|
+
proc = null
|
|
161
|
+
if (running !== null) {
|
|
162
|
+
running.kill('SIGTERM')
|
|
163
|
+
await Promise.race([
|
|
164
|
+
running.exited,
|
|
165
|
+
sleep(stopGraceMs).then(() => {
|
|
166
|
+
running.kill('SIGKILL')
|
|
167
|
+
return running.exited
|
|
168
|
+
}),
|
|
169
|
+
])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
stopping = false
|
|
173
|
+
state.status = 'stopped'
|
|
174
|
+
state.detail = ''
|
|
175
|
+
},
|
|
176
|
+
snapshot(): TunnelState {
|
|
177
|
+
return { ...state }
|
|
178
|
+
},
|
|
179
|
+
tail(): string[] {
|
|
180
|
+
return logs.snapshot()
|
|
181
|
+
},
|
|
182
|
+
subscribeToLogs(cb: LogLineSubscriber): Unsubscribe {
|
|
183
|
+
return logs.subscribe(cb)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function pumpStderr(
|
|
189
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
190
|
+
logs: LogRing,
|
|
191
|
+
onLine: (line: string) => void,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
if (stream === null) return
|
|
194
|
+
const reader = stream.getReader()
|
|
195
|
+
const decoder = new TextDecoder()
|
|
196
|
+
let buffered = ''
|
|
197
|
+
try {
|
|
198
|
+
while (true) {
|
|
199
|
+
const { done, value } = await reader.read()
|
|
200
|
+
if (done) break
|
|
201
|
+
buffered += decoder.decode(value, { stream: true })
|
|
202
|
+
let newlineIndex = buffered.indexOf('\n')
|
|
203
|
+
while (newlineIndex !== -1) {
|
|
204
|
+
const line = buffered.slice(0, newlineIndex).replace(/\r$/, '')
|
|
205
|
+
logs.append(line)
|
|
206
|
+
onLine(line)
|
|
207
|
+
buffered = buffered.slice(newlineIndex + 1)
|
|
208
|
+
newlineIndex = buffered.indexOf('\n')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
buffered += decoder.decode()
|
|
212
|
+
if (buffered !== '') {
|
|
213
|
+
const line = buffered.replace(/\r$/, '')
|
|
214
|
+
logs.append(line)
|
|
215
|
+
onLine(line)
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
reader.releaseLock()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sleep(ms: number): Promise<void> {
|
|
223
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
224
|
+
}
|
package/src/tunnels/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Unsubscribe } from '@/stream'
|
|
2
2
|
|
|
3
|
-
export type TunnelProvider = 'external' | 'cloudflare-quick'
|
|
3
|
+
export type TunnelProvider = 'external' | 'cloudflare-quick' | 'cloudflare-named'
|
|
4
4
|
|
|
5
5
|
export type TunnelFor = { kind: 'channel'; name: string } | { kind: 'manual' }
|
|
6
6
|
|
|
@@ -10,6 +10,22 @@ export type TunnelConfig = {
|
|
|
10
10
|
for: TunnelFor
|
|
11
11
|
externalUrl?: string
|
|
12
12
|
upstreamPort?: number
|
|
13
|
+
// cloudflare-named only: the public hostname configured in the Cloudflare
|
|
14
|
+
// dashboard (e.g. `https://agent.example.com`). typeclaw uses it verbatim
|
|
15
|
+
// for `tunnel-url-changed` events and CLI display; cloudflared itself
|
|
16
|
+
// learns the hostname → upstream mapping from the dashboard at runtime, so
|
|
17
|
+
// the value here must mirror what the user typed in `Public Hostname`. If
|
|
18
|
+
// the two drift, traffic stops flowing but typeclaw still reports the
|
|
19
|
+
// stale URL — there is no programmatic way to detect this without hitting
|
|
20
|
+
// Cloudflare's API, which we deliberately don't do.
|
|
21
|
+
hostname?: string
|
|
22
|
+
// cloudflare-named only: name of an env var (set in the agent's `.env`)
|
|
23
|
+
// that holds the tunnel token printed by the Cloudflare dashboard when the
|
|
24
|
+
// tunnel was created. The token itself never lives in typeclaw.json — only
|
|
25
|
+
// the env-var name does. The container reads `process.env[tokenEnv]` at
|
|
26
|
+
// tunnel start. Missing/empty values fail the start with a clear message
|
|
27
|
+
// pointing at the env var name.
|
|
28
|
+
tokenEnv?: string
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
export type TunnelStatus = 'stopped' | 'starting' | 'healthy' | 'unhealthy' | 'permanently-failed'
|
package/typeclaw.schema.json
CHANGED
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"openai/gpt-5.4-mini",
|
|
27
27
|
"openai/gpt-5.4",
|
|
28
28
|
"openai/gpt-5.5",
|
|
29
|
-
"anthropic/claude-haiku-4-5",
|
|
30
|
-
"anthropic/claude-sonnet-4-6",
|
|
31
|
-
"anthropic/claude-opus-4-7",
|
|
32
29
|
"openai-codex/gpt-5.4-mini",
|
|
33
30
|
"openai-codex/gpt-5.4",
|
|
34
31
|
"openai-codex/gpt-5.5",
|
|
32
|
+
"anthropic/claude-haiku-4-5",
|
|
33
|
+
"anthropic/claude-sonnet-4-6",
|
|
34
|
+
"anthropic/claude-opus-4-7",
|
|
35
35
|
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
36
36
|
"zai/glm-4.5-air",
|
|
37
37
|
"zai/glm-4.6",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"openai/gpt-5.4-mini",
|
|
54
54
|
"openai/gpt-5.4",
|
|
55
55
|
"openai/gpt-5.5",
|
|
56
|
-
"anthropic/claude-haiku-4-5",
|
|
57
|
-
"anthropic/claude-sonnet-4-6",
|
|
58
|
-
"anthropic/claude-opus-4-7",
|
|
59
56
|
"openai-codex/gpt-5.4-mini",
|
|
60
57
|
"openai-codex/gpt-5.4",
|
|
61
58
|
"openai-codex/gpt-5.5",
|
|
59
|
+
"anthropic/claude-haiku-4-5",
|
|
60
|
+
"anthropic/claude-sonnet-4-6",
|
|
61
|
+
"anthropic/claude-opus-4-7",
|
|
62
62
|
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
63
63
|
"zai/glm-4.5-air",
|
|
64
64
|
"zai/glm-4.6",
|
|
@@ -457,6 +457,8 @@
|
|
|
457
457
|
"discussion_comment.created",
|
|
458
458
|
"issues.opened",
|
|
459
459
|
"pull_request.opened",
|
|
460
|
+
"pull_request.review_requested",
|
|
461
|
+
"pull_request.review_request_removed",
|
|
460
462
|
"discussion.created",
|
|
461
463
|
"pull_request_review.submitted"
|
|
462
464
|
],
|
|
@@ -1186,7 +1188,8 @@
|
|
|
1186
1188
|
"type": "string",
|
|
1187
1189
|
"enum": [
|
|
1188
1190
|
"external",
|
|
1189
|
-
"cloudflare-quick"
|
|
1191
|
+
"cloudflare-quick",
|
|
1192
|
+
"cloudflare-named"
|
|
1190
1193
|
]
|
|
1191
1194
|
},
|
|
1192
1195
|
"for": {
|
|
@@ -1230,6 +1233,14 @@
|
|
|
1230
1233
|
"type": "integer",
|
|
1231
1234
|
"minimum": 1,
|
|
1232
1235
|
"maximum": 65535
|
|
1236
|
+
},
|
|
1237
|
+
"hostname": {
|
|
1238
|
+
"type": "string",
|
|
1239
|
+
"format": "uri"
|
|
1240
|
+
},
|
|
1241
|
+
"tokenEnv": {
|
|
1242
|
+
"type": "string",
|
|
1243
|
+
"pattern": "^[A-Z_][A-Z0-9_]*$"
|
|
1233
1244
|
}
|
|
1234
1245
|
},
|
|
1235
1246
|
"required": [
|
|
@@ -1278,6 +1289,7 @@
|
|
|
1278
1289
|
"idleMs": 60000,
|
|
1279
1290
|
"bufferBytes": 500000,
|
|
1280
1291
|
"injectionBudgetBytes": 16384,
|
|
1292
|
+
"minIdleDeltaLines": 3,
|
|
1281
1293
|
"spawnTimeoutMs": 50000,
|
|
1282
1294
|
"retrievalSpawnTimeoutMs": 30000
|
|
1283
1295
|
},
|
|
@@ -1301,6 +1313,12 @@
|
|
|
1301
1313
|
"minimum": 4096,
|
|
1302
1314
|
"maximum": 9007199254740991
|
|
1303
1315
|
},
|
|
1316
|
+
"minIdleDeltaLines": {
|
|
1317
|
+
"default": 3,
|
|
1318
|
+
"type": "integer",
|
|
1319
|
+
"minimum": 0,
|
|
1320
|
+
"maximum": 9007199254740991
|
|
1321
|
+
},
|
|
1304
1322
|
"spawnTimeoutMs": {
|
|
1305
1323
|
"default": 50000,
|
|
1306
1324
|
"type": "integer",
|