bazilion 0.0.0 → 0.1.1

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 (265) hide show
  1. package/README.md +53 -152
  2. package/dist/cli.js +2730 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/daemon.js +6460 -0
  5. package/dist/daemon.js.map +1 -0
  6. package/dist/worker.js +2176 -0
  7. package/dist/worker.js.map +1 -0
  8. package/package.json +57 -28
  9. package/.understand-anything/.understandignore +0 -25
  10. package/.understand-anything/fingerprints.json +0 -14267
  11. package/.understand-anything/knowledge-graph.json +0 -18128
  12. package/.understand-anything/meta.json +0 -6
  13. package/CLAUDE.md +0 -164
  14. package/apps/cli/package.json +0 -21
  15. package/apps/cli/src/auth-file.ts +0 -18
  16. package/apps/cli/src/client.ts +0 -37
  17. package/apps/cli/src/columnize.ts +0 -32
  18. package/apps/cli/src/commands/agent.ts +0 -574
  19. package/apps/cli/src/commands/auth.ts +0 -110
  20. package/apps/cli/src/commands/backup.ts +0 -135
  21. package/apps/cli/src/commands/completion.ts +0 -155
  22. package/apps/cli/src/commands/config.ts +0 -95
  23. package/apps/cli/src/commands/doctor.ts +0 -131
  24. package/apps/cli/src/commands/group.ts +0 -132
  25. package/apps/cli/src/commands/inbox.ts +0 -82
  26. package/apps/cli/src/commands/login.ts +0 -73
  27. package/apps/cli/src/commands/memory.ts +0 -106
  28. package/apps/cli/src/commands/profile.ts +0 -259
  29. package/apps/cli/src/commands/provider.ts +0 -170
  30. package/apps/cli/src/commands/send.ts +0 -24
  31. package/apps/cli/src/commands/serve.ts +0 -89
  32. package/apps/cli/src/commands/skill.ts +0 -120
  33. package/apps/cli/src/commands/token.ts +0 -148
  34. package/apps/cli/src/commands/trigger.ts +0 -129
  35. package/apps/cli/src/commands/uninstall.ts +0 -156
  36. package/apps/cli/src/index.ts +0 -196
  37. package/apps/cli/src/paths.ts +0 -12
  38. package/apps/cli/test/agent.test.ts +0 -213
  39. package/apps/cli/test/backup.test.ts +0 -95
  40. package/apps/cli/test/chat.test.ts +0 -539
  41. package/apps/cli/test/columnize.test.ts +0 -29
  42. package/apps/cli/test/completion.test.ts +0 -45
  43. package/apps/cli/test/config-page.test.ts +0 -151
  44. package/apps/cli/test/group.test.ts +0 -74
  45. package/apps/cli/test/helpers.ts +0 -70
  46. package/apps/cli/test/inbox-autodeliver.test.ts +0 -143
  47. package/apps/cli/test/inbox.test.ts +0 -147
  48. package/apps/cli/test/memory.test.ts +0 -69
  49. package/apps/cli/test/profile.test.ts +0 -110
  50. package/apps/cli/test/send.test.ts +0 -30
  51. package/apps/cli/test/server-fixture.ts +0 -212
  52. package/apps/cli/test/session-head.test.ts +0 -45
  53. package/apps/cli/test/skill.test.ts +0 -128
  54. package/apps/cli/test/token.test.ts +0 -109
  55. package/apps/cli/test/trigger.test.ts +0 -245
  56. package/apps/cli/tsconfig.json +0 -4
  57. package/apps/daemon/package.json +0 -31
  58. package/apps/daemon/src/app.ts +0 -41
  59. package/apps/daemon/src/core/agent/archive.ts +0 -8
  60. package/apps/daemon/src/core/agent/delete.ts +0 -30
  61. package/apps/daemon/src/core/agent/resolve.ts +0 -31
  62. package/apps/daemon/src/core/agent/spawn.ts +0 -95
  63. package/apps/daemon/src/core/agent/unarchive.ts +0 -11
  64. package/apps/daemon/src/core/availableModels.ts +0 -58
  65. package/apps/daemon/src/core/db/client.ts +0 -113
  66. package/apps/daemon/src/core/db/migrate.ts +0 -41
  67. package/apps/daemon/src/core/db/migrations/0001_init.sql +0 -179
  68. package/apps/daemon/src/core/group/delete.ts +0 -21
  69. package/apps/daemon/src/core/group/register.ts +0 -68
  70. package/apps/daemon/src/core/index.ts +0 -71
  71. package/apps/daemon/src/core/paths.ts +0 -56
  72. package/apps/daemon/src/core/profile/create.ts +0 -78
  73. package/apps/daemon/src/core/profile/delete.ts +0 -26
  74. package/apps/daemon/src/core/profile/identity.ts +0 -70
  75. package/apps/daemon/src/core/profile/load.ts +0 -45
  76. package/apps/daemon/src/core/profile/seed.ts +0 -74
  77. package/apps/daemon/src/core/profile/templates.ts +0 -60
  78. package/apps/daemon/src/core/profile/update.ts +0 -57
  79. package/apps/daemon/src/core/profile/validate.ts +0 -9
  80. package/apps/daemon/src/core/repos/agents.ts +0 -202
  81. package/apps/daemon/src/core/repos/config.ts +0 -78
  82. package/apps/daemon/src/core/repos/groups.ts +0 -55
  83. package/apps/daemon/src/core/repos/messages.ts +0 -127
  84. package/apps/daemon/src/core/repos/profiles.ts +0 -83
  85. package/apps/daemon/src/core/repos/providerModels.ts +0 -58
  86. package/apps/daemon/src/core/repos/providerState.ts +0 -37
  87. package/apps/daemon/src/core/repos/secrets.ts +0 -145
  88. package/apps/daemon/src/core/repos/skillMeta.ts +0 -49
  89. package/apps/daemon/src/core/repos/triggers.ts +0 -101
  90. package/apps/daemon/src/core/repos/webTokens.ts +0 -87
  91. package/apps/daemon/src/core/secrets.ts +0 -65
  92. package/apps/daemon/src/core/services.ts +0 -264
  93. package/apps/daemon/src/core/skills/discover.ts +0 -28
  94. package/apps/daemon/src/core/skills/import.ts +0 -136
  95. package/apps/daemon/src/core/skills/parse.ts +0 -52
  96. package/apps/daemon/src/core/skills/resolve.ts +0 -50
  97. package/apps/daemon/src/index.ts +0 -45
  98. package/apps/daemon/src/lib/agent-cancel.ts +0 -48
  99. package/apps/daemon/src/lib/agent-id.ts +0 -13
  100. package/apps/daemon/src/lib/agent-turn.ts +0 -56
  101. package/apps/daemon/src/lib/api-key.ts +0 -56
  102. package/apps/daemon/src/lib/auth.ts +0 -41
  103. package/apps/daemon/src/lib/cron.ts +0 -93
  104. package/apps/daemon/src/lib/ctx.ts +0 -80
  105. package/apps/daemon/src/lib/messaging-host.ts +0 -34
  106. package/apps/daemon/src/lib/middleware-auth.ts +0 -52
  107. package/apps/daemon/src/lib/scheduler.ts +0 -294
  108. package/apps/daemon/src/routes/agents.ts +0 -772
  109. package/apps/daemon/src/routes/auth-login.ts +0 -193
  110. package/apps/daemon/src/routes/config.ts +0 -267
  111. package/apps/daemon/src/routes/groups.ts +0 -133
  112. package/apps/daemon/src/routes/messages.ts +0 -29
  113. package/apps/daemon/src/routes/misc.ts +0 -239
  114. package/apps/daemon/src/routes/profiles.ts +0 -197
  115. package/apps/daemon/src/routes/skills.ts +0 -123
  116. package/apps/daemon/src/routes/triggers.ts +0 -29
  117. package/apps/daemon/src/runtime/auth/openai-codex.ts +0 -121
  118. package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +0 -31
  119. package/apps/daemon/src/runtime/index.ts +0 -77
  120. package/apps/daemon/src/runtime/memory/files.ts +0 -103
  121. package/apps/daemon/src/runtime/memory/qmd.ts +0 -152
  122. package/apps/daemon/src/runtime/memory/types.ts +0 -16
  123. package/apps/daemon/src/runtime/pi/events.ts +0 -173
  124. package/apps/daemon/src/runtime/pi/session.ts +0 -536
  125. package/apps/daemon/src/runtime/pi/tools.ts +0 -85
  126. package/apps/daemon/src/runtime/providers/catalog.ts +0 -145
  127. package/apps/daemon/src/runtime/providers/pi-adapter.ts +0 -272
  128. package/apps/daemon/src/runtime/providers/registry.ts +0 -374
  129. package/apps/daemon/src/runtime/providers/retry.ts +0 -176
  130. package/apps/daemon/src/runtime/providers/types.ts +0 -33
  131. package/apps/daemon/src/runtime/session/prompt.ts +0 -83
  132. package/apps/daemon/src/runtime/tools/bootstrap.ts +0 -22
  133. package/apps/daemon/src/runtime/tools/home.ts +0 -114
  134. package/apps/daemon/src/runtime/tools/memory.ts +0 -81
  135. package/apps/daemon/src/runtime/tools/messaging.ts +0 -127
  136. package/apps/daemon/src/runtime/tools/registry.ts +0 -29
  137. package/apps/daemon/src/runtime/tools/types.ts +0 -13
  138. package/apps/daemon/src/runtime/tools/web-extract.ts +0 -110
  139. package/apps/daemon/src/runtime/tools/web-ssrf.ts +0 -245
  140. package/apps/daemon/src/runtime/tools/web.ts +0 -273
  141. package/apps/daemon/src/runtime/worker/entry.ts +0 -221
  142. package/apps/daemon/src/runtime/worker/ipc-protocol.ts +0 -75
  143. package/apps/daemon/src/runtime/worker/spawn.ts +0 -249
  144. package/apps/daemon/test/core/agents.test.ts +0 -319
  145. package/apps/daemon/test/core/available-models.test.ts +0 -63
  146. package/apps/daemon/test/core/config.test.ts +0 -99
  147. package/apps/daemon/test/core/groups.test.ts +0 -63
  148. package/apps/daemon/test/core/helpers.ts +0 -50
  149. package/apps/daemon/test/core/identity.test.ts +0 -85
  150. package/apps/daemon/test/core/migrations.test.ts +0 -83
  151. package/apps/daemon/test/core/profiles.test.ts +0 -182
  152. package/apps/daemon/test/core/provider-models.test.ts +0 -57
  153. package/apps/daemon/test/core/provider-state.test.ts +0 -36
  154. package/apps/daemon/test/core/skill-meta.test.ts +0 -45
  155. package/apps/daemon/test/core/skills.test.ts +0 -271
  156. package/apps/daemon/test/core/triggers.test.ts +0 -182
  157. package/apps/daemon/test/core/web-tokens.test.ts +0 -79
  158. package/apps/daemon/test/cron.test.ts +0 -90
  159. package/apps/daemon/test/runtime/heartbeat.test.ts +0 -34
  160. package/apps/daemon/test/runtime/memory-qmd.test.ts +0 -97
  161. package/apps/daemon/test/runtime/memory.test.ts +0 -78
  162. package/apps/daemon/test/runtime/messaging.test.ts +0 -213
  163. package/apps/daemon/test/runtime/mock-server.ts +0 -65
  164. package/apps/daemon/test/runtime/openai-codex-auth.test.ts +0 -116
  165. package/apps/daemon/test/runtime/providers.test.ts +0 -306
  166. package/apps/daemon/test/runtime/retry.test.ts +0 -191
  167. package/apps/daemon/test/runtime/session-head.test.ts +0 -90
  168. package/apps/daemon/test/runtime/tools-home.test.ts +0 -102
  169. package/apps/daemon/test/runtime/tools-web.test.ts +0 -206
  170. package/apps/daemon/tsconfig.json +0 -4
  171. package/apps/mobile/README.md +0 -60
  172. package/apps/mobile/app/_layout.tsx +0 -58
  173. package/apps/mobile/app/agents/[id]/chat.tsx +0 -486
  174. package/apps/mobile/app/agents/[id]/index.tsx +0 -166
  175. package/apps/mobile/app/agents/index.tsx +0 -212
  176. package/apps/mobile/app/index.tsx +0 -21
  177. package/apps/mobile/app/pair.tsx +0 -226
  178. package/apps/mobile/app/settings.tsx +0 -419
  179. package/apps/mobile/app.json +0 -49
  180. package/apps/mobile/assets/adaptive-icon.png +0 -0
  181. package/apps/mobile/assets/favicon.png +0 -0
  182. package/apps/mobile/assets/icon.png +0 -0
  183. package/apps/mobile/assets/splash-icon.png +0 -0
  184. package/apps/mobile/babel.config.js +0 -6
  185. package/apps/mobile/metro.config.js +0 -28
  186. package/apps/mobile/package.json +0 -44
  187. package/apps/mobile/src/auth.ts +0 -66
  188. package/apps/mobile/src/pair-url.ts +0 -48
  189. package/apps/mobile/src/theme-context.tsx +0 -88
  190. package/apps/mobile/src/theme.ts +0 -135
  191. package/apps/mobile/test/pair-url.test.ts +0 -46
  192. package/apps/mobile/tsconfig.json +0 -23
  193. package/apps/web/components.json +0 -25
  194. package/apps/web/package.json +0 -39
  195. package/apps/web/public/baziu.svg +0 -8
  196. package/apps/web/src/components/AgentTabs.tsx +0 -45
  197. package/apps/web/src/components/BaziuLogo.tsx +0 -21
  198. package/apps/web/src/components/ChatPane.tsx +0 -1033
  199. package/apps/web/src/components/ConfigTabs.tsx +0 -29
  200. package/apps/web/src/components/CopyButton.tsx +0 -68
  201. package/apps/web/src/components/CreateGroupDialog.tsx +0 -127
  202. package/apps/web/src/components/FieldRow.tsx +0 -94
  203. package/apps/web/src/components/Footer.tsx +0 -10
  204. package/apps/web/src/components/PawIcon.tsx +0 -15
  205. package/apps/web/src/components/Sidebar.tsx +0 -287
  206. package/apps/web/src/components/SpawnDialog.tsx +0 -129
  207. package/apps/web/src/components/ThemeToggle.tsx +0 -75
  208. package/apps/web/src/components/TopNav.tsx +0 -34
  209. package/apps/web/src/components/ui/button.tsx +0 -67
  210. package/apps/web/src/components/ui/card.tsx +0 -103
  211. package/apps/web/src/components/ui/checkbox.tsx +0 -31
  212. package/apps/web/src/components/ui/dialog.tsx +0 -168
  213. package/apps/web/src/components/ui/input.tsx +0 -19
  214. package/apps/web/src/components/ui/label.tsx +0 -22
  215. package/apps/web/src/components/ui/radio-group.tsx +0 -44
  216. package/apps/web/src/components/ui/select.tsx +0 -192
  217. package/apps/web/src/components/ui/separator.tsx +0 -26
  218. package/apps/web/src/components/ui/table.tsx +0 -116
  219. package/apps/web/src/components/ui/tabs.tsx +0 -88
  220. package/apps/web/src/components/ui/textarea.tsx +0 -18
  221. package/apps/web/src/lib/auth.ts +0 -50
  222. package/apps/web/src/lib/daemon-client.ts +0 -34
  223. package/apps/web/src/lib/md.ts +0 -45
  224. package/apps/web/src/lib/utils.ts +0 -6
  225. package/apps/web/src/lib/wire-constants.ts +0 -27
  226. package/apps/web/src/routeTree.gen.ts +0 -408
  227. package/apps/web/src/router.tsx +0 -20
  228. package/apps/web/src/routes/__root.tsx +0 -123
  229. package/apps/web/src/routes/agents/$id/inbox.tsx +0 -207
  230. package/apps/web/src/routes/agents/$id/index.tsx +0 -527
  231. package/apps/web/src/routes/agents/$id/triggers.tsx +0 -239
  232. package/apps/web/src/routes/agents/index.tsx +0 -265
  233. package/apps/web/src/routes/api/$.ts +0 -88
  234. package/apps/web/src/routes/config/index.tsx +0 -315
  235. package/apps/web/src/routes/config/services.tsx +0 -49
  236. package/apps/web/src/routes/config/tokens.tsx +0 -192
  237. package/apps/web/src/routes/groups/$id/index.tsx +0 -153
  238. package/apps/web/src/routes/groups/$id/memory.tsx +0 -321
  239. package/apps/web/src/routes/groups/index.tsx +0 -191
  240. package/apps/web/src/routes/index.tsx +0 -133
  241. package/apps/web/src/routes/login.tsx +0 -63
  242. package/apps/web/src/routes/profiles/$id.tsx +0 -549
  243. package/apps/web/src/routes/profiles/index.tsx +0 -458
  244. package/apps/web/src/routes/skills/index.tsx +0 -297
  245. package/apps/web/src/routes/welcome.tsx +0 -61
  246. package/apps/web/src/styles.css +0 -449
  247. package/apps/web/tsconfig.json +0 -25
  248. package/apps/web/vite.config.ts +0 -25
  249. package/biome.json +0 -23
  250. package/docs/agent-engine.md +0 -219
  251. package/docs/architecture.md +0 -627
  252. package/docs/backlog/README.md +0 -42
  253. package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +0 -125
  254. package/docs/openclaw-reference.md +0 -210
  255. package/packages/api-types/package.json +0 -11
  256. package/packages/api-types/src/entities.ts +0 -146
  257. package/packages/api-types/src/events.ts +0 -55
  258. package/packages/api-types/src/index.ts +0 -488
  259. package/packages/api-types/src/memory.ts +0 -15
  260. package/packages/client/package.json +0 -13
  261. package/packages/client/src/index.ts +0 -117
  262. package/pnpm-workspace.yaml +0 -3
  263. package/tsconfig.base.json +0 -23
  264. package/tsconfig.json +0 -11
  265. package/vitest.config.ts +0 -22
