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.
Files changed (112) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -2
  3. package/src/agent/index.ts +31 -11
  4. package/src/agent/live-sessions.ts +12 -0
  5. package/src/agent/model-fallback.ts +17 -15
  6. package/src/agent/model-overrides.ts +2 -2
  7. package/src/agent/session-meta.ts +10 -0
  8. package/src/agent/subagents.ts +11 -2
  9. package/src/agent/system-prompt.ts +9 -3
  10. package/src/agent/todo/continuation-policy.ts +6 -3
  11. package/src/agent/todo/continuation-wiring.ts +4 -2
  12. package/src/agent/todo/continuation.ts +3 -3
  13. package/src/agent/tools/todo/index.ts +27 -4
  14. package/src/bundled-plugins/agent-browser/index.ts +33 -108
  15. package/src/bundled-plugins/agent-browser/shim.ts +3 -94
  16. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
  17. package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
  18. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
  19. package/src/bundled-plugins/memory/README.md +80 -23
  20. package/src/bundled-plugins/memory/append-tool.ts +74 -53
  21. package/src/bundled-plugins/memory/citation-superset.ts +4 -0
  22. package/src/bundled-plugins/memory/citations.ts +54 -0
  23. package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
  24. package/src/bundled-plugins/memory/dreaming.ts +444 -21
  25. package/src/bundled-plugins/memory/index.ts +544 -400
  26. package/src/bundled-plugins/memory/load-memory.ts +87 -10
  27. package/src/bundled-plugins/memory/load-shards.ts +48 -22
  28. package/src/bundled-plugins/memory/memory-logger.ts +95 -106
  29. package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
  30. package/src/bundled-plugins/memory/parent-link.ts +33 -0
  31. package/src/bundled-plugins/memory/paths.ts +12 -0
  32. package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
  33. package/src/bundled-plugins/memory/references/load-references.ts +212 -0
  34. package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
  35. package/src/bundled-plugins/memory/search-tool.ts +282 -45
  36. package/src/bundled-plugins/memory/stream-events.ts +1 -0
  37. package/src/bundled-plugins/memory/stream-io.ts +28 -3
  38. package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
  39. package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
  40. package/src/bundled-plugins/memory/vector/config.ts +28 -0
  41. package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
  42. package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
  43. package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
  44. package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
  45. package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
  46. package/src/bundled-plugins/memory/vector/passages.ts +125 -0
  47. package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
  48. package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
  49. package/src/bundled-plugins/memory/vector/startup.ts +71 -0
  50. package/src/bundled-plugins/memory/vector/store.ts +203 -0
  51. package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
  52. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
  53. package/src/channels/router.ts +239 -40
  54. package/src/cli/incomplete-init.ts +57 -0
  55. package/src/cli/init.ts +143 -12
  56. package/src/cli/inspect.ts +11 -5
  57. package/src/cli/model.ts +112 -34
  58. package/src/cli/restart.ts +24 -0
  59. package/src/cli/start.ts +24 -0
  60. package/src/cli/tunnel.ts +53 -8
  61. package/src/config/config.ts +110 -19
  62. package/src/config/index.ts +5 -1
  63. package/src/config/models-mutation.ts +29 -11
  64. package/src/config/providers-mutation.ts +2 -2
  65. package/src/config/providers.ts +146 -12
  66. package/src/container/shared.ts +9 -0
  67. package/src/container/start.ts +87 -4
  68. package/src/cron/consumer.ts +13 -7
  69. package/src/hostd/models.ts +64 -0
  70. package/src/hostd/paths.ts +6 -0
  71. package/src/hostd/portbroker-manager.ts +2 -2
  72. package/src/init/checkpoint.ts +201 -0
  73. package/src/init/dockerfile.ts +164 -51
  74. package/src/init/gitignore.ts +7 -7
  75. package/src/init/index.ts +41 -9
  76. package/src/init/line-auth.ts +50 -21
  77. package/src/init/models-dev.ts +96 -21
  78. package/src/init/oauth-login.ts +3 -3
  79. package/src/init/progress.ts +29 -0
  80. package/src/init/validate-api-key.ts +4 -0
  81. package/src/inspect/index.ts +13 -6
  82. package/src/inspect/item-list.ts +11 -2
  83. package/src/inspect/live-list.ts +65 -0
  84. package/src/inspect/open-item.ts +22 -1
  85. package/src/inspect/session-list.ts +29 -0
  86. package/src/models/embedding-model.ts +114 -0
  87. package/src/models/transformers-version.ts +55 -0
  88. package/src/plugin/types.ts +3 -0
  89. package/src/portbroker/container-server.ts +23 -0
  90. package/src/portbroker/forward-request-bus.ts +35 -0
  91. package/src/portbroker/forward-result-bus.ts +2 -3
  92. package/src/portbroker/hostd-client.ts +182 -36
  93. package/src/portbroker/index.ts +6 -1
  94. package/src/portbroker/protocol.ts +9 -2
  95. package/src/run/channel-session-factory.ts +11 -1
  96. package/src/run/index.ts +41 -7
  97. package/src/server/command-runner.ts +24 -1
  98. package/src/server/index.ts +42 -8
  99. package/src/shared/index.ts +2 -0
  100. package/src/shared/protocol.ts +31 -0
  101. package/src/skills/typeclaw-channels/SKILL.md +4 -4
  102. package/src/skills/typeclaw-config/SKILL.md +2 -2
  103. package/src/skills/typeclaw-memory/SKILL.md +3 -1
  104. package/src/skills/typeclaw-permissions/SKILL.md +3 -3
  105. package/src/skills/typeclaw-skills/SKILL.md +1 -1
  106. package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
  107. package/src/tunnels/providers/cloudflare-quick.ts +65 -7
  108. package/src/tunnels/upstream-probe.ts +25 -0
  109. package/typeclaw.schema.json +156 -67
  110. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
  111. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
  112. 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
