typeclaw 0.36.7 → 0.37.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/README.md +2 -2
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +11 -2
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +164 -51
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/line-auth.ts +50 -21
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +41 -7
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
|
@@ -3,12 +3,14 @@ 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 { isUpstreamReachable, type UpstreamProbe } from '../upstream-probe'
|
|
6
7
|
import { isBinaryNotFound, MISSING_BINARY_DETAIL } from './cloudflared-binary'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_BINARY = 'cloudflared'
|
|
9
10
|
const DEFAULT_RESTART_BACKOFF_MS = [1_000, 2_000, 4_000, 10_000, 30_000]
|
|
10
11
|
const DEFAULT_MAX_FAILURES_WITHOUT_URL = 10
|
|
11
12
|
const DEFAULT_STOP_GRACE_MS = 5_000
|
|
13
|
+
const DEFAULT_UPSTREAM_RECHECK_MS = 2_000
|
|
12
14
|
|
|
13
15
|
export type CloudflareQuickProviderOptions = {
|
|
14
16
|
config: TunnelConfig
|
|
@@ -18,6 +20,8 @@ export type CloudflareQuickProviderOptions = {
|
|
|
18
20
|
restartBackoffMs?: number[]
|
|
19
21
|
maxConsecutiveFailuresWithoutUrl?: number
|
|
20
22
|
stopGraceMs?: number
|
|
23
|
+
probeUpstream?: UpstreamProbe
|
|
24
|
+
upstreamRecheckMs?: number
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export type CloudflareQuickProviderHandle = TunnelProviderHandle & {
|
|
@@ -38,6 +42,8 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
38
42
|
const restartBackoffMs = options.restartBackoffMs ?? DEFAULT_RESTART_BACKOFF_MS
|
|
39
43
|
const maxConsecutiveFailuresWithoutUrl = options.maxConsecutiveFailuresWithoutUrl ?? DEFAULT_MAX_FAILURES_WITHOUT_URL
|
|
40
44
|
const stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS
|
|
45
|
+
const probeUpstream = options.probeUpstream ?? isUpstreamReachable
|
|
46
|
+
const upstreamRecheckMs = options.upstreamRecheckMs ?? DEFAULT_UPSTREAM_RECHECK_MS
|
|
41
47
|
const logs = createLogRing()
|
|
42
48
|
const state: TunnelState = {
|
|
43
49
|
name: config.name,
|
|
@@ -53,12 +59,65 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
53
59
|
let stopping = false
|
|
54
60
|
let proc: ReturnType<typeof Bun.spawn> | null = null
|
|
55
61
|
let retryTimer: ReturnType<typeof setTimeout> | null = null
|
|
62
|
+
let recheckTimer: ReturnType<typeof setTimeout> | null = null
|
|
56
63
|
let restartFailuresWithoutUrl = 0
|
|
57
64
|
let attemptEmittedUrl = false
|
|
65
|
+
let broadcastedUrl: string | null = null
|
|
66
|
+
// Identifies the current live cloudflared attempt. Bumped on every launch, on
|
|
67
|
+
// process exit, and on stop. A probe captures the generation it was started
|
|
68
|
+
// under; if the process exits (into restart backoff) or the tunnel is stopped
|
|
69
|
+
// while a probe is in flight, the resolved probe sees a stale generation and
|
|
70
|
+
// bails — so it can never mark a dead process's tunnel healthy.
|
|
71
|
+
let launchGeneration = 0
|
|
72
|
+
|
|
73
|
+
function clearRecheckTimer(): void {
|
|
74
|
+
if (recheckTimer !== null) {
|
|
75
|
+
clearTimeout(recheckTimer)
|
|
76
|
+
recheckTimer = null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// cloudflared emits the URL once, but the upstream service may still be
|
|
81
|
+
// booting. We broadcast the URL immediately (channel adapters need it) yet
|
|
82
|
+
// gate `healthy` on a real upstream probe, re-checking on an interval so the
|
|
83
|
+
// status flips to healthy the moment the service comes up — and surfaces a
|
|
84
|
+
// 502-explaining detail until then.
|
|
85
|
+
async function onQuickUrl(url: string, generation: number): Promise<void> {
|
|
86
|
+
if (generation !== launchGeneration) return
|
|
87
|
+
attemptEmittedUrl = true
|
|
88
|
+
restartFailuresWithoutUrl = 0
|
|
89
|
+
state.url = url
|
|
90
|
+
state.lastUrlAt = Date.now()
|
|
91
|
+
if (broadcastedUrl !== url) {
|
|
92
|
+
broadcastedUrl = url
|
|
93
|
+
onUrlChange(url)
|
|
94
|
+
}
|
|
95
|
+
await reprobeUpstream(generation)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function reprobeUpstream(generation: number): Promise<void> {
|
|
99
|
+
if (generation !== launchGeneration || !started || stopping || state.url === null) return
|
|
100
|
+
const reachable = await probeUpstream(upstreamPort)
|
|
101
|
+
if (generation !== launchGeneration || !started || stopping || state.url === null) return
|
|
102
|
+
if (reachable) {
|
|
103
|
+
state.status = 'healthy'
|
|
104
|
+
state.detail = 'quick tunnel URL emitted; upstream reachable'
|
|
105
|
+
clearRecheckTimer()
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
state.status = 'unhealthy'
|
|
109
|
+
state.detail = `quick tunnel URL emitted but upstream 127.0.0.1:${upstreamPort} is not reachable (requests will 502)`
|
|
110
|
+
clearRecheckTimer()
|
|
111
|
+
recheckTimer = setTimeout(() => {
|
|
112
|
+
recheckTimer = null
|
|
113
|
+
void reprobeUpstream(generation)
|
|
114
|
+
}, upstreamRecheckMs)
|
|
115
|
+
}
|
|
58
116
|
|
|
59
117
|
async function launch(): Promise<void> {
|
|
60
118
|
if (!started || stopping) return
|
|
61
119
|
|
|
120
|
+
const generation = ++launchGeneration
|
|
62
121
|
attemptEmittedUrl = false
|
|
63
122
|
state.status = 'starting'
|
|
64
123
|
state.detail = 'starting cloudflared'
|
|
@@ -81,13 +140,7 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
81
140
|
void pumpStderr(spawned.stderr, logs, (line) => {
|
|
82
141
|
const url = extractQuickTunnelUrl(line)
|
|
83
142
|
if (url === null) return
|
|
84
|
-
|
|
85
|
-
restartFailuresWithoutUrl = 0
|
|
86
|
-
state.url = url
|
|
87
|
-
state.status = 'healthy'
|
|
88
|
-
state.lastUrlAt = Date.now()
|
|
89
|
-
state.detail = 'quick tunnel URL emitted'
|
|
90
|
-
onUrlChange(url)
|
|
143
|
+
void onQuickUrl(url, generation)
|
|
91
144
|
})
|
|
92
145
|
|
|
93
146
|
void spawned.exited.then((code) => {
|
|
@@ -99,6 +152,8 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
99
152
|
}
|
|
100
153
|
|
|
101
154
|
function handleExit(code: number): void {
|
|
155
|
+
launchGeneration += 1
|
|
156
|
+
clearRecheckTimer()
|
|
102
157
|
if (!attemptEmittedUrl) restartFailuresWithoutUrl += 1
|
|
103
158
|
if (restartFailuresWithoutUrl >= maxConsecutiveFailuresWithoutUrl) {
|
|
104
159
|
state.status = 'permanently-failed'
|
|
@@ -127,6 +182,9 @@ export function createCloudflareQuickProvider(options: CloudflareQuickProviderOp
|
|
|
127
182
|
if (!started && proc === null) return
|
|
128
183
|
started = false
|
|
129
184
|
stopping = true
|
|
185
|
+
broadcastedUrl = null
|
|
186
|
+
launchGeneration += 1
|
|
187
|
+
clearRecheckTimer()
|
|
130
188
|
if (retryTimer !== null) {
|
|
131
189
|
clearTimeout(retryTimer)
|
|
132
190
|
retryTimer = null
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createConnection } from 'node:net'
|
|
2
|
+
|
|
3
|
+
// cloudflared allocates a public quick-tunnel URL even when nothing is
|
|
4
|
+
// listening upstream, so a "healthy" tunnel can still 502 every request. We
|
|
5
|
+
// probe the upstream ourselves before claiming health; refused connections,
|
|
6
|
+
// timeouts, and socket errors all count as unreachable.
|
|
7
|
+
export async function isUpstreamReachable(port: number, timeoutMs = 1_000): Promise<boolean> {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false
|
|
10
|
+
const finish = (reachable: boolean): void => {
|
|
11
|
+
if (settled) return
|
|
12
|
+
settled = true
|
|
13
|
+
socket.destroy()
|
|
14
|
+
resolve(reachable)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const socket = createConnection({ host: '127.0.0.1', port })
|
|
18
|
+
socket.setTimeout(timeoutMs)
|
|
19
|
+
socket.once('connect', () => finish(true))
|
|
20
|
+
socket.once('timeout', () => finish(false))
|
|
21
|
+
socket.once('error', () => finish(false))
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type UpstreamProbe = (port: number) => Promise<boolean>
|
package/typeclaw.schema.json
CHANGED
|
@@ -20,84 +20,158 @@
|
|
|
20
20
|
"additionalProperties": {
|
|
21
21
|
"anyOf": [
|
|
22
22
|
{
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
23
|
+
"anyOf": [
|
|
24
|
+
{
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": [
|
|
27
|
+
"openai/gpt-5.4-nano",
|
|
28
|
+
"openai/gpt-5.4-mini",
|
|
29
|
+
"openai/gpt-5.4",
|
|
30
|
+
"openai/gpt-5.5",
|
|
31
|
+
"openai-codex/gpt-5.4-mini",
|
|
32
|
+
"openai-codex/gpt-5.4",
|
|
33
|
+
"openai-codex/gpt-5.5",
|
|
34
|
+
"anthropic/claude-haiku-4-5",
|
|
35
|
+
"anthropic/claude-sonnet-4-6",
|
|
36
|
+
"anthropic/claude-opus-4-7",
|
|
37
|
+
"anthropic/claude-opus-4-8",
|
|
38
|
+
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
39
|
+
"zai/glm-4.5-air",
|
|
40
|
+
"zai/glm-4.6",
|
|
41
|
+
"zai/glm-4.7",
|
|
42
|
+
"zai-coding/glm-4.5-air",
|
|
43
|
+
"zai-coding/glm-4.7",
|
|
44
|
+
"zai-coding/glm-5",
|
|
45
|
+
"zai-coding/glm-5-turbo",
|
|
46
|
+
"zai-coding/glm-5.1",
|
|
47
|
+
"xai/grok-4.3",
|
|
48
|
+
"xai/grok-4.20-0309-reasoning",
|
|
49
|
+
"xai/grok-4.20-0309-non-reasoning",
|
|
50
|
+
"xai/grok-build-0.1",
|
|
51
|
+
"minimax/MiniMax-M3",
|
|
52
|
+
"minimax/MiniMax-M2.7",
|
|
53
|
+
"minimax/MiniMax-M2.5",
|
|
54
|
+
"minimax/MiniMax-M2.1",
|
|
55
|
+
"minimax/MiniMax-M2",
|
|
56
|
+
"deepseek/deepseek-v4-flash",
|
|
57
|
+
"deepseek/deepseek-v4-pro",
|
|
58
|
+
"moonshot/kimi-k2.7-code",
|
|
59
|
+
"moonshot/kimi-k2.6",
|
|
60
|
+
"moonshot/kimi-k2.5",
|
|
61
|
+
"moonshot-coding/kimi-for-coding"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "string"
|
|
66
|
+
}
|
|
56
67
|
]
|
|
57
68
|
},
|
|
58
69
|
{
|
|
59
70
|
"minItems": 1,
|
|
60
71
|
"type": "array",
|
|
61
72
|
"items": {
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
73
|
+
"anyOf": [
|
|
74
|
+
{
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": [
|
|
77
|
+
"openai/gpt-5.4-nano",
|
|
78
|
+
"openai/gpt-5.4-mini",
|
|
79
|
+
"openai/gpt-5.4",
|
|
80
|
+
"openai/gpt-5.5",
|
|
81
|
+
"openai-codex/gpt-5.4-mini",
|
|
82
|
+
"openai-codex/gpt-5.4",
|
|
83
|
+
"openai-codex/gpt-5.5",
|
|
84
|
+
"anthropic/claude-haiku-4-5",
|
|
85
|
+
"anthropic/claude-sonnet-4-6",
|
|
86
|
+
"anthropic/claude-opus-4-7",
|
|
87
|
+
"anthropic/claude-opus-4-8",
|
|
88
|
+
"fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
|
|
89
|
+
"zai/glm-4.5-air",
|
|
90
|
+
"zai/glm-4.6",
|
|
91
|
+
"zai/glm-4.7",
|
|
92
|
+
"zai-coding/glm-4.5-air",
|
|
93
|
+
"zai-coding/glm-4.7",
|
|
94
|
+
"zai-coding/glm-5",
|
|
95
|
+
"zai-coding/glm-5-turbo",
|
|
96
|
+
"zai-coding/glm-5.1",
|
|
97
|
+
"xai/grok-4.3",
|
|
98
|
+
"xai/grok-4.20-0309-reasoning",
|
|
99
|
+
"xai/grok-4.20-0309-non-reasoning",
|
|
100
|
+
"xai/grok-build-0.1",
|
|
101
|
+
"minimax/MiniMax-M3",
|
|
102
|
+
"minimax/MiniMax-M2.7",
|
|
103
|
+
"minimax/MiniMax-M2.5",
|
|
104
|
+
"minimax/MiniMax-M2.1",
|
|
105
|
+
"minimax/MiniMax-M2",
|
|
106
|
+
"deepseek/deepseek-v4-flash",
|
|
107
|
+
"deepseek/deepseek-v4-pro",
|
|
108
|
+
"moonshot/kimi-k2.7-code",
|
|
109
|
+
"moonshot/kimi-k2.6",
|
|
110
|
+
"moonshot/kimi-k2.5",
|
|
111
|
+
"moonshot-coding/kimi-for-coding"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"type": "string"
|
|
116
|
+
}
|
|
95
117
|
]
|
|
96
118
|
}
|
|
97
119
|
}
|
|
98
120
|
]
|
|
99
121
|
}
|
|
100
122
|
},
|
|
123
|
+
"customModels": {
|
|
124
|
+
"default": {},
|
|
125
|
+
"type": "object",
|
|
126
|
+
"propertyNames": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"minLength": 1
|
|
129
|
+
},
|
|
130
|
+
"additionalProperties": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"name": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"minLength": 1
|
|
136
|
+
},
|
|
137
|
+
"reasoning": {
|
|
138
|
+
"type": "boolean"
|
|
139
|
+
},
|
|
140
|
+
"input": {
|
|
141
|
+
"type": "array",
|
|
142
|
+
"items": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"minLength": 1
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"contextWindow": {
|
|
148
|
+
"type": "number"
|
|
149
|
+
},
|
|
150
|
+
"maxTokens": {
|
|
151
|
+
"type": "number"
|
|
152
|
+
},
|
|
153
|
+
"cost": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"input": {
|
|
157
|
+
"type": "number"
|
|
158
|
+
},
|
|
159
|
+
"output": {
|
|
160
|
+
"type": "number"
|
|
161
|
+
},
|
|
162
|
+
"cacheRead": {
|
|
163
|
+
"type": "number"
|
|
164
|
+
},
|
|
165
|
+
"cacheWrite": {
|
|
166
|
+
"type": "number"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"additionalProperties": {}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"additionalProperties": {}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
101
175
|
"mounts": {
|
|
102
176
|
"default": [],
|
|
103
177
|
"type": "array",
|
|
@@ -1632,7 +1706,10 @@
|
|
|
1632
1706
|
"injectionBudgetBytes": 16384,
|
|
1633
1707
|
"minIdleDeltaLines": 3,
|
|
1634
1708
|
"spawnTimeoutMs": 50000,
|
|
1635
|
-
"retrievalSpawnTimeoutMs": 30000
|
|
1709
|
+
"retrievalSpawnTimeoutMs": 30000,
|
|
1710
|
+
"vector": {
|
|
1711
|
+
"enabled": false
|
|
1712
|
+
}
|
|
1636
1713
|
},
|
|
1637
1714
|
"type": "object",
|
|
1638
1715
|
"properties": {
|
|
@@ -1680,6 +1757,18 @@
|
|
|
1680
1757
|
"minLength": 1
|
|
1681
1758
|
}
|
|
1682
1759
|
}
|
|
1760
|
+
},
|
|
1761
|
+
"vector": {
|
|
1762
|
+
"default": {
|
|
1763
|
+
"enabled": false
|
|
1764
|
+
},
|
|
1765
|
+
"type": "object",
|
|
1766
|
+
"properties": {
|
|
1767
|
+
"enabled": {
|
|
1768
|
+
"default": false,
|
|
1769
|
+
"type": "boolean"
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1683
1772
|
}
|
|
1684
1773
|
}
|
|
1685
1774
|
},
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// Discovers the actual port the agent-browser dashboard daemon is listening
|
|
2
|
-
// on. Necessary because the previous design hardcoded 4849 in the proxy and
|
|
3
|
-
// trusted the shim to force upstream onto that port — but the shim is bypass-
|
|
4
|
-
// able (someone runs `bunx agent-browser dashboard --port 9999`, the binary
|
|
5
|
-
// gets invoked from a path that isn't shimmed, an old container leaves a
|
|
6
|
-
// stale daemon, etc.). The proxy now consults this module to find the
|
|
7
|
-
// dashboard wherever it actually is.
|
|
8
|
-
//
|
|
9
|
-
// Two-stage discovery, fastest signal first:
|
|
10
|
-
//
|
|
11
|
-
// 1. Hint file at PORT_HINT_PATH. The shim writes the port it asked
|
|
12
|
-
// upstream to bind to (via the rewritten --port). If the file exists,
|
|
13
|
-
// points at a port, AND that port currently has a LISTEN socket we
|
|
14
|
-
// can fast-probe with HEAD /api/sessions, we use it. Zero I/O on the
|
|
15
|
-
// hot path beyond a small file read.
|
|
16
|
-
//
|
|
17
|
-
// 2. Fallback: read the dashboard's own pidfile at DASHBOARD_PID_PATH
|
|
18
|
-
// (written by upstream itself). If the PID is alive, scan
|
|
19
|
-
// /proc/<pid>/fd for socket inodes, cross-reference with /proc/net/tcp
|
|
20
|
-
// to find LISTEN sockets owned by that PID, drop the proxy's own port,
|
|
21
|
-
// probe each remaining port with HEAD /api/sessions, return the
|
|
22
|
-
// first that responds 2xx. Linux-only, which is fine — typeclaw runs
|
|
23
|
-
// in a Linux container.
|
|
24
|
-
//
|
|
25
|
-
// The fallback is what makes "agent uses other port" work when the shim
|
|
26
|
-
// doesn't catch the call. Without it, the proxy is stuck at whatever port
|
|
27
|
-
// it was configured with and silently 502s on a moved dashboard.
|
|
28
|
-
|
|
29
|
-
import { existsSync, readdirSync, readFileSync, readlinkSync } from 'node:fs'
|
|
30
|
-
|
|
31
|
-
export const PORT_HINT_PATH = '/tmp/typeclaw-agent-browser-upstream-port'
|
|
32
|
-
export const DASHBOARD_PID_PATH = '/root/.agent-browser/dashboard.pid'
|
|
33
|
-
const DEFAULT_PROBE_TIMEOUT_MS = 250
|
|
34
|
-
|
|
35
|
-
export type DiscoveryOptions = {
|
|
36
|
-
hintPath?: string
|
|
37
|
-
pidPath?: string
|
|
38
|
-
excludePort?: number
|
|
39
|
-
fetchImpl?: typeof fetch
|
|
40
|
-
probeTimeoutMs?: number
|
|
41
|
-
procfs?: ProcFs
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ProcFs = {
|
|
45
|
-
pidExists: (pid: number) => boolean
|
|
46
|
-
listenInodesForPid: (pid: number) => Set<string>
|
|
47
|
-
listenSockets: () => Array<{ port: number; inode: string }>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function discoverDashboardPort(opts: DiscoveryOptions = {}): Promise<number | null> {
|
|
51
|
-
const hintPath = opts.hintPath ?? PORT_HINT_PATH
|
|
52
|
-
const pidPath = opts.pidPath ?? DASHBOARD_PID_PATH
|
|
53
|
-
const fetcher = opts.fetchImpl ?? fetch
|
|
54
|
-
const probeTimeout = opts.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
55
|
-
const procfs = opts.procfs ?? defaultProcFs()
|
|
56
|
-
|
|
57
|
-
const hint = readPortHint(hintPath)
|
|
58
|
-
if (hint !== null && (await isDashboardPort(hint, fetcher, probeTimeout))) return hint
|
|
59
|
-
|
|
60
|
-
const pidContents = readPidFile(pidPath)
|
|
61
|
-
if (pidContents === null) return null
|
|
62
|
-
if (!procfs.pidExists(pidContents)) return null
|
|
63
|
-
|
|
64
|
-
const pidInodes = procfs.listenInodesForPid(pidContents)
|
|
65
|
-
const candidates: number[] = []
|
|
66
|
-
for (const socket of procfs.listenSockets()) {
|
|
67
|
-
if (!pidInodes.has(socket.inode)) continue
|
|
68
|
-
if (opts.excludePort !== undefined && socket.port === opts.excludePort) continue
|
|
69
|
-
candidates.push(socket.port)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const port of candidates) {
|
|
73
|
-
if (await isDashboardPort(port, fetcher, probeTimeout)) return port
|
|
74
|
-
}
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function writePortHint(port: number, hintPath: string = PORT_HINT_PATH): void {
|
|
79
|
-
Bun.write(hintPath, String(port))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readPortHint(path: string): number | null {
|
|
83
|
-
try {
|
|
84
|
-
const raw = readFileSync(path, 'utf-8').trim()
|
|
85
|
-
const port = Number(raw)
|
|
86
|
-
if (!Number.isInteger(port) || port < 1 || port > 65_535) return null
|
|
87
|
-
return port
|
|
88
|
-
} catch {
|
|
89
|
-
return null
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readPidFile(path: string): number | null {
|
|
94
|
-
try {
|
|
95
|
-
const raw = readFileSync(path, 'utf-8').trim()
|
|
96
|
-
const pid = Number(raw)
|
|
97
|
-
if (!Number.isInteger(pid) || pid < 1) return null
|
|
98
|
-
return pid
|
|
99
|
-
} catch {
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function isDashboardPort(port: number, fetcher: typeof fetch, timeoutMs: number): Promise<boolean> {
|
|
105
|
-
const ctrl = new AbortController()
|
|
106
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
107
|
-
try {
|
|
108
|
-
const res = await fetcher(`http://127.0.0.1:${port}/api/sessions`, {
|
|
109
|
-
method: 'GET',
|
|
110
|
-
signal: ctrl.signal,
|
|
111
|
-
})
|
|
112
|
-
return res.ok
|
|
113
|
-
} catch {
|
|
114
|
-
return false
|
|
115
|
-
} finally {
|
|
116
|
-
clearTimeout(timer)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function defaultProcFs(): ProcFs {
|
|
121
|
-
return {
|
|
122
|
-
pidExists: (pid) => existsSync(`/proc/${pid}`),
|
|
123
|
-
listenInodesForPid: (pid) => {
|
|
124
|
-
const inodes = new Set<string>()
|
|
125
|
-
const fdDir = `/proc/${pid}/fd`
|
|
126
|
-
let entries: string[]
|
|
127
|
-
try {
|
|
128
|
-
entries = readdirSync(fdDir)
|
|
129
|
-
} catch {
|
|
130
|
-
return inodes
|
|
131
|
-
}
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
try {
|
|
134
|
-
const target = readlinkSync(`${fdDir}/${entry}`)
|
|
135
|
-
const match = target.match(/^socket:\[(\d+)\]$/)
|
|
136
|
-
if (match) inodes.add(match[1]!)
|
|
137
|
-
} catch {
|
|
138
|
-
continue
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return inodes
|
|
142
|
-
},
|
|
143
|
-
listenSockets: () => {
|
|
144
|
-
const out: Array<{ port: number; inode: string }> = []
|
|
145
|
-
for (const file of ['/proc/net/tcp', '/proc/net/tcp6']) {
|
|
146
|
-
let raw: string
|
|
147
|
-
try {
|
|
148
|
-
raw = readFileSync(file, 'utf-8')
|
|
149
|
-
} catch {
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
const lines = raw.split('\n').slice(1)
|
|
153
|
-
for (const line of lines) {
|
|
154
|
-
const cols = line.trim().split(/\s+/)
|
|
155
|
-
if (cols.length < 10) continue
|
|
156
|
-
if (cols[3] !== '0A') continue
|
|
157
|
-
const local = cols[1] ?? ''
|
|
158
|
-
const colonIdx = local.lastIndexOf(':')
|
|
159
|
-
if (colonIdx < 0) continue
|
|
160
|
-
const port = Number.parseInt(local.slice(colonIdx + 1), 16)
|
|
161
|
-
if (!Number.isInteger(port) || port < 1 || port > 65_535) continue
|
|
162
|
-
const inode = cols[9]
|
|
163
|
-
if (inode === undefined) continue
|
|
164
|
-
out.push({ port, inode })
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return out
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
}
|