@@ -1,245 +0,0 @@
1
- import { lookup as dnsLookupCb, type LookupAddress } from 'node:dns'
2
- import { lookup as dnsLookup } from 'node:dns/promises'
3
- import { Agent, type Dispatcher } from 'undici'
4
-
5
- export class SsrFBlockedError extends Error {
6
- constructor(message: string) {
7
- super(message)
8
- this.name = 'SsrFBlockedError'
9
- }
10
- }
11
-
12
- const BLOCKED_HOSTNAMES = new Set(['localhost', 'metadata.google.internal'])
13
- const PRIVATE_IPV6_PREFIXES = ['fe80:', 'fec0:', 'fc', 'fd']
14
-
15
- function normalizeHostname(hostname: string): string {
16
- let h = hostname.trim().toLowerCase().replace(/\.$/, '')
17
- if (h.startsWith('[') && h.endsWith(']')) h = h.slice(1, -1)
18
- return h
19
- }
20
-
21
- function parseIpv4(address: string): number[] | null {
22
- const parts = address.split('.')
23
- if (parts.length !== 4) return null
24
- const nums = parts.map((p) => Number.parseInt(p, 10))
25
- if (nums.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return null
26
- return nums
27
- }
28
-
29
- function isPrivateIpv4(parts: number[]): boolean {
30
- const [a, b] = parts as [number, number, number, number]
31
- if (a === 0 || a === 10 || a === 127) return true
32
- if (a === 169 && b === 254) return true
33
- if (a === 172 && b >= 16 && b <= 31) return true
34
- if (a === 192 && b === 168) return true
35
- if (a === 100 && b >= 64 && b <= 127) return true
36
- return false
37
- }
38
-
39
- export function isPrivateIpAddress(address: string): boolean {
40
- let norm = address.trim().toLowerCase()
41
- if (norm.startsWith('[') && norm.endsWith(']')) norm = norm.slice(1, -1)
42
- if (!norm) return false
43
- if (norm.startsWith('::ffff:')) {
44
- const mapped = norm.slice('::ffff:'.length)
45
- const ipv4 = parseIpv4(mapped)
46
- if (ipv4) return isPrivateIpv4(ipv4)
47
- }
48
- if (norm.includes(':')) {
49
- if (norm === '::' || norm === '::1') return true
50
- return PRIVATE_IPV6_PREFIXES.some((p) => norm.startsWith(p))
51
- }
52
- const ipv4 = parseIpv4(norm)
53
- return ipv4 ? isPrivateIpv4(ipv4) : false
54
- }
55
-
56
- export function isBlockedHostname(hostname: string): boolean {
57
- const h = normalizeHostname(hostname)
58
- if (!h) return false
59
- if (BLOCKED_HOSTNAMES.has(h)) return true
60
- return h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.internal')
61
- }
62
-
63
- type LookupCallback = (
64
- err: NodeJS.ErrnoException | null,
65
- address: string | LookupAddress[],
66
- family?: number,
67
- ) => void
68
-
69
- function createPinnedLookup(hostname: string, addresses: string[]): typeof dnsLookupCb {
70
- const normalized = normalizeHostname(hostname)
71
- const records = addresses.map((address) => ({
72
- address,
73
- family: (address.includes(':') ? 6 : 4) as 4 | 6,
74
- }))
75
- let index = 0
76
- return ((host: string, options?: unknown, callback?: unknown) => {
77
- const cb: LookupCallback =
78
- typeof options === 'function' ? (options as LookupCallback) : (callback as LookupCallback)
79
- if (!cb) return
80
- if (normalizeHostname(host) !== normalized) {
81
- if (typeof options === 'function' || options === undefined) {
82
- return (dnsLookupCb as unknown as (h: string, cb: LookupCallback) => void)(host, cb)
83
- }
84
- return (dnsLookupCb as unknown as (h: string, o: unknown, cb: LookupCallback) => void)(
85
- host,
86
- options,
87
- cb,
88
- )
89
- }
90
- const opts =
91
- typeof options === 'object' && options !== null
92
- ? (options as { all?: boolean; family?: number })
93
- : {}
94
- const family = typeof options === 'number' ? options : (opts.family ?? 0)
95
- const candidates =
96
- family === 4 || family === 6 ? records.filter((r) => r.family === family) : records
97
- const usable = candidates.length > 0 ? candidates : records
98
- if (opts.all) {
99
- cb(null, usable as LookupAddress[])
100
- return
101
- }
102
- const chosen = usable[index % usable.length]
103
- if (!chosen) return
104
- index += 1
105
- cb(null, chosen.address, chosen.family)
106
- }) as typeof dnsLookupCb
107
- }
108
-
109
- async function resolveAndCheck(hostname: string): Promise<string[]> {
110
- const norm = normalizeHostname(hostname)
111
- if (!norm) throw new SsrFBlockedError('Invalid hostname')
112
- if (isBlockedHostname(norm)) throw new SsrFBlockedError(`Blocked hostname: ${hostname}`)
113
- if (isPrivateIpAddress(norm)) throw new SsrFBlockedError('Blocked: private IP literal')
114
- const results = await dnsLookup(norm, { all: true })
115
- if (results.length === 0) throw new SsrFBlockedError(`Cannot resolve: ${hostname}`)
116
- for (const r of results) {
117
- if (isPrivateIpAddress(r.address)) throw new SsrFBlockedError('Blocked: resolves to private IP')
118
- }
119
- return Array.from(new Set(results.map((r) => r.address)))
120
- }
121
-
122
- type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>
123
-
124
- export interface GuardedFetchOptions {
125
- url: string
126
- fetchImpl?: FetchLike
127
- init?: RequestInit
128
- maxRedirects?: number
129
- timeoutMs?: number
130
- signal?: AbortSignal
131
- /** If true, skip all SSRF checks (for tests hitting 127.0.0.1 mocks). */
132
- allowPrivate?: boolean
133
- }
134
-
135
- export interface GuardedFetchResult {
136
- response: Response
137
- finalUrl: string
138
- release: () => Promise<void>
139
- }
140
-
141
- function isRedirectStatus(s: number): boolean {
142
- return s === 301 || s === 302 || s === 303 || s === 307 || s === 308
143
- }
144
-
145
- async function closeDispatcher(d: Dispatcher | null): Promise<void> {
146
- if (!d) return
147
- try {
148
- await d.close()
149
- } catch {
150
- // ignore
151
- }
152
- }
153
-
154
- export async function guardedFetch(opts: GuardedFetchOptions): Promise<GuardedFetchResult> {
155
- const fetcher: FetchLike = opts.fetchImpl ?? (globalThis.fetch as FetchLike)
156
- const maxRedirects = opts.maxRedirects ?? 3
157
- const abortController = new AbortController()
158
- const timeoutId = opts.timeoutMs
159
- ? setTimeout(() => abortController.abort(new Error('timeout')), opts.timeoutMs)
160
- : null
161
- if (opts.signal) {
162
- if (opts.signal.aborted) abortController.abort(opts.signal.reason)
163
- else
164
- opts.signal.addEventListener('abort', () => abortController.abort(opts.signal?.reason), {
165
- once: true,
166
- })
167
- }
168
-
169
- let current = opts.url
170
- const visited = new Set<string>()
171
- let redirects = 0
172
- let dispatcher: Dispatcher | null = null
173
-
174
- const release = async (): Promise<void> => {
175
- if (timeoutId) clearTimeout(timeoutId)
176
- await closeDispatcher(dispatcher)
177
- dispatcher = null
178
- }
179
-
180
- while (true) {
181
- let parsed: URL
182
- try {
183
- parsed = new URL(current)
184
- } catch {
185
- await release()
186
- throw new Error('Invalid URL')
187
- }
188
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
189
- await release()
190
- throw new Error('Invalid URL: must be http or https')
191
- }
192
-
193
- // Pin DNS only when we control the dispatcher (native fetch path).
194
- const canPin = !opts.fetchImpl
195
- if (!opts.allowPrivate) {
196
- const addrs = await resolveAndCheck(parsed.hostname)
197
- await closeDispatcher(dispatcher)
198
- dispatcher = canPin
199
- ? new Agent({ connect: { lookup: createPinnedLookup(parsed.hostname, addrs) } })
200
- : null
201
- }
202
-
203
- const init: RequestInit = {
204
- ...(opts.init ?? {}),
205
- redirect: 'manual',
206
- signal: abortController.signal,
207
- }
208
- // `dispatcher` is a Node fetch extension not reflected in the DOM
209
- // RequestInit type — and the undici Dispatcher type brand can diverge
210
- // from the one @types/node bundles. Stamp it on via a cast.
211
- if (dispatcher) (init as unknown as { dispatcher: Dispatcher }).dispatcher = dispatcher
212
-
213
- let res: Response
214
- try {
215
- res = await fetcher(parsed.toString(), init)
216
- } catch (err) {
217
- await release()
218
- throw err
219
- }
220
-
221
- if (isRedirectStatus(res.status)) {
222
- const loc = res.headers.get('location')
223
- if (!loc) {
224
- await release()
225
- throw new Error(`Redirect ${res.status} missing Location header`)
226
- }
227
- redirects += 1
228
- if (redirects > maxRedirects) {
229
- await release()
230
- throw new Error(`Too many redirects (> ${maxRedirects})`)
231
- }
232
- const next = new URL(loc, parsed).toString()
233
- if (visited.has(next)) {
234
- await release()
235
- throw new Error('Redirect loop')
236
- }
237
- visited.add(next)
238
- void res.body?.cancel()
239
- current = next
240
- continue
241
- }
242
-
243
- return { response: res, finalUrl: parsed.toString(), release }
244
- }
245
- }
@@ -1,273 +0,0 @@
1
- import type { ToolHandler } from './types.ts'
2
- import { type ExtractMode, type ExtractResult, extractReadable } from './web-extract.ts'
3
- import { guardedFetch, SsrFBlockedError } from './web-ssrf.ts'
4
-
5
- const DEFAULT_USER_AGENT =
6
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
7
- const DEFAULT_CACHE_TTL_MS = 15 * 60_000
8
- const DEFAULT_CACHE_MAX = 100
9
- const DEFAULT_MAX_LENGTH = 20_000
10
- const DEFAULT_TIMEOUT_MS = 20_000
11
-
12
- interface SearchResult {
13
- title: string
14
- url: string
15
- snippet: string
16
- }
17
-
18
- function stripHtml(s: string): string {
19
- return s
20
- .replace(/<[^>]*>/g, '')
21
- .replace(/&amp;/g, '&')
22
- .replace(/&lt;/g, '<')
23
- .replace(/&gt;/g, '>')
24
- .replace(/&quot;/g, '"')
25
- .replace(/&#39;/g, "'")
26
- .replace(/&nbsp;/g, ' ')
27
- .trim()
28
- }
29
-
30
- // --- search backends ---
31
-
32
- async function braveSearch(
33
- query: string,
34
- limit: number,
35
- apiKey: string,
36
- fetchFn: typeof fetch,
37
- ): Promise<SearchResult[]> {
38
- const params = new URLSearchParams({ q: query, count: String(limit) })
39
- const res = await fetchFn(`https://api.search.brave.com/res/v1/web/search?${params}`, {
40
- headers: {
41
- accept: 'application/json',
42
- 'accept-encoding': 'gzip',
43
- 'x-subscription-token': apiKey,
44
- },
45
- })
46
- if (!res.ok) throw new Error(`Brave Search: ${res.status} ${await res.text()}`)
47
- const data = (await res.json()) as {
48
- web?: { results?: { title?: string; url?: string; description?: string }[] }
49
- }
50
- return (data.web?.results ?? []).slice(0, limit).map((r) => ({
51
- title: r.title ?? '',
52
- url: r.url ?? '',
53
- snippet: r.description ? stripHtml(r.description) : '',
54
- }))
55
- }
56
-
57
- async function searxngSearch(
58
- query: string,
59
- limit: number,
60
- baseURL: string,
61
- fetchFn: typeof fetch,
62
- ): Promise<SearchResult[]> {
63
- const params = new URLSearchParams({ q: query, format: 'json' })
64
- const res = await fetchFn(`${baseURL}/search?${params}`, {
65
- headers: { accept: 'application/json' },
66
- })
67
- if (!res.ok) throw new Error(`SearXNG: ${res.status} ${await res.text()}`)
68
- const data = (await res.json()) as {
69
- results?: { title?: string; url?: string; content?: string }[]
70
- }
71
- return (data.results ?? []).slice(0, limit).map((r) => ({
72
- title: r.title ?? '',
73
- url: r.url ?? '',
74
- snippet: r.content ?? '',
75
- }))
76
- }
77
-
78
- // --- per-URL cache ---
79
-
80
- interface CacheEntry {
81
- value: ExtractResult
82
- expiresAt: number
83
- }
84
-
85
- function cacheGet(cache: Map<string, CacheEntry>, key: string): ExtractResult | null {
86
- const entry = cache.get(key)
87
- if (!entry) return null
88
- if (entry.expiresAt < Date.now()) {
89
- cache.delete(key)
90
- return null
91
- }
92
- // Refresh LRU position
93
- cache.delete(key)
94
- cache.set(key, entry)
95
- return entry.value
96
- }
97
-
98
- function cacheSet(
99
- cache: Map<string, CacheEntry>,
100
- key: string,
101
- value: ExtractResult,
102
- ttlMs: number,
103
- maxEntries: number,
104
- ): void {
105
- cache.set(key, { value, expiresAt: Date.now() + ttlMs })
106
- while (cache.size > maxEntries) {
107
- const oldest = cache.keys().next().value
108
- if (!oldest) break
109
- cache.delete(oldest)
110
- }
111
- }
112
-
113
- // --- tool factory ---
114
-
115
- export interface WebToolsOpts {
116
- fetchImpl?: typeof fetch
117
- env?: NodeJS.ProcessEnv
118
- /** Disable SSRF checks (tests against 127.0.0.1 mock servers). Default false. */
119
- allowPrivate?: boolean
120
- /** Cache TTL in ms. Default 15 min. */
121
- cacheTtlMs?: number
122
- /** Max cache entries. Default 100. */
123
- cacheMax?: number
124
- /** Fetch timeout in ms. Default 20s. */
125
- timeoutMs?: number
126
- }
127
-
128
- /**
129
- * web_search backends (tried in order):
130
- * 1. Brave Search (env: BRAVE_API_KEY) — free tier, 2000 req/month
131
- * 2. SearXNG (env: SEARXNG_URL) — self-hosted, unlimited
132
- * 3. Error with setup instructions if neither is configured
133
- *
134
- * web_fetch: guarded fetch (SSRF-blocked private IPs) + Readability + markdown,
135
- * with per-URL in-memory cache (15 min TTL).
136
- */
137
- export function webTools(opts?: WebToolsOpts): ToolHandler[] {
138
- const fetchFn = opts?.fetchImpl ?? fetch
139
- const env = opts?.env ?? process.env
140
- const allowPrivate = opts?.allowPrivate ?? false
141
- const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
142
- const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX
143
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS
144
- const cache = new Map<string, CacheEntry>()
145
-
146
- return [
147
- {
148
- def: {
149
- name: 'web_search',
150
- description:
151
- 'Search the internet. Returns a list of titles, URLs, and snippets. Requires BRAVE_API_KEY or SEARXNG_URL to be configured.',
152
- parameters: {
153
- type: 'object',
154
- properties: {
155
- query: { type: 'string', description: 'Search query' },
156
- limit: { type: 'number', description: 'Max results to return (default 5)' },
157
- },
158
- required: ['query'],
159
- },
160
- },
161
- async invoke(args) {
162
- const query = String(args.query ?? '')
163
- if (!query) throw new Error('web_search: query is required')
164
- const limit = typeof args.limit === 'number' ? args.limit : 5
165
-
166
- const braveKey = env.BRAVE_API_KEY
167
- if (braveKey) {
168
- const results = await braveSearch(query, limit, braveKey, fetchFn)
169
- return results.length === 0 ? 'no results' : formatResults(results)
170
- }
171
-
172
- const searxngUrl = env.SEARXNG_URL
173
- if (searxngUrl) {
174
- const results = await searxngSearch(query, limit, searxngUrl, fetchFn)
175
- return results.length === 0 ? 'no results' : formatResults(results)
176
- }
177
-
178
- throw new Error(
179
- 'web_search: no search backend configured. Set BRAVE_API_KEY (free at https://brave.com/search/api/) or SEARXNG_URL.',
180
- )
181
- },
182
- },
183
- {
184
- def: {
185
- name: 'web_fetch',
186
- description:
187
- 'Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. Results are cached for 15 minutes.',
188
- parameters: {
189
- type: 'object',
190
- properties: {
191
- url: { type: 'string', description: 'The URL to fetch (http or https)' },
192
- max_length: {
193
- type: 'number',
194
- description: 'Max characters to return (default 20000)',
195
- },
196
- extract_mode: {
197
- type: 'string',
198
- enum: ['markdown', 'text'],
199
- description: 'Output format for HTML pages. Default "markdown".',
200
- },
201
- },
202
- required: ['url'],
203
- },
204
- },
205
- async invoke(args) {
206
- const url = String(args.url ?? '')
207
- if (!url) throw new Error('web_fetch: url is required')
208
- const maxLen = typeof args.max_length === 'number' ? args.max_length : DEFAULT_MAX_LENGTH
209
- const mode: ExtractMode = args.extract_mode === 'text' ? 'text' : 'markdown'
210
- const cacheKey = `${mode}|${url}`
211
-
212
- const cached = cacheGet(cache, cacheKey)
213
- if (cached) return formatOutput(cached, maxLen)
214
-
215
- let result: GuardedFetchResultShape
216
- try {
217
- result = await guardedFetch({
218
- url,
219
- fetchImpl: opts?.fetchImpl,
220
- allowPrivate,
221
- timeoutMs,
222
- init: {
223
- headers: {
224
- 'user-agent': DEFAULT_USER_AGENT,
225
- accept: 'text/html,application/xhtml+xml,application/json,text/plain,*/*',
226
- 'accept-language': 'en-US,en;q=0.9',
227
- },
228
- },
229
- })
230
- } catch (err) {
231
- if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`)
232
- throw err
233
- }
234
-
235
- try {
236
- if (!result.response.ok) {
237
- throw new Error(`web_fetch: ${result.response.status} ${result.response.statusText}`)
238
- }
239
- const ct = result.response.headers.get('content-type') ?? ''
240
- const body = await result.response.text()
241
- let extracted: ExtractResult
242
- if (ct.includes('text/html') || ct.includes('xhtml')) {
243
- extracted = extractReadable(body, result.finalUrl, mode)
244
- } else if (ct.includes('application/json')) {
245
- try {
246
- extracted = { text: JSON.stringify(JSON.parse(body), null, 2) }
247
- } catch {
248
- extracted = { text: body }
249
- }
250
- } else {
251
- extracted = { text: body }
252
- }
253
- cacheSet(cache, cacheKey, extracted, cacheTtlMs, cacheMax)
254
- return formatOutput(extracted, maxLen)
255
- } finally {
256
- await result.release()
257
- }
258
- },
259
- },
260
- ]
261
- }
262
-
263
- type GuardedFetchResultShape = Awaited<ReturnType<typeof guardedFetch>>
264
-
265
- function formatOutput(r: ExtractResult, maxLen: number): string {
266
- const body = r.title ? `# ${r.title}\n\n${r.text}` : r.text
267
- if (body.length > maxLen) return `${body.slice(0, maxLen)}\n\n[truncated at ${maxLen} chars]`
268
- return body
269
- }
270
-
271
- function formatResults(results: SearchResult[]): string {
272
- return results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join('\n\n')
273
- }