- attemptEmittedUrl = true
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>
@@ -20,84 +20,158 @@
20
20
  "additionalProperties": {
21
21
  "anyOf": [
22
22
  {
23
- "type": "string",
24
- "enum": [
25
- "openai/gpt-5.4-nano",
26
- "openai/gpt-5.4-mini",
27
- "openai/gpt-5.4",
28
- "openai/gpt-5.5",
29
- "openai-codex/gpt-5.4-mini",
30
- "openai-codex/gpt-5.4",
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
- "anthropic/claude-opus-4-8",
36
- "fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
37
- "zai/glm-4.5-air",
38
- "zai/glm-4.6",
39
- "zai/glm-4.7",
40
- "zai-coding/glm-4.5-air",
41
- "zai-coding/glm-4.7",
42
- "zai-coding/glm-5",
43
- "zai-coding/glm-5-turbo",
44
- "zai-coding/glm-5.1",
45
- "xai/grok-4.3",
46
- "xai/grok-4.20-0309-reasoning",
47
- "xai/grok-4.20-0309-non-reasoning",
48
- "xai/grok-build-0.1",
49
- "minimax/MiniMax-M3",
50
- "minimax/MiniMax-M2.7",
51
- "minimax/MiniMax-M2.5",
52
- "minimax/MiniMax-M2.1",
53
- "minimax/MiniMax-M2",
54
- "deepseek/deepseek-v4-flash",
55
- "deepseek/deepseek-v4-pro"
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
- "type": "string",
63
- "enum": [
64
- "openai/gpt-5.4-nano",
65
- "openai/gpt-5.4-mini",
66
- "openai/gpt-5.4",
67
- "openai/gpt-5.5",
68
- "openai-codex/gpt-5.4-mini",
69
- "openai-codex/gpt-5.4",
70
- "openai-codex/gpt-5.5",
71
- "anthropic/claude-haiku-4-5",
72
- "anthropic/claude-sonnet-4-6",
73
- "anthropic/claude-opus-4-7",
74
- "anthropic/claude-opus-4-8",
75
- "fireworks/accounts/fireworks/routers/kimi-k2p6-turbo",
76
- "zai/glm-4.5-air",
77
- "zai/glm-4.6",
78
- "zai/glm-4.7",
79
- "zai-coding/glm-4.5-air",
80
- "zai-coding/glm-4.7",
81
- "zai-coding/glm-5",
82
- "zai-coding/glm-5-turbo",
83
- "zai-coding/glm-5.1",
84
- "xai/grok-4.3",
85
- "xai/grok-4.20-0309-reasoning",
86
- "xai/grok-4.20-0309-non-reasoning",
87
- "xai/grok-build-0.1",
88
- "minimax/MiniMax-M3",
89
- "minimax/MiniMax-M2.7",
90
- "minimax/MiniMax-M2.5",
91
- "minimax/MiniMax-M2.1",
92
- "minimax/MiniMax-M2",
93
- "deepseek/deepseek-v4-flash",
94
- "deepseek/deepseek-v4-pro"
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
- }