bazilion 0.7.0 → 0.8.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 (37) hide show
  1. package/dist/cli.js +80 -17
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +143 -23
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/web/client/assets/{TemplatesTabs-Da7mg9FI.js → TemplatesTabs-UM04RMO9.js} +1 -1
  6. package/dist/web/client/assets/{_id-LT6Ejjr0.js → _id-BNJapCT4.js} +1 -1
  7. package/dist/web/client/assets/{_id-_6jIt0_-.js → _id-CjGoV8Mr.js} +1 -1
  8. package/dist/web/client/assets/{_id-BdKmo0gb.js → _id-DehuCHUw.js} +1 -1
  9. package/dist/web/client/assets/{_id-MR2XjWh0.js → _id-Dz9fjcCM.js} +1 -1
  10. package/dist/web/client/assets/{agents-0arKbVYG.js → agents-M6omAXcZ.js} +1 -1
  11. package/dist/web/client/assets/{config-BSAVT-17.js → config-DIuEc5bm.js} +1 -1
  12. package/dist/web/client/assets/{groups-BqTkhHes.js → groups-DpE2d2zZ.js} +1 -1
  13. package/dist/web/client/assets/{inbox-ozoCXzr5.js → inbox-BPnLsRNf.js} +1 -1
  14. package/dist/web/client/assets/{index-iayCEuU2.js → index-MAJuXnie.js} +2 -2
  15. package/dist/web/client/assets/{login-D8lNr2SX.js → login-CawJX16E.js} +1 -1
  16. package/dist/web/client/assets/{mcp-BLnngDsj.js → mcp-DIrh9-v_.js} +1 -1
  17. package/dist/web/client/assets/{memory-D1GQjEvN.js → memory-BpHbdKsd.js} +1 -1
  18. package/dist/web/client/assets/{profile-groups-C2yJPTaE.js → profile-groups-ByoKak0c.js} +1 -1
  19. package/dist/web/client/assets/{profiles-BV7AAZjj.js → profiles-BEjv8LGo.js} +1 -1
  20. package/dist/web/client/assets/{routes-DSHQLA7K.js → routes-D2COHy7v.js} +1 -1
  21. package/dist/web/client/assets/{services-BbCJe1Du.js → services-D212jSK_.js} +1 -1
  22. package/dist/web/client/assets/skills-Da0MURjY.js +1 -0
  23. package/dist/web/client/assets/styles-BgE5bCHA.css +2 -0
  24. package/dist/web/client/assets/{telegram-gE4j8gte.js → telegram-Czh2MdVm.js} +1 -1
  25. package/dist/web/client/assets/{tokens-D2_O9ARe.js → tokens-E_JvO5JN.js} +1 -1
  26. package/dist/web/client/assets/{triggers-WljnfEfJ.js → triggers-BBRZQOm7.js} +1 -1
  27. package/dist/web/server/assets/{_id-D0ytyXGY.js → _id-BdRiQb2H.js} +42 -5
  28. package/dist/web/server/assets/{_id-81NrHA5d.js → _id-DUIrFV3G.js} +1 -1
  29. package/dist/web/server/assets/{_tanstack-start-manifest_v-DBlFH2gg.js → _tanstack-start-manifest_v-D9FLKShQ.js} +23 -23
  30. package/dist/web/server/assets/{router-BpBZgRwn.js → router-BrDascdO.js} +3 -3
  31. package/dist/web/server/assets/{skills-dLyBZb3B.js → skills-D89ND2kR.js} +67 -7
  32. package/dist/web/server/assets/{skills-CpsyR_WA.js → skills-l2cyQ3zS.js} +1 -1
  33. package/dist/web/server/server.js +2 -2
  34. package/dist/worker.js.map +1 -1
  35. package/package.json +3 -3
  36. package/dist/web/client/assets/skills-C6czKR5I.js +0 -1
  37. package/dist/web/client/assets/styles-C06axZZk.css +0 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../daemon/src/runtime/worker/entry.ts","../../daemon/src/core/agent/delete.ts","../../daemon/src/core/agent/resolve.ts","../../daemon/src/core/profile/identity.ts","../../daemon/src/core/agent/spawn.ts","../../daemon/src/core/profile/load.ts","../../daemon/src/core/profile/seed.ts","../../daemon/src/core/group/register.ts","../../daemon/src/core/profile/create.ts","../../daemon/src/core/skills/discover.ts","../../daemon/src/core/db/client.ts","../../daemon/src/core/db/migrate.ts","../../daemon/src/core/paths.ts","../../daemon/src/core/profile/delete.ts","../../daemon/src/core/profile/update.ts","../../daemon/src/core/profile-group/spawn.ts","../../daemon/src/core/profile-group/rm-with-retry.ts","../../daemon/src/core/services.ts","../../daemon/src/core/repos/config.ts","../../daemon/src/core/repos/mcpServers.ts","../../daemon/src/core/repos/messages.ts","../../daemon/src/core/repos/secrets.ts","../../daemon/src/core/repos/triggers.ts","../../daemon/src/core/repos/webTokens.ts","../../daemon/src/core/secrets.ts","../../daemon/src/core/skills/import.ts","../../daemon/src/core/skills/parse.ts","../../daemon/src/runtime/memory/qmd.ts","../../daemon/src/runtime/pi/events.ts","../../daemon/src/runtime/pi/session.ts","../../daemon/src/runtime/providers/pi-adapter.ts","../../daemon/src/runtime/auth/openai-codex.ts","../../daemon/src/runtime/session/prompt.ts","../../daemon/src/runtime/pi/tools.ts","../../daemon/src/runtime/tools/bootstrap.ts","../../daemon/src/runtime/tools/browser.ts","../../daemon/src/runtime/tools/deliver-file.ts","../../daemon/src/runtime/tools/home.ts","../../daemon/src/runtime/tools/mcp.ts","../../daemon/src/runtime/tools/memory.ts","../../daemon/src/runtime/tools/messaging.ts","../../daemon/src/runtime/tools/user-md.ts","../../daemon/src/runtime/tools/web.ts","../../daemon/src/runtime/tools/web-extract.ts","../../daemon/src/runtime/tools/web-ssrf.ts"],"sourcesContent":["// Subprocess entrypoint for whole-run isolation — no SQLite handle inside.\n//\n// Reads a `WorkerInput` JSON object from stdin describing the turn to run:\n// the pre-resolved agent record, enabled-provider set, and message text.\n// The daemon does all DB-backed lookups before spawning us.\n//\n// Live messaging (the `send_message` / `read_inbox` / `wait_for_reply` tools)\n// is delegated back to the daemon over Node IPC: each tool invocation sends\n// a `{type: 'rpc', ...}` message and awaits the matching reply. The parent\n// dispatches the request through its own DB-backed `MessagingHost`.\n//\n// Ownership split now reads:\n// - daemon: SQLite handle, agent resolution, secrets envelope, scheduler,\n// run-cancel registry, messaging RPC dispatch.\n// - pi: conversation transcript, compaction entries, tool execution,\n// provider retries.\n// - this worker: glue between the two — runs `session.prompt(message)`,\n// translates `AgentSessionEvent`s into Bazilion `SessionEvent`s, emits\n// them as NDJSON `ChatFrame`s on stdout.\n//\n// Cancellation: SIGTERM from the parent → `session.abort()` → pi aborts the\n// current prompt → `agent_end` fires with error → translator emits an\n// `error` event so the client renders \"cancelled\" identically to other\n// failures.\n\nimport { randomUUID } from 'node:crypto'\nimport { join } from 'node:path'\nimport type { Attachment, ChatFrame, ResolvedAgent } from '@bazilion/api-types'\nimport { resolvePaths } from '../../core/index.ts'\nimport { qmdBackend } from '../memory/qmd.ts'\nimport { piMessagesToProviderView, translatePiEvent } from '../pi/events.ts'\nimport { createBazilionSession } from '../pi/session.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n IpcReply,\n IpcRequest,\n McpHost,\n MessagingHost,\n RpcMethod,\n UserMdGetResult,\n UserMdHost,\n UserMdWriteResult,\n} from './ipc-protocol.ts'\n\ninterface WorkerInput {\n agent: ResolvedAgent\n message: string\n enabledProviders: string[]\n /** Pre-fetched API key for OAuth providers; undefined for env-key ones. */\n apiKey?: string\n /** When true, expose the `browser_*` tools (proxied to the daemon pool). */\n browserEnabled?: boolean\n /** MCP tools discovered daemon-side, exposed as IPC-proxied proxy tools. */\n mcpTools?: InjectedMcpTool[]\n /** Image attachments — passed to pi's prompt (vision). Pre-classified by the daemon. */\n images?: Attachment[]\n}\n\nfunction emit(frame: ChatFrame): void {\n process.stdout.write(`${JSON.stringify(frame)}\\n`)\n}\n\nasync function readInput(): Promise<WorkerInput> {\n const chunks: Buffer[] = []\n for await (const c of process.stdin) chunks.push(c as Buffer)\n const raw = Buffer.concat(chunks).toString('utf8').trim()\n if (!raw) throw new Error('worker: empty stdin input')\n const parsed = JSON.parse(raw) as WorkerInput\n if (\n !parsed.agent ||\n typeof parsed.message !== 'string' ||\n !Array.isArray(parsed.enabledProviders)\n ) {\n throw new Error('worker: stdin must be {agent, message, enabledProviders}')\n }\n return parsed\n}\n\n/**\n * Single shared IPC client. Both `MessagingHost` and `UserMdHost` route\n * through this — the `id` correlation id disambiguates replies, so multiple\n * concurrent tool calls (e.g. read_inbox + user_md_append on the same turn)\n * each get their own promise resolved without crosstalk. The promise\n * rejects when the channel disconnects mid-request; that path only fires\n * during shutdown, where the LLM loop has already been aborted, so the\n * unresolved tool call doesn't matter.\n */\ntype IpcCall = <T>(method: RpcMethod, args: unknown) => Promise<T>\n\nfunction createIpcClient(): IpcCall {\n type Pending = { resolve: (value: unknown) => void; reject: (err: Error) => void }\n const pending = new Map<string, Pending>()\n\n process.on('message', (msg: unknown) => {\n if (!isIpcReply(msg)) return\n const entry = pending.get(msg.id)\n if (!entry) return\n pending.delete(msg.id)\n if (msg.ok) entry.resolve(msg.result)\n else entry.reject(new Error(msg.error))\n })\n\n process.on('disconnect', () => {\n for (const [, entry] of pending) entry.reject(new Error('worker IPC channel disconnected'))\n pending.clear()\n })\n\n return <T>(method: RpcMethod, args: unknown): Promise<T> => {\n if (!process.send) {\n return Promise.reject(new Error('worker: no IPC channel — daemon must spawn with stdio:ipc'))\n }\n const id = randomUUID()\n // The wire format is uniform \"method + args\"; the parent's dispatcher\n // re-narrows on `method`. TS can't follow that union construction here,\n // so we cast through unknown rather than push the discrimination through\n // every call site.\n const message = { type: 'rpc', id, method, args } as unknown as IpcRequest\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: (v) => resolve(v as T), reject })\n // process.send is fire-and-forget; the callback only signals serialization\n // failure. Treat it as a reject to surface the bug rather than hang.\n process.send?.(message, undefined, undefined, (err) => {\n if (err) {\n pending.delete(id)\n reject(err)\n }\n })\n })\n }\n}\n\nfunction createIpcMessagingHost(call: IpcCall): MessagingHost {\n return {\n agentExists: (agentId) => call<boolean>('agentExists', { agentId }),\n sendMessage: (input) => call<{ messageId: string }>('sendMessage', input),\n listInbox: (agentId, opts) => call('listInbox', { agentId, unreadOnly: opts.unreadOnly }),\n markRead: async (messageId) => {\n await call<null>('markRead', { messageId })\n },\n findReplies: (agentId, replyTo) => call('findReplies', { agentId, replyTo }),\n }\n}\n\nfunction createIpcUserMdHost(call: IpcCall): UserMdHost {\n return {\n get: (groupId) => call<UserMdGetResult>('userMdGet', { groupId }),\n write: (groupId, content, ifMatch) =>\n call<UserMdWriteResult>('userMdWrite', { groupId, content, ifMatch }),\n }\n}\n\nfunction createIpcBrowserHost(call: IpcCall): BrowserHost {\n return {\n invoke: (agentId, action, args) => call('browserInvoke', { agentId, action, args }),\n }\n}\n\nfunction createIpcMcpHost(call: IpcCall): McpHost {\n return {\n invoke: (serverId, toolName, args) => call('mcpInvoke', { serverId, toolName, args }),\n }\n}\n\nfunction isIpcReply(msg: unknown): msg is IpcReply {\n if (!msg || typeof msg !== 'object') return false\n const m = msg as Record<string, unknown>\n return m.type === 'rpc-reply' && typeof m.id === 'string' && typeof m.ok === 'boolean'\n}\n\nasync function main(): Promise<void> {\n // Install signal handlers FIRST so a SIGTERM that arrives during setup\n // doesn't kill us with the Node default (exit-by-signal, no frame emitted).\n // Pre-session, we just exit cleanly with a synthetic `cancelled` event;\n // post-session, we delegate to `session.abort()` so pi unwinds the\n // provider fetch and surfaces its own `error` event.\n let aborted = false\n let abortSession: (() => void) | null = null\n const onSignal = (): void => {\n aborted = true\n if (abortSession) {\n abortSession()\n } else {\n try {\n emit({ kind: 'event', event: { type: 'error', error: 'cancelled' } })\n } catch {}\n process.exit(0)\n }\n }\n process.on('SIGTERM', onSignal)\n process.on('SIGINT', onSignal)\n\n const { agent, message, enabledProviders, apiKey, browserEnabled, mcpTools, images } =\n await readInput()\n\n // Path resolution still happens in the worker — `resolvePaths()` only reads\n // the `BAZILION_HOME` env var the daemon hands down. No DB, no filesystem\n // probe of the live tree.\n const paths = resolvePaths()\n\n const memory = qmdBackend(join(agent.group.path, 'memory'))\n await memory.init()\n\n const ipcCall = createIpcClient()\n const messagingHost = createIpcMessagingHost(ipcCall)\n const userMdHost = createIpcUserMdHost(ipcCall)\n const browserHost = browserEnabled ? createIpcBrowserHost(ipcCall) : undefined\n const mcpHost = mcpTools && mcpTools.length > 0 ? createIpcMcpHost(ipcCall) : undefined\n\n const { session, dispose } = await createBazilionSession({\n agent,\n paths,\n env: process.env,\n memory,\n enabledProviders: new Set(enabledProviders),\n messagingHost,\n userMdHost,\n apiKey,\n browserHost,\n mcpHost,\n mcpTools,\n // deliver_file emits a `file` event straight onto our stdout frame stream.\n fileSink: (f) => emit({ kind: 'event', event: { type: 'file', ...f } }),\n })\n\n abortSession = (): void => {\n void session.abort()\n }\n\n const unsubscribe = session.subscribe((piEvent) => {\n for (const ev of translatePiEvent(piEvent)) {\n emit({ kind: 'event', event: ev })\n }\n })\n\n try {\n // Map Bazilion image attachments to pi's ImageContent and pass them as the\n // prompt's `images` option — the model sees them via vision.\n const promptImages = (images ?? []).map((img) => ({\n type: 'image' as const,\n data: img.data,\n mimeType: img.mimeType,\n }))\n await session.prompt(message, promptImages.length > 0 ? { images: promptImages } : undefined)\n await session.agent.waitForIdle()\n\n emit({\n kind: 'done',\n messages: piMessagesToProviderView(session.agent.state.messages),\n })\n } catch (err) {\n if (!aborted) {\n emit({ kind: 'event', event: { type: 'error', error: (err as Error).message } })\n }\n emit({ kind: 'fatal', error: (err as Error).message })\n process.exitCode = 1\n } finally {\n unsubscribe()\n process.off('SIGTERM', onSignal)\n process.off('SIGINT', onSignal)\n dispose()\n // Detach from the parent's IPC channel so the worker's event loop can\n // exit. `process.on('message', …)` registered above otherwise keeps\n // the loop alive even after the turn finishes — Node treats an open\n // IPC handle as a live ref the same way it treats an open socket.\n try {\n process.disconnect?.()\n } catch {\n // already disconnected (parent closed first) — fine\n }\n }\n}\n\nmain().catch((err) => {\n try {\n emit({ kind: 'fatal', error: (err as Error)?.message ?? String(err) })\n } catch {}\n process.exit(1)\n})\n","import { existsSync, rmSync } from 'node:fs'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as agentRepo from '../repos/agents.ts'\n\nexport function deleteAgent(db: BazilionDb, id: string): void {\n const agent = agentRepo.get(db, id)\n if (!agent) throw new Error(`agent not found: ${id}`)\n const fullId = agent.id\n\n db.raw.transaction(() => {\n // messages.from_agent_id and to_agent_id reference agents(id) with no ON\n // DELETE rule, and messages.reply_to references messages(id) the same way,\n // so a naive DELETE of the agent fails if it has any mailbox history.\n // Null out inbound reply pointers to this agent's messages, then purge the\n // messages themselves, then let agentRepo.remove cascade the rest\n // (agent_skills, runs, events). agents.group_id is `ON DELETE RESTRICT`\n // from the group side, but the agent row itself goes away freely.\n db.raw.run(\n `UPDATE messages SET reply_to = NULL\n WHERE reply_to IN (SELECT id FROM messages WHERE from_agent_id = ? OR to_agent_id = ?)`,\n [fullId, fullId],\n )\n db.raw.run('DELETE FROM messages WHERE from_agent_id = ? OR to_agent_id = ?', [fullId, fullId])\n agentRepo.remove(db, fullId)\n })()\n\n if (existsSync(agent.dir)) {\n rmSync(agent.dir, { recursive: true, force: true })\n }\n}\n","import { join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { loadIdentityFromFile } from '../profile/identity.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport function resolveAgent(db: BazilionDb, paths: Paths, agentId: string): ResolvedAgent {\n // `agentRepo.get` accepts either a full UUID or an unambiguous prefix.\n const agent = agentRepo.get(db, agentId)\n if (!agent) throw new Error(`agent not found: ${agentId}`)\n\n // Parse the agent's OWN IDENTITY.md (not the profile's) so the detail page\n // can show avatar/creature. `null` when the file is missing or still holds\n // only placeholder values.\n agent.identity = loadIdentityFromFile(join(agent.dir, 'IDENTITY.md'))\n\n const profile = profileRepo.get(db, agent.profileId)\n if (!profile) {\n throw new Error(`profile not found for agent ${agentId}: ${agent.profileId}`)\n }\n\n const group = groupRepo.get(db, agent.groupId, paths)\n if (!group) {\n throw new Error(`group not found for agent ${agentId}: ${agent.groupId}`)\n }\n\n return {\n agent,\n profile,\n model: agent.modelOverride ?? profile.defaultModel,\n reasoningLevel: agent.reasoningLevel,\n group,\n skills: agentRepo.listAttachedSkills(db, agentId),\n }\n}\n","import { readFileSync } from 'node:fs'\nimport type { AgentIdentityFile } from '@bazilion/api-types'\n\nconst IDENTITY_PLACEHOLDER_VALUES = new Set([\n 'pick something you like',\n 'ai? robot? familiar? ghost in the machine? something weirder?',\n 'how do you come across? sharp? warm? chaotic? calm?',\n 'your signature - pick one that feels right',\n 'workspace-relative path, http(s) url, or data uri',\n 'an http(s) url or data uri - optional',\n])\n\nfunction normalizeIdentityValue(value: string): string {\n let normalized = value.trim()\n normalized = normalized.replace(/^[*_]+|[*_]+$/g, '').trim()\n if (normalized.startsWith('(') && normalized.endsWith(')')) {\n normalized = normalized.slice(1, -1).trim()\n }\n normalized = normalized.replace(/[\\u2013\\u2014]/g, '-')\n normalized = normalized.replace(/\\s+/g, ' ').toLowerCase()\n return normalized\n}\n\nfunction isIdentityPlaceholder(value: string): boolean {\n return IDENTITY_PLACEHOLDER_VALUES.has(normalizeIdentityValue(value))\n}\n\nexport function parseIdentityMarkdown(content: string): AgentIdentityFile {\n const identity: AgentIdentityFile = {}\n for (const line of content.split(/\\r?\\n/)) {\n const cleaned = line.trim().replace(/^\\s*-\\s*/, '')\n const colonIndex = cleaned.indexOf(':')\n if (colonIndex === -1) continue\n const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, '').trim().toLowerCase()\n const value = cleaned\n .slice(colonIndex + 1)\n .replace(/^[*_]+|[*_]+$/g, '')\n .trim()\n if (!value) continue\n if (isIdentityPlaceholder(value)) continue\n if (label === 'name') identity.name = value\n else if (label === 'emoji') identity.emoji = value\n else if (label === 'creature') identity.creature = value\n else if (label === 'vibe') identity.vibe = value\n else if (label === 'theme') identity.theme = value\n else if (label === 'avatar') identity.avatar = value\n }\n return identity\n}\n\nexport function identityHasValues(identity: AgentIdentityFile): boolean {\n return Boolean(\n identity.name ||\n identity.emoji ||\n identity.theme ||\n identity.creature ||\n identity.vibe ||\n identity.avatar,\n )\n}\n\nexport function loadIdentityFromFile(path: string): AgentIdentityFile | null {\n let content: string\n try {\n content = readFileSync(path, 'utf8')\n } catch {\n return null\n }\n const parsed = parseIdentityMarkdown(content)\n return identityHasValues(parsed) ? parsed : null\n}\n","import { randomUUID } from 'node:crypto'\nimport { mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Agent, ReasoningLevel } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { loadProfile } from '../profile/load.ts'\nimport { DEFAULT_GROUP_ID } from '../profile/seed.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport { discoverSkills } from '../skills/discover.ts'\n\nexport interface SpawnAgentInput {\n profileId: string\n name?: string\n modelOverride?: string | null\n reasoningLevel?: ReasoningLevel\n /**\n * Group the new agent joins. One agent belongs to exactly one group. When\n * omitted, falls back to the seeded 'default' group — the same fallback\n * used everywhere we need a sensible cwd. Must refer to an existing group.\n */\n groupId?: string\n}\n\nexport function spawnAgent(db: BazilionDb, paths: Paths, input: SpawnAgentInput): Agent {\n const loaded = loadProfile(db, input.profileId)\n const id = randomUUID()\n const dir = paths.agentDir(id)\n // Agent's private home: identity files + sessions. Memory now lives at\n // the group level (`groups/<slug>/memory/`), shared by all member agents,\n // so we no longer create per-agent memory dirs here.\n mkdirSync(dir, { recursive: true })\n mkdirSync(join(dir, 'sessions'), { recursive: true })\n\n // Copy profile templates verbatim so the agent can diverge per-instance.\n // BOOTSTRAP.md is intentionally NOT personalized with the spawn slug —\n // the slug is a routing label, not necessarily the persona name. Bootstrap\n // is a pure conversation: the agent asks, the human answers, IDENTITY.md\n // is populated from that exchange.\n writeFileSync(join(dir, 'SOUL.md'), loaded.files.soul)\n writeFileSync(join(dir, 'IDENTITY.md'), loaded.files.identity)\n if (loaded.files.bootstrap !== null) {\n writeFileSync(join(dir, 'BOOTSTRAP.md'), loaded.files.bootstrap)\n }\n if (loaded.files.agents !== null) {\n writeFileSync(join(dir, 'AGENTS.md'), loaded.files.agents)\n }\n if (loaded.files.tools !== null) {\n writeFileSync(join(dir, 'TOOLS.md'), loaded.files.tools)\n }\n if (loaded.files.heartbeat !== null) {\n writeFileSync(join(dir, 'HEARTBEAT.md'), loaded.files.heartbeat)\n }\n\n const reasoningLevel: ReasoningLevel = input.reasoningLevel ?? 'medium'\n\n // Resolve group: explicit input wins; otherwise fall back to the seeded\n // 'default'. If neither exists, error out — an agent can't live without a\n // group (the FK `agents.group_id REFERENCES groups(id)` enforces it too).\n const groupId = input.groupId ?? DEFAULT_GROUP_ID\n const group = groupRepo.get(db, groupId, paths)\n if (!group) {\n throw new Error(\n `spawnAgent: group \"${groupId}\" does not exist. Pass an explicit --group or complete first-run setup first.`,\n )\n }\n\n const agentJson = {\n profileId: input.profileId,\n name: input.name ?? loaded.profile.name,\n modelOverride: input.modelOverride ?? null,\n reasoningLevel,\n groupId: group.id,\n }\n writeFileSync(join(dir, 'agent.json'), `${JSON.stringify(agentJson, null, 2)}\\n`)\n\n const agent = agentRepo.insert(db, {\n id,\n profileId: input.profileId,\n name: agentJson.name,\n modelOverride: agentJson.modelOverride,\n reasoningLevel,\n status: 'idle',\n dir,\n groupId: group.id,\n })\n\n // Skills come from the profile only — `skills_mode='all'` attaches every\n // installed skill, `'selected'` attaches the profile's default list.\n // Per-agent skill changes happen post-spawn via `agent skill add/rm`.\n const skills =\n loaded.profile.skillsMode === 'all'\n ? discoverSkills(paths).map((s) => s.name)\n : loaded.defaultSkills\n for (const s of skills) agentRepo.attachSkill(db, id, s)\n\n return agent\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { LoadedProfile } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { parseIdentityMarkdown } from './identity.ts'\n\nfunction readOptional(path: string): string | null {\n return existsSync(path) ? readFileSync(path, 'utf8') : null\n}\n\nexport function loadProfile(db: BazilionDb, id: string): LoadedProfile {\n const profile = profileRepo.get(db, id)\n if (!profile) throw new Error(`profile not found: ${id}`)\n\n const soul = readFileSync(join(profile.dir, 'SOUL.md'), 'utf8')\n const identityRaw = readFileSync(join(profile.dir, 'IDENTITY.md'), 'utf8')\n const bootstrap = readOptional(join(profile.dir, 'BOOTSTRAP.md'))\n const agents = readOptional(join(profile.dir, 'AGENTS.md'))\n const tools = readOptional(join(profile.dir, 'TOOLS.md'))\n const heartbeat = readOptional(join(profile.dir, 'HEARTBEAT.md'))\n\n const parsedIdentity = parseIdentityMarkdown(identityRaw)\n const anyIdentity =\n parsedIdentity.name ||\n parsedIdentity.emoji ||\n parsedIdentity.theme ||\n parsedIdentity.creature ||\n parsedIdentity.vibe ||\n parsedIdentity.avatar\n\n return {\n profile,\n defaultSkills: profileRepo.getDefaultSkills(db, id),\n files: {\n soul,\n identity: identityRaw,\n bootstrap,\n agents,\n tools,\n heartbeat,\n },\n identity: anyIdentity ? parsedIdentity : null,\n }\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Group, Profile } from '@bazilion/api-types'\nimport { isSetupComplete, listAvailableModels } from '../availableModels.ts'\nimport type { BazilionDb } from '../db/client.ts'\nimport { registerGroup } from '../group/register.ts'\nimport type { Paths } from '../paths.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { createProfile } from './create.ts'\nimport {\n DEFAULT_AGENTS,\n DEFAULT_BOOTSTRAP,\n DEFAULT_IDENTITY,\n DEFAULT_SOUL,\n DEFAULT_TOOLS,\n} from './templates.ts'\n\nexport const DEFAULT_PROFILE_ID = 'default'\nexport const DEFAULT_GROUP_ID = 'default'\n\n// HEARTBEAT.md is intentionally absent — it's opt-in, so the\n// managed default profile doesn't ship it.\nconst DEFAULT_PROFILE_FILES: [string, string][] = [\n ['SOUL.md', DEFAULT_SOUL],\n ['IDENTITY.md', DEFAULT_IDENTITY],\n ['BOOTSTRAP.md', DEFAULT_BOOTSTRAP],\n ['AGENTS.md', DEFAULT_AGENTS],\n ['TOOLS.md', DEFAULT_TOOLS],\n]\n\n/**\n * Bring the on-disk `default` profile's template files up to the current\n * defaults. The `default` profile is bazilion-managed — it always reflects\n * the shipped templates, so operators who want a customized template make\n * their own profile (which is never touched here). Pre-1.0, single-user:\n * we just overwrite rather than diffing against historical defaults.\n *\n * Write-if-differs so a steady-state boot is a no-op (and the log stays\n * quiet). No-op when the default profile isn't on disk yet — a fresh install\n * gets the current templates via seedDefaults → createProfile. Returns the\n * names of files that were (re)written.\n */\nexport function refreshDefaultProfileTemplates(paths: Paths): string[] {\n const dir = paths.profileDir(DEFAULT_PROFILE_ID)\n if (!existsSync(dir)) return []\n const written: string[] = []\n for (const [name, content] of DEFAULT_PROFILE_FILES) {\n const path = join(dir, name)\n if (!existsSync(path) || readFileSync(path, 'utf8') !== content) {\n writeFileSync(path, content)\n written.push(name)\n }\n }\n return written\n}\n\nexport interface SeedDefaultsInput {\n /** `provider:model` used as the profile's defaultModel. */\n model: string\n}\n\nexport interface SeedResult {\n profile: Profile\n group: Group\n /** true if the helper created the profile this call (vs. returned an existing one). */\n profileCreated: boolean\n /** true if the helper created the group this call. */\n groupCreated: boolean\n}\n\n/**\n * Seed the on-disk + DB defaults users land on after finishing first-run setup:\n * a `default` group at `~/.bazilion/groups/default/` and a `default` profile\n * wired to the just-enabled model. Fresh agents spawned from the default\n * profile land in the default group unless another is specified.\n *\n * Idempotent: re-seeding reuses whichever pieces already exist, so it's safe\n * to call whenever the setup state changes.\n */\nexport function seedDefaults(db: BazilionDb, paths: Paths, input: SeedDefaultsInput): SeedResult {\n let group = groupRepo.get(db, DEFAULT_GROUP_ID, paths)\n let groupCreated = false\n if (!group) {\n group = registerGroup(db, { id: DEFAULT_GROUP_ID, name: 'Default' }, paths)\n groupCreated = true\n }\n\n let profile = profileRepo.get(db, DEFAULT_PROFILE_ID)\n let profileCreated = false\n if (!profile) {\n // skillsMode='all' so freshly-spawned default agents inherit every\n // installed skill — the friendlier first-run posture. Custom profiles\n // still default to 'selected' (createProfile's own default).\n profile = createProfile(db, paths, {\n id: DEFAULT_PROFILE_ID,\n name: 'Default',\n defaultModel: input.model,\n skillsMode: 'all',\n })\n profileCreated = true\n }\n\n return { profile, group, profileCreated, groupCreated }\n}\n\n/**\n * Safe to call from any endpoint that can change setup state (toggling a\n * provider, editing model lists). No-op when either setup isn't complete\n * (nothing to seed against yet) or the default profile already exists.\n * Returns the seed result only on the cold-start transition.\n */\nexport function ensureSetupSeeded(db: BazilionDb, paths: Paths): SeedResult | null {\n if (!isSetupComplete(db)) return null\n if (profileRepo.get(db, DEFAULT_PROFILE_ID)) return null\n const first = listAvailableModels(db)[0]\n if (!first) return null\n return seedDefaults(db, paths, { model: first.value })\n}\n","// Register a group: pick a slug, materialize `~/.bazilion/groups/<slug>/`\n// (real dir or symlink), insert the row.\n//\n// Two modes:\n// - default: creates a real directory at `paths.groupDir(slug)`.\n// - `--link <target>`: creates a symlink at `paths.groupDir(slug)` →\n// `<target>`. The link target must exist and be a directory — that's\n// the \"I want my agents working on my existing project tree\" path.\n//\n// Slug = the row's id. Names are humanized labels separate from the slug;\n// callers may pass `name` explicitly or let it default to the slug.\n\nimport { existsSync, mkdirSync, statSync, symlinkSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport type { Group } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { validateSlug } from '../profile/validate.ts'\nimport * as groupRepo from '../repos/groups.ts'\n\nexport interface RegisterGroupInput {\n /** Slug. Becomes the row id AND the directory name under `groups/`. */\n id: string\n /** Human-readable label. Defaults to `id`. */\n name?: string\n /**\n * If set, materialize `paths.groupDir(id)` as a symlink to this absolute\n * path instead of as a real directory. Target must exist and be a dir.\n */\n link?: string\n}\n\nexport function registerGroup(db: BazilionDb, input: RegisterGroupInput, paths: Paths): Group {\n validateSlug(input.id)\n\n if (groupRepo.get(db, input.id, paths)) {\n throw new Error(`group already registered: ${input.id}`)\n }\n\n const slot = paths.groupDir(input.id)\n if (existsSync(slot)) {\n throw new Error(`group slot already on disk at ${slot} (move or remove it first)`)\n }\n\n // Make sure the parent `groups/` dir exists — the daemon's bootstrap creates\n // it but tests sometimes resolve a custom $BAZILION_HOME without going\n // through bootstrap.\n mkdirSync(paths.groupsDir, { recursive: true })\n\n if (input.link) {\n const target = resolve(input.link)\n if (!existsSync(target)) {\n throw new Error(`--link target does not exist: ${target}`)\n }\n if (!statSync(target).isDirectory()) {\n throw new Error(`--link target is not a directory: ${target}`)\n }\n symlinkSync(target, slot, 'dir')\n } else {\n mkdirSync(slot, { recursive: true })\n }\n\n // Memory subdir is the qmd index root for this group — created here so\n // the first tool call doesn't have to ensure it.\n mkdirSync(resolve(slot, 'memory'), { recursive: true })\n\n return groupRepo.insert(db, { id: input.id, name: input.name ?? input.id }, paths)\n}\n","import { mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Profile, SkillsMode } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport {\n DEFAULT_AGENTS,\n DEFAULT_BOOTSTRAP,\n DEFAULT_IDENTITY,\n DEFAULT_SOUL,\n DEFAULT_TOOLS,\n} from './templates.ts'\nimport { validateSlug } from './validate.ts'\n\nexport interface CreateProfileInput {\n id: string\n name?: string\n defaultModel: string\n skillsMode?: SkillsMode\n defaultSkills?: string[]\n templates?: {\n soul?: string\n identity?: string\n /** undefined = default bootstrap, null = skip bootstrap, string = override */\n bootstrap?: string | null\n /** undefined = default template, null = skip, string = override */\n agents?: string | null\n /** undefined = default template, null = skip, string = override */\n tools?: string | null\n /** opt-in (off by default): null/undefined = skip, string = seed */\n heartbeat?: string | null\n }\n}\n\nexport function createProfile(db: BazilionDb, paths: Paths, input: CreateProfileInput): Profile {\n validateSlug(input.id)\n\n const dir = paths.profileDir(input.id)\n mkdirSync(dir, { recursive: true })\n\n const soul = input.templates?.soul ?? DEFAULT_SOUL\n const identity = input.templates?.identity ?? DEFAULT_IDENTITY\n // SOUL + IDENTITY are always written. BOOTSTRAP/AGENTS/TOOLS are default-on\n // (undefined → default template, null → skip, string → override). HEARTBEAT\n // is the exception: opt-in — only written when an explicit\n // string is supplied (undefined/null → skip).\n const bootstrap =\n input.templates?.bootstrap === null ? null : (input.templates?.bootstrap ?? DEFAULT_BOOTSTRAP)\n const agents =\n input.templates?.agents === null ? null : (input.templates?.agents ?? DEFAULT_AGENTS)\n const tools = input.templates?.tools === null ? null : (input.templates?.tools ?? DEFAULT_TOOLS)\n const heartbeat =\n typeof input.templates?.heartbeat === 'string' ? input.templates.heartbeat : null\n\n writeFileSync(join(dir, 'SOUL.md'), soul)\n writeFileSync(join(dir, 'IDENTITY.md'), identity)\n if (bootstrap !== null) {\n writeFileSync(join(dir, 'BOOTSTRAP.md'), bootstrap)\n }\n if (agents !== null) {\n writeFileSync(join(dir, 'AGENTS.md'), agents)\n }\n if (tools !== null) {\n writeFileSync(join(dir, 'TOOLS.md'), tools)\n }\n if (heartbeat !== null) {\n writeFileSync(join(dir, 'HEARTBEAT.md'), heartbeat)\n }\n\n const skillsMode: SkillsMode = input.skillsMode ?? 'selected'\n const profileJson = {\n name: input.name ?? input.id,\n defaultModel: input.defaultModel,\n skillsMode,\n defaultSkills: input.defaultSkills ?? [],\n }\n writeFileSync(join(dir, 'profile.json'), `${JSON.stringify(profileJson, null, 2)}\\n`)\n\n const profile = profileRepo.insert(db, {\n id: input.id,\n name: profileJson.name,\n dir,\n defaultModel: input.defaultModel,\n skillsMode,\n })\n\n if (skillsMode === 'selected' && input.defaultSkills && input.defaultSkills.length > 0) {\n profileRepo.setDefaultSkills(db, input.id, input.defaultSkills)\n }\n\n return profile\n}\n","import { existsSync, readdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Paths } from '../paths.ts'\n\nexport interface DiscoveredSkill {\n name: string\n dir: string\n skillFile: string\n}\n\n/**\n * Walk the bazilion skill library and return every directory that contains a\n * SKILL.md file. Pure filesystem read — does not parse the markdown.\n */\nexport function discoverSkills(paths: Paths): DiscoveredSkill[] {\n if (!existsSync(paths.skillsDir)) return []\n\n const entries = readdirSync(paths.skillsDir, { withFileTypes: true })\n const skills: DiscoveredSkill[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const dir = join(paths.skillsDir, entry.name)\n const skillFile = join(dir, 'SKILL.md')\n if (!existsSync(skillFile)) continue\n skills.push({ name: entry.name, dir, skillFile })\n }\n return skills.sort((a, b) => a.name.localeCompare(b.name))\n}\n","import { DatabaseSync, type SQLInputValue } from 'node:sqlite'\n\nexport interface QueryStmt<RowType, ParamsType extends unknown[]> {\n get(...params: ParamsType): RowType | null\n all(...params: ParamsType): RowType[]\n run(...params: ParamsType): { changes: number; lastInsertRowid: number | bigint }\n}\n\nexport interface QueryableDatabase {\n query<RowType, ParamsType extends unknown[]>(sql: string): QueryStmt<RowType, ParamsType>\n run(sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint }\n exec(sql: string): void\n transaction<T>(fn: () => T): () => T\n}\n\nexport interface BazilionDb {\n raw: QueryableDatabase\n close(): void\n}\n\nfunction wrap(rawDb: DatabaseSync): QueryableDatabase {\n const cache = new Map<string, ReturnType<DatabaseSync['prepare']>>()\n function getStmt(sql: string) {\n let s = cache.get(sql)\n if (!s) {\n s = rawDb.prepare(sql)\n cache.set(sql, s)\n }\n return s\n }\n\n return {\n query<R, P extends unknown[]>(sql: string): QueryStmt<R, P> {\n const stmt = getStmt(sql)\n return {\n get(...params: P): R | null {\n const result = stmt.get(...(params as SQLInputValue[]))\n return (result as R | undefined) ?? null\n },\n all(...params: P): R[] {\n return stmt.all(...(params as SQLInputValue[])) as R[]\n },\n run(...params: P) {\n return stmt.run(...(params as SQLInputValue[])) as {\n changes: number\n lastInsertRowid: number | bigint\n }\n },\n }\n },\n run(sql, params) {\n const stmt = getStmt(sql)\n return stmt.run(...((params ?? []) as SQLInputValue[])) as {\n changes: number\n lastInsertRowid: number | bigint\n }\n },\n exec(sql) {\n rawDb.exec(sql)\n },\n // node:sqlite has no callable `transaction` wrapper; use manual BEGIN/COMMIT/ROLLBACK.\n transaction<T>(fn: () => T): () => T {\n return () => {\n rawDb.exec('BEGIN')\n try {\n const result = fn()\n rawDb.exec('COMMIT')\n return result\n } catch (err) {\n rawDb.exec('ROLLBACK')\n throw err\n }\n }\n },\n }\n}\n\nfunction applyPragmas(rawDb: DatabaseSync, includeWal: boolean): void {\n if (includeWal) {\n try {\n rawDb.exec('PRAGMA journal_mode = WAL')\n } catch {\n // some sqlite builds reject WAL on :memory: — ignore\n }\n }\n rawDb.exec('PRAGMA foreign_keys = ON')\n}\n\nexport function openDb(path: string): BazilionDb {\n const raw = new DatabaseSync(path)\n applyPragmas(raw, true)\n return {\n raw: wrap(raw),\n close() {\n raw.close()\n },\n }\n}\n\nexport function openInMemoryDb(): BazilionDb {\n const raw = new DatabaseSync(':memory:')\n applyPragmas(raw, false)\n return {\n raw: wrap(raw),\n close() {\n raw.close()\n },\n }\n}\n\nexport function inTx<T>(db: BazilionDb, fn: () => T): T {\n return db.raw.transaction(fn)()\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { BazilionDb } from './client.ts'\n\nconst migrationsDir = join(dirname(fileURLToPath(import.meta.url)), 'migrations')\n\nexport function runMigrations(db: BazilionDb): void {\n db.raw.exec(`\n CREATE TABLE IF NOT EXISTS schema_migrations (\n version TEXT PRIMARY KEY,\n applied_at INTEGER NOT NULL\n )\n `)\n\n const applied = new Set(\n db.raw\n .query<{ version: string }, []>('SELECT version FROM schema_migrations')\n .all()\n .map((r) => r.version),\n )\n\n const files = readdirSync(migrationsDir)\n .filter((f) => f.endsWith('.sql'))\n .sort()\n\n for (const file of files) {\n const version = file.replace(/\\.sql$/, '')\n if (applied.has(version)) continue\n\n const sql = readFileSync(join(migrationsDir, file), 'utf8')\n const tx = db.raw.transaction(() => {\n db.raw.exec(sql)\n db.raw.run('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)', [\n version,\n Date.now(),\n ])\n })\n tx()\n }\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface Paths {\n home: string\n db: string\n /**\n * Bootstrap auth file shared by the daemon and the CLI: `{token, remote?}`.\n * - `token` is the plaintext of the bootstrap web token. The daemon reads\n * it once at startup to derive the encryption key for the `secrets`\n * table (PBKDF2 over it) and to validate that it matches a row in\n * `web_tokens` (so a corrupted file fails loudly). The CLI reads it as\n * its loopback bearer.\n * - `remote` (set via `bazilion login`) is a CLI-only override pointing at\n * a remote daemon. The local daemon ignores this field.\n *\n * One file replaces the previous `config.json` + `secrets.enc` split:\n * encrypted secrets and plaintext config now live as DB rows.\n */\n authFile: string\n profilesDir: string\n agentsDir: string\n skillsDir: string\n groupsDir: string\n logsDir: string\n profileDir(id: string): string\n agentDir(id: string): string\n skillDir(name: string): string\n groupDir(slug: string): string\n}\n\nexport function resolvePaths(home?: string): Paths {\n const root = home ?? process.env.BAZILION_HOME ?? join(homedir(), '.bazilion')\n return {\n home: root,\n db: join(root, 'bazilion.db'),\n authFile: join(root, 'auth.json'),\n profilesDir: join(root, 'profiles'),\n agentsDir: join(root, 'agents'),\n skillsDir: join(root, 'skills'),\n groupsDir: join(root, 'groups'),\n logsDir: join(root, 'logs'),\n profileDir(id) {\n return join(root, 'profiles', id)\n },\n agentDir(id) {\n return join(root, 'agents', id)\n },\n skillDir(name) {\n return join(root, 'skills', name)\n },\n groupDir(slug) {\n return join(root, 'groups', slug)\n },\n }\n}\n","import { existsSync, rmSync } from 'node:fs'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as profileGroupRepo from '../repos/profileGroups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport function deleteProfile(db: BazilionDb, id: string): void {\n const profile = profileRepo.get(db, id)\n if (!profile) throw new Error(`profile not found: ${id}`)\n\n // Check for agents still using this profile\n const agents = agentRepo.list(db, { includeArchived: true }).filter((a) => a.profileId === id)\n if (agents.length > 0) {\n const names = agents.map((a) => `${a.name} (${a.id.slice(0, 8)})`).join(', ')\n throw new Error(\n `cannot delete profile \"${id}\": ${agents.length} agent(s) still reference it: ${names}. Delete or re-profile them first.`,\n )\n }\n\n // Check for profile groups (team templates) still referencing this profile —\n // the FK on profile_group_members.profile_id is RESTRICT, so a raw delete\n // would surface as an opaque SQLite constraint error.\n const refGroups = profileGroupRepo.findReferencingProfile(db, id)\n if (refGroups.length > 0) {\n const names = refGroups.map((g) => `${g.name} (${g.id})`).join(', ')\n throw new Error(\n `cannot delete profile \"${id}\": ${refGroups.length} profile group(s) still reference it: ${names}. Remove the member(s) first.`,\n )\n }\n\n // Remove from DB (CASCADE deletes profile_default_skills)\n profileRepo.remove(db, id)\n\n // Remove the profile directory from disk\n if (existsSync(profile.dir)) {\n rmSync(profile.dir, { recursive: true, force: true })\n }\n}\n","import { writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Profile, SkillsMode } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport interface UpdateProfileInput {\n name?: string\n defaultModel?: string\n skillsMode?: SkillsMode\n defaultSkills?: string[]\n}\n\n/**\n * Update the mutable settings in profile.json + the DB row in lockstep.\n * Profile files (SOUL/IDENTITY/BOOTSTRAP) and the memory backend are NOT\n * touched — those have their own paths.\n */\nexport function updateProfile(\n db: BazilionDb,\n paths: Paths,\n id: string,\n input: UpdateProfileInput,\n): Profile {\n const existing = profileRepo.get(db, id)\n if (!existing) throw new Error(`profile not found: ${id}`)\n\n const nextSkillsMode: SkillsMode = input.skillsMode ?? existing.skillsMode\n\n const next = {\n name: input.name ?? existing.name,\n defaultModel: input.defaultModel ?? existing.defaultModel,\n skillsMode: nextSkillsMode,\n }\n profileRepo.update(db, id, next)\n\n if (input.defaultSkills !== undefined) {\n profileRepo.setDefaultSkills(db, id, input.defaultSkills)\n }\n\n const skills = profileRepo.getDefaultSkills(db, id)\n const profileJson = {\n name: next.name,\n defaultModel: next.defaultModel,\n skillsMode: next.skillsMode,\n defaultSkills: skills,\n }\n writeFileSync(\n join(paths.profileDir(id), 'profile.json'),\n `${JSON.stringify(profileJson, null, 2)}\\n`,\n )\n\n const updated = profileRepo.get(db, id)\n if (!updated) throw new Error(`profile vanished after update: ${id}`)\n return updated\n}\n","import { readdirSync, rmSync } from 'node:fs'\nimport type { ProfileGroupMember } from '@bazilion/api-types'\nimport { spawnAgent } from '../agent/spawn.ts'\nimport type { BazilionDb } from '../db/client.ts'\nimport { inTx } from '../db/client.ts'\nimport { registerGroup } from '../group/register.ts'\nimport type { Paths } from '../paths.ts'\nimport { DEFAULT_GROUP_ID } from '../profile/seed.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileGroupRepo from '../repos/profileGroups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { rmWithRetry } from './rm-with-retry.ts'\n\nexport interface SpawnProfileGroupInput {\n profileGroupId: string\n /** Target group slug. Falls back to the default group when omitted. */\n groupSlug?: string\n /** Override the template's `userMd` for this spawn only. */\n userMd?: string\n}\n\nexport interface SpawnProfileGroupResult {\n groupSlug: string\n /** Created agents in spawn order, with their final (post-suffix) names. */\n agents: { id: string; name: string }[]\n /** Empty on success; populated only by `SpawnProfileGroupError`. */\n orphanAgentIds: string[]\n}\n\n/**\n * Thrown when the spawn loop fails. The DB transaction is rolled back by\n * the time this is constructed; `orphanAgentIds` lists any agent dirs the\n * cleanup retry helper couldn't remove (typically empty — populated when\n * the filesystem rejects the rmSync after all three retries).\n */\nexport class SpawnProfileGroupError extends Error {\n override name = 'SpawnProfileGroupError'\n orphanAgentIds: string[]\n override cause: unknown\n constructor(message: string, orphanAgentIds: string[], cause: unknown) {\n super(message)\n this.orphanAgentIds = orphanAgentIds\n this.cause = cause\n }\n}\n\n/**\n * Walk members in `position` order, resolving each `agentName` to a unique\n * final name by appending `-2`, `-3`, ... when taken. The `existing` set\n * starts with whatever agents already live in the target group; resolved\n * names are added back into it so two members that share an `agentName`\n * collide with each other as well as with pre-existing agents.\n *\n * Pure function — exported so the spawn integration test can target the\n * algorithm directly without orchestrating a full spawn.\n */\nexport function resolveMemberNames(\n existing: ReadonlySet<string>,\n members: ProfileGroupMember[],\n): string[] {\n const taken = new Set(existing)\n const out: string[] = []\n for (const m of members) {\n let candidate = m.agentName\n let n = 2\n while (taken.has(candidate)) {\n candidate = `${m.agentName}-${n}`\n n++\n }\n taken.add(candidate)\n out.push(candidate)\n }\n return out\n}\n\nexport async function spawnProfileGroup(\n db: BazilionDb,\n paths: Paths,\n input: SpawnProfileGroupInput,\n): Promise<SpawnProfileGroupResult> {\n const template = profileGroupRepo.get(db, input.profileGroupId)\n if (!template) {\n throw new Error(`profile group not found: ${input.profileGroupId}`)\n }\n const members = profileGroupRepo.members(db, input.profileGroupId)\n\n // Pre-flight: every referenced profile must still exist. Bail before any\n // side effect rather than discovering it mid-loop.\n const missing: string[] = []\n for (const m of members) {\n if (!profileRepo.get(db, m.profileId)) missing.push(m.profileId)\n }\n if (missing.length > 0) {\n throw new Error(`profile group spawn: missing profiles: ${missing.join(', ')}`)\n }\n\n const targetSlug = input.groupSlug ?? DEFAULT_GROUP_ID\n const targetGroupExists = !!groupRepo.get(db, targetSlug, paths)\n const existingNames = new Set<string>(\n targetGroupExists\n ? db.raw\n .query<{ name: string }, [string]>('SELECT name FROM agents WHERE group_id = ?')\n .all(targetSlug)\n .map((r) => r.name)\n : [],\n )\n const resolvedNames = resolveMemberNames(existingNames, members)\n\n // Snapshot dir contents so the rollback path can identify fs orphans by\n // diff regardless of whether spawnAgent crashed before or after its DB\n // insert. The agents-dir scan catches the rare mkdir-then-fail window;\n // the groups-dir scan catches a registerGroup that wrote the dir before\n // failing on the INSERT.\n const beforeAgentDirs = new Set(safeReaddir(paths.agentsDir))\n const beforeGroupDirs = new Set(safeReaddir(paths.groupsDir))\n\n let groupCreated = false\n const created: { id: string; name: string }[] = []\n try {\n inTx(db, () => {\n if (!targetGroupExists) {\n registerGroup(db, { id: targetSlug, name: targetSlug }, paths)\n groupCreated = true\n }\n // The existing groups table stores user_md as\n // NOT NULL DEFAULT '', so a pre-existing group's empty user_md is\n // operator-mediated (initial value or explicit clear). Seed only into\n // a group we just created in this same spawn — never overwrite an\n // existing row.\n const seedUserMd = input.userMd ?? template.userMd ?? null\n if (groupCreated && seedUserMd) {\n groupRepo.setUserMd(db, targetSlug, seedUserMd)\n }\n for (let i = 0; i < members.length; i++) {\n const member = members[i]\n const name = resolvedNames[i]\n if (!member || !name) continue\n const agent = spawnAgent(db, paths, {\n profileId: member.profileId,\n name,\n modelOverride: member.modelOverride,\n reasoningLevel: member.reasoningLevel ?? 'medium',\n groupId: targetSlug,\n })\n created.push({ id: agent.id, name: agent.name })\n }\n })\n return { groupSlug: targetSlug, agents: created, orphanAgentIds: [] }\n } catch (err) {\n const newAgentDirs = safeReaddir(paths.agentsDir).filter((d) => !beforeAgentDirs.has(d))\n const orphans: string[] = []\n for (const dir of newAgentDirs) {\n if (!(await rmWithRetry(paths.agentDir(dir)))) {\n orphans.push(dir)\n }\n }\n const newGroupDirs = safeReaddir(paths.groupsDir).filter((d) => !beforeGroupDirs.has(d))\n for (const slug of newGroupDirs) {\n try {\n rmSync(paths.groupDir(slug), { recursive: true, force: true })\n } catch (cleanupErr) {\n console.error(`spawnProfileGroup: failed to clean up group dir ${slug}`, cleanupErr)\n }\n }\n for (const id of orphans) {\n console.error(`spawnProfileGroup: orphan agent dir left on disk: ${paths.agentDir(id)}`)\n }\n const original = err instanceof Error ? err.message : String(err)\n const message =\n orphans.length > 0 ? `${original} (orphan agent dirs: ${orphans.join(', ')})` : original\n throw new SpawnProfileGroupError(message, orphans, err)\n }\n}\n\nfunction safeReaddir(dir: string): string[] {\n try {\n return readdirSync(dir)\n } catch {\n return []\n }\n}\n","import { rmSync } from 'node:fs'\n\nexport const DEFAULT_RM_RETRY_DELAYS_MS = [100, 500, 2000] as const\n\nexport interface RmRetryOptions {\n /** Injectable rm fn for tests. Defaults to `rmSync(target, { recursive, force })`. */\n rm?: (target: string) => void\n /** Backoff between attempts. Defaults to [100, 500, 2000]. */\n delays?: readonly number[]\n /** Injectable sleep for tests. Defaults to `setTimeout`-backed promise. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Retry filesystem removal with backoff. Returns true if the target is\n * gone after some attempt, false if every attempt failed. Used during\n * profile-group spawn rollback to clean up agent dirs created by\n * successful `spawnAgent` calls before the failing slot.\n *\n * The default rm is `rmSync(target, { recursive: true, force: true })` —\n * `force: true` swallows ENOENT, so a missing target counts as success.\n */\nexport async function rmWithRetry(target: string, opts: RmRetryOptions = {}): Promise<boolean> {\n const rm = opts.rm ?? ((p: string) => rmSync(p, { recursive: true, force: true }))\n const delays = opts.delays ?? DEFAULT_RM_RETRY_DELAYS_MS\n const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n for (let attempt = 0; attempt <= delays.length; attempt++) {\n try {\n rm(target)\n return true\n } catch {\n if (attempt === delays.length) return false\n const delay = delays[attempt] ?? 0\n await sleep(delay)\n }\n }\n return false\n}\n","// Per-service field registry — the shape of the /config page.\n//\n// Each entry describes one \"thing\" the user might configure: an LLM provider\n// (Anthropic, LM Studio, …) or an ancillary service (Brave Search, SearXNG).\n// Fields know which storage backend they live in: `secret` → the encrypted\n// `secrets` table, `config` → the plaintext `config` table. The registry is\n// the single source of truth for the UI layout and for the generic\n// field-write endpoint's dispatch.\n//\n// When adding a new provider or service, append an entry here and the\n// config page + CLI pick it up automatically.\n\nexport type FieldKind = 'secret' | 'config'\n\nexport interface ServiceField {\n /** Env var name — canonical key in both stores. */\n envVar: string\n kind: FieldKind\n label: string\n placeholder?: string\n description?: string\n}\n\nexport type ServiceCategory = 'provider' | 'service' | 'integration'\n\nexport interface ServiceDef {\n /** Matches the provider-registry key for providers (e.g. 'anthropic'). */\n id: string\n displayName: string\n category: ServiceCategory\n /** Display grouping label shown above the card on the /config tabs. */\n group?: string\n /** Sign-up link, docs, or 1-line description shown on the card. */\n hint?: string\n fields: ServiceField[]\n}\n\n/**\n * One-liner: what's shown on each service card.\n * Order here is the display order on the config page.\n */\nexport const SERVICES: ServiceDef[] = [\n // --- LLM providers (configured via API keys / URLs) ---\n // Top 3: openai-codex (ChatGPT OAuth), openai (API key), anthropic.\n // Everything else in rough popularity order; locals last.\n {\n id: 'openai-codex',\n displayName: 'OpenAI ChatGPT (OAuth)',\n category: 'provider',\n hint: 'Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)',\n // No form fields — credentials come from an OAuth flow. The /config page\n // renders a Connect/Disconnect card using /api/auth/openai instead of the\n // standard field inputs.\n fields: [],\n },\n {\n id: 'openai',\n displayName: 'OpenAI',\n category: 'provider',\n hint: 'GPT models · platform.openai.com',\n fields: [{ envVar: 'OPENAI_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-...' }],\n },\n {\n id: 'anthropic',\n displayName: 'Anthropic',\n category: 'provider',\n hint: 'Claude models · console.anthropic.com',\n fields: [\n { envVar: 'ANTHROPIC_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-ant-...' },\n {\n envVar: 'ANTHROPIC_OAUTH_TOKEN',\n kind: 'secret',\n label: 'OAuth token (alternative to API key)',\n description: 'Takes precedence over ANTHROPIC_API_KEY when set',\n },\n ],\n },\n {\n id: 'google',\n displayName: 'Google (Gemini)',\n category: 'provider',\n hint: 'Gemini models · ai.google.dev (free tier available)',\n fields: [\n { envVar: 'GEMINI_API_KEY', kind: 'secret', label: 'API key', placeholder: 'AIza...' },\n ],\n },\n {\n id: 'google-vertex',\n displayName: 'Google Vertex AI',\n category: 'provider',\n hint: 'Authenticates via `gcloud auth application-default login`',\n fields: [\n {\n envVar: 'GOOGLE_CLOUD_PROJECT',\n kind: 'config',\n label: 'GCP project ID',\n placeholder: 'my-project-123456',\n },\n {\n envVar: 'GOOGLE_CLOUD_LOCATION',\n kind: 'config',\n label: 'GCP region',\n placeholder: 'us-central1',\n },\n ],\n },\n {\n id: 'azure-openai',\n displayName: 'Azure OpenAI',\n category: 'provider',\n fields: [{ envVar: 'AZURE_OPENAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'bedrock',\n displayName: 'Amazon Bedrock',\n category: 'provider',\n hint: 'Authenticates via AWS SDK env (AWS_PROFILE or AWS_ACCESS_KEY_ID/SECRET)',\n fields: [],\n },\n {\n id: 'github-copilot',\n displayName: 'GitHub Copilot',\n category: 'provider',\n hint: 'Use a GitHub Copilot subscription to call Claude/GPT/Gemini via Copilot',\n fields: [\n {\n envVar: 'COPILOT_GITHUB_TOKEN',\n kind: 'secret',\n label: 'GitHub token',\n description:\n 'Generic GH_TOKEN/GITHUB_TOKEN are ignored — set this scoped variable explicitly (or run `bazilion auth copilot login` once available).',\n },\n ],\n },\n {\n id: 'deepseek',\n displayName: 'DeepSeek',\n category: 'provider',\n hint: 'DeepSeek V4 Flash / Pro · platform.deepseek.com',\n fields: [{ envVar: 'DEEPSEEK_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'mistral',\n displayName: 'Mistral',\n category: 'provider',\n hint: 'mistral.ai',\n fields: [{ envVar: 'MISTRAL_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xai',\n displayName: 'xAI',\n category: 'provider',\n hint: 'Grok · x.ai',\n fields: [{ envVar: 'XAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'groq',\n displayName: 'Groq',\n category: 'provider',\n hint: 'Fast inference · groq.com',\n fields: [{ envVar: 'GROQ_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'cerebras',\n displayName: 'Cerebras',\n category: 'provider',\n fields: [{ envVar: 'CEREBRAS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'fireworks',\n displayName: 'Fireworks AI',\n category: 'provider',\n hint: 'DeepSeek/GLM/Kimi via fireworks.ai',\n fields: [{ envVar: 'FIREWORKS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'together',\n displayName: 'Together AI',\n category: 'provider',\n hint: 'Open-weight models · together.ai',\n fields: [{ envVar: 'TOGETHER_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'moonshotai',\n displayName: 'Moonshot AI',\n category: 'provider',\n hint: 'Kimi K2/K2.5/K2.6/K2.7 · platform.moonshot.ai',\n fields: [{ envVar: 'MOONSHOT_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'moonshotai-cn',\n displayName: 'Moonshot AI (CN)',\n category: 'provider',\n hint: 'Kimi models via the China endpoint · platform.moonshot.cn',\n fields: [{ envVar: 'MOONSHOT_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'kimi-coding',\n displayName: 'Kimi Coding',\n category: 'provider',\n hint: 'Coding-tuned Kimi endpoint · platform.moonshot.cn',\n fields: [{ envVar: 'KIMI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'minimax',\n displayName: 'MiniMax',\n category: 'provider',\n hint: 'MiniMax M2/M3 family · platform.minimaxi.com',\n fields: [{ envVar: 'MINIMAX_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'minimax-cn',\n displayName: 'MiniMax (CN)',\n category: 'provider',\n hint: 'MiniMax M2/M3 family via the China endpoint',\n fields: [{ envVar: 'MINIMAX_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi',\n displayName: 'Xiaomi MiMo',\n category: 'provider',\n hint: 'API billing endpoint · platform.xiaomimimo.com',\n fields: [{ envVar: 'XIAOMI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-ams',\n displayName: 'Xiaomi MiMo Token Plan (AMS)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in Amsterdam',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_AMS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-cn',\n displayName: 'Xiaomi MiMo Token Plan (CN)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in China',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-sgp',\n displayName: 'Xiaomi MiMo Token Plan (SGP)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in Singapore',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_SGP_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'ant-ling',\n displayName: 'Ant Ling',\n category: 'provider',\n hint: 'Ling/Ring models · api.ant-ling.com',\n fields: [{ envVar: 'ANT_LING_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'nvidia',\n displayName: 'NVIDIA NIM',\n category: 'provider',\n hint: 'NVIDIA hosted models · build.nvidia.com',\n fields: [{ envVar: 'NVIDIA_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'zai',\n displayName: 'zAI',\n category: 'provider',\n fields: [{ envVar: 'ZAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'zai-coding-cn',\n displayName: 'zAI Coding (CN)',\n category: 'provider',\n hint: 'GLM coding endpoint · open.bigmodel.cn',\n fields: [{ envVar: 'ZAI_CODING_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'huggingface',\n displayName: 'Hugging Face',\n category: 'provider',\n hint: 'Inference endpoints · huggingface.co',\n fields: [{ envVar: 'HF_TOKEN', kind: 'secret', label: 'Access token', placeholder: 'hf_...' }],\n },\n {\n id: 'cloudflare-ai-gateway',\n displayName: 'Cloudflare AI Gateway',\n category: 'provider',\n hint: 'Per-gateway routing to OpenAI/Anthropic/Workers AI',\n fields: [\n { envVar: 'CLOUDFLARE_API_KEY', kind: 'secret', label: 'API key' },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', kind: 'config', label: 'Account ID' },\n { envVar: 'CLOUDFLARE_GATEWAY_ID', kind: 'config', label: 'Gateway ID' },\n ],\n },\n {\n id: 'cloudflare-workers-ai',\n displayName: 'Cloudflare Workers AI',\n category: 'provider',\n hint: 'Inference on Cloudflare Workers · ai.cloudflare.com',\n fields: [\n { envVar: 'CLOUDFLARE_API_KEY', kind: 'secret', label: 'API key' },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', kind: 'config', label: 'Account ID' },\n ],\n },\n {\n id: 'openrouter',\n displayName: 'OpenRouter',\n category: 'provider',\n hint: 'Proxy for 200+ models · openrouter.ai',\n fields: [\n { envVar: 'OPENROUTER_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-or-...' },\n ],\n },\n {\n id: 'vercel-ai-gateway',\n displayName: 'Vercel AI Gateway',\n category: 'provider',\n fields: [\n { envVar: 'AI_GATEWAY_API_KEY', kind: 'secret', label: 'API key' },\n {\n envVar: 'AI_GATEWAY_BASE_URL',\n kind: 'config',\n label: 'Base URL (optional)',\n placeholder: 'https://ai-gateway.vercel.sh/v1',\n },\n ],\n },\n {\n id: 'opencode',\n displayName: 'OpenCode',\n category: 'provider',\n hint: 'OpenAI-compatible proxy from the OpenCode CLI',\n fields: [{ envVar: 'OPENCODE_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'opencode-go',\n displayName: 'OpenCode Go',\n category: 'provider',\n hint: 'OpenCode hosted Go endpoint',\n fields: [{ envVar: 'OPENCODE_GO_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'lmstudio',\n displayName: 'LM Studio',\n category: 'provider',\n hint: 'Local inference · lmstudio.ai',\n fields: [\n {\n envVar: 'LMSTUDIO_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:1234/v1',\n },\n {\n envVar: 'LMSTUDIO_API_KEY',\n kind: 'secret',\n label: 'API key (rarely needed)',\n },\n ],\n },\n {\n id: 'ollama',\n displayName: 'Ollama',\n category: 'provider',\n hint: 'Local inference · ollama.com',\n fields: [\n {\n envVar: 'OLLAMA_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:11434/v1',\n },\n {\n envVar: 'OLLAMA_API_KEY',\n kind: 'secret',\n label: 'API key (rarely needed)',\n },\n ],\n },\n {\n id: 'llamacpp',\n displayName: 'llama.cpp',\n category: 'provider',\n hint: 'Local inference · llama.cpp llama-server (OpenAI-compat /v1 endpoint)',\n fields: [\n {\n envVar: 'LLAMACPP_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:8080/v1',\n },\n {\n envVar: 'LLAMACPP_API_KEY',\n kind: 'secret',\n label: 'API key (only if started with --api-key)',\n description:\n 'llama-server runs without auth by default. Set this only if you launched the server with the `--api-key KEY` flag.',\n },\n ],\n },\n\n // --- Ancillary services (web search, etc) ---\n {\n id: 'firecrawl',\n displayName: 'Firecrawl',\n category: 'service',\n group: 'Web Search',\n hint: 'web_fetch fallback for JS-heavy/blocked pages · firecrawl.dev (free tier available)',\n fields: [\n {\n envVar: 'FIRECRAWL_API_KEY',\n kind: 'secret',\n label: 'API key',\n placeholder: 'fc-...',\n description:\n 'When set, web_fetch automatically falls back to Firecrawl if the primary Readability extraction yields too little content.',\n },\n {\n envVar: 'FIRECRAWL_URL',\n kind: 'config',\n label: 'Base URL (optional, for self-hosted)',\n placeholder: 'https://api.firecrawl.dev',\n },\n ],\n },\n {\n id: 'brave-search',\n displayName: 'Brave Search',\n category: 'service',\n group: 'Web Search',\n hint: 'Web search tool · free tier at brave.com/search/api/',\n fields: [{ envVar: 'BRAVE_API_KEY', kind: 'secret', label: 'API key', placeholder: 'BSA...' }],\n },\n {\n id: 'searxng',\n displayName: 'SearXNG',\n category: 'service',\n group: 'Web Search',\n hint: 'Self-hosted meta-search engine · searxng.org',\n fields: [\n {\n envVar: 'SEARXNG_URL',\n kind: 'config',\n label: 'Instance URL',\n placeholder: 'https://searxng.example.com',\n },\n ],\n },\n {\n id: 'browser',\n displayName: 'Browser Automation',\n category: 'service',\n group: 'Browser',\n hint: 'Playwright-driven browser tools (navigate, snapshot, click, screenshot). Run `pnpm exec playwright install chromium` once.',\n fields: [\n {\n envVar: 'BROWSER_ENABLED',\n kind: 'config',\n label: 'Enable browser tools',\n placeholder: 'true',\n description: 'Expose the browser_* tools to agents (true/false). Default true.',\n },\n {\n envVar: 'BROWSER_HEADLESS',\n kind: 'config',\n label: 'Headless',\n placeholder: 'true',\n description: 'Run Chromium headless (true/false). Default true.',\n },\n {\n envVar: 'BROWSER_ALLOW_PRIVATE_NETWORK',\n kind: 'config',\n label: 'Allow private network',\n placeholder: 'false',\n description:\n 'Permit the browser to reach loopback/private IPs (SSRF guard off). Default false — only enable for local dev.',\n },\n {\n envVar: 'BROWSER_IDLE_MS',\n kind: 'config',\n label: 'Idle timeout (ms)',\n placeholder: '900000',\n description: 'Close an idle browser session after this many ms. Default 900000 (15 min).',\n },\n {\n envVar: 'BROWSER_MAX_SESSIONS',\n kind: 'config',\n label: 'Max concurrent sessions',\n placeholder: '4',\n description: 'Cap on simultaneously-open browser sessions (LRU-evicted). Default 4.',\n },\n ],\n },\n\n // --- External integrations (chat bridges, etc) ---\n // Each integration has its own dedicated /config/integrations/* page with\n // workflow-specific UI (preflight health, setup wizard, …). The fields\n // here exist so the keys are allowlisted in the config/secrets stores and\n // surfaced through the generic `PUT /api/config/fields/:envVar` endpoint.\n // Daemon-managed internal state keys (watermarks, derived topic ids) live\n // in repos/config.ts:INTERNAL_CONFIG_KEYS instead, since the user never\n // edits them.\n {\n id: 'telegram',\n displayName: 'Telegram',\n category: 'integration',\n hint: 'Forum-supergroup bot for talking to your agents from a phone',\n fields: [\n {\n envVar: 'TELEGRAM_BOT_TOKEN',\n kind: 'secret',\n label: 'Bot token',\n placeholder: '1234567890:ABC...',\n description: 'Get one from @BotFather → /newbot. Disable Privacy Mode in Bot Settings.',\n },\n {\n envVar: 'TELEGRAM_CHAT_ID',\n kind: 'config',\n label: 'Supergroup chat ID',\n placeholder: '-1001234567890',\n description: 'Numeric id of the forum-enabled supergroup the bot is admin in.',\n },\n ],\n },\n]\n\n/**\n * Fast lookup: envVar → the field definition + owning service.\n * Rebuilt once at module init — the list is static.\n */\nconst FIELD_INDEX: Map<string, { service: ServiceDef; field: ServiceField }> = (() => {\n const m = new Map<string, { service: ServiceDef; field: ServiceField }>()\n for (const service of SERVICES) {\n for (const field of service.fields) {\n m.set(field.envVar, { service, field })\n }\n }\n return m\n})()\n\nexport function findFieldByEnvVar(\n envVar: string,\n): { service: ServiceDef; field: ServiceField } | undefined {\n return FIELD_INDEX.get(envVar)\n}\n\nexport function servicesByCategory(category: ServiceCategory): ServiceDef[] {\n return SERVICES.filter((s) => s.category === category)\n}\n","// Plaintext config store, backed by the `config` table.\n//\n// Companion to `secrets.ts` — same key-shaped values, but for the ones that\n// don't need confidentiality (server URLs, region slugs, project IDs). Kept\n// separate so the /config UI can show plaintext values directly without\n// extra masking logic.\n//\n// The CONFIG_KEYS allowlist (derived from the services registry) is enforced\n// here on writes — a typo or accidental misclassification can't put an API\n// key in this table.\n\nimport type { BazilionDb } from '../db/client.ts'\nimport { SERVICES } from '../services.ts'\n\n/**\n * Daemon-managed internal state keys. Not exposed in the services registry\n * because the user never edits them — they're written by background machinery\n * (polling watermarks, derived topic ids, …). Kept here so the allowlist\n * still gates accidental misuse from the generic field write endpoint.\n */\nconst INTERNAL_CONFIG_KEYS: readonly string[] = [\n // Telegram (step 2+): polling watermark + derived topic/message ids.\n 'TELEGRAM_LAST_UPDATE_ID',\n 'TELEGRAM_SERVICE_TOPIC_ID',\n 'TELEGRAM_DIRECTORY_MESSAGE_ID',\n // Phase 5: proposed new chat id after a migrate_to_chat_id event, pending\n // operator confirmation via POST /api/config/telegram/reconnect.\n 'TELEGRAM_MIGRATED_CHAT_ID',\n]\n\n/**\n * Env var names that live in the plaintext config store. Derived from the\n * services registry — any field marked `kind: 'config'` ends up here — and\n * unioned with the internal keys list above.\n */\nexport const CONFIG_KEYS: readonly string[] = [\n ...SERVICES.flatMap((s) => s.fields.filter((f) => f.kind === 'config').map((f) => f.envVar)),\n ...INTERNAL_CONFIG_KEYS,\n]\n\nconst CONFIG_KEY_SET = new Set<string>(CONFIG_KEYS)\n\nexport function isConfigKey(key: string): boolean {\n return CONFIG_KEY_SET.has(key)\n}\n\ninterface RawRow {\n key: string\n value: string\n updated_at: number\n}\n\nexport interface ConfigStore {\n get(key: string): string | undefined\n set(key: string, value: string): void\n remove(key: string): void\n list(): { key: string; value: string }[]\n getAll(): Record<string, string>\n}\n\nexport function openConfig(db: BazilionDb): ConfigStore {\n return {\n get(key) {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM config WHERE key = ?').get(key)\n return row?.value\n },\n set(key, value) {\n if (!isConfigKey(key)) {\n throw new Error(\n `config.set: \"${key}\" is not a known config key (${CONFIG_KEYS.join(', ')})`,\n )\n }\n db.raw.run(\n `INSERT INTO config (key, value, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n [key, value, Date.now()],\n )\n },\n remove(key) {\n db.raw.run('DELETE FROM config WHERE key = ?', [key])\n },\n list() {\n return db.raw\n .query<RawRow, []>('SELECT * FROM config ORDER BY key ASC')\n .all()\n .map((r) => ({ key: r.key, value: r.value }))\n },\n getAll() {\n const out: Record<string, string> = {}\n for (const r of db.raw.query<RawRow, []>('SELECT * FROM config').all()) {\n out[r.key] = r.value\n }\n return out\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { McpServer, McpTransport } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawRow {\n id: string\n name: string\n transport: string\n command: string | null\n args: string\n url: string | null\n has_auth: number\n enabled: number\n created_at: number\n updated_at: number\n}\n\nfunction toServer(r: RawRow): McpServer {\n let args: string[] = []\n try {\n const parsed = JSON.parse(r.args)\n if (Array.isArray(parsed)) args = parsed.filter((a): a is string => typeof a === 'string')\n } catch {}\n return {\n id: r.id,\n name: r.name,\n transport: r.transport as McpTransport,\n command: r.command,\n args,\n url: r.url,\n hasAuthToken: r.has_auth === 1,\n enabled: r.enabled === 1,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }\n}\n\nexport interface InsertMcpServerInput {\n name: string\n transport: McpTransport\n command?: string | null\n args?: string[]\n url?: string | null\n hasAuth?: boolean\n enabled?: boolean\n}\n\nexport function insert(db: BazilionDb, input: InsertMcpServerInput): McpServer {\n const id = randomUUID()\n const now = Date.now()\n db.raw.run(\n `INSERT INTO mcp_servers (id, name, transport, command, args, url, has_auth, enabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id,\n input.name,\n input.transport,\n input.command ?? null,\n JSON.stringify(input.args ?? []),\n input.url ?? null,\n input.hasAuth ? 1 : 0,\n input.enabled === false ? 0 : 1,\n now,\n now,\n ],\n )\n const row = get(db, id)\n if (!row) throw new Error('mcp server insert failed')\n return row\n}\n\nexport function get(db: BazilionDb, id: string): McpServer | null {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM mcp_servers WHERE id = ?').get(id)\n return row ? toServer(row) : null\n}\n\nexport function getByName(db: BazilionDb, name: string): McpServer | null {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM mcp_servers WHERE name = ?').get(name)\n return row ? toServer(row) : null\n}\n\nexport function list(db: BazilionDb): McpServer[] {\n return db.raw\n .query<RawRow, []>('SELECT * FROM mcp_servers ORDER BY created_at ASC')\n .all()\n .map(toServer)\n}\n\nexport function listEnabled(db: BazilionDb): McpServer[] {\n return db.raw\n .query<RawRow, []>('SELECT * FROM mcp_servers WHERE enabled = 1 ORDER BY created_at ASC')\n .all()\n .map(toServer)\n}\n\nexport interface UpdateMcpServerInput {\n name?: string\n transport?: McpTransport\n command?: string | null\n args?: string[]\n url?: string | null\n enabled?: boolean\n hasAuth?: boolean\n}\n\nexport function update(db: BazilionDb, id: string, input: UpdateMcpServerInput): McpServer | null {\n const existing = get(db, id)\n if (!existing) return null\n const next = {\n name: input.name ?? existing.name,\n transport: input.transport ?? existing.transport,\n command: input.command !== undefined ? input.command : existing.command,\n args: input.args ?? existing.args,\n url: input.url !== undefined ? input.url : existing.url,\n enabled: input.enabled ?? existing.enabled,\n hasAuth: input.hasAuth ?? existing.hasAuthToken,\n }\n db.raw.run(\n `UPDATE mcp_servers\n SET name = ?, transport = ?, command = ?, args = ?, url = ?, enabled = ?, has_auth = ?, updated_at = ?\n WHERE id = ?`,\n [\n next.name,\n next.transport,\n next.command,\n JSON.stringify(next.args),\n next.url,\n next.enabled ? 1 : 0,\n next.hasAuth ? 1 : 0,\n Date.now(),\n id,\n ],\n )\n return get(db, id)\n}\n\nexport function setEnabled(db: BazilionDb, id: string, enabled: boolean): void {\n db.raw.run('UPDATE mcp_servers SET enabled = ?, updated_at = ? WHERE id = ?', [\n enabled ? 1 : 0,\n Date.now(),\n id,\n ])\n}\n\nexport function remove(db: BazilionDb, id: string): void {\n db.raw.run('DELETE FROM mcp_servers WHERE id = ?', [id])\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Message } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawMessage {\n id: string\n from_agent_id: string\n to_agent_id: string\n reply_to: string | null\n payload: string\n created_at: number\n read_at: number | null\n}\n\nfunction toMessage(r: RawMessage): Message {\n return {\n id: r.id,\n fromAgentId: r.from_agent_id,\n toAgentId: r.to_agent_id,\n replyTo: r.reply_to,\n payload: r.payload,\n createdAt: r.created_at,\n readAt: r.read_at,\n }\n}\n\nexport function send(\n db: BazilionDb,\n input: { from: string; to: string; payload: string; replyTo?: string | null },\n): Message {\n const id = randomUUID()\n const now = Date.now()\n db.raw.run(\n `INSERT INTO messages (id, from_agent_id, to_agent_id, reply_to, payload, created_at, read_at)\n VALUES (?, ?, ?, ?, ?, ?, NULL)`,\n [id, input.from, input.to, input.replyTo ?? null, input.payload, now],\n )\n return {\n id,\n fromAgentId: input.from,\n toAgentId: input.to,\n replyTo: input.replyTo ?? null,\n payload: input.payload,\n createdAt: now,\n readAt: null,\n }\n}\n\nexport function get(db: BazilionDb, id: string): Message | null {\n const row = db.raw.query<RawMessage, [string]>('SELECT * FROM messages WHERE id = ?').get(id)\n return row ? toMessage(row) : null\n}\n\nexport function listInbox(\n db: BazilionDb,\n agentId: string,\n opts?: { unreadOnly?: boolean },\n): Message[] {\n const sql = opts?.unreadOnly\n ? 'SELECT * FROM messages WHERE to_agent_id = ? AND read_at IS NULL ORDER BY created_at ASC'\n : 'SELECT * FROM messages WHERE to_agent_id = ? ORDER BY created_at ASC'\n return db.raw.query<RawMessage, [string]>(sql).all(agentId).map(toMessage)\n}\n\nexport function markRead(db: BazilionDb, id: string): void {\n db.raw.run('UPDATE messages SET read_at = ? WHERE id = ? AND read_at IS NULL', [Date.now(), id])\n}\n\n/**\n * Find messages that are replies to a given message id, addressed to a specific agent.\n * Used by `wait_for_reply` to poll for incoming responses.\n */\nexport function findReplies(db: BazilionDb, toAgentId: string, inReplyTo: string): Message[] {\n return db.raw\n .query<RawMessage, [string, string]>(\n `SELECT * FROM messages\n WHERE to_agent_id = ? AND reply_to = ?\n ORDER BY created_at ASC`,\n )\n .all(toAgentId, inReplyTo)\n .map(toMessage)\n}\n\n/**\n * Return the distinct `to_agent_id`s that currently have at least one unread\n * message, filtered to agents not in a terminal state (idle or starting).\n * Used by the scheduler's message-wake loop so a tick can fan-out auto-\n * delivery turns without walking every agent in the DB.\n */\nexport function listRecipientsWithUnread(db: BazilionDb): string[] {\n const rows = db.raw\n .query<{ to_agent_id: string }, []>(\n `SELECT DISTINCT m.to_agent_id FROM messages m\n JOIN agents a ON a.id = m.to_agent_id\n WHERE m.read_at IS NULL AND a.status = 'idle'\n ORDER BY m.to_agent_id`,\n )\n .all()\n return rows.map((r) => r.to_agent_id)\n}\n\n/**\n * Atomically fetch + mark-read all unread messages addressed to `agentId`.\n * Runs inside a transaction so two concurrent schedulers / manual deliveries\n * can't double-dispatch the same message. Returns the fetched messages in\n * ascending `created_at` order — callers format them into the recipient's\n * wake-up prompt.\n */\nexport function drainUnreadForAgent(db: BazilionDb, agentId: string): Message[] {\n return db.raw.transaction(() => {\n const rows = db.raw\n .query<RawMessage, [string]>(\n `SELECT * FROM messages\n WHERE to_agent_id = ? AND read_at IS NULL\n ORDER BY created_at ASC`,\n )\n .all(agentId)\n if (rows.length === 0) return []\n const now = Date.now()\n db.raw.run(\n `UPDATE messages SET read_at = ?\n WHERE to_agent_id = ? AND read_at IS NULL`,\n [now, agentId],\n )\n return rows.map((r) => toMessage({ ...r, read_at: now }))\n })()\n}\n","// Encrypted secrets store, backed by the `secrets` table.\n//\n// Layout: one row per env-var-shaped key (`ANTHROPIC_API_KEY`,\n// `OPENAI_CODEX_OAUTH`, …). Each value is an AES-256-GCM envelope (salt +\n// iv + tag + data, hex-encoded JSON), with the key derived from the\n// bootstrap token via PBKDF2-SHA256 (100k iterations). The crypto matches\n// the previous `secrets.enc` file format byte-for-byte — only the storage\n// medium changed.\n//\n// Why encrypt at all when the password lives in `~/.bazilion/auth.json`\n// next to the DB? Same reason as before: it's defense against accidental\n// exposure (cat'd dumps, screenshares, naive backups), not against an\n// attacker with filesystem read. Anyone who can read both files wins.\n//\n// Caching: `deriveKey` runs PBKDF2 once per (password, salt) pair and the\n// salt is per-row, so a busy daemon does ~one PBKDF2 per secret read. The\n// `secretCache` keeps decrypted values in-memory keyed by row id so repeated\n// reads inside one process don't repeat the work; mutations clear the cache\n// row.\n\nimport { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from 'node:crypto'\nimport type { BazilionDb } from '../db/client.ts'\n\nconst ALGORITHM = 'aes-256-gcm'\nconst KEY_LEN = 32\nconst ITERATIONS = 100_000\nconst DIGEST = 'sha256'\n\ninterface EncryptedEnvelope {\n salt: string\n iv: string\n tag: string\n data: string\n}\n\nfunction deriveKey(password: string, salt: Buffer): Buffer {\n return pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST)\n}\n\nfunction encrypt(plaintext: string, password: string): EncryptedEnvelope {\n const salt = randomBytes(16)\n const key = deriveKey(password, salt)\n const iv = randomBytes(12)\n const cipher = createCipheriv(ALGORITHM, key, iv)\n let data = cipher.update(plaintext, 'utf8', 'hex')\n data += cipher.final('hex')\n const tag = cipher.getAuthTag()\n return {\n salt: salt.toString('hex'),\n iv: iv.toString('hex'),\n tag: tag.toString('hex'),\n data,\n }\n}\n\nfunction decrypt(envelope: EncryptedEnvelope, password: string): string {\n const salt = Buffer.from(envelope.salt, 'hex')\n const key = deriveKey(password, salt)\n const iv = Buffer.from(envelope.iv, 'hex')\n const tag = Buffer.from(envelope.tag, 'hex')\n const decipher = createDecipheriv(ALGORITHM, key, iv)\n decipher.setAuthTag(tag)\n let plaintext = decipher.update(envelope.data, 'hex', 'utf8')\n plaintext += decipher.final('utf8')\n return plaintext\n}\n\ninterface RawRow {\n key: string\n envelope: string\n updated_at: number\n}\n\nexport interface SecretsStore {\n get(key: string): string | undefined\n set(key: string, value: string): void\n remove(key: string): void\n has(key: string): boolean\n list(): { key: string; preview: string }[]\n getAll(): Record<string, string>\n}\n\n/**\n * Open the encrypted secrets store. `password` is the bootstrap token\n * (read from `auth.json` by the caller). Throws on individual-row decrypt\n * failures only when actively reading that row — bad rows show as\n * `undefined` from `get`, the caller can `set` to overwrite.\n */\nexport function openSecrets(db: BazilionDb, password: string): SecretsStore {\n function getRow(key: string): RawRow | null {\n return db.raw.query<RawRow, [string]>('SELECT * FROM secrets WHERE key = ?').get(key)\n }\n\n function listAll(): RawRow[] {\n return db.raw.query<RawRow, []>('SELECT * FROM secrets ORDER BY key ASC').all()\n }\n\n function tryDecrypt(row: RawRow): string | undefined {\n try {\n const envelope = JSON.parse(row.envelope) as EncryptedEnvelope\n return decrypt(envelope, password)\n } catch {\n return undefined\n }\n }\n\n return {\n get(key) {\n const row = getRow(key)\n if (!row) return undefined\n return tryDecrypt(row)\n },\n set(key, value) {\n const envelope = JSON.stringify(encrypt(value, password))\n db.raw.run(\n `INSERT INTO secrets (key, envelope, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET envelope = excluded.envelope, updated_at = excluded.updated_at`,\n [key, envelope, Date.now()],\n )\n },\n remove(key) {\n db.raw.run('DELETE FROM secrets WHERE key = ?', [key])\n },\n has(key) {\n return getRow(key) !== null\n },\n list() {\n return listAll().map((r) => {\n const value = tryDecrypt(r)\n return {\n key: r.key,\n preview: value ? (value.length > 8 ? `${value.slice(0, 6)}…` : '***') : '(unreadable)',\n }\n })\n },\n getAll() {\n const out: Record<string, string> = {}\n for (const r of listAll()) {\n const v = tryDecrypt(r)\n if (v !== undefined) out[r.key] = v\n }\n return out\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { AgentTrigger, TriggerKind } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawTrigger {\n id: string\n agent_id: string\n kind: string\n interval_sec: number | null\n cron_expr: string | null\n message: string\n enabled: number\n last_fired_at: number | null\n created_at: number\n}\n\nfunction toTrigger(r: RawTrigger): AgentTrigger {\n return {\n id: r.id,\n agentId: r.agent_id,\n kind: r.kind as TriggerKind,\n intervalSec: r.interval_sec,\n cronExpr: r.cron_expr,\n message: r.message,\n enabled: r.enabled === 1,\n lastFiredAt: r.last_fired_at,\n createdAt: r.created_at,\n }\n}\n\nexport interface InsertTriggerInput {\n agentId: string\n kind: TriggerKind\n intervalSec: number | null\n cronExpr: string | null\n message: string\n enabled?: boolean\n}\n\nexport function insert(db: BazilionDb, input: InsertTriggerInput): AgentTrigger {\n const id = randomUUID()\n const now = Date.now()\n const enabled = input.enabled === false ? 0 : 1\n db.raw.run(\n `INSERT INTO agent_triggers\n (id, agent_id, kind, interval_sec, cron_expr, message, enabled, last_fired_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)`,\n [id, input.agentId, input.kind, input.intervalSec, input.cronExpr, input.message, enabled, now],\n )\n return {\n id,\n agentId: input.agentId,\n kind: input.kind,\n intervalSec: input.intervalSec,\n cronExpr: input.cronExpr,\n message: input.message,\n enabled: enabled === 1,\n lastFiredAt: null,\n createdAt: now,\n }\n}\n\nexport function get(db: BazilionDb, id: string): AgentTrigger | null {\n const row = db.raw\n .query<RawTrigger, [string]>('SELECT * FROM agent_triggers WHERE id = ?')\n .get(id)\n return row ? toTrigger(row) : null\n}\n\nexport function listForAgent(db: BazilionDb, agentId: string): AgentTrigger[] {\n return db.raw\n .query<RawTrigger, [string]>(\n 'SELECT * FROM agent_triggers WHERE agent_id = ? ORDER BY created_at ASC',\n )\n .all(agentId)\n .map(toTrigger)\n}\n\nexport function listEnabled(db: BazilionDb): AgentTrigger[] {\n return db.raw\n .query<RawTrigger, []>(\n `SELECT t.* FROM agent_triggers t\n JOIN agents a ON a.id = t.agent_id\n WHERE t.enabled = 1 AND a.status != 'archived'\n ORDER BY t.created_at ASC`,\n )\n .all()\n .map(toTrigger)\n}\n\nexport function setEnabled(db: BazilionDb, id: string, enabled: boolean): void {\n db.raw.run('UPDATE agent_triggers SET enabled = ? WHERE id = ?', [enabled ? 1 : 0, id])\n}\n\nexport function markFired(db: BazilionDb, id: string, when: number = Date.now()): void {\n db.raw.run('UPDATE agent_triggers SET last_fired_at = ? WHERE id = ?', [when, id])\n}\n\nexport function remove(db: BazilionDb, id: string): void {\n db.raw.run('DELETE FROM agent_triggers WHERE id = ?', [id])\n}\n","import { createHash, randomBytes, randomUUID } from 'node:crypto'\nimport type { WebToken } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawToken {\n id: string\n label: string\n token_hash: string\n created_at: number\n last_used_at: number | null\n revoked_at: number | null\n}\n\nfunction toToken(r: RawToken): WebToken {\n return {\n id: r.id,\n label: r.label,\n createdAt: r.created_at,\n lastUsedAt: r.last_used_at,\n revokedAt: r.revoked_at,\n }\n}\n\nexport function hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\nexport interface CreatedToken {\n meta: WebToken\n /** Plaintext token — shown exactly once, never re-queryable. */\n token: string\n}\n\nexport function create(db: BazilionDb, label: string): CreatedToken {\n const id = randomUUID()\n const token = randomBytes(24).toString('hex')\n const tokenHash = hashToken(token)\n const now = Date.now()\n db.raw.run(\n `INSERT INTO web_tokens (id, label, token_hash, created_at, last_used_at, revoked_at)\n VALUES (?, ?, ?, ?, NULL, NULL)`,\n [id, label, tokenHash, now],\n )\n return {\n token,\n meta: { id, label, createdAt: now, lastUsedAt: null, revokedAt: null },\n }\n}\n\nexport function list(db: BazilionDb, opts?: { includeRevoked?: boolean }): WebToken[] {\n const sql = opts?.includeRevoked\n ? 'SELECT * FROM web_tokens ORDER BY created_at ASC'\n : 'SELECT * FROM web_tokens WHERE revoked_at IS NULL ORDER BY created_at ASC'\n return db.raw.query<RawToken, []>(sql).all().map(toToken)\n}\n\nexport function get(db: BazilionDb, id: string): WebToken | null {\n const row = db.raw.query<RawToken, [string]>('SELECT * FROM web_tokens WHERE id = ?').get(id)\n return row ? toToken(row) : null\n}\n\n/**\n * Returns the active token row matching the given plaintext, or null.\n * Does NOT bump last_used_at — call markUsed separately once the caller\n * has decided the request is authorized.\n */\nexport function findActiveByToken(db: BazilionDb, token: string): WebToken | null {\n const tokenHash = hashToken(token)\n const row = db.raw\n .query<RawToken, [string]>(\n 'SELECT * FROM web_tokens WHERE token_hash = ? AND revoked_at IS NULL',\n )\n .get(tokenHash)\n return row ? toToken(row) : null\n}\n\nexport function markUsed(db: BazilionDb, id: string, when: number = Date.now()): void {\n db.raw.run('UPDATE web_tokens SET last_used_at = ? WHERE id = ?', [when, id])\n}\n\nexport function revoke(db: BazilionDb, id: string, when: number = Date.now()): boolean {\n const res = db.raw.run(\n 'UPDATE web_tokens SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL',\n [when, id],\n )\n return res.changes > 0\n}\n","// Auth / secrets entry points keyed off the bootstrap `auth.json` file.\n//\n// `auth.json` carries one mandatory field — `token` — written by the daemon's\n// first-run bootstrap. The daemon uses it as the PBKDF2 seed for the `secrets`\n// table; the CLI uses it as the bearer for loopback HTTP. CLI-side `remote`\n// overrides (set by `bazilion login`) coexist in the same file.\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport type { BazilionDb } from './db/client.ts'\nimport { openConfig } from './repos/config.ts'\nimport { openSecrets } from './repos/secrets.ts'\n\nexport interface AuthFile {\n token: string\n remote?: { server: string; token: string } | null\n}\n\n/**\n * Read `auth.json` and return the parsed contents. Throws when missing or\n * malformed — callers should treat that as \"bazilion not initialized.\"\n */\nexport function readAuthFile(authFile: string): AuthFile {\n if (!existsSync(authFile)) {\n throw new Error(\n `${authFile} not found. Start the daemon (\\`bazilion serve\\`) — it auto-bootstraps on first run.`,\n )\n }\n const raw = readFileSync(authFile, 'utf8')\n const parsed = JSON.parse(raw) as Partial<AuthFile>\n if (typeof parsed.token !== 'string' || !parsed.token) {\n throw new Error(`${authFile} is missing the \"token\" field`)\n }\n return {\n token: parsed.token,\n remote: parsed.remote ?? null,\n }\n}\n\n/**\n * Merge plaintext config + decrypted secrets + process env into a single\n * env-shaped record. Precedence (low → high): config → secrets → env. The\n * caller supplies the bootstrap `password` (typically `readAuthFile().token`)\n * because the secrets table is encrypted with it.\n *\n * Either layer may fail to read (corrupt row, wrong key, table absent at\n * fixture-bootstrap time); failures are swallowed per-layer so a single bad\n * value never blocks the merge.\n */\nexport function mergeSecretsIntoEnv(\n db: BazilionDb,\n password: string,\n env: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n let configValues: Record<string, string> = {}\n let secretValues: Record<string, string> = {}\n try {\n configValues = openConfig(db).getAll()\n } catch {\n // table missing or unreadable — continue without the layer\n }\n try {\n secretValues = openSecrets(db, password).getAll()\n } catch {\n // table missing or wrong password — continue without the layer\n }\n return { ...configValues, ...secretValues, ...env }\n}\n","import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, join, resolve, sep } from 'node:path'\nimport AdmZip from 'adm-zip'\nimport type { Paths } from '../paths.ts'\nimport { parseSkillFile } from './parse.ts'\n\nexport interface ImportSkillsInput {\n /**\n * Absolute path to either:\n * - a single skill folder (contains SKILL.md),\n * - a parent directory containing multiple skill folders,\n * - a `.zip` file whose top level holds one-folder-per-skill (or a single\n * wrapping folder that holds them).\n */\n source: string\n /** overwrite existing target skills */\n force?: boolean\n}\n\nexport interface ImportResult {\n imported: string[]\n skipped: { name: string; reason: string }[]\n}\n\n/**\n * Extract a zip archive into a fresh temp dir with a zip-slip guard — every\n * entry's resolved path must stay under the extraction root. Returns both the\n * mkdtemp root (for cleanup) and the effective source path to hand to the\n * importer. When the archive is wrapped in a single top-level directory\n * (common with GitHub-style archives), we descend into it so `source` lines\n * up with the \"parent-of-skill-dirs\" shape the importer already understands.\n */\nfunction extractZipSafely(zipPath: string): { root: string; effectiveSource: string } {\n const root = mkdtempSync(join(tmpdir(), 'bazilion-skill-zip-'))\n try {\n const zip = new AdmZip(zipPath)\n for (const entry of zip.getEntries()) {\n const rawName = entry.entryName\n if (rawName.startsWith('/') || rawName.startsWith('\\\\')) {\n throw new Error(`zip entry has absolute path: ${rawName}`)\n }\n const resolved = resolve(root, rawName)\n if (resolved !== root && !resolved.startsWith(root + sep)) {\n throw new Error(`zip entry escapes extraction root: ${rawName}`)\n }\n }\n zip.extractAllTo(root, true)\n } catch (err) {\n rmSync(root, { recursive: true, force: true })\n throw err\n }\n\n // Unwrap a single top-level folder if present — the importer's\n // \"source-is-a-directory\" cases (single-skill dir vs. parent-of-skills dir)\n // both apply equally well to the unwrapped path.\n let effectiveSource = root\n const topEntries = readdirSync(root, { withFileTypes: true })\n if (topEntries.length === 1 && topEntries[0]?.isDirectory()) {\n effectiveSource = join(root, topEntries[0].name)\n }\n return { root, effectiveSource }\n}\n\nexport function importSkills(paths: Paths, input: ImportSkillsInput): ImportResult {\n const rawSource = resolve(input.source)\n if (!existsSync(rawSource)) {\n throw new Error(`source does not exist: ${rawSource}`)\n }\n\n let source = rawSource\n let tempRoot: string | null = null\n const sourceStat = statSync(rawSource)\n if (sourceStat.isFile()) {\n if (!rawSource.toLowerCase().endsWith('.zip')) {\n throw new Error(`source file must be a .zip archive: ${rawSource}`)\n }\n const { root, effectiveSource } = extractZipSafely(rawSource)\n tempRoot = root\n source = effectiveSource\n } else if (!sourceStat.isDirectory()) {\n throw new Error(`source is not a directory: ${rawSource}`)\n }\n\n try {\n return importSkillsFromDir(paths, source, input)\n } finally {\n if (tempRoot) rmSync(tempRoot, { recursive: true, force: true })\n }\n}\n\nfunction importSkillsFromDir(paths: Paths, source: string, input: ImportSkillsInput): ImportResult {\n const candidates: { name: string; dir: string }[] = []\n\n // Two shapes are accepted:\n // 1. source is itself a single skill dir (contains SKILL.md)\n // 2. source is a parent containing multiple skill dirs\n if (existsSync(join(source, 'SKILL.md'))) {\n candidates.push({ name: basename(source), dir: source })\n } else {\n const entries = readdirSync(source, { withFileTypes: true })\n for (const e of entries) {\n if (!e.isDirectory()) continue\n const skillDir = join(source, e.name)\n if (!existsSync(join(skillDir, 'SKILL.md'))) continue\n candidates.push({ name: e.name, dir: skillDir })\n }\n }\n\n if (candidates.length === 0) {\n throw new Error(`no skills found in ${source}`)\n }\n\n // Validate every SKILL.md before touching the target dir.\n for (const c of candidates) {\n parseSkillFile(join(c.dir, 'SKILL.md'))\n }\n\n const imported: string[] = []\n const skipped: { name: string; reason: string }[] = []\n\n for (const c of candidates) {\n const target = join(paths.skillsDir, c.name)\n if (existsSync(target) && !input.force) {\n skipped.push({\n name: c.name,\n reason: 'already exists (use --force to overwrite)',\n })\n continue\n }\n cpSync(c.dir, target, { recursive: true, force: !!input.force })\n imported.push(c.name)\n }\n\n return { imported, skipped }\n}\n","import { readFileSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\n\n/**\n * Standard agent-skill frontmatter. Required fields are typed; the rest is open\n * (skill formats may add fields like `allowed-tools`, `homepage`, etc.).\n */\nexport interface SkillFrontmatter {\n name: string\n description: string\n [key: string]: unknown\n}\n\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/\n\nexport function parseSkillContent(raw: string): ParsedSkill {\n const m = raw.match(FRONTMATTER_RE)\n if (!m) {\n throw new Error('SKILL.md missing YAML frontmatter (expected leading \"---\")')\n }\n const yamlBlock = m[1] ?? ''\n const body = m[2] ?? ''\n\n let fm: unknown\n try {\n fm = parseYaml(yamlBlock)\n } catch (err) {\n throw new Error(`SKILL.md frontmatter is not valid YAML: ${(err as Error).message}`)\n }\n if (!fm || typeof fm !== 'object' || Array.isArray(fm)) {\n throw new Error('SKILL.md frontmatter must be a YAML object')\n }\n const fmObj = fm as Record<string, unknown>\n if (typeof fmObj.name !== 'string' || fmObj.name.length === 0) {\n throw new Error('SKILL.md frontmatter missing required \"name\"')\n }\n if (typeof fmObj.description !== 'string' || fmObj.description.length === 0) {\n throw new Error('SKILL.md frontmatter missing required \"description\"')\n }\n return { frontmatter: fmObj as SkillFrontmatter, body, raw }\n}\n\nexport function parseSkillFile(path: string): ParsedSkill {\n const raw = readFileSync(path, 'utf8')\n return parseSkillContent(raw)\n}\n","import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { MemoryEntry, MemoryHit } from '@bazilion/api-types'\nimport { createStore, extractSnippet, type QMDStore } from '@tobilu/qmd'\nimport type { MemoryBackend } from './types.ts'\n\nconst INDEX_FILENAME = '.qmd-index.sqlite'\nconst COLLECTION_NAME = 'memory'\nconst PATTERN = '**/*.md'\n\n// One store per memory directory per process. createStore opens a SQLite\n// handle; we dedupe so concurrent chat requests for the same agent reuse it.\nconst storeCache = new Map<string, Promise<QMDStore>>()\n\nfunction getStore(dir: string): Promise<QMDStore> {\n let p = storeCache.get(dir)\n if (!p) {\n p = createStore({\n dbPath: join(dir, INDEX_FILENAME),\n config: {\n collections: {\n [COLLECTION_NAME]: { path: dir, pattern: PATTERN },\n },\n },\n })\n storeCache.set(dir, p)\n }\n return p\n}\n\nfunction safeKey(root: string, key: string): string {\n if (key.includes('..') || key.startsWith('/') || key.includes('\\0')) {\n throw new Error(`unsafe memory key: ${key}`)\n }\n return join(root, key)\n}\n\nfunction walkMd(dir: string, prefix: string, out: MemoryEntry[]): void {\n if (!existsSync(dir)) return\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n if (e.name.startsWith('.')) continue // skip .qmd-index.sqlite and friends\n const full = join(dir, e.name)\n const key = prefix ? `${prefix}/${e.name}` : e.name\n if (e.isDirectory()) {\n walkMd(full, key, out)\n } else if (e.isFile() && e.name.endsWith('.md')) {\n const stats = statSync(full)\n out.push({\n key,\n content: readFileSync(full, 'utf8'),\n updatedAt: stats.mtimeMs,\n })\n }\n }\n}\n\n/**\n * Memory backend backed by @tobilu/qmd — BM25 keyword search over markdown\n * files under `root`. Writes markdown to disk, then asks qmd to reindex.\n *\n * Uses `searchLex` only; no embeddings, no LLM rerank, no model download.\n * The hybrid `search()` / `searchVector()` paths exist in the qmd SDK and\n * can be wired in later if we want semantic search — they'd add a dependency\n * on `node-llama-cpp` and several GB of GGUF models.\n */\nexport function qmdBackend(root: string): MemoryBackend {\n return {\n async init() {\n mkdirSync(root, { recursive: true })\n // Opening the store + initial scan. update() is idempotent.\n const store = await getStore(root)\n await store.update()\n },\n\n async read(key) {\n const path = safeKey(root, key)\n if (!existsSync(path)) {\n throw new Error(`memory entry not found: ${key}`)\n }\n const stats = statSync(path)\n return {\n key,\n content: readFileSync(path, 'utf8'),\n updatedAt: stats.mtimeMs,\n }\n },\n\n async write(key, content) {\n const path = safeKey(root, key)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n const stats = statSync(path)\n // Reindex so the newly-written file is searchable on the next search().\n // update() re-scans the collection; for typical memory sizes (tens of\n // files) this is sub-millisecond.\n const store = await getStore(root)\n await store.update()\n return { key, content, updatedAt: stats.mtimeMs }\n },\n\n async search(query, opts) {\n const limit = opts?.limit ?? 10\n const store = await getStore(root)\n const results = await store.searchLex(query, {\n limit,\n collection: COLLECTION_NAME,\n })\n const hits: MemoryHit[] = []\n for (const r of results) {\n // qmd's filepath is a synthetic URI (`qmd://<collection>/<path>`);\n // displayPath is collection-prefixed (`<collection>/<path>`). Strip\n // the leading `<collection>/` to get the key the caller wrote.\n const prefix = `${COLLECTION_NAME}/`\n const key = r.displayPath.startsWith(prefix)\n ? r.displayPath.slice(prefix.length)\n : r.displayPath\n let content = r.body ?? ''\n if (!content) {\n try {\n content = readFileSync(join(root, key), 'utf8')\n } catch {\n content = ''\n }\n }\n const snippet = extractSnippet(content, query).snippet\n hits.push({ key, snippet, score: r.score })\n }\n return hits\n },\n\n async list() {\n const out: MemoryEntry[] = []\n walkMd(root, '', out)\n return out.sort((a, b) => a.key.localeCompare(b.key))\n },\n\n async remove(key) {\n const path = safeKey(root, key)\n if (existsSync(path)) rmSync(path)\n const store = await getStore(root)\n await store.update()\n },\n }\n}\n","// Translator: pi `AgentSessionEvent` → Bazilion `SessionEvent[]`.\n//\n// The worker's NDJSON wire format (`ChatFrame`) is still the same discrete\n// event stream CLI / browser clients know how to render. This module is the\n// adapter at the single choke-point — pi event in, zero-or-more Bazilion\n// events out.\n//\n// Coverage:\n// - user message_start → one `user_message`\n// - assistant text_delta → one `assistant_delta` per chunk\n// - assistant message_end:\n// - if the message carries text → one `assistant_message`\n// - for every tool-call content → one `tool_call`\n// - tool_execution_end → `tool_result` or `tool_error` based on isError\n// - message_end with aborted/error stopReason → one `error`\n//\n// Not surfaced (intentional):\n// - agent_start / agent_end — run-row lifecycle, not chat events\n// - turn_start / turn_end — too chatty, nothing to render\n// - message_start (assistant) — text will come via updates + end\n// - message_update (non-text) — thinking deltas etc; left for a later\n// pass when UIs render thinking blocks\n// - queue_update / compaction_* — session meta, not assistant output\n// - auto_retry_* — silently retried, user sees only the\n// eventual success or failure\n\nimport type { ProviderMessage, SessionEvent, ToolCall, ToolResultImage } from '@bazilion/api-types'\nimport type { AgentMessage, AgentToolResult } from '@earendil-works/pi-agent-core'\nimport type { AssistantMessage } from '@earendil-works/pi-ai'\nimport type { AgentSessionEvent } from '@earendil-works/pi-coding-agent'\n\nexport function translatePiEvent(e: AgentSessionEvent): SessionEvent[] {\n switch (e.type) {\n case 'message_start': {\n if (e.message.role === 'user') {\n return [{ type: 'user_message', text: stringifyContent(e.message.content) }]\n }\n return []\n }\n\n case 'message_update': {\n const inner = e.assistantMessageEvent\n if (inner.type === 'text_delta') {\n return [{ type: 'assistant_delta', delta: inner.delta }]\n }\n return []\n }\n\n case 'message_end': {\n const m = e.message\n if (m.role !== 'assistant') return []\n const out: SessionEvent[] = []\n const text = extractAssistantText(m as AssistantMessage)\n if (text) out.push({ type: 'assistant_message', text })\n for (const block of (m as AssistantMessage).content ?? []) {\n if (block.type === 'toolCall') {\n out.push({\n type: 'tool_call',\n id: block.id,\n name: block.name,\n arguments: JSON.stringify(block.arguments ?? {}),\n })\n }\n }\n const stopReason = (m as AssistantMessage).stopReason\n if (stopReason === 'aborted' || stopReason === 'error') {\n const errText = (m as AssistantMessage).errorMessage ?? stopReason\n out.push({ type: 'error', error: errText })\n }\n return out\n }\n\n case 'tool_execution_end': {\n const text = extractToolResultText(e.result)\n if (e.isError) {\n return [{ type: 'tool_error', id: e.toolCallId, name: e.toolName, error: text }]\n }\n const images = extractToolResultImages(e.result)\n return [\n {\n type: 'tool_result',\n id: e.toolCallId,\n name: e.toolName,\n result: text,\n ...(images.length > 0 ? { images } : {}),\n },\n ]\n }\n\n default:\n return []\n }\n}\n\nexport function extractAssistantText(m: AssistantMessage): string {\n let out = ''\n for (const block of m.content ?? []) {\n if (block.type === 'text') out += block.text\n }\n return out\n}\n\nexport function extractAssistantToolCalls(m: AssistantMessage): ToolCall[] {\n const out: ToolCall[] = []\n for (const block of m.content ?? []) {\n if (block.type === 'toolCall') {\n out.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.arguments ?? {}) })\n }\n }\n return out\n}\n\n/** Flatten `AgentToolResult.content` blocks into a single string — matches the\n * shape Bazilion tools always returned before pi adoption. */\nexport function extractToolResultText(result: unknown): string {\n const r = result as AgentToolResult<unknown> | undefined\n if (!r?.content) return ''\n let out = ''\n for (const block of r.content) {\n if (block.type === 'text') out += block.text\n }\n return out\n}\n\n/** Pull image blocks out of a tool result (browser screenshots, MCP images). */\nexport function extractToolResultImages(result: unknown): ToolResultImage[] {\n const r = result as AgentToolResult<unknown> | undefined\n if (!r?.content) return []\n const out: ToolResultImage[] = []\n for (const block of r.content) {\n if (block.type === 'image') {\n const b = block as { data?: string; mimeType?: string }\n if (typeof b.data === 'string' && typeof b.mimeType === 'string') {\n out.push({ data: b.data, mimeType: b.mimeType })\n }\n }\n }\n return out\n}\n\n/** Stringify a pi user-message content array. */\nfunction stringifyContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n let out = ''\n for (const block of content as { type: string; text?: string }[]) {\n if (block.type === 'text' && typeof block.text === 'string') out += block.text\n }\n return out\n}\n\n/**\n * Convert pi's authoritative `session.state.messages` (AgentMessage[]) into\n * Bazilion's ProviderMessage[] shape, so the `done` ChatFrame stays\n * compatible with browser + CLI clients that rehydrate from it.\n *\n * Notes on role mapping: pi uses `\"toolResult\"` for tool-response messages;\n * Bazilion uses `\"tool\"`. AssistantMessage content arrays become either\n * `content` string (text blocks joined) or a `toolCalls` array (toolCall\n * blocks converted).\n */\nexport function piMessagesToProviderView(messages: AgentMessage[]): ProviderMessage[] {\n const out: ProviderMessage[] = []\n for (const m of messages) {\n // Custom Bazilion message types would land in this switch too, but we\n // don't register any via CustomAgentMessages declaration merging yet.\n switch (m.role) {\n case 'user': {\n const content = (m as { content: unknown }).content\n const images = extractToolResultImages({ content })\n out.push({\n role: 'user',\n content: stringifyContent(content),\n ...(images.length > 0 ? { images } : {}),\n })\n break\n }\n case 'assistant': {\n const am = m as AssistantMessage\n const text = extractAssistantText(am)\n const toolCalls = extractAssistantToolCalls(am)\n const msg: ProviderMessage = { role: 'assistant', content: text }\n if (toolCalls.length > 0) msg.toolCalls = toolCalls\n out.push(msg)\n break\n }\n case 'toolResult': {\n const tr = m as { content: unknown; toolCallId?: string; toolName?: string }\n const images = extractToolResultImages({ content: tr.content })\n out.push({\n role: 'tool',\n content: stringifyContent(tr.content),\n toolCallId: tr.toolCallId,\n toolName: tr.toolName,\n ...(images.length > 0 ? { images } : {}),\n })\n break\n }\n default:\n // System and other unknown roles are skipped — the runtime's system\n // prompt is already wired via pi's settingsManager + our buildSystemPrompt.\n break\n }\n }\n return out\n}\n","// Bazilion → pi-coding-agent session bridge.\n//\n// `createBazilionSession` returns a fully-wired `AgentSession` suitable for\n// calling `session.prompt(text)` / `session.compact(instructions)` / etc.\n//\n// What we take ownership of (and hand to pi):\n// - cwd: the agent's default workspace path (or agent.dir as a degenerate\n// fallback when no workspace is mounted). Pi's built-in `read/bash/edit/\n// write/grep/find/ls` tools are rooted here.\n// - agentDir: `<bazilion-home>/pi` — pi writes transient state here\n// (settings overrides, resource caches). We don't share it with the\n// user's global `~/.pi/agent` so a Bazilion install never clobbers an\n// independent pi CLI install.\n// - authStorage: `InMemoryAuthStorageBackend` pre-seeded with the resolved\n// API key for the agent's current provider. We never let pi read/write\n// its own auth file — secrets live in the daemon-owned `secrets` table\n// and reach us via `opts.apiKey` (initial) + `opts.refreshApiKey`\n// (OAuth refresher for long turns).\n// - modelRegistry: in-memory. Native providers (anthropic/openai/google/…)\n// come from pi's bundled catalog. For Bazilion-only providers\n// (`lmstudio`, `ollama`) we call `registerProvider(name, {baseUrl,\n// api: 'openai-completions', authHeader: false})` — matches the\n// openai-completions shim our pi-adapter has been using.\n// - sessionManager: `SessionManager.create(cwd, <agentDir>/sessions)`\n// writing JSONL to `~/.bazilion/agents/<id>/sessions/<sessionId>.jsonl`.\n// Crash-survival, append-only, branching, compaction entries — all owned\n// by pi now. Replaces our `agents.chat_messages` blob.\n// - settingsManager: in-memory. Bazilion controls auto-compaction\n// (disabled — we compact manually on user request via /compact) and\n// retry (enabled with Bazilion-tuned caps).\n//\n// What stays outside pi's purview:\n// - spawning agents / profiles / skills discovery (core/)\n// - workspaces registry & mount tracking (core/)\n// - inter-agent messaging, triggers, scheduler (core + apps/web)\n// - memory backend (we wrap it as a pi customTool via `createBazilionCustomTools`)\n\nimport { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { basename, join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { AgentMessage, ThinkingLevel } from '@earendil-works/pi-agent-core'\nimport {\n type AgentSession,\n AuthStorage,\n createAgentSession,\n createExtensionRuntime,\n ModelRegistry,\n type ResourceLoader,\n SessionManager,\n SettingsManager,\n} from '@earendil-works/pi-coding-agent'\nimport type { BazilionDb, Paths } from '../../core/index.ts'\nimport { providerStateRepo } from '../../core/index.ts'\nimport type { MemoryBackend } from '../memory/types.ts'\nimport { resolveModel as resolvePiModel } from '../providers/pi-adapter.ts'\nimport { createProviderRegistry, loadProviderConfigFromEnv } from '../providers/registry.ts'\nimport { buildSystemPrompt } from '../session/prompt.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n McpHost,\n MessagingHost,\n UserMdHost,\n} from '../worker/ipc-protocol.ts'\nimport { createBazilionCustomTools } from './tools.ts'\n\nconst BUILTIN_TOOL_NAMES = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'] as const\n\nexport interface CreateBazilionSessionOptions {\n agent: ResolvedAgent\n paths: Paths\n /** Merged env (process.env + secrets) — produced via `mergeSecretsIntoEnv`. */\n env: NodeJS.ProcessEnv\n memory: MemoryBackend\n /**\n * Names of providers the user has explicitly enabled in /config.\n * Empty set means \"no per-provider gating configured\" — all providers pass.\n * Pre-computed by the daemon and handed in so the session never has to\n * touch the SQLite `provider_state` table itself.\n */\n enabledProviders: Set<string>\n /**\n * Optional host for inter-agent messaging. Wired from the worker's IPC\n * channel back to the daemon — workers no longer hold a SQLite handle of\n * their own. Omit to disable the messaging tools entirely (e.g. unit\n * tests that don't exercise inbox flows).\n */\n messagingHost?: MessagingHost\n /**\n * Optional host for the group-shared USER.md append tool. Like\n * `messagingHost`, this is wired via the worker's IPC channel. Omit to\n * disable the `user_md_append` tool.\n */\n userMdHost?: UserMdHost\n /**\n * Optional browser pool surface for the `browser_*` tools. Wired from the\n * worker's IPC channel back to the daemon (the Playwright session lives in\n * the daemon, persistent across turns). Omit to disable browser automation.\n */\n browserHost?: BrowserHost\n /**\n * Optional MCP connection-pool surface for proxied MCP tools, paired with\n * `mcpTools`. Wired via the worker's IPC channel. Omit when no MCP servers\n * are enabled.\n */\n mcpHost?: McpHost\n /** MCP tools discovered daemon-side, exposed as proxy tools alongside `mcpHost`. */\n mcpTools?: InjectedMcpTool[]\n /** If provided, enables the `deliver_file` tool — the agent's outbound file channel. */\n fileSink?: import('../tools/deliver-file.ts').FileSink\n /**\n * Optional explicit API key for the agent's provider. Wins over any value\n * derived from `env`. Required for OAuth-backed providers (`openai-codex`)\n * since their credentials live in the daemon-owned `secrets` table, not\n * in env vars.\n */\n apiKey?: string\n /**\n * Optional callback for OAuth-backed providers whose access tokens may\n * expire mid-turn. When provided, pi calls it to refresh the JWT during\n * long tool-execution loops. Daemon-side callers (compact/context/truncate)\n * wire this directly against the secrets repo; worker turns currently\n * skip it (the initial token from `apiKey` carries the whole turn).\n */\n refreshApiKey?: (providerName: string) => Promise<string>\n /**\n * Session id to resume. When omitted, pi starts a fresh session file.\n * `/reset` passes `undefined` to rotate; normal chat passes the agent's\n * current session id (persisted on the Bazilion side as `agents.session_id`\n * if we later add that column — today we just restore the most recent\n * session file, which pi's SessionManager locates automatically).\n */\n sessionId?: string\n}\n\nexport interface BazilionSessionHandle {\n session: AgentSession\n /** Call when done — disposes listeners + closes the pi session. */\n dispose(): void\n}\n\n/**\n * Build a pi `AgentSession` using Bazilion's resolved agent + provider state.\n * The returned session is ready for `prompt()` / `compact()` / `reset()`.\n */\nexport async function createBazilionSession(\n opts: CreateBazilionSessionOptions,\n): Promise<BazilionSessionHandle> {\n const {\n agent,\n paths,\n env,\n memory,\n enabledProviders,\n messagingHost,\n userMdHost,\n browserHost,\n mcpHost,\n mcpTools,\n fileSink,\n refreshApiKey,\n } = opts\n\n const { providerName, modelId } = splitModelString(agent.model)\n\n // Enabled-set gate — mirrors createProviderRegistry's check. We keep the\n // Bazilion-side enabled/disabled /config toggles authoritative even though\n // pi does its own provider resolution: we simply refuse to build a session\n // for a disabled provider. The set is pre-computed by the daemon (the\n // worker has no SQLite handle of its own).\n if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {\n throw new Error(`${providerName} provider is disabled — enable it on the /config page`)\n }\n\n // Build the pi Model<Api>. This reuses the same catalog-lookup + literal-\n // fallback that the pi-adapter uses for Provider.chat today, so `lmstudio:\n // any-model` / unreleased OpenAI models / etc. keep working.\n const piProviderName = mapProviderName(providerName)\n const model = resolvePiModel(\n {\n providerName,\n piProviderName,\n fallbackApi: pickFallbackApi(providerName),\n baseUrl: resolveBaseUrl(providerName, env),\n },\n modelId,\n )\n\n // Resolve the API key. Caller-supplied `opts.apiKey` wins (the daemon\n // passes pre-fetched OAuth tokens for `openai-codex` here); otherwise\n // fall back to the env-derived key. Pi's AuthStorage is in-memory only —\n // we never write `auth.json`. `setRuntimeApiKey` is the process-scoped\n // override hook AuthStorage exposes for exactly this.\n const apiKey = opts.apiKey ?? resolveApiKey(providerName, env)\n\n const authStorage = AuthStorage.inMemory()\n if (apiKey) {\n authStorage.setRuntimeApiKey(piProviderName, apiKey)\n }\n\n const modelRegistry = ModelRegistry.inMemory(authStorage)\n // Bazilion-only providers aren't in pi's bundled catalog; register them\n // dynamically so ModelRegistry accepts the Model<> object + resolves auth.\n if (providerName === 'lmstudio' || providerName === 'ollama') {\n modelRegistry.registerProvider(piProviderName, {\n baseUrl: model.baseUrl,\n api: 'openai-completions',\n authHeader: false,\n apiKey: apiKey ?? 'dummy',\n })\n }\n\n // cwd for pi's coding tools is the agent's group directory. Every agent\n // belongs to exactly one group; the group's filesystem root is where work\n // product lives and where the agent's `read`/`bash`/`edit`/`write` are\n // rooted. Private identity/soul files live in `agent.dir` and are reached\n // through the scoped `home_*` tools, not via cwd.\n const cwd = agent.group.path\n if (!existsSync(cwd)) mkdirSync(cwd, { recursive: true })\n\n // Session file under the agent's own directory. Keeping it under\n // `agents/<id>/sessions/` makes `bazilion uninstall` (data tier) already\n // clean them up without changes.\n //\n // Resume-or-create: pi's SessionManager has no built-in \"latest session\"\n // opener. We walk the session dir for the newest `.jsonl` and open it;\n // fall back to `create()` when none exists (fresh agent or post-/reset).\n // This is what makes turn-to-turn continuity work: each worker turn picks\n // up where the last one left off.\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n mkdirSync(sessionDir, { recursive: true })\n const existing = findMostRecent(sessionDir)\n const sessionManager = existing\n ? SessionManager.open(existing, sessionDir, cwd)\n : SessionManager.create(cwd, sessionDir)\n\n // In-memory settings: auto-compaction off (we trigger compaction manually\n // via /compact), retry on with Bazilion-tuned caps matching what withRetry\n // used to apply before pi-adoption.\n const settingsManager = SettingsManager.inMemory({\n compaction: { enabled: false },\n retry: {\n enabled: true,\n maxRetries: 2,\n baseDelayMs: 500,\n provider: { maxRetryDelayMs: 8_000 },\n },\n })\n\n // Bazilion-authored system prompt becomes an `appendSystemPrompt` entry.\n // Pi keeps its default base (which lists built-in tools + guidelines), our\n // profile content (SOUL.md / IDENTITY.md / workspaces / memory hint) is\n // concatenated after it. This is the same injection hook pi extensions use.\n const bazilionPrompt = buildSystemPrompt(agent)\n const resourceLoader = createBazilionResourceLoader(bazilionPrompt)\n await resourceLoader.reload()\n\n // Tool allowlist: pi's `tools` option is exclusive when provided — only\n // the listed names are enabled, regardless of what's in `customTools`.\n // So we have to enumerate both pi's built-in coding tools *and* every\n // Bazilion custom tool we want the LLM to see. Missing the custom names\n // from the allowlist would silently drop memory/messaging/web/bootstrap\n // tools from the agent's surface.\n const customTools = createBazilionCustomTools({\n agent,\n memory,\n messagingHost,\n userMdHost,\n browserHost,\n mcpHost,\n mcpTools,\n fileSink,\n env,\n })\n const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)]\n\n const { session } = await createAgentSession({\n cwd,\n agentDir: join(paths.home, 'pi'),\n model,\n thinkingLevel: toPiThinkingLevel(agent.reasoningLevel),\n tools: allowedTools,\n customTools,\n sessionManager,\n settingsManager,\n authStorage,\n modelRegistry,\n resourceLoader,\n })\n\n // OAuth providers: wire pi's per-request `getApiKey` callback so the JWT\n // gets refreshed *during* a long tool-execution loop, not just at the\n // start of the turn. This is exactly the use case pi-agent-core's doc\n // calls out for this hook (\"short-lived OAuth tokens that may expire\n // during long-running tool execution phases\"). Caller supplies the\n // refresher because only they have access to the secrets table.\n if (refreshApiKey) {\n session.agent.getApiKey = async (requestedProvider) => {\n if (requestedProvider !== piProviderName) return undefined\n try {\n return await refreshApiKey(providerName)\n } catch {\n // Stale/removed credentials mid-session → return undefined so pi\n // surfaces a \"no auth\" error cleanly instead of us throwing out of\n // the provider callback (which would drag down the whole turn).\n return undefined\n }\n }\n }\n\n return {\n session,\n dispose() {\n session.dispose()\n },\n }\n}\n\n// --- helpers ---\n\nfunction splitModelString(s: string): { providerName: string; modelId: string } {\n const idx = s.indexOf(':')\n if (idx === -1) {\n throw new Error(`invalid model string \"${s}\": expected \"provider:model\"`)\n }\n return { providerName: s.slice(0, idx), modelId: s.slice(idx + 1) }\n}\n\n/**\n * Map Bazilion provider names to pi's canonical `piProviderName` for catalog\n * lookups. The split exists because Bazilion was registering e.g. `bedrock`\n * but pi catalogs it as `amazon-bedrock`.\n */\nfunction mapProviderName(name: string): string {\n if (name === 'bedrock') return 'amazon-bedrock'\n return name\n}\n\nfunction pickFallbackApi(providerName: string): string {\n switch (providerName) {\n case 'anthropic':\n return 'anthropic-messages'\n case 'google':\n return 'google-generative-ai'\n case 'google-vertex':\n return 'google-vertex'\n case 'azure-openai':\n return 'azure-openai-responses'\n case 'bedrock':\n return 'bedrock-converse-stream'\n case 'openai-codex':\n return 'openai-codex-responses'\n default:\n return 'openai-completions'\n }\n}\n\nfunction resolveBaseUrl(providerName: string, env: NodeJS.ProcessEnv): string | undefined {\n if (providerName === 'lmstudio') return env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1'\n if (providerName === 'ollama') return env.OLLAMA_URL ?? 'http://127.0.0.1:11434/v1'\n return undefined\n}\n\nfunction resolveApiKey(providerName: string, env: NodeJS.ProcessEnv): string | undefined {\n // Hand-written table mirroring loadProviderConfigFromEnv — cheaper than\n // spinning up a whole ProviderRegistry just to pluck one field.\n switch (providerName) {\n case 'anthropic':\n return env.ANTHROPIC_OAUTH_TOKEN ?? env.ANTHROPIC_API_KEY\n case 'openai':\n return env.OPENAI_API_KEY\n case 'google':\n return env.GEMINI_API_KEY\n case 'mistral':\n return env.MISTRAL_API_KEY\n case 'groq':\n return env.GROQ_API_KEY\n case 'cerebras':\n return env.CEREBRAS_API_KEY\n case 'xai':\n return env.XAI_API_KEY\n case 'zai':\n return env.ZAI_API_KEY\n case 'huggingface':\n return env.HF_TOKEN\n case 'openrouter':\n return env.OPENROUTER_API_KEY\n case 'vercel-ai-gateway':\n return env.AI_GATEWAY_API_KEY\n case 'azure-openai':\n return env.AZURE_OPENAI_API_KEY\n case 'lmstudio':\n return env.LMSTUDIO_API_KEY ?? 'lm-studio'\n case 'ollama':\n return env.OLLAMA_API_KEY ?? 'ollama'\n default:\n return undefined\n }\n}\n\nfunction toPiThinkingLevel(level: string): ThinkingLevel {\n switch (level) {\n case 'off':\n case 'minimal':\n case 'low':\n case 'medium':\n case 'high':\n case 'xhigh':\n return level\n default:\n return 'medium'\n }\n}\n\n/**\n * Minimal `ResourceLoader` implementation — feeds pi our Bazilion-authored\n * system prompt block via `getAppendSystemPrompt` and returns empty collections\n * for everything else. Pi's default loader reads skill/prompt/theme markdown\n * from the workspace cwd; we intentionally opt out because Bazilion owns skill\n * discovery at the platform level (see `apps/daemon/src/core/skills`).\n */\nfunction createBazilionResourceLoader(appendSystemPrompt: string): ResourceLoader {\n const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() }\n return {\n getExtensions: () => extensions,\n getSkills: () => ({ skills: [], diagnostics: [] }),\n getPrompts: () => ({ prompts: [], diagnostics: [] }),\n getThemes: () => ({ themes: [], diagnostics: [] }),\n getAgentsFiles: () => ({ agentsFiles: [] }),\n getSystemPrompt: () => undefined,\n getAppendSystemPrompt: () => (appendSystemPrompt ? [appendSystemPrompt] : []),\n extendResources: () => {},\n async reload() {},\n }\n}\n\n/**\n * Re-exported for callers that want to check whether a provider is\n * Bazilion-enabled before even trying to spawn a session (e.g. /context\n * endpoint which builds a session just to enumerate tools).\n */\nexport function isProviderEnabled(db: BazilionDb, providerName: string): boolean {\n const enabled = providerStateRepo.listEnabled(db)\n return enabled.size === 0 || enabled.has(providerName)\n}\n\n/**\n * Escape hatch for callers that need the raw provider registry (e.g. the\n * current /api/providers/test endpoint). Keeps that one endpoint on our\n * existing non-pi path until we migrate it in a follow-up.\n */\nexport function loadEnabledRegistry(db: BazilionDb, authToken: string, env: NodeJS.ProcessEnv) {\n return createProviderRegistry(loadProviderConfigFromEnv(env, { db, authToken }), {\n enabledSet: providerStateRepo.listEnabled(db),\n })\n}\n\n/**\n * Read the most recent session file for an agent *without* spawning a full\n * AgentSession, and return the resolved provider-message view. Used for SSR\n * page loads that only need to render the canonical transcript — no need\n * to boot pi just to inspect the transcript.\n *\n * Returns an empty array when the agent has no prior session (fresh spawn,\n * or post-/reset).\n */\nexport function loadInitialMessages(agent: ResolvedAgent, paths: Paths): AgentMessage[] {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n if (!existsSync(sessionDir)) return []\n const cwd = agent.group.path\n if (!existsSync(cwd)) return []\n const recent = findMostRecent(sessionDir)\n if (!recent) return []\n try {\n const sm = SessionManager.open(recent, sessionDir)\n const ctx = sm.buildSessionContext()\n return ctx.messages\n } catch (err) {\n // Corrupt session file, stale format, or pi version bump — log loud\n // enough that an operator noticing a blank chat can find the cause in\n // server logs. The turn loop itself starts a fresh session on the\n // next message, so this isn't load-bearing for writes, only reads.\n console.error(\n `[session] loadInitialMessages failed for agent ${agent.agent.id} (${recent}):`,\n err instanceof Error ? (err.stack ?? err.message) : err,\n )\n return []\n }\n}\n\n/**\n * Cheap \"has the session changed?\" probe for polling clients (the web chat\n * stale-tab banner). Returns the most recent session file's basename plus\n * byte size — append-only JSONL, so either value moving means new activity.\n * Returns `{ file: null, size: 0 }` for agents that have never had a turn.\n */\nexport function loadSessionHead(\n agent: ResolvedAgent,\n paths: Paths,\n): { file: string | null; size: number } {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n if (!existsSync(sessionDir)) return { file: null, size: 0 }\n const recent = findMostRecent(sessionDir)\n if (!recent) return { file: null, size: 0 }\n try {\n const s = statSync(recent)\n return { file: basename(recent), size: s.size }\n } catch {\n return { file: null, size: 0 }\n }\n}\n\n/**\n * Test helper: seed a pi session file for an agent with `n` synthetic\n * user/assistant message pairs. Writes a real JSONL entry tree via\n * SessionManager so round-tripping through pi's own reader stays honest.\n * Exported from runtime rather than lived in tests because tests in apps/cli\n * can't directly import pi packages (not a direct dep).\n */\nexport function seedSessionForTest(\n agent: ResolvedAgent,\n paths: Paths,\n messages: Array<{ role: 'user' | 'assistant'; text: string }>,\n): void {\n const cwd = agent.group.path\n if (!existsSync(cwd)) mkdirSync(cwd, { recursive: true })\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n mkdirSync(sessionDir, { recursive: true })\n const sm = SessionManager.create(cwd, sessionDir)\n const now = Date.now()\n messages.forEach((m, i) => {\n if (m.role === 'user') {\n sm.appendMessage({\n role: 'user',\n content: [{ type: 'text', text: m.text }],\n timestamp: now + i,\n })\n } else {\n sm.appendMessage({\n role: 'assistant',\n content: [{ type: 'text', text: m.text }],\n api: 'openai-completions',\n provider: 'lmstudio',\n model: 'test-model',\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'stop',\n timestamp: now + i,\n })\n }\n })\n}\n\n/**\n * Test helper: count message entries on the current leaf's branch of the\n * agent's most-recent session file. Returns 0 when no session file exists.\n */\nexport function countSessionMessagesForTest(agent: ResolvedAgent, paths: Paths): number {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n const recent = findMostRecent(sessionDir)\n if (!recent) return 0\n const cwd = agent.group.path\n try {\n const sm = SessionManager.open(recent, sessionDir, cwd)\n return sm.getBranch().filter((e) => e.type === 'message').length\n } catch {\n return 0\n }\n}\n\n/** Newest `.jsonl` in a pi session directory by mtime, or null if empty. */\nfunction findMostRecent(sessionDir: string): string | null {\n if (!existsSync(sessionDir)) return null\n let newest: { path: string; mtimeMs: number } | null = null\n for (const entry of readdirSync(sessionDir)) {\n if (!entry.endsWith('.jsonl')) continue\n const path = join(sessionDir, entry)\n try {\n const s = statSync(path)\n if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs }\n } catch {\n // ignore races\n }\n }\n return newest?.path ?? null\n}\n","// Adapter from Bazilion's Provider interface → pi-ai's streamSimple.\n//\n// Pi-ai (`@earendil-works/pi-ai`) is Mario Zechner's unified LLM SDK — 15+\n// providers behind one event-stream API. This file is the only place in the\n// codebase that touches pi-ai directly; everything else downstream\n// (`runTurnStream`, `persistRun`, the worker entry, CLI, web) sees the same\n// `Provider.chat(ProviderRequest): Promise<ProviderResponse>` contract it\n// always has. That keeps the wire format, DB schema, and chat UI stable while\n// giving us cost/usage, thinking levels, prompt caching, and every provider\n// pi supports — for free.\n//\n// Model resolution: we prefer pi's typed catalog via `getModel(provider, id)`\n// which carries cost + context-window metadata; for anything outside the\n// catalog (local models, newly-released models, custom OpenAI-compat\n// endpoints) we construct a `Model<>` literal with sensible defaults. This\n// preserves Bazilion's \"any model string\" flexibility.\n\nimport type { ProviderMessage, ReasoningLevel, ToolCall, ToolDef } from '@bazilion/api-types'\nimport {\n type AssistantMessage,\n getModel,\n type Model,\n type Message as PiMessage,\n type Tool as PiTool,\n type ToolCall as PiToolCall,\n streamSimple,\n type TextContent,\n Type,\n} from '@earendil-works/pi-ai'\nimport type { Provider, ProviderRequest, ProviderResponse, StopReason } from './types.ts'\n\nexport interface PiProviderConfig {\n /** Display name on the returned Provider; also the registry key (e.g. 'bedrock', 'azure-openai'). */\n providerName: string\n /** Pi's canonical provider name for catalog lookup (e.g. 'amazon-bedrock', 'azure-openai-responses'). Defaults to providerName. */\n piProviderName?: string\n /** Override baseUrl for openai-compat endpoints (lmstudio, ollama, custom). */\n baseUrl?: string\n /**\n * Static key or an async supplier. Suppliers are called at the top of each\n * chat() so OAuth-backed providers can refresh expiring tokens without\n * rebuilding the Provider instance (which the registry caches).\n */\n apiKey?: string | (() => string | Promise<string>)\n /** Which pi `Api` to use when the model isn't in pi's catalog. */\n fallbackApi: string\n}\n\nfunction defaultBaseUrlFor(providerName: string): string {\n switch (providerName) {\n case 'lmstudio':\n return process.env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1'\n case 'ollama':\n return process.env.OLLAMA_URL ?? 'http://127.0.0.1:11434/v1'\n default:\n return ''\n }\n}\n\nfunction buildModelLiteral(cfg: PiProviderConfig, modelId: string): Model<string> {\n return {\n id: modelId,\n name: modelId,\n api: cfg.fallbackApi,\n provider: cfg.providerName,\n baseUrl: cfg.baseUrl ?? defaultBaseUrlFor(cfg.providerName),\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: 32_768,\n maxTokens: 4_096,\n }\n}\n\nexport function resolveModel(cfg: PiProviderConfig, modelId: string): Model<string> {\n const lookupName = cfg.piProviderName ?? cfg.providerName\n // Try pi's typed catalog first — gets us cost, context window, reasoning flags\n // for free on the known providers' known models.\n try {\n // getModel's type signature is catalog-constrained, but at runtime it just\n // indexes MODELS[provider][id]. Cast through unknown so unknown ids fall\n // through to the literal builder instead of tripping the compiler.\n const known = (getModel as unknown as (p: string, m: string) => Model<string> | undefined)(\n lookupName,\n modelId,\n )\n if (known && typeof known === 'object' && 'api' in known) {\n // Override baseUrl + provider for local / compat endpoints — pi's catalog\n // doesn't know about lmstudio/ollama but if a user types\n // `openai:gpt-4o` with a custom OPENAI_BASE_URL, honor that here.\n if (cfg.baseUrl) return { ...known, baseUrl: cfg.baseUrl }\n return known\n }\n } catch {\n // Fall through.\n }\n return buildModelLiteral(cfg, modelId)\n}\n\nfunction convertMessages(messages: ProviderMessage[]): PiMessage[] {\n const out: PiMessage[] = []\n const now = Date.now()\n for (const m of messages) {\n if (m.role === 'system') continue // pi takes system prompt separately\n if (m.role === 'user') {\n out.push({ role: 'user', content: m.content, timestamp: now })\n continue\n }\n if (m.role === 'assistant') {\n const content: AssistantMessage['content'] = []\n if (m.content) content.push({ type: 'text', text: m.content } satisfies TextContent)\n if (m.toolCalls) {\n for (const tc of m.toolCalls) {\n let parsed: Record<string, unknown> = {}\n try {\n parsed = JSON.parse(tc.arguments) as Record<string, unknown>\n } catch {\n // leave empty\n }\n content.push({\n type: 'toolCall',\n id: tc.id,\n name: tc.name,\n arguments: parsed,\n } satisfies PiToolCall)\n }\n }\n // Synthesize the AssistantMessage fields pi expects on replays — these\n // are only load-bearing for the LLM that gets the transcript; since we\n // don't persist usage/stopReason in Bazilion's message store, defaults\n // are fine.\n out.push({\n role: 'assistant',\n content,\n api: 'anthropic-messages',\n provider: 'anthropic',\n model: 'unknown',\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'stop',\n timestamp: now,\n })\n continue\n }\n if (m.role === 'tool') {\n out.push({\n role: 'toolResult',\n toolCallId: m.toolCallId ?? '',\n toolName: m.toolName ?? 'tool',\n content: [{ type: 'text', text: m.content }],\n isError: false,\n timestamp: now,\n })\n }\n }\n return out\n}\n\nfunction convertTools(tools: ToolDef[] | undefined): PiTool[] | undefined {\n if (!tools || tools.length === 0) return undefined\n return tools.map((t) => ({\n name: t.name,\n description: t.description,\n // Type.Unsafe lets us pass raw JSON Schema through without re-authoring in\n // typebox. Providers validate against the schema, not typebox's TSchema.\n parameters: Type.Unsafe<unknown>(t.parameters as Record<string, unknown>),\n }))\n}\n\nfunction toBazilionStopReason(reason: string): StopReason {\n switch (reason) {\n case 'stop':\n return 'stop'\n case 'length':\n return 'length'\n case 'toolUse':\n return 'tool_use'\n default:\n return 'error'\n }\n}\n\nfunction extractFinalResponse(msg: AssistantMessage): ProviderResponse {\n let text = ''\n const toolCalls: ToolCall[] = []\n for (const block of msg.content) {\n if (block.type === 'text') {\n text += block.text\n } else if (block.type === 'toolCall') {\n toolCalls.push({\n id: block.id,\n name: block.name,\n arguments: JSON.stringify(block.arguments ?? {}),\n })\n }\n }\n const res: ProviderResponse = {\n content: text,\n toolCalls,\n stopReason: toBazilionStopReason(msg.stopReason),\n }\n if (msg.usage) {\n res.usage = {\n promptTokens: msg.usage.input,\n completionTokens: msg.usage.output,\n }\n }\n return res\n}\n\nfunction mapReasoning(\n r: ReasoningLevel | undefined,\n): 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | undefined {\n if (!r || r === 'off') return undefined\n return r\n}\n\nasync function resolveApiKey(cfg: PiProviderConfig): Promise<string | undefined> {\n if (typeof cfg.apiKey === 'function') return await cfg.apiKey()\n return cfg.apiKey\n}\n\nexport function piProvider(cfg: PiProviderConfig): Provider {\n return {\n name: cfg.providerName,\n async chat(req: ProviderRequest): Promise<ProviderResponse> {\n const model = resolveModel(cfg, req.model)\n const apiKey = await resolveApiKey(cfg)\n\n const stream = streamSimple(\n model,\n {\n systemPrompt: req.system ?? '',\n messages: convertMessages(req.messages),\n tools: convertTools(req.tools),\n },\n {\n signal: req.signal,\n apiKey,\n reasoning: mapReasoning(req.reasoning),\n maxTokens: req.maxTokens,\n temperature: req.temperature,\n },\n )\n\n let finalMessage: AssistantMessage | null = null\n for await (const event of stream) {\n if (event.type === 'text_delta' && req.onDelta) {\n req.onDelta(event.delta)\n } else if (event.type === 'done') {\n finalMessage = event.message\n } else if (event.type === 'error') {\n finalMessage = event.error\n }\n }\n if (!finalMessage) {\n throw new Error(`pi provider ${cfg.providerName} returned no terminal event`)\n }\n if (finalMessage.stopReason === 'aborted' || finalMessage.stopReason === 'error') {\n const msg = finalMessage.errorMessage ?? 'provider error'\n throw new Error(msg)\n }\n return extractFinalResponse(finalMessage)\n },\n }\n}\n","// OpenAI ChatGPT / Codex OAuth — token storage + refresh on top of pi-ai.\n//\n// Pi-ai ships a complete OAuth flow for the ChatGPT backend (`@earendil-works/pi-ai`\n// exports `loginOpenAICodex` + `refreshOpenAICodexToken`), so this module is\n// thin: it adapts the credential I/O to Bazilion's encrypted secrets store\n// and exposes a single `loadAccessToken(db, authToken)` call that refreshes\n// when the token is about to expire.\n//\n// Storage: the JSON blob `{refresh, access, expires}` lives under the\n// secrets key `OPENAI_CODEX_OAUTH` in the `secrets` table. The blob is\n// never copied into the env (unlike plain-API-key providers) — refresh is\n// stateful, so every call reads and writes through the live secrets store.\n\nimport type { OpenAICodexStatus } from '@bazilion/api-types'\nimport type { OAuthCredentials } from '@earendil-works/pi-ai'\nimport { loginOpenAICodex, refreshOpenAICodexToken } from '@earendil-works/pi-ai/oauth'\nimport { type BazilionDb, openSecrets } from '../../core/index.ts'\n\nexport const OPENAI_CODEX_SECRET_KEY = 'OPENAI_CODEX_OAUTH'\n\n/** Refresh when the access token has less than this much life left. */\nconst REFRESH_MARGIN_MS = 60_000\n\nexport interface StoredCredentials {\n refresh: string\n access: string\n expires: number\n}\n\nfunction readCredentials(db: BazilionDb, authToken: string): StoredCredentials | null {\n const raw = openSecrets(db, authToken).get(OPENAI_CODEX_SECRET_KEY)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredentials>\n if (\n typeof parsed.refresh === 'string' &&\n typeof parsed.access === 'string' &&\n typeof parsed.expires === 'number'\n ) {\n return { refresh: parsed.refresh, access: parsed.access, expires: parsed.expires }\n }\n return null\n } catch {\n return null\n }\n}\n\nfunction writeCredentials(db: BazilionDb, authToken: string, creds: StoredCredentials): void {\n openSecrets(db, authToken).set(OPENAI_CODEX_SECRET_KEY, JSON.stringify(creds))\n}\n\nexport function clearCredentials(db: BazilionDb, authToken: string): void {\n openSecrets(db, authToken).remove(OPENAI_CODEX_SECRET_KEY)\n}\n\nexport function hasCredentials(db: BazilionDb, authToken: string): boolean {\n return readCredentials(db, authToken) !== null\n}\n\nfunction decodeAccountId(accessToken: string): string | null {\n const parts = accessToken.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = JSON.parse(Buffer.from(parts[1] as string, 'base64').toString('utf8')) as {\n 'https://api.openai.com/auth'?: { chatgpt_account_id?: string }\n }\n return payload['https://api.openai.com/auth']?.chatgpt_account_id ?? null\n } catch {\n return null\n }\n}\n\nexport function getStatus(db: BazilionDb, authToken: string): OpenAICodexStatus {\n const creds = readCredentials(db, authToken)\n if (!creds) return { connected: false, expiresAt: null, accountId: null }\n return {\n connected: true,\n expiresAt: creds.expires,\n accountId: decodeAccountId(creds.access),\n }\n}\n\n/**\n * Returns a valid access token, refreshing via pi-ai if the stored one is\n * within `REFRESH_MARGIN_MS` of expiry. Throws when no credentials are stored\n * so callers can surface an actionable \"run `bazilion auth openai login`\"\n * error rather than a 401 from the upstream API.\n */\nexport async function loadAccessToken(db: BazilionDb, authToken: string): Promise<string> {\n const creds = readCredentials(db, authToken)\n if (!creds) {\n throw new Error(\n 'OpenAI ChatGPT OAuth not configured — run `bazilion auth openai login` (or use the Connect button on /config)',\n )\n }\n if (creds.expires > Date.now() + REFRESH_MARGIN_MS) return creds.access\n\n const refreshed = (await refreshOpenAICodexToken(creds.refresh)) as OAuthCredentials\n const next: StoredCredentials = {\n refresh: refreshed.refresh,\n access: refreshed.access,\n expires: refreshed.expires,\n }\n writeCredentials(db, authToken, next)\n return next.access\n}\n\n/** Persist credentials fetched by pi-ai's `loginOpenAICodex`. */\nexport function saveLoginCredentials(\n db: BazilionDb,\n authToken: string,\n creds: OAuthCredentials,\n): void {\n writeCredentials(db, authToken, {\n refresh: creds.refresh,\n access: creds.access,\n expires: creds.expires,\n })\n}\n\nexport { loginOpenAICodex, refreshOpenAICodexToken }\n","// Bazilion system-prompt builder. Feeds pi's `getAppendSystemPrompt()` hook\n// so the agent sees its persona + skills + workspaces + memory guidance\n// stacked on top of pi's built-in base prompt (which lists coding tools and\n// general guidelines). Pure filesystem read — no LLM, no DB.\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\n\n// Prompt order: peers first (who else is around), then persona, then tooling\n// hints, then self-knowledge, then the wake-up playbook.\n//\n// BOOTSTRAP.md is intentionally NOT in this generic-context list — it gets\n// its own dedicated \"First-Run Ritual\" section below with explicit\n// anti-checklist framing, so models treat it as multi-turn Q&A guidance\n// rather than a one-shot script to execute. The system prompt regenerates\n// per turn, so the section auto-vanishes when `bootstrap_done` removes the\n// file (vs. wrapping the user message, which would persist in pi's session\n// JSONL forever and replay on every future turn).\nconst CONTEXT_FILE_ORDER = [\n 'AGENTS.md',\n 'SOUL.md',\n 'TOOLS.md',\n 'IDENTITY.md',\n 'HEARTBEAT.md',\n] as const\n\nexport function buildSystemPrompt(agent: ResolvedAgent): string {\n const parts: string[] = []\n\n const contextBlocks: string[] = []\n for (const file of CONTEXT_FILE_ORDER) {\n const path = join(agent.agent.dir, file)\n if (!existsSync(path)) continue\n const content = readFileSync(path, 'utf8').trimEnd()\n if (!content) continue\n contextBlocks.push(`## ${file}\\n\\n${content}`)\n }\n if (contextBlocks.length > 0) {\n parts.push(`# Project Context\\n\\n${contextBlocks.join('\\n\\n')}`)\n }\n\n // First-Run Ritual block — only emitted while BOOTSTRAP.md exists on disk.\n // The wording deliberately frames it as \"multi-turn Q&A\" not \"checklist\"\n // and lists hard rules at the top so tool-eager models still notice them\n // even if they skim the body.\n const bootstrapPath = join(agent.agent.dir, 'BOOTSTRAP.md')\n if (existsSync(bootstrapPath)) {\n const bootstrap = readFileSync(bootstrapPath, 'utf8').trimEnd()\n if (bootstrap) {\n parts.push(\n [\n '# First-Run Ritual',\n '',\n 'This is your first session. The document below is **conversational guidance**, not a checklist to execute in one shot. It describes a multi-turn Q&A you should have with the human, one question per turn.',\n '',\n '## Hard rules',\n '- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.',\n '- Each subsequent turn: at most one new question. Wait between turns.',\n '- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.',\n '',\n '## BOOTSTRAP.md',\n '',\n bootstrap,\n ].join('\\n'),\n )\n }\n }\n\n parts.push(\n [\n '# Agent Home',\n '',\n 'Your private home holds who you are — identity, soul, behaviour rules, wake-up routine. It is not shared with other agents and cannot be overwritten by them. The files above (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, HEARTBEAT.md) live in this home, plus BOOTSTRAP.md when you are still in your first-run ritual.',\n '',\n '- To change who you are (name, vibe, personality, how you behave): use `home_write`.',\n '- To inspect exact wording of your own files: use `home_read` or `home_list`.',\n '- To remember facts the user told you or things you learned: use `memory_write` — NOT `home_write`.',\n '- To produce work output (code, docs, artefacts): use `write` / `edit` — those land in your workspace, not your home.',\n ].join('\\n'),\n )\n\n if (agent.skills.length > 0) {\n parts.push(\n `# Available Skills\\n\\nYou have access to the following skills: ${agent.skills.join(', ')}.`,\n )\n }\n\n const groupLines = [\n '# Group',\n '',\n `- ${agent.group.id} (${agent.group.name}): ${agent.group.path}`,\n '',\n 'Your group is where work product lives — code, docs, artefacts, shared scratch. It may be shared with other agents in the same group. Your coding tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) are rooted at the group directory. Never use these tools to edit your identity/soul/behaviour files — those live in your home and are reached via `home_write` / `home_read`.',\n ]\n parts.push(groupLines.join('\\n'))\n\n if (agent.group.userMd.trim()) {\n parts.push(\n `# About the User\\n\\nShared context about the human you're working with in this group. Both you and the human curate it. To update: call \\`user_md_get\\` (returns current content + an etag), merge your change into the full text, then call \\`user_md_write\\` with the merged content and the etag. Use this for STABLE user-specific facts (preferences, role, working hours, how they like to be addressed) — and to CORRECT stale entries when the human tells you something different from what's recorded. For project knowledge use \\`memory_write\\` instead; for personal notes about yourself use \\`home_write\\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes — every agent in the group sees the new content in their system prompt on their next turn automatically.**\\n\\n${agent.group.userMd.trim()}`,\n )\n } else {\n parts.push(\n `# About the User\\n\\nThis group's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \\`user_md_get\\` then \\`user_md_write\\` (always get first — you need the etag). Reserve this for things you're confident are durable — project knowledge belongs in \\`memory_write\\`, personal notes about yourself in \\`home_write\\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes — every agent in the group sees the new content in their system prompt on their next turn automatically.**`,\n )\n }\n\n parts.push(\n '# Memory\\n\\nYou share a persistent memory backend with every other agent in this group. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge — codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the group may have already learned something useful. **Do NOT send peer messages announcing memory writes — every agent has access to the same store via `memory_search` and will find your note when they need it.**',\n )\n\n return parts.join('\\n\\n---\\n\\n')\n}\n","// Adapter: Bazilion ToolHandler → pi-coding-agent ToolDefinition.\n//\n// Pi expects tools to return `AgentToolResult<TDetails>` =\n// `{ content: (TextContent | ImageContent)[]; details: TDetails; terminate?: boolean }`.\n// Our handlers return plain strings. The adapter wraps the string into a single\n// text content block with empty details — the same fidelity we had before.\n//\n// Pi's tool execute contract: throw on failure. The agent loop wraps thrown\n// errors as tool-result messages with `isError: true`. We preserve this shape\n// directly because our handlers already throw on bad args / runtime failures.\n//\n// What's in this module:\n// - `ourToolToPiTool` — single-handler wrapper.\n// - `createBazilionCustomTools` — composed suite of the Bazilion-specific\n// tools (memory_*, messaging, bootstrap_done, web_search/fetch). File I/O\n// tools are *not* here — pi's createCodingTools(cwd, …) replaces them.\n\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { ToolDefinition } from '@earendil-works/pi-coding-agent'\nimport { Type } from 'typebox'\nimport type { MemoryBackend } from '../memory/types.ts'\nimport { bootstrapTool } from '../tools/bootstrap.ts'\nimport { browserTools } from '../tools/browser.ts'\nimport { deliverFileTool, type FileSink } from '../tools/deliver-file.ts'\nimport { homeTools } from '../tools/home.ts'\nimport { mcpProxyTools } from '../tools/mcp.ts'\nimport { memoryTools } from '../tools/memory.ts'\nimport { messagingTools } from '../tools/messaging.ts'\nimport type { ToolHandler, ToolOutput } from '../tools/types.ts'\nimport { userMdTools } from '../tools/user-md.ts'\nimport { webTools } from '../tools/web.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n McpHost,\n MessagingHost,\n UserMdHost,\n} from '../worker/ipc-protocol.ts'\n\n/**\n * Wrap a Bazilion `ToolHandler` as a pi `ToolDefinition` so it can be passed\n * through `customTools` to `createAgentSession`.\n *\n * Design note: we keep the same tool name + description + parameter JSON schema\n * that the handler already declares. Pi wants a `TypeBox` schema; we pass the\n * JSONSchema through `Type.Unsafe` so the LLM validation happens on pi's side\n * without us having to re-author schemas in typebox syntax.\n */\nexport function ourToolToPiTool(h: ToolHandler): ToolDefinition {\n return {\n name: h.def.name,\n label: h.def.name,\n description: h.def.description,\n parameters: Type.Unsafe<Record<string, unknown>>(h.def.parameters as Record<string, unknown>),\n async execute(_toolCallId, params) {\n const out = await h.invoke(params as Record<string, unknown>)\n return {\n content: toPiContent(out),\n details: {},\n }\n },\n }\n}\n\n/**\n * Map a Bazilion `ToolOutput` to pi's `(TextContent | ImageContent)[]`. A bare\n * string becomes a single text block (the legacy shape); a part array maps\n * 1:1 — pi's content blocks have the same field names.\n */\nfunction toPiContent(\n out: ToolOutput,\n): Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> {\n if (typeof out === 'string') return [{ type: 'text', text: out }]\n return out.map((p) =>\n p.type === 'text'\n ? { type: 'text', text: p.text }\n : { type: 'image', data: p.data, mimeType: p.mimeType },\n )\n}\n\nexport interface BazilionCustomToolsOpts {\n agent: ResolvedAgent\n memory: MemoryBackend\n /** If provided, enables inter-agent messaging tools. */\n messagingHost?: MessagingHost\n /** If provided, enables the `user_md_append` tool. */\n userMdHost?: UserMdHost\n /** If provided, enables the `browser_*` tools (proxied to the daemon pool). */\n browserHost?: BrowserHost\n /** If provided alongside `mcpTools`, enables the discovered MCP proxy tools. */\n mcpHost?: McpHost\n /** MCP tools discovered daemon-side, exposed as proxy tools. */\n mcpTools?: InjectedMcpTool[]\n /** If provided, enables the `deliver_file` tool (emits a `file` event). */\n fileSink?: FileSink\n /** Merged env (process.env + secrets). */\n env?: NodeJS.ProcessEnv\n}\n\n/**\n * Build the list of Bazilion-specific custom tools in the shape pi expects.\n *\n * Excludes file-I/O tools (`read`/`bash`/`edit`/`write`/`grep`/`find`/`ls`) —\n * those come from pi's `createCodingTools(cwd, …)` now that we've adopted the\n * richer toolset. Also excludes the legacy `workspace_list/read/write`\n * triumvirate: pi's tools work against a single cwd (the agent's default\n * workspace), and mounted non-default workspaces are reachable via absolute\n * paths through pi's `bash`/`read`/`edit`.\n */\nexport function createBazilionCustomTools(opts: BazilionCustomToolsOpts): ToolDefinition[] {\n const handlers: ToolHandler[] = [\n ...memoryTools(opts.memory),\n ...homeTools(opts.agent.agent.dir),\n bootstrapTool(opts.agent.agent.dir),\n ...webTools({ env: opts.env }),\n ]\n if (opts.messagingHost) {\n handlers.push(...messagingTools(opts.messagingHost, opts.agent.agent.id))\n }\n if (opts.userMdHost) {\n handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id))\n }\n if (opts.browserHost) {\n handlers.push(...browserTools(opts.browserHost, opts.agent.agent.id))\n }\n if (opts.mcpHost && opts.mcpTools && opts.mcpTools.length > 0) {\n handlers.push(...mcpProxyTools(opts.mcpHost, opts.mcpTools))\n }\n if (opts.fileSink) {\n handlers.push(deliverFileTool(opts.agent.group.path, opts.fileSink))\n }\n return handlers.map(ourToolToPiTool)\n}\n","import { existsSync, rmSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\nexport function bootstrapTool(agentDir: string): ToolHandler {\n return {\n def: {\n name: 'bootstrap_done',\n description:\n 'Call this once you have finished your bootstrap conversation (introduced yourself, learned the user, updated IDENTITY.md). Removes BOOTSTRAP.md so it does not appear in future sessions.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const path = join(agentDir, 'BOOTSTRAP.md')\n if (existsSync(path)) {\n rmSync(path)\n return 'BOOTSTRAP.md removed. Bootstrap is complete.'\n }\n return 'BOOTSTRAP.md was already removed.'\n },\n }\n}\n","// Browser automation tools (worker-side definitions).\n//\n// These are thin proxies: each tool's `invoke` forwards to the daemon-side\n// Playwright pool over IPC via the injected `BrowserHost`. The browser session\n// (stateful, persistent across turns) lives entirely in the daemon — see\n// `lib/browser/pool.ts`. Results are multimodal: snapshots/console/network come\n// back as text, `browser_take_screenshot` as a base64 image.\n//\n// Perception is accessibility-tree-first. `browser_snapshot` returns an aria\n// tree with `[ref=eN]` element refs; every interaction targets an element by\n// that `ref`. No vision model is needed for routine automation — screenshots\n// are the exception, not the rule.\n\nimport type { BrowserHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\nconst REF_DESC = 'Element ref from the latest browser_snapshot, e.g. \"e5\".'\n\n/**\n * Build the `browser_*` tool suite bound to one agent's session. Each tool\n * forwards to `host.invoke(agentId, <action>, args)`.\n */\nexport function browserTools(host: BrowserHost, agentId: string): ToolHandler[] {\n const proxy = (\n name: string,\n action: string,\n description: string,\n properties: Record<string, unknown>,\n required: string[] = [],\n ): ToolHandler => ({\n def: {\n name,\n description,\n parameters: { type: 'object', properties, required, additionalProperties: false },\n },\n invoke: (args) => host.invoke(agentId, action, args),\n })\n\n return [\n proxy(\n 'browser_navigate',\n 'navigate',\n 'Open a URL in the browser and return an accessibility snapshot of the loaded page. Starts a persistent browser session that survives across turns (cookies/logins carry over).',\n { url: { type: 'string', description: 'Absolute URL to navigate to.' } },\n ['url'],\n ),\n proxy(\n 'browser_snapshot',\n 'snapshot',\n 'Capture the current page as an accessibility tree (YAML) with [ref=eN] element references. This is the primary way to \"see\" the page — prefer it over screenshots. Use the refs with browser_click/type/etc.',\n {},\n ),\n proxy(\n 'browser_click',\n 'click',\n 'Click an element identified by its snapshot ref. Returns a fresh snapshot.',\n { ref: { type: 'string', description: REF_DESC } },\n ['ref'],\n ),\n proxy(\n 'browser_type',\n 'type',\n 'Type text into an input/textarea identified by its ref. Set submit=true to press Enter afterwards. Returns a fresh snapshot.',\n {\n ref: { type: 'string', description: REF_DESC },\n text: { type: 'string', description: 'Text to type into the field.' },\n submit: { type: 'boolean', description: 'Press Enter after typing.' },\n },\n ['ref', 'text'],\n ),\n proxy(\n 'browser_hover',\n 'hover',\n 'Hover the pointer over an element identified by its ref. Returns a fresh snapshot.',\n { ref: { type: 'string', description: REF_DESC } },\n ['ref'],\n ),\n proxy(\n 'browser_select',\n 'select',\n 'Select option(s) in a <select> element identified by its ref.',\n {\n ref: { type: 'string', description: REF_DESC },\n values: {\n type: 'array',\n items: { type: 'string' },\n description: 'Option values to select.',\n },\n },\n ['ref', 'values'],\n ),\n proxy(\n 'browser_fill_form',\n 'fill_form',\n 'Fill multiple form fields in one call. Each field is { ref, value }.',\n {\n fields: {\n type: 'array',\n description: 'Fields to fill.',\n items: {\n type: 'object',\n properties: {\n ref: { type: 'string', description: REF_DESC },\n value: { type: 'string' },\n },\n required: ['ref', 'value'],\n },\n },\n },\n ['fields'],\n ),\n proxy(\n 'browser_press_key',\n 'press_key',\n 'Press a keyboard key (e.g. \"Enter\", \"Escape\", \"ArrowDown\", \"Control+A\").',\n { key: { type: 'string', description: 'Key or chord to press.' } },\n ['key'],\n ),\n proxy(\n 'browser_go_back',\n 'go_back',\n 'Navigate back to the previous page in history. Returns a fresh snapshot.',\n {},\n ),\n proxy(\n 'browser_tabs',\n 'tabs',\n 'Manage tabs. op=\"list\" lists open tabs; \"new\" opens a tab (optional url); \"select\" focuses tab at index; \"close\" closes tab at index (defaults to active).',\n {\n op: { type: 'string', enum: ['list', 'new', 'select', 'close'] },\n index: { type: 'number', description: 'Tab index for select/close.' },\n url: { type: 'string', description: 'URL to open for op=new.' },\n },\n ['op'],\n ),\n proxy(\n 'browser_take_screenshot',\n 'take_screenshot',\n 'Take a PNG screenshot of the current page (returned as an image). Use only for visual verification or canvas/pixel content the accessibility snapshot cannot represent — prefer browser_snapshot for interaction.',\n { full_page: { type: 'boolean', description: 'Capture the full scrollable page.' } },\n ),\n proxy(\n 'browser_console',\n 'console',\n 'Return recent browser console messages (logs, warnings, errors).',\n {},\n ),\n proxy(\n 'browser_network',\n 'network',\n 'Return recent network requests issued by the page (method, status, URL).',\n {},\n ),\n ]\n}\n","// `deliver_file` — the agent's outbound document channel.\n//\n// The agent produces a file in its workspace (write a report, render a chart,\n// zip something up) and calls deliver_file(path) to hand it to the user. The\n// file lives on the daemon's filesystem (the worker is a subprocess on the same\n// host), so we read it directly, base64-encode it, and emit it as a `file`\n// SessionEvent via an injected sink — the worker writes that frame to stdout,\n// the daemon forwards it, and each client surfaces it (web download link,\n// Telegram document, CLI save-to-disk).\n\nimport { readFileSync, statSync } from 'node:fs'\nimport { basename, extname, isAbsolute, resolve } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\nconst MAX_DELIVER_BYTES = 25 * 1024 * 1024\n\nconst MIME: Record<string, string> = {\n '.pdf': 'application/pdf',\n '.txt': 'text/plain',\n '.md': 'text/markdown',\n '.csv': 'text/csv',\n '.json': 'application/json',\n '.html': 'text/html',\n '.zip': 'application/zip',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n}\n\nexport type FileSink = (file: { name: string; mimeType: string; data: string }) => void\n\nexport function deliverFileTool(cwd: string, sink: FileSink): ToolHandler {\n return {\n def: {\n name: 'deliver_file',\n description:\n 'Send a file from your workspace to the user so they can download it (web), receive it as a document (Telegram), or save it (CLI). Use this to deliver reports, exports, or any artifact you produced. Max 25 MB.',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file, relative to your workspace or absolute.',\n },\n },\n required: ['path'],\n additionalProperties: false,\n },\n },\n async invoke(args) {\n const p = String(args.path ?? '')\n if (!p) throw new Error('deliver_file: path is required')\n const abs = isAbsolute(p) ? p : resolve(cwd, p)\n let size: number\n try {\n size = statSync(abs).size\n } catch {\n throw new Error(`deliver_file: no such file: ${p}`)\n }\n if (size > MAX_DELIVER_BYTES) {\n throw new Error(\n `deliver_file: \"${basename(abs)}\" is too large (${(size / 1024 / 1024).toFixed(1)} MB > 25 MB)`,\n )\n }\n const name = basename(abs)\n const mimeType = MIME[extname(abs).toLowerCase()] ?? 'application/octet-stream'\n sink({ name, mimeType, data: readFileSync(abs).toString('base64') })\n return `Delivered \"${name}\" (${mimeType}) to the user.`\n },\n }\n}\n","import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\n// Files the agent may read/write in its private home directory.\n// BOOTSTRAP.md is readable but not writable — its lifecycle belongs to\n// the `bootstrap_done` tool, not `home_write`.\nconst HOME_FILES_READABLE = [\n 'IDENTITY.md',\n 'SOUL.md',\n 'BOOTSTRAP.md',\n 'AGENTS.md',\n 'TOOLS.md',\n 'HEARTBEAT.md',\n] as const\n\nconst HOME_FILES_WRITABLE = [\n 'IDENTITY.md',\n 'SOUL.md',\n 'AGENTS.md',\n 'TOOLS.md',\n 'HEARTBEAT.md',\n] as const\n\nexport function homeTools(agentDir: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'home_read',\n description:\n 'Read one of your own home files — your identity, soul, or behaviour rules. These files are private to you and are also injected into your system prompt; read them when you need to quote exact wording or check current state.',\n parameters: {\n type: 'object',\n properties: {\n file: { type: 'string', enum: [...HOME_FILES_READABLE] },\n },\n required: ['file'],\n },\n },\n async invoke(args) {\n const file = String(args.file ?? '')\n if (!HOME_FILES_READABLE.includes(file as (typeof HOME_FILES_READABLE)[number])) {\n throw new Error(\n `home_read: \"file\" must be one of ${HOME_FILES_READABLE.join(', ')}; got \"${file}\"`,\n )\n }\n const path = join(agentDir, file)\n try {\n return readFileSync(path, 'utf8')\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(`home_read: could not read ${file}: ${msg}`)\n }\n },\n },\n {\n def: {\n name: 'home_write',\n description:\n \"Overwrite one of your own home files. Use this to update your name, personality, or persistent self-definition. Do NOT use this for work output (use `write` / `edit` — those land in your group's shared directory) or for facts you want to remember (use `memory_write`). BOOTSTRAP.md is not writable here; call `bootstrap_done` to retire it.\",\n parameters: {\n type: 'object',\n properties: {\n file: { type: 'string', enum: [...HOME_FILES_WRITABLE] },\n content: { type: 'string', description: 'new full file content' },\n },\n required: ['file', 'content'],\n },\n },\n async invoke(args) {\n const file = String(args.file ?? '')\n if (!HOME_FILES_WRITABLE.includes(file as (typeof HOME_FILES_WRITABLE)[number])) {\n throw new Error(\n `home_write: \"file\" must be one of ${HOME_FILES_WRITABLE.join(', ')}; got \"${file}\"`,\n )\n }\n const content = typeof args.content === 'string' ? args.content : ''\n const path = join(agentDir, file)\n writeFileSync(path, content, 'utf8')\n return `wrote ${file} (${Buffer.byteLength(content, 'utf8')} bytes)`\n },\n },\n {\n def: {\n name: 'home_list',\n description: 'List your home files with their sizes.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const entries: string[] = []\n for (const file of HOME_FILES_READABLE) {\n const path = join(agentDir, file)\n try {\n const s = statSync(path)\n entries.push(`${file} (${s.size}b)`)\n } catch {\n // file not present — skip\n }\n }\n if (entries.length === 0) {\n const dirEntries = (() => {\n try {\n return readdirSync(agentDir)\n } catch {\n return []\n }\n })()\n return `(no home files found; agent dir contains: ${dirEntries.join(', ') || 'nothing'})`\n }\n return entries.join('\\n')\n },\n },\n ]\n}\n","// MCP proxy tools (worker-side definitions).\n//\n// The daemon discovers each enabled MCP server's tools (`tools/list`) before\n// the worker spawns and ships them in `WorkerInput.mcpTools`. Here we turn each\n// into a Bazilion `ToolHandler` whose `invoke` forwards to the daemon-side\n// connection pool over IPC via the injected `McpHost`. The LLM sees the\n// namespaced name (`mcp__<server>__<tool>`); the proxy carries the original\n// `serverId` + `rawName` so the daemon routes the `tools/call` correctly.\n\nimport type { InjectedMcpTool, McpHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\n/** Build proxy tools for every discovered MCP tool. */\nexport function mcpProxyTools(host: McpHost, tools: InjectedMcpTool[]): ToolHandler[] {\n return tools.map((t) => ({\n def: {\n name: t.toolName,\n description: t.description,\n parameters: t.inputSchema,\n },\n invoke: (args) => host.invoke(t.serverId, t.rawName, args),\n }))\n}\n","import type { MemoryBackend } from '../memory/types.ts'\nimport type { ToolHandler } from './types.ts'\n\nexport function memoryTools(memory: MemoryBackend): ToolHandler[] {\n return [\n {\n def: {\n name: 'memory_write',\n description:\n 'Write or update a memory note in the GROUP-SHARED memory. All agents in this group can read what you write. Use it for project knowledge, codebase notes, decisions, and findings — anything other agents in the group should benefit from. For personal notes about yourself (preferences, persona) use `home_write` on IDENTITY.md. For STABLE facts about the human you\\'re working with (their preferences, role, working hours, how they like to be addressed) use `user_md_get` then `user_md_write` — those land in every agent\\'s system prompt directly. Key is a path-like string with a markdown extension, e.g. \"auth-flow.md\" or \"decisions/2026-05-migration.md\".',\n parameters: {\n type: 'object',\n properties: {\n key: { type: 'string', description: 'memory key (relative path)' },\n content: { type: 'string', description: 'note content (plain text or markdown)' },\n },\n required: ['key', 'content'],\n },\n },\n async invoke(args) {\n const key = String(args.key ?? '')\n const content = String(args.content ?? '')\n if (!key) throw new Error('memory_write: \"key\" is required')\n const entry = await memory.write(key, content)\n return `wrote ${entry.key} (${entry.content.length} bytes)`\n },\n },\n {\n def: {\n name: 'memory_search',\n description:\n 'Search the group-shared memory by substring. Returns matching entry keys with short snippets around the match.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string' },\n limit: { type: 'number', description: 'max results (default 10)' },\n },\n required: ['query'],\n },\n },\n async invoke(args) {\n const query = String(args.query ?? '')\n if (!query) throw new Error('memory_search: \"query\" is required')\n const limit = typeof args.limit === 'number' ? args.limit : 10\n const hits = await memory.search(query, { limit })\n if (hits.length === 0) return 'no matches'\n return hits.map((h) => `${h.key}: ${h.snippet.replaceAll('\\n', ' ')}`).join('\\n')\n },\n },\n {\n def: {\n name: 'memory_read',\n description: 'Read a single entry from the group-shared memory by key.',\n parameters: {\n type: 'object',\n properties: { key: { type: 'string' } },\n required: ['key'],\n },\n },\n async invoke(args) {\n const key = String(args.key ?? '')\n if (!key) throw new Error('memory_read: \"key\" is required')\n const entry = await memory.read(key)\n return entry.content\n },\n },\n {\n def: {\n name: 'memory_list',\n description: 'List every entry in the group-shared memory with its byte size.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const all = await memory.list()\n if (all.length === 0) return '(empty)'\n return all.map((e) => `${e.key} (${e.content.length}b)`).join('\\n')\n },\n },\n ]\n}\n","import type { MessagingHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\ninterface MessagePayload {\n text: string\n [key: string]: unknown\n}\n\nfunction decodeText(payload: string): string {\n try {\n const parsed = JSON.parse(payload) as MessagePayload\n if (parsed && typeof parsed.text === 'string') return parsed.text\n } catch {\n // not JSON; return raw payload\n }\n return payload\n}\n\nexport function messagingTools(host: MessagingHost, fromAgentId: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'send_message',\n description:\n 'Send a message to another agent. Use the recipient\\'s agent id (UUID). Use this ONLY for things the recipient needs to ACT on: delegating a task, asking a peer for information you cannot get yourself, escalating a decision. Do NOT use it for status updates or to announce changes to group-shared resources — USER.md and the group memory backend both propagate to every agent in the group automatically on their next turn, so messages like \"I updated USER.md\" or \"I wrote a new memory note\" are pure noise and will trigger an inbox-wake loop on the recipient.',\n parameters: {\n type: 'object',\n properties: {\n to: { type: 'string', description: 'Recipient agent id' },\n text: { type: 'string', description: 'Message text' },\n reply_to: {\n type: 'string',\n description: 'Optional: id of the message you are replying to',\n },\n },\n required: ['to', 'text'],\n },\n },\n async invoke(args) {\n const to = String(args.to ?? '')\n const text = String(args.text ?? '')\n if (!to) throw new Error('send_message: \"to\" is required')\n if (!text) throw new Error('send_message: \"text\" is required')\n if (!(await host.agentExists(to))) {\n throw new Error(`send_message: agent not found: ${to}`)\n }\n const replyTo = typeof args.reply_to === 'string' ? args.reply_to : null\n const { messageId } = await host.sendMessage({\n from: fromAgentId,\n to,\n payload: JSON.stringify({ text }),\n replyTo,\n })\n return `sent message ${messageId}`\n },\n },\n {\n def: {\n name: 'read_inbox',\n description: 'Read messages addressed to you. Marks unread messages as read by default.',\n parameters: {\n type: 'object',\n properties: {\n include_read: {\n type: 'boolean',\n description: 'Also include already-read messages (default false)',\n },\n },\n },\n },\n async invoke(args) {\n const includeRead = args.include_read === true\n const messages = await host.listInbox(fromAgentId, { unreadOnly: !includeRead })\n if (messages.length === 0) return '(no messages)'\n const lines: string[] = []\n for (const m of messages) {\n lines.push(`from ${m.fromAgentId} [${m.id}]: ${decodeText(m.payload)}`)\n if (!m.readAt) await host.markRead(m.id)\n }\n return lines.join('\\n')\n },\n },\n {\n def: {\n name: 'wait_for_reply',\n description:\n 'Block until a reply to a message you sent arrives, or until the timeout expires.',\n parameters: {\n type: 'object',\n properties: {\n message_id: {\n type: 'string',\n description: 'id of the message you sent',\n },\n timeout_ms: {\n type: 'number',\n description: 'max wait in milliseconds (default 30000)',\n },\n poll_ms: {\n type: 'number',\n description: 'poll interval in milliseconds (default 200)',\n },\n },\n required: ['message_id'],\n },\n },\n async invoke(args) {\n const messageId = String(args.message_id ?? '')\n if (!messageId) throw new Error('wait_for_reply: \"message_id\" is required')\n const timeout = typeof args.timeout_ms === 'number' ? args.timeout_ms : 30000\n const poll = typeof args.poll_ms === 'number' ? args.poll_ms : 200\n const start = Date.now()\n while (Date.now() - start < timeout) {\n const replies = await host.findReplies(fromAgentId, messageId)\n if (replies.length > 0) {\n const r = replies[0]\n if (r) {\n if (!r.readAt) await host.markRead(r.id)\n return `reply from ${r.fromAgentId} [${r.id}]: ${decodeText(r.payload)}`\n }\n }\n await new Promise((r) => setTimeout(r, poll))\n }\n return `no reply within ${timeout}ms`\n },\n },\n ]\n}\n","// Group-shared USER.md read/write surface for agents.\n//\n// USER.md is inlined into every agent's system prompt every turn (capped at\n// 12 KB on the daemon-side host). Agents previously could only read it; now\n// they can also update it via read-modify-write so they can correct stale\n// facts, not just stack new ones. Concurrency between multiple agents in\n// the same group is handled via optimistic etag checks — see\n// `lib/user-md-host.ts` for the full rationale.\n//\n// Two tools:\n// - `user_md_get` — returns current content + an etag.\n// - `user_md_write` — replaces content; requires `if_match` to equal the\n// most recent etag, else returns a conflict.\n//\n// Project knowledge (codebase notes, decisions) still goes to `memory_write`.\n// Personal notes about the agent itself go to `home_write` on IDENTITY.md.\n\nimport type { UserMdHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\nexport function userMdTools(host: UserMdHost, groupId: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'user_md_get',\n description:\n 'Read the group-shared USER.md (facts every agent in the group knows about the human). Returns the current content followed by an `etag:` line — you MUST pass that etag back as `if_match` on the next `user_md_write` so the daemon can detect concurrent edits by other agents in the group. Always call this immediately before any `user_md_write`.',\n parameters: {\n type: 'object',\n properties: {},\n },\n },\n async invoke() {\n const { content, etag } = await host.get(groupId)\n const body = content.length > 0 ? content : '(USER.md is empty)'\n return `${body}\\n\\n---\\netag: ${etag}`\n },\n },\n {\n def: {\n name: 'user_md_write',\n description:\n 'Replace the group-shared USER.md with new content. Use this for STABLE user-specific facts (preferences, role, working hours, how the human likes to be addressed). MANDATORY workflow: (1) call `user_md_get` first, (2) integrate your change into the full content preserving everything unrelated, (3) call `user_md_write` with the merged content and the etag you got from `user_md_get`. If another agent in the group wrote to USER.md between your get and write, this returns an etag-mismatch error — just call `user_md_get` again and retry the merge. The full result must fit under 12 KB. For project knowledge use `memory_write`; for notes about yourself use `home_write` on IDENTITY.md.',\n parameters: {\n type: 'object',\n properties: {\n content: {\n type: 'string',\n description:\n 'Full new contents of USER.md (this is a complete replacement, NOT an append). Include everything you want to keep.',\n },\n if_match: {\n type: 'string',\n description:\n 'The etag returned by your most recent `user_md_get`. The write fails if USER.md changed in the meantime.',\n },\n },\n required: ['content', 'if_match'],\n },\n },\n async invoke(args) {\n const content = String(args.content ?? '')\n const ifMatch = String(args.if_match ?? '')\n if (!ifMatch) {\n throw new Error(\n 'user_md_write: \"if_match\" is required — call user_md_get first to obtain the current etag.',\n )\n }\n const { etag, totalBytes } = await host.write(groupId, content, ifMatch)\n return `wrote USER.md (${totalBytes} bytes, new etag: ${etag})`\n },\n },\n ]\n}\n","import { fetch as undiciFetch } from 'undici'\nimport type { ToolHandler } from './types.ts'\nimport {\n type ExtractMode,\n type ExtractResult,\n extractReadable,\n markdownToPlain,\n} from './web-extract.ts'\nimport { guardedFetch, SsrFBlockedError } from './web-ssrf.ts'\n\nconst DEFAULT_USER_AGENT =\n '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'\nconst DEFAULT_CACHE_TTL_MS = 15 * 60_000\nconst DEFAULT_CACHE_MAX = 100\nconst DEFAULT_MAX_LENGTH = 20_000\nconst DEFAULT_TIMEOUT_MS = 30_000\n// Raw body cap applied before parsing. Multi-megabyte HTML (Mintlify/Next.js\n// docs sites, ad-heavy pages) can stall or OOM Readability+linkedom's\n// synchronous DOM walk. 3 MB comfortably covers real articles; anything\n// larger gets truncated and the caller sees a note in the output.\nconst DEFAULT_MAX_BODY_BYTES = 3 * 1024 * 1024\n// If the primary Readability path returns fewer than this many characters\n// from a 2xx HTML response, and FIRECRAWL_API_KEY is configured, retry the\n// fetch via Firecrawl which renders JS server-side. Threshold is heuristic:\n// real articles routinely exceed 200 chars; pages that bottom out below it\n// are almost always JS-shell pages where extraction collapsed.\nconst FIRECRAWL_FALLBACK_THRESHOLD = 200\nconst FIRECRAWL_DEFAULT_URL = 'https://api.firecrawl.dev'\n\ninterface SearchResult {\n title: string\n url: string\n snippet: string\n}\n\nfunction stripHtml(s: string): string {\n return s\n .replace(/<[^>]*>/g, '')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .trim()\n}\n\n// --- search backends ---\n\nasync function braveSearch(\n query: string,\n limit: number,\n apiKey: string,\n fetchFn: typeof fetch,\n): Promise<SearchResult[]> {\n const params = new URLSearchParams({ q: query, count: String(limit) })\n const res = await fetchFn(`https://api.search.brave.com/res/v1/web/search?${params}`, {\n headers: {\n accept: 'application/json',\n 'accept-encoding': 'gzip',\n 'x-subscription-token': apiKey,\n },\n })\n if (!res.ok) throw new Error(`Brave Search: ${res.status} ${await res.text()}`)\n const data = (await res.json()) as {\n web?: { results?: { title?: string; url?: string; description?: string }[] }\n }\n return (data.web?.results ?? []).slice(0, limit).map((r) => ({\n title: r.title ?? '',\n url: r.url ?? '',\n snippet: r.description ? stripHtml(r.description) : '',\n }))\n}\n\nasync function searxngSearch(\n query: string,\n limit: number,\n baseURL: string,\n fetchFn: typeof fetch,\n): Promise<SearchResult[]> {\n const params = new URLSearchParams({ q: query, format: 'json' })\n const res = await fetchFn(`${baseURL}/search?${params}`, {\n headers: { accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`SearXNG: ${res.status} ${await res.text()}`)\n const data = (await res.json()) as {\n results?: { title?: string; url?: string; content?: string }[]\n }\n return (data.results ?? []).slice(0, limit).map((r) => ({\n title: r.title ?? '',\n url: r.url ?? '',\n snippet: r.content ?? '',\n }))\n}\n\n// --- error formatting ---\n\n/**\n * Flatten an Error and its `cause` chain into a single readable string.\n * undici surfaces network failures as `TypeError: fetch failed` with the\n * real reason (UND_ERR_*, ECONNRESET, certificate errors, …) stashed in\n * `err.cause`; without unwrapping it the agent only ever sees \"fetch\n * failed\" and has no path to diagnose or work around the problem.\n */\nfunction describeError(err: unknown): string {\n if (!(err instanceof Error)) return String(err)\n const parts: string[] = []\n const seen = new Set<unknown>()\n let cur: unknown = err\n while (cur instanceof Error && !seen.has(cur)) {\n seen.add(cur)\n const code = (cur as { code?: string }).code\n parts.push(code ? `[${code}] ${cur.message}` : cur.message)\n cur = (cur as { cause?: unknown }).cause\n }\n return parts.join(' — cause: ')\n}\n\n// --- bounded body reader ---\n\n/**\n * Stream `res.body` and accumulate up to `maxBytes`, then truncate. Returns\n * the decoded text (using the charset from content-type, falling back to\n * UTF-8) and a flag the caller can surface to the agent so it knows the\n * page was larger than the cap.\n */\nasync function readBodyCapped(\n res: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const ct = res.headers.get('content-type') ?? ''\n const charset = /charset=([^;]+)/i.exec(ct)?.[1]?.trim().toLowerCase() || 'utf-8'\n const decoder = new TextDecoder(charset, { fatal: false })\n const reader = res.body?.getReader()\n if (!reader) {\n const text = await res.text()\n if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true }\n return { text, truncated: false }\n }\n const chunks: Uint8Array[] = []\n let total = 0\n let truncated = false\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n if (total + value.byteLength > maxBytes) {\n const keep = maxBytes - total\n if (keep > 0) chunks.push(value.subarray(0, keep))\n truncated = true\n try {\n await reader.cancel()\n } catch {\n // body already settled\n }\n break\n }\n chunks.push(value)\n total += value.byteLength\n }\n const sum = chunks.reduce((s, c) => s + c.byteLength, 0)\n const merged = new Uint8Array(sum)\n let off = 0\n for (const c of chunks) {\n merged.set(c, off)\n off += c.byteLength\n }\n return { text: decoder.decode(merged), truncated }\n}\n\n// --- Firecrawl fallback ---\n\ninterface FirecrawlResponse {\n success?: boolean\n data?: {\n markdown?: string\n metadata?: { title?: string }\n }\n error?: string\n}\n\n/**\n * Last-resort HTML rendering via Firecrawl's `/v1/scrape` endpoint. Used\n * when the primary Readability path returns near-empty content from a 2xx\n * HTML response (JS-only shells, anti-bot walls, login redirects). Returns\n * `null` when Firecrawl is not configured or the call fails so the caller\n * falls back to the primary extraction unchanged — Firecrawl is best-effort,\n * never an error source.\n */\nasync function firecrawlScrape(\n url: string,\n mode: ExtractMode,\n env: NodeJS.ProcessEnv,\n fetchFn: typeof fetch,\n timeoutMs: number,\n): Promise<ExtractResult | null> {\n const apiKey = env.FIRECRAWL_API_KEY\n if (!apiKey) return null\n const base = (env.FIRECRAWL_URL ?? FIRECRAWL_DEFAULT_URL).replace(/\\/$/, '')\n const ac = new AbortController()\n const t = setTimeout(() => ac.abort(new Error('firecrawl timeout')), timeoutMs)\n try {\n const res = await fetchFn(`${base}/v1/scrape`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${apiKey}`,\n accept: 'application/json',\n },\n body: JSON.stringify({\n url,\n formats: ['markdown'],\n onlyMainContent: true,\n }),\n signal: ac.signal,\n })\n if (!res.ok) return null\n const body = (await res.json()) as FirecrawlResponse\n if (!body.success || !body.data?.markdown) return null\n const md = body.data.markdown\n const title = body.data.metadata?.title\n const text = mode === 'text' ? markdownToPlain(md) : md\n return title ? { text, title } : { text }\n } catch {\n return null\n } finally {\n clearTimeout(t)\n }\n}\n\n// --- per-URL cache ---\n\ninterface CacheEntry {\n value: ExtractResult\n expiresAt: number\n}\n\nfunction cacheGet(cache: Map<string, CacheEntry>, key: string): ExtractResult | null {\n const entry = cache.get(key)\n if (!entry) return null\n if (entry.expiresAt < Date.now()) {\n cache.delete(key)\n return null\n }\n // Refresh LRU position\n cache.delete(key)\n cache.set(key, entry)\n return entry.value\n}\n\nfunction cacheSet(\n cache: Map<string, CacheEntry>,\n key: string,\n value: ExtractResult,\n ttlMs: number,\n maxEntries: number,\n): void {\n cache.set(key, { value, expiresAt: Date.now() + ttlMs })\n while (cache.size > maxEntries) {\n const oldest = cache.keys().next().value\n if (!oldest) break\n cache.delete(oldest)\n }\n}\n\n// --- tool factory ---\n\nexport interface WebToolsOpts {\n fetchImpl?: typeof fetch\n env?: NodeJS.ProcessEnv\n /** Disable SSRF checks (tests against 127.0.0.1 mock servers). Default false. */\n allowPrivate?: boolean\n /** Cache TTL in ms. Default 15 min. */\n cacheTtlMs?: number\n /** Max cache entries. Default 100. */\n cacheMax?: number\n /** Fetch timeout in ms. Default 30s. */\n timeoutMs?: number\n /** Cap on raw response body bytes before parsing. Default 3 MB. */\n maxBodyBytes?: number\n /** Disable the Firecrawl fallback even when FIRECRAWL_API_KEY is set. */\n firecrawlDisabled?: boolean\n}\n\n/**\n * web_search backends (tried in order):\n * 1. Brave Search (env: BRAVE_API_KEY) — free tier, 2000 req/month\n * 2. SearXNG (env: SEARXNG_URL) — self-hosted, unlimited\n * 3. Error with setup instructions if neither is configured\n *\n * web_fetch: guarded fetch (SSRF-blocked private IPs) + Readability + markdown,\n * with per-URL in-memory cache (15 min TTL).\n */\nexport function webTools(opts?: WebToolsOpts): ToolHandler[] {\n // Default to undici 8's fetch so search-backend HTTP shares the same\n // stack as guardedFetch (and stays out of Node 24's bundled undici 7).\n const fetchFn = opts?.fetchImpl ?? (undiciFetch as unknown as typeof fetch)\n const env = opts?.env ?? process.env\n const allowPrivate = opts?.allowPrivate ?? false\n const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS\n const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const maxBodyBytes = opts?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES\n const firecrawlDisabled = opts?.firecrawlDisabled ?? false\n const cache = new Map<string, CacheEntry>()\n\n return [\n {\n def: {\n name: 'web_search',\n description:\n 'Search the internet. Returns a list of titles, URLs, and snippets. Requires BRAVE_API_KEY or SEARXNG_URL to be configured.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n limit: { type: 'number', description: 'Max results to return (default 5)' },\n },\n required: ['query'],\n },\n },\n async invoke(args) {\n const query = String(args.query ?? '')\n if (!query) throw new Error('web_search: query is required')\n const limit = typeof args.limit === 'number' ? args.limit : 5\n\n const braveKey = env.BRAVE_API_KEY\n if (braveKey) {\n const results = await braveSearch(query, limit, braveKey, fetchFn)\n return results.length === 0 ? 'no results' : formatResults(results)\n }\n\n const searxngUrl = env.SEARXNG_URL\n if (searxngUrl) {\n const results = await searxngSearch(query, limit, searxngUrl, fetchFn)\n return results.length === 0 ? 'no results' : formatResults(results)\n }\n\n throw new Error(\n 'web_search: no search backend configured. Set BRAVE_API_KEY (free at https://brave.com/search/api/) or SEARXNG_URL.',\n )\n },\n },\n {\n def: {\n name: 'web_fetch',\n description:\n 'Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. When the primary extraction returns near-empty content from a 2xx HTML page (typical of JS-only shells), the tool automatically retries via Firecrawl if FIRECRAWL_API_KEY is configured. Results are cached for 15 minutes.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The URL to fetch (http or https)' },\n max_length: {\n type: 'number',\n description: 'Max characters to return (default 20000)',\n },\n extract_mode: {\n type: 'string',\n enum: ['markdown', 'text'],\n description: 'Output format for HTML pages. Default \"markdown\".',\n },\n },\n required: ['url'],\n },\n },\n async invoke(args) {\n const url = String(args.url ?? '')\n if (!url) throw new Error('web_fetch: url is required')\n const maxLen = typeof args.max_length === 'number' ? args.max_length : DEFAULT_MAX_LENGTH\n const mode: ExtractMode = args.extract_mode === 'text' ? 'text' : 'markdown'\n const cacheKey = `${mode}|${url}`\n\n const cached = cacheGet(cache, cacheKey)\n if (cached) return formatOutput(cached, maxLen)\n\n let result: GuardedFetchResultShape | null = null\n try {\n result = await guardedFetch({\n url,\n fetchImpl: opts?.fetchImpl,\n allowPrivate,\n timeoutMs,\n init: {\n headers: {\n 'user-agent': DEFAULT_USER_AGENT,\n accept: 'text/html,application/xhtml+xml,application/json,text/plain,*/*',\n 'accept-language': 'en-US,en;q=0.9',\n },\n },\n })\n if (!result.response.ok) {\n throw new Error(`${result.response.status} ${result.response.statusText}`)\n }\n const ct = result.response.headers.get('content-type') ?? ''\n const isHtml = ct.includes('text/html') || ct.includes('xhtml')\n const { text: body, truncated } = await readBodyCapped(result.response, maxBodyBytes)\n let extracted: ExtractResult\n if (isHtml) {\n extracted = extractReadable(body, result.finalUrl, mode)\n } else if (ct.includes('application/json')) {\n try {\n extracted = { text: JSON.stringify(JSON.parse(body), null, 2) }\n } catch {\n extracted = { text: body }\n }\n } else {\n extracted = { text: body }\n }\n // Firecrawl fallback: only meaningful for HTML pages where the\n // primary extractor collapsed. Skip JSON / plain-text bodies and\n // skip when extraction already produced something substantial.\n if (\n isHtml &&\n !firecrawlDisabled &&\n extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD\n ) {\n const rescued = await firecrawlScrape(result.finalUrl, mode, env, fetchFn, timeoutMs)\n if (rescued) {\n extracted = {\n ...rescued,\n text: `${rescued.text}\\n\\n[content rendered via Firecrawl fallback — primary extraction returned ${extracted.text.length} chars]`,\n }\n }\n }\n if (truncated) {\n extracted = {\n ...extracted,\n text: `${extracted.text}\\n\\n[raw body truncated at ${maxBodyBytes} bytes before extraction — page exceeded the size cap]`,\n }\n }\n cacheSet(cache, cacheKey, extracted, cacheTtlMs, cacheMax)\n return formatOutput(extracted, maxLen)\n } catch (err) {\n if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`)\n throw new Error(`web_fetch: ${describeError(err)}`)\n } finally {\n if (result) await result.release()\n }\n },\n },\n ]\n}\n\ntype GuardedFetchResultShape = Awaited<ReturnType<typeof guardedFetch>>\n\nfunction formatOutput(r: ExtractResult, maxLen: number): string {\n const body = r.title ? `# ${r.title}\\n\\n${r.text}` : r.text\n if (body.length > maxLen) return `${body.slice(0, maxLen)}\\n\\n[truncated at ${maxLen} chars]`\n return body\n}\n\nfunction formatResults(results: SearchResult[]): string {\n return results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join('\\n\\n')\n}\n","import { Readability } from '@mozilla/readability'\nimport { parseHTML } from 'linkedom'\n\nexport type ExtractMode = 'markdown' | 'text'\n\nexport interface ExtractResult {\n text: string\n title?: string\n}\n\nfunction decodeEntities(value: string): string {\n return value\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\")\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))\n .replace(/&#(\\d+);/gi, (_, dec) => String.fromCharCode(Number.parseInt(dec, 10)))\n}\n\nfunction stripTags(value: string): string {\n return decodeEntities(value.replace(/<[^>]+>/g, ''))\n}\n\nfunction normalizeWhitespace(value: string): string {\n return value\n .replace(/\\r/g, '')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .replace(/[ \\t]{2,}/g, ' ')\n .trim()\n}\n\nfunction htmlToMarkdown(html: string): { text: string; title?: string } {\n const titleMatch = html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)\n const title = titleMatch ? normalizeWhitespace(stripTags(titleMatch[1] ?? '')) : undefined\n let text = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '')\n .replace(/<nav[\\s\\S]*?<\\/nav>/gi, '')\n .replace(/<header[\\s\\S]*?<\\/header>/gi, '')\n .replace(/<footer[\\s\\S]*?<\\/footer>/gi, '')\n text = text.replace(/<a\\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>([\\s\\S]*?)<\\/a>/gi, (_, href, body) => {\n const label = normalizeWhitespace(stripTags(body))\n return label ? `[${label}](${href})` : href\n })\n text = text.replace(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_, level, body) => {\n const n = Math.max(1, Math.min(6, Number.parseInt(level, 10)))\n return `\\n${'#'.repeat(n)} ${normalizeWhitespace(stripTags(body))}\\n`\n })\n text = text.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, (_, body) => {\n const label = normalizeWhitespace(stripTags(body))\n return label ? `\\n- ${label}` : ''\n })\n text = text\n .replace(/<(br|hr)\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, '\\n')\n text = stripTags(text)\n return { text: normalizeWhitespace(text), title }\n}\n\nexport function markdownToPlain(md: string): string {\n let t = md\n t = t.replace(/!\\[[^\\]]*]\\([^)]+\\)/g, '')\n t = t.replace(/\\[([^\\]]+)]\\([^)]+\\)/g, '$1')\n t = t.replace(/```[\\s\\S]*?```/g, (block) =>\n block.replace(/```[^\\n]*\\n?/g, '').replace(/```/g, ''),\n )\n t = t.replace(/`([^`]+)`/g, '$1')\n t = t.replace(/^#{1,6}\\s+/gm, '')\n t = t.replace(/^\\s*[-*+]\\s+/gm, '')\n t = t.replace(/^\\s*\\d+\\.\\s+/gm, '')\n return normalizeWhitespace(t)\n}\n\n/**\n * Extract readable content from HTML using Readability, with a regex-based\n * markdown fallback when Readability can't identify an article.\n */\nexport function extractReadable(html: string, url: string, mode: ExtractMode): ExtractResult {\n const fallback = (): ExtractResult => {\n const r = htmlToMarkdown(html)\n return mode === 'text' ? { text: markdownToPlain(r.text), title: r.title } : r\n }\n try {\n const { document } = parseHTML(html)\n try {\n ;(document as unknown as { baseURI?: string }).baseURI = url\n } catch {\n // best-effort\n }\n type ReadabilityArg = ConstructorParameters<typeof Readability>[0]\n const parsed = new Readability(document as unknown as ReadabilityArg, {\n charThreshold: 0,\n }).parse()\n if (!parsed?.content) return fallback()\n const title = parsed.title || undefined\n if (mode === 'text') {\n const text = normalizeWhitespace(parsed.textContent ?? '')\n return text ? { text, title } : fallback()\n }\n const rendered = htmlToMarkdown(parsed.content)\n return { text: rendered.text, title: title ?? rendered.title }\n } catch {\n return fallback()\n }\n}\n","import { lookup as dnsLookupCb, type LookupAddress } from 'node:dns'\nimport { lookup as dnsLookup } from 'node:dns/promises'\nimport { Agent, type Dispatcher, fetch as undiciFetch } from 'undici'\n\nexport class SsrFBlockedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SsrFBlockedError'\n }\n}\n\nconst BLOCKED_HOSTNAMES = new Set(['localhost', 'metadata.google.internal'])\nconst PRIVATE_IPV6_PREFIXES = ['fe80:', 'fec0:', 'fc', 'fd']\n\nfunction normalizeHostname(hostname: string): string {\n let h = hostname.trim().toLowerCase().replace(/\\.$/, '')\n if (h.startsWith('[') && h.endsWith(']')) h = h.slice(1, -1)\n return h\n}\n\nfunction parseIpv4(address: string): number[] | null {\n const parts = address.split('.')\n if (parts.length !== 4) return null\n const nums = parts.map((p) => Number.parseInt(p, 10))\n if (nums.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return null\n return nums\n}\n\nfunction isPrivateIpv4(parts: number[]): boolean {\n const [a, b] = parts as [number, number, number, number]\n if (a === 0 || a === 10 || a === 127) return true\n if (a === 169 && b === 254) return true\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 192 && b === 168) return true\n if (a === 100 && b >= 64 && b <= 127) return true\n return false\n}\n\nexport function isPrivateIpAddress(address: string): boolean {\n let norm = address.trim().toLowerCase()\n if (norm.startsWith('[') && norm.endsWith(']')) norm = norm.slice(1, -1)\n if (!norm) return false\n if (norm.startsWith('::ffff:')) {\n const mapped = norm.slice('::ffff:'.length)\n const ipv4 = parseIpv4(mapped)\n if (ipv4) return isPrivateIpv4(ipv4)\n }\n if (norm.includes(':')) {\n if (norm === '::' || norm === '::1') return true\n return PRIVATE_IPV6_PREFIXES.some((p) => norm.startsWith(p))\n }\n const ipv4 = parseIpv4(norm)\n return ipv4 ? isPrivateIpv4(ipv4) : false\n}\n\nexport function isBlockedHostname(hostname: string): boolean {\n const h = normalizeHostname(hostname)\n if (!h) return false\n if (BLOCKED_HOSTNAMES.has(h)) return true\n return h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.internal')\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address: string | LookupAddress[],\n family?: number,\n) => void\n\nfunction createPinnedLookup(hostname: string, addresses: string[]): typeof dnsLookupCb {\n const normalized = normalizeHostname(hostname)\n const records = addresses.map((address) => ({\n address,\n family: (address.includes(':') ? 6 : 4) as 4 | 6,\n }))\n let index = 0\n return ((host: string, options?: unknown, callback?: unknown) => {\n const cb: LookupCallback =\n typeof options === 'function' ? (options as LookupCallback) : (callback as LookupCallback)\n if (!cb) return\n if (normalizeHostname(host) !== normalized) {\n if (typeof options === 'function' || options === undefined) {\n return (dnsLookupCb as unknown as (h: string, cb: LookupCallback) => void)(host, cb)\n }\n return (dnsLookupCb as unknown as (h: string, o: unknown, cb: LookupCallback) => void)(\n host,\n options,\n cb,\n )\n }\n const opts =\n typeof options === 'object' && options !== null\n ? (options as { all?: boolean; family?: number })\n : {}\n const family = typeof options === 'number' ? options : (opts.family ?? 0)\n const candidates =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records\n const usable = candidates.length > 0 ? candidates : records\n if (opts.all) {\n cb(null, usable as LookupAddress[])\n return\n }\n const chosen = usable[index % usable.length]\n if (!chosen) return\n index += 1\n cb(null, chosen.address, chosen.family)\n }) as typeof dnsLookupCb\n}\n\nasync function resolveAndCheck(hostname: string): Promise<string[]> {\n const norm = normalizeHostname(hostname)\n if (!norm) throw new SsrFBlockedError('Invalid hostname')\n if (isBlockedHostname(norm)) throw new SsrFBlockedError(`Blocked hostname: ${hostname}`)\n if (isPrivateIpAddress(norm)) throw new SsrFBlockedError('Blocked: private IP literal')\n const results = await dnsLookup(norm, { all: true })\n if (results.length === 0) throw new SsrFBlockedError(`Cannot resolve: ${hostname}`)\n for (const r of results) {\n if (isPrivateIpAddress(r.address)) throw new SsrFBlockedError('Blocked: resolves to private IP')\n }\n return Array.from(new Set(results.map((r) => r.address)))\n}\n\ntype FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>\n\nexport interface GuardedFetchOptions {\n url: string\n fetchImpl?: FetchLike\n init?: RequestInit\n maxRedirects?: number\n timeoutMs?: number\n signal?: AbortSignal\n /** If true, skip all SSRF checks (for tests hitting 127.0.0.1 mocks). */\n allowPrivate?: boolean\n}\n\nexport interface GuardedFetchResult {\n response: Response\n finalUrl: string\n release: () => Promise<void>\n}\n\nfunction isRedirectStatus(s: number): boolean {\n return s === 301 || s === 302 || s === 303 || s === 307 || s === 308\n}\n\nasync function closeDispatcher(d: Dispatcher | null): Promise<void> {\n if (!d) return\n try {\n await d.close()\n } catch {\n // ignore\n }\n}\n\nexport async function guardedFetch(opts: GuardedFetchOptions): Promise<GuardedFetchResult> {\n // Use undici's own fetch (not globalThis.fetch) so the `dispatcher` Agent\n // we attach below is the same undici major version as the fetch impl\n // consuming it. Node 24 ships undici 7.x as its built-in fetch; mixing\n // a standalone undici 8.x Agent with the 7.x dispatcher fails with\n // `UND_ERR_INVALID_ARG: invalid onRequestStart method` because the\n // diagnostics-channel handler signatures changed between majors.\n const fetcher: FetchLike = opts.fetchImpl ?? (undiciFetch as unknown as FetchLike)\n const maxRedirects = opts.maxRedirects ?? 3\n const abortController = new AbortController()\n const timeoutId = opts.timeoutMs\n ? setTimeout(() => abortController.abort(new Error('timeout')), opts.timeoutMs)\n : null\n if (opts.signal) {\n if (opts.signal.aborted) abortController.abort(opts.signal.reason)\n else\n opts.signal.addEventListener('abort', () => abortController.abort(opts.signal?.reason), {\n once: true,\n })\n }\n\n let current = opts.url\n const visited = new Set<string>()\n let redirects = 0\n let dispatcher: Dispatcher | null = null\n\n const release = async (): Promise<void> => {\n if (timeoutId) clearTimeout(timeoutId)\n await closeDispatcher(dispatcher)\n dispatcher = null\n }\n\n while (true) {\n let parsed: URL\n try {\n parsed = new URL(current)\n } catch {\n await release()\n throw new Error('Invalid URL')\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n await release()\n throw new Error('Invalid URL: must be http or https')\n }\n\n // Pin DNS only when we control the dispatcher (native fetch path).\n const canPin = !opts.fetchImpl\n if (!opts.allowPrivate) {\n const addrs = await resolveAndCheck(parsed.hostname)\n await closeDispatcher(dispatcher)\n dispatcher = canPin\n ? new Agent({ connect: { lookup: createPinnedLookup(parsed.hostname, addrs) } })\n : null\n }\n\n const init: RequestInit = {\n ...(opts.init ?? {}),\n redirect: 'manual',\n signal: abortController.signal,\n }\n // `dispatcher` is a Node fetch extension not reflected in the DOM\n // RequestInit type — and the undici Dispatcher type brand can diverge\n // from the one @types/node bundles. Stamp it on via a cast.\n if (dispatcher) (init as unknown as { dispatcher: Dispatcher }).dispatcher = dispatcher\n\n let res: Response\n try {\n res = await fetcher(parsed.toString(), init)\n } catch (err) {\n await release()\n throw err\n }\n\n if (isRedirectStatus(res.status)) {\n const loc = res.headers.get('location')\n if (!loc) {\n await release()\n throw new Error(`Redirect ${res.status} missing Location header`)\n }\n redirects += 1\n if (redirects > maxRedirects) {\n await release()\n throw new Error(`Too many redirects (> ${maxRedirects})`)\n }\n const next = new URL(loc, parsed).toString()\n if (visited.has(next)) {\n await release()\n throw new Error('Redirect loop')\n }\n visited.add(next)\n void res.body?.cancel()\n current = next\n continue\n }\n\n return { response: res, finalUrl: parsed.toString(), release }\n }\n}\n"],"mappings":"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","names":["randomUUID","join","mkdirSync","writeFileSync","join","existsSync","readFileSync","join","existsSync","readFileSync","writeFileSync","join","existsSync","mkdirSync","join","existsSync","join","readdirSync","readFileSync","join","join","existsSync","rmSync","writeFileSync","join","readdirSync","rmSync","rmSync","randomUUID","randomUUID","randomUUID","randomBytes","randomUUID","existsSync","readFileSync","existsSync","readdirSync","rmSync","statSync","join","resolve","readFileSync","existsSync","mkdirSync","readdirSync","readFileSync","rmSync","statSync","writeFileSync","dirname","join","existsSync","mkdirSync","readdirSync","statSync","basename","join","existsSync","readFileSync","join","Type","existsSync","rmSync","join","readFileSync","statSync","basename","resolve","readdirSync","readFileSync","statSync","writeFileSync","join","undiciFetch","ipv4","undiciFetch","Type","existsSync","mkdirSync","join","existsSync","readdirSync","join","statSync","randomUUID","resolve","join"]}
1
+ {"version":3,"sources":["../../daemon/src/runtime/worker/entry.ts","../../daemon/src/core/agent/delete.ts","../../daemon/src/core/agent/resolve.ts","../../daemon/src/core/profile/identity.ts","../../daemon/src/core/agent/spawn.ts","../../daemon/src/core/profile/load.ts","../../daemon/src/core/profile/seed.ts","../../daemon/src/core/group/register.ts","../../daemon/src/core/profile/create.ts","../../daemon/src/core/skills/discover.ts","../../daemon/src/core/db/client.ts","../../daemon/src/core/db/migrate.ts","../../daemon/src/core/paths.ts","../../daemon/src/core/profile/delete.ts","../../daemon/src/core/profile/update.ts","../../daemon/src/core/profile-group/spawn.ts","../../daemon/src/core/profile-group/rm-with-retry.ts","../../daemon/src/core/services.ts","../../daemon/src/core/repos/config.ts","../../daemon/src/core/repos/mcpServers.ts","../../daemon/src/core/repos/messages.ts","../../daemon/src/core/repos/secrets.ts","../../daemon/src/core/repos/triggers.ts","../../daemon/src/core/repos/webTokens.ts","../../daemon/src/core/secrets.ts","../../daemon/src/core/skills/import.ts","../../daemon/src/core/skills/parse.ts","../../daemon/src/runtime/memory/qmd.ts","../../daemon/src/runtime/pi/events.ts","../../daemon/src/runtime/pi/session.ts","../../daemon/src/runtime/providers/pi-adapter.ts","../../daemon/src/runtime/auth/openai-codex.ts","../../daemon/src/runtime/session/prompt.ts","../../daemon/src/runtime/pi/tools.ts","../../daemon/src/runtime/tools/bootstrap.ts","../../daemon/src/runtime/tools/browser.ts","../../daemon/src/runtime/tools/deliver-file.ts","../../daemon/src/runtime/tools/home.ts","../../daemon/src/runtime/tools/mcp.ts","../../daemon/src/runtime/tools/memory.ts","../../daemon/src/runtime/tools/messaging.ts","../../daemon/src/runtime/tools/user-md.ts","../../daemon/src/runtime/tools/web.ts","../../daemon/src/runtime/tools/web-extract.ts","../../daemon/src/runtime/tools/web-ssrf.ts"],"sourcesContent":["// Subprocess entrypoint for whole-run isolation — no SQLite handle inside.\n//\n// Reads a `WorkerInput` JSON object from stdin describing the turn to run:\n// the pre-resolved agent record, enabled-provider set, and message text.\n// The daemon does all DB-backed lookups before spawning us.\n//\n// Live messaging (the `send_message` / `read_inbox` / `wait_for_reply` tools)\n// is delegated back to the daemon over Node IPC: each tool invocation sends\n// a `{type: 'rpc', ...}` message and awaits the matching reply. The parent\n// dispatches the request through its own DB-backed `MessagingHost`.\n//\n// Ownership split now reads:\n// - daemon: SQLite handle, agent resolution, secrets envelope, scheduler,\n// run-cancel registry, messaging RPC dispatch.\n// - pi: conversation transcript, compaction entries, tool execution,\n// provider retries.\n// - this worker: glue between the two — runs `session.prompt(message)`,\n// translates `AgentSessionEvent`s into Bazilion `SessionEvent`s, emits\n// them as NDJSON `ChatFrame`s on stdout.\n//\n// Cancellation: SIGTERM from the parent → `session.abort()` → pi aborts the\n// current prompt → `agent_end` fires with error → translator emits an\n// `error` event so the client renders \"cancelled\" identically to other\n// failures.\n\nimport { randomUUID } from 'node:crypto'\nimport { join } from 'node:path'\nimport type { Attachment, ChatFrame, ResolvedAgent } from '@bazilion/api-types'\nimport { resolvePaths } from '../../core/index.ts'\nimport { qmdBackend } from '../memory/qmd.ts'\nimport { piMessagesToProviderView, translatePiEvent } from '../pi/events.ts'\nimport { createBazilionSession } from '../pi/session.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n IpcReply,\n IpcRequest,\n McpHost,\n MessagingHost,\n RpcMethod,\n UserMdGetResult,\n UserMdHost,\n UserMdWriteResult,\n} from './ipc-protocol.ts'\n\ninterface WorkerInput {\n agent: ResolvedAgent\n message: string\n enabledProviders: string[]\n /** Pre-fetched API key for OAuth providers; undefined for env-key ones. */\n apiKey?: string\n /** When true, expose the `browser_*` tools (proxied to the daemon pool). */\n browserEnabled?: boolean\n /** MCP tools discovered daemon-side, exposed as IPC-proxied proxy tools. */\n mcpTools?: InjectedMcpTool[]\n /** Image attachments — passed to pi's prompt (vision). Pre-classified by the daemon. */\n images?: Attachment[]\n}\n\nfunction emit(frame: ChatFrame): void {\n process.stdout.write(`${JSON.stringify(frame)}\\n`)\n}\n\nasync function readInput(): Promise<WorkerInput> {\n const chunks: Buffer[] = []\n for await (const c of process.stdin) chunks.push(c as Buffer)\n const raw = Buffer.concat(chunks).toString('utf8').trim()\n if (!raw) throw new Error('worker: empty stdin input')\n const parsed = JSON.parse(raw) as WorkerInput\n if (\n !parsed.agent ||\n typeof parsed.message !== 'string' ||\n !Array.isArray(parsed.enabledProviders)\n ) {\n throw new Error('worker: stdin must be {agent, message, enabledProviders}')\n }\n return parsed\n}\n\n/**\n * Single shared IPC client. Both `MessagingHost` and `UserMdHost` route\n * through this — the `id` correlation id disambiguates replies, so multiple\n * concurrent tool calls (e.g. read_inbox + user_md_append on the same turn)\n * each get their own promise resolved without crosstalk. The promise\n * rejects when the channel disconnects mid-request; that path only fires\n * during shutdown, where the LLM loop has already been aborted, so the\n * unresolved tool call doesn't matter.\n */\ntype IpcCall = <T>(method: RpcMethod, args: unknown) => Promise<T>\n\nfunction createIpcClient(): IpcCall {\n type Pending = { resolve: (value: unknown) => void; reject: (err: Error) => void }\n const pending = new Map<string, Pending>()\n\n process.on('message', (msg: unknown) => {\n if (!isIpcReply(msg)) return\n const entry = pending.get(msg.id)\n if (!entry) return\n pending.delete(msg.id)\n if (msg.ok) entry.resolve(msg.result)\n else entry.reject(new Error(msg.error))\n })\n\n process.on('disconnect', () => {\n for (const [, entry] of pending) entry.reject(new Error('worker IPC channel disconnected'))\n pending.clear()\n })\n\n return <T>(method: RpcMethod, args: unknown): Promise<T> => {\n if (!process.send) {\n return Promise.reject(new Error('worker: no IPC channel — daemon must spawn with stdio:ipc'))\n }\n const id = randomUUID()\n // The wire format is uniform \"method + args\"; the parent's dispatcher\n // re-narrows on `method`. TS can't follow that union construction here,\n // so we cast through unknown rather than push the discrimination through\n // every call site.\n const message = { type: 'rpc', id, method, args } as unknown as IpcRequest\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: (v) => resolve(v as T), reject })\n // process.send is fire-and-forget; the callback only signals serialization\n // failure. Treat it as a reject to surface the bug rather than hang.\n process.send?.(message, undefined, undefined, (err) => {\n if (err) {\n pending.delete(id)\n reject(err)\n }\n })\n })\n }\n}\n\nfunction createIpcMessagingHost(call: IpcCall): MessagingHost {\n return {\n agentExists: (agentId) => call<boolean>('agentExists', { agentId }),\n sendMessage: (input) => call<{ messageId: string }>('sendMessage', input),\n listInbox: (agentId, opts) => call('listInbox', { agentId, unreadOnly: opts.unreadOnly }),\n markRead: async (messageId) => {\n await call<null>('markRead', { messageId })\n },\n findReplies: (agentId, replyTo) => call('findReplies', { agentId, replyTo }),\n }\n}\n\nfunction createIpcUserMdHost(call: IpcCall): UserMdHost {\n return {\n get: (groupId) => call<UserMdGetResult>('userMdGet', { groupId }),\n write: (groupId, content, ifMatch) =>\n call<UserMdWriteResult>('userMdWrite', { groupId, content, ifMatch }),\n }\n}\n\nfunction createIpcBrowserHost(call: IpcCall): BrowserHost {\n return {\n invoke: (agentId, action, args) => call('browserInvoke', { agentId, action, args }),\n }\n}\n\nfunction createIpcMcpHost(call: IpcCall): McpHost {\n return {\n invoke: (serverId, toolName, args) => call('mcpInvoke', { serverId, toolName, args }),\n }\n}\n\nfunction isIpcReply(msg: unknown): msg is IpcReply {\n if (!msg || typeof msg !== 'object') return false\n const m = msg as Record<string, unknown>\n return m.type === 'rpc-reply' && typeof m.id === 'string' && typeof m.ok === 'boolean'\n}\n\nasync function main(): Promise<void> {\n // Install signal handlers FIRST so a SIGTERM that arrives during setup\n // doesn't kill us with the Node default (exit-by-signal, no frame emitted).\n // Pre-session, we just exit cleanly with a synthetic `cancelled` event;\n // post-session, we delegate to `session.abort()` so pi unwinds the\n // provider fetch and surfaces its own `error` event.\n let aborted = false\n let abortSession: (() => void) | null = null\n const onSignal = (): void => {\n aborted = true\n if (abortSession) {\n abortSession()\n } else {\n try {\n emit({ kind: 'event', event: { type: 'error', error: 'cancelled' } })\n } catch {}\n process.exit(0)\n }\n }\n process.on('SIGTERM', onSignal)\n process.on('SIGINT', onSignal)\n\n const { agent, message, enabledProviders, apiKey, browserEnabled, mcpTools, images } =\n await readInput()\n\n // Path resolution still happens in the worker — `resolvePaths()` only reads\n // the `BAZILION_HOME` env var the daemon hands down. No DB, no filesystem\n // probe of the live tree.\n const paths = resolvePaths()\n\n const memory = qmdBackend(join(agent.group.path, 'memory'))\n await memory.init()\n\n const ipcCall = createIpcClient()\n const messagingHost = createIpcMessagingHost(ipcCall)\n const userMdHost = createIpcUserMdHost(ipcCall)\n const browserHost = browserEnabled ? createIpcBrowserHost(ipcCall) : undefined\n const mcpHost = mcpTools && mcpTools.length > 0 ? createIpcMcpHost(ipcCall) : undefined\n\n const { session, dispose } = await createBazilionSession({\n agent,\n paths,\n env: process.env,\n memory,\n enabledProviders: new Set(enabledProviders),\n messagingHost,\n userMdHost,\n apiKey,\n browserHost,\n mcpHost,\n mcpTools,\n // deliver_file emits a `file` event straight onto our stdout frame stream.\n fileSink: (f) => emit({ kind: 'event', event: { type: 'file', ...f } }),\n })\n\n abortSession = (): void => {\n void session.abort()\n }\n\n const unsubscribe = session.subscribe((piEvent) => {\n for (const ev of translatePiEvent(piEvent)) {\n emit({ kind: 'event', event: ev })\n }\n })\n\n try {\n // Map Bazilion image attachments to pi's ImageContent and pass them as the\n // prompt's `images` option — the model sees them via vision.\n const promptImages = (images ?? []).map((img) => ({\n type: 'image' as const,\n data: img.data,\n mimeType: img.mimeType,\n }))\n await session.prompt(message, promptImages.length > 0 ? { images: promptImages } : undefined)\n await session.agent.waitForIdle()\n\n emit({\n kind: 'done',\n messages: piMessagesToProviderView(session.agent.state.messages),\n })\n } catch (err) {\n if (!aborted) {\n emit({ kind: 'event', event: { type: 'error', error: (err as Error).message } })\n }\n emit({ kind: 'fatal', error: (err as Error).message })\n process.exitCode = 1\n } finally {\n unsubscribe()\n process.off('SIGTERM', onSignal)\n process.off('SIGINT', onSignal)\n dispose()\n // Detach from the parent's IPC channel so the worker's event loop can\n // exit. `process.on('message', …)` registered above otherwise keeps\n // the loop alive even after the turn finishes — Node treats an open\n // IPC handle as a live ref the same way it treats an open socket.\n try {\n process.disconnect?.()\n } catch {\n // already disconnected (parent closed first) — fine\n }\n }\n}\n\nmain().catch((err) => {\n try {\n emit({ kind: 'fatal', error: (err as Error)?.message ?? String(err) })\n } catch {}\n process.exit(1)\n})\n","import { existsSync, rmSync } from 'node:fs'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as agentRepo from '../repos/agents.ts'\n\nexport function deleteAgent(db: BazilionDb, id: string): void {\n const agent = agentRepo.get(db, id)\n if (!agent) throw new Error(`agent not found: ${id}`)\n const fullId = agent.id\n\n db.raw.transaction(() => {\n // messages.from_agent_id and to_agent_id reference agents(id) with no ON\n // DELETE rule, and messages.reply_to references messages(id) the same way,\n // so a naive DELETE of the agent fails if it has any mailbox history.\n // Null out inbound reply pointers to this agent's messages, then purge the\n // messages themselves, then let agentRepo.remove cascade the rest\n // (agent_skills, runs, events). agents.group_id is `ON DELETE RESTRICT`\n // from the group side, but the agent row itself goes away freely.\n db.raw.run(\n `UPDATE messages SET reply_to = NULL\n WHERE reply_to IN (SELECT id FROM messages WHERE from_agent_id = ? OR to_agent_id = ?)`,\n [fullId, fullId],\n )\n db.raw.run('DELETE FROM messages WHERE from_agent_id = ? OR to_agent_id = ?', [fullId, fullId])\n agentRepo.remove(db, fullId)\n })()\n\n if (existsSync(agent.dir)) {\n rmSync(agent.dir, { recursive: true, force: true })\n }\n}\n","import { join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { loadIdentityFromFile } from '../profile/identity.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport function resolveAgent(db: BazilionDb, paths: Paths, agentId: string): ResolvedAgent {\n // `agentRepo.get` accepts either a full UUID or an unambiguous prefix.\n const agent = agentRepo.get(db, agentId)\n if (!agent) throw new Error(`agent not found: ${agentId}`)\n\n // Parse the agent's OWN IDENTITY.md (not the profile's) so the detail page\n // can show avatar/creature. `null` when the file is missing or still holds\n // only placeholder values.\n agent.identity = loadIdentityFromFile(join(agent.dir, 'IDENTITY.md'))\n\n const profile = profileRepo.get(db, agent.profileId)\n if (!profile) {\n throw new Error(`profile not found for agent ${agentId}: ${agent.profileId}`)\n }\n\n const group = groupRepo.get(db, agent.groupId, paths)\n if (!group) {\n throw new Error(`group not found for agent ${agentId}: ${agent.groupId}`)\n }\n\n return {\n agent,\n profile,\n model: agent.modelOverride ?? profile.defaultModel,\n reasoningLevel: agent.reasoningLevel,\n group,\n skills: agentRepo.listAttachedSkills(db, agentId),\n }\n}\n","import { readFileSync } from 'node:fs'\nimport type { AgentIdentityFile } from '@bazilion/api-types'\n\nconst IDENTITY_PLACEHOLDER_VALUES = new Set([\n 'pick something you like',\n 'ai? robot? familiar? ghost in the machine? something weirder?',\n 'how do you come across? sharp? warm? chaotic? calm?',\n 'your signature - pick one that feels right',\n 'workspace-relative path, http(s) url, or data uri',\n 'an http(s) url or data uri - optional',\n])\n\nfunction normalizeIdentityValue(value: string): string {\n let normalized = value.trim()\n normalized = normalized.replace(/^[*_]+|[*_]+$/g, '').trim()\n if (normalized.startsWith('(') && normalized.endsWith(')')) {\n normalized = normalized.slice(1, -1).trim()\n }\n normalized = normalized.replace(/[\\u2013\\u2014]/g, '-')\n normalized = normalized.replace(/\\s+/g, ' ').toLowerCase()\n return normalized\n}\n\nfunction isIdentityPlaceholder(value: string): boolean {\n return IDENTITY_PLACEHOLDER_VALUES.has(normalizeIdentityValue(value))\n}\n\nexport function parseIdentityMarkdown(content: string): AgentIdentityFile {\n const identity: AgentIdentityFile = {}\n for (const line of content.split(/\\r?\\n/)) {\n const cleaned = line.trim().replace(/^\\s*-\\s*/, '')\n const colonIndex = cleaned.indexOf(':')\n if (colonIndex === -1) continue\n const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, '').trim().toLowerCase()\n const value = cleaned\n .slice(colonIndex + 1)\n .replace(/^[*_]+|[*_]+$/g, '')\n .trim()\n if (!value) continue\n if (isIdentityPlaceholder(value)) continue\n if (label === 'name') identity.name = value\n else if (label === 'emoji') identity.emoji = value\n else if (label === 'creature') identity.creature = value\n else if (label === 'vibe') identity.vibe = value\n else if (label === 'theme') identity.theme = value\n else if (label === 'avatar') identity.avatar = value\n }\n return identity\n}\n\nexport function identityHasValues(identity: AgentIdentityFile): boolean {\n return Boolean(\n identity.name ||\n identity.emoji ||\n identity.theme ||\n identity.creature ||\n identity.vibe ||\n identity.avatar,\n )\n}\n\nexport function loadIdentityFromFile(path: string): AgentIdentityFile | null {\n let content: string\n try {\n content = readFileSync(path, 'utf8')\n } catch {\n return null\n }\n const parsed = parseIdentityMarkdown(content)\n return identityHasValues(parsed) ? parsed : null\n}\n","import { randomUUID } from 'node:crypto'\nimport { mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Agent, ReasoningLevel } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { loadProfile } from '../profile/load.ts'\nimport { DEFAULT_GROUP_ID } from '../profile/seed.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport { discoverSkills } from '../skills/discover.ts'\n\nexport interface SpawnAgentInput {\n profileId: string\n name?: string\n modelOverride?: string | null\n reasoningLevel?: ReasoningLevel\n /**\n * Group the new agent joins. One agent belongs to exactly one group. When\n * omitted, falls back to the seeded 'default' group — the same fallback\n * used everywhere we need a sensible cwd. Must refer to an existing group.\n */\n groupId?: string\n}\n\nexport function spawnAgent(db: BazilionDb, paths: Paths, input: SpawnAgentInput): Agent {\n const loaded = loadProfile(db, input.profileId)\n const id = randomUUID()\n const dir = paths.agentDir(id)\n // Agent's private home: identity files + sessions. Memory now lives at\n // the group level (`groups/<slug>/memory/`), shared by all member agents,\n // so we no longer create per-agent memory dirs here.\n mkdirSync(dir, { recursive: true })\n mkdirSync(join(dir, 'sessions'), { recursive: true })\n\n // Copy profile templates verbatim so the agent can diverge per-instance.\n // BOOTSTRAP.md is intentionally NOT personalized with the spawn slug —\n // the slug is a routing label, not necessarily the persona name. Bootstrap\n // is a pure conversation: the agent asks, the human answers, IDENTITY.md\n // is populated from that exchange.\n writeFileSync(join(dir, 'SOUL.md'), loaded.files.soul)\n writeFileSync(join(dir, 'IDENTITY.md'), loaded.files.identity)\n if (loaded.files.bootstrap !== null) {\n writeFileSync(join(dir, 'BOOTSTRAP.md'), loaded.files.bootstrap)\n }\n if (loaded.files.agents !== null) {\n writeFileSync(join(dir, 'AGENTS.md'), loaded.files.agents)\n }\n if (loaded.files.tools !== null) {\n writeFileSync(join(dir, 'TOOLS.md'), loaded.files.tools)\n }\n if (loaded.files.heartbeat !== null) {\n writeFileSync(join(dir, 'HEARTBEAT.md'), loaded.files.heartbeat)\n }\n\n const reasoningLevel: ReasoningLevel = input.reasoningLevel ?? 'medium'\n\n // Resolve group: explicit input wins; otherwise fall back to the seeded\n // 'default'. If neither exists, error out — an agent can't live without a\n // group (the FK `agents.group_id REFERENCES groups(id)` enforces it too).\n const groupId = input.groupId ?? DEFAULT_GROUP_ID\n const group = groupRepo.get(db, groupId, paths)\n if (!group) {\n throw new Error(\n `spawnAgent: group \"${groupId}\" does not exist. Pass an explicit --group or complete first-run setup first.`,\n )\n }\n\n const agentJson = {\n profileId: input.profileId,\n name: input.name ?? loaded.profile.name,\n modelOverride: input.modelOverride ?? null,\n reasoningLevel,\n groupId: group.id,\n }\n writeFileSync(join(dir, 'agent.json'), `${JSON.stringify(agentJson, null, 2)}\\n`)\n\n const agent = agentRepo.insert(db, {\n id,\n profileId: input.profileId,\n name: agentJson.name,\n modelOverride: agentJson.modelOverride,\n reasoningLevel,\n status: 'idle',\n dir,\n groupId: group.id,\n })\n\n // Skills come from the profile only — `skills_mode='all'` attaches every\n // installed skill, `'selected'` attaches the profile's default list.\n // Per-agent skill changes happen post-spawn via `agent skill add/rm`.\n const skills =\n loaded.profile.skillsMode === 'all'\n ? discoverSkills(paths).map((s) => s.name)\n : loaded.defaultSkills\n for (const s of skills) agentRepo.attachSkill(db, id, s)\n\n return agent\n}\n","import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { LoadedProfile } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { parseIdentityMarkdown } from './identity.ts'\n\nfunction readOptional(path: string): string | null {\n return existsSync(path) ? readFileSync(path, 'utf8') : null\n}\n\nexport function loadProfile(db: BazilionDb, id: string): LoadedProfile {\n const profile = profileRepo.get(db, id)\n if (!profile) throw new Error(`profile not found: ${id}`)\n\n const soul = readFileSync(join(profile.dir, 'SOUL.md'), 'utf8')\n const identityRaw = readFileSync(join(profile.dir, 'IDENTITY.md'), 'utf8')\n const bootstrap = readOptional(join(profile.dir, 'BOOTSTRAP.md'))\n const agents = readOptional(join(profile.dir, 'AGENTS.md'))\n const tools = readOptional(join(profile.dir, 'TOOLS.md'))\n const heartbeat = readOptional(join(profile.dir, 'HEARTBEAT.md'))\n\n const parsedIdentity = parseIdentityMarkdown(identityRaw)\n const anyIdentity =\n parsedIdentity.name ||\n parsedIdentity.emoji ||\n parsedIdentity.theme ||\n parsedIdentity.creature ||\n parsedIdentity.vibe ||\n parsedIdentity.avatar\n\n return {\n profile,\n defaultSkills: profileRepo.getDefaultSkills(db, id),\n files: {\n soul,\n identity: identityRaw,\n bootstrap,\n agents,\n tools,\n heartbeat,\n },\n identity: anyIdentity ? parsedIdentity : null,\n }\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Group, Profile } from '@bazilion/api-types'\nimport { isSetupComplete, listAvailableModels } from '../availableModels.ts'\nimport type { BazilionDb } from '../db/client.ts'\nimport { registerGroup } from '../group/register.ts'\nimport type { Paths } from '../paths.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { createProfile } from './create.ts'\nimport {\n DEFAULT_AGENTS,\n DEFAULT_BOOTSTRAP,\n DEFAULT_IDENTITY,\n DEFAULT_SOUL,\n DEFAULT_TOOLS,\n} from './templates.ts'\n\nexport const DEFAULT_PROFILE_ID = 'default'\nexport const DEFAULT_GROUP_ID = 'default'\n\n// HEARTBEAT.md is intentionally absent — it's opt-in, so the\n// managed default profile doesn't ship it.\nconst DEFAULT_PROFILE_FILES: [string, string][] = [\n ['SOUL.md', DEFAULT_SOUL],\n ['IDENTITY.md', DEFAULT_IDENTITY],\n ['BOOTSTRAP.md', DEFAULT_BOOTSTRAP],\n ['AGENTS.md', DEFAULT_AGENTS],\n ['TOOLS.md', DEFAULT_TOOLS],\n]\n\n/**\n * Bring the on-disk `default` profile's template files up to the current\n * defaults. The `default` profile is bazilion-managed — it always reflects\n * the shipped templates, so operators who want a customized template make\n * their own profile (which is never touched here). Pre-1.0, single-user:\n * we just overwrite rather than diffing against historical defaults.\n *\n * Write-if-differs so a steady-state boot is a no-op (and the log stays\n * quiet). No-op when the default profile isn't on disk yet — a fresh install\n * gets the current templates via seedDefaults → createProfile. Returns the\n * names of files that were (re)written.\n */\nexport function refreshDefaultProfileTemplates(paths: Paths): string[] {\n const dir = paths.profileDir(DEFAULT_PROFILE_ID)\n if (!existsSync(dir)) return []\n const written: string[] = []\n for (const [name, content] of DEFAULT_PROFILE_FILES) {\n const path = join(dir, name)\n if (!existsSync(path) || readFileSync(path, 'utf8') !== content) {\n writeFileSync(path, content)\n written.push(name)\n }\n }\n return written\n}\n\nexport interface SeedDefaultsInput {\n /** `provider:model` used as the profile's defaultModel. */\n model: string\n}\n\nexport interface SeedResult {\n profile: Profile\n group: Group\n /** true if the helper created the profile this call (vs. returned an existing one). */\n profileCreated: boolean\n /** true if the helper created the group this call. */\n groupCreated: boolean\n}\n\n/**\n * Seed the on-disk + DB defaults users land on after finishing first-run setup:\n * a `default` group at `~/.bazilion/groups/default/` and a `default` profile\n * wired to the just-enabled model. Fresh agents spawned from the default\n * profile land in the default group unless another is specified.\n *\n * Idempotent: re-seeding reuses whichever pieces already exist, so it's safe\n * to call whenever the setup state changes.\n */\nexport function seedDefaults(db: BazilionDb, paths: Paths, input: SeedDefaultsInput): SeedResult {\n let group = groupRepo.get(db, DEFAULT_GROUP_ID, paths)\n let groupCreated = false\n if (!group) {\n group = registerGroup(db, { id: DEFAULT_GROUP_ID, name: 'Default' }, paths)\n groupCreated = true\n }\n\n let profile = profileRepo.get(db, DEFAULT_PROFILE_ID)\n let profileCreated = false\n if (!profile) {\n // skillsMode='all' so freshly-spawned default agents inherit every\n // installed skill — the friendlier first-run posture. Custom profiles\n // still default to 'selected' (createProfile's own default).\n profile = createProfile(db, paths, {\n id: DEFAULT_PROFILE_ID,\n name: 'Default',\n defaultModel: input.model,\n skillsMode: 'all',\n })\n profileCreated = true\n }\n\n return { profile, group, profileCreated, groupCreated }\n}\n\n/**\n * Safe to call from any endpoint that can change setup state (toggling a\n * provider, editing model lists). No-op when either setup isn't complete\n * (nothing to seed against yet) or the default profile already exists.\n * Returns the seed result only on the cold-start transition.\n */\nexport function ensureSetupSeeded(db: BazilionDb, paths: Paths): SeedResult | null {\n if (!isSetupComplete(db)) return null\n if (profileRepo.get(db, DEFAULT_PROFILE_ID)) return null\n const first = listAvailableModels(db)[0]\n if (!first) return null\n return seedDefaults(db, paths, { model: first.value })\n}\n","// Register a group: pick a slug, materialize `~/.bazilion/groups/<slug>/`\n// (real dir or symlink), insert the row.\n//\n// Two modes:\n// - default: creates a real directory at `paths.groupDir(slug)`.\n// - `--link <target>`: creates a symlink at `paths.groupDir(slug)` →\n// `<target>`. The link target must exist and be a directory — that's\n// the \"I want my agents working on my existing project tree\" path.\n//\n// Slug = the row's id. Names are humanized labels separate from the slug;\n// callers may pass `name` explicitly or let it default to the slug.\n\nimport { existsSync, mkdirSync, statSync, symlinkSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport type { Group } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport { validateSlug } from '../profile/validate.ts'\nimport * as groupRepo from '../repos/groups.ts'\n\nexport interface RegisterGroupInput {\n /** Slug. Becomes the row id AND the directory name under `groups/`. */\n id: string\n /** Human-readable label. Defaults to `id`. */\n name?: string\n /**\n * If set, materialize `paths.groupDir(id)` as a symlink to this absolute\n * path instead of as a real directory. Target must exist and be a dir.\n */\n link?: string\n}\n\nexport function registerGroup(db: BazilionDb, input: RegisterGroupInput, paths: Paths): Group {\n validateSlug(input.id)\n\n if (groupRepo.get(db, input.id, paths)) {\n throw new Error(`group already registered: ${input.id}`)\n }\n\n const slot = paths.groupDir(input.id)\n if (existsSync(slot)) {\n throw new Error(`group slot already on disk at ${slot} (move or remove it first)`)\n }\n\n // Make sure the parent `groups/` dir exists — the daemon's bootstrap creates\n // it but tests sometimes resolve a custom $BAZILION_HOME without going\n // through bootstrap.\n mkdirSync(paths.groupsDir, { recursive: true })\n\n if (input.link) {\n const target = resolve(input.link)\n if (!existsSync(target)) {\n throw new Error(`--link target does not exist: ${target}`)\n }\n if (!statSync(target).isDirectory()) {\n throw new Error(`--link target is not a directory: ${target}`)\n }\n symlinkSync(target, slot, 'dir')\n } else {\n mkdirSync(slot, { recursive: true })\n }\n\n // Memory subdir is the qmd index root for this group — created here so\n // the first tool call doesn't have to ensure it.\n mkdirSync(resolve(slot, 'memory'), { recursive: true })\n\n return groupRepo.insert(db, { id: input.id, name: input.name ?? input.id }, paths)\n}\n","import { mkdirSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Profile, SkillsMode } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport {\n DEFAULT_AGENTS,\n DEFAULT_BOOTSTRAP,\n DEFAULT_IDENTITY,\n DEFAULT_SOUL,\n DEFAULT_TOOLS,\n} from './templates.ts'\nimport { validateSlug } from './validate.ts'\n\nexport interface CreateProfileInput {\n id: string\n name?: string\n defaultModel: string\n skillsMode?: SkillsMode\n defaultSkills?: string[]\n templates?: {\n soul?: string\n identity?: string\n /** undefined = default bootstrap, null = skip bootstrap, string = override */\n bootstrap?: string | null\n /** undefined = default template, null = skip, string = override */\n agents?: string | null\n /** undefined = default template, null = skip, string = override */\n tools?: string | null\n /** opt-in (off by default): null/undefined = skip, string = seed */\n heartbeat?: string | null\n }\n}\n\nexport function createProfile(db: BazilionDb, paths: Paths, input: CreateProfileInput): Profile {\n validateSlug(input.id)\n\n const dir = paths.profileDir(input.id)\n mkdirSync(dir, { recursive: true })\n\n const soul = input.templates?.soul ?? DEFAULT_SOUL\n const identity = input.templates?.identity ?? DEFAULT_IDENTITY\n // SOUL + IDENTITY are always written. BOOTSTRAP/AGENTS/TOOLS are default-on\n // (undefined → default template, null → skip, string → override). HEARTBEAT\n // is the exception: opt-in — only written when an explicit\n // string is supplied (undefined/null → skip).\n const bootstrap =\n input.templates?.bootstrap === null ? null : (input.templates?.bootstrap ?? DEFAULT_BOOTSTRAP)\n const agents =\n input.templates?.agents === null ? null : (input.templates?.agents ?? DEFAULT_AGENTS)\n const tools = input.templates?.tools === null ? null : (input.templates?.tools ?? DEFAULT_TOOLS)\n const heartbeat =\n typeof input.templates?.heartbeat === 'string' ? input.templates.heartbeat : null\n\n writeFileSync(join(dir, 'SOUL.md'), soul)\n writeFileSync(join(dir, 'IDENTITY.md'), identity)\n if (bootstrap !== null) {\n writeFileSync(join(dir, 'BOOTSTRAP.md'), bootstrap)\n }\n if (agents !== null) {\n writeFileSync(join(dir, 'AGENTS.md'), agents)\n }\n if (tools !== null) {\n writeFileSync(join(dir, 'TOOLS.md'), tools)\n }\n if (heartbeat !== null) {\n writeFileSync(join(dir, 'HEARTBEAT.md'), heartbeat)\n }\n\n const skillsMode: SkillsMode = input.skillsMode ?? 'selected'\n const profileJson = {\n name: input.name ?? input.id,\n defaultModel: input.defaultModel,\n skillsMode,\n defaultSkills: input.defaultSkills ?? [],\n }\n writeFileSync(join(dir, 'profile.json'), `${JSON.stringify(profileJson, null, 2)}\\n`)\n\n const profile = profileRepo.insert(db, {\n id: input.id,\n name: profileJson.name,\n dir,\n defaultModel: input.defaultModel,\n skillsMode,\n })\n\n if (skillsMode === 'selected' && input.defaultSkills && input.defaultSkills.length > 0) {\n profileRepo.setDefaultSkills(db, input.id, input.defaultSkills)\n }\n\n return profile\n}\n","import { existsSync, readdirSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Paths } from '../paths.ts'\n\nexport interface DiscoveredSkill {\n name: string\n dir: string\n skillFile: string\n}\n\n/**\n * Walk the bazilion skill library and return every directory that contains a\n * SKILL.md file. Pure filesystem read — does not parse the markdown.\n */\nexport function discoverSkills(paths: Paths): DiscoveredSkill[] {\n if (!existsSync(paths.skillsDir)) return []\n\n const entries = readdirSync(paths.skillsDir, { withFileTypes: true })\n const skills: DiscoveredSkill[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const dir = join(paths.skillsDir, entry.name)\n const skillFile = join(dir, 'SKILL.md')\n if (!existsSync(skillFile)) continue\n skills.push({ name: entry.name, dir, skillFile })\n }\n return skills.sort((a, b) => a.name.localeCompare(b.name))\n}\n","import { DatabaseSync, type SQLInputValue } from 'node:sqlite'\n\nexport interface QueryStmt<RowType, ParamsType extends unknown[]> {\n get(...params: ParamsType): RowType | null\n all(...params: ParamsType): RowType[]\n run(...params: ParamsType): { changes: number; lastInsertRowid: number | bigint }\n}\n\nexport interface QueryableDatabase {\n query<RowType, ParamsType extends unknown[]>(sql: string): QueryStmt<RowType, ParamsType>\n run(sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint }\n exec(sql: string): void\n transaction<T>(fn: () => T): () => T\n}\n\nexport interface BazilionDb {\n raw: QueryableDatabase\n close(): void\n}\n\nfunction wrap(rawDb: DatabaseSync): QueryableDatabase {\n const cache = new Map<string, ReturnType<DatabaseSync['prepare']>>()\n function getStmt(sql: string) {\n let s = cache.get(sql)\n if (!s) {\n s = rawDb.prepare(sql)\n cache.set(sql, s)\n }\n return s\n }\n\n return {\n query<R, P extends unknown[]>(sql: string): QueryStmt<R, P> {\n const stmt = getStmt(sql)\n return {\n get(...params: P): R | null {\n const result = stmt.get(...(params as SQLInputValue[]))\n return (result as R | undefined) ?? null\n },\n all(...params: P): R[] {\n return stmt.all(...(params as SQLInputValue[])) as R[]\n },\n run(...params: P) {\n return stmt.run(...(params as SQLInputValue[])) as {\n changes: number\n lastInsertRowid: number | bigint\n }\n },\n }\n },\n run(sql, params) {\n const stmt = getStmt(sql)\n return stmt.run(...((params ?? []) as SQLInputValue[])) as {\n changes: number\n lastInsertRowid: number | bigint\n }\n },\n exec(sql) {\n rawDb.exec(sql)\n },\n // node:sqlite has no callable `transaction` wrapper; use manual BEGIN/COMMIT/ROLLBACK.\n transaction<T>(fn: () => T): () => T {\n return () => {\n rawDb.exec('BEGIN')\n try {\n const result = fn()\n rawDb.exec('COMMIT')\n return result\n } catch (err) {\n rawDb.exec('ROLLBACK')\n throw err\n }\n }\n },\n }\n}\n\nfunction applyPragmas(rawDb: DatabaseSync, includeWal: boolean): void {\n if (includeWal) {\n try {\n rawDb.exec('PRAGMA journal_mode = WAL')\n } catch {\n // some sqlite builds reject WAL on :memory: — ignore\n }\n }\n rawDb.exec('PRAGMA foreign_keys = ON')\n}\n\nexport function openDb(path: string): BazilionDb {\n const raw = new DatabaseSync(path)\n applyPragmas(raw, true)\n return {\n raw: wrap(raw),\n close() {\n raw.close()\n },\n }\n}\n\nexport function openInMemoryDb(): BazilionDb {\n const raw = new DatabaseSync(':memory:')\n applyPragmas(raw, false)\n return {\n raw: wrap(raw),\n close() {\n raw.close()\n },\n }\n}\n\nexport function inTx<T>(db: BazilionDb, fn: () => T): T {\n return db.raw.transaction(fn)()\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { BazilionDb } from './client.ts'\n\nconst migrationsDir = join(dirname(fileURLToPath(import.meta.url)), 'migrations')\n\nexport function runMigrations(db: BazilionDb): void {\n db.raw.exec(`\n CREATE TABLE IF NOT EXISTS schema_migrations (\n version TEXT PRIMARY KEY,\n applied_at INTEGER NOT NULL\n )\n `)\n\n const applied = new Set(\n db.raw\n .query<{ version: string }, []>('SELECT version FROM schema_migrations')\n .all()\n .map((r) => r.version),\n )\n\n const files = readdirSync(migrationsDir)\n .filter((f) => f.endsWith('.sql'))\n .sort()\n\n for (const file of files) {\n const version = file.replace(/\\.sql$/, '')\n if (applied.has(version)) continue\n\n const sql = readFileSync(join(migrationsDir, file), 'utf8')\n const tx = db.raw.transaction(() => {\n db.raw.exec(sql)\n db.raw.run('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)', [\n version,\n Date.now(),\n ])\n })\n tx()\n }\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface Paths {\n home: string\n db: string\n /**\n * Bootstrap auth file shared by the daemon and the CLI: `{token, remote?}`.\n * - `token` is the plaintext of the bootstrap web token. The daemon reads\n * it once at startup to derive the encryption key for the `secrets`\n * table (PBKDF2 over it) and to validate that it matches a row in\n * `web_tokens` (so a corrupted file fails loudly). The CLI reads it as\n * its loopback bearer.\n * - `remote` (set via `bazilion login`) is a CLI-only override pointing at\n * a remote daemon. The local daemon ignores this field.\n *\n * One file replaces the previous `config.json` + `secrets.enc` split:\n * encrypted secrets and plaintext config now live as DB rows.\n */\n authFile: string\n profilesDir: string\n agentsDir: string\n skillsDir: string\n groupsDir: string\n logsDir: string\n profileDir(id: string): string\n agentDir(id: string): string\n skillDir(name: string): string\n groupDir(slug: string): string\n}\n\nexport function resolvePaths(home?: string): Paths {\n const root = home ?? process.env.BAZILION_HOME ?? join(homedir(), '.bazilion')\n return {\n home: root,\n db: join(root, 'bazilion.db'),\n authFile: join(root, 'auth.json'),\n profilesDir: join(root, 'profiles'),\n agentsDir: join(root, 'agents'),\n skillsDir: join(root, 'skills'),\n groupsDir: join(root, 'groups'),\n logsDir: join(root, 'logs'),\n profileDir(id) {\n return join(root, 'profiles', id)\n },\n agentDir(id) {\n return join(root, 'agents', id)\n },\n skillDir(name) {\n return join(root, 'skills', name)\n },\n groupDir(slug) {\n return join(root, 'groups', slug)\n },\n }\n}\n","import { existsSync, rmSync } from 'node:fs'\nimport type { BazilionDb } from '../db/client.ts'\nimport * as agentRepo from '../repos/agents.ts'\nimport * as profileGroupRepo from '../repos/profileGroups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport function deleteProfile(db: BazilionDb, id: string): void {\n const profile = profileRepo.get(db, id)\n if (!profile) throw new Error(`profile not found: ${id}`)\n\n // Check for agents still using this profile\n const agents = agentRepo.list(db, { includeArchived: true }).filter((a) => a.profileId === id)\n if (agents.length > 0) {\n const names = agents.map((a) => `${a.name} (${a.id.slice(0, 8)})`).join(', ')\n throw new Error(\n `cannot delete profile \"${id}\": ${agents.length} agent(s) still reference it: ${names}. Delete or re-profile them first.`,\n )\n }\n\n // Check for profile groups (team templates) still referencing this profile —\n // the FK on profile_group_members.profile_id is RESTRICT, so a raw delete\n // would surface as an opaque SQLite constraint error.\n const refGroups = profileGroupRepo.findReferencingProfile(db, id)\n if (refGroups.length > 0) {\n const names = refGroups.map((g) => `${g.name} (${g.id})`).join(', ')\n throw new Error(\n `cannot delete profile \"${id}\": ${refGroups.length} profile group(s) still reference it: ${names}. Remove the member(s) first.`,\n )\n }\n\n // Remove from DB (CASCADE deletes profile_default_skills)\n profileRepo.remove(db, id)\n\n // Remove the profile directory from disk\n if (existsSync(profile.dir)) {\n rmSync(profile.dir, { recursive: true, force: true })\n }\n}\n","import { writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Profile, SkillsMode } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\nimport type { Paths } from '../paths.ts'\nimport * as profileRepo from '../repos/profiles.ts'\n\nexport interface UpdateProfileInput {\n name?: string\n defaultModel?: string\n skillsMode?: SkillsMode\n defaultSkills?: string[]\n}\n\n/**\n * Update the mutable settings in profile.json + the DB row in lockstep.\n * Profile files (SOUL/IDENTITY/BOOTSTRAP) and the memory backend are NOT\n * touched — those have their own paths.\n */\nexport function updateProfile(\n db: BazilionDb,\n paths: Paths,\n id: string,\n input: UpdateProfileInput,\n): Profile {\n const existing = profileRepo.get(db, id)\n if (!existing) throw new Error(`profile not found: ${id}`)\n\n const nextSkillsMode: SkillsMode = input.skillsMode ?? existing.skillsMode\n\n const next = {\n name: input.name ?? existing.name,\n defaultModel: input.defaultModel ?? existing.defaultModel,\n skillsMode: nextSkillsMode,\n }\n profileRepo.update(db, id, next)\n\n if (input.defaultSkills !== undefined) {\n profileRepo.setDefaultSkills(db, id, input.defaultSkills)\n }\n\n const skills = profileRepo.getDefaultSkills(db, id)\n const profileJson = {\n name: next.name,\n defaultModel: next.defaultModel,\n skillsMode: next.skillsMode,\n defaultSkills: skills,\n }\n writeFileSync(\n join(paths.profileDir(id), 'profile.json'),\n `${JSON.stringify(profileJson, null, 2)}\\n`,\n )\n\n const updated = profileRepo.get(db, id)\n if (!updated) throw new Error(`profile vanished after update: ${id}`)\n return updated\n}\n","import { readdirSync, rmSync } from 'node:fs'\nimport type { ProfileGroupMember } from '@bazilion/api-types'\nimport { spawnAgent } from '../agent/spawn.ts'\nimport type { BazilionDb } from '../db/client.ts'\nimport { inTx } from '../db/client.ts'\nimport { registerGroup } from '../group/register.ts'\nimport type { Paths } from '../paths.ts'\nimport { DEFAULT_GROUP_ID } from '../profile/seed.ts'\nimport * as groupRepo from '../repos/groups.ts'\nimport * as profileGroupRepo from '../repos/profileGroups.ts'\nimport * as profileRepo from '../repos/profiles.ts'\nimport { rmWithRetry } from './rm-with-retry.ts'\n\nexport interface SpawnProfileGroupInput {\n profileGroupId: string\n /** Target group slug. Falls back to the default group when omitted. */\n groupSlug?: string\n /** Override the template's `userMd` for this spawn only. */\n userMd?: string\n}\n\nexport interface SpawnProfileGroupResult {\n groupSlug: string\n /** Created agents in spawn order, with their final (post-suffix) names. */\n agents: { id: string; name: string }[]\n /** Empty on success; populated only by `SpawnProfileGroupError`. */\n orphanAgentIds: string[]\n}\n\n/**\n * Thrown when the spawn loop fails. The DB transaction is rolled back by\n * the time this is constructed; `orphanAgentIds` lists any agent dirs the\n * cleanup retry helper couldn't remove (typically empty — populated when\n * the filesystem rejects the rmSync after all three retries).\n */\nexport class SpawnProfileGroupError extends Error {\n override name = 'SpawnProfileGroupError'\n orphanAgentIds: string[]\n override cause: unknown\n constructor(message: string, orphanAgentIds: string[], cause: unknown) {\n super(message)\n this.orphanAgentIds = orphanAgentIds\n this.cause = cause\n }\n}\n\n/**\n * Walk members in `position` order, resolving each `agentName` to a unique\n * final name by appending `-2`, `-3`, ... when taken. The `existing` set\n * starts with whatever agents already live in the target group; resolved\n * names are added back into it so two members that share an `agentName`\n * collide with each other as well as with pre-existing agents.\n *\n * Pure function — exported so the spawn integration test can target the\n * algorithm directly without orchestrating a full spawn.\n */\nexport function resolveMemberNames(\n existing: ReadonlySet<string>,\n members: ProfileGroupMember[],\n): string[] {\n const taken = new Set(existing)\n const out: string[] = []\n for (const m of members) {\n let candidate = m.agentName\n let n = 2\n while (taken.has(candidate)) {\n candidate = `${m.agentName}-${n}`\n n++\n }\n taken.add(candidate)\n out.push(candidate)\n }\n return out\n}\n\nexport async function spawnProfileGroup(\n db: BazilionDb,\n paths: Paths,\n input: SpawnProfileGroupInput,\n): Promise<SpawnProfileGroupResult> {\n const template = profileGroupRepo.get(db, input.profileGroupId)\n if (!template) {\n throw new Error(`profile group not found: ${input.profileGroupId}`)\n }\n const members = profileGroupRepo.members(db, input.profileGroupId)\n\n // Pre-flight: every referenced profile must still exist. Bail before any\n // side effect rather than discovering it mid-loop.\n const missing: string[] = []\n for (const m of members) {\n if (!profileRepo.get(db, m.profileId)) missing.push(m.profileId)\n }\n if (missing.length > 0) {\n throw new Error(`profile group spawn: missing profiles: ${missing.join(', ')}`)\n }\n\n const targetSlug = input.groupSlug ?? DEFAULT_GROUP_ID\n const targetGroupExists = !!groupRepo.get(db, targetSlug, paths)\n const existingNames = new Set<string>(\n targetGroupExists\n ? db.raw\n .query<{ name: string }, [string]>('SELECT name FROM agents WHERE group_id = ?')\n .all(targetSlug)\n .map((r) => r.name)\n : [],\n )\n const resolvedNames = resolveMemberNames(existingNames, members)\n\n // Snapshot dir contents so the rollback path can identify fs orphans by\n // diff regardless of whether spawnAgent crashed before or after its DB\n // insert. The agents-dir scan catches the rare mkdir-then-fail window;\n // the groups-dir scan catches a registerGroup that wrote the dir before\n // failing on the INSERT.\n const beforeAgentDirs = new Set(safeReaddir(paths.agentsDir))\n const beforeGroupDirs = new Set(safeReaddir(paths.groupsDir))\n\n let groupCreated = false\n const created: { id: string; name: string }[] = []\n try {\n inTx(db, () => {\n if (!targetGroupExists) {\n registerGroup(db, { id: targetSlug, name: targetSlug }, paths)\n groupCreated = true\n }\n // The existing groups table stores user_md as\n // NOT NULL DEFAULT '', so a pre-existing group's empty user_md is\n // operator-mediated (initial value or explicit clear). Seed only into\n // a group we just created in this same spawn — never overwrite an\n // existing row.\n const seedUserMd = input.userMd ?? template.userMd ?? null\n if (groupCreated && seedUserMd) {\n groupRepo.setUserMd(db, targetSlug, seedUserMd)\n }\n for (let i = 0; i < members.length; i++) {\n const member = members[i]\n const name = resolvedNames[i]\n if (!member || !name) continue\n const agent = spawnAgent(db, paths, {\n profileId: member.profileId,\n name,\n modelOverride: member.modelOverride,\n reasoningLevel: member.reasoningLevel ?? 'medium',\n groupId: targetSlug,\n })\n created.push({ id: agent.id, name: agent.name })\n }\n })\n return { groupSlug: targetSlug, agents: created, orphanAgentIds: [] }\n } catch (err) {\n const newAgentDirs = safeReaddir(paths.agentsDir).filter((d) => !beforeAgentDirs.has(d))\n const orphans: string[] = []\n for (const dir of newAgentDirs) {\n if (!(await rmWithRetry(paths.agentDir(dir)))) {\n orphans.push(dir)\n }\n }\n const newGroupDirs = safeReaddir(paths.groupsDir).filter((d) => !beforeGroupDirs.has(d))\n for (const slug of newGroupDirs) {\n try {\n rmSync(paths.groupDir(slug), { recursive: true, force: true })\n } catch (cleanupErr) {\n console.error(`spawnProfileGroup: failed to clean up group dir ${slug}`, cleanupErr)\n }\n }\n for (const id of orphans) {\n console.error(`spawnProfileGroup: orphan agent dir left on disk: ${paths.agentDir(id)}`)\n }\n const original = err instanceof Error ? err.message : String(err)\n const message =\n orphans.length > 0 ? `${original} (orphan agent dirs: ${orphans.join(', ')})` : original\n throw new SpawnProfileGroupError(message, orphans, err)\n }\n}\n\nfunction safeReaddir(dir: string): string[] {\n try {\n return readdirSync(dir)\n } catch {\n return []\n }\n}\n","import { rmSync } from 'node:fs'\n\nexport const DEFAULT_RM_RETRY_DELAYS_MS = [100, 500, 2000] as const\n\nexport interface RmRetryOptions {\n /** Injectable rm fn for tests. Defaults to `rmSync(target, { recursive, force })`. */\n rm?: (target: string) => void\n /** Backoff between attempts. Defaults to [100, 500, 2000]. */\n delays?: readonly number[]\n /** Injectable sleep for tests. Defaults to `setTimeout`-backed promise. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Retry filesystem removal with backoff. Returns true if the target is\n * gone after some attempt, false if every attempt failed. Used during\n * profile-group spawn rollback to clean up agent dirs created by\n * successful `spawnAgent` calls before the failing slot.\n *\n * The default rm is `rmSync(target, { recursive: true, force: true })` —\n * `force: true` swallows ENOENT, so a missing target counts as success.\n */\nexport async function rmWithRetry(target: string, opts: RmRetryOptions = {}): Promise<boolean> {\n const rm = opts.rm ?? ((p: string) => rmSync(p, { recursive: true, force: true }))\n const delays = opts.delays ?? DEFAULT_RM_RETRY_DELAYS_MS\n const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n for (let attempt = 0; attempt <= delays.length; attempt++) {\n try {\n rm(target)\n return true\n } catch {\n if (attempt === delays.length) return false\n const delay = delays[attempt] ?? 0\n await sleep(delay)\n }\n }\n return false\n}\n","// Per-service field registry — the shape of the /config page.\n//\n// Each entry describes one \"thing\" the user might configure: an LLM provider\n// (Anthropic, LM Studio, …) or an ancillary service (Brave Search, SearXNG).\n// Fields know which storage backend they live in: `secret` → the encrypted\n// `secrets` table, `config` → the plaintext `config` table. The registry is\n// the single source of truth for the UI layout and for the generic\n// field-write endpoint's dispatch.\n//\n// When adding a new provider or service, append an entry here and the\n// config page + CLI pick it up automatically.\n\nexport type FieldKind = 'secret' | 'config'\n\nexport interface ServiceField {\n /** Env var name — canonical key in both stores. */\n envVar: string\n kind: FieldKind\n label: string\n placeholder?: string\n description?: string\n}\n\nexport type ServiceCategory = 'provider' | 'service' | 'integration'\n\nexport interface ServiceDef {\n /** Matches the provider-registry key for providers (e.g. 'anthropic'). */\n id: string\n displayName: string\n category: ServiceCategory\n /** Display grouping label shown above the card on the /config tabs. */\n group?: string\n /** Sign-up link, docs, or 1-line description shown on the card. */\n hint?: string\n fields: ServiceField[]\n}\n\n/**\n * One-liner: what's shown on each service card.\n * Order here is the display order on the config page.\n */\nexport const SERVICES: ServiceDef[] = [\n // --- LLM providers (configured via API keys / URLs) ---\n // Top 3: openai-codex (ChatGPT OAuth), openai (API key), anthropic.\n // Everything else in rough popularity order; locals last.\n {\n id: 'openai-codex',\n displayName: 'OpenAI ChatGPT (OAuth)',\n category: 'provider',\n hint: 'Use your ChatGPT Plus/Pro/Team account (same login as Codex CLI)',\n // No form fields — credentials come from an OAuth flow. The /config page\n // renders a Connect/Disconnect card using /api/auth/openai instead of the\n // standard field inputs.\n fields: [],\n },\n {\n id: 'openai',\n displayName: 'OpenAI',\n category: 'provider',\n hint: 'GPT models · platform.openai.com',\n fields: [{ envVar: 'OPENAI_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-...' }],\n },\n {\n id: 'anthropic',\n displayName: 'Anthropic',\n category: 'provider',\n hint: 'Claude models · console.anthropic.com',\n fields: [\n { envVar: 'ANTHROPIC_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-ant-...' },\n {\n envVar: 'ANTHROPIC_OAUTH_TOKEN',\n kind: 'secret',\n label: 'OAuth token (alternative to API key)',\n description: 'Takes precedence over ANTHROPIC_API_KEY when set',\n },\n ],\n },\n {\n id: 'google',\n displayName: 'Google (Gemini)',\n category: 'provider',\n hint: 'Gemini models · ai.google.dev (free tier available)',\n fields: [\n { envVar: 'GEMINI_API_KEY', kind: 'secret', label: 'API key', placeholder: 'AIza...' },\n ],\n },\n {\n id: 'google-vertex',\n displayName: 'Google Vertex AI',\n category: 'provider',\n hint: 'Authenticates via `gcloud auth application-default login`',\n fields: [\n {\n envVar: 'GOOGLE_CLOUD_PROJECT',\n kind: 'config',\n label: 'GCP project ID',\n placeholder: 'my-project-123456',\n },\n {\n envVar: 'GOOGLE_CLOUD_LOCATION',\n kind: 'config',\n label: 'GCP region',\n placeholder: 'us-central1',\n },\n ],\n },\n {\n id: 'azure-openai',\n displayName: 'Azure OpenAI',\n category: 'provider',\n fields: [{ envVar: 'AZURE_OPENAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'bedrock',\n displayName: 'Amazon Bedrock',\n category: 'provider',\n hint: 'Authenticates via AWS SDK env (AWS_PROFILE or AWS_ACCESS_KEY_ID/SECRET)',\n fields: [],\n },\n {\n id: 'github-copilot',\n displayName: 'GitHub Copilot',\n category: 'provider',\n hint: 'Use a GitHub Copilot subscription to call Claude/GPT/Gemini via Copilot',\n fields: [\n {\n envVar: 'COPILOT_GITHUB_TOKEN',\n kind: 'secret',\n label: 'GitHub token',\n description:\n 'Generic GH_TOKEN/GITHUB_TOKEN are ignored — set this scoped variable explicitly (or run `bazilion auth copilot login` once available).',\n },\n ],\n },\n {\n id: 'deepseek',\n displayName: 'DeepSeek',\n category: 'provider',\n hint: 'DeepSeek V4 Flash / Pro · platform.deepseek.com',\n fields: [{ envVar: 'DEEPSEEK_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'mistral',\n displayName: 'Mistral',\n category: 'provider',\n hint: 'mistral.ai',\n fields: [{ envVar: 'MISTRAL_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xai',\n displayName: 'xAI',\n category: 'provider',\n hint: 'Grok · x.ai',\n fields: [{ envVar: 'XAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'groq',\n displayName: 'Groq',\n category: 'provider',\n hint: 'Fast inference · groq.com',\n fields: [{ envVar: 'GROQ_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'cerebras',\n displayName: 'Cerebras',\n category: 'provider',\n fields: [{ envVar: 'CEREBRAS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'fireworks',\n displayName: 'Fireworks AI',\n category: 'provider',\n hint: 'DeepSeek/GLM/Kimi via fireworks.ai',\n fields: [{ envVar: 'FIREWORKS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'together',\n displayName: 'Together AI',\n category: 'provider',\n hint: 'Open-weight models · together.ai',\n fields: [{ envVar: 'TOGETHER_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'moonshotai',\n displayName: 'Moonshot AI',\n category: 'provider',\n hint: 'Kimi K2/K2.5/K2.6/K2.7 · platform.moonshot.ai',\n fields: [{ envVar: 'MOONSHOT_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'moonshotai-cn',\n displayName: 'Moonshot AI (CN)',\n category: 'provider',\n hint: 'Kimi models via the China endpoint · platform.moonshot.cn',\n fields: [{ envVar: 'MOONSHOT_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'kimi-coding',\n displayName: 'Kimi Coding',\n category: 'provider',\n hint: 'Coding-tuned Kimi endpoint · platform.moonshot.cn',\n fields: [{ envVar: 'KIMI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'minimax',\n displayName: 'MiniMax',\n category: 'provider',\n hint: 'MiniMax M2/M3 family · platform.minimaxi.com',\n fields: [{ envVar: 'MINIMAX_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'minimax-cn',\n displayName: 'MiniMax (CN)',\n category: 'provider',\n hint: 'MiniMax M2/M3 family via the China endpoint',\n fields: [{ envVar: 'MINIMAX_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi',\n displayName: 'Xiaomi MiMo',\n category: 'provider',\n hint: 'API billing endpoint · platform.xiaomimimo.com',\n fields: [{ envVar: 'XIAOMI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-ams',\n displayName: 'Xiaomi MiMo Token Plan (AMS)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in Amsterdam',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_AMS_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-cn',\n displayName: 'Xiaomi MiMo Token Plan (CN)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in China',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'xiaomi-token-plan-sgp',\n displayName: 'Xiaomi MiMo Token Plan (SGP)',\n category: 'provider',\n hint: 'MiMo token-plan endpoint in Singapore',\n fields: [{ envVar: 'XIAOMI_TOKEN_PLAN_SGP_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'ant-ling',\n displayName: 'Ant Ling',\n category: 'provider',\n hint: 'Ling/Ring models · api.ant-ling.com',\n fields: [{ envVar: 'ANT_LING_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'nvidia',\n displayName: 'NVIDIA NIM',\n category: 'provider',\n hint: 'NVIDIA hosted models · build.nvidia.com',\n fields: [{ envVar: 'NVIDIA_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'zai',\n displayName: 'zAI',\n category: 'provider',\n fields: [{ envVar: 'ZAI_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'zai-coding-cn',\n displayName: 'zAI Coding (CN)',\n category: 'provider',\n hint: 'GLM coding endpoint · open.bigmodel.cn',\n fields: [{ envVar: 'ZAI_CODING_CN_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'huggingface',\n displayName: 'Hugging Face',\n category: 'provider',\n hint: 'Inference endpoints · huggingface.co',\n fields: [{ envVar: 'HF_TOKEN', kind: 'secret', label: 'Access token', placeholder: 'hf_...' }],\n },\n {\n id: 'cloudflare-ai-gateway',\n displayName: 'Cloudflare AI Gateway',\n category: 'provider',\n hint: 'Per-gateway routing to OpenAI/Anthropic/Workers AI',\n fields: [\n { envVar: 'CLOUDFLARE_API_KEY', kind: 'secret', label: 'API key' },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', kind: 'config', label: 'Account ID' },\n { envVar: 'CLOUDFLARE_GATEWAY_ID', kind: 'config', label: 'Gateway ID' },\n ],\n },\n {\n id: 'cloudflare-workers-ai',\n displayName: 'Cloudflare Workers AI',\n category: 'provider',\n hint: 'Inference on Cloudflare Workers · ai.cloudflare.com',\n fields: [\n { envVar: 'CLOUDFLARE_API_KEY', kind: 'secret', label: 'API key' },\n { envVar: 'CLOUDFLARE_ACCOUNT_ID', kind: 'config', label: 'Account ID' },\n ],\n },\n {\n id: 'openrouter',\n displayName: 'OpenRouter',\n category: 'provider',\n hint: 'Proxy for 200+ models · openrouter.ai',\n fields: [\n { envVar: 'OPENROUTER_API_KEY', kind: 'secret', label: 'API key', placeholder: 'sk-or-...' },\n ],\n },\n {\n id: 'vercel-ai-gateway',\n displayName: 'Vercel AI Gateway',\n category: 'provider',\n fields: [\n { envVar: 'AI_GATEWAY_API_KEY', kind: 'secret', label: 'API key' },\n {\n envVar: 'AI_GATEWAY_BASE_URL',\n kind: 'config',\n label: 'Base URL (optional)',\n placeholder: 'https://ai-gateway.vercel.sh/v1',\n },\n ],\n },\n {\n id: 'opencode',\n displayName: 'OpenCode',\n category: 'provider',\n hint: 'OpenAI-compatible proxy from the OpenCode CLI',\n fields: [{ envVar: 'OPENCODE_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'opencode-go',\n displayName: 'OpenCode Go',\n category: 'provider',\n hint: 'OpenCode hosted Go endpoint',\n fields: [{ envVar: 'OPENCODE_GO_API_KEY', kind: 'secret', label: 'API key' }],\n },\n {\n id: 'lmstudio',\n displayName: 'LM Studio',\n category: 'provider',\n hint: 'Local inference · lmstudio.ai',\n fields: [\n {\n envVar: 'LMSTUDIO_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:1234/v1',\n },\n {\n envVar: 'LMSTUDIO_API_KEY',\n kind: 'secret',\n label: 'API key (rarely needed)',\n },\n ],\n },\n {\n id: 'ollama',\n displayName: 'Ollama',\n category: 'provider',\n hint: 'Local inference · ollama.com',\n fields: [\n {\n envVar: 'OLLAMA_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:11434/v1',\n },\n {\n envVar: 'OLLAMA_API_KEY',\n kind: 'secret',\n label: 'API key (rarely needed)',\n },\n ],\n },\n {\n id: 'llamacpp',\n displayName: 'llama.cpp',\n category: 'provider',\n hint: 'Local inference · llama.cpp llama-server (OpenAI-compat /v1 endpoint)',\n fields: [\n {\n envVar: 'LLAMACPP_URL',\n kind: 'config',\n label: 'Endpoint URL',\n placeholder: 'http://127.0.0.1:8080/v1',\n },\n {\n envVar: 'LLAMACPP_API_KEY',\n kind: 'secret',\n label: 'API key (only if started with --api-key)',\n description:\n 'llama-server runs without auth by default. Set this only if you launched the server with the `--api-key KEY` flag.',\n },\n ],\n },\n\n // --- Ancillary services (web search, etc) ---\n {\n id: 'firecrawl',\n displayName: 'Firecrawl',\n category: 'service',\n group: 'Web Search',\n hint: 'web_fetch fallback for JS-heavy/blocked pages · firecrawl.dev (free tier available)',\n fields: [\n {\n envVar: 'FIRECRAWL_API_KEY',\n kind: 'secret',\n label: 'API key',\n placeholder: 'fc-...',\n description:\n 'When set, web_fetch automatically falls back to Firecrawl if the primary Readability extraction yields too little content.',\n },\n {\n envVar: 'FIRECRAWL_URL',\n kind: 'config',\n label: 'Base URL (optional, for self-hosted)',\n placeholder: 'https://api.firecrawl.dev',\n },\n ],\n },\n {\n id: 'brave-search',\n displayName: 'Brave Search',\n category: 'service',\n group: 'Web Search',\n hint: 'Web search tool · free tier at brave.com/search/api/',\n fields: [{ envVar: 'BRAVE_API_KEY', kind: 'secret', label: 'API key', placeholder: 'BSA...' }],\n },\n {\n id: 'searxng',\n displayName: 'SearXNG',\n category: 'service',\n group: 'Web Search',\n hint: 'Self-hosted meta-search engine · searxng.org',\n fields: [\n {\n envVar: 'SEARXNG_URL',\n kind: 'config',\n label: 'Instance URL',\n placeholder: 'https://searxng.example.com',\n },\n ],\n },\n {\n id: 'browser',\n displayName: 'Browser Automation',\n category: 'service',\n group: 'Browser',\n hint: 'Playwright-driven browser tools (navigate, snapshot, click, screenshot). Run `pnpm exec playwright install chromium` once.',\n fields: [\n {\n envVar: 'BROWSER_ENABLED',\n kind: 'config',\n label: 'Enable browser tools',\n placeholder: 'true',\n description: 'Expose the browser_* tools to agents (true/false). Default true.',\n },\n {\n envVar: 'BROWSER_HEADLESS',\n kind: 'config',\n label: 'Headless',\n placeholder: 'true',\n description: 'Run Chromium headless (true/false). Default true.',\n },\n {\n envVar: 'BROWSER_ALLOW_PRIVATE_NETWORK',\n kind: 'config',\n label: 'Allow private network',\n placeholder: 'false',\n description:\n 'Permit the browser to reach loopback/private IPs (SSRF guard off). Default false — only enable for local dev.',\n },\n {\n envVar: 'BROWSER_IDLE_MS',\n kind: 'config',\n label: 'Idle timeout (ms)',\n placeholder: '900000',\n description: 'Close an idle browser session after this many ms. Default 900000 (15 min).',\n },\n {\n envVar: 'BROWSER_MAX_SESSIONS',\n kind: 'config',\n label: 'Max concurrent sessions',\n placeholder: '4',\n description: 'Cap on simultaneously-open browser sessions (LRU-evicted). Default 4.',\n },\n ],\n },\n\n // --- External integrations (chat bridges, etc) ---\n // Each integration has its own dedicated /config/integrations/* page with\n // workflow-specific UI (preflight health, setup wizard, …). The fields\n // here exist so the keys are allowlisted in the config/secrets stores and\n // surfaced through the generic `PUT /api/config/fields/:envVar` endpoint.\n // Daemon-managed internal state keys (watermarks, derived topic ids) live\n // in repos/config.ts:INTERNAL_CONFIG_KEYS instead, since the user never\n // edits them.\n {\n id: 'telegram',\n displayName: 'Telegram',\n category: 'integration',\n hint: 'Forum-supergroup bot for talking to your agents from a phone',\n fields: [\n {\n envVar: 'TELEGRAM_BOT_TOKEN',\n kind: 'secret',\n label: 'Bot token',\n placeholder: '1234567890:ABC...',\n description: 'Get one from @BotFather → /newbot. Disable Privacy Mode in Bot Settings.',\n },\n {\n envVar: 'TELEGRAM_CHAT_ID',\n kind: 'config',\n label: 'Supergroup chat ID',\n placeholder: '-1001234567890',\n description: 'Numeric id of the forum-enabled supergroup the bot is admin in.',\n },\n ],\n },\n]\n\n/**\n * Fast lookup: envVar → the field definition + owning service.\n * Rebuilt once at module init — the list is static.\n */\nconst FIELD_INDEX: Map<string, { service: ServiceDef; field: ServiceField }> = (() => {\n const m = new Map<string, { service: ServiceDef; field: ServiceField }>()\n for (const service of SERVICES) {\n for (const field of service.fields) {\n m.set(field.envVar, { service, field })\n }\n }\n return m\n})()\n\nexport function findFieldByEnvVar(\n envVar: string,\n): { service: ServiceDef; field: ServiceField } | undefined {\n return FIELD_INDEX.get(envVar)\n}\n\nexport function servicesByCategory(category: ServiceCategory): ServiceDef[] {\n return SERVICES.filter((s) => s.category === category)\n}\n","// Plaintext config store, backed by the `config` table.\n//\n// Companion to `secrets.ts` — same key-shaped values, but for the ones that\n// don't need confidentiality (server URLs, region slugs, project IDs). Kept\n// separate so the /config UI can show plaintext values directly without\n// extra masking logic.\n//\n// The CONFIG_KEYS allowlist (derived from the services registry) is enforced\n// here on writes — a typo or accidental misclassification can't put an API\n// key in this table.\n\nimport type { BazilionDb } from '../db/client.ts'\nimport { SERVICES } from '../services.ts'\n\n/**\n * Daemon-managed internal state keys. Not exposed in the services registry\n * because the user never edits them — they're written by background machinery\n * (polling watermarks, derived topic ids, …). Kept here so the allowlist\n * still gates accidental misuse from the generic field write endpoint.\n */\nconst INTERNAL_CONFIG_KEYS: readonly string[] = [\n // Telegram (step 2+): polling watermark + derived topic/message ids.\n 'TELEGRAM_LAST_UPDATE_ID',\n 'TELEGRAM_SERVICE_TOPIC_ID',\n 'TELEGRAM_DIRECTORY_MESSAGE_ID',\n // Phase 5: proposed new chat id after a migrate_to_chat_id event, pending\n // operator confirmation via POST /api/config/telegram/reconnect.\n 'TELEGRAM_MIGRATED_CHAT_ID',\n]\n\n/**\n * Env var names that live in the plaintext config store. Derived from the\n * services registry — any field marked `kind: 'config'` ends up here — and\n * unioned with the internal keys list above.\n */\nexport const CONFIG_KEYS: readonly string[] = [\n ...SERVICES.flatMap((s) => s.fields.filter((f) => f.kind === 'config').map((f) => f.envVar)),\n ...INTERNAL_CONFIG_KEYS,\n]\n\nconst CONFIG_KEY_SET = new Set<string>(CONFIG_KEYS)\n\nexport function isConfigKey(key: string): boolean {\n return CONFIG_KEY_SET.has(key)\n}\n\ninterface RawRow {\n key: string\n value: string\n updated_at: number\n}\n\nexport interface ConfigStore {\n get(key: string): string | undefined\n set(key: string, value: string): void\n remove(key: string): void\n list(): { key: string; value: string }[]\n getAll(): Record<string, string>\n}\n\nexport function openConfig(db: BazilionDb): ConfigStore {\n return {\n get(key) {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM config WHERE key = ?').get(key)\n return row?.value\n },\n set(key, value) {\n if (!isConfigKey(key)) {\n throw new Error(\n `config.set: \"${key}\" is not a known config key (${CONFIG_KEYS.join(', ')})`,\n )\n }\n db.raw.run(\n `INSERT INTO config (key, value, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n [key, value, Date.now()],\n )\n },\n remove(key) {\n db.raw.run('DELETE FROM config WHERE key = ?', [key])\n },\n list() {\n return db.raw\n .query<RawRow, []>('SELECT * FROM config ORDER BY key ASC')\n .all()\n .map((r) => ({ key: r.key, value: r.value }))\n },\n getAll() {\n const out: Record<string, string> = {}\n for (const r of db.raw.query<RawRow, []>('SELECT * FROM config').all()) {\n out[r.key] = r.value\n }\n return out\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { McpServer, McpTransport } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawRow {\n id: string\n name: string\n transport: string\n command: string | null\n args: string\n url: string | null\n has_auth: number\n enabled: number\n created_at: number\n updated_at: number\n}\n\nfunction toServer(r: RawRow): McpServer {\n let args: string[] = []\n try {\n const parsed = JSON.parse(r.args)\n if (Array.isArray(parsed)) args = parsed.filter((a): a is string => typeof a === 'string')\n } catch {}\n return {\n id: r.id,\n name: r.name,\n transport: r.transport as McpTransport,\n command: r.command,\n args,\n url: r.url,\n hasAuthToken: r.has_auth === 1,\n enabled: r.enabled === 1,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }\n}\n\nexport interface InsertMcpServerInput {\n name: string\n transport: McpTransport\n command?: string | null\n args?: string[]\n url?: string | null\n hasAuth?: boolean\n enabled?: boolean\n}\n\nexport function insert(db: BazilionDb, input: InsertMcpServerInput): McpServer {\n const id = randomUUID()\n const now = Date.now()\n db.raw.run(\n `INSERT INTO mcp_servers (id, name, transport, command, args, url, has_auth, enabled, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id,\n input.name,\n input.transport,\n input.command ?? null,\n JSON.stringify(input.args ?? []),\n input.url ?? null,\n input.hasAuth ? 1 : 0,\n input.enabled === false ? 0 : 1,\n now,\n now,\n ],\n )\n const row = get(db, id)\n if (!row) throw new Error('mcp server insert failed')\n return row\n}\n\nexport function get(db: BazilionDb, id: string): McpServer | null {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM mcp_servers WHERE id = ?').get(id)\n return row ? toServer(row) : null\n}\n\nexport function getByName(db: BazilionDb, name: string): McpServer | null {\n const row = db.raw.query<RawRow, [string]>('SELECT * FROM mcp_servers WHERE name = ?').get(name)\n return row ? toServer(row) : null\n}\n\nexport function list(db: BazilionDb): McpServer[] {\n return db.raw\n .query<RawRow, []>('SELECT * FROM mcp_servers ORDER BY created_at ASC')\n .all()\n .map(toServer)\n}\n\nexport function listEnabled(db: BazilionDb): McpServer[] {\n return db.raw\n .query<RawRow, []>('SELECT * FROM mcp_servers WHERE enabled = 1 ORDER BY created_at ASC')\n .all()\n .map(toServer)\n}\n\nexport interface UpdateMcpServerInput {\n name?: string\n transport?: McpTransport\n command?: string | null\n args?: string[]\n url?: string | null\n enabled?: boolean\n hasAuth?: boolean\n}\n\nexport function update(db: BazilionDb, id: string, input: UpdateMcpServerInput): McpServer | null {\n const existing = get(db, id)\n if (!existing) return null\n const next = {\n name: input.name ?? existing.name,\n transport: input.transport ?? existing.transport,\n command: input.command !== undefined ? input.command : existing.command,\n args: input.args ?? existing.args,\n url: input.url !== undefined ? input.url : existing.url,\n enabled: input.enabled ?? existing.enabled,\n hasAuth: input.hasAuth ?? existing.hasAuthToken,\n }\n db.raw.run(\n `UPDATE mcp_servers\n SET name = ?, transport = ?, command = ?, args = ?, url = ?, enabled = ?, has_auth = ?, updated_at = ?\n WHERE id = ?`,\n [\n next.name,\n next.transport,\n next.command,\n JSON.stringify(next.args),\n next.url,\n next.enabled ? 1 : 0,\n next.hasAuth ? 1 : 0,\n Date.now(),\n id,\n ],\n )\n return get(db, id)\n}\n\nexport function setEnabled(db: BazilionDb, id: string, enabled: boolean): void {\n db.raw.run('UPDATE mcp_servers SET enabled = ?, updated_at = ? WHERE id = ?', [\n enabled ? 1 : 0,\n Date.now(),\n id,\n ])\n}\n\nexport function remove(db: BazilionDb, id: string): void {\n db.raw.run('DELETE FROM mcp_servers WHERE id = ?', [id])\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Message } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawMessage {\n id: string\n from_agent_id: string\n to_agent_id: string\n reply_to: string | null\n payload: string\n created_at: number\n read_at: number | null\n}\n\nfunction toMessage(r: RawMessage): Message {\n return {\n id: r.id,\n fromAgentId: r.from_agent_id,\n toAgentId: r.to_agent_id,\n replyTo: r.reply_to,\n payload: r.payload,\n createdAt: r.created_at,\n readAt: r.read_at,\n }\n}\n\nexport function send(\n db: BazilionDb,\n input: { from: string; to: string; payload: string; replyTo?: string | null },\n): Message {\n const id = randomUUID()\n const now = Date.now()\n db.raw.run(\n `INSERT INTO messages (id, from_agent_id, to_agent_id, reply_to, payload, created_at, read_at)\n VALUES (?, ?, ?, ?, ?, ?, NULL)`,\n [id, input.from, input.to, input.replyTo ?? null, input.payload, now],\n )\n return {\n id,\n fromAgentId: input.from,\n toAgentId: input.to,\n replyTo: input.replyTo ?? null,\n payload: input.payload,\n createdAt: now,\n readAt: null,\n }\n}\n\nexport function get(db: BazilionDb, id: string): Message | null {\n const row = db.raw.query<RawMessage, [string]>('SELECT * FROM messages WHERE id = ?').get(id)\n return row ? toMessage(row) : null\n}\n\nexport function listInbox(\n db: BazilionDb,\n agentId: string,\n opts?: { unreadOnly?: boolean },\n): Message[] {\n const sql = opts?.unreadOnly\n ? 'SELECT * FROM messages WHERE to_agent_id = ? AND read_at IS NULL ORDER BY created_at ASC'\n : 'SELECT * FROM messages WHERE to_agent_id = ? ORDER BY created_at ASC'\n return db.raw.query<RawMessage, [string]>(sql).all(agentId).map(toMessage)\n}\n\nexport function markRead(db: BazilionDb, id: string): void {\n db.raw.run('UPDATE messages SET read_at = ? WHERE id = ? AND read_at IS NULL', [Date.now(), id])\n}\n\n/**\n * Find messages that are replies to a given message id, addressed to a specific agent.\n * Used by `wait_for_reply` to poll for incoming responses.\n */\nexport function findReplies(db: BazilionDb, toAgentId: string, inReplyTo: string): Message[] {\n return db.raw\n .query<RawMessage, [string, string]>(\n `SELECT * FROM messages\n WHERE to_agent_id = ? AND reply_to = ?\n ORDER BY created_at ASC`,\n )\n .all(toAgentId, inReplyTo)\n .map(toMessage)\n}\n\n/**\n * Return the distinct `to_agent_id`s that currently have at least one unread\n * message, filtered to agents not in a terminal state (idle or starting).\n * Used by the scheduler's message-wake loop so a tick can fan-out auto-\n * delivery turns without walking every agent in the DB.\n */\nexport function listRecipientsWithUnread(db: BazilionDb): string[] {\n const rows = db.raw\n .query<{ to_agent_id: string }, []>(\n `SELECT DISTINCT m.to_agent_id FROM messages m\n JOIN agents a ON a.id = m.to_agent_id\n WHERE m.read_at IS NULL AND a.status = 'idle'\n ORDER BY m.to_agent_id`,\n )\n .all()\n return rows.map((r) => r.to_agent_id)\n}\n\n/**\n * Atomically fetch + mark-read all unread messages addressed to `agentId`.\n * Runs inside a transaction so two concurrent schedulers / manual deliveries\n * can't double-dispatch the same message. Returns the fetched messages in\n * ascending `created_at` order — callers format them into the recipient's\n * wake-up prompt.\n */\nexport function drainUnreadForAgent(db: BazilionDb, agentId: string): Message[] {\n return db.raw.transaction(() => {\n const rows = db.raw\n .query<RawMessage, [string]>(\n `SELECT * FROM messages\n WHERE to_agent_id = ? AND read_at IS NULL\n ORDER BY created_at ASC`,\n )\n .all(agentId)\n if (rows.length === 0) return []\n const now = Date.now()\n db.raw.run(\n `UPDATE messages SET read_at = ?\n WHERE to_agent_id = ? AND read_at IS NULL`,\n [now, agentId],\n )\n return rows.map((r) => toMessage({ ...r, read_at: now }))\n })()\n}\n","// Encrypted secrets store, backed by the `secrets` table.\n//\n// Layout: one row per env-var-shaped key (`ANTHROPIC_API_KEY`,\n// `OPENAI_CODEX_OAUTH`, …). Each value is an AES-256-GCM envelope (salt +\n// iv + tag + data, hex-encoded JSON), with the key derived from the\n// bootstrap token via PBKDF2-SHA256 (100k iterations). The crypto matches\n// the previous `secrets.enc` file format byte-for-byte — only the storage\n// medium changed.\n//\n// Why encrypt at all when the password lives in `~/.bazilion/auth.json`\n// next to the DB? Same reason as before: it's defense against accidental\n// exposure (cat'd dumps, screenshares, naive backups), not against an\n// attacker with filesystem read. Anyone who can read both files wins.\n//\n// Caching: `deriveKey` runs PBKDF2 once per (password, salt) pair and the\n// salt is per-row, so a busy daemon does ~one PBKDF2 per secret read. The\n// `secretCache` keeps decrypted values in-memory keyed by row id so repeated\n// reads inside one process don't repeat the work; mutations clear the cache\n// row.\n\nimport { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from 'node:crypto'\nimport type { BazilionDb } from '../db/client.ts'\n\nconst ALGORITHM = 'aes-256-gcm'\nconst KEY_LEN = 32\nconst ITERATIONS = 100_000\nconst DIGEST = 'sha256'\n\ninterface EncryptedEnvelope {\n salt: string\n iv: string\n tag: string\n data: string\n}\n\nfunction deriveKey(password: string, salt: Buffer): Buffer {\n return pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST)\n}\n\nfunction encrypt(plaintext: string, password: string): EncryptedEnvelope {\n const salt = randomBytes(16)\n const key = deriveKey(password, salt)\n const iv = randomBytes(12)\n const cipher = createCipheriv(ALGORITHM, key, iv)\n let data = cipher.update(plaintext, 'utf8', 'hex')\n data += cipher.final('hex')\n const tag = cipher.getAuthTag()\n return {\n salt: salt.toString('hex'),\n iv: iv.toString('hex'),\n tag: tag.toString('hex'),\n data,\n }\n}\n\nfunction decrypt(envelope: EncryptedEnvelope, password: string): string {\n const salt = Buffer.from(envelope.salt, 'hex')\n const key = deriveKey(password, salt)\n const iv = Buffer.from(envelope.iv, 'hex')\n const tag = Buffer.from(envelope.tag, 'hex')\n const decipher = createDecipheriv(ALGORITHM, key, iv)\n decipher.setAuthTag(tag)\n let plaintext = decipher.update(envelope.data, 'hex', 'utf8')\n plaintext += decipher.final('utf8')\n return plaintext\n}\n\ninterface RawRow {\n key: string\n envelope: string\n updated_at: number\n}\n\nexport interface SecretsStore {\n get(key: string): string | undefined\n set(key: string, value: string): void\n remove(key: string): void\n has(key: string): boolean\n list(): { key: string; preview: string }[]\n getAll(): Record<string, string>\n}\n\n/**\n * Open the encrypted secrets store. `password` is the bootstrap token\n * (read from `auth.json` by the caller). Throws on individual-row decrypt\n * failures only when actively reading that row — bad rows show as\n * `undefined` from `get`, the caller can `set` to overwrite.\n */\nexport function openSecrets(db: BazilionDb, password: string): SecretsStore {\n function getRow(key: string): RawRow | null {\n return db.raw.query<RawRow, [string]>('SELECT * FROM secrets WHERE key = ?').get(key)\n }\n\n function listAll(): RawRow[] {\n return db.raw.query<RawRow, []>('SELECT * FROM secrets ORDER BY key ASC').all()\n }\n\n function tryDecrypt(row: RawRow): string | undefined {\n try {\n const envelope = JSON.parse(row.envelope) as EncryptedEnvelope\n return decrypt(envelope, password)\n } catch {\n return undefined\n }\n }\n\n return {\n get(key) {\n const row = getRow(key)\n if (!row) return undefined\n return tryDecrypt(row)\n },\n set(key, value) {\n const envelope = JSON.stringify(encrypt(value, password))\n db.raw.run(\n `INSERT INTO secrets (key, envelope, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET envelope = excluded.envelope, updated_at = excluded.updated_at`,\n [key, envelope, Date.now()],\n )\n },\n remove(key) {\n db.raw.run('DELETE FROM secrets WHERE key = ?', [key])\n },\n has(key) {\n return getRow(key) !== null\n },\n list() {\n return listAll().map((r) => {\n const value = tryDecrypt(r)\n return {\n key: r.key,\n preview: value ? (value.length > 8 ? `${value.slice(0, 6)}…` : '***') : '(unreadable)',\n }\n })\n },\n getAll() {\n const out: Record<string, string> = {}\n for (const r of listAll()) {\n const v = tryDecrypt(r)\n if (v !== undefined) out[r.key] = v\n }\n return out\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { AgentTrigger, TriggerKind } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawTrigger {\n id: string\n agent_id: string\n kind: string\n interval_sec: number | null\n cron_expr: string | null\n message: string\n enabled: number\n last_fired_at: number | null\n created_at: number\n}\n\nfunction toTrigger(r: RawTrigger): AgentTrigger {\n return {\n id: r.id,\n agentId: r.agent_id,\n kind: r.kind as TriggerKind,\n intervalSec: r.interval_sec,\n cronExpr: r.cron_expr,\n message: r.message,\n enabled: r.enabled === 1,\n lastFiredAt: r.last_fired_at,\n createdAt: r.created_at,\n }\n}\n\nexport interface InsertTriggerInput {\n agentId: string\n kind: TriggerKind\n intervalSec: number | null\n cronExpr: string | null\n message: string\n enabled?: boolean\n}\n\nexport function insert(db: BazilionDb, input: InsertTriggerInput): AgentTrigger {\n const id = randomUUID()\n const now = Date.now()\n const enabled = input.enabled === false ? 0 : 1\n db.raw.run(\n `INSERT INTO agent_triggers\n (id, agent_id, kind, interval_sec, cron_expr, message, enabled, last_fired_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)`,\n [id, input.agentId, input.kind, input.intervalSec, input.cronExpr, input.message, enabled, now],\n )\n return {\n id,\n agentId: input.agentId,\n kind: input.kind,\n intervalSec: input.intervalSec,\n cronExpr: input.cronExpr,\n message: input.message,\n enabled: enabled === 1,\n lastFiredAt: null,\n createdAt: now,\n }\n}\n\nexport function get(db: BazilionDb, id: string): AgentTrigger | null {\n const row = db.raw\n .query<RawTrigger, [string]>('SELECT * FROM agent_triggers WHERE id = ?')\n .get(id)\n return row ? toTrigger(row) : null\n}\n\nexport function listForAgent(db: BazilionDb, agentId: string): AgentTrigger[] {\n return db.raw\n .query<RawTrigger, [string]>(\n 'SELECT * FROM agent_triggers WHERE agent_id = ? ORDER BY created_at ASC',\n )\n .all(agentId)\n .map(toTrigger)\n}\n\nexport function listEnabled(db: BazilionDb): AgentTrigger[] {\n return db.raw\n .query<RawTrigger, []>(\n `SELECT t.* FROM agent_triggers t\n JOIN agents a ON a.id = t.agent_id\n WHERE t.enabled = 1 AND a.status != 'archived'\n ORDER BY t.created_at ASC`,\n )\n .all()\n .map(toTrigger)\n}\n\nexport function setEnabled(db: BazilionDb, id: string, enabled: boolean): void {\n db.raw.run('UPDATE agent_triggers SET enabled = ? WHERE id = ?', [enabled ? 1 : 0, id])\n}\n\nexport function markFired(db: BazilionDb, id: string, when: number = Date.now()): void {\n db.raw.run('UPDATE agent_triggers SET last_fired_at = ? WHERE id = ?', [when, id])\n}\n\nexport function remove(db: BazilionDb, id: string): void {\n db.raw.run('DELETE FROM agent_triggers WHERE id = ?', [id])\n}\n","import { createHash, randomBytes, randomUUID } from 'node:crypto'\nimport type { WebToken } from '@bazilion/api-types'\nimport type { BazilionDb } from '../db/client.ts'\n\ninterface RawToken {\n id: string\n label: string\n token_hash: string\n created_at: number\n last_used_at: number | null\n revoked_at: number | null\n}\n\nfunction toToken(r: RawToken): WebToken {\n return {\n id: r.id,\n label: r.label,\n createdAt: r.created_at,\n lastUsedAt: r.last_used_at,\n revokedAt: r.revoked_at,\n }\n}\n\nexport function hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\nexport interface CreatedToken {\n meta: WebToken\n /** Plaintext token — shown exactly once, never re-queryable. */\n token: string\n}\n\nexport function create(db: BazilionDb, label: string): CreatedToken {\n const id = randomUUID()\n const token = randomBytes(24).toString('hex')\n const tokenHash = hashToken(token)\n const now = Date.now()\n db.raw.run(\n `INSERT INTO web_tokens (id, label, token_hash, created_at, last_used_at, revoked_at)\n VALUES (?, ?, ?, ?, NULL, NULL)`,\n [id, label, tokenHash, now],\n )\n return {\n token,\n meta: { id, label, createdAt: now, lastUsedAt: null, revokedAt: null },\n }\n}\n\nexport function list(db: BazilionDb, opts?: { includeRevoked?: boolean }): WebToken[] {\n const sql = opts?.includeRevoked\n ? 'SELECT * FROM web_tokens ORDER BY created_at ASC'\n : 'SELECT * FROM web_tokens WHERE revoked_at IS NULL ORDER BY created_at ASC'\n return db.raw.query<RawToken, []>(sql).all().map(toToken)\n}\n\nexport function get(db: BazilionDb, id: string): WebToken | null {\n const row = db.raw.query<RawToken, [string]>('SELECT * FROM web_tokens WHERE id = ?').get(id)\n return row ? toToken(row) : null\n}\n\n/**\n * Returns the active token row matching the given plaintext, or null.\n * Does NOT bump last_used_at — call markUsed separately once the caller\n * has decided the request is authorized.\n */\nexport function findActiveByToken(db: BazilionDb, token: string): WebToken | null {\n const tokenHash = hashToken(token)\n const row = db.raw\n .query<RawToken, [string]>(\n 'SELECT * FROM web_tokens WHERE token_hash = ? AND revoked_at IS NULL',\n )\n .get(tokenHash)\n return row ? toToken(row) : null\n}\n\nexport function markUsed(db: BazilionDb, id: string, when: number = Date.now()): void {\n db.raw.run('UPDATE web_tokens SET last_used_at = ? WHERE id = ?', [when, id])\n}\n\nexport function revoke(db: BazilionDb, id: string, when: number = Date.now()): boolean {\n const res = db.raw.run(\n 'UPDATE web_tokens SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL',\n [when, id],\n )\n return res.changes > 0\n}\n","// Auth / secrets entry points keyed off the bootstrap `auth.json` file.\n//\n// `auth.json` carries one mandatory field — `token` — written by the daemon's\n// first-run bootstrap. The daemon uses it as the PBKDF2 seed for the `secrets`\n// table; the CLI uses it as the bearer for loopback HTTP. CLI-side `remote`\n// overrides (set by `bazilion login`) coexist in the same file.\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport type { BazilionDb } from './db/client.ts'\nimport { openConfig } from './repos/config.ts'\nimport { openSecrets } from './repos/secrets.ts'\n\nexport interface AuthFile {\n token: string\n remote?: { server: string; token: string } | null\n}\n\n/**\n * Read `auth.json` and return the parsed contents. Throws when missing or\n * malformed — callers should treat that as \"bazilion not initialized.\"\n */\nexport function readAuthFile(authFile: string): AuthFile {\n if (!existsSync(authFile)) {\n throw new Error(\n `${authFile} not found. Start the daemon (\\`bazilion serve\\`) — it auto-bootstraps on first run.`,\n )\n }\n const raw = readFileSync(authFile, 'utf8')\n const parsed = JSON.parse(raw) as Partial<AuthFile>\n if (typeof parsed.token !== 'string' || !parsed.token) {\n throw new Error(`${authFile} is missing the \"token\" field`)\n }\n return {\n token: parsed.token,\n remote: parsed.remote ?? null,\n }\n}\n\n/**\n * Merge plaintext config + decrypted secrets + process env into a single\n * env-shaped record. Precedence (low → high): config → secrets → env. The\n * caller supplies the bootstrap `password` (typically `readAuthFile().token`)\n * because the secrets table is encrypted with it.\n *\n * Either layer may fail to read (corrupt row, wrong key, table absent at\n * fixture-bootstrap time); failures are swallowed per-layer so a single bad\n * value never blocks the merge.\n */\nexport function mergeSecretsIntoEnv(\n db: BazilionDb,\n password: string,\n env: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n let configValues: Record<string, string> = {}\n let secretValues: Record<string, string> = {}\n try {\n configValues = openConfig(db).getAll()\n } catch {\n // table missing or unreadable — continue without the layer\n }\n try {\n secretValues = openSecrets(db, password).getAll()\n } catch {\n // table missing or wrong password — continue without the layer\n }\n return { ...configValues, ...secretValues, ...env }\n}\n","import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { basename, join, resolve, sep } from 'node:path'\nimport type { SkillScanFinding } from '@bazilion/api-types'\nimport AdmZip from 'adm-zip'\nimport type { Paths } from '../paths.ts'\nimport { parseSkillFile } from './parse.ts'\nimport { scanSkillContent } from './scan.ts'\n\nexport interface ImportSkillsInput {\n /**\n * Absolute path to either:\n * - a single skill folder (contains SKILL.md),\n * - a parent directory containing multiple skill folders,\n * - a `.zip` file whose top level holds one-folder-per-skill (or a single\n * wrapping folder that holds them).\n */\n source: string\n /** overwrite existing target skills */\n force?: boolean\n}\n\nexport interface ImportResult {\n imported: string[]\n skipped: { name: string; reason: string; findings?: SkillScanFinding[] }[]\n findings?: Record<string, SkillScanFinding[]>\n}\n\nexport class SkillScanBlockedError extends Error {\n findings: Record<string, SkillScanFinding[]>\n\n constructor(findings: Record<string, SkillScanFinding[]>) {\n const names = Object.keys(findings).sort()\n super(\n `skill scan blocked import: ${names.join(', ')} ${names.length === 1 ? 'has' : 'have'} findings (rerun with --force to confirm)`,\n )\n this.name = 'SkillScanBlockedError'\n this.findings = findings\n }\n}\n\n/**\n * Extract a zip archive into a fresh temp dir with a zip-slip guard — every\n * entry's resolved path must stay under the extraction root. Returns both the\n * mkdtemp root (for cleanup) and the effective source path to hand to the\n * importer. When the archive is wrapped in a single top-level directory\n * (common with GitHub-style archives), we descend into it so `source` lines\n * up with the \"parent-of-skill-dirs\" shape the importer already understands.\n */\nfunction extractZipSafely(zipPath: string): { root: string; effectiveSource: string } {\n const root = mkdtempSync(join(tmpdir(), 'bazilion-skill-zip-'))\n try {\n const zip = new AdmZip(zipPath)\n for (const entry of zip.getEntries()) {\n const rawName = entry.entryName\n if (rawName.startsWith('/') || rawName.startsWith('\\\\')) {\n throw new Error(`zip entry has absolute path: ${rawName}`)\n }\n const resolved = resolve(root, rawName)\n if (resolved !== root && !resolved.startsWith(root + sep)) {\n throw new Error(`zip entry escapes extraction root: ${rawName}`)\n }\n }\n zip.extractAllTo(root, true)\n } catch (err) {\n rmSync(root, { recursive: true, force: true })\n throw err\n }\n\n // Unwrap a single top-level folder if present — the importer's\n // \"source-is-a-directory\" cases (single-skill dir vs. parent-of-skills dir)\n // both apply equally well to the unwrapped path.\n let effectiveSource = root\n const topEntries = readdirSync(root, { withFileTypes: true })\n if (topEntries.length === 1 && topEntries[0]?.isDirectory()) {\n effectiveSource = join(root, topEntries[0].name)\n }\n return { root, effectiveSource }\n}\n\nexport function importSkills(paths: Paths, input: ImportSkillsInput): ImportResult {\n const rawSource = resolve(input.source)\n if (!existsSync(rawSource)) {\n throw new Error(`source does not exist: ${rawSource}`)\n }\n\n let source = rawSource\n let tempRoot: string | null = null\n const sourceStat = statSync(rawSource)\n if (sourceStat.isFile()) {\n if (!rawSource.toLowerCase().endsWith('.zip')) {\n throw new Error(`source file must be a .zip archive: ${rawSource}`)\n }\n const { root, effectiveSource } = extractZipSafely(rawSource)\n tempRoot = root\n source = effectiveSource\n } else if (!sourceStat.isDirectory()) {\n throw new Error(`source is not a directory: ${rawSource}`)\n }\n\n try {\n return importSkillsFromDir(paths, source, input)\n } finally {\n if (tempRoot) rmSync(tempRoot, { recursive: true, force: true })\n }\n}\n\nfunction importSkillsFromDir(paths: Paths, source: string, input: ImportSkillsInput): ImportResult {\n const candidates: { name: string; dir: string }[] = []\n\n // Two shapes are accepted:\n // 1. source is itself a single skill dir (contains SKILL.md)\n // 2. source is a parent containing multiple skill dirs\n if (existsSync(join(source, 'SKILL.md'))) {\n candidates.push({ name: basename(source), dir: source })\n } else {\n const entries = readdirSync(source, { withFileTypes: true })\n for (const e of entries) {\n if (!e.isDirectory()) continue\n const skillDir = join(source, e.name)\n if (!existsSync(join(skillDir, 'SKILL.md'))) continue\n candidates.push({ name: e.name, dir: skillDir })\n }\n }\n\n if (candidates.length === 0) {\n throw new Error(`no skills found in ${source}`)\n }\n\n // Validate and scan every SKILL.md before touching the target dir.\n const findingsBySkill: Record<string, SkillScanFinding[]> = {}\n for (const c of candidates) {\n const parsed = parseSkillFile(join(c.dir, 'SKILL.md'))\n const findings = scanSkillContent(parsed.raw)\n if (findings.length > 0) findingsBySkill[c.name] = findings\n }\n if (Object.keys(findingsBySkill).length > 0 && !input.force) {\n throw new SkillScanBlockedError(findingsBySkill)\n }\n\n const imported: string[] = []\n const skipped: { name: string; reason: string; findings?: SkillScanFinding[] }[] = []\n\n for (const c of candidates) {\n const target = join(paths.skillsDir, c.name)\n if (existsSync(target) && !input.force) {\n skipped.push({\n name: c.name,\n reason: 'already exists (use --force to overwrite)',\n findings: findingsBySkill[c.name],\n })\n continue\n }\n cpSync(c.dir, target, { recursive: true, force: !!input.force })\n imported.push(c.name)\n }\n\n return {\n imported,\n skipped,\n findings: Object.keys(findingsBySkill).length > 0 ? findingsBySkill : undefined,\n }\n}\n","import { readFileSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\n\n/**\n * Standard agent-skill frontmatter. Required fields are typed; the rest is open\n * (skill formats may add fields like `allowed-tools`, `homepage`, etc.).\n */\nexport interface SkillFrontmatter {\n name: string\n description: string\n [key: string]: unknown\n}\n\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/\n\nexport function parseSkillContent(raw: string): ParsedSkill {\n const m = raw.match(FRONTMATTER_RE)\n if (!m) {\n throw new Error('SKILL.md missing YAML frontmatter (expected leading \"---\")')\n }\n const yamlBlock = m[1] ?? ''\n const body = m[2] ?? ''\n\n let fm: unknown\n try {\n fm = parseYaml(yamlBlock)\n } catch (err) {\n throw new Error(`SKILL.md frontmatter is not valid YAML: ${(err as Error).message}`)\n }\n if (!fm || typeof fm !== 'object' || Array.isArray(fm)) {\n throw new Error('SKILL.md frontmatter must be a YAML object')\n }\n const fmObj = fm as Record<string, unknown>\n if (typeof fmObj.name !== 'string' || fmObj.name.length === 0) {\n throw new Error('SKILL.md frontmatter missing required \"name\"')\n }\n if (typeof fmObj.description !== 'string' || fmObj.description.length === 0) {\n throw new Error('SKILL.md frontmatter missing required \"description\"')\n }\n return { frontmatter: fmObj as SkillFrontmatter, body, raw }\n}\n\nexport function parseSkillFile(path: string): ParsedSkill {\n const raw = readFileSync(path, 'utf8')\n return parseSkillContent(raw)\n}\n","import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { MemoryEntry, MemoryHit } from '@bazilion/api-types'\nimport { createStore, extractSnippet, type QMDStore } from '@tobilu/qmd'\nimport type { MemoryBackend } from './types.ts'\n\nconst INDEX_FILENAME = '.qmd-index.sqlite'\nconst COLLECTION_NAME = 'memory'\nconst PATTERN = '**/*.md'\n\n// One store per memory directory per process. createStore opens a SQLite\n// handle; we dedupe so concurrent chat requests for the same agent reuse it.\nconst storeCache = new Map<string, Promise<QMDStore>>()\n\nfunction getStore(dir: string): Promise<QMDStore> {\n let p = storeCache.get(dir)\n if (!p) {\n p = createStore({\n dbPath: join(dir, INDEX_FILENAME),\n config: {\n collections: {\n [COLLECTION_NAME]: { path: dir, pattern: PATTERN },\n },\n },\n })\n storeCache.set(dir, p)\n }\n return p\n}\n\nfunction safeKey(root: string, key: string): string {\n if (key.includes('..') || key.startsWith('/') || key.includes('\\0')) {\n throw new Error(`unsafe memory key: ${key}`)\n }\n return join(root, key)\n}\n\nfunction walkMd(dir: string, prefix: string, out: MemoryEntry[]): void {\n if (!existsSync(dir)) return\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n if (e.name.startsWith('.')) continue // skip .qmd-index.sqlite and friends\n const full = join(dir, e.name)\n const key = prefix ? `${prefix}/${e.name}` : e.name\n if (e.isDirectory()) {\n walkMd(full, key, out)\n } else if (e.isFile() && e.name.endsWith('.md')) {\n const stats = statSync(full)\n out.push({\n key,\n content: readFileSync(full, 'utf8'),\n updatedAt: stats.mtimeMs,\n })\n }\n }\n}\n\n/**\n * Memory backend backed by @tobilu/qmd — BM25 keyword search over markdown\n * files under `root`. Writes markdown to disk, then asks qmd to reindex.\n *\n * Uses `searchLex` only; no embeddings, no LLM rerank, no model download.\n * The hybrid `search()` / `searchVector()` paths exist in the qmd SDK and\n * can be wired in later if we want semantic search — they'd add a dependency\n * on `node-llama-cpp` and several GB of GGUF models.\n */\nexport function qmdBackend(root: string): MemoryBackend {\n return {\n async init() {\n mkdirSync(root, { recursive: true })\n // Opening the store + initial scan. update() is idempotent.\n const store = await getStore(root)\n await store.update()\n },\n\n async read(key) {\n const path = safeKey(root, key)\n if (!existsSync(path)) {\n throw new Error(`memory entry not found: ${key}`)\n }\n const stats = statSync(path)\n return {\n key,\n content: readFileSync(path, 'utf8'),\n updatedAt: stats.mtimeMs,\n }\n },\n\n async write(key, content) {\n const path = safeKey(root, key)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, content)\n const stats = statSync(path)\n // Reindex so the newly-written file is searchable on the next search().\n // update() re-scans the collection; for typical memory sizes (tens of\n // files) this is sub-millisecond.\n const store = await getStore(root)\n await store.update()\n return { key, content, updatedAt: stats.mtimeMs }\n },\n\n async search(query, opts) {\n const limit = opts?.limit ?? 10\n const store = await getStore(root)\n const results = await store.searchLex(query, {\n limit,\n collection: COLLECTION_NAME,\n })\n const hits: MemoryHit[] = []\n for (const r of results) {\n // qmd's filepath is a synthetic URI (`qmd://<collection>/<path>`);\n // displayPath is collection-prefixed (`<collection>/<path>`). Strip\n // the leading `<collection>/` to get the key the caller wrote.\n const prefix = `${COLLECTION_NAME}/`\n const key = r.displayPath.startsWith(prefix)\n ? r.displayPath.slice(prefix.length)\n : r.displayPath\n let content = r.body ?? ''\n if (!content) {\n try {\n content = readFileSync(join(root, key), 'utf8')\n } catch {\n content = ''\n }\n }\n const snippet = extractSnippet(content, query).snippet\n hits.push({ key, snippet, score: r.score })\n }\n return hits\n },\n\n async list() {\n const out: MemoryEntry[] = []\n walkMd(root, '', out)\n return out.sort((a, b) => a.key.localeCompare(b.key))\n },\n\n async remove(key) {\n const path = safeKey(root, key)\n if (existsSync(path)) rmSync(path)\n const store = await getStore(root)\n await store.update()\n },\n }\n}\n","// Translator: pi `AgentSessionEvent` → Bazilion `SessionEvent[]`.\n//\n// The worker's NDJSON wire format (`ChatFrame`) is still the same discrete\n// event stream CLI / browser clients know how to render. This module is the\n// adapter at the single choke-point — pi event in, zero-or-more Bazilion\n// events out.\n//\n// Coverage:\n// - user message_start → one `user_message`\n// - assistant text_delta → one `assistant_delta` per chunk\n// - assistant message_end:\n// - if the message carries text → one `assistant_message`\n// - for every tool-call content → one `tool_call`\n// - tool_execution_end → `tool_result` or `tool_error` based on isError\n// - message_end with aborted/error stopReason → one `error`\n//\n// Not surfaced (intentional):\n// - agent_start / agent_end — run-row lifecycle, not chat events\n// - turn_start / turn_end — too chatty, nothing to render\n// - message_start (assistant) — text will come via updates + end\n// - message_update (non-text) — thinking deltas etc; left for a later\n// pass when UIs render thinking blocks\n// - queue_update / compaction_* — session meta, not assistant output\n// - auto_retry_* — silently retried, user sees only the\n// eventual success or failure\n\nimport type { ProviderMessage, SessionEvent, ToolCall, ToolResultImage } from '@bazilion/api-types'\nimport type { AgentMessage, AgentToolResult } from '@earendil-works/pi-agent-core'\nimport type { AssistantMessage } from '@earendil-works/pi-ai'\nimport type { AgentSessionEvent } from '@earendil-works/pi-coding-agent'\n\nexport function translatePiEvent(e: AgentSessionEvent): SessionEvent[] {\n switch (e.type) {\n case 'message_start': {\n if (e.message.role === 'user') {\n return [{ type: 'user_message', text: stringifyContent(e.message.content) }]\n }\n return []\n }\n\n case 'message_update': {\n const inner = e.assistantMessageEvent\n if (inner.type === 'text_delta') {\n return [{ type: 'assistant_delta', delta: inner.delta }]\n }\n return []\n }\n\n case 'message_end': {\n const m = e.message\n if (m.role !== 'assistant') return []\n const out: SessionEvent[] = []\n const text = extractAssistantText(m as AssistantMessage)\n if (text) out.push({ type: 'assistant_message', text })\n for (const block of (m as AssistantMessage).content ?? []) {\n if (block.type === 'toolCall') {\n out.push({\n type: 'tool_call',\n id: block.id,\n name: block.name,\n arguments: JSON.stringify(block.arguments ?? {}),\n })\n }\n }\n const stopReason = (m as AssistantMessage).stopReason\n if (stopReason === 'aborted' || stopReason === 'error') {\n const errText = (m as AssistantMessage).errorMessage ?? stopReason\n out.push({ type: 'error', error: errText })\n }\n return out\n }\n\n case 'tool_execution_end': {\n const text = extractToolResultText(e.result)\n if (e.isError) {\n return [{ type: 'tool_error', id: e.toolCallId, name: e.toolName, error: text }]\n }\n const images = extractToolResultImages(e.result)\n return [\n {\n type: 'tool_result',\n id: e.toolCallId,\n name: e.toolName,\n result: text,\n ...(images.length > 0 ? { images } : {}),\n },\n ]\n }\n\n default:\n return []\n }\n}\n\nexport function extractAssistantText(m: AssistantMessage): string {\n let out = ''\n for (const block of m.content ?? []) {\n if (block.type === 'text') out += block.text\n }\n return out\n}\n\nexport function extractAssistantToolCalls(m: AssistantMessage): ToolCall[] {\n const out: ToolCall[] = []\n for (const block of m.content ?? []) {\n if (block.type === 'toolCall') {\n out.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.arguments ?? {}) })\n }\n }\n return out\n}\n\n/** Flatten `AgentToolResult.content` blocks into a single string — matches the\n * shape Bazilion tools always returned before pi adoption. */\nexport function extractToolResultText(result: unknown): string {\n const r = result as AgentToolResult<unknown> | undefined\n if (!r?.content) return ''\n let out = ''\n for (const block of r.content) {\n if (block.type === 'text') out += block.text\n }\n return out\n}\n\n/** Pull image blocks out of a tool result (browser screenshots, MCP images). */\nexport function extractToolResultImages(result: unknown): ToolResultImage[] {\n const r = result as AgentToolResult<unknown> | undefined\n if (!r?.content) return []\n const out: ToolResultImage[] = []\n for (const block of r.content) {\n if (block.type === 'image') {\n const b = block as { data?: string; mimeType?: string }\n if (typeof b.data === 'string' && typeof b.mimeType === 'string') {\n out.push({ data: b.data, mimeType: b.mimeType })\n }\n }\n }\n return out\n}\n\n/** Stringify a pi user-message content array. */\nfunction stringifyContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n let out = ''\n for (const block of content as { type: string; text?: string }[]) {\n if (block.type === 'text' && typeof block.text === 'string') out += block.text\n }\n return out\n}\n\n/**\n * Convert pi's authoritative `session.state.messages` (AgentMessage[]) into\n * Bazilion's ProviderMessage[] shape, so the `done` ChatFrame stays\n * compatible with browser + CLI clients that rehydrate from it.\n *\n * Notes on role mapping: pi uses `\"toolResult\"` for tool-response messages;\n * Bazilion uses `\"tool\"`. AssistantMessage content arrays become either\n * `content` string (text blocks joined) or a `toolCalls` array (toolCall\n * blocks converted).\n */\nexport function piMessagesToProviderView(messages: AgentMessage[]): ProviderMessage[] {\n const out: ProviderMessage[] = []\n for (const m of messages) {\n // Custom Bazilion message types would land in this switch too, but we\n // don't register any via CustomAgentMessages declaration merging yet.\n switch (m.role) {\n case 'user': {\n const content = (m as { content: unknown }).content\n const images = extractToolResultImages({ content })\n out.push({\n role: 'user',\n content: stringifyContent(content),\n ...(images.length > 0 ? { images } : {}),\n })\n break\n }\n case 'assistant': {\n const am = m as AssistantMessage\n const text = extractAssistantText(am)\n const toolCalls = extractAssistantToolCalls(am)\n const msg: ProviderMessage = { role: 'assistant', content: text }\n if (toolCalls.length > 0) msg.toolCalls = toolCalls\n out.push(msg)\n break\n }\n case 'toolResult': {\n const tr = m as { content: unknown; toolCallId?: string; toolName?: string }\n const images = extractToolResultImages({ content: tr.content })\n out.push({\n role: 'tool',\n content: stringifyContent(tr.content),\n toolCallId: tr.toolCallId,\n toolName: tr.toolName,\n ...(images.length > 0 ? { images } : {}),\n })\n break\n }\n default:\n // System and other unknown roles are skipped — the runtime's system\n // prompt is already wired via pi's settingsManager + our buildSystemPrompt.\n break\n }\n }\n return out\n}\n","// Bazilion → pi-coding-agent session bridge.\n//\n// `createBazilionSession` returns a fully-wired `AgentSession` suitable for\n// calling `session.prompt(text)` / `session.compact(instructions)` / etc.\n//\n// What we take ownership of (and hand to pi):\n// - cwd: the agent's default workspace path (or agent.dir as a degenerate\n// fallback when no workspace is mounted). Pi's built-in `read/bash/edit/\n// write/grep/find/ls` tools are rooted here.\n// - agentDir: `<bazilion-home>/pi` — pi writes transient state here\n// (settings overrides, resource caches). We don't share it with the\n// user's global `~/.pi/agent` so a Bazilion install never clobbers an\n// independent pi CLI install.\n// - authStorage: `InMemoryAuthStorageBackend` pre-seeded with the resolved\n// API key for the agent's current provider. We never let pi read/write\n// its own auth file — secrets live in the daemon-owned `secrets` table\n// and reach us via `opts.apiKey` (initial) + `opts.refreshApiKey`\n// (OAuth refresher for long turns).\n// - modelRegistry: in-memory. Native providers (anthropic/openai/google/…)\n// come from pi's bundled catalog. For Bazilion-only providers\n// (`lmstudio`, `ollama`) we call `registerProvider(name, {baseUrl,\n// api: 'openai-completions', authHeader: false})` — matches the\n// openai-completions shim our pi-adapter has been using.\n// - sessionManager: `SessionManager.create(cwd, <agentDir>/sessions)`\n// writing JSONL to `~/.bazilion/agents/<id>/sessions/<sessionId>.jsonl`.\n// Crash-survival, append-only, branching, compaction entries — all owned\n// by pi now. Replaces our `agents.chat_messages` blob.\n// - settingsManager: in-memory. Bazilion controls auto-compaction\n// (disabled — we compact manually on user request via /compact) and\n// retry (enabled with Bazilion-tuned caps).\n//\n// What stays outside pi's purview:\n// - spawning agents / profiles / skills discovery (core/)\n// - workspaces registry & mount tracking (core/)\n// - inter-agent messaging, triggers, scheduler (core + apps/web)\n// - memory backend (we wrap it as a pi customTool via `createBazilionCustomTools`)\n\nimport { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { basename, join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { AgentMessage, ThinkingLevel } from '@earendil-works/pi-agent-core'\nimport {\n type AgentSession,\n AuthStorage,\n createAgentSession,\n createExtensionRuntime,\n ModelRegistry,\n type ResourceLoader,\n SessionManager,\n SettingsManager,\n} from '@earendil-works/pi-coding-agent'\nimport type { BazilionDb, Paths } from '../../core/index.ts'\nimport { providerStateRepo } from '../../core/index.ts'\nimport type { MemoryBackend } from '../memory/types.ts'\nimport { resolveModel as resolvePiModel } from '../providers/pi-adapter.ts'\nimport { createProviderRegistry, loadProviderConfigFromEnv } from '../providers/registry.ts'\nimport { buildSystemPrompt } from '../session/prompt.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n McpHost,\n MessagingHost,\n UserMdHost,\n} from '../worker/ipc-protocol.ts'\nimport { createBazilionCustomTools } from './tools.ts'\n\nconst BUILTIN_TOOL_NAMES = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'] as const\n\nexport interface CreateBazilionSessionOptions {\n agent: ResolvedAgent\n paths: Paths\n /** Merged env (process.env + secrets) — produced via `mergeSecretsIntoEnv`. */\n env: NodeJS.ProcessEnv\n memory: MemoryBackend\n /**\n * Names of providers the user has explicitly enabled in /config.\n * Empty set means \"no per-provider gating configured\" — all providers pass.\n * Pre-computed by the daemon and handed in so the session never has to\n * touch the SQLite `provider_state` table itself.\n */\n enabledProviders: Set<string>\n /**\n * Optional host for inter-agent messaging. Wired from the worker's IPC\n * channel back to the daemon — workers no longer hold a SQLite handle of\n * their own. Omit to disable the messaging tools entirely (e.g. unit\n * tests that don't exercise inbox flows).\n */\n messagingHost?: MessagingHost\n /**\n * Optional host for the group-shared USER.md append tool. Like\n * `messagingHost`, this is wired via the worker's IPC channel. Omit to\n * disable the `user_md_append` tool.\n */\n userMdHost?: UserMdHost\n /**\n * Optional browser pool surface for the `browser_*` tools. Wired from the\n * worker's IPC channel back to the daemon (the Playwright session lives in\n * the daemon, persistent across turns). Omit to disable browser automation.\n */\n browserHost?: BrowserHost\n /**\n * Optional MCP connection-pool surface for proxied MCP tools, paired with\n * `mcpTools`. Wired via the worker's IPC channel. Omit when no MCP servers\n * are enabled.\n */\n mcpHost?: McpHost\n /** MCP tools discovered daemon-side, exposed as proxy tools alongside `mcpHost`. */\n mcpTools?: InjectedMcpTool[]\n /** If provided, enables the `deliver_file` tool — the agent's outbound file channel. */\n fileSink?: import('../tools/deliver-file.ts').FileSink\n /**\n * Optional explicit API key for the agent's provider. Wins over any value\n * derived from `env`. Required for OAuth-backed providers (`openai-codex`)\n * since their credentials live in the daemon-owned `secrets` table, not\n * in env vars.\n */\n apiKey?: string\n /**\n * Optional callback for OAuth-backed providers whose access tokens may\n * expire mid-turn. When provided, pi calls it to refresh the JWT during\n * long tool-execution loops. Daemon-side callers (compact/context/truncate)\n * wire this directly against the secrets repo; worker turns currently\n * skip it (the initial token from `apiKey` carries the whole turn).\n */\n refreshApiKey?: (providerName: string) => Promise<string>\n /**\n * Session id to resume. When omitted, pi starts a fresh session file.\n * `/reset` passes `undefined` to rotate; normal chat passes the agent's\n * current session id (persisted on the Bazilion side as `agents.session_id`\n * if we later add that column — today we just restore the most recent\n * session file, which pi's SessionManager locates automatically).\n */\n sessionId?: string\n}\n\nexport interface BazilionSessionHandle {\n session: AgentSession\n /** Call when done — disposes listeners + closes the pi session. */\n dispose(): void\n}\n\n/**\n * Build a pi `AgentSession` using Bazilion's resolved agent + provider state.\n * The returned session is ready for `prompt()` / `compact()` / `reset()`.\n */\nexport async function createBazilionSession(\n opts: CreateBazilionSessionOptions,\n): Promise<BazilionSessionHandle> {\n const {\n agent,\n paths,\n env,\n memory,\n enabledProviders,\n messagingHost,\n userMdHost,\n browserHost,\n mcpHost,\n mcpTools,\n fileSink,\n refreshApiKey,\n } = opts\n\n const { providerName, modelId } = splitModelString(agent.model)\n\n // Enabled-set gate — mirrors createProviderRegistry's check. We keep the\n // Bazilion-side enabled/disabled /config toggles authoritative even though\n // pi does its own provider resolution: we simply refuse to build a session\n // for a disabled provider. The set is pre-computed by the daemon (the\n // worker has no SQLite handle of its own).\n if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {\n throw new Error(`${providerName} provider is disabled — enable it on the /config page`)\n }\n\n // Build the pi Model<Api>. This reuses the same catalog-lookup + literal-\n // fallback that the pi-adapter uses for Provider.chat today, so `lmstudio:\n // any-model` / unreleased OpenAI models / etc. keep working.\n const piProviderName = mapProviderName(providerName)\n const model = resolvePiModel(\n {\n providerName,\n piProviderName,\n fallbackApi: pickFallbackApi(providerName),\n baseUrl: resolveBaseUrl(providerName, env),\n },\n modelId,\n )\n\n // Resolve the API key. Caller-supplied `opts.apiKey` wins (the daemon\n // passes pre-fetched OAuth tokens for `openai-codex` here); otherwise\n // fall back to the env-derived key. Pi's AuthStorage is in-memory only —\n // we never write `auth.json`. `setRuntimeApiKey` is the process-scoped\n // override hook AuthStorage exposes for exactly this.\n const apiKey = opts.apiKey ?? resolveApiKey(providerName, env)\n\n const authStorage = AuthStorage.inMemory()\n if (apiKey) {\n authStorage.setRuntimeApiKey(piProviderName, apiKey)\n }\n\n const modelRegistry = ModelRegistry.inMemory(authStorage)\n // Bazilion-only providers aren't in pi's bundled catalog; register them\n // dynamically so ModelRegistry accepts the Model<> object + resolves auth.\n if (providerName === 'lmstudio' || providerName === 'ollama') {\n modelRegistry.registerProvider(piProviderName, {\n baseUrl: model.baseUrl,\n api: 'openai-completions',\n authHeader: false,\n apiKey: apiKey ?? 'dummy',\n })\n }\n\n // cwd for pi's coding tools is the agent's group directory. Every agent\n // belongs to exactly one group; the group's filesystem root is where work\n // product lives and where the agent's `read`/`bash`/`edit`/`write` are\n // rooted. Private identity/soul files live in `agent.dir` and are reached\n // through the scoped `home_*` tools, not via cwd.\n const cwd = agent.group.path\n if (!existsSync(cwd)) mkdirSync(cwd, { recursive: true })\n\n // Session file under the agent's own directory. Keeping it under\n // `agents/<id>/sessions/` makes `bazilion uninstall` (data tier) already\n // clean them up without changes.\n //\n // Resume-or-create: pi's SessionManager has no built-in \"latest session\"\n // opener. We walk the session dir for the newest `.jsonl` and open it;\n // fall back to `create()` when none exists (fresh agent or post-/reset).\n // This is what makes turn-to-turn continuity work: each worker turn picks\n // up where the last one left off.\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n mkdirSync(sessionDir, { recursive: true })\n const existing = findMostRecent(sessionDir)\n const sessionManager = existing\n ? SessionManager.open(existing, sessionDir, cwd)\n : SessionManager.create(cwd, sessionDir)\n\n // In-memory settings: auto-compaction off (we trigger compaction manually\n // via /compact), retry on with Bazilion-tuned caps matching what withRetry\n // used to apply before pi-adoption.\n const settingsManager = SettingsManager.inMemory({\n compaction: { enabled: false },\n retry: {\n enabled: true,\n maxRetries: 2,\n baseDelayMs: 500,\n provider: { maxRetryDelayMs: 8_000 },\n },\n })\n\n // Bazilion-authored system prompt becomes an `appendSystemPrompt` entry.\n // Pi keeps its default base (which lists built-in tools + guidelines), our\n // profile content (SOUL.md / IDENTITY.md / workspaces / memory hint) is\n // concatenated after it. This is the same injection hook pi extensions use.\n const bazilionPrompt = buildSystemPrompt(agent)\n const resourceLoader = createBazilionResourceLoader(bazilionPrompt)\n await resourceLoader.reload()\n\n // Tool allowlist: pi's `tools` option is exclusive when provided — only\n // the listed names are enabled, regardless of what's in `customTools`.\n // So we have to enumerate both pi's built-in coding tools *and* every\n // Bazilion custom tool we want the LLM to see. Missing the custom names\n // from the allowlist would silently drop memory/messaging/web/bootstrap\n // tools from the agent's surface.\n const customTools = createBazilionCustomTools({\n agent,\n memory,\n messagingHost,\n userMdHost,\n browserHost,\n mcpHost,\n mcpTools,\n fileSink,\n env,\n })\n const allowedTools = [...BUILTIN_TOOL_NAMES, ...customTools.map((t) => t.name)]\n\n const { session } = await createAgentSession({\n cwd,\n agentDir: join(paths.home, 'pi'),\n model,\n thinkingLevel: toPiThinkingLevel(agent.reasoningLevel),\n tools: allowedTools,\n customTools,\n sessionManager,\n settingsManager,\n authStorage,\n modelRegistry,\n resourceLoader,\n })\n\n // OAuth providers: wire pi's per-request `getApiKey` callback so the JWT\n // gets refreshed *during* a long tool-execution loop, not just at the\n // start of the turn. This is exactly the use case pi-agent-core's doc\n // calls out for this hook (\"short-lived OAuth tokens that may expire\n // during long-running tool execution phases\"). Caller supplies the\n // refresher because only they have access to the secrets table.\n if (refreshApiKey) {\n session.agent.getApiKey = async (requestedProvider) => {\n if (requestedProvider !== piProviderName) return undefined\n try {\n return await refreshApiKey(providerName)\n } catch {\n // Stale/removed credentials mid-session → return undefined so pi\n // surfaces a \"no auth\" error cleanly instead of us throwing out of\n // the provider callback (which would drag down the whole turn).\n return undefined\n }\n }\n }\n\n return {\n session,\n dispose() {\n session.dispose()\n },\n }\n}\n\n// --- helpers ---\n\nfunction splitModelString(s: string): { providerName: string; modelId: string } {\n const idx = s.indexOf(':')\n if (idx === -1) {\n throw new Error(`invalid model string \"${s}\": expected \"provider:model\"`)\n }\n return { providerName: s.slice(0, idx), modelId: s.slice(idx + 1) }\n}\n\n/**\n * Map Bazilion provider names to pi's canonical `piProviderName` for catalog\n * lookups. The split exists because Bazilion was registering e.g. `bedrock`\n * but pi catalogs it as `amazon-bedrock`.\n */\nfunction mapProviderName(name: string): string {\n if (name === 'bedrock') return 'amazon-bedrock'\n return name\n}\n\nfunction pickFallbackApi(providerName: string): string {\n switch (providerName) {\n case 'anthropic':\n return 'anthropic-messages'\n case 'google':\n return 'google-generative-ai'\n case 'google-vertex':\n return 'google-vertex'\n case 'azure-openai':\n return 'azure-openai-responses'\n case 'bedrock':\n return 'bedrock-converse-stream'\n case 'openai-codex':\n return 'openai-codex-responses'\n default:\n return 'openai-completions'\n }\n}\n\nfunction resolveBaseUrl(providerName: string, env: NodeJS.ProcessEnv): string | undefined {\n if (providerName === 'lmstudio') return env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1'\n if (providerName === 'ollama') return env.OLLAMA_URL ?? 'http://127.0.0.1:11434/v1'\n return undefined\n}\n\nfunction resolveApiKey(providerName: string, env: NodeJS.ProcessEnv): string | undefined {\n // Hand-written table mirroring loadProviderConfigFromEnv — cheaper than\n // spinning up a whole ProviderRegistry just to pluck one field.\n switch (providerName) {\n case 'anthropic':\n return env.ANTHROPIC_OAUTH_TOKEN ?? env.ANTHROPIC_API_KEY\n case 'openai':\n return env.OPENAI_API_KEY\n case 'google':\n return env.GEMINI_API_KEY\n case 'mistral':\n return env.MISTRAL_API_KEY\n case 'groq':\n return env.GROQ_API_KEY\n case 'cerebras':\n return env.CEREBRAS_API_KEY\n case 'xai':\n return env.XAI_API_KEY\n case 'zai':\n return env.ZAI_API_KEY\n case 'huggingface':\n return env.HF_TOKEN\n case 'openrouter':\n return env.OPENROUTER_API_KEY\n case 'vercel-ai-gateway':\n return env.AI_GATEWAY_API_KEY\n case 'azure-openai':\n return env.AZURE_OPENAI_API_KEY\n case 'lmstudio':\n return env.LMSTUDIO_API_KEY ?? 'lm-studio'\n case 'ollama':\n return env.OLLAMA_API_KEY ?? 'ollama'\n default:\n return undefined\n }\n}\n\nfunction toPiThinkingLevel(level: string): ThinkingLevel {\n switch (level) {\n case 'off':\n case 'minimal':\n case 'low':\n case 'medium':\n case 'high':\n case 'xhigh':\n return level\n default:\n return 'medium'\n }\n}\n\n/**\n * Minimal `ResourceLoader` implementation — feeds pi our Bazilion-authored\n * system prompt block via `getAppendSystemPrompt` and returns empty collections\n * for everything else. Pi's default loader reads skill/prompt/theme markdown\n * from the workspace cwd; we intentionally opt out because Bazilion owns skill\n * discovery at the platform level (see `apps/daemon/src/core/skills`).\n */\nfunction createBazilionResourceLoader(appendSystemPrompt: string): ResourceLoader {\n const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() }\n return {\n getExtensions: () => extensions,\n getSkills: () => ({ skills: [], diagnostics: [] }),\n getPrompts: () => ({ prompts: [], diagnostics: [] }),\n getThemes: () => ({ themes: [], diagnostics: [] }),\n getAgentsFiles: () => ({ agentsFiles: [] }),\n getSystemPrompt: () => undefined,\n getAppendSystemPrompt: () => (appendSystemPrompt ? [appendSystemPrompt] : []),\n extendResources: () => {},\n async reload() {},\n }\n}\n\n/**\n * Re-exported for callers that want to check whether a provider is\n * Bazilion-enabled before even trying to spawn a session (e.g. /context\n * endpoint which builds a session just to enumerate tools).\n */\nexport function isProviderEnabled(db: BazilionDb, providerName: string): boolean {\n const enabled = providerStateRepo.listEnabled(db)\n return enabled.size === 0 || enabled.has(providerName)\n}\n\n/**\n * Escape hatch for callers that need the raw provider registry (e.g. the\n * current /api/providers/test endpoint). Keeps that one endpoint on our\n * existing non-pi path until we migrate it in a follow-up.\n */\nexport function loadEnabledRegistry(db: BazilionDb, authToken: string, env: NodeJS.ProcessEnv) {\n return createProviderRegistry(loadProviderConfigFromEnv(env, { db, authToken }), {\n enabledSet: providerStateRepo.listEnabled(db),\n })\n}\n\n/**\n * Read the most recent session file for an agent *without* spawning a full\n * AgentSession, and return the resolved provider-message view. Used for SSR\n * page loads that only need to render the canonical transcript — no need\n * to boot pi just to inspect the transcript.\n *\n * Returns an empty array when the agent has no prior session (fresh spawn,\n * or post-/reset).\n */\nexport function loadInitialMessages(agent: ResolvedAgent, paths: Paths): AgentMessage[] {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n if (!existsSync(sessionDir)) return []\n const cwd = agent.group.path\n if (!existsSync(cwd)) return []\n const recent = findMostRecent(sessionDir)\n if (!recent) return []\n try {\n const sm = SessionManager.open(recent, sessionDir)\n const ctx = sm.buildSessionContext()\n return ctx.messages\n } catch (err) {\n // Corrupt session file, stale format, or pi version bump — log loud\n // enough that an operator noticing a blank chat can find the cause in\n // server logs. The turn loop itself starts a fresh session on the\n // next message, so this isn't load-bearing for writes, only reads.\n console.error(\n `[session] loadInitialMessages failed for agent ${agent.agent.id} (${recent}):`,\n err instanceof Error ? (err.stack ?? err.message) : err,\n )\n return []\n }\n}\n\n/**\n * Cheap \"has the session changed?\" probe for polling clients (the web chat\n * stale-tab banner). Returns the most recent session file's basename plus\n * byte size — append-only JSONL, so either value moving means new activity.\n * Returns `{ file: null, size: 0 }` for agents that have never had a turn.\n */\nexport function loadSessionHead(\n agent: ResolvedAgent,\n paths: Paths,\n): { file: string | null; size: number } {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n if (!existsSync(sessionDir)) return { file: null, size: 0 }\n const recent = findMostRecent(sessionDir)\n if (!recent) return { file: null, size: 0 }\n try {\n const s = statSync(recent)\n return { file: basename(recent), size: s.size }\n } catch {\n return { file: null, size: 0 }\n }\n}\n\n/**\n * Test helper: seed a pi session file for an agent with `n` synthetic\n * user/assistant message pairs. Writes a real JSONL entry tree via\n * SessionManager so round-tripping through pi's own reader stays honest.\n * Exported from runtime rather than lived in tests because tests in apps/cli\n * can't directly import pi packages (not a direct dep).\n */\nexport function seedSessionForTest(\n agent: ResolvedAgent,\n paths: Paths,\n messages: Array<{ role: 'user' | 'assistant'; text: string }>,\n): void {\n const cwd = agent.group.path\n if (!existsSync(cwd)) mkdirSync(cwd, { recursive: true })\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n mkdirSync(sessionDir, { recursive: true })\n const sm = SessionManager.create(cwd, sessionDir)\n const now = Date.now()\n messages.forEach((m, i) => {\n if (m.role === 'user') {\n sm.appendMessage({\n role: 'user',\n content: [{ type: 'text', text: m.text }],\n timestamp: now + i,\n })\n } else {\n sm.appendMessage({\n role: 'assistant',\n content: [{ type: 'text', text: m.text }],\n api: 'openai-completions',\n provider: 'lmstudio',\n model: 'test-model',\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'stop',\n timestamp: now + i,\n })\n }\n })\n}\n\n/**\n * Test helper: count message entries on the current leaf's branch of the\n * agent's most-recent session file. Returns 0 when no session file exists.\n */\nexport function countSessionMessagesForTest(agent: ResolvedAgent, paths: Paths): number {\n const sessionDir = join(paths.agentDir(agent.agent.id), 'sessions')\n const recent = findMostRecent(sessionDir)\n if (!recent) return 0\n const cwd = agent.group.path\n try {\n const sm = SessionManager.open(recent, sessionDir, cwd)\n return sm.getBranch().filter((e) => e.type === 'message').length\n } catch {\n return 0\n }\n}\n\n/** Newest `.jsonl` in a pi session directory by mtime, or null if empty. */\nfunction findMostRecent(sessionDir: string): string | null {\n if (!existsSync(sessionDir)) return null\n let newest: { path: string; mtimeMs: number } | null = null\n for (const entry of readdirSync(sessionDir)) {\n if (!entry.endsWith('.jsonl')) continue\n const path = join(sessionDir, entry)\n try {\n const s = statSync(path)\n if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs }\n } catch {\n // ignore races\n }\n }\n return newest?.path ?? null\n}\n","// Adapter from Bazilion's Provider interface → pi-ai's streamSimple.\n//\n// Pi-ai (`@earendil-works/pi-ai`) is Mario Zechner's unified LLM SDK — 15+\n// providers behind one event-stream API. This file is the only place in the\n// codebase that touches pi-ai directly; everything else downstream\n// (`runTurnStream`, `persistRun`, the worker entry, CLI, web) sees the same\n// `Provider.chat(ProviderRequest): Promise<ProviderResponse>` contract it\n// always has. That keeps the wire format, DB schema, and chat UI stable while\n// giving us cost/usage, thinking levels, prompt caching, and every provider\n// pi supports — for free.\n//\n// Model resolution: we prefer pi's typed catalog via `getModel(provider, id)`\n// which carries cost + context-window metadata; for anything outside the\n// catalog (local models, newly-released models, custom OpenAI-compat\n// endpoints) we construct a `Model<>` literal with sensible defaults. This\n// preserves Bazilion's \"any model string\" flexibility.\n\nimport type { ProviderMessage, ReasoningLevel, ToolCall, ToolDef } from '@bazilion/api-types'\nimport {\n type AssistantMessage,\n getModel,\n type Model,\n type Message as PiMessage,\n type Tool as PiTool,\n type ToolCall as PiToolCall,\n streamSimple,\n type TextContent,\n Type,\n} from '@earendil-works/pi-ai'\nimport type { Provider, ProviderRequest, ProviderResponse, StopReason } from './types.ts'\n\nexport interface PiProviderConfig {\n /** Display name on the returned Provider; also the registry key (e.g. 'bedrock', 'azure-openai'). */\n providerName: string\n /** Pi's canonical provider name for catalog lookup (e.g. 'amazon-bedrock', 'azure-openai-responses'). Defaults to providerName. */\n piProviderName?: string\n /** Override baseUrl for openai-compat endpoints (lmstudio, ollama, custom). */\n baseUrl?: string\n /**\n * Static key or an async supplier. Suppliers are called at the top of each\n * chat() so OAuth-backed providers can refresh expiring tokens without\n * rebuilding the Provider instance (which the registry caches).\n */\n apiKey?: string | (() => string | Promise<string>)\n /** Which pi `Api` to use when the model isn't in pi's catalog. */\n fallbackApi: string\n}\n\nfunction defaultBaseUrlFor(providerName: string): string {\n switch (providerName) {\n case 'lmstudio':\n return process.env.LMSTUDIO_URL ?? 'http://127.0.0.1:1234/v1'\n case 'ollama':\n return process.env.OLLAMA_URL ?? 'http://127.0.0.1:11434/v1'\n default:\n return ''\n }\n}\n\nfunction buildModelLiteral(cfg: PiProviderConfig, modelId: string): Model<string> {\n return {\n id: modelId,\n name: modelId,\n api: cfg.fallbackApi,\n provider: cfg.providerName,\n baseUrl: cfg.baseUrl ?? defaultBaseUrlFor(cfg.providerName),\n reasoning: false,\n input: ['text'],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: 32_768,\n maxTokens: 4_096,\n }\n}\n\nexport function resolveModel(cfg: PiProviderConfig, modelId: string): Model<string> {\n const lookupName = cfg.piProviderName ?? cfg.providerName\n // Try pi's typed catalog first — gets us cost, context window, reasoning flags\n // for free on the known providers' known models.\n try {\n // getModel's type signature is catalog-constrained, but at runtime it just\n // indexes MODELS[provider][id]. Cast through unknown so unknown ids fall\n // through to the literal builder instead of tripping the compiler.\n const known = (getModel as unknown as (p: string, m: string) => Model<string> | undefined)(\n lookupName,\n modelId,\n )\n if (known && typeof known === 'object' && 'api' in known) {\n // Override baseUrl + provider for local / compat endpoints — pi's catalog\n // doesn't know about lmstudio/ollama but if a user types\n // `openai:gpt-4o` with a custom OPENAI_BASE_URL, honor that here.\n if (cfg.baseUrl) return { ...known, baseUrl: cfg.baseUrl }\n return known\n }\n } catch {\n // Fall through.\n }\n return buildModelLiteral(cfg, modelId)\n}\n\nfunction convertMessages(messages: ProviderMessage[]): PiMessage[] {\n const out: PiMessage[] = []\n const now = Date.now()\n for (const m of messages) {\n if (m.role === 'system') continue // pi takes system prompt separately\n if (m.role === 'user') {\n out.push({ role: 'user', content: m.content, timestamp: now })\n continue\n }\n if (m.role === 'assistant') {\n const content: AssistantMessage['content'] = []\n if (m.content) content.push({ type: 'text', text: m.content } satisfies TextContent)\n if (m.toolCalls) {\n for (const tc of m.toolCalls) {\n let parsed: Record<string, unknown> = {}\n try {\n parsed = JSON.parse(tc.arguments) as Record<string, unknown>\n } catch {\n // leave empty\n }\n content.push({\n type: 'toolCall',\n id: tc.id,\n name: tc.name,\n arguments: parsed,\n } satisfies PiToolCall)\n }\n }\n // Synthesize the AssistantMessage fields pi expects on replays — these\n // are only load-bearing for the LLM that gets the transcript; since we\n // don't persist usage/stopReason in Bazilion's message store, defaults\n // are fine.\n out.push({\n role: 'assistant',\n content,\n api: 'anthropic-messages',\n provider: 'anthropic',\n model: 'unknown',\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: 'stop',\n timestamp: now,\n })\n continue\n }\n if (m.role === 'tool') {\n out.push({\n role: 'toolResult',\n toolCallId: m.toolCallId ?? '',\n toolName: m.toolName ?? 'tool',\n content: [{ type: 'text', text: m.content }],\n isError: false,\n timestamp: now,\n })\n }\n }\n return out\n}\n\nfunction convertTools(tools: ToolDef[] | undefined): PiTool[] | undefined {\n if (!tools || tools.length === 0) return undefined\n return tools.map((t) => ({\n name: t.name,\n description: t.description,\n // Type.Unsafe lets us pass raw JSON Schema through without re-authoring in\n // typebox. Providers validate against the schema, not typebox's TSchema.\n parameters: Type.Unsafe<unknown>(t.parameters as Record<string, unknown>),\n }))\n}\n\nfunction toBazilionStopReason(reason: string): StopReason {\n switch (reason) {\n case 'stop':\n return 'stop'\n case 'length':\n return 'length'\n case 'toolUse':\n return 'tool_use'\n default:\n return 'error'\n }\n}\n\nfunction extractFinalResponse(msg: AssistantMessage): ProviderResponse {\n let text = ''\n const toolCalls: ToolCall[] = []\n for (const block of msg.content) {\n if (block.type === 'text') {\n text += block.text\n } else if (block.type === 'toolCall') {\n toolCalls.push({\n id: block.id,\n name: block.name,\n arguments: JSON.stringify(block.arguments ?? {}),\n })\n }\n }\n const res: ProviderResponse = {\n content: text,\n toolCalls,\n stopReason: toBazilionStopReason(msg.stopReason),\n }\n if (msg.usage) {\n res.usage = {\n promptTokens: msg.usage.input,\n completionTokens: msg.usage.output,\n }\n }\n return res\n}\n\nfunction mapReasoning(\n r: ReasoningLevel | undefined,\n): 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | undefined {\n if (!r || r === 'off') return undefined\n return r\n}\n\nasync function resolveApiKey(cfg: PiProviderConfig): Promise<string | undefined> {\n if (typeof cfg.apiKey === 'function') return await cfg.apiKey()\n return cfg.apiKey\n}\n\nexport function piProvider(cfg: PiProviderConfig): Provider {\n return {\n name: cfg.providerName,\n async chat(req: ProviderRequest): Promise<ProviderResponse> {\n const model = resolveModel(cfg, req.model)\n const apiKey = await resolveApiKey(cfg)\n\n const stream = streamSimple(\n model,\n {\n systemPrompt: req.system ?? '',\n messages: convertMessages(req.messages),\n tools: convertTools(req.tools),\n },\n {\n signal: req.signal,\n apiKey,\n reasoning: mapReasoning(req.reasoning),\n maxTokens: req.maxTokens,\n temperature: req.temperature,\n },\n )\n\n let finalMessage: AssistantMessage | null = null\n for await (const event of stream) {\n if (event.type === 'text_delta' && req.onDelta) {\n req.onDelta(event.delta)\n } else if (event.type === 'done') {\n finalMessage = event.message\n } else if (event.type === 'error') {\n finalMessage = event.error\n }\n }\n if (!finalMessage) {\n throw new Error(`pi provider ${cfg.providerName} returned no terminal event`)\n }\n if (finalMessage.stopReason === 'aborted' || finalMessage.stopReason === 'error') {\n const msg = finalMessage.errorMessage ?? 'provider error'\n throw new Error(msg)\n }\n return extractFinalResponse(finalMessage)\n },\n }\n}\n","// OpenAI ChatGPT / Codex OAuth — token storage + refresh on top of pi-ai.\n//\n// Pi-ai ships a complete OAuth flow for the ChatGPT backend (`@earendil-works/pi-ai`\n// exports `loginOpenAICodex` + `refreshOpenAICodexToken`), so this module is\n// thin: it adapts the credential I/O to Bazilion's encrypted secrets store\n// and exposes a single `loadAccessToken(db, authToken)` call that refreshes\n// when the token is about to expire.\n//\n// Storage: the JSON blob `{refresh, access, expires}` lives under the\n// secrets key `OPENAI_CODEX_OAUTH` in the `secrets` table. The blob is\n// never copied into the env (unlike plain-API-key providers) — refresh is\n// stateful, so every call reads and writes through the live secrets store.\n\nimport type { OpenAICodexStatus } from '@bazilion/api-types'\nimport type { OAuthCredentials } from '@earendil-works/pi-ai'\nimport { loginOpenAICodex, refreshOpenAICodexToken } from '@earendil-works/pi-ai/oauth'\nimport { type BazilionDb, openSecrets } from '../../core/index.ts'\n\nexport const OPENAI_CODEX_SECRET_KEY = 'OPENAI_CODEX_OAUTH'\n\n/** Refresh when the access token has less than this much life left. */\nconst REFRESH_MARGIN_MS = 60_000\n\nexport interface StoredCredentials {\n refresh: string\n access: string\n expires: number\n}\n\nfunction readCredentials(db: BazilionDb, authToken: string): StoredCredentials | null {\n const raw = openSecrets(db, authToken).get(OPENAI_CODEX_SECRET_KEY)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredentials>\n if (\n typeof parsed.refresh === 'string' &&\n typeof parsed.access === 'string' &&\n typeof parsed.expires === 'number'\n ) {\n return { refresh: parsed.refresh, access: parsed.access, expires: parsed.expires }\n }\n return null\n } catch {\n return null\n }\n}\n\nfunction writeCredentials(db: BazilionDb, authToken: string, creds: StoredCredentials): void {\n openSecrets(db, authToken).set(OPENAI_CODEX_SECRET_KEY, JSON.stringify(creds))\n}\n\nexport function clearCredentials(db: BazilionDb, authToken: string): void {\n openSecrets(db, authToken).remove(OPENAI_CODEX_SECRET_KEY)\n}\n\nexport function hasCredentials(db: BazilionDb, authToken: string): boolean {\n return readCredentials(db, authToken) !== null\n}\n\nfunction decodeAccountId(accessToken: string): string | null {\n const parts = accessToken.split('.')\n if (parts.length !== 3) return null\n try {\n const payload = JSON.parse(Buffer.from(parts[1] as string, 'base64').toString('utf8')) as {\n 'https://api.openai.com/auth'?: { chatgpt_account_id?: string }\n }\n return payload['https://api.openai.com/auth']?.chatgpt_account_id ?? null\n } catch {\n return null\n }\n}\n\nexport function getStatus(db: BazilionDb, authToken: string): OpenAICodexStatus {\n const creds = readCredentials(db, authToken)\n if (!creds) return { connected: false, expiresAt: null, accountId: null }\n return {\n connected: true,\n expiresAt: creds.expires,\n accountId: decodeAccountId(creds.access),\n }\n}\n\n/**\n * Returns a valid access token, refreshing via pi-ai if the stored one is\n * within `REFRESH_MARGIN_MS` of expiry. Throws when no credentials are stored\n * so callers can surface an actionable \"run `bazilion auth openai login`\"\n * error rather than a 401 from the upstream API.\n */\nexport async function loadAccessToken(db: BazilionDb, authToken: string): Promise<string> {\n const creds = readCredentials(db, authToken)\n if (!creds) {\n throw new Error(\n 'OpenAI ChatGPT OAuth not configured — run `bazilion auth openai login` (or use the Connect button on /config)',\n )\n }\n if (creds.expires > Date.now() + REFRESH_MARGIN_MS) return creds.access\n\n const refreshed = (await refreshOpenAICodexToken(creds.refresh)) as OAuthCredentials\n const next: StoredCredentials = {\n refresh: refreshed.refresh,\n access: refreshed.access,\n expires: refreshed.expires,\n }\n writeCredentials(db, authToken, next)\n return next.access\n}\n\n/** Persist credentials fetched by pi-ai's `loginOpenAICodex`. */\nexport function saveLoginCredentials(\n db: BazilionDb,\n authToken: string,\n creds: OAuthCredentials,\n): void {\n writeCredentials(db, authToken, {\n refresh: creds.refresh,\n access: creds.access,\n expires: creds.expires,\n })\n}\n\nexport { loginOpenAICodex, refreshOpenAICodexToken }\n","// Bazilion system-prompt builder. Feeds pi's `getAppendSystemPrompt()` hook\n// so the agent sees its persona + skills + workspaces + memory guidance\n// stacked on top of pi's built-in base prompt (which lists coding tools and\n// general guidelines). Pure filesystem read — no LLM, no DB.\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ResolvedAgent } from '@bazilion/api-types'\n\n// Prompt order: peers first (who else is around), then persona, then tooling\n// hints, then self-knowledge, then the wake-up playbook.\n//\n// BOOTSTRAP.md is intentionally NOT in this generic-context list — it gets\n// its own dedicated \"First-Run Ritual\" section below with explicit\n// anti-checklist framing, so models treat it as multi-turn Q&A guidance\n// rather than a one-shot script to execute. The system prompt regenerates\n// per turn, so the section auto-vanishes when `bootstrap_done` removes the\n// file (vs. wrapping the user message, which would persist in pi's session\n// JSONL forever and replay on every future turn).\nconst CONTEXT_FILE_ORDER = [\n 'AGENTS.md',\n 'SOUL.md',\n 'TOOLS.md',\n 'IDENTITY.md',\n 'HEARTBEAT.md',\n] as const\n\nexport function buildSystemPrompt(agent: ResolvedAgent): string {\n const parts: string[] = []\n\n const contextBlocks: string[] = []\n for (const file of CONTEXT_FILE_ORDER) {\n const path = join(agent.agent.dir, file)\n if (!existsSync(path)) continue\n const content = readFileSync(path, 'utf8').trimEnd()\n if (!content) continue\n contextBlocks.push(`## ${file}\\n\\n${content}`)\n }\n if (contextBlocks.length > 0) {\n parts.push(`# Project Context\\n\\n${contextBlocks.join('\\n\\n')}`)\n }\n\n // First-Run Ritual block — only emitted while BOOTSTRAP.md exists on disk.\n // The wording deliberately frames it as \"multi-turn Q&A\" not \"checklist\"\n // and lists hard rules at the top so tool-eager models still notice them\n // even if they skim the body.\n const bootstrapPath = join(agent.agent.dir, 'BOOTSTRAP.md')\n if (existsSync(bootstrapPath)) {\n const bootstrap = readFileSync(bootstrapPath, 'utf8').trimEnd()\n if (bootstrap) {\n parts.push(\n [\n '# First-Run Ritual',\n '',\n 'This is your first session. The document below is **conversational guidance**, not a checklist to execute in one shot. It describes a multi-turn Q&A you should have with the human, one question per turn.',\n '',\n '## Hard rules',\n '- Your first reply is ONLY a greeting + ONE question. No tool calls. Wait for the human to answer.',\n '- Each subsequent turn: at most one new question. Wait between turns.',\n '- Only after the ritual is complete (you have enough to write IDENTITY.md): call `home_write` once, then `bootstrap_done`.',\n '',\n '## BOOTSTRAP.md',\n '',\n bootstrap,\n ].join('\\n'),\n )\n }\n }\n\n parts.push(\n [\n '# Agent Home',\n '',\n 'Your private home holds who you are — identity, soul, behaviour rules, wake-up routine. It is not shared with other agents and cannot be overwritten by them. The files above (IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, HEARTBEAT.md) live in this home, plus BOOTSTRAP.md when you are still in your first-run ritual.',\n '',\n '- To change who you are (name, vibe, personality, how you behave): use `home_write`.',\n '- To inspect exact wording of your own files: use `home_read` or `home_list`.',\n '- To remember facts the user told you or things you learned: use `memory_write` — NOT `home_write`.',\n '- To produce work output (code, docs, artefacts): use `write` / `edit` — those land in your workspace, not your home.',\n ].join('\\n'),\n )\n\n if (agent.skills.length > 0) {\n parts.push(\n `# Available Skills\\n\\nYou have access to the following skills: ${agent.skills.join(', ')}.`,\n )\n }\n\n const groupLines = [\n '# Group',\n '',\n `- ${agent.group.id} (${agent.group.name}): ${agent.group.path}`,\n '',\n 'Your group is where work product lives — code, docs, artefacts, shared scratch. It may be shared with other agents in the same group. Your coding tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) are rooted at the group directory. Never use these tools to edit your identity/soul/behaviour files — those live in your home and are reached via `home_write` / `home_read`.',\n ]\n parts.push(groupLines.join('\\n'))\n\n if (agent.group.userMd.trim()) {\n parts.push(\n `# About the User\\n\\nShared context about the human you're working with in this group. Both you and the human curate it. To update: call \\`user_md_get\\` (returns current content + an etag), merge your change into the full text, then call \\`user_md_write\\` with the merged content and the etag. Use this for STABLE user-specific facts (preferences, role, working hours, how they like to be addressed) — and to CORRECT stale entries when the human tells you something different from what's recorded. For project knowledge use \\`memory_write\\` instead; for personal notes about yourself use \\`home_write\\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes — every agent in the group sees the new content in their system prompt on their next turn automatically.**\\n\\n${agent.group.userMd.trim()}`,\n )\n } else {\n parts.push(\n `# About the User\\n\\nThis group's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \\`user_md_get\\` then \\`user_md_write\\` (always get first — you need the etag). Reserve this for things you're confident are durable — project knowledge belongs in \\`memory_write\\`, personal notes about yourself in \\`home_write\\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes — every agent in the group sees the new content in their system prompt on their next turn automatically.**`,\n )\n }\n\n parts.push(\n '# Memory\\n\\nYou share a persistent memory backend with every other agent in this group. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge — codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the group may have already learned something useful. **Do NOT send peer messages announcing memory writes — every agent has access to the same store via `memory_search` and will find your note when they need it.**',\n )\n\n return parts.join('\\n\\n---\\n\\n')\n}\n","// Adapter: Bazilion ToolHandler → pi-coding-agent ToolDefinition.\n//\n// Pi expects tools to return `AgentToolResult<TDetails>` =\n// `{ content: (TextContent | ImageContent)[]; details: TDetails; terminate?: boolean }`.\n// Our handlers return plain strings. The adapter wraps the string into a single\n// text content block with empty details — the same fidelity we had before.\n//\n// Pi's tool execute contract: throw on failure. The agent loop wraps thrown\n// errors as tool-result messages with `isError: true`. We preserve this shape\n// directly because our handlers already throw on bad args / runtime failures.\n//\n// What's in this module:\n// - `ourToolToPiTool` — single-handler wrapper.\n// - `createBazilionCustomTools` — composed suite of the Bazilion-specific\n// tools (memory_*, messaging, bootstrap_done, web_search/fetch). File I/O\n// tools are *not* here — pi's createCodingTools(cwd, …) replaces them.\n\nimport type { ResolvedAgent } from '@bazilion/api-types'\nimport type { ToolDefinition } from '@earendil-works/pi-coding-agent'\nimport { Type } from 'typebox'\nimport type { MemoryBackend } from '../memory/types.ts'\nimport { bootstrapTool } from '../tools/bootstrap.ts'\nimport { browserTools } from '../tools/browser.ts'\nimport { deliverFileTool, type FileSink } from '../tools/deliver-file.ts'\nimport { homeTools } from '../tools/home.ts'\nimport { mcpProxyTools } from '../tools/mcp.ts'\nimport { memoryTools } from '../tools/memory.ts'\nimport { messagingTools } from '../tools/messaging.ts'\nimport type { ToolHandler, ToolOutput } from '../tools/types.ts'\nimport { userMdTools } from '../tools/user-md.ts'\nimport { webTools } from '../tools/web.ts'\nimport type {\n BrowserHost,\n InjectedMcpTool,\n McpHost,\n MessagingHost,\n UserMdHost,\n} from '../worker/ipc-protocol.ts'\n\n/**\n * Wrap a Bazilion `ToolHandler` as a pi `ToolDefinition` so it can be passed\n * through `customTools` to `createAgentSession`.\n *\n * Design note: we keep the same tool name + description + parameter JSON schema\n * that the handler already declares. Pi wants a `TypeBox` schema; we pass the\n * JSONSchema through `Type.Unsafe` so the LLM validation happens on pi's side\n * without us having to re-author schemas in typebox syntax.\n */\nexport function ourToolToPiTool(h: ToolHandler): ToolDefinition {\n return {\n name: h.def.name,\n label: h.def.name,\n description: h.def.description,\n parameters: Type.Unsafe<Record<string, unknown>>(h.def.parameters as Record<string, unknown>),\n async execute(_toolCallId, params) {\n const out = await h.invoke(params as Record<string, unknown>)\n return {\n content: toPiContent(out),\n details: {},\n }\n },\n }\n}\n\n/**\n * Map a Bazilion `ToolOutput` to pi's `(TextContent | ImageContent)[]`. A bare\n * string becomes a single text block (the legacy shape); a part array maps\n * 1:1 — pi's content blocks have the same field names.\n */\nfunction toPiContent(\n out: ToolOutput,\n): Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> {\n if (typeof out === 'string') return [{ type: 'text', text: out }]\n return out.map((p) =>\n p.type === 'text'\n ? { type: 'text', text: p.text }\n : { type: 'image', data: p.data, mimeType: p.mimeType },\n )\n}\n\nexport interface BazilionCustomToolsOpts {\n agent: ResolvedAgent\n memory: MemoryBackend\n /** If provided, enables inter-agent messaging tools. */\n messagingHost?: MessagingHost\n /** If provided, enables the `user_md_append` tool. */\n userMdHost?: UserMdHost\n /** If provided, enables the `browser_*` tools (proxied to the daemon pool). */\n browserHost?: BrowserHost\n /** If provided alongside `mcpTools`, enables the discovered MCP proxy tools. */\n mcpHost?: McpHost\n /** MCP tools discovered daemon-side, exposed as proxy tools. */\n mcpTools?: InjectedMcpTool[]\n /** If provided, enables the `deliver_file` tool (emits a `file` event). */\n fileSink?: FileSink\n /** Merged env (process.env + secrets). */\n env?: NodeJS.ProcessEnv\n}\n\n/**\n * Build the list of Bazilion-specific custom tools in the shape pi expects.\n *\n * Excludes file-I/O tools (`read`/`bash`/`edit`/`write`/`grep`/`find`/`ls`) —\n * those come from pi's `createCodingTools(cwd, …)` now that we've adopted the\n * richer toolset. Also excludes the legacy `workspace_list/read/write`\n * triumvirate: pi's tools work against a single cwd (the agent's default\n * workspace), and mounted non-default workspaces are reachable via absolute\n * paths through pi's `bash`/`read`/`edit`.\n */\nexport function createBazilionCustomTools(opts: BazilionCustomToolsOpts): ToolDefinition[] {\n const handlers: ToolHandler[] = [\n ...memoryTools(opts.memory),\n ...homeTools(opts.agent.agent.dir),\n bootstrapTool(opts.agent.agent.dir),\n ...webTools({ env: opts.env }),\n ]\n if (opts.messagingHost) {\n handlers.push(...messagingTools(opts.messagingHost, opts.agent.agent.id))\n }\n if (opts.userMdHost) {\n handlers.push(...userMdTools(opts.userMdHost, opts.agent.group.id))\n }\n if (opts.browserHost) {\n handlers.push(...browserTools(opts.browserHost, opts.agent.agent.id))\n }\n if (opts.mcpHost && opts.mcpTools && opts.mcpTools.length > 0) {\n handlers.push(...mcpProxyTools(opts.mcpHost, opts.mcpTools))\n }\n if (opts.fileSink) {\n handlers.push(deliverFileTool(opts.agent.group.path, opts.fileSink))\n }\n return handlers.map(ourToolToPiTool)\n}\n","import { existsSync, rmSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\nexport function bootstrapTool(agentDir: string): ToolHandler {\n return {\n def: {\n name: 'bootstrap_done',\n description:\n 'Call this once you have finished your bootstrap conversation (introduced yourself, learned the user, updated IDENTITY.md). Removes BOOTSTRAP.md so it does not appear in future sessions.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const path = join(agentDir, 'BOOTSTRAP.md')\n if (existsSync(path)) {\n rmSync(path)\n return 'BOOTSTRAP.md removed. Bootstrap is complete.'\n }\n return 'BOOTSTRAP.md was already removed.'\n },\n }\n}\n","// Browser automation tools (worker-side definitions).\n//\n// These are thin proxies: each tool's `invoke` forwards to the daemon-side\n// Playwright pool over IPC via the injected `BrowserHost`. The browser session\n// (stateful, persistent across turns) lives entirely in the daemon — see\n// `lib/browser/pool.ts`. Results are multimodal: snapshots/console/network come\n// back as text, `browser_take_screenshot` as a base64 image.\n//\n// Perception is accessibility-tree-first. `browser_snapshot` returns an aria\n// tree with `[ref=eN]` element refs; every interaction targets an element by\n// that `ref`. No vision model is needed for routine automation — screenshots\n// are the exception, not the rule.\n\nimport type { BrowserHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\nconst REF_DESC = 'Element ref from the latest browser_snapshot, e.g. \"e5\".'\n\n/**\n * Build the `browser_*` tool suite bound to one agent's session. Each tool\n * forwards to `host.invoke(agentId, <action>, args)`.\n */\nexport function browserTools(host: BrowserHost, agentId: string): ToolHandler[] {\n const proxy = (\n name: string,\n action: string,\n description: string,\n properties: Record<string, unknown>,\n required: string[] = [],\n ): ToolHandler => ({\n def: {\n name,\n description,\n parameters: { type: 'object', properties, required, additionalProperties: false },\n },\n invoke: (args) => host.invoke(agentId, action, args),\n })\n\n return [\n proxy(\n 'browser_navigate',\n 'navigate',\n 'Open a URL in the browser and return an accessibility snapshot of the loaded page. Starts a persistent browser session that survives across turns (cookies/logins carry over).',\n { url: { type: 'string', description: 'Absolute URL to navigate to.' } },\n ['url'],\n ),\n proxy(\n 'browser_snapshot',\n 'snapshot',\n 'Capture the current page as an accessibility tree (YAML) with [ref=eN] element references. This is the primary way to \"see\" the page — prefer it over screenshots. Use the refs with browser_click/type/etc.',\n {},\n ),\n proxy(\n 'browser_click',\n 'click',\n 'Click an element identified by its snapshot ref. Returns a fresh snapshot.',\n { ref: { type: 'string', description: REF_DESC } },\n ['ref'],\n ),\n proxy(\n 'browser_type',\n 'type',\n 'Type text into an input/textarea identified by its ref. Set submit=true to press Enter afterwards. Returns a fresh snapshot.',\n {\n ref: { type: 'string', description: REF_DESC },\n text: { type: 'string', description: 'Text to type into the field.' },\n submit: { type: 'boolean', description: 'Press Enter after typing.' },\n },\n ['ref', 'text'],\n ),\n proxy(\n 'browser_hover',\n 'hover',\n 'Hover the pointer over an element identified by its ref. Returns a fresh snapshot.',\n { ref: { type: 'string', description: REF_DESC } },\n ['ref'],\n ),\n proxy(\n 'browser_select',\n 'select',\n 'Select option(s) in a <select> element identified by its ref.',\n {\n ref: { type: 'string', description: REF_DESC },\n values: {\n type: 'array',\n items: { type: 'string' },\n description: 'Option values to select.',\n },\n },\n ['ref', 'values'],\n ),\n proxy(\n 'browser_fill_form',\n 'fill_form',\n 'Fill multiple form fields in one call. Each field is { ref, value }.',\n {\n fields: {\n type: 'array',\n description: 'Fields to fill.',\n items: {\n type: 'object',\n properties: {\n ref: { type: 'string', description: REF_DESC },\n value: { type: 'string' },\n },\n required: ['ref', 'value'],\n },\n },\n },\n ['fields'],\n ),\n proxy(\n 'browser_press_key',\n 'press_key',\n 'Press a keyboard key (e.g. \"Enter\", \"Escape\", \"ArrowDown\", \"Control+A\").',\n { key: { type: 'string', description: 'Key or chord to press.' } },\n ['key'],\n ),\n proxy(\n 'browser_go_back',\n 'go_back',\n 'Navigate back to the previous page in history. Returns a fresh snapshot.',\n {},\n ),\n proxy(\n 'browser_tabs',\n 'tabs',\n 'Manage tabs. op=\"list\" lists open tabs; \"new\" opens a tab (optional url); \"select\" focuses tab at index; \"close\" closes tab at index (defaults to active).',\n {\n op: { type: 'string', enum: ['list', 'new', 'select', 'close'] },\n index: { type: 'number', description: 'Tab index for select/close.' },\n url: { type: 'string', description: 'URL to open for op=new.' },\n },\n ['op'],\n ),\n proxy(\n 'browser_take_screenshot',\n 'take_screenshot',\n 'Take a PNG screenshot of the current page (returned as an image). Use only for visual verification or canvas/pixel content the accessibility snapshot cannot represent — prefer browser_snapshot for interaction.',\n { full_page: { type: 'boolean', description: 'Capture the full scrollable page.' } },\n ),\n proxy(\n 'browser_console',\n 'console',\n 'Return recent browser console messages (logs, warnings, errors).',\n {},\n ),\n proxy(\n 'browser_network',\n 'network',\n 'Return recent network requests issued by the page (method, status, URL).',\n {},\n ),\n ]\n}\n","// `deliver_file` — the agent's outbound document channel.\n//\n// The agent produces a file in its workspace (write a report, render a chart,\n// zip something up) and calls deliver_file(path) to hand it to the user. The\n// file lives on the daemon's filesystem (the worker is a subprocess on the same\n// host), so we read it directly, base64-encode it, and emit it as a `file`\n// SessionEvent via an injected sink — the worker writes that frame to stdout,\n// the daemon forwards it, and each client surfaces it (web download link,\n// Telegram document, CLI save-to-disk).\n\nimport { readFileSync, statSync } from 'node:fs'\nimport { basename, extname, isAbsolute, resolve } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\nconst MAX_DELIVER_BYTES = 25 * 1024 * 1024\n\nconst MIME: Record<string, string> = {\n '.pdf': 'application/pdf',\n '.txt': 'text/plain',\n '.md': 'text/markdown',\n '.csv': 'text/csv',\n '.json': 'application/json',\n '.html': 'text/html',\n '.zip': 'application/zip',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n}\n\nexport type FileSink = (file: { name: string; mimeType: string; data: string }) => void\n\nexport function deliverFileTool(cwd: string, sink: FileSink): ToolHandler {\n return {\n def: {\n name: 'deliver_file',\n description:\n 'Send a file from your workspace to the user so they can download it (web), receive it as a document (Telegram), or save it (CLI). Use this to deliver reports, exports, or any artifact you produced. Max 25 MB.',\n parameters: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file, relative to your workspace or absolute.',\n },\n },\n required: ['path'],\n additionalProperties: false,\n },\n },\n async invoke(args) {\n const p = String(args.path ?? '')\n if (!p) throw new Error('deliver_file: path is required')\n const abs = isAbsolute(p) ? p : resolve(cwd, p)\n let size: number\n try {\n size = statSync(abs).size\n } catch {\n throw new Error(`deliver_file: no such file: ${p}`)\n }\n if (size > MAX_DELIVER_BYTES) {\n throw new Error(\n `deliver_file: \"${basename(abs)}\" is too large (${(size / 1024 / 1024).toFixed(1)} MB > 25 MB)`,\n )\n }\n const name = basename(abs)\n const mimeType = MIME[extname(abs).toLowerCase()] ?? 'application/octet-stream'\n sink({ name, mimeType, data: readFileSync(abs).toString('base64') })\n return `Delivered \"${name}\" (${mimeType}) to the user.`\n },\n }\n}\n","import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { ToolHandler } from './types.ts'\n\n// Files the agent may read/write in its private home directory.\n// BOOTSTRAP.md is readable but not writable — its lifecycle belongs to\n// the `bootstrap_done` tool, not `home_write`.\nconst HOME_FILES_READABLE = [\n 'IDENTITY.md',\n 'SOUL.md',\n 'BOOTSTRAP.md',\n 'AGENTS.md',\n 'TOOLS.md',\n 'HEARTBEAT.md',\n] as const\n\nconst HOME_FILES_WRITABLE = [\n 'IDENTITY.md',\n 'SOUL.md',\n 'AGENTS.md',\n 'TOOLS.md',\n 'HEARTBEAT.md',\n] as const\n\nexport function homeTools(agentDir: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'home_read',\n description:\n 'Read one of your own home files — your identity, soul, or behaviour rules. These files are private to you and are also injected into your system prompt; read them when you need to quote exact wording or check current state.',\n parameters: {\n type: 'object',\n properties: {\n file: { type: 'string', enum: [...HOME_FILES_READABLE] },\n },\n required: ['file'],\n },\n },\n async invoke(args) {\n const file = String(args.file ?? '')\n if (!HOME_FILES_READABLE.includes(file as (typeof HOME_FILES_READABLE)[number])) {\n throw new Error(\n `home_read: \"file\" must be one of ${HOME_FILES_READABLE.join(', ')}; got \"${file}\"`,\n )\n }\n const path = join(agentDir, file)\n try {\n return readFileSync(path, 'utf8')\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(`home_read: could not read ${file}: ${msg}`)\n }\n },\n },\n {\n def: {\n name: 'home_write',\n description:\n \"Overwrite one of your own home files. Use this to update your name, personality, or persistent self-definition. Do NOT use this for work output (use `write` / `edit` — those land in your group's shared directory) or for facts you want to remember (use `memory_write`). BOOTSTRAP.md is not writable here; call `bootstrap_done` to retire it.\",\n parameters: {\n type: 'object',\n properties: {\n file: { type: 'string', enum: [...HOME_FILES_WRITABLE] },\n content: { type: 'string', description: 'new full file content' },\n },\n required: ['file', 'content'],\n },\n },\n async invoke(args) {\n const file = String(args.file ?? '')\n if (!HOME_FILES_WRITABLE.includes(file as (typeof HOME_FILES_WRITABLE)[number])) {\n throw new Error(\n `home_write: \"file\" must be one of ${HOME_FILES_WRITABLE.join(', ')}; got \"${file}\"`,\n )\n }\n const content = typeof args.content === 'string' ? args.content : ''\n const path = join(agentDir, file)\n writeFileSync(path, content, 'utf8')\n return `wrote ${file} (${Buffer.byteLength(content, 'utf8')} bytes)`\n },\n },\n {\n def: {\n name: 'home_list',\n description: 'List your home files with their sizes.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const entries: string[] = []\n for (const file of HOME_FILES_READABLE) {\n const path = join(agentDir, file)\n try {\n const s = statSync(path)\n entries.push(`${file} (${s.size}b)`)\n } catch {\n // file not present — skip\n }\n }\n if (entries.length === 0) {\n const dirEntries = (() => {\n try {\n return readdirSync(agentDir)\n } catch {\n return []\n }\n })()\n return `(no home files found; agent dir contains: ${dirEntries.join(', ') || 'nothing'})`\n }\n return entries.join('\\n')\n },\n },\n ]\n}\n","// MCP proxy tools (worker-side definitions).\n//\n// The daemon discovers each enabled MCP server's tools (`tools/list`) before\n// the worker spawns and ships them in `WorkerInput.mcpTools`. Here we turn each\n// into a Bazilion `ToolHandler` whose `invoke` forwards to the daemon-side\n// connection pool over IPC via the injected `McpHost`. The LLM sees the\n// namespaced name (`mcp__<server>__<tool>`); the proxy carries the original\n// `serverId` + `rawName` so the daemon routes the `tools/call` correctly.\n\nimport type { InjectedMcpTool, McpHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\n/** Build proxy tools for every discovered MCP tool. */\nexport function mcpProxyTools(host: McpHost, tools: InjectedMcpTool[]): ToolHandler[] {\n return tools.map((t) => ({\n def: {\n name: t.toolName,\n description: t.description,\n parameters: t.inputSchema,\n },\n invoke: (args) => host.invoke(t.serverId, t.rawName, args),\n }))\n}\n","import type { MemoryBackend } from '../memory/types.ts'\nimport type { ToolHandler } from './types.ts'\n\nexport function memoryTools(memory: MemoryBackend): ToolHandler[] {\n return [\n {\n def: {\n name: 'memory_write',\n description:\n 'Write or update a memory note in the GROUP-SHARED memory. All agents in this group can read what you write. Use it for project knowledge, codebase notes, decisions, and findings — anything other agents in the group should benefit from. For personal notes about yourself (preferences, persona) use `home_write` on IDENTITY.md. For STABLE facts about the human you\\'re working with (their preferences, role, working hours, how they like to be addressed) use `user_md_get` then `user_md_write` — those land in every agent\\'s system prompt directly. Key is a path-like string with a markdown extension, e.g. \"auth-flow.md\" or \"decisions/2026-05-migration.md\".',\n parameters: {\n type: 'object',\n properties: {\n key: { type: 'string', description: 'memory key (relative path)' },\n content: { type: 'string', description: 'note content (plain text or markdown)' },\n },\n required: ['key', 'content'],\n },\n },\n async invoke(args) {\n const key = String(args.key ?? '')\n const content = String(args.content ?? '')\n if (!key) throw new Error('memory_write: \"key\" is required')\n const entry = await memory.write(key, content)\n return `wrote ${entry.key} (${entry.content.length} bytes)`\n },\n },\n {\n def: {\n name: 'memory_search',\n description:\n 'Search the group-shared memory by substring. Returns matching entry keys with short snippets around the match.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string' },\n limit: { type: 'number', description: 'max results (default 10)' },\n },\n required: ['query'],\n },\n },\n async invoke(args) {\n const query = String(args.query ?? '')\n if (!query) throw new Error('memory_search: \"query\" is required')\n const limit = typeof args.limit === 'number' ? args.limit : 10\n const hits = await memory.search(query, { limit })\n if (hits.length === 0) return 'no matches'\n return hits.map((h) => `${h.key}: ${h.snippet.replaceAll('\\n', ' ')}`).join('\\n')\n },\n },\n {\n def: {\n name: 'memory_read',\n description: 'Read a single entry from the group-shared memory by key.',\n parameters: {\n type: 'object',\n properties: { key: { type: 'string' } },\n required: ['key'],\n },\n },\n async invoke(args) {\n const key = String(args.key ?? '')\n if (!key) throw new Error('memory_read: \"key\" is required')\n const entry = await memory.read(key)\n return entry.content\n },\n },\n {\n def: {\n name: 'memory_list',\n description: 'List every entry in the group-shared memory with its byte size.',\n parameters: { type: 'object', properties: {} },\n },\n async invoke() {\n const all = await memory.list()\n if (all.length === 0) return '(empty)'\n return all.map((e) => `${e.key} (${e.content.length}b)`).join('\\n')\n },\n },\n ]\n}\n","import type { MessagingHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\ninterface MessagePayload {\n text: string\n [key: string]: unknown\n}\n\nfunction decodeText(payload: string): string {\n try {\n const parsed = JSON.parse(payload) as MessagePayload\n if (parsed && typeof parsed.text === 'string') return parsed.text\n } catch {\n // not JSON; return raw payload\n }\n return payload\n}\n\nexport function messagingTools(host: MessagingHost, fromAgentId: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'send_message',\n description:\n 'Send a message to another agent. Use the recipient\\'s agent id (UUID). Use this ONLY for things the recipient needs to ACT on: delegating a task, asking a peer for information you cannot get yourself, escalating a decision. Do NOT use it for status updates or to announce changes to group-shared resources — USER.md and the group memory backend both propagate to every agent in the group automatically on their next turn, so messages like \"I updated USER.md\" or \"I wrote a new memory note\" are pure noise and will trigger an inbox-wake loop on the recipient.',\n parameters: {\n type: 'object',\n properties: {\n to: { type: 'string', description: 'Recipient agent id' },\n text: { type: 'string', description: 'Message text' },\n reply_to: {\n type: 'string',\n description: 'Optional: id of the message you are replying to',\n },\n },\n required: ['to', 'text'],\n },\n },\n async invoke(args) {\n const to = String(args.to ?? '')\n const text = String(args.text ?? '')\n if (!to) throw new Error('send_message: \"to\" is required')\n if (!text) throw new Error('send_message: \"text\" is required')\n if (!(await host.agentExists(to))) {\n throw new Error(`send_message: agent not found: ${to}`)\n }\n const replyTo = typeof args.reply_to === 'string' ? args.reply_to : null\n const { messageId } = await host.sendMessage({\n from: fromAgentId,\n to,\n payload: JSON.stringify({ text }),\n replyTo,\n })\n return `sent message ${messageId}`\n },\n },\n {\n def: {\n name: 'read_inbox',\n description: 'Read messages addressed to you. Marks unread messages as read by default.',\n parameters: {\n type: 'object',\n properties: {\n include_read: {\n type: 'boolean',\n description: 'Also include already-read messages (default false)',\n },\n },\n },\n },\n async invoke(args) {\n const includeRead = args.include_read === true\n const messages = await host.listInbox(fromAgentId, { unreadOnly: !includeRead })\n if (messages.length === 0) return '(no messages)'\n const lines: string[] = []\n for (const m of messages) {\n lines.push(`from ${m.fromAgentId} [${m.id}]: ${decodeText(m.payload)}`)\n if (!m.readAt) await host.markRead(m.id)\n }\n return lines.join('\\n')\n },\n },\n {\n def: {\n name: 'wait_for_reply',\n description:\n 'Block until a reply to a message you sent arrives, or until the timeout expires.',\n parameters: {\n type: 'object',\n properties: {\n message_id: {\n type: 'string',\n description: 'id of the message you sent',\n },\n timeout_ms: {\n type: 'number',\n description: 'max wait in milliseconds (default 30000)',\n },\n poll_ms: {\n type: 'number',\n description: 'poll interval in milliseconds (default 200)',\n },\n },\n required: ['message_id'],\n },\n },\n async invoke(args) {\n const messageId = String(args.message_id ?? '')\n if (!messageId) throw new Error('wait_for_reply: \"message_id\" is required')\n const timeout = typeof args.timeout_ms === 'number' ? args.timeout_ms : 30000\n const poll = typeof args.poll_ms === 'number' ? args.poll_ms : 200\n const start = Date.now()\n while (Date.now() - start < timeout) {\n const replies = await host.findReplies(fromAgentId, messageId)\n if (replies.length > 0) {\n const r = replies[0]\n if (r) {\n if (!r.readAt) await host.markRead(r.id)\n return `reply from ${r.fromAgentId} [${r.id}]: ${decodeText(r.payload)}`\n }\n }\n await new Promise((r) => setTimeout(r, poll))\n }\n return `no reply within ${timeout}ms`\n },\n },\n ]\n}\n","// Group-shared USER.md read/write surface for agents.\n//\n// USER.md is inlined into every agent's system prompt every turn (capped at\n// 12 KB on the daemon-side host). Agents previously could only read it; now\n// they can also update it via read-modify-write so they can correct stale\n// facts, not just stack new ones. Concurrency between multiple agents in\n// the same group is handled via optimistic etag checks — see\n// `lib/user-md-host.ts` for the full rationale.\n//\n// Two tools:\n// - `user_md_get` — returns current content + an etag.\n// - `user_md_write` — replaces content; requires `if_match` to equal the\n// most recent etag, else returns a conflict.\n//\n// Project knowledge (codebase notes, decisions) still goes to `memory_write`.\n// Personal notes about the agent itself go to `home_write` on IDENTITY.md.\n\nimport type { UserMdHost } from '../worker/ipc-protocol.ts'\nimport type { ToolHandler } from './types.ts'\n\nexport function userMdTools(host: UserMdHost, groupId: string): ToolHandler[] {\n return [\n {\n def: {\n name: 'user_md_get',\n description:\n 'Read the group-shared USER.md (facts every agent in the group knows about the human). Returns the current content followed by an `etag:` line — you MUST pass that etag back as `if_match` on the next `user_md_write` so the daemon can detect concurrent edits by other agents in the group. Always call this immediately before any `user_md_write`.',\n parameters: {\n type: 'object',\n properties: {},\n },\n },\n async invoke() {\n const { content, etag } = await host.get(groupId)\n const body = content.length > 0 ? content : '(USER.md is empty)'\n return `${body}\\n\\n---\\netag: ${etag}`\n },\n },\n {\n def: {\n name: 'user_md_write',\n description:\n 'Replace the group-shared USER.md with new content. Use this for STABLE user-specific facts (preferences, role, working hours, how the human likes to be addressed). MANDATORY workflow: (1) call `user_md_get` first, (2) integrate your change into the full content preserving everything unrelated, (3) call `user_md_write` with the merged content and the etag you got from `user_md_get`. If another agent in the group wrote to USER.md between your get and write, this returns an etag-mismatch error — just call `user_md_get` again and retry the merge. The full result must fit under 12 KB. For project knowledge use `memory_write`; for notes about yourself use `home_write` on IDENTITY.md.',\n parameters: {\n type: 'object',\n properties: {\n content: {\n type: 'string',\n description:\n 'Full new contents of USER.md (this is a complete replacement, NOT an append). Include everything you want to keep.',\n },\n if_match: {\n type: 'string',\n description:\n 'The etag returned by your most recent `user_md_get`. The write fails if USER.md changed in the meantime.',\n },\n },\n required: ['content', 'if_match'],\n },\n },\n async invoke(args) {\n const content = String(args.content ?? '')\n const ifMatch = String(args.if_match ?? '')\n if (!ifMatch) {\n throw new Error(\n 'user_md_write: \"if_match\" is required — call user_md_get first to obtain the current etag.',\n )\n }\n const { etag, totalBytes } = await host.write(groupId, content, ifMatch)\n return `wrote USER.md (${totalBytes} bytes, new etag: ${etag})`\n },\n },\n ]\n}\n","import { fetch as undiciFetch } from 'undici'\nimport type { ToolHandler } from './types.ts'\nimport {\n type ExtractMode,\n type ExtractResult,\n extractReadable,\n markdownToPlain,\n} from './web-extract.ts'\nimport { guardedFetch, SsrFBlockedError } from './web-ssrf.ts'\n\nconst DEFAULT_USER_AGENT =\n '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'\nconst DEFAULT_CACHE_TTL_MS = 15 * 60_000\nconst DEFAULT_CACHE_MAX = 100\nconst DEFAULT_MAX_LENGTH = 20_000\nconst DEFAULT_TIMEOUT_MS = 30_000\n// Raw body cap applied before parsing. Multi-megabyte HTML (Mintlify/Next.js\n// docs sites, ad-heavy pages) can stall or OOM Readability+linkedom's\n// synchronous DOM walk. 3 MB comfortably covers real articles; anything\n// larger gets truncated and the caller sees a note in the output.\nconst DEFAULT_MAX_BODY_BYTES = 3 * 1024 * 1024\n// If the primary Readability path returns fewer than this many characters\n// from a 2xx HTML response, and FIRECRAWL_API_KEY is configured, retry the\n// fetch via Firecrawl which renders JS server-side. Threshold is heuristic:\n// real articles routinely exceed 200 chars; pages that bottom out below it\n// are almost always JS-shell pages where extraction collapsed.\nconst FIRECRAWL_FALLBACK_THRESHOLD = 200\nconst FIRECRAWL_DEFAULT_URL = 'https://api.firecrawl.dev'\n\ninterface SearchResult {\n title: string\n url: string\n snippet: string\n}\n\nfunction stripHtml(s: string): string {\n return s\n .replace(/<[^>]*>/g, '')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&nbsp;/g, ' ')\n .trim()\n}\n\n// --- search backends ---\n\nasync function braveSearch(\n query: string,\n limit: number,\n apiKey: string,\n fetchFn: typeof fetch,\n): Promise<SearchResult[]> {\n const params = new URLSearchParams({ q: query, count: String(limit) })\n const res = await fetchFn(`https://api.search.brave.com/res/v1/web/search?${params}`, {\n headers: {\n accept: 'application/json',\n 'accept-encoding': 'gzip',\n 'x-subscription-token': apiKey,\n },\n })\n if (!res.ok) throw new Error(`Brave Search: ${res.status} ${await res.text()}`)\n const data = (await res.json()) as {\n web?: { results?: { title?: string; url?: string; description?: string }[] }\n }\n return (data.web?.results ?? []).slice(0, limit).map((r) => ({\n title: r.title ?? '',\n url: r.url ?? '',\n snippet: r.description ? stripHtml(r.description) : '',\n }))\n}\n\nasync function searxngSearch(\n query: string,\n limit: number,\n baseURL: string,\n fetchFn: typeof fetch,\n): Promise<SearchResult[]> {\n const params = new URLSearchParams({ q: query, format: 'json' })\n const res = await fetchFn(`${baseURL}/search?${params}`, {\n headers: { accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`SearXNG: ${res.status} ${await res.text()}`)\n const data = (await res.json()) as {\n results?: { title?: string; url?: string; content?: string }[]\n }\n return (data.results ?? []).slice(0, limit).map((r) => ({\n title: r.title ?? '',\n url: r.url ?? '',\n snippet: r.content ?? '',\n }))\n}\n\n// --- error formatting ---\n\n/**\n * Flatten an Error and its `cause` chain into a single readable string.\n * undici surfaces network failures as `TypeError: fetch failed` with the\n * real reason (UND_ERR_*, ECONNRESET, certificate errors, …) stashed in\n * `err.cause`; without unwrapping it the agent only ever sees \"fetch\n * failed\" and has no path to diagnose or work around the problem.\n */\nfunction describeError(err: unknown): string {\n if (!(err instanceof Error)) return String(err)\n const parts: string[] = []\n const seen = new Set<unknown>()\n let cur: unknown = err\n while (cur instanceof Error && !seen.has(cur)) {\n seen.add(cur)\n const code = (cur as { code?: string }).code\n parts.push(code ? `[${code}] ${cur.message}` : cur.message)\n cur = (cur as { cause?: unknown }).cause\n }\n return parts.join(' — cause: ')\n}\n\n// --- bounded body reader ---\n\n/**\n * Stream `res.body` and accumulate up to `maxBytes`, then truncate. Returns\n * the decoded text (using the charset from content-type, falling back to\n * UTF-8) and a flag the caller can surface to the agent so it knows the\n * page was larger than the cap.\n */\nasync function readBodyCapped(\n res: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const ct = res.headers.get('content-type') ?? ''\n const charset = /charset=([^;]+)/i.exec(ct)?.[1]?.trim().toLowerCase() || 'utf-8'\n const decoder = new TextDecoder(charset, { fatal: false })\n const reader = res.body?.getReader()\n if (!reader) {\n const text = await res.text()\n if (text.length > maxBytes) return { text: text.slice(0, maxBytes), truncated: true }\n return { text, truncated: false }\n }\n const chunks: Uint8Array[] = []\n let total = 0\n let truncated = false\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n if (total + value.byteLength > maxBytes) {\n const keep = maxBytes - total\n if (keep > 0) chunks.push(value.subarray(0, keep))\n truncated = true\n try {\n await reader.cancel()\n } catch {\n // body already settled\n }\n break\n }\n chunks.push(value)\n total += value.byteLength\n }\n const sum = chunks.reduce((s, c) => s + c.byteLength, 0)\n const merged = new Uint8Array(sum)\n let off = 0\n for (const c of chunks) {\n merged.set(c, off)\n off += c.byteLength\n }\n return { text: decoder.decode(merged), truncated }\n}\n\n// --- Firecrawl fallback ---\n\ninterface FirecrawlResponse {\n success?: boolean\n data?: {\n markdown?: string\n metadata?: { title?: string }\n }\n error?: string\n}\n\n/**\n * Last-resort HTML rendering via Firecrawl's `/v1/scrape` endpoint. Used\n * when the primary Readability path returns near-empty content from a 2xx\n * HTML response (JS-only shells, anti-bot walls, login redirects). Returns\n * `null` when Firecrawl is not configured or the call fails so the caller\n * falls back to the primary extraction unchanged — Firecrawl is best-effort,\n * never an error source.\n */\nasync function firecrawlScrape(\n url: string,\n mode: ExtractMode,\n env: NodeJS.ProcessEnv,\n fetchFn: typeof fetch,\n timeoutMs: number,\n): Promise<ExtractResult | null> {\n const apiKey = env.FIRECRAWL_API_KEY\n if (!apiKey) return null\n const base = (env.FIRECRAWL_URL ?? FIRECRAWL_DEFAULT_URL).replace(/\\/$/, '')\n const ac = new AbortController()\n const t = setTimeout(() => ac.abort(new Error('firecrawl timeout')), timeoutMs)\n try {\n const res = await fetchFn(`${base}/v1/scrape`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${apiKey}`,\n accept: 'application/json',\n },\n body: JSON.stringify({\n url,\n formats: ['markdown'],\n onlyMainContent: true,\n }),\n signal: ac.signal,\n })\n if (!res.ok) return null\n const body = (await res.json()) as FirecrawlResponse\n if (!body.success || !body.data?.markdown) return null\n const md = body.data.markdown\n const title = body.data.metadata?.title\n const text = mode === 'text' ? markdownToPlain(md) : md\n return title ? { text, title } : { text }\n } catch {\n return null\n } finally {\n clearTimeout(t)\n }\n}\n\n// --- per-URL cache ---\n\ninterface CacheEntry {\n value: ExtractResult\n expiresAt: number\n}\n\nfunction cacheGet(cache: Map<string, CacheEntry>, key: string): ExtractResult | null {\n const entry = cache.get(key)\n if (!entry) return null\n if (entry.expiresAt < Date.now()) {\n cache.delete(key)\n return null\n }\n // Refresh LRU position\n cache.delete(key)\n cache.set(key, entry)\n return entry.value\n}\n\nfunction cacheSet(\n cache: Map<string, CacheEntry>,\n key: string,\n value: ExtractResult,\n ttlMs: number,\n maxEntries: number,\n): void {\n cache.set(key, { value, expiresAt: Date.now() + ttlMs })\n while (cache.size > maxEntries) {\n const oldest = cache.keys().next().value\n if (!oldest) break\n cache.delete(oldest)\n }\n}\n\n// --- tool factory ---\n\nexport interface WebToolsOpts {\n fetchImpl?: typeof fetch\n env?: NodeJS.ProcessEnv\n /** Disable SSRF checks (tests against 127.0.0.1 mock servers). Default false. */\n allowPrivate?: boolean\n /** Cache TTL in ms. Default 15 min. */\n cacheTtlMs?: number\n /** Max cache entries. Default 100. */\n cacheMax?: number\n /** Fetch timeout in ms. Default 30s. */\n timeoutMs?: number\n /** Cap on raw response body bytes before parsing. Default 3 MB. */\n maxBodyBytes?: number\n /** Disable the Firecrawl fallback even when FIRECRAWL_API_KEY is set. */\n firecrawlDisabled?: boolean\n}\n\n/**\n * web_search backends (tried in order):\n * 1. Brave Search (env: BRAVE_API_KEY) — free tier, 2000 req/month\n * 2. SearXNG (env: SEARXNG_URL) — self-hosted, unlimited\n * 3. Error with setup instructions if neither is configured\n *\n * web_fetch: guarded fetch (SSRF-blocked private IPs) + Readability + markdown,\n * with per-URL in-memory cache (15 min TTL).\n */\nexport function webTools(opts?: WebToolsOpts): ToolHandler[] {\n // Default to undici 8's fetch so search-backend HTTP shares the same\n // stack as guardedFetch (and stays out of Node 24's bundled undici 7).\n const fetchFn = opts?.fetchImpl ?? (undiciFetch as unknown as typeof fetch)\n const env = opts?.env ?? process.env\n const allowPrivate = opts?.allowPrivate ?? false\n const cacheTtlMs = opts?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS\n const cacheMax = opts?.cacheMax ?? DEFAULT_CACHE_MAX\n const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const maxBodyBytes = opts?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES\n const firecrawlDisabled = opts?.firecrawlDisabled ?? false\n const cache = new Map<string, CacheEntry>()\n\n return [\n {\n def: {\n name: 'web_search',\n description:\n 'Search the internet. Returns a list of titles, URLs, and snippets. Requires BRAVE_API_KEY or SEARXNG_URL to be configured.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n limit: { type: 'number', description: 'Max results to return (default 5)' },\n },\n required: ['query'],\n },\n },\n async invoke(args) {\n const query = String(args.query ?? '')\n if (!query) throw new Error('web_search: query is required')\n const limit = typeof args.limit === 'number' ? args.limit : 5\n\n const braveKey = env.BRAVE_API_KEY\n if (braveKey) {\n const results = await braveSearch(query, limit, braveKey, fetchFn)\n return results.length === 0 ? 'no results' : formatResults(results)\n }\n\n const searxngUrl = env.SEARXNG_URL\n if (searxngUrl) {\n const results = await searxngSearch(query, limit, searxngUrl, fetchFn)\n return results.length === 0 ? 'no results' : formatResults(results)\n }\n\n throw new Error(\n 'web_search: no search backend configured. Set BRAVE_API_KEY (free at https://brave.com/search/api/) or SEARXNG_URL.',\n )\n },\n },\n {\n def: {\n name: 'web_fetch',\n description:\n 'Fetch a URL and return its readable content. HTML is extracted via Readability and converted to markdown. When the primary extraction returns near-empty content from a 2xx HTML page (typical of JS-only shells), the tool automatically retries via Firecrawl if FIRECRAWL_API_KEY is configured. Results are cached for 15 minutes.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The URL to fetch (http or https)' },\n max_length: {\n type: 'number',\n description: 'Max characters to return (default 20000)',\n },\n extract_mode: {\n type: 'string',\n enum: ['markdown', 'text'],\n description: 'Output format for HTML pages. Default \"markdown\".',\n },\n },\n required: ['url'],\n },\n },\n async invoke(args) {\n const url = String(args.url ?? '')\n if (!url) throw new Error('web_fetch: url is required')\n const maxLen = typeof args.max_length === 'number' ? args.max_length : DEFAULT_MAX_LENGTH\n const mode: ExtractMode = args.extract_mode === 'text' ? 'text' : 'markdown'\n const cacheKey = `${mode}|${url}`\n\n const cached = cacheGet(cache, cacheKey)\n if (cached) return formatOutput(cached, maxLen)\n\n let result: GuardedFetchResultShape | null = null\n try {\n result = await guardedFetch({\n url,\n fetchImpl: opts?.fetchImpl,\n allowPrivate,\n timeoutMs,\n init: {\n headers: {\n 'user-agent': DEFAULT_USER_AGENT,\n accept: 'text/html,application/xhtml+xml,application/json,text/plain,*/*',\n 'accept-language': 'en-US,en;q=0.9',\n },\n },\n })\n if (!result.response.ok) {\n throw new Error(`${result.response.status} ${result.response.statusText}`)\n }\n const ct = result.response.headers.get('content-type') ?? ''\n const isHtml = ct.includes('text/html') || ct.includes('xhtml')\n const { text: body, truncated } = await readBodyCapped(result.response, maxBodyBytes)\n let extracted: ExtractResult\n if (isHtml) {\n extracted = extractReadable(body, result.finalUrl, mode)\n } else if (ct.includes('application/json')) {\n try {\n extracted = { text: JSON.stringify(JSON.parse(body), null, 2) }\n } catch {\n extracted = { text: body }\n }\n } else {\n extracted = { text: body }\n }\n // Firecrawl fallback: only meaningful for HTML pages where the\n // primary extractor collapsed. Skip JSON / plain-text bodies and\n // skip when extraction already produced something substantial.\n if (\n isHtml &&\n !firecrawlDisabled &&\n extracted.text.length < FIRECRAWL_FALLBACK_THRESHOLD\n ) {\n const rescued = await firecrawlScrape(result.finalUrl, mode, env, fetchFn, timeoutMs)\n if (rescued) {\n extracted = {\n ...rescued,\n text: `${rescued.text}\\n\\n[content rendered via Firecrawl fallback — primary extraction returned ${extracted.text.length} chars]`,\n }\n }\n }\n if (truncated) {\n extracted = {\n ...extracted,\n text: `${extracted.text}\\n\\n[raw body truncated at ${maxBodyBytes} bytes before extraction — page exceeded the size cap]`,\n }\n }\n cacheSet(cache, cacheKey, extracted, cacheTtlMs, cacheMax)\n return formatOutput(extracted, maxLen)\n } catch (err) {\n if (err instanceof SsrFBlockedError) throw new Error(`web_fetch: ${err.message}`)\n throw new Error(`web_fetch: ${describeError(err)}`)\n } finally {\n if (result) await result.release()\n }\n },\n },\n ]\n}\n\ntype GuardedFetchResultShape = Awaited<ReturnType<typeof guardedFetch>>\n\nfunction formatOutput(r: ExtractResult, maxLen: number): string {\n const body = r.title ? `# ${r.title}\\n\\n${r.text}` : r.text\n if (body.length > maxLen) return `${body.slice(0, maxLen)}\\n\\n[truncated at ${maxLen} chars]`\n return body\n}\n\nfunction formatResults(results: SearchResult[]): string {\n return results.map((r, i) => `${i + 1}. ${r.title}\\n ${r.url}\\n ${r.snippet}`).join('\\n\\n')\n}\n","import { Readability } from '@mozilla/readability'\nimport { parseHTML } from 'linkedom'\n\nexport type ExtractMode = 'markdown' | 'text'\n\nexport interface ExtractResult {\n text: string\n title?: string\n}\n\nfunction decodeEntities(value: string): string {\n return value\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\")\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))\n .replace(/&#(\\d+);/gi, (_, dec) => String.fromCharCode(Number.parseInt(dec, 10)))\n}\n\nfunction stripTags(value: string): string {\n return decodeEntities(value.replace(/<[^>]+>/g, ''))\n}\n\nfunction normalizeWhitespace(value: string): string {\n return value\n .replace(/\\r/g, '')\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .replace(/[ \\t]{2,}/g, ' ')\n .trim()\n}\n\nfunction htmlToMarkdown(html: string): { text: string; title?: string } {\n const titleMatch = html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)\n const title = titleMatch ? normalizeWhitespace(stripTags(titleMatch[1] ?? '')) : undefined\n let text = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '')\n .replace(/<nav[\\s\\S]*?<\\/nav>/gi, '')\n .replace(/<header[\\s\\S]*?<\\/header>/gi, '')\n .replace(/<footer[\\s\\S]*?<\\/footer>/gi, '')\n text = text.replace(/<a\\s+[^>]*href=[\"']([^\"']+)[\"'][^>]*>([\\s\\S]*?)<\\/a>/gi, (_, href, body) => {\n const label = normalizeWhitespace(stripTags(body))\n return label ? `[${label}](${href})` : href\n })\n text = text.replace(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_, level, body) => {\n const n = Math.max(1, Math.min(6, Number.parseInt(level, 10)))\n return `\\n${'#'.repeat(n)} ${normalizeWhitespace(stripTags(body))}\\n`\n })\n text = text.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, (_, body) => {\n const label = normalizeWhitespace(stripTags(body))\n return label ? `\\n- ${label}` : ''\n })\n text = text\n .replace(/<(br|hr)\\s*\\/?>/gi, '\\n')\n .replace(/<\\/(p|div|section|article|tr|ul|ol|table|blockquote)>/gi, '\\n')\n text = stripTags(text)\n return { text: normalizeWhitespace(text), title }\n}\n\nexport function markdownToPlain(md: string): string {\n let t = md\n t = t.replace(/!\\[[^\\]]*]\\([^)]+\\)/g, '')\n t = t.replace(/\\[([^\\]]+)]\\([^)]+\\)/g, '$1')\n t = t.replace(/```[\\s\\S]*?```/g, (block) =>\n block.replace(/```[^\\n]*\\n?/g, '').replace(/```/g, ''),\n )\n t = t.replace(/`([^`]+)`/g, '$1')\n t = t.replace(/^#{1,6}\\s+/gm, '')\n t = t.replace(/^\\s*[-*+]\\s+/gm, '')\n t = t.replace(/^\\s*\\d+\\.\\s+/gm, '')\n return normalizeWhitespace(t)\n}\n\n/**\n * Extract readable content from HTML using Readability, with a regex-based\n * markdown fallback when Readability can't identify an article.\n */\nexport function extractReadable(html: string, url: string, mode: ExtractMode): ExtractResult {\n const fallback = (): ExtractResult => {\n const r = htmlToMarkdown(html)\n return mode === 'text' ? { text: markdownToPlain(r.text), title: r.title } : r\n }\n try {\n const { document } = parseHTML(html)\n try {\n ;(document as unknown as { baseURI?: string }).baseURI = url\n } catch {\n // best-effort\n }\n type ReadabilityArg = ConstructorParameters<typeof Readability>[0]\n const parsed = new Readability(document as unknown as ReadabilityArg, {\n charThreshold: 0,\n }).parse()\n if (!parsed?.content) return fallback()\n const title = parsed.title || undefined\n if (mode === 'text') {\n const text = normalizeWhitespace(parsed.textContent ?? '')\n return text ? { text, title } : fallback()\n }\n const rendered = htmlToMarkdown(parsed.content)\n return { text: rendered.text, title: title ?? rendered.title }\n } catch {\n return fallback()\n }\n}\n","import { lookup as dnsLookupCb, type LookupAddress } from 'node:dns'\nimport { lookup as dnsLookup } from 'node:dns/promises'\nimport { Agent, type Dispatcher, fetch as undiciFetch } from 'undici'\n\nexport class SsrFBlockedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SsrFBlockedError'\n }\n}\n\nconst BLOCKED_HOSTNAMES = new Set(['localhost', 'metadata.google.internal'])\nconst PRIVATE_IPV6_PREFIXES = ['fe80:', 'fec0:', 'fc', 'fd']\n\nfunction normalizeHostname(hostname: string): string {\n let h = hostname.trim().toLowerCase().replace(/\\.$/, '')\n if (h.startsWith('[') && h.endsWith(']')) h = h.slice(1, -1)\n return h\n}\n\nfunction parseIpv4(address: string): number[] | null {\n const parts = address.split('.')\n if (parts.length !== 4) return null\n const nums = parts.map((p) => Number.parseInt(p, 10))\n if (nums.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return null\n return nums\n}\n\nfunction isPrivateIpv4(parts: number[]): boolean {\n const [a, b] = parts as [number, number, number, number]\n if (a === 0 || a === 10 || a === 127) return true\n if (a === 169 && b === 254) return true\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 192 && b === 168) return true\n if (a === 100 && b >= 64 && b <= 127) return true\n return false\n}\n\nexport function isPrivateIpAddress(address: string): boolean {\n let norm = address.trim().toLowerCase()\n if (norm.startsWith('[') && norm.endsWith(']')) norm = norm.slice(1, -1)\n if (!norm) return false\n if (norm.startsWith('::ffff:')) {\n const mapped = norm.slice('::ffff:'.length)\n const ipv4 = parseIpv4(mapped)\n if (ipv4) return isPrivateIpv4(ipv4)\n }\n if (norm.includes(':')) {\n if (norm === '::' || norm === '::1') return true\n return PRIVATE_IPV6_PREFIXES.some((p) => norm.startsWith(p))\n }\n const ipv4 = parseIpv4(norm)\n return ipv4 ? isPrivateIpv4(ipv4) : false\n}\n\nexport function isBlockedHostname(hostname: string): boolean {\n const h = normalizeHostname(hostname)\n if (!h) return false\n if (BLOCKED_HOSTNAMES.has(h)) return true\n return h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.internal')\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address: string | LookupAddress[],\n family?: number,\n) => void\n\nfunction createPinnedLookup(hostname: string, addresses: string[]): typeof dnsLookupCb {\n const normalized = normalizeHostname(hostname)\n const records = addresses.map((address) => ({\n address,\n family: (address.includes(':') ? 6 : 4) as 4 | 6,\n }))\n let index = 0\n return ((host: string, options?: unknown, callback?: unknown) => {\n const cb: LookupCallback =\n typeof options === 'function' ? (options as LookupCallback) : (callback as LookupCallback)\n if (!cb) return\n if (normalizeHostname(host) !== normalized) {\n if (typeof options === 'function' || options === undefined) {\n return (dnsLookupCb as unknown as (h: string, cb: LookupCallback) => void)(host, cb)\n }\n return (dnsLookupCb as unknown as (h: string, o: unknown, cb: LookupCallback) => void)(\n host,\n options,\n cb,\n )\n }\n const opts =\n typeof options === 'object' && options !== null\n ? (options as { all?: boolean; family?: number })\n : {}\n const family = typeof options === 'number' ? options : (opts.family ?? 0)\n const candidates =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records\n const usable = candidates.length > 0 ? candidates : records\n if (opts.all) {\n cb(null, usable as LookupAddress[])\n return\n }\n const chosen = usable[index % usable.length]\n if (!chosen) return\n index += 1\n cb(null, chosen.address, chosen.family)\n }) as typeof dnsLookupCb\n}\n\nasync function resolveAndCheck(hostname: string): Promise<string[]> {\n const norm = normalizeHostname(hostname)\n if (!norm) throw new SsrFBlockedError('Invalid hostname')\n if (isBlockedHostname(norm)) throw new SsrFBlockedError(`Blocked hostname: ${hostname}`)\n if (isPrivateIpAddress(norm)) throw new SsrFBlockedError('Blocked: private IP literal')\n const results = await dnsLookup(norm, { all: true })\n if (results.length === 0) throw new SsrFBlockedError(`Cannot resolve: ${hostname}`)\n for (const r of results) {\n if (isPrivateIpAddress(r.address)) throw new SsrFBlockedError('Blocked: resolves to private IP')\n }\n return Array.from(new Set(results.map((r) => r.address)))\n}\n\ntype FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>\n\nexport interface GuardedFetchOptions {\n url: string\n fetchImpl?: FetchLike\n init?: RequestInit\n maxRedirects?: number\n timeoutMs?: number\n signal?: AbortSignal\n /** If true, skip all SSRF checks (for tests hitting 127.0.0.1 mocks). */\n allowPrivate?: boolean\n}\n\nexport interface GuardedFetchResult {\n response: Response\n finalUrl: string\n release: () => Promise<void>\n}\n\nfunction isRedirectStatus(s: number): boolean {\n return s === 301 || s === 302 || s === 303 || s === 307 || s === 308\n}\n\nasync function closeDispatcher(d: Dispatcher | null): Promise<void> {\n if (!d) return\n try {\n await d.close()\n } catch {\n // ignore\n }\n}\n\nexport async function guardedFetch(opts: GuardedFetchOptions): Promise<GuardedFetchResult> {\n // Use undici's own fetch (not globalThis.fetch) so the `dispatcher` Agent\n // we attach below is the same undici major version as the fetch impl\n // consuming it. Node 24 ships undici 7.x as its built-in fetch; mixing\n // a standalone undici 8.x Agent with the 7.x dispatcher fails with\n // `UND_ERR_INVALID_ARG: invalid onRequestStart method` because the\n // diagnostics-channel handler signatures changed between majors.\n const fetcher: FetchLike = opts.fetchImpl ?? (undiciFetch as unknown as FetchLike)\n const maxRedirects = opts.maxRedirects ?? 3\n const abortController = new AbortController()\n const timeoutId = opts.timeoutMs\n ? setTimeout(() => abortController.abort(new Error('timeout')), opts.timeoutMs)\n : null\n if (opts.signal) {\n if (opts.signal.aborted) abortController.abort(opts.signal.reason)\n else\n opts.signal.addEventListener('abort', () => abortController.abort(opts.signal?.reason), {\n once: true,\n })\n }\n\n let current = opts.url\n const visited = new Set<string>()\n let redirects = 0\n let dispatcher: Dispatcher | null = null\n\n const release = async (): Promise<void> => {\n if (timeoutId) clearTimeout(timeoutId)\n await closeDispatcher(dispatcher)\n dispatcher = null\n }\n\n while (true) {\n let parsed: URL\n try {\n parsed = new URL(current)\n } catch {\n await release()\n throw new Error('Invalid URL')\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n await release()\n throw new Error('Invalid URL: must be http or https')\n }\n\n // Pin DNS only when we control the dispatcher (native fetch path).\n const canPin = !opts.fetchImpl\n if (!opts.allowPrivate) {\n const addrs = await resolveAndCheck(parsed.hostname)\n await closeDispatcher(dispatcher)\n dispatcher = canPin\n ? new Agent({ connect: { lookup: createPinnedLookup(parsed.hostname, addrs) } })\n : null\n }\n\n const init: RequestInit = {\n ...(opts.init ?? {}),\n redirect: 'manual',\n signal: abortController.signal,\n }\n // `dispatcher` is a Node fetch extension not reflected in the DOM\n // RequestInit type — and the undici Dispatcher type brand can diverge\n // from the one @types/node bundles. Stamp it on via a cast.\n if (dispatcher) (init as unknown as { dispatcher: Dispatcher }).dispatcher = dispatcher\n\n let res: Response\n try {\n res = await fetcher(parsed.toString(), init)\n } catch (err) {\n await release()\n throw err\n }\n\n if (isRedirectStatus(res.status)) {\n const loc = res.headers.get('location')\n if (!loc) {\n await release()\n throw new Error(`Redirect ${res.status} missing Location header`)\n }\n redirects += 1\n if (redirects > maxRedirects) {\n await release()\n throw new Error(`Too many redirects (> ${maxRedirects})`)\n }\n const next = new URL(loc, parsed).toString()\n if (visited.has(next)) {\n await release()\n throw new Error('Redirect loop')\n }\n visited.add(next)\n void res.body?.cancel()\n current = next\n continue\n }\n\n return { response: res, finalUrl: parsed.toString(), release }\n }\n}\n"],"mappings":"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","names":["randomUUID","join","mkdirSync","writeFileSync","join","existsSync","readFileSync","join","existsSync","readFileSync","writeFileSync","join","existsSync","mkdirSync","join","existsSync","join","readdirSync","readFileSync","join","join","existsSync","rmSync","writeFileSync","join","readdirSync","rmSync","rmSync","randomUUID","randomUUID","randomUUID","randomBytes","randomUUID","existsSync","readFileSync","existsSync","readdirSync","rmSync","statSync","join","resolve","readFileSync","existsSync","mkdirSync","readdirSync","readFileSync","rmSync","statSync","writeFileSync","dirname","join","existsSync","mkdirSync","readdirSync","statSync","basename","join","existsSync","readFileSync","join","Type","existsSync","rmSync","join","readFileSync","statSync","basename","resolve","readdirSync","readFileSync","statSync","writeFileSync","join","undiciFetch","ipv4","undiciFetch","Type","existsSync","mkdirSync","join","existsSync","readdirSync","join","statSync","randomUUID","resolve","join"]}