diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aa0b410bcc..e741444b841 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,9 +74,9 @@ jobs: - runtime: node task: protocol command: pnpm protocol:check - - runtime: bun - task: lint - command: bunx biome check src + - runtime: node + task: format + command: pnpm format - runtime: bun task: test command: bunx vitest run diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc new file mode 100644 index 00000000000..7d80600ac94 --- /dev/null +++ b/.oxfmtrc.jsonc @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "indentWidth": 2, + "printWidth": 100 +} diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc new file mode 100644 index 00000000000..115aa1a8113 --- /dev/null +++ b/.oxlintrc.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "https://json.schemastore.org/oxlintrc", + "extends": ["recommended"] +} diff --git a/AGENTS.md b/AGENTS.md index 55368592f6c..b77adf83613 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,12 +25,12 @@ - Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`. - Node remains supported for running built output (`dist/*`) and production installs. - Type-check/build: `pnpm build` (tsc) -- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format) +- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt) - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` ## Coding Style & Naming Conventions - Language: TypeScript (ESM). Prefer strict typing; avoid `any`. -- Formatting/linting via Biome; run `pnpm lint` before commits. +- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits. - Add brief code comments for tricky or non-obvious logic. - Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`. - Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability. diff --git a/biome.json b/biome.json deleted file mode 100644 index 0409581af04..00000000000 --- a/biome.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/biome.json", - "formatter": { - "enabled": true, - "indentWidth": 2, - "indentStyle": "space" - }, - "files": { - "includes": ["src/**/*.ts", "test/**/*.ts"] - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - } -} diff --git a/package.json b/package.json index b9363b9fb3d..6ec3c86c4e8 100644 --- a/package.json +++ b/package.json @@ -87,14 +87,14 @@ "mac:restart": "bash scripts/restart-mac.sh", "mac:package": "bash scripts/package-mac-app.sh", "mac:open": "open dist/Clawdbot.app", - "lint": "biome check src test && oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js", + "lint": "oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js", "lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)", "lint:all": "pnpm lint && pnpm lint:swift", - "lint:fix": "biome check --write --unsafe src && biome format --write src", - "format": "biome format src", + "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js", + "format": "oxfmt --check src test", "format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources", "format:all": "pnpm format && pnpm format:swift", - "format:fix": "biome format src --write", + "format:fix": "oxfmt --write src test", "test": "vitest run", "test:watch": "vitest", "test:ui": "pnpm --dir ui test", @@ -176,7 +176,6 @@ "zod": "^4.3.5" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", "@grammyjs/types": "^3.23.0", "@lit-labs/signals": "^0.2.0", "@lit/context": "^1.1.6", @@ -194,7 +193,8 @@ "lit": "^3.3.2", "lucide": "^0.562.0", "ollama": "^0.6.3", - "oxlint": "^1.38.0", + "oxfmt": "0.24.0", + "oxlint": "^1.39.0", "oxlint-tsgolint": "^0.11.0", "quicktype-core": "^23.2.6", "rolldown": "1.0.0-beta.59", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1161e783e7f..7266472d577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,9 +149,6 @@ importers: specifier: ^4.3.5 version: 4.3.5 devDependencies: - '@biomejs/biome': - specifier: ^2.3.11 - version: 2.3.11 '@grammyjs/types': specifier: ^3.23.0 version: 3.23.0 @@ -203,8 +200,11 @@ importers: ollama: specifier: ^0.6.3 version: 0.6.3 + oxfmt: + specifier: 0.24.0 + version: 0.24.0 oxlint: - specifier: ^1.38.0 + specifier: ^1.39.0 version: 1.39.0(oxlint-tsgolint@0.11.0) oxlint-tsgolint: specifier: ^0.11.0 @@ -452,59 +452,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.3.11': - resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.3.11': - resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.3.11': - resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.3.11': - resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-arm64@2.3.11': - resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - - '@biomejs/cli-linux-x64-musl@2.3.11': - resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-linux-x64@2.3.11': - resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - - '@biomejs/cli-win32-arm64@2.3.11': - resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.3.11': - resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@borewit/text-codec@0.2.1': resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} @@ -1245,6 +1192,46 @@ packages: '@oxc-project/types@0.107.0': resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==} + '@oxfmt/darwin-arm64@0.24.0': + resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==} + cpu: [arm64] + os: [darwin] + + '@oxfmt/darwin-x64@0.24.0': + resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==} + cpu: [x64] + os: [darwin] + + '@oxfmt/linux-arm64-gnu@0.24.0': + resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==} + cpu: [arm64] + os: [linux] + + '@oxfmt/linux-arm64-musl@0.24.0': + resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==} + cpu: [arm64] + os: [linux] + + '@oxfmt/linux-x64-gnu@0.24.0': + resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==} + cpu: [x64] + os: [linux] + + '@oxfmt/linux-x64-musl@0.24.0': + resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==} + cpu: [x64] + os: [linux] + + '@oxfmt/win32-arm64@0.24.0': + resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==} + cpu: [arm64] + os: [win32] + + '@oxfmt/win32-x64@0.24.0': + resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==} + cpu: [x64] + os: [win32] + '@oxlint-tsgolint/darwin-arm64@0.11.0': resolution: {integrity: sha512-F67T8dXgYIrgv6wpd52fKQFdmieSOHaxBkscgso64YdtEHrV3s52ASiZGNzw62TKihn9Ox9ek3PYx9XsxIJDUw==} cpu: [arm64] @@ -3324,6 +3311,11 @@ packages: resolution: {integrity: sha512-GJR9XnS8dQ+sAdbhX90RA4WbmEyrso7X9aHMws4MaQ2GRpfEjnOUSZIdOXJQfnIfBoy9oCc7US/MNFCyuJQzjg==} engines: {node: '>=20'} + oxfmt@0.24.0: + resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + oxlint-tsgolint@0.11.0: resolution: {integrity: sha512-fGYb7z/cljC0Rjtbxh7mIe8vtF/M9TShLvniwc2rdcqNG3Z9g3nM01cr2kWRb1DZdbY4/kItvIsrV4uhaMifyQ==} hasBin: true @@ -3858,6 +3850,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@2.0.0: + resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -4641,41 +4637,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.3.11': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.11 - '@biomejs/cli-darwin-x64': 2.3.11 - '@biomejs/cli-linux-arm64': 2.3.11 - '@biomejs/cli-linux-arm64-musl': 2.3.11 - '@biomejs/cli-linux-x64': 2.3.11 - '@biomejs/cli-linux-x64-musl': 2.3.11 - '@biomejs/cli-win32-arm64': 2.3.11 - '@biomejs/cli-win32-x64': 2.3.11 - - '@biomejs/cli-darwin-arm64@2.3.11': - optional: true - - '@biomejs/cli-darwin-x64@2.3.11': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.3.11': - optional: true - - '@biomejs/cli-linux-arm64@2.3.11': - optional: true - - '@biomejs/cli-linux-x64-musl@2.3.11': - optional: true - - '@biomejs/cli-linux-x64@2.3.11': - optional: true - - '@biomejs/cli-win32-arm64@2.3.11': - optional: true - - '@biomejs/cli-win32-x64@2.3.11': - optional: true - '@borewit/text-codec@0.2.1': {} '@buape/carbon@0.0.0-beta-20260110172854(hono@4.11.3)': @@ -5420,6 +5381,30 @@ snapshots: '@oxc-project/types@0.107.0': {} + '@oxfmt/darwin-arm64@0.24.0': + optional: true + + '@oxfmt/darwin-x64@0.24.0': + optional: true + + '@oxfmt/linux-arm64-gnu@0.24.0': + optional: true + + '@oxfmt/linux-arm64-musl@0.24.0': + optional: true + + '@oxfmt/linux-x64-gnu@0.24.0': + optional: true + + '@oxfmt/linux-x64-musl@0.24.0': + optional: true + + '@oxfmt/win32-arm64@0.24.0': + optional: true + + '@oxfmt/win32-x64@0.24.0': + optional: true + '@oxlint-tsgolint/darwin-arm64@0.11.0': optional: true @@ -7653,6 +7638,19 @@ snapshots: osc-progress@0.2.0: {} + oxfmt@0.24.0: + dependencies: + tinypool: 2.0.0 + optionalDependencies: + '@oxfmt/darwin-arm64': 0.24.0 + '@oxfmt/darwin-x64': 0.24.0 + '@oxfmt/linux-arm64-gnu': 0.24.0 + '@oxfmt/linux-arm64-musl': 0.24.0 + '@oxfmt/linux-x64-gnu': 0.24.0 + '@oxfmt/linux-x64-musl': 0.24.0 + '@oxfmt/win32-arm64': 0.24.0 + '@oxfmt/win32-x64': 0.24.0 + oxlint-tsgolint@0.11.0: optionalDependencies: '@oxlint-tsgolint/darwin-arm64': 0.11.0 @@ -8291,6 +8289,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@2.0.0: {} + tinyrainbow@3.0.3: {} to-regex-range@5.0.1: diff --git a/src/agents/agent-paths.ts b/src/agents/agent-paths.ts index 1dd54ea81a4..aaab94690a1 100644 --- a/src/agents/agent-paths.ts +++ b/src/agents/agent-paths.ts @@ -6,15 +6,9 @@ import { resolveUserPath } from "../utils.js"; export function resolveClawdbotAgentDir(): string { const override = - process.env.CLAWDBOT_AGENT_DIR?.trim() || - process.env.PI_CODING_AGENT_DIR?.trim(); + process.env.CLAWDBOT_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim(); if (override) return resolveUserPath(override); - const defaultAgentDir = path.join( - resolveStateDir(), - "agents", - DEFAULT_AGENT_ID, - "agent", - ); + const defaultAgentDir = path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent"); return resolveUserPath(defaultAgentDir); } diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.test.ts index 585c7bca90a..3f8c77c9aea 100644 --- a/src/agents/agent-scope.test.ts +++ b/src/agents/agent-scope.test.ts @@ -72,12 +72,8 @@ describe("resolveAgentConfig", () => { }, }; - expect(resolveAgentModelPrimary(cfg, "linus")).toBe( - "anthropic/claude-opus-4", - ); - expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual([ - "openai/gpt-5.2", - ]); + expect(resolveAgentModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4"); + expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.2"]); // If fallbacks isn't present, we don't override the global fallbacks. const cfgNoOverride: ClawdbotConfig = { @@ -92,9 +88,7 @@ describe("resolveAgentConfig", () => { ], }, }; - expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe( - undefined, - ); + expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(undefined); // Explicit empty list disables global fallbacks for that agent. const cfgDisable: ClawdbotConfig = { diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 718cf877dd2..dcaad42b43b 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -13,9 +13,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js"; export { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; -type AgentEntry = NonNullable< - NonNullable["list"] ->[number]; +type AgentEntry = NonNullable["list"]>[number]; type ResolvedAgentConfig = { name?: string; @@ -36,9 +34,7 @@ let defaultAgentWarned = false; function listAgents(cfg: ClawdbotConfig): AgentEntry[] { const list = cfg.agents?.list; if (!Array.isArray(list)) return []; - return list.filter((entry): entry is AgentEntry => - Boolean(entry && typeof entry === "object"), - ); + return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object")); } export function resolveDefaultAgentId(cfg: ClawdbotConfig): string { @@ -47,24 +43,20 @@ export function resolveDefaultAgentId(cfg: ClawdbotConfig): string { const defaults = agents.filter((agent) => agent?.default); if (defaults.length > 1 && !defaultAgentWarned) { defaultAgentWarned = true; - console.warn( - "Multiple agents marked default=true; using the first entry as default.", - ); + console.warn("Multiple agents marked default=true; using the first entry as default."); } const chosen = (defaults[0] ?? agents[0])?.id?.trim(); return normalizeAgentId(chosen || DEFAULT_AGENT_ID); } -export function resolveSessionAgentIds(params: { - sessionKey?: string; - config?: ClawdbotConfig; -}): { defaultAgentId: string; sessionAgentId: string } { +export function resolveSessionAgentIds(params: { sessionKey?: string; config?: ClawdbotConfig }): { + defaultAgentId: string; + sessionAgentId: string; +} { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); const sessionKey = params.sessionKey?.trim(); const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null; - const sessionAgentId = parsed?.agentId - ? normalizeAgentId(parsed.agentId) - : defaultAgentId; + const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId; return { defaultAgentId, sessionAgentId }; } @@ -75,10 +67,7 @@ export function resolveSessionAgentId(params: { return resolveSessionAgentIds(params).sessionAgentId; } -function resolveAgentEntry( - cfg: ClawdbotConfig, - agentId: string, -): AgentEntry | undefined { +function resolveAgentEntry(cfg: ClawdbotConfig, agentId: string): AgentEntry | undefined { const id = normalizeAgentId(agentId); return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id); } @@ -92,31 +81,23 @@ export function resolveAgentConfig( if (!entry) return undefined; return { name: typeof entry.name === "string" ? entry.name : undefined, - workspace: - typeof entry.workspace === "string" ? entry.workspace : undefined, + workspace: typeof entry.workspace === "string" ? entry.workspace : undefined, agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined, model: - typeof entry.model === "string" || - (entry.model && typeof entry.model === "object") + typeof entry.model === "string" || (entry.model && typeof entry.model === "object") ? entry.model : undefined, memorySearch: entry.memorySearch, humanDelay: entry.humanDelay, identity: entry.identity, groupChat: entry.groupChat, - subagents: - typeof entry.subagents === "object" && entry.subagents - ? entry.subagents - : undefined, + subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined, sandbox: entry.sandbox, tools: entry.tools, }; } -export function resolveAgentModelPrimary( - cfg: ClawdbotConfig, - agentId: string, -): string | undefined { +export function resolveAgentModelPrimary(cfg: ClawdbotConfig, agentId: string): string | undefined { const raw = resolveAgentConfig(cfg, agentId)?.model; if (!raw) return undefined; if (typeof raw === "string") return raw.trim() || undefined; diff --git a/src/agents/anthropic.setup-token.live.test.ts b/src/agents/anthropic.setup-token.live.test.ts index 6c3a99f5103..61512150f78 100644 --- a/src/agents/anthropic.setup-token.live.test.ts +++ b/src/agents/anthropic.setup-token.live.test.ts @@ -4,10 +4,7 @@ import os from "node:os"; import path from "node:path"; import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai"; -import { - discoverAuthStorage, - discoverModels, -} from "@mariozechner/pi-coding-agent"; +import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; import { describe, expect, it } from "vitest"; import { ANTHROPIC_SETUP_TOKEN_PREFIX, @@ -26,15 +23,11 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1"; const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? ""; -const SETUP_TOKEN_VALUE = - process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? ""; -const SETUP_TOKEN_PROFILE = - process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? ""; -const SETUP_TOKEN_MODEL = - process.env.CLAWDBOT_LIVE_SETUP_TOKEN_MODEL?.trim() ?? ""; +const SETUP_TOKEN_VALUE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? ""; +const SETUP_TOKEN_PROFILE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? ""; +const SETUP_TOKEN_MODEL = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_MODEL?.trim() ?? ""; -const ENABLED = - LIVE && Boolean(SETUP_TOKEN_RAW || SETUP_TOKEN_VALUE || SETUP_TOKEN_PROFILE); +const ENABLED = LIVE && Boolean(SETUP_TOKEN_RAW || SETUP_TOKEN_VALUE || SETUP_TOKEN_PROFILE); const describeLive = ENABLED ? describe : describe.skip; type TokenSource = { @@ -60,11 +53,7 @@ function listSetupTokenProfiles(store: { } function pickSetupTokenProfile(candidates: string[]): string { - const preferred = [ - "anthropic:setup-token-test", - "anthropic:setup-token", - "anthropic:default", - ]; + const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"]; for (const id of preferred) { if (candidates.includes(id)) return id; } @@ -73,17 +62,14 @@ function pickSetupTokenProfile(candidates: string[]): string { async function resolveTokenSource(): Promise { const explicitToken = - (SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || - SETUP_TOKEN_VALUE; + (SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE; if (explicitToken) { const error = validateAnthropicSetupToken(explicitToken); if (error) { throw new Error(`Invalid setup-token: ${error}`); } - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-setup-token-"), - ); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-setup-token-")); const profileId = `anthropic:setup-token-live-${randomUUID()}`; const store = ensureAuthProfileStore(tempDir, { allowKeychainPrompt: false, @@ -111,8 +97,7 @@ async function resolveTokenSource(): Promise { const candidates = listSetupTokenProfiles(store); if (SETUP_TOKEN_PROFILE) { if (!candidates.includes(SETUP_TOKEN_PROFILE)) { - const available = - candidates.length > 0 ? candidates.join(", ") : "(none)"; + const available = candidates.length > 0 ? candidates.join(", ") : "(none)"; throw new Error( `Setup-token profile "${SETUP_TOKEN_PROFILE}" not found. Available: ${available}.`, ); @@ -120,11 +105,7 @@ async function resolveTokenSource(): Promise { return { agentDir, profileId: SETUP_TOKEN_PROFILE }; } - if ( - SETUP_TOKEN_RAW && - SETUP_TOKEN_RAW !== "1" && - SETUP_TOKEN_RAW !== "auto" - ) { + if (SETUP_TOKEN_RAW && SETUP_TOKEN_RAW !== "1" && SETUP_TOKEN_RAW !== "auto") { throw new Error( "CLAWDBOT_LIVE_SETUP_TOKEN did not look like a setup-token. Use CLAWDBOT_LIVE_SETUP_TOKEN_VALUE for raw tokens.", ); @@ -146,8 +127,7 @@ function pickModel(models: Array>, raw?: string): Model | null { return ( models.find( (model) => - normalizeProviderId(model.provider) === parsed.provider && - model.id === parsed.model, + normalizeProviderId(model.provider) === parsed.provider && model.id === parsed.model, ) ?? null ); } @@ -176,9 +156,7 @@ describeLive("live anthropic setup-token", () => { const authStorage = discoverAuthStorage(tokenSource.agentDir); const modelRegistry = discoverModels(authStorage, tokenSource.agentDir); - const all = Array.isArray(modelRegistry) - ? modelRegistry - : modelRegistry.getAll(); + const all = Array.isArray(modelRegistry) ? modelRegistry : modelRegistry.getAll(); const candidates = all.filter( (model) => normalizeProviderId(model.provider) === "anthropic", ) as Array>; @@ -201,9 +179,7 @@ describeLive("live anthropic setup-token", () => { }); const tokenError = validateAnthropicSetupToken(apiKeyInfo.apiKey); if (tokenError) { - throw new Error( - `Resolved profile is not a setup-token: ${tokenError}`, - ); + throw new Error(`Resolved profile is not a setup-token: ${tokenError}`); } const res = await completeSimple( diff --git a/src/agents/apply-patch-update.ts b/src/agents/apply-patch-update.ts index de535a18a5a..597d14b9357 100644 --- a/src/agents/apply-patch-update.ts +++ b/src/agents/apply-patch-update.ts @@ -16,10 +16,7 @@ export async function applyUpdateHunk( }); const originalLines = originalContents.split("\n"); - if ( - originalLines.length > 0 && - originalLines[originalLines.length - 1] === "" - ) { + if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { originalLines.pop(); } @@ -41,24 +38,16 @@ function computeReplacements( for (const chunk of chunks) { if (chunk.changeContext) { - const ctxIndex = seekSequence( - originalLines, - [chunk.changeContext], - lineIndex, - false, - ); + const ctxIndex = seekSequence(originalLines, [chunk.changeContext], lineIndex, false); if (ctxIndex === null) { - throw new Error( - `Failed to find context '${chunk.changeContext}' in ${filePath}`, - ); + throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}`); } lineIndex = ctxIndex + 1; } if (chunk.oldLines.length === 0) { const insertionIndex = - originalLines.length > 0 && - originalLines[originalLines.length - 1] === "" + originalLines.length > 0 && originalLines[originalLines.length - 1] === "" ? originalLines.length - 1 : originalLines.length; replacements.push([insertionIndex, 0, chunk.newLines]); @@ -67,24 +56,14 @@ function computeReplacements( let pattern = chunk.oldLines; let newSlice = chunk.newLines; - let found = seekSequence( - originalLines, - pattern, - lineIndex, - chunk.isEndOfFile, - ); + let found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile); if (found === null && pattern[pattern.length - 1] === "") { pattern = pattern.slice(0, -1); if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { newSlice = newSlice.slice(0, -1); } - found = seekSequence( - originalLines, - pattern, - lineIndex, - chunk.isEndOfFile, - ); + found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile); } if (found === null) { @@ -142,11 +121,7 @@ function seekSequence( if (linesMatch(lines, pattern, i, (value) => value.trim())) return i; } for (let i = searchStart; i <= maxStart; i += 1) { - if ( - linesMatch(lines, pattern, i, (value) => - normalizePunctuation(value.trim()), - ) - ) { + if (linesMatch(lines, pattern, i, (value) => normalizePunctuation(value.trim()))) { return i; } } diff --git a/src/agents/apply-patch.ts b/src/agents/apply-patch.ts index 7acb6094bca..045bb712d3c 100644 --- a/src/agents/apply-patch.ts +++ b/src/agents/apply-patch.ts @@ -282,10 +282,7 @@ function checkPatchBoundariesLenient(lines: string[]): string[] { } const first = lines[0]; const last = lines[lines.length - 1]; - if ( - (first === "< { const now = 1_700_000_000_000; @@ -59,9 +56,7 @@ describe("buildAuthHealthSummary", () => { expect(statuses["anthropic:expired"]).toBe("expired"); expect(statuses["anthropic:api"]).toBe("static"); - const provider = summary.providers.find( - (entry) => entry.provider === "anthropic", - ); + const provider = summary.providers.find((entry) => entry.provider === "anthropic"); expect(provider?.status).toBe("expired"); }); }); diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index 8455b47276a..96e79dc663c 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -9,12 +9,7 @@ import { export type AuthProfileSource = "claude-cli" | "codex-cli" | "store"; -export type AuthProfileHealthStatus = - | "ok" - | "expiring" - | "expired" - | "missing" - | "static"; +export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static"; export type AuthProfileHealth = { profileId: string; @@ -27,12 +22,7 @@ export type AuthProfileHealth = { label: string; }; -export type AuthProviderHealthStatus = - | "ok" - | "expiring" - | "expired" - | "missing" - | "static"; +export type AuthProviderHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static"; export type AuthProviderHealth = { provider: string; @@ -111,8 +101,7 @@ function buildProfileHealth(params: { if (credential.type === "token") { const expiresAt = - typeof credential.expires === "number" && - Number.isFinite(credential.expires) + typeof credential.expires === "number" && Number.isFinite(credential.expires) ? credential.expires : undefined; if (!expiresAt || expiresAt <= 0) { @@ -125,11 +114,7 @@ function buildProfileHealth(params: { label, }; } - const { status, remainingMs } = resolveOAuthStatus( - expiresAt, - now, - warnAfterMs, - ); + const { status, remainingMs } = resolveOAuthStatus(expiresAt, now, warnAfterMs); return { profileId, provider: credential.provider, @@ -142,11 +127,7 @@ function buildProfileHealth(params: { }; } - const { status, remainingMs } = resolveOAuthStatus( - credential.expires, - now, - warnAfterMs, - ); + const { status, remainingMs } = resolveOAuthStatus(credential.expires, now, warnAfterMs); return { profileId, provider: credential.provider, @@ -172,9 +153,7 @@ export function buildAuthHealthSummary(params: { : null; const profiles = Object.entries(params.store.profiles) - .filter(([_, cred]) => - providerFilter ? providerFilter.has(cred.provider) : true, - ) + .filter(([_, cred]) => (providerFilter ? providerFilter.has(cred.provider) : true)) .map(([profileId, credential]) => buildProfileHealth({ profileId, @@ -226,9 +205,7 @@ export function buildAuthHealthSummary(params: { const oauthProfiles = provider.profiles.filter((p) => p.type === "oauth"); const tokenProfiles = provider.profiles.filter((p) => p.type === "token"); - const apiKeyProfiles = provider.profiles.filter( - (p) => p.type === "api_key", - ); + const apiKeyProfiles = provider.profiles.filter((p) => p.type === "api_key"); const expirable = [...oauthProfiles, ...tokenProfiles]; if (expirable.length === 0) { diff --git a/src/agents/auth-profiles.chutes.test.ts b/src/agents/auth-profiles.chutes.test.ts index 6d48b2b43fb..00865157ab7 100644 --- a/src/agents/auth-profiles.chutes.test.ts +++ b/src/agents/auth-profiles.chutes.test.ts @@ -8,10 +8,7 @@ import { ensureAuthProfileStore, resolveApiKeyForProfile, } from "./auth-profiles.js"; -import { - CHUTES_TOKEN_ENDPOINT, - type ChutesStoredOAuth, -} from "./chutes-oauth.js"; +import { CHUTES_TOKEN_ENDPOINT, type ChutesStoredOAuth } from "./chutes-oauth.js"; describe("auth-profiles (chutes)", () => { const previousStateDir = process.env.CLAWDBOT_STATE_DIR; @@ -30,32 +27,19 @@ describe("auth-profiles (chutes)", () => { else process.env.CLAWDBOT_STATE_DIR = previousStateDir; if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR; else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; - if (previousPiAgentDir === undefined) - delete process.env.PI_CODING_AGENT_DIR; + if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - if (previousChutesClientId === undefined) - delete process.env.CHUTES_CLIENT_ID; + if (previousChutesClientId === undefined) delete process.env.CHUTES_CLIENT_ID; else process.env.CHUTES_CLIENT_ID = previousChutesClientId; }); it("refreshes expired Chutes OAuth credentials", async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-chutes-")); process.env.CLAWDBOT_STATE_DIR = tempDir; - process.env.CLAWDBOT_AGENT_DIR = path.join( - tempDir, - "agents", - "main", - "agent", - ); + process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agents", "main", "agent"); process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; - const authProfilePath = path.join( - tempDir, - "agents", - "main", - "agent", - "auth-profiles.json", - ); + const authProfilePath = path.join(tempDir, "agents", "main", "agent", "auth-profiles.json"); await fs.mkdir(path.dirname(authProfilePath), { recursive: true }); const store: AuthProfileStore = { @@ -75,8 +59,7 @@ describe("auth-profiles (chutes)", () => { const fetchSpy = vi.fn(async (input: string | URL) => { const url = typeof input === "string" ? input : input.toString(); - if (url !== CHUTES_TOKEN_ENDPOINT) - return new Response("not found", { status: 404 }); + if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 }); return new Response( JSON.stringify({ access_token: "at_new", @@ -96,9 +79,7 @@ describe("auth-profiles (chutes)", () => { expect(resolved?.apiKey).toBe("at_new"); expect(fetchSpy).toHaveBeenCalled(); - const persisted = JSON.parse( - await fs.readFile(authProfilePath, "utf8"), - ) as { + const persisted = JSON.parse(await fs.readFile(authProfilePath, "utf8")) as { profiles?: Record; }; expect(persisted.profiles?.["chutes:default"]?.access).toBe("at_new"); diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index b3ab664bc47..5e8420c78d8 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -6,9 +6,7 @@ import { ensureAuthProfileStore } from "./auth-profiles.js"; describe("ensureAuthProfileStore", () => { it("migrates legacy auth.json and deletes it (PR #368)", () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-auth-profiles-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-profiles-")); try { const legacyPath = path.join(agentDir, "auth.json"); fs.writeFileSync( diff --git a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts index 3402f6a11e6..a56aa746fcf 100644 --- a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts +++ b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts @@ -3,16 +3,11 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; -import { - CLAUDE_CLI_PROFILE_ID, - ensureAuthProfileStore, -} from "./auth-profiles.js"; +import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; describe("external CLI credential sync", () => { it("does not overwrite API keys when syncing external CLI creds", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-no-overwrite-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-no-overwrite-")); try { await withTempHome( async (tempHome) => { @@ -26,10 +21,7 @@ describe("external CLI credential sync", () => { expiresAt: Date.now() + 30 * 60 * 1000, }, }; - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify(claudeCreds), - ); + fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); // Create auth-profiles.json with an API key const authPath = path.join(agentDir, "auth-profiles.json"); @@ -50,9 +42,7 @@ describe("external CLI credential sync", () => { const store = ensureAuthProfileStore(agentDir); // Should keep the store's API key and still add the CLI profile. - expect( - (store.profiles["anthropic:default"] as { key: string }).key, - ).toBe("sk-store"); + expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-store"); expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); }, { prefix: "clawdbot-home-" }, @@ -62,9 +52,7 @@ describe("external CLI credential sync", () => { } }); it("prefers oauth over token even if token has later expiry (oauth enables auto-refresh)", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-")); try { await withTempHome( async (tempHome) => { @@ -103,9 +91,7 @@ describe("external CLI credential sync", () => { // OAuth should be preferred over token because it can auto-refresh const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe( - "cli-oauth-access", - ); + expect((cliProfile as { access: string }).access).toBe("cli-oauth-access"); }, { prefix: "clawdbot-home-" }, ); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts index 9fd0c81c60b..3ca83a57603 100644 --- a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts +++ b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts @@ -3,16 +3,11 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; -import { - CLAUDE_CLI_PROFILE_ID, - ensureAuthProfileStore, -} from "./auth-profiles.js"; +import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; describe("external CLI credential sync", () => { it("does not overwrite fresher store oauth with older CLI oauth", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-")); try { await withTempHome( async (tempHome) => { @@ -52,9 +47,7 @@ describe("external CLI credential sync", () => { // Fresher store oauth should be kept const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe( - "store-oauth-access", - ); + expect((cliProfile as { access: string }).access).toBe("store-oauth-access"); }, { prefix: "clawdbot-home-" }, ); @@ -63,9 +56,7 @@ describe("external CLI credential sync", () => { } }); it("does not downgrade store oauth to token when CLI lacks refresh token", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-")); try { await withTempHome( async (tempHome) => { @@ -104,9 +95,7 @@ describe("external CLI credential sync", () => { // Keep oauth to preserve auto-refresh capability const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe( - "store-oauth-access", - ); + expect((cliProfile as { access: string }).access).toBe("store-oauth-access"); }, { prefix: "clawdbot-home-" }, ); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts index aa2331dbcc3..4d9908287d7 100644 --- a/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts +++ b/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts @@ -3,16 +3,11 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; -import { - CLAUDE_CLI_PROFILE_ID, - ensureAuthProfileStore, -} from "./auth-profiles.js"; +import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; describe("external CLI credential sync", () => { it("syncs Claude CLI OAuth credentials into anthropic:claude-cli", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-sync-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-sync-")); try { // Create a temp home with Claude CLI credentials await withTempHome( @@ -27,10 +22,7 @@ describe("external CLI credential sync", () => { expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now }, }; - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify(claudeCreds), - ); + fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); // Create empty auth-profiles.json const authPath = path.join(agentDir, "auth-profiles.json"); @@ -52,22 +44,14 @@ describe("external CLI credential sync", () => { const store = ensureAuthProfileStore(agentDir); expect(store.profiles["anthropic:default"]).toBeDefined(); - expect( - (store.profiles["anthropic:default"] as { key: string }).key, - ).toBe("sk-default"); + expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-default"); expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); // Should be stored as OAuth credential (type: "oauth") for auto-refresh const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe( - "fresh-access-token", - ); - expect((cliProfile as { refresh: string }).refresh).toBe( - "fresh-refresh-token", - ); - expect((cliProfile as { expires: number }).expires).toBeGreaterThan( - Date.now(), - ); + expect((cliProfile as { access: string }).access).toBe("fresh-access-token"); + expect((cliProfile as { refresh: string }).refresh).toBe("fresh-refresh-token"); + expect((cliProfile as { expires: number }).expires).toBeGreaterThan(Date.now()); }, { prefix: "clawdbot-home-" }, ); @@ -76,9 +60,7 @@ describe("external CLI credential sync", () => { } }); it("syncs Claude CLI credentials without refreshToken as token type", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-token-sync-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-token-sync-")); try { await withTempHome( async (tempHome) => { @@ -92,16 +74,10 @@ describe("external CLI credential sync", () => { expiresAt: Date.now() + 60 * 60 * 1000, }, }; - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify(claudeCreds), - ); + fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ version: 1, profiles: {} }), - ); + fs.writeFileSync(authPath, JSON.stringify({ version: 1, profiles: {} })); const store = ensureAuthProfileStore(agentDir); @@ -109,9 +85,7 @@ describe("external CLI credential sync", () => { // Should be stored as token type (no refresh capability) const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("token"); - expect((cliProfile as { token: string }).token).toBe( - "access-only-token", - ); + expect((cliProfile as { token: string }).token).toBe("access-only-token"); }, { prefix: "clawdbot-home-" }, ); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts index 197506d2e41..16fe775ab4d 100644 --- a/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts +++ b/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts @@ -3,16 +3,11 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; -import { - CODEX_CLI_PROFILE_ID, - ensureAuthProfileStore, -} from "./auth-profiles.js"; +import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; describe("external CLI credential sync", () => { it("updates codex-cli profile when Codex CLI refresh token changes", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-")); try { await withTempHome( async (tempHome) => { @@ -48,10 +43,9 @@ describe("external CLI credential sync", () => { ); const store = ensureAuthProfileStore(agentDir); - expect( - (store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }) - .refresh, - ).toBe("new-refresh"); + expect((store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh).toBe( + "new-refresh", + ); }, { prefix: "clawdbot-home-" }, ); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts index 9950cd00bec..4994a9cff71 100644 --- a/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts +++ b/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts @@ -11,9 +11,7 @@ import { describe("external CLI credential sync", () => { it("upgrades token to oauth when Claude CLI gets refreshToken", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-cli-upgrade-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-upgrade-")); try { await withTempHome( async (tempHome) => { @@ -53,12 +51,8 @@ describe("external CLI credential sync", () => { // Should upgrade from token to oauth const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe( - "new-oauth-access", - ); - expect((cliProfile as { refresh: string }).refresh).toBe( - "new-refresh-token", - ); + expect((cliProfile as { access: string }).access).toBe("new-oauth-access"); + expect((cliProfile as { refresh: string }).refresh).toBe("new-refresh-token"); }, { prefix: "clawdbot-home-" }, ); @@ -67,9 +61,7 @@ describe("external CLI credential sync", () => { } }); it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => { - const agentDir = fs.mkdtempSync( - path.join(os.tmpdir(), "clawdbot-codex-sync-"), - ); + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-sync-")); try { await withTempHome( async (tempHome) => { @@ -98,9 +90,9 @@ describe("external CLI credential sync", () => { const store = ensureAuthProfileStore(agentDir); expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined(); - expect( - (store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access, - ).toBe("codex-access-token"); + expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe( + "codex-access-token", + ); }, { prefix: "clawdbot-home-" }, ); diff --git a/src/agents/auth-profiles.markauthprofilefailure.test.ts b/src/agents/auth-profiles.markauthprofilefailure.test.ts index 60372efc58a..ead7ee5b52c 100644 --- a/src/agents/auth-profiles.markauthprofilefailure.test.ts +++ b/src/agents/auth-profiles.markauthprofilefailure.test.ts @@ -2,10 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { - ensureAuthProfileStore, - markAuthProfileFailure, -} from "./auth-profiles.js"; +import { ensureAuthProfileStore, markAuthProfileFailure } from "./auth-profiles.js"; describe("markAuthProfileFailure", () => { it("disables billing failures for ~5 hours by default", async () => { @@ -35,8 +32,7 @@ describe("markAuthProfileFailure", () => { agentDir, }); - const disabledUntil = - store.usageStats?.["anthropic:default"]?.disabledUntil; + const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil; expect(typeof disabledUntil).toBe("number"); const remainingMs = (disabledUntil as number) - startedAt; expect(remainingMs).toBeGreaterThan(4.5 * 60 * 60 * 1000); @@ -80,8 +76,7 @@ describe("markAuthProfileFailure", () => { } as never, }); - const disabledUntil = - store.usageStats?.["anthropic:default"]?.disabledUntil; + const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil; expect(typeof disabledUntil).toBe("number"); const remainingMs = (disabledUntil as number) - startedAt; expect(remainingMs).toBeGreaterThan(0.8 * 60 * 60 * 1000); @@ -128,9 +123,7 @@ describe("markAuthProfileFailure", () => { }); expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1); - expect( - store.usageStats?.["anthropic:default"]?.failureCounts?.billing, - ).toBe(1); + expect(store.usageStats?.["anthropic:default"]?.failureCounts?.billing).toBe(1); } finally { fs.rmSync(agentDir, { recursive: true, force: true }); } diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts index f9ee167453b..55816522c27 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.orders-by-lastused-no-explicit-order-exists.test.ts @@ -91,10 +91,6 @@ describe("resolveAuthProfileOrder", () => { }, provider: "anthropic", }); - expect(order).toEqual([ - "anthropic:ready", - "anthropic:cool2", - "anthropic:cool1", - ]); + expect(order).toEqual(["anthropic:ready", "anthropic:cool2", "anthropic:cool1"]); }); }); diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index f53798b3d16..9a6c75b10a0 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -1,7 +1,4 @@ -export { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, -} from "./auth-profiles/constants.js"; +export { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "./auth-profiles/constants.js"; export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js"; export { formatAuthDoctorHint } from "./auth-profiles/doctor.js"; export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js"; diff --git a/src/agents/auth-profiles/display.ts b/src/agents/auth-profiles/display.ts index 50fa46a6296..7d3c813d462 100644 --- a/src/agents/auth-profiles/display.ts +++ b/src/agents/auth-profiles/display.ts @@ -11,9 +11,7 @@ export function resolveAuthProfileDisplayLabel(params: { const configEmail = cfg?.auth?.profiles?.[profileId]?.email?.trim(); const email = configEmail || - (profile && "email" in profile - ? (profile.email as string | undefined)?.trim() - : undefined); + (profile && "email" in profile ? (profile.email as string | undefined)?.trim() : undefined); if (email) return `${profileId} (${email})`; return profileId; } diff --git a/src/agents/auth-profiles/doctor.ts b/src/agents/auth-profiles/doctor.ts index ea0b3250c80..cb9ca5d6ad9 100644 --- a/src/agents/auth-profiles/doctor.ts +++ b/src/agents/auth-profiles/doctor.ts @@ -33,9 +33,7 @@ export function formatAuthDoctorHint(params: { "Doctor hint (for GitHub issue):", `- provider: ${providerKey}`, `- config: ${legacyProfileId}${ - cfgProvider || cfgMode - ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` - : "" + cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : "" }`, `- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`, `- suggested profile: ${suggested}`, diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index f0d197f688a..9843a000889 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -16,10 +16,7 @@ import type { TokenCredential, } from "./types.js"; -function shallowEqualOAuthCredentials( - a: OAuthCredential | undefined, - b: OAuthCredential, -): boolean { +function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { if (!a) return false; if (a.type !== "oauth") return false; return ( @@ -34,10 +31,7 @@ function shallowEqualOAuthCredentials( ); } -function shallowEqualTokenCredentials( - a: TokenCredential | undefined, - b: TokenCredential, -): boolean { +function shallowEqualTokenCredentials(a: TokenCredential | undefined, b: TokenCredential): boolean { if (!a) return false; if (a.type !== "token") return false; return ( @@ -48,10 +42,7 @@ function shallowEqualTokenCredentials( ); } -function isExternalProfileFresh( - cred: AuthProfileCredential | undefined, - now: number, -): boolean { +function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; if (cred.provider !== "anthropic" && cred.provider !== "openai-codex") { @@ -104,8 +95,7 @@ export function syncExternalCliCredentials( !existingOAuth || existingOAuth.provider !== "anthropic" || existingOAuth.expires <= now || - (claudeCredsExpires > now && - claudeCredsExpires > existingOAuth.expires); + (claudeCredsExpires > now && claudeCredsExpires > existingOAuth.expires); } else { const existingToken = existing?.type === "token" ? existing : undefined; isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds); @@ -114,8 +104,7 @@ export function syncExternalCliCredentials( !existingToken || existingToken.provider !== "anthropic" || (existingToken.expires ?? 0) <= now || - (claudeCredsExpires > now && - claudeCredsExpires > (existingToken.expires ?? 0)); + (claudeCredsExpires > now && claudeCredsExpires > (existingToken.expires ?? 0)); } // Also update if credential type changed (token -> oauth upgrade) @@ -166,10 +155,7 @@ export function syncExternalCliCredentials( existingOAuth.expires <= now || codexCreds.expires > existingOAuth.expires; - if ( - shouldUpdate && - !shallowEqualOAuthCredentials(existingOAuth, codexCreds) - ) { + if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) { store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds; mutated = true; log.info("synced openai-codex credentials from codex cli", { diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 66f085a7d84..7a90f224105 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -1,8 +1,4 @@ -import { - getOAuthApiKey, - type OAuthCredentials, - type OAuthProvider, -} from "@mariozechner/pi-ai"; +import { getOAuthApiKey, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai"; import lockfile from "proper-lockfile"; import type { ClawdbotConfig } from "../../config/config.js"; @@ -15,12 +11,8 @@ import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js"; import type { AuthProfileStore } from "./types.js"; -function buildOAuthApiKey( - provider: string, - credentials: OAuthCredentials, -): string { - const needsProjectId = - provider === "google-gemini-cli" || provider === "google-antigravity"; +function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string { + const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity"; return needsProjectId ? JSON.stringify({ token: credentials.access, @@ -76,10 +68,7 @@ async function refreshOAuthTokenWithLock(params: { // Sync refreshed credentials back to Claude CLI if this is the claude-cli profile // This ensures Claude Code continues to work after ClawdBot refreshes the token - if ( - params.profileId === CLAUDE_CLI_PROFILE_ID && - cred.provider === "anthropic" - ) { + if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") { writeClaudeCliCredentials(result.newCredentials); } diff --git a/src/agents/auth-profiles/order.ts b/src/agents/auth-profiles/order.ts index b1affb2c4ac..5e9842389c6 100644 --- a/src/agents/auth-profiles/order.ts +++ b/src/agents/auth-profiles/order.ts @@ -43,17 +43,12 @@ export function resolveAuthProfileOrder(params: { const explicitOrder = storedOrder ?? configuredOrder; const explicitProfiles = cfg?.auth?.profiles ? Object.entries(cfg.auth.profiles) - .filter( - ([, profile]) => - normalizeProviderId(profile.provider) === providerKey, - ) + .filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey) .map(([profileId]) => profileId) : []; const baseOrder = explicitOrder ?? - (explicitProfiles.length > 0 - ? explicitProfiles - : listProfilesForProvider(store, providerKey)); + (explicitProfiles.length > 0 ? explicitProfiles : listProfilesForProvider(store, providerKey)); if (baseOrder.length === 0) return []; const filtered = baseOrder.filter((profileId) => { @@ -66,8 +61,7 @@ export function resolveAuthProfileOrder(params: { return false; } if (profileConfig.mode !== cred.type) { - const oauthCompatible = - profileConfig.mode === "oauth" && cred.type === "token"; + const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token"; if (!oauthCompatible) return false; } } @@ -104,8 +98,7 @@ export function resolveAuthProfileOrder(params: { const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = []; for (const profileId of deduped) { - const cooldownUntil = - resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? 0; + const cooldownUntil = resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? 0; if ( typeof cooldownUntil === "number" && Number.isFinite(cooldownUntil) && @@ -126,10 +119,7 @@ export function resolveAuthProfileOrder(params: { // Still put preferredProfile first if specified if (preferredProfile && ordered.includes(preferredProfile)) { - return [ - preferredProfile, - ...ordered.filter((e) => e !== preferredProfile), - ]; + return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)]; } return ordered; } @@ -146,10 +136,7 @@ export function resolveAuthProfileOrder(params: { return sorted; } -function orderProfilesByMode( - order: string[], - store: AuthProfileStore, -): string[] { +function orderProfilesByMode(order: string[], store: AuthProfileStore): string[] { const now = Date.now(); // Partition into available and in-cooldown @@ -168,8 +155,7 @@ function orderProfilesByMode( // Then by lastUsed (oldest first = round-robin within type) const scored = available.map((profileId) => { const type = store.profiles[profileId]?.type; - const typeScore = - type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3; + const typeScore = type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3; const lastUsed = store.usageStats?.[profileId]?.lastUsed ?? 0; return { profileId, typeScore, lastUsed }; }); @@ -189,8 +175,7 @@ function orderProfilesByMode( const cooldownSorted = inCooldown .map((profileId) => ({ profileId, - cooldownUntil: - resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? now, + cooldownUntil: resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? now, })) .sort((a, b) => a.cooldownUntil - b.cooldownUntil) .map((entry) => entry.profileId); diff --git a/src/agents/auth-profiles/paths.ts b/src/agents/auth-profiles/paths.ts index b0e092f882d..dbffcfab709 100644 --- a/src/agents/auth-profiles/paths.ts +++ b/src/agents/auth-profiles/paths.ts @@ -4,11 +4,7 @@ import path from "node:path"; import { saveJsonFile } from "../../infra/json-file.js"; import { resolveUserPath } from "../../utils.js"; import { resolveClawdbotAgentDir } from "../agent-paths.js"; -import { - AUTH_PROFILE_FILENAME, - AUTH_STORE_VERSION, - LEGACY_AUTH_FILENAME, -} from "./constants.js"; +import { AUTH_PROFILE_FILENAME, AUTH_STORE_VERSION, LEGACY_AUTH_FILENAME } from "./constants.js"; import type { AuthProfileStore } from "./types.js"; export function resolveAuthStorePath(agentDir?: string): string { diff --git a/src/agents/auth-profiles/profiles.ts b/src/agents/auth-profiles/profiles.ts index bf30756d290..b14b83324a6 100644 --- a/src/agents/auth-profiles/profiles.ts +++ b/src/agents/auth-profiles/profiles.ts @@ -50,10 +50,7 @@ export function upsertAuthProfile(params: { saveAuthProfileStore(store, params.agentDir); } -export function listProfilesForProvider( - store: AuthProfileStore, - provider: string, -): string[] { +export function listProfilesForProvider(store: AuthProfileStore, provider: string): string[] { const providerKey = normalizeProviderId(provider); return Object.entries(store.profiles) .filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey) diff --git a/src/agents/auth-profiles/repair.ts b/src/agents/auth-profiles/repair.ts index 75995983dda..5b22fef61f8 100644 --- a/src/agents/auth-profiles/repair.ts +++ b/src/agents/auth-profiles/repair.ts @@ -35,10 +35,9 @@ export function suggestOAuthProfileIdForLegacyDefault(params: { return null; } - const oauthProfiles = listProfilesForProvider( - params.store, - providerKey, - ).filter((id) => params.store.profiles[id]?.type === "oauth"); + const oauthProfiles = listProfilesForProvider(params.store, providerKey).filter( + (id) => params.store.profiles[id]?.type === "oauth", + ); if (oauthProfiles.length === 0) return null; const configuredEmail = legacyCfg?.email?.trim(); @@ -47,16 +46,12 @@ export function suggestOAuthProfileIdForLegacyDefault(params: { const cred = params.store.profiles[id]; if (!cred || cred.type !== "oauth") return false; const email = (cred.email as string | undefined)?.trim(); - return ( - email === configuredEmail || id === `${providerKey}:${configuredEmail}` - ); + return email === configuredEmail || id === `${providerKey}:${configuredEmail}`; }); if (byEmail) return byEmail; } - const lastGood = - params.store.lastGood?.[providerKey] ?? - params.store.lastGood?.[params.provider]; + const lastGood = params.store.lastGood?.[providerKey] ?? params.store.lastGood?.[params.provider]; if (lastGood && oauthProfiles.includes(lastGood)) return lastGood; const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId); @@ -83,10 +78,7 @@ export function repairOAuthProfileIdMismatch(params: { if (legacyCfg.mode !== "oauth") { return { config: params.cfg, changes: [], migrated: false }; } - if ( - normalizeProviderId(legacyCfg.provider) !== - normalizeProviderId(params.provider) - ) { + if (normalizeProviderId(legacyCfg.provider) !== normalizeProviderId(params.provider)) { return { config: params.cfg, changes: [], migrated: false }; } @@ -102,14 +94,10 @@ export function repairOAuthProfileIdMismatch(params: { const toCred = params.store.profiles[toProfileId]; const toEmail = - toCred?.type === "oauth" - ? (toCred.email as string | undefined)?.trim() - : undefined; + toCred?.type === "oauth" ? (toCred.email as string | undefined)?.trim() : undefined; const nextProfiles = { - ...(params.cfg.auth?.profiles as - | Record - | undefined), + ...(params.cfg.auth?.profiles as Record | undefined), } as Record; delete nextProfiles[legacyProfileId]; nextProfiles[toProfileId] = { @@ -121,17 +109,13 @@ export function repairOAuthProfileIdMismatch(params: { const nextOrder = (() => { const order = params.cfg.auth?.order; if (!order) return undefined; - const resolvedKey = Object.keys(order).find( - (key) => normalizeProviderId(key) === providerKey, - ); + const resolvedKey = Object.keys(order).find((key) => normalizeProviderId(key) === providerKey); if (!resolvedKey) return order; const existing = order[resolvedKey]; if (!Array.isArray(existing)) return order; const replaced = existing .map((id) => (id === legacyProfileId ? toProfileId : id)) - .filter( - (id): id is string => typeof id === "string" && id.trim().length > 0, - ); + .filter((id): id is string => typeof id === "string" && id.trim().length > 0); const deduped: string[] = []; for (const entry of replaced) { if (!deduped.includes(entry)) deduped.push(entry); @@ -148,9 +132,7 @@ export function repairOAuthProfileIdMismatch(params: { }, }; - const changes = [ - `Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`, - ]; + const changes = [`Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`]; return { config: nextCfg, diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 62796916137..062fbad6cf4 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -3,29 +3,14 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; import lockfile from "proper-lockfile"; import { resolveOAuthPath } from "../../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js"; -import { - AUTH_STORE_LOCK_OPTIONS, - AUTH_STORE_VERSION, - log, -} from "./constants.js"; +import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js"; import { syncExternalCliCredentials } from "./external-cli-sync.js"; -import { - ensureAuthStoreFile, - resolveAuthStorePath, - resolveLegacyAuthStorePath, -} from "./paths.js"; -import type { - AuthProfileCredential, - AuthProfileStore, - ProfileUsageStats, -} from "./types.js"; +import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js"; +import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js"; type LegacyAuthStore = Record; -function _syncAuthProfileStore( - target: AuthProfileStore, - source: AuthProfileStore, -): void { +function _syncAuthProfileStore(target: AuthProfileStore, source: AuthProfileStore): void { target.version = source.version; target.profiles = source.profiles; target.order = source.order; @@ -70,11 +55,7 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null { for (const [key, value] of Object.entries(record)) { if (!value || typeof value !== "object") continue; const typed = value as Partial; - if ( - typed.type !== "api_key" && - typed.type !== "oauth" && - typed.type !== "token" - ) { + if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") { continue; } entries[key] = { @@ -94,11 +75,7 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null { for (const [key, value] of Object.entries(profiles)) { if (!value || typeof value !== "object") continue; const typed = value as Partial; - if ( - typed.type !== "api_key" && - typed.type !== "oauth" && - typed.type !== "token" - ) { + if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") { continue; } if (!typed.provider) continue; @@ -188,9 +165,7 @@ export function loadAuthProfileStore(): AuthProfileStore { type: "token", provider: String(cred.provider ?? provider), token: cred.token, - ...(typeof cred.expires === "number" - ? { expires: cred.expires } - : {}), + ...(typeof cred.expires === "number" ? { expires: cred.expires } : {}), ...(cred.email ? { email: cred.email } : {}), }; } else { @@ -253,9 +228,7 @@ export function ensureAuthProfileStore( type: "token", provider: String(cred.provider ?? provider), token: cred.token, - ...(typeof cred.expires === "number" - ? { expires: cred.expires } - : {}), + ...(typeof cred.expires === "number" ? { expires: cred.expires } : {}), ...(cred.email ? { email: cred.email } : {}), }; } else { @@ -301,10 +274,7 @@ export function ensureAuthProfileStore( return store; } -export function saveAuthProfileStore( - store: AuthProfileStore, - agentDir?: string, -): void { +export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void { const authPath = resolveAuthStorePath(agentDir); const payload = { version: AUTH_STORE_VERSION, diff --git a/src/agents/auth-profiles/types.ts b/src/agents/auth-profiles/types.ts index 149c2c7de44..32a4a44bdc8 100644 --- a/src/agents/auth-profiles/types.ts +++ b/src/agents/auth-profiles/types.ts @@ -29,10 +29,7 @@ export type OAuthCredential = OAuthCredentials & { email?: string; }; -export type AuthProfileCredential = - | ApiKeyCredential - | TokenCredential - | OAuthCredential; +export type AuthProfileCredential = ApiKeyCredential | TokenCredential | OAuthCredential; export type AuthProfileFailureReason = | "auth" diff --git a/src/agents/auth-profiles/usage.ts b/src/agents/auth-profiles/usage.ts index 20d2352b99b..945548a9574 100644 --- a/src/agents/auth-profiles/usage.ts +++ b/src/agents/auth-profiles/usage.ts @@ -1,14 +1,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { normalizeProviderId } from "../model-selection.js"; -import { - saveAuthProfileStore, - updateAuthProfileStoreWithLock, -} from "./store.js"; -import type { - AuthProfileFailureReason, - AuthProfileStore, - ProfileUsageStats, -} from "./types.js"; +import { saveAuthProfileStore, updateAuthProfileStoreWithLock } from "./store.js"; +import type { AuthProfileFailureReason, AuthProfileStore, ProfileUsageStats } from "./types.js"; function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null { const values = [stats.cooldownUntil, stats.disabledUntil] @@ -21,10 +14,7 @@ function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null { /** * Check if a profile is currently in cooldown (due to rate limiting or errors). */ -export function isProfileInCooldown( - store: AuthProfileStore, - profileId: string, -): boolean { +export function isProfileInCooldown(store: AuthProfileStore, profileId: string): boolean { const stats = store.usageStats?.[profileId]; if (!stats) return false; const unusableUntil = resolveProfileUnusableUntil(stats); @@ -102,9 +92,7 @@ function resolveAuthCooldownConfig(params: { } as const; const resolveHours = (value: unknown, fallback: number) => - typeof value === "number" && Number.isFinite(value) && value > 0 - ? value - : fallback; + typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback; const cooldowns = params.cfg?.auth?.cooldowns; const billingOverride = (() => { @@ -120,10 +108,7 @@ function resolveAuthCooldownConfig(params: { billingOverride ?? cooldowns?.billingBackoffHours, defaults.billingBackoffHours, ); - const billingMaxHours = resolveHours( - cooldowns?.billingMaxHours, - defaults.billingMaxHours, - ); + const billingMaxHours = resolveHours(cooldowns?.billingMaxHours, defaults.billingMaxHours); const failureWindowHours = resolveHours( cooldowns?.failureWindowHours, defaults.failureWindowHours, @@ -172,9 +157,7 @@ function computeNextProfileUsageStats(params: { const baseErrorCount = windowExpired ? 0 : (params.existing.errorCount ?? 0); const nextErrorCount = baseErrorCount + 1; - const failureCounts = windowExpired - ? {} - : { ...params.existing.failureCounts }; + const failureCounts = windowExpired ? {} : { ...params.existing.failureCounts }; failureCounts[params.reason] = (failureCounts[params.reason] ?? 0) + 1; const updatedStats: ProfileUsageStats = { @@ -246,9 +229,7 @@ export async function markAuthProfileFailure(params: { store.usageStats = store.usageStats ?? {}; const existing = store.usageStats[profileId] ?? {}; const now = Date.now(); - const providerKey = normalizeProviderId( - store.profiles[profileId]?.provider ?? "", - ); + const providerKey = normalizeProviderId(store.profiles[profileId]?.provider ?? ""); const cfgResolved = resolveAuthCooldownConfig({ cfg, providerId: providerKey, diff --git a/src/agents/bash-process-registry.ts b/src/agents/bash-process-registry.ts index 71c911376e3..c68531d9c5d 100644 --- a/src/agents/bash-process-registry.ts +++ b/src/agents/bash-process-registry.ts @@ -9,9 +9,7 @@ function clampTtl(value: number | undefined) { return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS); } -let jobTtlMs = clampTtl( - Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10), -); +let jobTtlMs = clampTtl(Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10)); export type ProcessStatus = "running" | "completed" | "failed" | "killed"; @@ -75,24 +73,15 @@ export function deleteSession(id: string) { finishedSessions.delete(id); } -export function appendOutput( - session: ProcessSession, - stream: "stdout" | "stderr", - chunk: string, -) { +export function appendOutput(session: ProcessSession, stream: "stdout" | "stderr", chunk: string) { session.pendingStdout ??= []; session.pendingStderr ??= []; - const buffer = - stream === "stdout" ? session.pendingStdout : session.pendingStderr; + const buffer = stream === "stdout" ? session.pendingStdout : session.pendingStderr; buffer.push(chunk); session.totalOutputChars += chunk.length; - const aggregated = trimWithCap( - session.aggregated + chunk, - session.maxOutputChars, - ); + const aggregated = trimWithCap(session.aggregated + chunk, session.maxOutputChars); session.truncated = - session.truncated || - aggregated.length < session.aggregated.length + chunk.length; + session.truncated || aggregated.length < session.aggregated.length + chunk.length; session.aggregated = aggregated; session.tail = tail(session.aggregated, 2000); } diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 23521de70ba..efa57a4ab23 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -4,12 +4,7 @@ import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core"; import { Type } from "@sinclair/typebox"; import { logInfo } from "../logger.js"; -import { - addSession, - appendOutput, - markBackgrounded, - markExited, -} from "./bash-process-registry.js"; +import { addSession, appendOutput, markBackgrounded, markExited } from "./bash-process-registry.js"; import type { BashSandboxConfig } from "./bash-tools.shared.js"; import { buildDockerExecArgs, @@ -32,8 +27,7 @@ const DEFAULT_MAX_OUTPUT = clampNumber( 150_000, ); const DEFAULT_PATH = - process.env.PATH ?? - "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; export type ExecToolDefaults = { backgroundMs?: number; @@ -55,18 +49,14 @@ export type ExecElevatedDefaults = { const execSchema = Type.Object({ command: Type.String({ description: "Shell command to execute" }), - workdir: Type.Optional( - Type.String({ description: "Working directory (defaults to cwd)" }), - ), + workdir: Type.Optional(Type.String({ description: "Working directory (defaults to cwd)" })), env: Type.Optional(Type.Record(Type.String(), Type.String())), yieldMs: Type.Optional( Type.Number({ description: "Milliseconds to wait before backgrounding (default 10000)", }), ), - background: Type.Optional( - Type.Boolean({ description: "Run in background immediately" }), - ), + background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })), timeout: Type.Optional( Type.Number({ description: "Timeout in seconds (optional, kills process on expiry)", @@ -140,19 +130,12 @@ export function createExecTool( const backgroundRequested = params.background === true; const yieldRequested = typeof params.yieldMs === "number"; if (!allowBackground && (backgroundRequested || yieldRequested)) { - warnings.push( - "Warning: background execution is disabled; running synchronously.", - ); + warnings.push("Warning: background execution is disabled; running synchronously."); } const yieldWindow = allowBackground ? backgroundRequested ? 0 - : clampNumber( - params.yieldMs ?? defaultBackgroundMs, - defaultBackgroundMs, - 10, - 120_000, - ) + : clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000) : null; const elevatedDefaults = defaults?.elevated; const elevatedDefaultOn = @@ -160,17 +143,13 @@ export function createExecTool( elevatedDefaults.enabled && elevatedDefaults.allowed; const elevatedRequested = - typeof params.elevated === "boolean" - ? params.elevated - : elevatedDefaultOn; + typeof params.elevated === "boolean" ? params.elevated : elevatedDefaultOn; if (elevatedRequested) { if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) { const runtime = defaults?.sandbox ? "sandboxed" : "direct"; const gates: string[] = []; if (!elevatedDefaults?.enabled) { - gates.push( - "enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)", - ); + gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)"); } else { gates.push( "allowFrom (tools.elevated.allowFrom. / agents.list[].tools.elevated.allowFrom.)", @@ -197,8 +176,7 @@ export function createExecTool( } const sandbox = elevatedRequested ? undefined : defaults?.sandbox; - const rawWorkdir = - params.workdir?.trim() || defaults?.cwd || process.cwd(); + const rawWorkdir = params.workdir?.trim() || defaults?.cwd || process.cwd(); let workdir = rawWorkdir; let containerWorkdir = sandbox?.containerWorkdir; if (sandbox) { @@ -335,121 +313,111 @@ export function createExecTool( } }); - return new Promise>( - (resolve, reject) => { - const resolveRunning = () => { - settle(() => - resolve({ - content: [ - { - type: "text", - text: - `${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` + - `Command still running (session ${sessionId}, pid ${session.pid ?? "n/a"}). ` + - "Use process (list/poll/log/write/kill/clear/remove) for follow-up.", - }, - ], - details: { - status: "running", - sessionId, - pid: session.pid ?? undefined, - startedAt, - cwd: session.cwd, - tail: session.tail, + return new Promise>((resolve, reject) => { + const resolveRunning = () => { + settle(() => + resolve({ + content: [ + { + type: "text", + text: + `${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` + + `Command still running (session ${sessionId}, pid ${session.pid ?? "n/a"}). ` + + "Use process (list/poll/log/write/kill/clear/remove) for follow-up.", }, - }), - ); - }; + ], + details: { + status: "running", + sessionId, + pid: session.pid ?? undefined, + startedAt, + cwd: session.cwd, + tail: session.tail, + }, + }), + ); + }; - const onYieldNow = () => { - if (yieldTimer) clearTimeout(yieldTimer); - if (settled) return; - yielded = true; - markBackgrounded(session); - resolveRunning(); - }; + const onYieldNow = () => { + if (yieldTimer) clearTimeout(yieldTimer); + if (settled) return; + yielded = true; + markBackgrounded(session); + resolveRunning(); + }; - if (allowBackground && yieldWindow !== null) { - if (yieldWindow === 0) { - onYieldNow(); - } else { - yieldTimer = setTimeout(() => { - if (settled) return; - yielded = true; - markBackgrounded(session); - resolveRunning(); - }, yieldWindow); - } + if (allowBackground && yieldWindow !== null) { + if (yieldWindow === 0) { + onYieldNow(); + } else { + yieldTimer = setTimeout(() => { + if (settled) return; + yielded = true; + markBackgrounded(session); + resolveRunning(); + }, yieldWindow); + } + } + + const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => { + if (yieldTimer) clearTimeout(yieldTimer); + if (timeoutTimer) clearTimeout(timeoutTimer); + const durationMs = Date.now() - startedAt; + const wasSignal = exitSignal != null; + const isSuccess = code === 0 && !wasSignal && !signal?.aborted && !timedOut; + const status: "completed" | "failed" = isSuccess ? "completed" : "failed"; + markExited(session, code, exitSignal, status); + + if (yielded || session.backgrounded) return; + + const aggregated = session.aggregated.trim(); + if (!isSuccess) { + const reason = timedOut + ? `Command timed out after ${effectiveTimeout} seconds` + : wasSignal && exitSignal + ? `Command aborted by signal ${exitSignal}` + : code === null + ? "Command aborted before exit code was captured" + : `Command exited with code ${code}`; + const message = aggregated ? `${aggregated}\n\n${reason}` : reason; + settle(() => reject(new Error(message))); + return; } - const handleExit = ( - code: number | null, - exitSignal: NodeJS.Signals | number | null, - ) => { - if (yieldTimer) clearTimeout(yieldTimer); - if (timeoutTimer) clearTimeout(timeoutTimer); - const durationMs = Date.now() - startedAt; - const wasSignal = exitSignal != null; - const isSuccess = - code === 0 && !wasSignal && !signal?.aborted && !timedOut; - const status: "completed" | "failed" = isSuccess - ? "completed" - : "failed"; - markExited(session, code, exitSignal, status); - - if (yielded || session.backgrounded) return; - - const aggregated = session.aggregated.trim(); - if (!isSuccess) { - const reason = timedOut - ? `Command timed out after ${effectiveTimeout} seconds` - : wasSignal && exitSignal - ? `Command aborted by signal ${exitSignal}` - : code === null - ? "Command aborted before exit code was captured" - : `Command exited with code ${code}`; - const message = aggregated - ? `${aggregated}\n\n${reason}` - : reason; - settle(() => reject(new Error(message))); - return; - } - - settle(() => - resolve({ - content: [ - { - type: "text", - text: - `${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` + - (aggregated || "(no output)"), - }, - ], - details: { - status: "completed", - exitCode: code ?? 0, - durationMs, - aggregated, - cwd: session.cwd, + settle(() => + resolve({ + content: [ + { + type: "text", + text: + `${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` + + (aggregated || "(no output)"), }, - }), - ); - }; + ], + details: { + status: "completed", + exitCode: code ?? 0, + durationMs, + aggregated, + cwd: session.cwd, + }, + }), + ); + }; - // `exit` can fire before stdio fully flushes (notably on Windows). - // `close` waits for streams to close, so aggregated output is complete. - child.once("close", (code, exitSignal) => { - handleExit(code, exitSignal); - }); + // `exit` can fire before stdio fully flushes (notably on Windows). + // `close` waits for streams to close, so aggregated output is complete. + child.once("close", (code, exitSignal) => { + handleExit(code, exitSignal); + }); - child.once("error", (err) => { - if (yieldTimer) clearTimeout(yieldTimer); - if (timeoutTimer) clearTimeout(timeoutTimer); - markExited(session, null, null, "failed"); - settle(() => reject(err)); - }); - }, - ); + child.once("error", (err) => { + if (yieldTimer) clearTimeout(yieldTimer); + if (timeoutTimer) clearTimeout(timeoutTimer); + markExited(session, null, null, "failed"); + settle(() => reject(err)); + }); + }); }, }; } diff --git a/src/agents/bash-tools.process.ts b/src/agents/bash-tools.process.ts index 312436dbe19..3b2a32800c4 100644 --- a/src/agents/bash-tools.process.ts +++ b/src/agents/bash-tools.process.ts @@ -27,9 +27,7 @@ export type ProcessToolDefaults = { const processSchema = Type.Object({ action: Type.String({ description: "Process action" }), - sessionId: Type.Optional( - Type.String({ description: "Session id for actions other than list" }), - ), + sessionId: Type.Optional(Type.String({ description: "Session id for actions other than list" })), data: Type.Optional(Type.String({ description: "Data to write for write" })), eof: Type.Optional(Type.Boolean({ description: "Close stdin after write" })), offset: Type.Optional(Type.Number({ description: "Log offset" })), @@ -96,9 +94,7 @@ export function createProcessTool( const lines = [...running, ...finished] .sort((a, b) => b.startedAt - a.startedAt) .map((s) => { - const label = s.name - ? truncateMiddle(s.name, 80) - : truncateMiddle(s.command, 120); + const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120); return `${s.sessionId.slice(0, 8)} ${pad( s.status, 9, @@ -117,9 +113,7 @@ export function createProcessTool( if (!params.sessionId) { return { - content: [ - { type: "text", text: "sessionId is required for this action." }, - ], + content: [{ type: "text", text: "sessionId is required for this action." }], details: { status: "failed" }, }; } @@ -150,10 +144,7 @@ export function createProcessTool( }, ], details: { - status: - scopedFinished.status === "completed" - ? "completed" - : "failed", + status: scopedFinished.status === "completed" ? "completed" : "failed", sessionId: params.sessionId, exitCode: scopedFinished.exitCode ?? undefined, aggregated: scopedFinished.aggregated, @@ -187,8 +178,7 @@ export function createProcessTool( const exitCode = scopedSession.exitCode ?? 0; const exitSignal = scopedSession.exitSignal ?? undefined; if (exited) { - const status = - exitCode === 0 && exitSignal == null ? "completed" : "failed"; + const status = exitCode === 0 && exitSignal == null ? "completed" : "failed"; markExited( scopedSession, scopedSession.exitCode ?? null, @@ -201,10 +191,7 @@ export function createProcessTool( ? "completed" : "failed" : "running"; - const output = [stdout.trimEnd(), stderr.trimEnd()] - .filter(Boolean) - .join("\n") - .trim(); + const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n").trim(); return { content: [ { @@ -265,12 +252,9 @@ export function createProcessTool( params.offset, params.limit, ); - const status = - scopedFinished.status === "completed" ? "completed" : "failed"; + const status = scopedFinished.status === "completed" ? "completed" : "failed"; return { - content: [ - { type: "text", text: slice || "(no output recorded)" }, - ], + content: [{ type: "text", text: slice || "(no output recorded)" }], details: { status, sessionId: params.sessionId, @@ -318,10 +302,7 @@ export function createProcessTool( details: { status: "failed" }, }; } - if ( - !scopedSession.child?.stdin || - scopedSession.child.stdin.destroyed - ) { + if (!scopedSession.child?.stdin || scopedSession.child.stdin.destroyed) { return { content: [ { @@ -353,9 +334,7 @@ export function createProcessTool( details: { status: "running", sessionId: params.sessionId, - name: scopedSession - ? deriveSessionName(scopedSession.command) - : undefined, + name: scopedSession ? deriveSessionName(scopedSession.command) : undefined, }, }; } @@ -386,14 +365,10 @@ export function createProcessTool( killSession(scopedSession); markExited(scopedSession, null, "SIGKILL", "failed"); return { - content: [ - { type: "text", text: `Killed session ${params.sessionId}.` }, - ], + content: [{ type: "text", text: `Killed session ${params.sessionId}.` }], details: { status: "failed", - name: scopedSession - ? deriveSessionName(scopedSession.command) - : undefined, + name: scopedSession ? deriveSessionName(scopedSession.command) : undefined, }, }; } @@ -402,9 +377,7 @@ export function createProcessTool( if (scopedFinished) { deleteSession(params.sessionId); return { - content: [ - { type: "text", text: `Cleared session ${params.sessionId}.` }, - ], + content: [{ type: "text", text: `Cleared session ${params.sessionId}.` }], details: { status: "completed" }, }; } @@ -424,23 +397,17 @@ export function createProcessTool( killSession(scopedSession); markExited(scopedSession, null, "SIGKILL", "failed"); return { - content: [ - { type: "text", text: `Removed session ${params.sessionId}.` }, - ], + content: [{ type: "text", text: `Removed session ${params.sessionId}.` }], details: { status: "failed", - name: scopedSession - ? deriveSessionName(scopedSession.command) - : undefined, + name: scopedSession ? deriveSessionName(scopedSession.command) : undefined, }, }; } if (scopedFinished) { deleteSession(params.sessionId); return { - content: [ - { type: "text", text: `Removed session ${params.sessionId}.` }, - ], + content: [{ type: "text", text: `Removed session ${params.sessionId}.` }], details: { status: "completed" }, }; } @@ -457,9 +424,7 @@ export function createProcessTool( } return { - content: [ - { type: "text", text: `Unknown action ${params.action as string}` }, - ], + content: [{ type: "text", text: `Unknown action ${params.action as string}` }], details: { status: "failed" }, }; }, diff --git a/src/agents/bash-tools.shared.ts b/src/agents/bash-tools.shared.ts index adaf87ab5e2..e71541a13d9 100644 --- a/src/agents/bash-tools.shared.ts +++ b/src/agents/bash-tools.shared.ts @@ -98,10 +98,7 @@ export async function resolveSandboxWorkdir(params: { } } -export function killSession(session: { - pid?: number; - child?: ChildProcessWithoutNullStreams; -}) { +export function killSession(session: { pid?: number; child?: ChildProcessWithoutNullStreams }) { const pid = session.pid ?? session.child?.pid; if (pid) { killProcessTree(pid); @@ -117,9 +114,7 @@ export function resolveWorkdir(workdir: string, warnings: string[]) { } catch { // ignore, fallback below } - warnings.push( - `Warning: workdir "${workdir}" is unavailable; using "${fallback}".`, - ); + warnings.push(`Warning: workdir "${workdir}" is unavailable; using "${fallback}".`); return fallback; } @@ -177,9 +172,7 @@ export function sliceLogLines( const totalLines = lines.length; const totalChars = text.length; let start = - typeof offset === "number" && Number.isFinite(offset) - ? Math.max(0, Math.floor(offset)) - : 0; + typeof offset === "number" && Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0; if (limit !== undefined && offset === undefined) { const tailCount = Math.max(0, Math.floor(limit)); start = Math.max(totalLines - tailCount, 0); @@ -203,8 +196,7 @@ export function deriveSessionName(command: string): string | undefined { } function tokenizeCommand(command: string): string[] { - const matches = - command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? []; + const matches = command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? []; return matches.map((token) => stripQuotes(token)).filter(Boolean); } diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 3e587186b26..46843fc5f55 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -1,11 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { resetProcessRegistryForTests } from "./bash-process-registry.js"; -import { - createExecTool, - createProcessTool, - execTool, - processTool, -} from "./bash-tools.js"; +import { createExecTool, createProcessTool, execTool, processTool } from "./bash-tools.js"; import { sanitizeBinaryOutput } from "./shell-utils.js"; const isWin = process.platform === "win32"; @@ -15,10 +10,8 @@ const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2"; const longDelayCmd = isWin ? "Start-Sleep -Seconds 2" : "sleep 2"; // Both PowerShell and bash use ; for command separation const joinCommands = (commands: string[]) => commands.join("; "); -const echoAfterDelay = (message: string) => - joinCommands([shortDelayCmd, `echo ${message}`]); -const echoLines = (lines: string[]) => - joinCommands(lines.map((line) => `echo ${line}`)); +const echoAfterDelay = (message: string) => joinCommands([shortDelayCmd, `echo ${message}`]); +const echoLines = (lines: string[]) => joinCommands(lines.map((line) => `echo ${line}`)); const normalizeText = (value?: string) => sanitizeBinaryOutput(value ?? "") .replace(/\r\n/g, "\n") @@ -74,8 +67,7 @@ describe("exec tool backgrounding", () => { let status = "running"; let output = ""; - const deadline = - Date.now() + (process.platform === "win32" ? 8000 : 2000); + const deadline = Date.now() + (process.platform === "win32" ? 8000 : 2000); while (Date.now() < deadline && status === "running") { const poll = await processTool.execute("call2", { @@ -106,9 +98,7 @@ describe("exec tool backgrounding", () => { const sessionId = (result.details as { sessionId: string }).sessionId; const list = await processTool.execute("call2", { action: "list" }); - const sessions = ( - list.details as { sessions: Array<{ sessionId: string }> } - ).sessions; + const sessions = (list.details as { sessions: Array<{ sessionId: string }> }).sessions; expect(sessions.some((s) => s.sessionId === sessionId)).toBe(true); }); @@ -121,9 +111,8 @@ describe("exec tool backgrounding", () => { await sleep(25); const list = await processTool.execute("call2", { action: "list" }); - const sessions = ( - list.details as { sessions: Array<{ sessionId: string; name?: string }> } - ).sessions; + const sessions = (list.details as { sessions: Array<{ sessionId: string; name?: string }> }) + .sessions; const entry = sessions.find((s) => s.sessionId === sessionId); expect(entry?.name).toBe("echo hello"); }); @@ -239,9 +228,7 @@ describe("exec tool backgrounding", () => { const sessionB = (resultB.details as { sessionId: string }).sessionId; const listA = await processA.execute("call3", { action: "list" }); - const sessionsA = ( - listA.details as { sessions: Array<{ sessionId: string }> } - ).sessions; + const sessionsA = (listA.details as { sessions: Array<{ sessionId: string }> }).sessions; expect(sessionsA.some((s) => s.sessionId === sessionA)).toBe(true); expect(sessionsA.some((s) => s.sessionId === sessionB)).toBe(false); diff --git a/src/agents/channel-tools.ts b/src/agents/channel-tools.ts index 1f9c1e5236a..c09ecc7c06a 100644 --- a/src/agents/channel-tools.ts +++ b/src/agents/channel-tools.ts @@ -2,9 +2,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelAgentTool } from "../channels/plugins/types.js"; import type { ClawdbotConfig } from "../config/config.js"; -export function listChannelAgentTools(params: { - cfg?: ClawdbotConfig; -}): ChannelAgentTool[] { +export function listChannelAgentTools(params: { cfg?: ClawdbotConfig }): ChannelAgentTool[] { // Channel docking: aggregate channel-owned tools (login, etc.). const tools: ChannelAgentTool[] = []; for (const plugin of listChannelPlugins()) { diff --git a/src/agents/chutes-oauth.test.ts b/src/agents/chutes-oauth.test.ts index b1e92ce7d7d..8881901b3f7 100644 --- a/src/agents/chutes-oauth.test.ts +++ b/src/agents/chutes-oauth.test.ts @@ -14,10 +14,7 @@ describe("chutes-oauth", () => { if (url === CHUTES_TOKEN_ENDPOINT) { expect(init?.method).toBe("POST"); expect( - String( - init?.headers && - (init.headers as Record)["Content-Type"], - ), + String(init?.headers && (init.headers as Record)["Content-Type"]), ).toContain("application/x-www-form-urlencoded"); return new Response( JSON.stringify({ @@ -30,18 +27,12 @@ describe("chutes-oauth", () => { } if (url === CHUTES_USERINFO_ENDPOINT) { expect( - String( - init?.headers && - (init.headers as Record).Authorization, - ), + String(init?.headers && (init.headers as Record).Authorization), ).toBe("Bearer at_123"); - return new Response( - JSON.stringify({ username: "fred", sub: "sub_1" }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); + return new Response(JSON.stringify({ username: "fred", sub: "sub_1" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); } return new Response("not found", { status: 404 }); }; @@ -62,20 +53,15 @@ describe("chutes-oauth", () => { expect(creds.access).toBe("at_123"); expect(creds.refresh).toBe("rt_123"); expect(creds.email).toBe("fred"); - expect((creds as unknown as { accountId?: string }).accountId).toBe( - "sub_1", - ); - expect((creds as unknown as { clientId?: string }).clientId).toBe( - "cid_test", - ); + expect((creds as unknown as { accountId?: string }).accountId).toBe("sub_1"); + expect((creds as unknown as { clientId?: string }).clientId).toBe("cid_test"); expect(creds.expires).toBe(now + 3600 * 1000 - 5 * 60 * 1000); }); it("refreshes tokens using stored client id and falls back to old refresh token", async () => { const fetchFn: typeof fetch = async (input, init) => { const url = String(input); - if (url !== CHUTES_TOKEN_ENDPOINT) - return new Response("not found", { status: 404 }); + if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 }); expect(init?.method).toBe("POST"); const body = init?.body as URLSearchParams; expect(String(body.get("grant_type"))).toBe("refresh_token"); diff --git a/src/agents/chutes-oauth.ts b/src/agents/chutes-oauth.ts index b10a6669b0d..4890cb8e668 100644 --- a/src/agents/chutes-oauth.ts +++ b/src/agents/chutes-oauth.ts @@ -59,10 +59,7 @@ export function parseOAuthCallbackInput( } function coerceExpiresAt(expiresInSeconds: number, now: number): number { - const value = - now + - Math.max(0, Math.floor(expiresInSeconds)) * 1000 - - DEFAULT_EXPIRES_BUFFER_MS; + const value = now + Math.max(0, Math.floor(expiresInSeconds)) * 1000 - DEFAULT_EXPIRES_BUFFER_MS; return Math.max(value, now + 30_000); } @@ -122,8 +119,7 @@ export async function exchangeChutesCodeForTokens(params: { const refresh = data.refresh_token?.trim(); const expiresIn = data.expires_in ?? 0; - if (!access) - throw new Error("Chutes token exchange returned no access_token"); + if (!access) throw new Error("Chutes token exchange returned no access_token"); if (!refresh) { throw new Error("Chutes token exchange returned no refresh_token"); } @@ -153,12 +149,9 @@ export async function refreshChutesTokens(params: { throw new Error("Chutes OAuth credential is missing refresh token"); } - const clientId = - params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim(); + const clientId = params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim(); if (!clientId) { - throw new Error( - "Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).", - ); + throw new Error("Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth)."); } const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined; diff --git a/src/agents/claude-cli-runner.test.ts b/src/agents/claude-cli-runner.test.ts index a2f76254cc6..6414aecb559 100644 --- a/src/agents/claude-cli-runner.test.ts +++ b/src/agents/claude-cli-runner.test.ts @@ -18,10 +18,7 @@ function createDeferred() { }; } -async function waitForCalls( - mockFn: { mock: { calls: unknown[][] } }, - count: number, -) { +async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) { for (let i = 0; i < 50; i += 1) { if (mockFn.mock.calls.length >= count) return; await new Promise((resolve) => setTimeout(resolve, 0)); @@ -30,8 +27,7 @@ async function waitForCalls( } vi.mock("../process/exec.js", () => ({ - runCommandWithTimeout: (...args: unknown[]) => - runCommandWithTimeoutMock(...args), + runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), })); describe("runClaudeCliAgent", () => { diff --git a/src/agents/clawdbot-gateway-tool.test.ts b/src/agents/clawdbot-gateway-tool.test.ts index ea2ba8f1c0c..54b57692a2d 100644 --- a/src/agents/clawdbot-gateway-tool.test.ts +++ b/src/agents/clawdbot-gateway-tool.test.ts @@ -41,9 +41,7 @@ describe("gateway tool", () => { payload?: { kind?: string; doctorHint?: string | null }; }; expect(parsed.payload?.kind).toBe("restart"); - expect(parsed.payload?.doctorHint).toBe( - "Run: clawdbot doctor --non-interactive", - ); + expect(parsed.payload?.doctorHint).toBe("Run: clawdbot doctor --non-interactive"); expect(kill).not.toHaveBeenCalled(); await vi.runAllTimersAsync(); diff --git a/src/agents/clawdbot-tools.agents.test.ts b/src/agents/clawdbot-tools.agents.test.ts index b3d4ab76e46..5936c196c8c 100644 --- a/src/agents/clawdbot-tools.agents.test.ts +++ b/src/agents/clawdbot-tools.agents.test.ts @@ -1,8 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -41,8 +39,7 @@ describe("agents_list", () => { requester: "main", allowAny: false, }); - const agents = (result.details as { agents?: Array<{ id: string }> }) - .agents; + const agents = (result.details as { agents?: Array<{ id: string }> }).agents; expect(agents?.map((agent) => agent.id)).toEqual(["main"]); }); @@ -123,11 +120,7 @@ describe("agents_list", () => { agents?: Array<{ id: string }>; } ).agents; - expect(agents?.map((agent) => agent.id)).toEqual([ - "main", - "coder", - "research", - ]); + expect(agents?.map((agent) => agent.id)).toEqual(["main", "coder", "research"]); }); it("marks allowlisted-but-unconfigured agents", async () => { diff --git a/src/agents/clawdbot-tools.camera.test.ts b/src/agents/clawdbot-tools.camera.test.ts index 35e12b13153..dd47a6e89fc 100644 --- a/src/agents/clawdbot-tools.camera.test.ts +++ b/src/agents/clawdbot-tools.camera.test.ts @@ -35,9 +35,7 @@ describe("nodes camera_snap", () => { throw new Error(`unexpected method: ${String(method)}`); }); - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "nodes", - ); + const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes"); if (!tool) throw new Error("missing nodes tool"); const result = await tool.execute("call1", { @@ -46,9 +44,7 @@ describe("nodes camera_snap", () => { facing: "front", }); - const images = (result.content ?? []).filter( - (block) => block.type === "image", - ); + const images = (result.content ?? []).filter((block) => block.type === "image"); expect(images).toHaveLength(1); expect(images[0]?.mimeType).toBe("image/jpeg"); }); @@ -75,9 +71,7 @@ describe("nodes camera_snap", () => { throw new Error(`unexpected method: ${String(method)}`); }); - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "nodes", - ); + const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes"); if (!tool) throw new Error("missing nodes tool"); await tool.execute("call1", { @@ -118,9 +112,7 @@ describe("nodes run", () => { throw new Error(`unexpected method: ${String(method)}`); }); - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "nodes", - ); + const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes"); if (!tool) throw new Error("missing nodes tool"); await tool.execute("call1", { diff --git a/src/agents/clawdbot-tools.sessions.test.ts b/src/agents/clawdbot-tools.sessions.test.ts index 5c7e8d176f9..d795f463bb0 100644 --- a/src/agents/clawdbot-tools.sessions.test.ts +++ b/src/agents/clawdbot-tools.sessions.test.ts @@ -54,9 +54,7 @@ describe("sessions tools", () => { expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number"); expect(schemaProp("sessions_list", "messageLimit").type).toBe("number"); expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number"); - expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe( - "number", - ); + expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe("number"); expect(schemaProp("sessions_spawn", "timeoutSeconds").type).toBe("number"); }); @@ -108,9 +106,7 @@ describe("sessions tools", () => { return {}; }); - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "sessions_list", - ); + const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_list"); expect(tool).toBeDefined(); if (!tool) throw new Error("missing sessions_list tool"); @@ -147,9 +143,7 @@ describe("sessions tools", () => { return {}; }); - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "sessions_history", - ); + const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history"); expect(tool).toBeDefined(); if (!tool) throw new Error("missing sessions_history tool"); @@ -181,9 +175,7 @@ describe("sessions tools", () => { if (request.method === "agent") { agentCallCount += 1; const runId = `run-${agentCallCount}`; - const params = request.params as - | { message?: string; sessionKey?: string } - | undefined; + const params = request.params as { message?: string; sessionKey?: string } | undefined; const message = params?.message ?? ""; let reply = "REPLY_SKIP"; if (message === "ping" || message === "wait") { @@ -207,8 +199,7 @@ describe("sessions tools", () => { } if (request.method === "chat.history") { _historyCallCount += 1; - const text = - (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? ""; + const text = (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? ""; return { messages: [ { @@ -268,9 +259,7 @@ describe("sessions tools", () => { const agentCalls = calls.filter((call) => call.method === "agent"); const waitCalls = calls.filter((call) => call.method === "agent.wait"); - const historyOnlyCalls = calls.filter( - (call) => call.method === "chat.history", - ); + const historyOnlyCalls = calls.filter((call) => call.method === "chat.history"); expect(agentCalls).toHaveLength(8); for (const call of agentCalls) { expect(call.params).toMatchObject({ @@ -281,31 +270,28 @@ describe("sessions tools", () => { expect( agentCalls.some( (call) => - typeof (call.params as { extraSystemPrompt?: string }) - ?.extraSystemPrompt === "string" && - ( - call.params as { extraSystemPrompt?: string } - )?.extraSystemPrompt?.includes("Agent-to-agent message context"), + typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" && + (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes( + "Agent-to-agent message context", + ), ), ).toBe(true); expect( agentCalls.some( (call) => - typeof (call.params as { extraSystemPrompt?: string }) - ?.extraSystemPrompt === "string" && - ( - call.params as { extraSystemPrompt?: string } - )?.extraSystemPrompt?.includes("Agent-to-agent reply step"), + typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" && + (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes( + "Agent-to-agent reply step", + ), ), ).toBe(true); expect( agentCalls.some( (call) => - typeof (call.params as { extraSystemPrompt?: string }) - ?.extraSystemPrompt === "string" && - ( - call.params as { extraSystemPrompt?: string } - )?.extraSystemPrompt?.includes("Agent-to-agent announce step"), + typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" && + (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes( + "Agent-to-agent announce step", + ), ), ).toBe(true); expect(waitCalls).toHaveLength(8); @@ -339,9 +325,7 @@ describe("sessions tools", () => { if (params?.extraSystemPrompt?.includes("Agent-to-agent reply step")) { reply = params.sessionKey === requesterKey ? "pong-1" : "pong-2"; } - if ( - params?.extraSystemPrompt?.includes("Agent-to-agent announce step") - ) { + if (params?.extraSystemPrompt?.includes("Agent-to-agent announce step")) { reply = "announce now"; } replyByRunId.set(runId, reply); @@ -357,8 +341,7 @@ describe("sessions tools", () => { return { runId: params?.runId ?? "run-1", status: "ok" }; } if (request.method === "chat.history") { - const text = - (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? ""; + const text = (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? ""; return { messages: [ { @@ -414,11 +397,10 @@ describe("sessions tools", () => { const replySteps = calls.filter( (call) => call.method === "agent" && - typeof (call.params as { extraSystemPrompt?: string }) - ?.extraSystemPrompt === "string" && - ( - call.params as { extraSystemPrompt?: string } - )?.extraSystemPrompt?.includes("Agent-to-agent reply step"), + typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" && + (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes( + "Agent-to-agent reply step", + ), ); expect(replySteps).toHaveLength(2); expect(sendParams).toMatchObject({ diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts index 250e2bc6e89..3733348d94f 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-allows-cross-agent-spawning-configured.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts index 39fe5dec0f5..e8793fa6c10 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-agent-wait-lifecycle-events.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -78,9 +76,7 @@ describe("clawdbot-tools: subagents", () => { }; } if (request.method === "agent.wait") { - const params = request.params as - | { runId?: string; timeoutMs?: number } - | undefined; + const params = request.params as { runId?: string; timeoutMs?: number } | undefined; waitCalls.push(params ?? {}); return { runId: params?.runId ?? "run-1", @@ -91,8 +87,7 @@ describe("clawdbot-tools: subagents", () => { } if (request.method === "chat.history") { const params = request.params as { sessionKey?: string } | undefined; - const text = - sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; + const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; return { messages: [{ role: "assistant", content: [{ type: "text", text }] }], }; diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts index 956f08427c2..890f4cba728 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -79,17 +77,14 @@ describe("clawdbot-tools: subagents", () => { }; } if (request.method === "agent.wait") { - const params = request.params as - | { runId?: string; timeoutMs?: number } - | undefined; + const params = request.params as { runId?: string; timeoutMs?: number } | undefined; waitCalls.push(params ?? {}); const status = params?.runId === childRunId ? "timeout" : "ok"; return { runId: params?.runId ?? "run-1", status }; } if (request.method === "chat.history") { const params = request.params as { sessionKey?: string } | undefined; - const text = - sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; + const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; return { messages: [{ role: "assistant", content: [{ type: "text", text }] }], }; diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-applies-model-child-session.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-applies-model-child-session.test.ts index 6097d7bdc53..abc420f7ecf 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-applies-model-child-session.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-applies-model-child-session.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -83,9 +81,7 @@ describe("clawdbot-tools: subagents", () => { modelApplied: true, }); - const patchIndex = calls.findIndex( - (call) => call.method === "sessions.patch", - ); + const patchIndex = calls.findIndex((call) => call.method === "sessions.patch"); const agentIndex = calls.findIndex((call) => call.method === "agent"); expect(patchIndex).toBeGreaterThan(-1); expect(agentIndex).toBeGreaterThan(-1); diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts index a0d2f391bbe..1ad7e4fd60d 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts index e51cd30325a..c1afd211bd2 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-prefers-per-agent-subagent-model.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -124,9 +122,9 @@ describe("clawdbot-tools: subagents", () => { status: "accepted", modelApplied: false, }); - expect( - String((result.details as { warning?: string }).warning ?? ""), - ).toContain("invalid model"); + expect(String((result.details as { warning?: string }).warning ?? "")).toContain( + "invalid model", + ); expect(calls.some((call) => call.method === "agent")).toBe(true); }); it("sessions_spawn supports legacy timeoutSeconds alias", async () => { diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts index 6a5f1abdff1..8f74e46a122 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-resolves-main-announce-target-from.test.ts @@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); -let configOverride: ReturnType< - typeof import("../config/config.js")["loadConfig"] -> = { +let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", @@ -85,17 +83,14 @@ describe("clawdbot-tools: subagents", () => { }; } if (request.method === "agent.wait") { - const params = request.params as - | { runId?: string; timeoutMs?: number } - | undefined; + const params = request.params as { runId?: string; timeoutMs?: number } | undefined; waitCalls.push(params ?? {}); const status = params?.runId === childRunId ? "timeout" : "ok"; return { runId: params?.runId ?? "run-1", status }; } if (request.method === "chat.history") { const params = request.params as { sessionKey?: string } | undefined; - const text = - sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; + const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? ""; return { messages: [{ role: "assistant", content: [{ type: "text", text }] }], }; diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts index 87b3b17eb75..bf97483bae8 100644 --- a/src/agents/clawdbot-tools.ts +++ b/src/agents/clawdbot-tools.ts @@ -9,10 +9,7 @@ import type { AnyAgentTool } from "./tools/common.js"; import { createCronTool } from "./tools/cron-tool.js"; import { createGatewayTool } from "./tools/gateway-tool.js"; import { createImageTool } from "./tools/image-tool.js"; -import { - createMemoryGetTool, - createMemorySearchTool, -} from "./tools/memory-tool.js"; +import { createMemoryGetTool, createMemorySearchTool } from "./tools/memory-tool.js"; import { createMessageTool } from "./tools/message-tool.js"; import { createNodesTool } from "./tools/nodes-tool.js"; import { createSessionStatusTool } from "./tools/session-status-tool.js"; @@ -105,9 +102,7 @@ export function createClawdbotTools(options?: { agentSessionKey: options?.agentSessionKey, config: options?.config, }), - ...(memorySearchTool && memoryGetTool - ? [memorySearchTool, memoryGetTool] - : []), + ...(memorySearchTool && memoryGetTool ? [memorySearchTool, memoryGetTool] : []), ...(imageTool ? [imageTool] : []), ]; diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index 2462fca1600..a2fcaa8a53a 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -34,12 +34,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { modelAliases: CLAUDE_MODEL_ALIASES, sessionArg: "--session-id", sessionMode: "always", - sessionIdFields: [ - "session_id", - "sessionId", - "conversation_id", - "conversationId", - ], + sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"], systemPromptArg: "--append-system-prompt", systemPromptMode: "append", systemPromptWhen: "first", @@ -49,15 +44,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { const DEFAULT_CODEX_BACKEND: CliBackendConfig = { command: "codex", - args: [ - "exec", - "--json", - "--color", - "never", - "--sandbox", - "read-only", - "--skip-git-repo-check", - ], + args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"], resumeArgs: [ "exec", "resume", @@ -93,10 +80,7 @@ function pickBackendConfig( return undefined; } -function mergeBackendConfig( - base: CliBackendConfig, - override?: CliBackendConfig, -): CliBackendConfig { +function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig { if (!override) return { ...base }; return { ...base, @@ -104,9 +88,7 @@ function mergeBackendConfig( args: override.args ?? base.args, env: { ...base.env, ...override.env }, modelAliases: { ...base.modelAliases, ...override.modelAliases }, - clearEnv: Array.from( - new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])]), - ), + clearEnv: Array.from(new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])])), sessionIdFields: override.sessionIdFields ?? base.sessionIdFields, sessionArgs: override.sessionArgs ?? base.sessionArgs, resumeArgs: override.resumeArgs ?? base.resumeArgs, diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index 2ad424ba2ec..1a7901e342f 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -42,9 +42,7 @@ describe("cli credentials", () => { return ""; }); - const { writeClaudeCliKeychainCredentials } = await import( - "./cli-credentials.js" - ); + const { writeClaudeCliKeychainCredentials } = await import("./cli-credentials.js"); const ok = writeClaudeCliKeychainCredentials({ access: "new-access", @@ -53,13 +51,9 @@ describe("cli credentials", () => { }); expect(ok).toBe(true); - expect( - commands.some((cmd) => cmd.includes("delete-generic-password")), - ).toBe(false); + expect(commands.some((cmd) => cmd.includes("delete-generic-password"))).toBe(false); - const updateCommand = commands.find((cmd) => - cmd.includes("add-generic-password"), - ); + const updateCommand = commands.find((cmd) => cmd.includes("add-generic-password")); expect(updateCommand).toContain("-U"); }); @@ -130,9 +124,7 @@ describe("cli credentials", () => { vi.setSystemTime(new Date("2025-01-01T00:00:00Z")); - const { readClaudeCliCredentialsCached } = await import( - "./cli-credentials.js" - ); + const { readClaudeCliCredentialsCached } = await import("./cli-credentials.js"); const first = readClaudeCliCredentialsCached({ allowKeychainPrompt: true, @@ -163,9 +155,7 @@ describe("cli credentials", () => { vi.setSystemTime(new Date("2025-01-01T00:00:00Z")); - const { readClaudeCliCredentialsCached } = await import( - "./cli-credentials.js" - ); + const { readClaudeCliCredentialsCached } = await import("./cli-credentials.js"); const first = readClaudeCliCredentialsCached({ allowKeychainPrompt: true, diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index 2178d6b0a2a..0a150f925db 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -56,10 +56,7 @@ type ClaudeCliFileOptions = { type ClaudeCliWriteOptions = ClaudeCliFileOptions & { platform?: NodeJS.Platform; writeKeychain?: (credentials: OAuthCredentials) => boolean; - writeFile?: ( - credentials: OAuthCredentials, - options?: ClaudeCliFileOptions, - ) => boolean; + writeFile?: (credentials: OAuthCredentials, options?: ClaudeCliFileOptions) => boolean; }; function resolveClaudeCliCredentialsPath(homeDir?: string) { @@ -73,9 +70,7 @@ function resolveCodexCliAuthPath() { function resolveCodexHomePath() { const configured = process.env.CODEX_HOME; - const home = configured - ? resolveUserPath(configured) - : resolveUserPath("~/.codex"); + const home = configured ? resolveUserPath(configured) : resolveUserPath("~/.codex"); try { return fs.realpathSync.native(home); } catch { @@ -98,10 +93,11 @@ function readCodexKeychainCredentials(options?: { const account = computeCodexKeychainAccount(codexHome); try { - const secret = execSync( - `security find-generic-password -s "Codex Auth" -a "${account}" -w`, - { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }, - ).trim(); + const secret = execSync(`security find-generic-password -s "Codex Auth" -a "${account}" -w`, { + encoding: "utf8", + timeout: 5000, + stdio: ["pipe", "pipe", "pipe"], + }).trim(); const parsed = JSON.parse(secret) as Record; const tokens = parsed.tokens as Record | undefined; @@ -253,9 +249,7 @@ export function readClaudeCliCredentialsCached(options?: { return value; } -export function writeClaudeCliKeychainCredentials( - newCredentials: OAuthCredentials, -): boolean { +export function writeClaudeCliKeychainCredentials(newCredentials: OAuthCredentials): boolean { try { const existingResult = execSync( `security find-generic-password -s "${CLAUDE_CLI_KEYCHAIN_SERVICE}" -w 2>/dev/null`, @@ -309,9 +303,7 @@ export function writeClaudeCliFileCredentials( if (!raw || typeof raw !== "object") return false; const data = raw as Record; - const existingOauth = data.claudeAiOauth as - | Record - | undefined; + const existingOauth = data.claudeAiOauth as Record | undefined; if (!existingOauth || typeof existingOauth !== "object") return false; data.claudeAiOauth = { @@ -339,12 +331,10 @@ export function writeClaudeCliCredentials( options?: ClaudeCliWriteOptions, ): boolean { const platform = options?.platform ?? process.platform; - const writeKeychain = - options?.writeKeychain ?? writeClaudeCliKeychainCredentials; + const writeKeychain = options?.writeKeychain ?? writeClaudeCliKeychainCredentials; const writeFile = options?.writeFile ?? - ((credentials, fileOptions) => - writeClaudeCliFileCredentials(credentials, fileOptions)); + ((credentials, fileOptions) => writeClaudeCliFileCredentials(credentials, fileOptions)); if (platform === "darwin") { const didWriteKeychain = writeKeychain(newCredentials); diff --git a/src/agents/cli-runner.test.ts b/src/agents/cli-runner.test.ts index 5a215f7c833..af0e2719236 100644 --- a/src/agents/cli-runner.test.ts +++ b/src/agents/cli-runner.test.ts @@ -6,8 +6,7 @@ const runCommandWithTimeoutMock = vi.fn(); const runExecMock = vi.fn(); vi.mock("../process/exec.js", () => ({ - runCommandWithTimeout: (...args: unknown[]) => - runCommandWithTimeoutMock(...args), + runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), runExec: (...args: unknown[]) => runExecMock(...args), })); diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index 3d24152b1fb..63799aef35a 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -30,10 +30,7 @@ import { resolveBootstrapMaxChars, } from "./pi-embedded-helpers.js"; import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js"; -import { - filterBootstrapFilesForSession, - loadWorkspaceBootstrapFiles, -} from "./workspace.js"; +import { filterBootstrapFilesForSession, loadWorkspaceBootstrapFiles } from "./workspace.js"; const log = createSubsystemLogger("agent/claude-cli"); @@ -58,10 +55,7 @@ export async function runCliAgent(params: { const resolvedWorkspace = resolveUserPath(params.workspaceDir); const workspaceDir = resolvedWorkspace; - const backendResolved = resolveCliBackendConfig( - params.provider, - params.config, - ); + const backendResolved = resolveCliBackendConfig(params.provider, params.config); if (!backendResolved) { throw new Error(`Unknown CLI backend: ${params.provider}`); } @@ -92,9 +86,7 @@ export async function runCliAgent(params: { }); const heartbeatPrompt = sessionAgentId === defaultAgentId - ? resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ) + ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) : undefined; const systemPrompt = buildSystemPrompt({ workspaceDir, @@ -114,14 +106,12 @@ export async function runCliAgent(params: { }); const useResume = Boolean( params.cliSessionId && - cliSessionIdToSend && - backend.resumeArgs && - backend.resumeArgs.length > 0, + cliSessionIdToSend && + backend.resumeArgs && + backend.resumeArgs.length > 0, ); const sessionIdSent = cliSessionIdToSend - ? useResume || - Boolean(backend.sessionArg) || - Boolean(backend.sessionArgs?.length) + ? useResume || Boolean(backend.sessionArg) || Boolean(backend.sessionArgs?.length) ? cliSessionIdToSend : undefined : undefined; @@ -148,13 +138,9 @@ export async function runCliAgent(params: { prompt, }); const stdinPayload = stdin ?? ""; - const baseArgs = useResume - ? (backend.resumeArgs ?? backend.args ?? []) - : (backend.args ?? []); + const baseArgs = useResume ? (backend.resumeArgs ?? backend.args ?? []) : (backend.args ?? []); const resolvedArgs = useResume - ? baseArgs.map((entry) => - entry.replaceAll("{sessionId}", cliSessionIdToSend ?? ""), - ) + ? baseArgs.map((entry) => entry.replaceAll("{sessionId}", cliSessionIdToSend ?? "")) : baseArgs; const args = buildCliArgs({ backend, @@ -168,9 +154,7 @@ export async function runCliAgent(params: { }); const serialize = backend.serialize ?? true; - const queueKey = serialize - ? backendResolved.id - : `${backendResolved.id}:${params.runId}`; + const queueKey = serialize ? backendResolved.id : `${backendResolved.id}:${params.runId}`; try { const output = await enqueueCliRun(queueKey, async () => { @@ -184,10 +168,7 @@ export async function runCliAgent(params: { const arg = args[i] ?? ""; if (arg === backend.systemPromptArg) { const systemPromptValue = args[i + 1] ?? ""; - logArgs.push( - arg, - ``, - ); + logArgs.push(arg, ``); i += 1; continue; } @@ -259,9 +240,7 @@ export async function runCliAgent(params: { }); } - const outputMode = useResume - ? (backend.resumeOutput ?? backend.output) - : backend.output; + const outputMode = useResume ? (backend.resumeOutput ?? backend.output) : backend.output; if (outputMode === "text") { return { text: stdout, sessionId: undefined }; @@ -283,8 +262,7 @@ export async function runCliAgent(params: { meta: { durationMs: Date.now() - started, agentMeta: { - sessionId: - output.sessionId ?? sessionIdSent ?? params.sessionId ?? "", + sessionId: output.sessionId ?? sessionIdSent ?? params.sessionId ?? "", provider: params.provider, model: modelId, usage: output.usage, diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index f3b803716cf..268251fb3fd 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -29,9 +29,7 @@ export async function cleanupResumeProcesses( const commandToken = path.basename(backend.command ?? "").trim(); if (!commandToken) return; - const resumeTokens = resumeArgs.map((arg) => - arg.replaceAll("{sessionId}", sessionId), - ); + const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId)); const pattern = [commandToken, ...resumeTokens] .filter(Boolean) .map((token) => escapeRegex(token)) @@ -45,10 +43,7 @@ export async function cleanupResumeProcesses( } } -export function enqueueCliRun( - key: string, - task: () => Promise, -): Promise { +export function enqueueCliRun(key: string, task: () => Promise): Promise { const prior = CLI_RUN_QUEUE.get(key) ?? Promise.resolve(); const chained = prior.catch(() => undefined).then(task); const tracked = chained.finally(() => { @@ -78,9 +73,7 @@ function resolveUserTimezone(configured?: string): string { const trimmed = configured?.trim(); if (trimmed) { try { - new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format( - new Date(), - ); + new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date()); return trimmed; } catch { // ignore invalid timezone @@ -106,14 +99,7 @@ function formatUserTime(date: Date, timeZone: string): string | undefined { for (const part of parts) { if (part.type !== "literal") map[part.type] = part.value; } - if ( - !map.weekday || - !map.year || - !map.month || - !map.day || - !map.hour || - !map.minute - ) + if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) return undefined; return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`; } catch { @@ -127,9 +113,7 @@ function buildModelAliasLines(cfg?: ClawdbotConfig) { for (const [keyRaw, entryRaw] of Object.entries(models)) { const model = String(keyRaw ?? "").trim(); if (!model) continue; - const alias = String( - (entryRaw as { alias?: string } | undefined)?.alias ?? "", - ).trim(); + const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); if (!alias) continue; entries.push({ alias, model }); } @@ -149,9 +133,7 @@ export function buildSystemPrompt(params: { contextFiles?: EmbeddedContextFile[]; modelDisplay: string; }) { - const userTimezone = resolveUserTimezone( - params.config?.agents?.defaults?.userTimezone, - ); + const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); const userTime = formatUserTime(new Date(), userTimezone); return buildAgentSystemPrompt({ workspaceDir: params.workspaceDir, @@ -175,10 +157,7 @@ export function buildSystemPrompt(params: { }); } -export function normalizeCliModel( - modelId: string, - backend: CliBackendConfig, -): string { +export function normalizeCliModel(modelId: string, backend: CliBackendConfig): string { const trimmed = modelId.trim(); if (!trimmed) return trimmed; const direct = backend.modelAliases?.[trimmed]; @@ -191,19 +170,14 @@ export function normalizeCliModel( function toUsage(raw: Record): CliUsage | undefined { const pick = (key: string) => - typeof raw[key] === "number" && raw[key] > 0 - ? (raw[key] as number) - : undefined; + typeof raw[key] === "number" && raw[key] > 0 ? (raw[key] as number) : undefined; const input = pick("input_tokens") ?? pick("inputTokens"); const output = pick("output_tokens") ?? pick("outputTokens"); const cacheRead = - pick("cache_read_input_tokens") ?? - pick("cached_input_tokens") ?? - pick("cacheRead"); + pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead"); const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite"); const total = pick("total_tokens") ?? pick("total"); - if (!input && !output && !cacheRead && !cacheWrite && !total) - return undefined; + if (!input && !output && !cacheRead && !cacheWrite && !total) return undefined; return { input, output, cacheRead, cacheWrite, total }; } @@ -214,8 +188,7 @@ function isRecord(value: unknown): value is Record { function collectText(value: unknown): string { if (!value) return ""; if (typeof value === "string") return value; - if (Array.isArray(value)) - return value.map((entry) => collectText(entry)).join(""); + if (Array.isArray(value)) return value.map((entry) => collectText(entry)).join(""); if (!isRecord(value)) return ""; if (typeof value.text === "string") return value.text; if (typeof value.content === "string") return value.content; @@ -242,10 +215,7 @@ function pickSessionId( return undefined; } -export function parseCliJson( - raw: string, - backend: CliBackendConfig, -): CliOutput | null { +export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput | null { const trimmed = raw.trim(); if (!trimmed) return null; let parsed: unknown; @@ -265,10 +235,7 @@ export function parseCliJson( return { text: text.trim(), sessionId, usage }; } -export function parseCliJsonl( - raw: string, - backend: CliBackendConfig, -): CliOutput | null { +export function parseCliJsonl(raw: string, backend: CliBackendConfig): CliOutput | null { const lines = raw .split(/\r?\n/g) .map((line) => line.trim()) @@ -331,18 +298,15 @@ export function resolveSessionIdToSend(params: { return { sessionId: crypto.randomUUID(), isNew: true }; } -export function resolvePromptInput(params: { - backend: CliBackendConfig; - prompt: string; -}): { argsPrompt?: string; stdin?: string } { +export function resolvePromptInput(params: { backend: CliBackendConfig; prompt: string }): { + argsPrompt?: string; + stdin?: string; +} { const inputMode = params.backend.input ?? "arg"; if (inputMode === "stdin") { return { stdin: params.prompt }; } - if ( - params.backend.maxPromptArgChars && - params.prompt.length > params.backend.maxPromptArgChars - ) { + if (params.backend.maxPromptArgChars && params.prompt.length > params.backend.maxPromptArgChars) { return { stdin: params.prompt }; } return { argsPrompt: params.prompt }; @@ -357,10 +321,7 @@ function resolveImageExtension(mimeType: string): string { return "bin"; } -export function appendImagePathsToPrompt( - prompt: string, - paths: string[], -): string { +export function appendImagePathsToPrompt(prompt: string, paths: string[]): string { if (!paths.length) return prompt; const trimmed = prompt.trimEnd(); const separator = trimmed ? "\n\n" : ""; @@ -370,9 +331,7 @@ export function appendImagePathsToPrompt( export async function writeCliImages( images: ImageContent[], ): Promise<{ paths: string[]; cleanup: () => Promise }> { - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-cli-images-"), - ); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-cli-images-")); const paths: string[] = []; for (let i = 0; i < images.length; i += 1) { const image = images[i]; @@ -402,11 +361,7 @@ export function buildCliArgs(params: { if (!params.useResume && params.backend.modelArg && params.modelId) { args.push(params.backend.modelArg, params.modelId); } - if ( - !params.useResume && - params.systemPrompt && - params.backend.systemPromptArg - ) { + if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) { args.push(params.backend.systemPromptArg, params.systemPrompt); } if (!params.useResume && params.sessionId) { diff --git a/src/agents/cli-session.ts b/src/agents/cli-session.ts index dd5ab54f4e0..e1df7cf9b2b 100644 --- a/src/agents/cli-session.ts +++ b/src/agents/cli-session.ts @@ -16,11 +16,7 @@ export function getCliSessionId( return undefined; } -export function setCliSessionId( - entry: SessionEntry, - provider: string, - sessionId: string, -): void { +export function setCliSessionId(entry: SessionEntry, provider: string, sessionId: string): void { const normalized = normalizeProviderId(provider); const trimmed = sessionId.trim(); if (!trimmed) return; diff --git a/src/agents/context-window-guard.ts b/src/agents/context-window-guard.ts index 952d6a4d54f..47f3c052156 100644 --- a/src/agents/context-window-guard.ts +++ b/src/agents/context-window-guard.ts @@ -3,11 +3,7 @@ import type { ClawdbotConfig } from "../config/config.js"; export const CONTEXT_WINDOW_HARD_MIN_TOKENS = 16_000; export const CONTEXT_WINDOW_WARN_BELOW_TOKENS = 32_000; -export type ContextWindowSource = - | "model" - | "modelsConfig" - | "agentContextTokens" - | "default"; +export type ContextWindowSource = "model" | "modelsConfig" | "agentContextTokens" | "default"; export type ContextWindowInfo = { tokens: number; @@ -32,26 +28,17 @@ export function resolveContextWindowInfo(params: { const fromModelsConfig = (() => { const providers = params.cfg?.models?.providers as - | Record< - string, - { models?: Array<{ id?: string; contextWindow?: number }> } - > + | Record }> | undefined; const providerEntry = providers?.[params.provider]; - const models = Array.isArray(providerEntry?.models) - ? providerEntry.models - : []; + const models = Array.isArray(providerEntry?.models) ? providerEntry.models : []; const match = models.find((m) => m?.id === params.modelId); return normalizePositiveInt(match?.contextWindow); })(); - if (fromModelsConfig) - return { tokens: fromModelsConfig, source: "modelsConfig" }; + if (fromModelsConfig) return { tokens: fromModelsConfig, source: "modelsConfig" }; - const fromAgentConfig = normalizePositiveInt( - params.cfg?.agents?.defaults?.contextTokens, - ); - if (fromAgentConfig) - return { tokens: fromAgentConfig, source: "agentContextTokens" }; + const fromAgentConfig = normalizePositiveInt(params.cfg?.agents?.defaults?.contextTokens); + if (fromAgentConfig) return { tokens: fromAgentConfig, source: "agentContextTokens" }; return { tokens: Math.floor(params.defaultTokens), source: "default" }; } @@ -70,10 +57,7 @@ export function evaluateContextWindowGuard(params: { 1, Math.floor(params.warnBelowTokens ?? CONTEXT_WINDOW_WARN_BELOW_TOKENS), ); - const hardMin = Math.max( - 1, - Math.floor(params.hardMinTokens ?? CONTEXT_WINDOW_HARD_MIN_TOKENS), - ); + const hardMin = Math.max(1, Math.floor(params.hardMinTokens ?? CONTEXT_WINDOW_HARD_MIN_TOKENS)); const tokens = Math.max(0, Math.floor(params.info.tokens)); return { ...params.info, diff --git a/src/agents/context.ts b/src/agents/context.ts index d790d640032..17bba116034 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -10,9 +10,7 @@ type ModelEntry = { id: string; contextWindow?: number }; const MODEL_CACHE = new Map(); const loadPromise = (async () => { try { - const { discoverAuthStorage, discoverModels } = await import( - "@mariozechner/pi-coding-agent" - ); + const { discoverAuthStorage, discoverModels } = await import("@mariozechner/pi-coding-agent"); const cfg = loadConfig(); await ensureClawdbotModelsJson(cfg); const agentDir = resolveClawdbotAgentDir(); diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index 3bb59cbdd69..a43ae289fd9 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -8,9 +8,7 @@ import { describe("failover-error", () => { it("infers failover reason from HTTP status", () => { expect(resolveFailoverReasonFromError({ status: 402 })).toBe("billing"); - expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe( - "rate_limit", - ); + expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe("rate_limit"); expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth"); expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout"); }); @@ -24,12 +22,8 @@ describe("failover-error", () => { }); it("infers timeout from common node error codes", () => { - expect(resolveFailoverReasonFromError({ code: "ETIMEDOUT" })).toBe( - "timeout", - ); - expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe( - "timeout", - ); + expect(resolveFailoverReasonFromError({ code: "ETIMEDOUT" })).toBe("timeout"); + expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe("timeout"); }); it("coerces failover-worthy errors into FailoverError with metadata", () => { diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index 2a9113f61df..766affcd102 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -1,7 +1,4 @@ -import { - classifyFailoverReason, - type FailoverReason, -} from "./pi-embedded-helpers.js"; +import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js"; export class FailoverError extends Error { readonly reason: FailoverReason; @@ -38,9 +35,7 @@ export function isFailoverError(err: unknown): err is FailoverError { return err instanceof FailoverError; } -export function resolveFailoverStatus( - reason: FailoverReason, -): number | undefined { +export function resolveFailoverStatus(reason: FailoverReason): number | undefined { switch (reason) { case "billing": return 402; @@ -80,11 +75,7 @@ function getErrorCode(err: unknown): string | undefined { function getErrorMessage(err: unknown): string { if (err instanceof Error) return err.message; if (typeof err === "string") return err; - if ( - typeof err === "number" || - typeof err === "boolean" || - typeof err === "bigint" - ) { + if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { return String(err); } if (typeof err === "symbol") return err.description ?? ""; @@ -95,9 +86,7 @@ function getErrorMessage(err: unknown): string { return ""; } -export function resolveFailoverReasonFromError( - err: unknown, -): FailoverReason | null { +export function resolveFailoverReasonFromError(err: unknown): FailoverReason | null { if (isFailoverError(err)) return err.reason; const status = getStatusCode(err); @@ -107,11 +96,7 @@ export function resolveFailoverReasonFromError( if (status === 408) return "timeout"; const code = (getErrorCode(err) ?? "").toUpperCase(); - if ( - ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes( - code, - ) - ) { + if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(code)) { return "timeout"; } diff --git a/src/agents/identity.ts b/src/agents/identity.ts index fd634378c6d..6ac80da2fea 100644 --- a/src/agents/identity.ts +++ b/src/agents/identity.ts @@ -1,8 +1,4 @@ -import type { - ClawdbotConfig, - HumanDelayConfig, - IdentityConfig, -} from "../config/config.js"; +import type { ClawdbotConfig, HumanDelayConfig, IdentityConfig } from "../config/config.js"; import { resolveAgentConfig } from "./agent-scope.js"; const DEFAULT_ACK_REACTION = "👀"; @@ -14,10 +10,7 @@ export function resolveAgentIdentity( return resolveAgentConfig(cfg, agentId)?.identity; } -export function resolveAckReaction( - cfg: ClawdbotConfig, - agentId: string, -): string { +export function resolveAckReaction(cfg: ClawdbotConfig, agentId: string): string { const configured = cfg.messages?.ackReaction; if (configured !== undefined) return configured.trim(); const emoji = resolveAgentIdentity(cfg, agentId)?.emoji?.trim(); @@ -44,15 +37,10 @@ export function resolveMessagePrefix( const hasAllowFrom = opts?.hasAllowFrom === true; if (hasAllowFrom) return ""; - return ( - resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[clawdbot]" - ); + return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[clawdbot]"; } -export function resolveResponsePrefix( - cfg: ClawdbotConfig, - agentId: string, -): string | undefined { +export function resolveResponsePrefix(cfg: ClawdbotConfig, agentId: string): string | undefined { const configured = cfg.messages?.responsePrefix; if (configured !== undefined) { if (configured === "auto") { diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 3ea349c5923..155fdd81ea6 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -3,11 +3,7 @@ export type ModelRef = { id?: string | null; }; -const ANTHROPIC_PREFIXES = [ - "claude-opus-4-5", - "claude-sonnet-4-5", - "claude-haiku-4-5", -]; +const ANTHROPIC_PREFIXES = ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"]; const OPENAI_MODELS = ["gpt-5.2", "gpt-5.0"]; const CODEX_MODELS = [ "gpt-5.2", @@ -55,10 +51,7 @@ export function isModernModelRef(ref: ModelRef): boolean { } if (provider === "google-antigravity") { - return ( - matchesPrefix(id, GOOGLE_PREFIXES) || - matchesPrefix(id, ANTHROPIC_PREFIXES) - ); + return matchesPrefix(id, GOOGLE_PREFIXES) || matchesPrefix(id, ANTHROPIC_PREFIXES); } if (provider === "zai") { diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index 400a7c05503..842605e069f 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -52,9 +52,7 @@ function resolveStorePath(agentId: string, raw?: string): string { const stateDir = resolveStateDir(process.env, os.homedir); const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`); if (!raw) return fallback; - const withToken = raw.includes("{agentId}") - ? raw.replaceAll("{agentId}", agentId) - : raw; + const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw; return resolveUserPath(withToken); } @@ -77,47 +75,29 @@ function mergeConfig( const model = overrides?.model ?? defaults?.model ?? DEFAULT_MODEL; const local = { modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath, - modelCacheDir: - overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir, + modelCacheDir: overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir, }; const store = { driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite", - path: resolveStorePath( - agentId, - overrides?.store?.path ?? defaults?.store?.path, - ), + path: resolveStorePath(agentId, overrides?.store?.path ?? defaults?.store?.path), }; const chunking = { - tokens: - overrides?.chunking?.tokens ?? - defaults?.chunking?.tokens ?? - DEFAULT_CHUNK_TOKENS, - overlap: - overrides?.chunking?.overlap ?? - defaults?.chunking?.overlap ?? - DEFAULT_CHUNK_OVERLAP, + tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS, + overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP, }; const sync = { - onSessionStart: - overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true, + onSessionStart: overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true, onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true, watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true, watchDebounceMs: overrides?.sync?.watchDebounceMs ?? defaults?.sync?.watchDebounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS, - intervalMinutes: - overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0, + intervalMinutes: overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0, }; const query = { - maxResults: - overrides?.query?.maxResults ?? - defaults?.query?.maxResults ?? - DEFAULT_MAX_RESULTS, - minScore: - overrides?.query?.minScore ?? - defaults?.query?.minScore ?? - DEFAULT_MIN_SCORE, + maxResults: overrides?.query?.maxResults ?? defaults?.query?.maxResults ?? DEFAULT_MAX_RESULTS, + minScore: overrides?.query?.minScore ?? defaults?.query?.minScore ?? DEFAULT_MIN_SCORE, }; const overlap = Math.max(0, Math.min(chunking.overlap, chunking.tokens - 1)); diff --git a/src/agents/minimax-vlm.ts b/src/agents/minimax-vlm.ts index 0c7fd6ab0e1..4138f3a2d67 100644 --- a/src/agents/minimax-vlm.ts +++ b/src/agents/minimax-vlm.ts @@ -51,9 +51,7 @@ export async function minimaxUnderstandImage(params: { const imageDataUrl = params.imageDataUrl.trim(); if (!imageDataUrl) throw new Error("MiniMax VLM: imageDataUrl required"); if (!/^data:image\/(png|jpeg|webp);base64,/i.test(imageDataUrl)) { - throw new Error( - "MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL", - ); + throw new Error("MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL"); } const host = coerceApiHost({ @@ -92,17 +90,12 @@ export async function minimaxUnderstandImage(params: { throw new Error(`MiniMax VLM response was not JSON.${trace}`); } - const baseResp = isRecord(json.base_resp) - ? (json.base_resp as MinimaxBaseResp) - : {}; - const code = - typeof baseResp.status_code === "number" ? baseResp.status_code : -1; + const baseResp = isRecord(json.base_resp) ? (json.base_resp as MinimaxBaseResp) : {}; + const code = typeof baseResp.status_code === "number" ? baseResp.status_code : -1; if (code !== 0) { const msg = (baseResp.status_msg ?? "").trim(); const trace = traceId ? ` Trace-Id: ${traceId}` : ""; - throw new Error( - `MiniMax VLM API error (${code})${msg ? `: ${msg}` : ""}.${trace}`, - ); + throw new Error(`MiniMax VLM API error (${code})${msg ? `: ${msg}` : ""}.${trace}`); } const content = pickString(json, "content").trim(); diff --git a/src/agents/minimax.live.test.ts b/src/agents/minimax.live.test.ts index 430268d145b..c0f491de12e 100644 --- a/src/agents/minimax.live.test.ts +++ b/src/agents/minimax.live.test.ts @@ -2,8 +2,7 @@ import { completeSimple, type Model } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? ""; -const MINIMAX_BASE_URL = - process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic"; +const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic"; const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1"; const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1"; diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index bc08440b260..5918edede68 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -107,11 +107,7 @@ describe("getApiKeyForModel", () => { process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agent"); process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; - const authProfilesPath = path.join( - tempDir, - "agent", - "auth-profiles.json", - ); + const authProfilesPath = path.join(tempDir, "agent", "auth-profiles.json"); await fs.mkdir(path.dirname(authProfilesPath), { recursive: true, mode: 0o700, diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index d1c71ff0150..be59e42f051 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -11,10 +11,7 @@ import { } from "./auth-profiles.js"; import { normalizeProviderId } from "./model-selection.js"; -export { - ensureAuthProfileStore, - resolveAuthProfileOrder, -} from "./auth-profiles.js"; +export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js"; export function getCustomProviderApiKey( cfg: ClawdbotConfig | undefined, @@ -109,16 +106,12 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { const pick = (envVar: string): EnvApiKeyResult | null => { const value = process.env[envVar]?.trim(); if (!value) return null; - const source = applied.has(envVar) - ? `shell env: ${envVar}` - : `env: ${envVar}`; + const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`; return { apiKey: value, source }; }; if (normalized === "github-copilot") { - return ( - pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN") - ); + return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN"); } if (normalized === "anthropic") { diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index 9cb8929e08c..c7bb01fd9aa 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -58,8 +58,7 @@ export async function loadModelCatalog(params?: { typeof entry?.contextWindow === "number" && entry.contextWindow > 0 ? entry.contextWindow : undefined; - const reasoning = - typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined; + const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined; models.push({ id, name, provider, contextWindow, reasoning }); } diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index ea846eabbe1..160deb7e10f 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -20,10 +20,7 @@ function makeCfg(overrides: Partial = {}): ClawdbotConfig { describe("runWithModelFallback", () => { it("does not fall back on non-auth errors", async () => { const cfg = makeCfg(); - const run = vi - .fn() - .mockRejectedValueOnce(new Error("bad request")) - .mockResolvedValueOnce("ok"); + const run = vi.fn().mockRejectedValueOnce(new Error("bad request")).mockResolvedValueOnce("ok"); await expect( runWithModelFallback({ @@ -60,9 +57,7 @@ describe("runWithModelFallback", () => { const cfg = makeCfg(); const run = vi .fn() - .mockRejectedValueOnce( - Object.assign(new Error("payment required"), { status: 402 }), - ) + .mockRejectedValueOnce(Object.assign(new Error("payment required"), { status: 402 })) .mockResolvedValueOnce("ok"); const result = await runWithModelFallback({ @@ -106,9 +101,7 @@ describe("runWithModelFallback", () => { const cfg = makeCfg(); const run = vi .fn() - .mockRejectedValueOnce( - new Error('No credentials found for profile "anthropic:claude-cli".'), - ) + .mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:claude-cli".')) .mockResolvedValueOnce("ok"); const result = await runWithModelFallback({ @@ -136,9 +129,7 @@ describe("runWithModelFallback", () => { }); const run = vi .fn() - .mockImplementation(() => - Promise.reject(Object.assign(new Error("nope"), { status: 401 })), - ); + .mockImplementation(() => Promise.reject(Object.assign(new Error("nope"), { status: 401 }))); await expect( runWithModelFallback({ @@ -219,9 +210,7 @@ describe("runWithModelFallback", () => { }), ).rejects.toThrow("primary failed"); - expect(calls).toEqual([ - { provider: "anthropic", model: "claude-opus-4-5" }, - ]); + expect(calls).toEqual([{ provider: "anthropic", model: "claude-opus-4-5" }]); }); it("falls back on missing API key errors", async () => { @@ -277,9 +266,7 @@ describe("runWithModelFallback", () => { }); const run = vi .fn() - .mockRejectedValueOnce( - Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }), - ) + .mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })) .mockResolvedValueOnce("ok"); const result = await runWithModelFallback({ diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index b613243880c..f3d7e7d5274 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -1,10 +1,6 @@ import type { ClawdbotConfig } from "../config/config.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; -import { - coerceToFailoverError, - describeFailoverError, - isFailoverError, -} from "./failover-error.js"; +import { coerceToFailoverError, describeFailoverError, isFailoverError } from "./failover-error.js"; import { buildModelAliasIndex, modelKey, @@ -33,9 +29,7 @@ function isAbortError(err: unknown): boolean { const name = "name" in err ? String(err.name) : ""; if (name === "AbortError") return true; const message = - "message" in err && typeof err.message === "string" - ? err.message.toLowerCase() - : ""; + "message" in err && typeof err.message === "string" ? err.message.toLowerCase() : ""; return message.includes("aborted"); } @@ -70,10 +64,7 @@ function resolveImageFallbackCandidates(params: { const seen = new Set(); const candidates: ModelCandidate[] = []; - const addCandidate = ( - candidate: ModelCandidate, - enforceAllowlist: boolean, - ) => { + const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => { if (!candidate.provider || !candidate.model) return; const key = modelKey(candidate.provider, candidate.model); if (seen.has(key)) return; @@ -99,8 +90,7 @@ function resolveImageFallbackCandidates(params: { | { primary?: string } | string | undefined; - const primary = - typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary; + const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary; if (primary?.trim()) addRaw(primary, false); } @@ -146,10 +136,7 @@ function resolveFallbackCandidates(params: { const seen = new Set(); const candidates: ModelCandidate[] = []; - const addCandidate = ( - candidate: ModelCandidate, - enforceAllowlist: boolean, - ) => { + const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => { if (!candidate.provider || !candidate.model) return; const key = modelKey(candidate.provider, candidate.model); if (seen.has(key)) return; @@ -180,11 +167,7 @@ function resolveFallbackCandidates(params: { addCandidate(resolved.ref, true); } - if ( - params.fallbacksOverride === undefined && - primary?.provider && - primary.model - ) { + if (params.fallbacksOverride === undefined && primary?.provider && primary.model) { addCandidate({ provider: primary.provider, model: primary.model }, false); } @@ -271,10 +254,9 @@ export async function runWithModelFallback(params: { ) .join(" | ") : "unknown"; - throw new Error( - `All models failed (${attempts.length || candidates.length}): ${summary}`, - { cause: lastError instanceof Error ? lastError : undefined }, - ); + throw new Error(`All models failed (${attempts.length || candidates.length}): ${summary}`, { + cause: lastError instanceof Error ? lastError : undefined, + }); } export async function runWithImageModelFallback(params: { @@ -340,14 +322,10 @@ export async function runWithImageModelFallback(params: { const summary = attempts.length > 0 ? attempts - .map( - (attempt) => - `${attempt.provider}/${attempt.model}: ${attempt.error}`, - ) + .map((attempt) => `${attempt.provider}/${attempt.model}: ${attempt.error}`) .join(" | ") : "unknown"; - throw new Error( - `All image models failed (${attempts.length || candidates.length}): ${summary}`, - { cause: lastError instanceof Error ? lastError : undefined }, - ); + throw new Error(`All image models failed (${attempts.length || candidates.length}): ${summary}`, { + cause: lastError instanceof Error ? lastError : undefined, + }); } diff --git a/src/agents/model-scan.ts b/src/agents/model-scan.ts index 664eb22a165..ba477537260 100644 --- a/src/agents/model-scan.ts +++ b/src/agents/model-scan.ts @@ -79,11 +79,7 @@ export type OpenRouterScanOptions = { maxAgeDays?: number; providerFilter?: string; probe?: boolean; - onProgress?: (update: { - phase: "catalog" | "probe"; - completed: number; - total: number; - }) => void; + onProgress?: (update: { phase: "catalog" | "probe"; completed: number; total: number }) => void; }; type OpenAIModel = Model<"openai-completions">; @@ -97,9 +93,7 @@ function normalizeCreatedAtMs(value: unknown): number | null { function inferParamBFromIdOrName(text: string): number | null { const raw = text.toLowerCase(); - const matches = raw.matchAll( - /(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g, - ); + const matches = raw.matchAll(/(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g); let best: number | null = null; for (const match of matches) { const numRaw = match[1]; @@ -169,9 +163,7 @@ async function withTimeout( } } -async function fetchOpenRouterModels( - fetchImpl: typeof fetch, -): Promise { +async function fetchOpenRouterModels(fetchImpl: typeof fetch): Promise { const res = await fetchImpl(OPENROUTER_MODELS_URL, { headers: { Accept: "application/json" }, }); @@ -187,21 +179,17 @@ async function fetchOpenRouterModels( const obj = entry as Record; const id = typeof obj.id === "string" ? obj.id.trim() : ""; if (!id) return null; - const name = - typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; + const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; const contextLength = - typeof obj.context_length === "number" && - Number.isFinite(obj.context_length) + typeof obj.context_length === "number" && Number.isFinite(obj.context_length) ? obj.context_length : null; const maxCompletionTokens = - typeof obj.max_completion_tokens === "number" && - Number.isFinite(obj.max_completion_tokens) + typeof obj.max_completion_tokens === "number" && Number.isFinite(obj.max_completion_tokens) ? obj.max_completion_tokens - : typeof obj.max_output_tokens === "number" && - Number.isFinite(obj.max_output_tokens) + : typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens) ? obj.max_output_tokens : null; @@ -216,9 +204,7 @@ async function fetchOpenRouterModels( const supportsToolsMeta = supportedParameters.includes("tools"); const modality = - typeof obj.modality === "string" && obj.modality.trim() - ? obj.modality.trim() - : null; + typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null; const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`); const createdAtMs = normalizeCreatedAtMs(obj.created_at); @@ -268,9 +254,7 @@ async function probeTool( } satisfies OpenAICompletionsOptions), ); - const hasToolCall = message.content.some( - (block) => block.type === "toolCall", - ); + const hasToolCall = message.content.some((block) => block.type === "toolCall"); if (!hasToolCall) { return { ok: false, @@ -361,9 +345,7 @@ async function mapWithConcurrency( return results; } - await Promise.all( - Array.from({ length: Math.min(limit, items.length) }, () => worker()), - ); + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker())); return results; } @@ -374,19 +356,11 @@ export async function scanOpenRouterModels( const probe = options.probe ?? true; const apiKey = options.apiKey?.trim() || getEnvApiKey("openrouter") || ""; if (probe && !apiKey) { - throw new Error( - "Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan.", - ); + throw new Error("Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan."); } - const timeoutMs = Math.max( - 1, - Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS), - ); - const concurrency = Math.max( - 1, - Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY), - ); + const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS)); + const concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY)); const minParamB = Math.max(0, Math.floor(options.minParamB ?? 0)); const maxAgeDays = Math.max(0, Math.floor(options.maxAgeDays ?? 0)); const providerFilter = options.providerFilter?.trim().toLowerCase() ?? ""; diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index c054d64b87e..391aee3186b 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -39,9 +39,7 @@ describe("buildAllowedModelSet", () => { expect(allowed.allowAny).toBe(false); expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true); - expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe( - true, - ); + expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(true); }); it("includes the default model when no allowlist is set", () => { @@ -58,9 +56,7 @@ describe("buildAllowedModelSet", () => { expect(allowed.allowAny).toBe(true); expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true); - expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe( - true, - ); + expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(true); }); it("allows explicit custom providers from models.providers", () => { @@ -93,9 +89,7 @@ describe("buildAllowedModelSet", () => { }); expect(allowed.allowAny).toBe(false); - expect( - allowed.allowedKeys.has(modelKey("moonshot", "kimi-k2-0905-preview")), - ).toBe(true); + expect(allowed.allowedKeys.has(modelKey("moonshot", "kimi-k2-0905-preview"))).toBe(true); }); }); diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index c1a5b02cc15..b201549b55a 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -7,13 +7,7 @@ export type ModelRef = { model: string; }; -export type ThinkLevel = - | "off" - | "minimal" - | "low" - | "medium" - | "high" - | "xhigh"; +export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelAliasIndex = { byAlias: Map; @@ -40,9 +34,7 @@ export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean { if (normalized === "claude-cli") return true; if (normalized === "codex-cli") return true; const backends = cfg?.agents?.defaults?.cliBackends ?? {}; - return Object.keys(backends).some( - (key) => normalizeProviderId(key) === normalized, - ); + return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized); } function normalizeAnthropicModelId(model: string): string { @@ -60,10 +52,7 @@ function normalizeProviderModelId(provider: string, model: string): string { return model; } -export function parseModelRef( - raw: string, - defaultProvider: string, -): ModelRef | null { +export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null { const trimmed = raw.trim(); if (!trimmed) return null; const slash = trimmed.indexOf("/"); @@ -91,9 +80,7 @@ export function buildModelAliasIndex(params: { for (const [keyRaw, entryRaw] of Object.entries(rawModels)) { const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider); if (!parsed) continue; - const alias = String( - (entryRaw as { alias?: string } | undefined)?.alias ?? "", - ).trim(); + const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); if (!alias) continue; const aliasKey = normalizeAliasKey(alias); byAlias.set(aliasKey, { alias, ref: parsed }); @@ -131,10 +118,7 @@ export function resolveConfiguredModelRef(params: { defaultModel: string; }): ModelRef { const rawModel = (() => { - const raw = params.cfg.agents?.defaults?.model as - | { primary?: string } - | string - | undefined; + const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined; if (typeof raw === "string") return raw.trim(); return raw?.primary?.trim() ?? ""; })(); @@ -176,9 +160,7 @@ export function buildAllowedModelSet(params: { defaultModel && params.defaultProvider ? modelKey(params.defaultProvider, defaultModel) : undefined; - const catalogKeys = new Set( - params.catalog.map((entry) => modelKey(entry.provider, entry.id)), - ); + const catalogKeys = new Set(params.catalog.map((entry) => modelKey(entry.provider, entry.id))); if (allowAny) { if (defaultKey) catalogKeys.add(defaultKey); @@ -190,10 +172,7 @@ export function buildAllowedModelSet(params: { } const allowedKeys = new Set(); - const configuredProviders = (params.cfg.models?.providers ?? {}) as Record< - string, - unknown - >; + const configuredProviders = (params.cfg.models?.providers ?? {}) as Record; for (const raw of rawAllowlist) { const parsed = parseModelRef(String(raw), params.defaultProvider); if (!parsed) continue; @@ -253,9 +232,7 @@ export function getModelRefStatus(params: { const key = modelKey(params.ref.provider, params.ref.model); return { key, - inCatalog: params.catalog.some( - (entry) => modelKey(entry.provider, entry.id) === key, - ), + inCatalog: params.catalog.some((entry) => modelKey(entry.provider, entry.id) === key), allowAny: allowed.allowAny, allowed: allowed.allowAny || allowed.allowedKeys.has(key), }; diff --git a/src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts b/src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts index e65612f18ba..adfb2ebb7ea 100644 --- a/src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts +++ b/src/agents/models-config.auto-injects-github-copilot-provider-token-is.test.ts @@ -52,8 +52,7 @@ describe("models-config", () => { vi.resetModules(); vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: - "https://api.individual.githubcopilot.com", + DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", resolveCopilotApiToken: vi.fn().mockResolvedValue({ token: "copilot", expiresAt: Date.now() + 60 * 60 * 1000, @@ -67,17 +66,12 @@ describe("models-config", () => { const agentDir = path.join(home, "agent-default-base-url"); await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir); - const raw = await fs.readFile( - path.join(agentDir, "models.json"), - "utf8", - ); + const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); const parsed = JSON.parse(raw) as { providers: Record; }; - expect(parsed.providers["github-copilot"]?.baseUrl).toBe( - "https://api.copilot.example", - ); + expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example"); expect(parsed.providers["github-copilot"]?.models?.length ?? 0).toBe(0); } finally { process.env.COPILOT_GITHUB_TOKEN = previous; @@ -104,8 +98,7 @@ describe("models-config", () => { }); vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: - "https://api.individual.githubcopilot.com", + DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", resolveCopilotApiToken, })); diff --git a/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts b/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts index 886c0c3d00e..13090d170d1 100644 --- a/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts +++ b/src/agents/models-config.falls-back-default-baseurl-token-exchange-fails.test.ts @@ -62,17 +62,12 @@ describe("models-config", () => { await ensureClawdbotModelsJson({ models: { providers: {} } }); const agentDir = resolveClawdbotAgentDir(); - const raw = await fs.readFile( - path.join(agentDir, "models.json"), - "utf8", - ); + const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); const parsed = JSON.parse(raw) as { providers: Record; }; - expect(parsed.providers["github-copilot"]?.baseUrl).toBe( - "https://api.default.test", - ); + expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.default.test"); } finally { process.env.COPILOT_GITHUB_TOKEN = previous; } @@ -111,8 +106,7 @@ describe("models-config", () => { ); vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: - "https://api.individual.githubcopilot.com", + DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", resolveCopilotApiToken: vi.fn().mockResolvedValue({ token: "copilot", expiresAt: Date.now() + 60 * 60 * 1000, @@ -125,17 +119,12 @@ describe("models-config", () => { await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir); - const raw = await fs.readFile( - path.join(agentDir, "models.json"), - "utf8", - ); + const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); const parsed = JSON.parse(raw) as { providers: Record; }; - expect(parsed.providers["github-copilot"]?.baseUrl).toBe( - "https://api.copilot.example", - ); + expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example"); } finally { if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN; else process.env.COPILOT_GITHUB_TOKEN = previous; diff --git a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts index fd450424b3f..d4bc506f026 100644 --- a/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts +++ b/src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts @@ -79,10 +79,7 @@ describe("models-config", () => { const modelPath = path.join(resolveClawdbotAgentDir(), "models.json"); const raw = await fs.readFile(modelPath, "utf8"); const parsed = JSON.parse(raw) as { - providers: Record< - string, - { apiKey?: string; models?: Array<{ id: string }> } - >; + providers: Record }>; }; expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY"); const ids = parsed.providers.minimax?.models?.map((model) => model.id); @@ -138,12 +135,8 @@ describe("models-config", () => { providers: Record; }; - expect(parsed.providers.existing?.baseUrl).toBe( - "http://localhost:1234/v1", - ); - expect(parsed.providers["custom-proxy"]?.baseUrl).toBe( - "http://localhost:4000/v1", - ); + expect(parsed.providers.existing?.baseUrl).toBe("http://localhost:1234/v1"); + expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1"); }); }); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index c1b96063ec1..9b02a2396d9 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -3,10 +3,7 @@ import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken, } from "../providers/github-copilot-token.js"; -import { - ensureAuthProfileStore, - listProfilesForProvider, -} from "./auth-profiles.js"; +import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { resolveEnvApiKey } from "./model-auth.js"; import { buildSyntheticModelDefinition, @@ -104,8 +101,7 @@ export function normalizeProviders(params: { // Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR". if ( normalizedProvider.apiKey && - normalizeApiKeyConfig(normalizedProvider.apiKey) !== - normalizedProvider.apiKey + normalizeApiKeyConfig(normalizedProvider.apiKey) !== normalizedProvider.apiKey ) { mutated = true; normalizedProvider = { @@ -117,8 +113,7 @@ export function normalizeProviders(params: { // If a provider defines models, pi's ModelRegistry requires apiKey to be set. // Fill it from the environment or auth profiles when possible. const hasModels = - Array.isArray(normalizedProvider.models) && - normalizedProvider.models.length > 0; + Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0; if (hasModels && !normalizedProvider.apiKey?.trim()) { const fromEnv = resolveEnvApiKeyVarName(normalizedKey); const fromProfiles = resolveApiKeyFromProfiles({ @@ -197,9 +192,7 @@ function buildSyntheticProvider(): ProviderConfig { }; } -export function resolveImplicitProviders(params: { - agentDir: string; -}): ModelsConfig["providers"] { +export function resolveImplicitProviders(params: { agentDir: string }): ModelsConfig["providers"] { const providers: Record = {}; const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, @@ -235,8 +228,7 @@ export async function resolveImplicitCopilotProvider(params: { }): Promise { const env = params.env ?? process.env; const authStore = ensureAuthProfileStore(params.agentDir); - const hasProfile = - listProfilesForProvider(authStore, "github-copilot").length > 0; + const hasProfile = listProfilesForProvider(authStore, "github-copilot").length > 0; const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN; const githubToken = (envToken ?? "").trim(); diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index 590189691be..29ebe326590 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -70,9 +70,7 @@ describe("models-config", () => { agentDir, ); - await expect( - fs.stat(path.join(agentDir, "models.json")), - ).rejects.toThrow(); + await expect(fs.stat(path.join(agentDir, "models.json"))).rejects.toThrow(); expect(result.wrote).toBe(false); } finally { if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN; @@ -85,8 +83,7 @@ describe("models-config", () => { else process.env.MINIMAX_API_KEY = previousMinimax; if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY; else process.env.MOONSHOT_API_KEY = previousMoonshot; - if (previousSynthetic === undefined) - delete process.env.SYNTHETIC_API_KEY; + if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY; else process.env.SYNTHETIC_API_KEY = previousSynthetic; } }); @@ -105,9 +102,7 @@ describe("models-config", () => { providers: Record; }; - expect(parsed.providers["custom-proxy"]?.baseUrl).toBe( - "http://localhost:4000/v1", - ); + expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1"); }); }); it("adds minimax provider when MINIMAX_API_KEY is set", async () => { @@ -133,9 +128,7 @@ describe("models-config", () => { } >; }; - expect(parsed.providers.minimax?.baseUrl).toBe( - "https://api.minimax.io/anthropic", - ); + expect(parsed.providers.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic"); expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY"); const ids = parsed.providers.minimax?.models?.map((model) => model.id); expect(ids).toContain("MiniMax-M2.1"); @@ -169,13 +162,9 @@ describe("models-config", () => { } >; }; - expect(parsed.providers.synthetic?.baseUrl).toBe( - "https://api.synthetic.new/anthropic", - ); + expect(parsed.providers.synthetic?.baseUrl).toBe("https://api.synthetic.new/anthropic"); expect(parsed.providers.synthetic?.apiKey).toBe("SYNTHETIC_API_KEY"); - const ids = parsed.providers.synthetic?.models?.map( - (model) => model.id, - ); + const ids = parsed.providers.synthetic?.models?.map((model) => model.id); expect(ids).toContain("hf:MiniMaxAI/MiniMax-M2.1"); } finally { if (prevKey === undefined) delete process.env.SYNTHETIC_API_KEY; diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 4f9ad05e3d5..50d5da62b62 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -18,10 +18,7 @@ function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } -function mergeProviderModels( - implicit: ProviderConfig, - explicit: ProviderConfig, -): ProviderConfig { +function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig { const implicitModels = Array.isArray(implicit.models) ? implicit.models : []; const explicitModels = Array.isArray(explicit.models) ? explicit.models : []; if (implicitModels.length === 0) return { ...implicit, ...explicit }; @@ -55,16 +52,12 @@ function mergeProviders(params: { implicit?: Record | null; explicit?: Record | null; }): Record { - const out: Record = params.implicit - ? { ...params.implicit } - : {}; + const out: Record = params.implicit ? { ...params.implicit } : {}; for (const [key, explicit] of Object.entries(params.explicit ?? {})) { const providerKey = key.trim(); if (!providerKey) continue; const implicit = out[providerKey]; - out[providerKey] = implicit - ? mergeProviderModels(implicit, explicit) - : explicit; + out[providerKey] = implicit ? mergeProviderModels(implicit, explicit) : explicit; } return out; } @@ -83,14 +76,9 @@ export async function ensureClawdbotModelsJson( agentDirOverride?: string, ): Promise<{ agentDir: string; wrote: boolean }> { const cfg = config ?? loadConfig(); - const agentDir = agentDirOverride?.trim() - ? agentDirOverride.trim() - : resolveClawdbotAgentDir(); + const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveClawdbotAgentDir(); - const explicitProviders = (cfg.models?.providers ?? {}) as Record< - string, - ProviderConfig - >; + const explicitProviders = (cfg.models?.providers ?? {}) as Record; const implicitProviders = resolveImplicitProviders({ agentDir }); const providers: Record = mergeProviders({ implicit: implicitProviders, diff --git a/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts b/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts index 3bee7460682..e030e7d5268 100644 --- a/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts +++ b/src/agents/models-config.uses-first-github-copilot-profile-env-tokens.test.ts @@ -88,8 +88,7 @@ describe("models-config", () => { }); vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: - "https://api.individual.githubcopilot.com", + DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", resolveCopilotApiToken, })); @@ -119,8 +118,7 @@ describe("models-config", () => { vi.resetModules(); vi.doMock("../providers/github-copilot-token.js", () => ({ - DEFAULT_COPILOT_API_BASE_URL: - "https://api.individual.githubcopilot.com", + DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com", resolveCopilotApiToken: vi.fn().mockResolvedValue({ token: "copilot", expiresAt: Date.now() + 60 * 60 * 1000, @@ -145,17 +143,12 @@ describe("models-config", () => { }); const agentDir = resolveClawdbotAgentDir(); - const raw = await fs.readFile( - path.join(agentDir, "models.json"), - "utf8", - ); + const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); const parsed = JSON.parse(raw) as { providers: Record; }; - expect(parsed.providers["github-copilot"]?.baseUrl).toBe( - "https://copilot.local", - ); + expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://copilot.local"); } finally { process.env.COPILOT_GITHUB_TOKEN = previous; } diff --git a/src/agents/models.profiles.live.test.ts b/src/agents/models.profiles.live.test.ts index 92d107fe424..2b7a6a522f2 100644 --- a/src/agents/models.profiles.live.test.ts +++ b/src/agents/models.profiles.live.test.ts @@ -1,8 +1,5 @@ import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai"; -import { - discoverAuthStorage, - discoverModels, -} from "@mariozechner/pi-coding-agent"; +import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { describe, expect, it } from "vitest"; import { loadConfig } from "../config/config.js"; @@ -19,8 +16,7 @@ import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js"; const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1"; const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim()); -const REQUIRE_PROFILE_KEYS = - process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS === "1"; +const REQUIRE_PROFILE_KEYS = process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS === "1"; const describeLive = LIVE ? describe : describe.skip; @@ -62,8 +58,7 @@ function isModelNotFoundErrorMessage(raw: string): boolean { if (!msg) return false; if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) return true; if (/not_found_error/i.test(msg)) return true; - if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg)) - return true; + if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg)) return true; return false; } @@ -156,9 +151,7 @@ describeLive("live models (profile keys)", () => { const anthropicKeys = collectAnthropicApiKeys(); if (anthropicKeys.length > 0) { process.env.ANTHROPIC_API_KEY = anthropicKeys[0]; - logProgress( - `[live-models] anthropic keys loaded: ${anthropicKeys.length}`, - ); + logProgress(`[live-models] anthropic keys loaded: ${anthropicKeys.length}`); } const agentDir = resolveClawdbotAgentDir(); @@ -171,13 +164,8 @@ describeLive("live models (profile keys)", () => { const useExplicit = Boolean(rawModels) && !useModern; const filter = useExplicit ? parseModelFilter(rawModels) : null; const allowNotFoundSkip = useModern; - const providers = parseProviderFilter( - process.env.CLAWDBOT_LIVE_PROVIDERS, - ); - const perModelTimeoutMs = toInt( - process.env.CLAWDBOT_LIVE_MODEL_TIMEOUT_MS, - 30_000, - ); + const providers = parseProviderFilter(process.env.CLAWDBOT_LIVE_PROVIDERS); + const perModelTimeoutMs = toInt(process.env.CLAWDBOT_LIVE_MODEL_TIMEOUT_MS, 30_000); const failures: Array<{ model: string; error: string }> = []; const skipped: Array<{ model: string; reason: string }> = []; @@ -197,10 +185,7 @@ describeLive("live models (profile keys)", () => { } try { const apiKeyInfo = await getApiKeyForModel({ model, cfg }); - if ( - REQUIRE_PROFILE_KEYS && - !apiKeyInfo.source.startsWith("profile:") - ) { + if (REQUIRE_PROFILE_KEYS && !apiKeyInfo.source.startsWith("profile:")) { skipped.push({ model: id, reason: `non-profile credential source: ${apiKeyInfo.source}`, @@ -218,9 +203,7 @@ describeLive("live models (profile keys)", () => { return; } - logProgress( - `[live-models] selection=${useExplicit ? "explicit" : "modern"}`, - ); + logProgress(`[live-models] selection=${useExplicit ? "explicit" : "modern"}`); logProgress(`[live-models] running ${candidates.length} models`); const total = candidates.length; @@ -229,9 +212,7 @@ describeLive("live models (profile keys)", () => { const id = `${model.provider}/${model.id}`; const progressLabel = `[live-models] ${index + 1}/${total} ${id}`; const attemptMax = - model.provider === "anthropic" && anthropicKeys.length > 0 - ? anthropicKeys.length - : 1; + model.provider === "anthropic" && anthropicKeys.length > 0 ? anthropicKeys.length : 1; for (let attempt = 0; attempt < attemptMax; attempt += 1) { if (model.provider === "anthropic" && anthropicKeys.length > 0) { process.env.ANTHROPIC_API_KEY = anthropicKeys[attempt]; @@ -254,8 +235,7 @@ describeLive("live models (profile keys)", () => { parameters: Type.Object({}, { additionalProperties: false }), }; - let firstUserContent = - "Call the tool `noop` with {}. Do not write any other text."; + let firstUserContent = "Call the tool `noop` with {}. Do not write any other text."; let firstUser = { role: "user" as const, content: firstUserContent, @@ -282,11 +262,7 @@ describeLive("live models (profile keys)", () => { // Occasional flake: model answers in text instead of tool call (or adds text). // Retry a couple times with a stronger instruction so we still exercise the tool-only replay path. - for ( - let i = 0; - i < 2 && (!toolCall || firstText.length > 0); - i += 1 - ) { + for (let i = 0; i < 2 && (!toolCall || firstText.length > 0); i += 1) { firstUserContent = "Call the tool `noop` with {}. IMPORTANT: respond ONLY with the tool call; no other text."; firstUser = { @@ -405,29 +381,19 @@ describeLive("live models (profile keys)", () => { isAnthropicRateLimitError(message) && attempt + 1 < attemptMax ) { - logProgress( - `${progressLabel}: rate limit, retrying with next key`, - ); + logProgress(`${progressLabel}: rate limit, retrying with next key`); continue; } - if ( - model.provider === "anthropic" && - isAnthropicBillingError(message) - ) { + if (model.provider === "anthropic" && isAnthropicBillingError(message)) { if (attempt + 1 < attemptMax) { - logProgress( - `${progressLabel}: billing issue, retrying with next key`, - ); + logProgress(`${progressLabel}: billing issue, retrying with next key`); continue; } skipped.push({ model: id, reason: message }); logProgress(`${progressLabel}: skip (anthropic billing)`); break; } - if ( - model.provider === "google" && - isGoogleModelNotFoundError(err) - ) { + if (model.provider === "google" && isGoogleModelNotFoundError(err)) { skipped.push({ model: id, reason: message }); logProgress(`${progressLabel}: skip (google model not found)`); break; @@ -462,9 +428,7 @@ describeLive("live models (profile keys)", () => { .slice(0, 10) .map((f) => `- ${f.model}: ${f.error}`) .join("\n"); - throw new Error( - `live model failures (${failures.length}):\n${preview}`, - ); + throw new Error(`live model failures (${failures.length}):\n${preview}`); } void skipped; diff --git a/src/agents/openai-responses.reasoning-replay.test.ts b/src/agents/openai-responses.reasoning-replay.test.ts index acadf499cab..053d08a4590 100644 --- a/src/agents/openai-responses.reasoning-replay.test.ts +++ b/src/agents/openai-responses.reasoning-replay.test.ts @@ -1,8 +1,4 @@ -import type { - AssistantMessage, - Model, - ToolResultMessage, -} from "@mariozechner/pi-ai"; +import type { AssistantMessage, Model, ToolResultMessage } from "@mariozechner/pi-ai"; import { streamOpenAIResponses } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { describe, expect, it } from "vitest"; @@ -31,8 +27,7 @@ function installFailingFetchCapture() { const bodyText = (() => { if (!rawBody) return ""; if (typeof rawBody === "string") return rawBody; - if (rawBody instanceof Uint8Array) - return Buffer.from(rawBody).toString("utf8"); + if (rawBody instanceof Uint8Array) return Buffer.from(rawBody).toString("utf8"); if (rawBody instanceof ArrayBuffer) return Buffer.from(new Uint8Array(rawBody)).toString("utf8"); return String(rawBody); @@ -135,17 +130,13 @@ describe("openai-responses reasoning replay", () => { const input = Array.isArray(body?.input) ? body?.input : []; const types = input .map((item) => - item && typeof item === "object" - ? (item as Record).type - : undefined, + item && typeof item === "object" ? (item as Record).type : undefined, ) .filter((t): t is string => typeof t === "string"); expect(types).toContain("reasoning"); expect(types).toContain("function_call"); - expect(types.indexOf("reasoning")).toBeLessThan( - types.indexOf("function_call"), - ); + expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call")); } finally { cap.restore(); } @@ -204,9 +195,7 @@ describe("openai-responses reasoning replay", () => { const input = Array.isArray(body?.input) ? body?.input : []; const types = input .map((item) => - item && typeof item === "object" - ? (item as Record).type - : undefined, + item && typeof item === "object" ? (item as Record).type : undefined, ) .filter((t): t is string => typeof t === "string"); diff --git a/src/agents/opencode-zen-models.test.ts b/src/agents/opencode-zen-models.test.ts index f1c1efd7aa0..e732bae44d0 100644 --- a/src/agents/opencode-zen-models.test.ts +++ b/src/agents/opencode-zen-models.test.ts @@ -29,9 +29,7 @@ describe("resolveOpencodeZenAlias", () => { }); it("returns input if no alias exists", () => { - expect(resolveOpencodeZenAlias("some-unknown-model")).toBe( - "some-unknown-model", - ); + expect(resolveOpencodeZenAlias("some-unknown-model")).toBe("some-unknown-model"); }); it("is case-insensitive", () => { @@ -42,22 +40,12 @@ describe("resolveOpencodeZenAlias", () => { describe("resolveOpencodeZenModelApi", () => { it("maps APIs by model family", () => { - expect(resolveOpencodeZenModelApi("claude-opus-4-5")).toBe( - "anthropic-messages", - ); - expect(resolveOpencodeZenModelApi("minimax-m2.1-free")).toBe( - "anthropic-messages", - ); - expect(resolveOpencodeZenModelApi("gemini-3-pro")).toBe( - "google-generative-ai", - ); + expect(resolveOpencodeZenModelApi("claude-opus-4-5")).toBe("anthropic-messages"); + expect(resolveOpencodeZenModelApi("minimax-m2.1-free")).toBe("anthropic-messages"); + expect(resolveOpencodeZenModelApi("gemini-3-pro")).toBe("google-generative-ai"); expect(resolveOpencodeZenModelApi("gpt-5.2")).toBe("openai-responses"); - expect(resolveOpencodeZenModelApi("glm-4.7-free")).toBe( - "openai-completions", - ); - expect(resolveOpencodeZenModelApi("some-unknown-model")).toBe( - "openai-completions", - ); + expect(resolveOpencodeZenModelApi("glm-4.7-free")).toBe("openai-completions"); + expect(resolveOpencodeZenModelApi("some-unknown-model")).toBe("openai-completions"); }); }); diff --git a/src/agents/opencode-zen-models.ts b/src/agents/opencode-zen-models.ts index df906606dcb..6eb87b855ea 100644 --- a/src/agents/opencode-zen-models.ts +++ b/src/agents/opencode-zen-models.ts @@ -91,11 +91,7 @@ export function resolveOpencodeZenAlias(modelIdOrAlias: string): string { */ export function resolveOpencodeZenModelApi(modelId: string): ModelApi { const lower = modelId.toLowerCase(); - if ( - lower.startsWith("claude-") || - lower.startsWith("minimax") || - lower.startsWith("alpha-gd4") - ) { + if (lower.startsWith("claude-") || lower.startsWith("minimax") || lower.startsWith("alpha-gd4")) { return "anthropic-messages"; } if (lower.startsWith("gemini-")) { @@ -274,9 +270,7 @@ interface ZenModelsResponse { * @param apiKey - OpenCode Zen API key for authentication * @returns Array of model definitions, or static fallback on failure */ -export async function fetchOpencodeZenModels( - apiKey?: string, -): Promise { +export async function fetchOpencodeZenModels(apiKey?: string): Promise { // Return cached models if still valid const now = Date.now(); if (cachedModels && now - cacheTimestamp < CACHE_TTL_MS) { @@ -298,9 +292,7 @@ export async function fetchOpencodeZenModels( }); if (!response.ok) { - throw new Error( - `API returned ${response.status}: ${response.statusText}`, - ); + throw new Error(`API returned ${response.status}: ${response.statusText}`); } const data = (await response.json()) as ZenModelsResponse; @@ -316,9 +308,7 @@ export async function fetchOpencodeZenModels( return models; } catch (error) { - console.warn( - `[opencode-zen] Failed to fetch models, using static fallback: ${String(error)}`, - ); + console.warn(`[opencode-zen] Failed to fetch models, using static fallback: ${String(error)}`); return getOpencodeZenStaticFallbackModels(); } } diff --git a/src/agents/pi-embedded-block-chunker.ts b/src/agents/pi-embedded-block-chunker.ts index 3bf95c14b44..649488334b6 100644 --- a/src/agents/pi-embedded-block-chunker.ts +++ b/src/agents/pi-embedded-block-chunker.ts @@ -1,8 +1,4 @@ -import { - findFenceSpanAt, - isSafeFenceBreak, - parseFenceSpans, -} from "../markdown/fences.js"; +import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js"; export type BlockReplyChunking = { minChars: number; @@ -61,10 +57,7 @@ export class EmbeddedBlockChunker { return; } - while ( - this.#buffer.length >= minChars || - (force && this.#buffer.length > 0) - ) { + while (this.#buffer.length >= minChars || (force && this.#buffer.length > 0)) { const breakResult = force && this.#buffer.length <= maxChars ? this.#pickSoftBreakIndex(this.#buffer, 1) @@ -80,9 +73,7 @@ export class EmbeddedBlockChunker { const breakIdx = breakResult.index; let rawChunk = this.#buffer.slice(0, breakIdx); if (rawChunk.trim().length === 0) { - this.#buffer = stripLeadingNewlines( - this.#buffer.slice(breakIdx), - ).trimStart(); + this.#buffer = stripLeadingNewlines(this.#buffer.slice(breakIdx)).trimStart(); continue; } @@ -118,10 +109,7 @@ export class EmbeddedBlockChunker { } #pickSoftBreakIndex(buffer: string, minCharsOverride?: number): BreakResult { - const minChars = Math.max( - 1, - Math.floor(minCharsOverride ?? this.#chunking.minChars), - ); + const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars)); if (buffer.length < minChars) return { index: -1 }; const fenceSpans = parseFenceSpans(buffer); const preference = this.#chunking.breakPreference ?? "paragraph"; @@ -144,10 +132,7 @@ export class EmbeddedBlockChunker { if (preference === "paragraph" || preference === "newline") { let newlineIdx = buffer.indexOf("\n"); while (newlineIdx !== -1) { - if ( - newlineIdx >= minChars && - isSafeFenceBreak(fenceSpans, newlineIdx) - ) { + if (newlineIdx >= minChars && isSafeFenceBreak(fenceSpans, newlineIdx)) { return { index: newlineIdx }; } newlineIdx = buffer.indexOf("\n", newlineIdx + 1); @@ -172,10 +157,7 @@ export class EmbeddedBlockChunker { } #pickBreakIndex(buffer: string, minCharsOverride?: number): BreakResult { - const minChars = Math.max( - 1, - Math.floor(minCharsOverride ?? this.#chunking.minChars), - ); + const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars)); const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars)); if (buffer.length < minChars) return { index: -1 }; const window = buffer.slice(0, Math.min(maxChars, buffer.length)); diff --git a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts index bf28b3506d6..4139bf31984 100644 --- a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts +++ b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts @@ -1,13 +1,8 @@ import { describe, expect, it } from "vitest"; -import { - buildBootstrapContextFiles, - DEFAULT_BOOTSTRAP_MAX_CHARS, -} from "./pi-embedded-helpers.js"; +import { buildBootstrapContextFiles, DEFAULT_BOOTSTRAP_MAX_CHARS } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -40,9 +35,7 @@ describe("buildBootstrapContextFiles", () => { maxChars, warn: (message) => warnings.push(message), }); - expect(result?.content).toContain( - "[...truncated, read TOOLS.md for full content...]", - ); + expect(result?.content).toContain("[...truncated, read TOOLS.md for full content...]"); expect(result?.content.length).toBeLessThan(long.length); expect(result?.content.startsWith(long.slice(0, 120))).toBe(true); expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(true); @@ -55,8 +48,6 @@ describe("buildBootstrapContextFiles", () => { const files = [makeFile({ content: long })]; const [result] = buildBootstrapContextFiles(files); expect(result?.content).toBe(long); - expect(result?.content).not.toContain( - "[...truncated, read AGENTS.md for full content...]", - ); + expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]"); }); }); diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts index a57950e2f8b..d9b8fb1c6be 100644 --- a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts +++ b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { classifyFailoverReason } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -17,9 +15,7 @@ describe("classifyFailoverReason", () => { expect(classifyFailoverReason("no credentials found")).toBe("auth"); expect(classifyFailoverReason("no api key found")).toBe("auth"); expect(classifyFailoverReason("429 too many requests")).toBe("rate_limit"); - expect(classifyFailoverReason("resource has been exhausted")).toBe( - "rate_limit", - ); + expect(classifyFailoverReason("resource has been exhausted")).toBe("rate_limit"); expect( classifyFailoverReason( '{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}', @@ -28,16 +24,12 @@ describe("classifyFailoverReason", () => { expect(classifyFailoverReason("invalid request format")).toBe("format"); expect(classifyFailoverReason("credit balance too low")).toBe("billing"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); - expect(classifyFailoverReason("string should match pattern")).toBe( - "format", - ); + expect(classifyFailoverReason("string should match pattern")).toBe("format"); expect(classifyFailoverReason("bad request")).toBeNull(); }); it("classifies OpenAI usage limit errors as rate_limit", () => { - expect( - classifyFailoverReason( - "You have hit your ChatGPT usage limit (plus plan)", - ), - ).toBe("rate_limit"); + expect(classifyFailoverReason("You have hit your ChatGPT usage limit (plus plan)")).toBe( + "rate_limit", + ); }); }); diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 58400a7a851..2553724a454 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest"; import { formatAssistantErrorText } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -24,12 +22,8 @@ describe("formatAssistantErrorText", () => { expect(formatAssistantErrorText(msg)).toContain("Context overflow"); }); it("returns a friendly message for Anthropic role ordering", () => { - const msg = makeAssistantError( - 'messages: roles must alternate between "user" and "assistant"', - ); - expect(formatAssistantErrorText(msg)).toContain( - "Message ordering conflict", - ); + const msg = makeAssistantError('messages: roles must alternate between "user" and "assistant"'); + expect(formatAssistantErrorText(msg)).toContain("Message ordering conflict"); }); it("returns a friendly message for Anthropic overload errors", () => { const msg = makeAssistantError( diff --git a/src/agents/pi-embedded-helpers.isautherrormessage.test.ts b/src/agents/pi-embedded-helpers.isautherrormessage.test.ts index 4132a996b8a..ff5633305b8 100644 --- a/src/agents/pi-embedded-helpers.isautherrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isautherrormessage.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isAuthErrorMessage } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index d4d27ffe1a8..726b9a9c6bf 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isBillingErrorMessage } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts b/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts index d791ca7aa77..a2ecf5062fe 100644 --- a/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts +++ b/src/agents/pi-embedded-helpers.iscloudcodeassistformaterror.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isCloudCodeAssistFormatError } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts b/src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts index 0f193414cdd..bbcf495fa1a 100644 --- a/src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts +++ b/src/agents/pi-embedded-helpers.iscompactionfailureerror.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isCompactionFailureError } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -23,9 +21,7 @@ describe("isCompactionFailureError", () => { } }); it("ignores non-compaction overflow errors", () => { - expect(isCompactionFailureError("Context overflow: prompt too large")).toBe( - false, - ); + expect(isCompactionFailureError("Context overflow: prompt too large")).toBe(false); expect(isCompactionFailureError("rate limit exceeded")).toBe(false); }); }); diff --git a/src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts b/src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts index 06ba40aa3b7..f456f4319cb 100644 --- a/src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts +++ b/src/agents/pi-embedded-helpers.iscontextoverflowerror.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isContextOverflowError } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts b/src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts index 7ae6e16ec1c..2afb8557b2e 100644 --- a/src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isfailovererrormessage.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isFailoverErrorMessage } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts b/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts index 9b0ba05e815..2527218d8d3 100644 --- a/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts +++ b/src/agents/pi-embedded-helpers.ismessagingtoolduplicate.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { isMessagingToolDuplicate } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -41,10 +39,9 @@ describe("isMessagingToolDuplicate", () => { }); it("detects substring duplicates (LLM elaboration)", () => { expect( - isMessagingToolDuplicate( - 'I sent the message: "Hello, this is a test message!"', - ["Hello, this is a test message!"], - ), + isMessagingToolDuplicate('I sent the message: "Hello, this is a test message!"', [ + "Hello, this is a test message!", + ]), ).toBe(true); }); it("detects when sent text contains block reply (reverse substring)", () => { diff --git a/src/agents/pi-embedded-helpers.messaging-duplicate.test.ts b/src/agents/pi-embedded-helpers.messaging-duplicate.test.ts index 0b1923c90ae..04f88d023f2 100644 --- a/src/agents/pi-embedded-helpers.messaging-duplicate.test.ts +++ b/src/agents/pi-embedded-helpers.messaging-duplicate.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - isMessagingToolDuplicate, - normalizeTextForComparison, -} from "./pi-embedded-helpers.js"; +import { isMessagingToolDuplicate, normalizeTextForComparison } from "./pi-embedded-helpers.js"; describe("normalizeTextForComparison", () => { it("lowercases text", () => { @@ -22,9 +19,7 @@ describe("normalizeTextForComparison", () => { }); it("handles mixed normalization", () => { - expect(normalizeTextForComparison(" Hello 👋 WORLD 🌍 ")).toBe( - "hello world", - ); + expect(normalizeTextForComparison(" Hello 👋 WORLD 🌍 ")).toBe("hello world"); }); }); @@ -63,10 +58,9 @@ describe("isMessagingToolDuplicate", () => { it("detects substring duplicates (LLM elaboration)", () => { expect( - isMessagingToolDuplicate( - 'I sent the message: "Hello, this is a test message!"', - ["Hello, this is a test message!"], - ), + isMessagingToolDuplicate('I sent the message: "Hello, this is a test message!"', [ + "Hello, this is a test message!", + ]), ).toBe(true); }); diff --git a/src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts b/src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts index 0c308d8e90e..300dd234b36 100644 --- a/src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts +++ b/src/agents/pi-embedded-helpers.normalizetextforcomparison.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { normalizeTextForComparison } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -25,8 +23,6 @@ describe("normalizeTextForComparison", () => { expect(normalizeTextForComparison("Hello 👋 World 🌍")).toBe("hello world"); }); it("handles mixed normalization", () => { - expect(normalizeTextForComparison(" Hello 👋 WORLD 🌍 ")).toBe( - "hello world", - ); + expect(normalizeTextForComparison(" Hello 👋 WORLD 🌍 ")).toBe("hello world"); }); }); diff --git a/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts b/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts index 44baacd0490..c4b7685979e 100644 --- a/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts +++ b/src/agents/pi-embedded-helpers.resolvebootstrapmaxchars.test.ts @@ -1,14 +1,9 @@ import { describe, expect, it } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; -import { - DEFAULT_BOOTSTRAP_MAX_CHARS, - resolveBootstrapMaxChars, -} from "./pi-embedded-helpers.js"; +import { DEFAULT_BOOTSTRAP_MAX_CHARS, resolveBootstrapMaxChars } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts index c58e4a27e25..6c325bf8b06 100644 --- a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts +++ b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.keeps-tool-call-tool-result-ids-unchanged.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest"; import { sanitizeSessionMessagesImages } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -40,9 +38,9 @@ describe("sanitizeSessionMessagesImages", () => { const assistant = out[0] as unknown as { role?: string; content?: unknown }; expect(assistant.role).toBe("assistant"); expect(Array.isArray(assistant.content)).toBe(true); - const toolCall = ( - assistant.content as Array<{ type?: string; id?: string }> - ).find((b) => b.type === "toolCall"); + const toolCall = (assistant.content as Array<{ type?: string; id?: string }>).find( + (b) => b.type === "toolCall", + ); expect(toolCall?.id).toBe("call_123|fc_456"); const toolResult = out[1] as unknown as { @@ -81,9 +79,9 @@ describe("sanitizeSessionMessagesImages", () => { const assistant = out[0] as unknown as { role?: string; content?: unknown }; expect(assistant.role).toBe("assistant"); expect(Array.isArray(assistant.content)).toBe(true); - const toolCall = ( - assistant.content as Array<{ type?: string; id?: string }> - ).find((b) => b.type === "toolCall"); + const toolCall = (assistant.content as Array<{ type?: string; id?: string }>).find( + (b) => b.type === "toolCall", + ); expect(toolCall?.id).toBe("call_123_fc_456"); const toolResult = out[1] as unknown as { @@ -127,11 +125,6 @@ describe("sanitizeSessionMessagesImages", () => { const out = await sanitizeSessionMessagesImages(input, "test"); const assistant = out[0] as { content?: Array<{ type?: string }> }; - expect(assistant.content?.map((b) => b.type)).toEqual([ - "text", - "toolCall", - "thinking", - "text", - ]); + expect(assistant.content?.map((b) => b.type)).toEqual(["text", "toolCall", "thinking", "text"]); }); }); diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts index 6208365a1fc..bf2be6ef5a3 100644 --- a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts +++ b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest"; import { sanitizeSessionMessagesImages } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts b/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts index d542a2da1f9..a12f82367c9 100644 --- a/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizegoogleturnordering.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest"; import { sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -17,9 +15,7 @@ describe("sanitizeGoogleTurnOrdering", () => { const input = [ { role: "assistant", - content: [ - { type: "toolCall", id: "call_1", name: "exec", arguments: {} }, - ], + content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }], }, ] satisfies AgentMessage[]; diff --git a/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts b/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts index 458aec9aad0..977002ce9a6 100644 --- a/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizesessionmessagesimages-thought-signature-stripping.test.ts @@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest"; import { sanitizeSessionMessagesImages } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", @@ -34,8 +32,6 @@ describe("sanitizeSessionMessagesImages - thought_signature stripping", () => { const content = (out[0] as { content?: unknown[] }).content; expect(content).toHaveLength(2); expect("thought_signature" in ((content?.[0] ?? {}) as object)).toBe(false); - expect( - (content?.[1] as { thought_signature?: unknown })?.thought_signature, - ).toBe("AQID"); + expect((content?.[1] as { thought_signature?: unknown })?.thought_signature).toBe("AQID"); }); }); diff --git a/src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts b/src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts index a30728cc104..19fec5aaa01 100644 --- a/src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizetoolcallid.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { sanitizeToolCallId } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts b/src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts index 2872ad11f12..84ac4274fe4 100644 --- a/src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts +++ b/src/agents/pi-embedded-helpers.stripthoughtsignatures.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest"; import { stripThoughtSignatures } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; -const _makeFile = ( - overrides: Partial, -): WorkspaceBootstrapFile => ({ +const _makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index ef00c1a8c28..4dc87369a71 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -44,12 +44,6 @@ export { validateAnthropicTurns, validateGeminiTurns, } from "./pi-embedded-helpers/turns.js"; -export type { - EmbeddedContextFile, - FailoverReason, -} from "./pi-embedded-helpers/types.js"; +export type { EmbeddedContextFile, FailoverReason } from "./pi-embedded-helpers/types.js"; -export { - isValidCloudCodeAssistToolId, - sanitizeToolCallId, -} from "./tool-call-id.js"; +export { isValidCloudCodeAssistToolId, sanitizeToolCallId } from "./tool-call-id.js"; diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index 3edf36f2276..2e0b86287b3 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -205,10 +205,7 @@ describe("validateAnthropicTurns", () => { } as AgentMessage, ]; - const result = validateAnthropicTurns(msgs) as Extract< - AgentMessage, - { role: "user" } - >[]; + const result = validateAnthropicTurns(msgs) as Extract[]; expect(result).toHaveLength(1); const merged = result[0]; @@ -216,9 +213,7 @@ describe("validateAnthropicTurns", () => { expect((merged as { attachments?: unknown[] }).attachments).toEqual([ { type: "image", url: "new.png" }, ]); - expect((merged as { someCustomField?: string }).someCustomField).toBe( - "keep-me", - ); + expect((merged as { someCustomField?: string }).someCustomField).toBe("keep-me"); expect(merged.content).toEqual([ { type: "text", text: "Old" }, { type: "text", text: "New" }, @@ -243,10 +238,7 @@ describe("validateAnthropicTurns", () => { }, ]; - const [merged] = validateAnthropicTurns(msgs) as Extract< - AgentMessage, - { role: "user" } - >[]; + const [merged] = validateAnthropicTurns(msgs) as Extract[]; expect(merged.content).toEqual([ { type: "text", text: "first" }, { type: "image", url: "img1" }, @@ -328,9 +320,7 @@ describe("mergeConsecutiveUserTurns", () => { expect((merged as { attachments?: unknown[] }).attachments).toEqual([ { type: "image", url: "new.png" }, ]); - expect((merged as { someCustomField?: string }).someCustomField).toBe( - "keep-me", - ); + expect((merged as { someCustomField?: string }).someCustomField).toBe("keep-me"); expect(merged.timestamp).toBe(2000); }); diff --git a/src/agents/pi-embedded-helpers/bootstrap.ts b/src/agents/pi-embedded-helpers/bootstrap.ts index df9c8e24bc3..3e17e35e423 100644 --- a/src/agents/pi-embedded-helpers/bootstrap.ts +++ b/src/agents/pi-embedded-helpers/bootstrap.ts @@ -124,11 +124,7 @@ export function buildBootstrapContextFiles( }); continue; } - const trimmed = trimBootstrapContent( - file.content ?? "", - file.name, - maxChars, - ); + const trimmed = trimBootstrapContent(file.content ?? "", file.name, maxChars); if (!trimmed.content) continue; if (trimmed.truncated) { opts?.warn?.( @@ -143,13 +139,9 @@ export function buildBootstrapContextFiles( return result; } -export function sanitizeGoogleTurnOrdering( - messages: AgentMessage[], -): AgentMessage[] { +export function sanitizeGoogleTurnOrdering(messages: AgentMessage[]): AgentMessage[] { const GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT = "(session bootstrap)"; - const first = messages[0] as - | { role?: unknown; content?: unknown } - | undefined; + const first = messages[0] as { role?: unknown; content?: unknown } | undefined; const role = first?.role; const content = first?.content; if ( diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 76e580ea8c7..248df875ebf 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -40,9 +40,7 @@ export function formatAssistantErrorText( const unknownTool = raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ?? - raw.match( - /tool\s+["']?([a-z0-9_-]+)["']?\s+(?:not found|is not available)/i, - ); + raw.match(/tool\s+["']?([a-z0-9_-]+)["']?\s+(?:not found|is not available)/i); if (unknownTool?.[1]) { const rewritten = formatSandboxToolPolicyBlockedMessage({ cfg: opts?.cfg, @@ -66,9 +64,7 @@ export function formatAssistantErrorText( ); } - const invalidRequest = raw.match( - /"type":"invalid_request_error".*?"message":"([^"]+)"/, - ); + const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/); if (invalidRequest?.[1]) { return `LLM request rejected: ${invalidRequest[1]}`; } @@ -80,9 +76,7 @@ export function formatAssistantErrorText( return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw; } -export function isRateLimitAssistantError( - msg: AssistantMessage | undefined, -): boolean { +export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean { if (!msg || msg.stopReason !== "error") return false; return isRateLimitErrorMessage(msg.errorMessage ?? ""); } @@ -98,16 +92,8 @@ const ERROR_PATTERNS = { "resource_exhausted", "usage limit", ], - overloaded: [ - /overloaded_error|"type"\s*:\s*"overloaded_error"/i, - "overloaded", - ], - timeout: [ - "timeout", - "timed out", - "deadline exceeded", - "context deadline exceeded", - ], + overloaded: [/overloaded_error|"type"\s*:\s*"overloaded_error"/i, "overloaded"], + timeout: ["timeout", "timed out", "deadline exceeded", "context deadline exceeded"], billing: [ /\b402\b/, "payment required", @@ -140,10 +126,7 @@ const ERROR_PATTERNS = { ], } as const; -function matchesErrorPatterns( - raw: string, - patterns: readonly ErrorPattern[], -): boolean { +function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean { if (!raw) return false; const value = raw.toLowerCase(); return patterns.some((pattern) => @@ -172,9 +155,7 @@ export function isBillingErrorMessage(raw: string): boolean { ); } -export function isBillingAssistantError( - msg: AssistantMessage | undefined, -): boolean { +export function isBillingAssistantError(msg: AssistantMessage | undefined): boolean { if (!msg || msg.stopReason !== "error") return false; return isBillingErrorMessage(msg.errorMessage ?? ""); } @@ -191,9 +172,7 @@ export function isCloudCodeAssistFormatError(raw: string): boolean { return matchesErrorPatterns(raw, ERROR_PATTERNS.format); } -export function isAuthAssistantError( - msg: AssistantMessage | undefined, -): boolean { +export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean { if (!msg || msg.stopReason !== "error") return false; return isAuthErrorMessage(msg.errorMessage ?? ""); } @@ -212,9 +191,7 @@ export function isFailoverErrorMessage(raw: string): boolean { return classifyFailoverReason(raw) !== null; } -export function isFailoverAssistantError( - msg: AssistantMessage | undefined, -): boolean { +export function isFailoverAssistantError(msg: AssistantMessage | undefined): boolean { if (!msg || msg.stopReason !== "error") return false; return isFailoverErrorMessage(msg.errorMessage ?? ""); } diff --git a/src/agents/pi-embedded-helpers/google.ts b/src/agents/pi-embedded-helpers/google.ts index 0c39b5ac4ef..6f9846d80e6 100644 --- a/src/agents/pi-embedded-helpers/google.ts +++ b/src/agents/pi-embedded-helpers/google.ts @@ -4,9 +4,7 @@ import { sanitizeGoogleTurnOrdering } from "./bootstrap.js"; export function isGoogleModelApi(api?: string | null): boolean { return ( - api === "google-gemini-cli" || - api === "google-generative-ai" || - api === "google-antigravity" + api === "google-gemini-cli" || api === "google-generative-ai" || api === "google-antigravity" ); } @@ -28,9 +26,7 @@ type GeminiToolCallBlock = { input?: unknown; }; -export function downgradeGeminiHistory( - messages: AgentMessage[], -): AgentMessage[] { +export function downgradeGeminiHistory(messages: AgentMessage[]): AgentMessage[] { const downgradedIds = new Set(); const out: AgentMessage[] = []; @@ -63,11 +59,7 @@ export function downgradeGeminiHistory( if (!block || typeof block !== "object") return block; const blockRecord = block as GeminiToolCallBlock; const type = blockRecord.type; - if ( - type === "toolCall" || - type === "functionCall" || - type === "toolUse" - ) { + if (type === "toolCall" || type === "functionCall" || type === "toolUse") { const hasSignature = Boolean(blockRecord.thought_signature); if (!hasSignature) { const id = @@ -83,15 +75,12 @@ export function downgradeGeminiHistory( ? blockRecord.toolName : undefined; const args = - blockRecord.arguments !== undefined - ? blockRecord.arguments - : blockRecord.input; + blockRecord.arguments !== undefined ? blockRecord.arguments : blockRecord.input; if (id) downgradedIds.add(id); hasDowngraded = true; - const argsText = - typeof args === "string" ? args : JSON.stringify(args, null, 2); + const argsText = typeof args === "string" ? args : JSON.stringify(args, null, 2); return { type: "text", @@ -104,11 +93,7 @@ export function downgradeGeminiHistory( return block; }); - out.push( - hasDowngraded - ? ({ ...assistantMsg, content: newContent } as AgentMessage) - : msg, - ); + out.push(hasDowngraded ? ({ ...assistantMsg, content: newContent } as AgentMessage) : msg); continue; } diff --git a/src/agents/pi-embedded-helpers/images.ts b/src/agents/pi-embedded-helpers/images.ts index 81d6ce7b5cc..56130abec3e 100644 --- a/src/agents/pi-embedded-helpers/images.ts +++ b/src/agents/pi-embedded-helpers/images.ts @@ -1,7 +1,4 @@ -import type { - AgentMessage, - AgentToolResult, -} from "@mariozechner/pi-agent-core"; +import type { AgentMessage, AgentToolResult } from "@mariozechner/pi-agent-core"; import { sanitizeToolCallIdsForCloudCodeAssist } from "../tool-call-id.js"; import { sanitizeContentBlocksImages } from "../tool-images.js"; @@ -93,11 +90,7 @@ export async function sanitizeSessionMessagesImages( const block = filteredContent[i]; if (!block || typeof block !== "object") continue; const type = (block as { type?: unknown }).type; - if ( - type === "functionCall" || - type === "toolUse" || - type === "toolCall" - ) { + if (type === "functionCall" || type === "toolUse" || type === "toolCall") { lastToolIndex = i; break; } diff --git a/src/agents/pi-embedded-helpers/messaging-dedupe.ts b/src/agents/pi-embedded-helpers/messaging-dedupe.ts index dd157b2a13f..fe8acca321f 100644 --- a/src/agents/pi-embedded-helpers/messaging-dedupe.ts +++ b/src/agents/pi-embedded-helpers/messaging-dedupe.ts @@ -21,27 +21,16 @@ export function isMessagingToolDuplicateNormalized( normalizedSentTexts: string[], ): boolean { if (normalizedSentTexts.length === 0) return false; - if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) - return false; + if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false; return normalizedSentTexts.some((normalizedSent) => { - if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) - return false; - return ( - normalized.includes(normalizedSent) || normalizedSent.includes(normalized) - ); + if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) return false; + return normalized.includes(normalizedSent) || normalizedSent.includes(normalized); }); } -export function isMessagingToolDuplicate( - text: string, - sentTexts: string[], -): boolean { +export function isMessagingToolDuplicate(text: string, sentTexts: string[]): boolean { if (sentTexts.length === 0) return false; const normalized = normalizeTextForComparison(text); - if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) - return false; - return isMessagingToolDuplicateNormalized( - normalized, - sentTexts.map(normalizeTextForComparison), - ); + if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false; + return isMessagingToolDuplicateNormalized(normalized, sentTexts.map(normalizeTextForComparison)); } diff --git a/src/agents/pi-embedded-helpers/thinking.ts b/src/agents/pi-embedded-helpers/thinking.ts index 1e3c425e48c..474c16d855d 100644 --- a/src/agents/pi-embedded-helpers/thinking.ts +++ b/src/agents/pi-embedded-helpers/thinking.ts @@ -1,16 +1,12 @@ -import { - normalizeThinkLevel, - type ThinkLevel, -} from "../../auto-reply/thinking.js"; +import { normalizeThinkLevel, type ThinkLevel } from "../../auto-reply/thinking.js"; function extractSupportedValues(raw: string): string[] { const match = - raw.match(/supported values are:\s*([^\n.]+)/i) ?? - raw.match(/supported values:\s*([^\n.]+)/i); + raw.match(/supported values are:\s*([^\n.]+)/i) ?? raw.match(/supported values:\s*([^\n.]+)/i); if (!match?.[1]) return []; const fragment = match[1]; - const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g)).map( - (entry) => entry[1]?.trim(), + const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g)).map((entry) => + entry[1]?.trim(), ); if (quoted.length > 0) { return quoted.filter((entry): entry is string => Boolean(entry)); diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/pi-embedded-helpers/turns.ts index 81ce87e2b25..ed927d32cad 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -30,10 +30,7 @@ export function validateGeminiTurns(messages: AgentMessage[]): AgentMessage[] { const currentMsg = msg as Extract; if (lastMsg && typeof lastMsg === "object") { - const lastAsst = lastMsg as Extract< - AgentMessage, - { role: "assistant" } - >; + const lastAsst = lastMsg as Extract; const mergedContent = [ ...(Array.isArray(lastAsst.content) ? lastAsst.content : []), ...(Array.isArray(currentMsg.content) ? currentMsg.content : []), @@ -82,9 +79,7 @@ export function mergeConsecutiveUserTurns( * Anthropic requires strict alternating user→assistant pattern. * Merges consecutive user messages together. */ -export function validateAnthropicTurns( - messages: AgentMessage[], -): AgentMessage[] { +export function validateAnthropicTurns(messages: AgentMessage[]): AgentMessage[] { if (!Array.isArray(messages) || messages.length === 0) { return messages; } diff --git a/src/agents/pi-embedded-helpers/types.ts b/src/agents/pi-embedded-helpers/types.ts index 393eb427a28..f76ee6deac1 100644 --- a/src/agents/pi-embedded-helpers/types.ts +++ b/src/agents/pi-embedded-helpers/types.ts @@ -1,9 +1,3 @@ export type EmbeddedContextFile = { path: string; content: string }; -export type FailoverReason = - | "auth" - | "format" - | "rate_limit" - | "billing" - | "timeout" - | "unknown"; +export type FailoverReason = "auth" | "format" | "rate_limit" | "billing" | "timeout" | "unknown"; diff --git a/src/agents/pi-embedded-messaging.ts b/src/agents/pi-embedded-messaging.ts index 5ee4415c668..d48e0b6274c 100644 --- a/src/agents/pi-embedded-messaging.ts +++ b/src/agents/pi-embedded-messaging.ts @@ -1,7 +1,4 @@ -import { - getChannelPlugin, - normalizeChannelId, -} from "../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js"; export type MessagingToolSend = { tool: string; @@ -35,15 +32,11 @@ export function isMessagingToolSendAction( return Boolean(plugin.actions.extractToolSend({ args })?.to); } -export function normalizeTargetForProvider( - provider: string, - raw?: string, -): string | undefined { +export function normalizeTargetForProvider(provider: string, raw?: string): string | undefined { if (!raw) return undefined; const providerId = normalizeChannelId(provider); const plugin = providerId ? getChannelPlugin(providerId) : undefined; const normalized = - plugin?.messaging?.normalizeTarget?.(raw) ?? - (raw.trim().toLowerCase() || undefined); + plugin?.messaging?.normalizeTarget?.(raw) ?? (raw.trim().toLowerCase() || undefined); return normalized || undefined; } diff --git a/src/agents/pi-embedded-runner-extraparams.live.test.ts b/src/agents/pi-embedded-runner-extraparams.live.test.ts index c92be34482e..92b8eeb534a 100644 --- a/src/agents/pi-embedded-runner-extraparams.live.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.live.test.ts @@ -38,8 +38,7 @@ describeLive("pi embedded extra params (live)", () => { messages: [ { role: "user", - content: - "Write the alphabet letters A through Z as words separated by commas.", + content: "Write the alphabet letters A through Z as words separated by commas.", timestamp: Date.now(), }, ], diff --git a/src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts b/src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts index a541b445d3e..00fde9cccac 100644 --- a/src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts +++ b/src/agents/pi-embedded-runner.applygoogleturnorderingfix.test.ts @@ -7,9 +7,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { applyGoogleTurnOrderingFix } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { @@ -105,9 +103,7 @@ describe("applyGoogleTurnOrderingFix", () => { [ { role: "assistant", - content: [ - { type: "toolCall", id: "call_1", name: "exec", arguments: {} }, - ], + content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }], }, ] satisfies AgentMessage[]; @@ -130,8 +126,7 @@ describe("applyGoogleTurnOrderingFix", () => { .getEntries() .some( (entry) => - entry.type === "custom" && - entry.customType === "google-turn-ordering-bootstrap", + entry.type === "custom" && entry.customType === "google-turn-ordering-bootstrap", ), ).toBe(true); diff --git a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts b/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts index 9354f3d1d1a..a19bd64b749 100644 --- a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts +++ b/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts @@ -6,9 +6,7 @@ import { buildEmbeddedSandboxInfo } from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { diff --git a/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts b/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts index 6faef4c3c17..92a261d2d2c 100644 --- a/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts +++ b/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts @@ -5,9 +5,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { createSystemPromptOverride } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts index 699db95b4d3..5a6e2e25d63 100644 --- a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts +++ b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.falls-back-provider-default-per-dm-not.test.ts @@ -5,9 +5,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { @@ -119,9 +117,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { }, }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:789", config), - ).toBe(3); + expect(getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:789", config)).toBe(3); }); it("handles userId with colons (e.g., email)", () => { const config = { @@ -132,9 +128,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { }, }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("msteams:dm:user@example.com", config), - ).toBe(7); + expect(getDmHistoryLimitFromSessionKey("msteams:dm:user@example.com", config)).toBe(7); }); it("returns undefined when per-DM historyLimit is not set", () => { const config = { @@ -144,9 +138,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { }, }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("telegram:dm:123", config), - ).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBeUndefined(); }); it("returns 0 when per-DM historyLimit is explicitly 0 (unlimited)", () => { const config = { diff --git a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts index f3dcb9a89a4..74a98c9d556 100644 --- a/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts +++ b/src/agents/pi-embedded-runner.get-dm-history-limit-from-session-key.returns-undefined-sessionkey-is-undefined.test.ts @@ -5,9 +5,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { getDmHistoryLimitFromSessionKey } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { @@ -103,9 +101,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { expect(getDmHistoryLimitFromSessionKey(undefined, {})).toBeUndefined(); }); it("returns undefined when config is undefined", () => { - expect( - getDmHistoryLimitFromSessionKey("telegram:dm:123", undefined), - ).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", undefined)).toBeUndefined(); }); it("returns dmHistoryLimit for telegram provider", () => { const config = { @@ -123,9 +119,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { const config = { channels: { telegram: { dmHistoryLimit: 10 } }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config), - ).toBe(10); + expect(getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config)).toBe(10); }); it("returns undefined for non-dm session kinds", () => { const config = { @@ -134,26 +128,18 @@ describe("getDmHistoryLimitFromSessionKey", () => { slack: { dmHistoryLimit: 10 }, }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config), - ).toBeUndefined(); - expect( - getDmHistoryLimitFromSessionKey("telegram:slash:123", config), - ).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config)).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("telegram:slash:123", config)).toBeUndefined(); }); it("returns undefined for unknown provider", () => { const config = { channels: { telegram: { dmHistoryLimit: 15 } }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("unknown:dm:123", config), - ).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("unknown:dm:123", config)).toBeUndefined(); }); it("returns undefined when provider config has no dmHistoryLimit", () => { const config = { channels: { telegram: {} } } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey("telegram:dm:123", config), - ).toBeUndefined(); + expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBeUndefined(); }); it("handles all supported providers", () => { const providers = [ @@ -170,9 +156,7 @@ describe("getDmHistoryLimitFromSessionKey", () => { const config = { channels: { [provider]: { dmHistoryLimit: 5 } }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config), - ).toBe(5); + expect(getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config)).toBe(5); } }); it("handles per-DM overrides for all supported providers", () => { @@ -196,27 +180,16 @@ describe("getDmHistoryLimitFromSessionKey", () => { }, }, } as ClawdbotConfig; - expect( - getDmHistoryLimitFromSessionKey( - `${provider}:dm:user123`, - configWithOverride, - ), - ).toBe(7); + expect(getDmHistoryLimitFromSessionKey(`${provider}:dm:user123`, configWithOverride)).toBe(7); // Test fallback to provider default when user not in dms - expect( - getDmHistoryLimitFromSessionKey( - `${provider}:dm:otheruser`, - configWithOverride, - ), - ).toBe(20); + expect(getDmHistoryLimitFromSessionKey(`${provider}:dm:otheruser`, configWithOverride)).toBe( + 20, + ); // Test with agent-prefixed key expect( - getDmHistoryLimitFromSessionKey( - `agent:main:${provider}:dm:user123`, - configWithOverride, - ), + getDmHistoryLimitFromSessionKey(`agent:main:${provider}:dm:user123`, configWithOverride), ).toBe(7); } }); diff --git a/src/agents/pi-embedded-runner.guard.test.ts b/src/agents/pi-embedded-runner.guard.test.ts index 08a0c7c9ff1..e9ccfa753c4 100644 --- a/src/agents/pi-embedded-runner.guard.test.ts +++ b/src/agents/pi-embedded-runner.guard.test.ts @@ -27,11 +27,7 @@ describe("guardSessionManager integration", () => { .filter((e) => e.type === "message") .map((e) => (e as { message: AgentMessage }).message); - expect(messages.map((m) => m.role)).toEqual([ - "assistant", - "toolResult", - "assistant", - ]); + expect(messages.map((m) => m.role)).toEqual(["assistant", "toolResult", "assistant"]); expect((messages[1] as { toolCallId?: string }).toolCallId).toBe("call_1"); expect(sanitizeToolUseResultPairing(messages).map((m) => m.role)).toEqual([ "assistant", diff --git a/src/agents/pi-embedded-runner.limithistoryturns.test.ts b/src/agents/pi-embedded-runner.limithistoryturns.test.ts index cf64d6211bc..cf95f31b137 100644 --- a/src/agents/pi-embedded-runner.limithistoryturns.test.ts +++ b/src/agents/pi-embedded-runner.limithistoryturns.test.ts @@ -6,9 +6,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { limitHistoryTurns } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { @@ -126,40 +124,20 @@ describe("limitHistoryTurns", () => { expect(limitHistoryTurns(messages, 10)).toBe(messages); }); it("limits to last N user turns", () => { - const messages = makeMessages([ - "user", - "assistant", - "user", - "assistant", - "user", - "assistant", - ]); + const messages = makeMessages(["user", "assistant", "user", "assistant", "user", "assistant"]); const limited = limitHistoryTurns(messages, 2); expect(limited.length).toBe(4); expect(limited[0].content).toEqual([{ type: "text", text: "message 2" }]); }); it("handles single user turn limit", () => { - const messages = makeMessages([ - "user", - "assistant", - "user", - "assistant", - "user", - "assistant", - ]); + const messages = makeMessages(["user", "assistant", "user", "assistant", "user", "assistant"]); const limited = limitHistoryTurns(messages, 1); expect(limited.length).toBe(2); expect(limited[0].content).toEqual([{ type: "text", text: "message 4" }]); expect(limited[1].content).toEqual([{ type: "text", text: "message 5" }]); }); it("handles messages with multiple assistant responses per user turn", () => { - const messages = makeMessages([ - "user", - "assistant", - "assistant", - "user", - "assistant", - ]); + const messages = makeMessages(["user", "assistant", "assistant", "user", "assistant"]); const limited = limitHistoryTurns(messages, 1); expect(limited.length).toBe(2); expect(limited[0].role).toBe("user"); diff --git a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts b/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts index c654743b211..3f37bc67904 100644 --- a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts +++ b/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts @@ -5,9 +5,7 @@ import { resolveSessionAgentIds } from "./agent-scope.js"; import { ensureClawdbotModelsJson } from "./models-config.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts index 694e79c08c3..d559ce62ebd 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.appends-new-user-assistant-after-existing-transcript.test.ts @@ -6,15 +6,9 @@ import type { ClawdbotConfig } from "../config/config.js"; import { ensureClawdbotModelsJson } from "./models-config.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); - const buildAssistantMessage = (model: { - api: string; - provider: string; - id: string; - }) => ({ + const buildAssistantMessage = (model: { api: string; provider: string; id: string }) => ({ role: "assistant" as const, content: [{ type: "text" as const, text: "ok" }], stopReason: "stop" as const, @@ -38,11 +32,7 @@ vi.mock("@mariozechner/pi-ai", async () => { timestamp: Date.now(), }); - const buildAssistantErrorMessage = (model: { - api: string; - provider: string; - id: string; - }) => ({ + const buildAssistantErrorMessage = (model: { api: string; provider: string; id: string }) => ({ role: "assistant" as const, content: [] as const, stopReason: "error" as const, @@ -73,11 +63,7 @@ vi.mock("@mariozechner/pi-ai", async () => { if (model.id === "mock-error") return buildAssistantErrorMessage(model); return buildAssistantMessage(model); }, - completeSimple: async (model: { - api: string; - provider: string; - id: string; - }) => { + completeSimple: async (model: { api: string; provider: string; id: string }) => { if (model.id === "mock-error") return buildAssistantErrorMessage(model); return buildAssistantMessage(model); }, @@ -155,12 +141,8 @@ describe("runEmbeddedPiAgent", () => { it("appends new user + assistant after existing transcript entries", async () => { const { SessionManager } = await import("@mariozechner/pi-coding-agent"); - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-agent-"), - ); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); const sessionFile = path.join(workspaceDir, "session.jsonl"); const sessionManager = SessionManager.open(sessionFile); @@ -210,19 +192,14 @@ describe("runEmbeddedPiAgent", () => { const messages = await readSessionMessages(sessionFile); const seedUserIndex = messages.findIndex( - (message) => - message?.role === "user" && - textFromContent(message.content) === "seed user", + (message) => message?.role === "user" && textFromContent(message.content) === "seed user", ); const seedAssistantIndex = messages.findIndex( (message) => - message?.role === "assistant" && - textFromContent(message.content) === "seed assistant", + message?.role === "assistant" && textFromContent(message.content) === "seed assistant", ); const newUserIndex = messages.findIndex( - (message) => - message?.role === "user" && - textFromContent(message.content) === "hello", + (message) => message?.role === "user" && textFromContent(message.content) === "hello", ); const newAssistantIndex = messages.findIndex( (message, index) => index > newUserIndex && message?.role === "assistant", @@ -233,12 +210,8 @@ describe("runEmbeddedPiAgent", () => { expect(newAssistantIndex).toBeGreaterThan(newUserIndex); }, 20_000); it("persists multi-turn user/assistant ordering across runs", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-agent-"), - ); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); const sessionFile = path.join(workspaceDir, "session.jsonl"); const cfg = makeOpenAiConfig(["mock-1"]); @@ -272,22 +245,16 @@ describe("runEmbeddedPiAgent", () => { const messages = await readSessionMessages(sessionFile); const firstUserIndex = messages.findIndex( - (message) => - message?.role === "user" && - textFromContent(message.content) === "first", + (message) => message?.role === "user" && textFromContent(message.content) === "first", ); const firstAssistantIndex = messages.findIndex( - (message, index) => - index > firstUserIndex && message?.role === "assistant", + (message, index) => index > firstUserIndex && message?.role === "assistant", ); const secondUserIndex = messages.findIndex( - (message) => - message?.role === "user" && - textFromContent(message.content) === "second", + (message) => message?.role === "user" && textFromContent(message.content) === "second", ); const secondAssistantIndex = messages.findIndex( - (message, index) => - index > secondUserIndex && message?.role === "assistant", + (message, index) => index > secondUserIndex && message?.role === "assistant", ); expect(firstUserIndex).toBeGreaterThanOrEqual(0); expect(firstAssistantIndex).toBeGreaterThan(firstUserIndex); diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts index 024627fb603..a4a93c29038 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.writes-models-json-into-provided-agentdir.test.ts @@ -6,15 +6,9 @@ import type { ClawdbotConfig } from "../config/config.js"; import { ensureClawdbotModelsJson } from "./models-config.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); - const buildAssistantMessage = (model: { - api: string; - provider: string; - id: string; - }) => ({ + const buildAssistantMessage = (model: { api: string; provider: string; id: string }) => ({ role: "assistant" as const, content: [{ type: "text" as const, text: "ok" }], stopReason: "stop" as const, @@ -38,11 +32,7 @@ vi.mock("@mariozechner/pi-ai", async () => { timestamp: Date.now(), }); - const buildAssistantErrorMessage = (model: { - api: string; - provider: string; - id: string; - }) => ({ + const buildAssistantErrorMessage = (model: { api: string; provider: string; id: string }) => ({ role: "assistant" as const, content: [] as const, stopReason: "error" as const, @@ -73,11 +63,7 @@ vi.mock("@mariozechner/pi-ai", async () => { if (model.id === "mock-error") return buildAssistantErrorMessage(model); return buildAssistantMessage(model); }, - completeSimple: async (model: { - api: string; - provider: string; - id: string; - }) => { + completeSimple: async (model: { api: string; provider: string; id: string }) => { if (model.id === "mock-error") return buildAssistantErrorMessage(model); return buildAssistantMessage(model); }, @@ -153,12 +139,8 @@ const readSessionMessages = async (sessionFile: string) => { describe("runEmbeddedPiAgent", () => { it("writes models.json into the provided agentDir", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-agent-"), - ); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); const sessionFile = path.join(workspaceDir, "session.jsonl"); const cfg = { @@ -199,60 +181,42 @@ describe("runEmbeddedPiAgent", () => { }), ).rejects.toThrow(/Unknown model:/); - await expect( - fs.stat(path.join(agentDir, "models.json")), - ).resolves.toBeTruthy(); + await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy(); }); - it( - "persists the first user message before assistant output", - { timeout: 15_000 }, - async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-agent-"), - ); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); - const sessionFile = path.join(workspaceDir, "session.jsonl"); + it("persists the first user message before assistant output", { timeout: 15_000 }, async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); + const sessionFile = path.join(workspaceDir, "session.jsonl"); - const cfg = makeOpenAiConfig(["mock-1"]); - await ensureModels(cfg, agentDir); + const cfg = makeOpenAiConfig(["mock-1"]); + await ensureModels(cfg, agentDir); - await runEmbeddedPiAgent({ - sessionId: "session:test", - sessionKey: "agent:main:main", - sessionFile, - workspaceDir, - config: cfg, - prompt: "hello", - provider: "openai", - model: "mock-1", - timeoutMs: 5_000, - agentDir, - }); + await runEmbeddedPiAgent({ + sessionId: "session:test", + sessionKey: "agent:main:main", + sessionFile, + workspaceDir, + config: cfg, + prompt: "hello", + provider: "openai", + model: "mock-1", + timeoutMs: 5_000, + agentDir, + }); - const messages = await readSessionMessages(sessionFile); - const firstUserIndex = messages.findIndex( - (message) => - message?.role === "user" && - textFromContent(message.content) === "hello", - ); - const firstAssistantIndex = messages.findIndex( - (message) => message?.role === "assistant", - ); - expect(firstUserIndex).toBeGreaterThanOrEqual(0); - if (firstAssistantIndex !== -1) { - expect(firstUserIndex).toBeLessThan(firstAssistantIndex); - } - }, - ); + const messages = await readSessionMessages(sessionFile); + const firstUserIndex = messages.findIndex( + (message) => message?.role === "user" && textFromContent(message.content) === "hello", + ); + const firstAssistantIndex = messages.findIndex((message) => message?.role === "assistant"); + expect(firstUserIndex).toBeGreaterThanOrEqual(0); + if (firstAssistantIndex !== -1) { + expect(firstUserIndex).toBeLessThan(firstAssistantIndex); + } + }); it("persists the user message when prompt fails before assistant output", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-agent-"), - ); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); const sessionFile = path.join(workspaceDir, "session.jsonl"); const cfg = makeOpenAiConfig(["mock-error"]); @@ -274,8 +238,7 @@ describe("runEmbeddedPiAgent", () => { const messages = await readSessionMessages(sessionFile); const userIndex = messages.findIndex( - (message) => - message?.role === "user" && textFromContent(message.content) === "boom", + (message) => message?.role === "user" && textFromContent(message.content) === "boom", ); expect(userIndex).toBeGreaterThanOrEqual(0); }); diff --git a/src/agents/pi-embedded-runner.splitsdktools.test.ts b/src/agents/pi-embedded-runner.splitsdktools.test.ts index ade37893b92..813cfe9760d 100644 --- a/src/agents/pi-embedded-runner.splitsdktools.test.ts +++ b/src/agents/pi-embedded-runner.splitsdktools.test.ts @@ -6,9 +6,7 @@ import { ensureClawdbotModelsJson } from "./models-config.js"; import { splitSdkTools } from "./pi-embedded-runner.js"; vi.mock("@mariozechner/pi-ai", async () => { - const actual = await vi.importActual( - "@mariozechner/pi-ai", - ); + const actual = await vi.importActual("@mariozechner/pi-ai"); return { ...actual, streamSimple: (model: { api: string; provider: string; id: string }) => { diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index d7f774e81bd..bdebd000522 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -1,9 +1,6 @@ export type { MessagingToolSend } from "./pi-embedded-messaging.js"; export { compactEmbeddedPiSession } from "./pi-embedded-runner/compact.js"; -export { - applyExtraParamsToAgent, - resolveExtraParams, -} from "./pi-embedded-runner/extra-params.js"; +export { applyExtraParamsToAgent, resolveExtraParams } from "./pi-embedded-runner/extra-params.js"; export { applyGoogleTurnOrderingFix } from "./pi-embedded-runner/google.js"; export { diff --git a/src/agents/pi-embedded-runner/abort.ts b/src/agents/pi-embedded-runner/abort.ts index c60927a4d7d..5eedc1adf65 100644 --- a/src/agents/pi-embedded-runner/abort.ts +++ b/src/agents/pi-embedded-runner/abort.ts @@ -3,8 +3,6 @@ export function isAbortError(err: unknown): boolean { const name = "name" in err ? String(err.name) : ""; if (name === "AbortError") return true; const message = - "message" in err && typeof err.message === "string" - ? err.message.toLowerCase() - : ""; + "message" in err && typeof err.message === "string" ? err.message.toLowerCase() : ""; return message.includes("aborted"); } diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 160de6cbee8..f518803227f 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -1,21 +1,14 @@ import fs from "node:fs/promises"; import os from "node:os"; -import { - createAgentSession, - SessionManager, - SettingsManager, -} from "@mariozechner/pi-coding-agent"; +import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js"; import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js"; import { resolveChannelCapabilities } from "../../config/channel-capabilities.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { getMachineDisplayName } from "../../infra/machine-name.js"; -import { - type enqueueCommand, - enqueueCommandInLane, -} from "../../process/command-queue.js"; +import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; import { resolveUserPath } from "../../utils.js"; @@ -48,28 +41,16 @@ import { resolveSkillsPromptForRun, type SkillSnapshot, } from "../skills.js"; -import { - filterBootstrapFilesForSession, - loadWorkspaceBootstrapFiles, -} from "../workspace.js"; +import { filterBootstrapFilesForSession, loadWorkspaceBootstrapFiles } from "../workspace.js"; import { buildEmbeddedExtensionPaths } from "./extensions.js"; import { logToolSchemasForGoogle, sanitizeSessionHistory } from "./google.js"; -import { - getDmHistoryLimitFromSessionKey, - limitHistoryTurns, -} from "./history.js"; +import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js"; import { resolveGlobalLane, resolveSessionLane } from "./lanes.js"; import { log } from "./logger.js"; import { buildModelAliasLines, resolveModel } from "./model.js"; import { buildEmbeddedSandboxInfo } from "./sandbox-info.js"; -import { - prewarmSessionFile, - trackSessionManagerAccess, -} from "./session-manager-cache.js"; -import { - buildEmbeddedSystemPrompt, - createSystemPromptOverride, -} from "./system-prompt.js"; +import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js"; +import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "./system-prompt.js"; import { splitSdkTools } from "./tool-split.js"; import type { EmbeddedPiCompactResult } from "./types.js"; import { @@ -102,20 +83,16 @@ export async function compactEmbeddedPiSession(params: { extraSystemPrompt?: string; ownerNumbers?: string[]; }): Promise { - const sessionLane = resolveSessionLane( - params.sessionKey?.trim() || params.sessionId, - ); + const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId); const globalLane = resolveGlobalLane(params.lane); const enqueueGlobal = - params.enqueue ?? - ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); + params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); return enqueueCommandInLane(sessionLane, () => enqueueGlobal(async () => { const resolvedWorkspace = resolveUserPath(params.workspaceDir); const prevCwd = process.cwd(); - const provider = - (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; + const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL; const agentDir = params.agentDir ?? resolveClawdbotAgentDir(); await ensureClawdbotModelsJson(params.config, agentDir); @@ -139,9 +116,8 @@ export async function compactEmbeddedPiSession(params: { }); if (model.provider === "github-copilot") { - const { resolveCopilotApiToken } = await import( - "../../providers/github-copilot-token.js" - ); + const { resolveCopilotApiToken } = + await import("../../providers/github-copilot-token.js"); const copilotToken = await resolveCopilotApiToken({ githubToken: apiKeyInfo.apiKey, }); @@ -205,14 +181,10 @@ export async function compactEmbeddedPiSession(params: { params.sessionKey ?? params.sessionId, ); const sessionLabel = params.sessionKey ?? params.sessionId; - const contextFiles: EmbeddedContextFile[] = buildBootstrapContextFiles( - bootstrapFiles, - { - maxChars: resolveBootstrapMaxChars(params.config), - warn: (message) => - log.warn(`${message} (sessionKey=${sessionLabel})`), - }, - ); + const contextFiles: EmbeddedContextFile[] = buildBootstrapContextFiles(bootstrapFiles, { + maxChars: resolveBootstrapMaxChars(params.config), + warn: (message) => log.warn(`${message} (sessionKey=${sessionLabel})`), + }); const runAbortController = new AbortController(); const tools = createClawdbotCodingTools({ exec: { @@ -252,14 +224,9 @@ export async function compactEmbeddedPiSession(params: { channel: runtimeChannel, capabilities: runtimeCapabilities, }; - const sandboxInfo = buildEmbeddedSandboxInfo( - sandbox, - params.bashElevated, - ); + const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); const reasoningTagHint = isReasoningTagProvider(provider); - const userTimezone = resolveUserTimezone( - params.config?.agents?.defaults?.userTimezone, - ); + const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); const userTime = formatUserTime(new Date(), userTimezone); const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, @@ -274,9 +241,7 @@ export async function compactEmbeddedPiSession(params: { ownerNumbers: params.ownerNumbers, reasoningTagHint, heartbeatPrompt: isDefaultAgent - ? resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ) + ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) : undefined, skillsPrompt, runtimeInfo, @@ -294,19 +259,12 @@ export async function compactEmbeddedPiSession(params: { }); try { await prewarmSessionFile(params.sessionFile); - const sessionManager = guardSessionManager( - SessionManager.open(params.sessionFile), - ); + const sessionManager = guardSessionManager(SessionManager.open(params.sessionFile)); trackSessionManagerAccess(params.sessionFile); - const settingsManager = SettingsManager.create( - effectiveWorkspace, - agentDir, - ); + const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); ensurePiCompactionReserveTokens({ settingsManager, - minReserveTokens: resolveCompactionReserveTokensFloor( - params.config, - ), + minReserveTokens: resolveCompactionReserveTokensFloor(params.config), }); const additionalExtensionPaths = buildEmbeddedExtensionPaths({ cfg: params.config, @@ -321,9 +279,7 @@ export async function compactEmbeddedPiSession(params: { sandboxEnabled: !!sandbox?.enabled, }); - let session: Awaited< - ReturnType - >["session"]; + let session: Awaited>["session"]; ({ session } = await createAgentSession({ cwd: resolvedWorkspace, agentDir, diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index fbd9d4ec03b..a7fd27ebb76 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -60,9 +60,7 @@ function buildContextPruningExtension(params: { } function resolveCompactionMode(cfg?: ClawdbotConfig): "default" | "safeguard" { - return cfg?.agents?.defaults?.compaction?.mode === "safeguard" - ? "safeguard" - : "default"; + return cfg?.agents?.defaults?.compaction?.mode === "safeguard" ? "safeguard" : "default"; } export function buildEmbeddedExtensionPaths(params: { diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 4016e392ccd..220d1c2d635 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -84,9 +84,7 @@ function createStreamFnWithExtraParams( return undefined; } - log.debug( - `creating streamFn wrapper with params: ${JSON.stringify(streamParams)}`, - ); + log.debug(`creating streamFn wrapper with params: ${JSON.stringify(streamParams)}`); const underlying = baseStreamFn ?? streamSimple; const wrappedStreamFn: StreamFn = (model, context, options) => @@ -116,15 +114,10 @@ export function applyExtraParamsToAgent( modelId, thinkLevel, }); - const wrappedStreamFn = createStreamFnWithExtraParams( - agent.streamFn, - extraParams, - ); + const wrappedStreamFn = createStreamFnWithExtraParams(agent.streamFn, extraParams); if (wrappedStreamFn) { - log.debug( - `applying extraParams to agent streamFn for ${provider}/${modelId}`, - ); + log.debug(`applying extraParams to agent streamFn for ${provider}/${modelId}`); agent.streamFn = wrappedStreamFn; } } diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index be22256e3dc..0405f5e2719 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -37,10 +37,7 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([ "maxProperties", ]); -function findUnsupportedSchemaKeywords( - schema: unknown, - path: string, -): string[] { +function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] { if (!schema || typeof schema !== "object") return []; if (Array.isArray(schema)) { return schema.flatMap((item, index) => @@ -54,22 +51,14 @@ function findUnsupportedSchemaKeywords( violations.push(`${path}.${key}`); } if (value && typeof value === "object") { - violations.push( - ...findUnsupportedSchemaKeywords(value, `${path}.${key}`), - ); + violations.push(...findUnsupportedSchemaKeywords(value, `${path}.${key}`)); } } return violations; } -export function logToolSchemasForGoogle(params: { - tools: AgentTool[]; - provider: string; -}) { - if ( - params.provider !== "google-antigravity" && - params.provider !== "google-gemini-cli" - ) { +export function logToolSchemasForGoogle(params: { tools: AgentTool[]; provider: string }) { + if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") { return; } const toolNames = params.tools.map((tool, index) => `${index}:${tool.name}`); @@ -79,10 +68,7 @@ export function logToolSchemasForGoogle(params: { tools: toolNames, }); for (const [index, tool] of params.tools.entries()) { - const violations = findUnsupportedSchemaKeywords( - tool.parameters, - `${tool.name}.parameters`, - ); + const violations = findUnsupportedSchemaKeywords(tool.parameters, `${tool.name}.parameters`); if (violations.length > 0) { log.warn("google tool schema has unsupported keywords", { index, @@ -110,8 +96,7 @@ function hasGoogleTurnOrderingMarker(sessionManager: SessionManager): boolean { .some( (entry) => (entry as CustomEntryLike)?.type === "custom" && - (entry as CustomEntryLike)?.customType === - GOOGLE_TURN_ORDERING_CUSTOM_TYPE, + (entry as CustomEntryLike)?.customType === GOOGLE_TURN_ORDERING_CUSTOM_TYPE, ); } catch { return false; @@ -138,9 +123,7 @@ export function applyGoogleTurnOrderingFix(params: { if (!isGoogleModelApi(params.modelApi)) { return { messages: params.messages, didPrepend: false }; } - const first = params.messages[0] as - | { role?: unknown; content?: unknown } - | undefined; + const first = params.messages[0] as { role?: unknown; content?: unknown } | undefined; if (first?.role !== "assistant") { return { messages: params.messages, didPrepend: false }; } @@ -148,9 +131,7 @@ export function applyGoogleTurnOrderingFix(params: { const didPrepend = sanitized !== params.messages; if (didPrepend && !hasGoogleTurnOrderingMarker(params.sessionManager)) { const warn = params.warn ?? ((message: string) => log.warn(message)); - warn( - `google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`, - ); + warn(`google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`); markGoogleTurnOrderingMarker(params.sessionManager); } return { messages: sanitized, didPrepend }; @@ -162,14 +143,10 @@ export async function sanitizeSessionHistory(params: { sessionManager: SessionManager; sessionId: string; }): Promise { - const sanitizedImages = await sanitizeSessionMessagesImages( - params.messages, - "session:history", - { - sanitizeToolCallIds: isGoogleModelApi(params.modelApi), - enforceToolCallLast: params.modelApi === "anthropic-messages", - }, - ); + const sanitizedImages = await sanitizeSessionMessagesImages(params.messages, "session:history", { + sanitizeToolCallIds: isGoogleModelApi(params.modelApi), + enforceToolCallLast: params.modelApi === "anthropic-messages", + }); const repairedTools = sanitizeToolUseResultPairing(sanitizedImages); const downgraded = isGoogleModelApi(params.modelApi) diff --git a/src/agents/pi-embedded-runner/history.ts b/src/agents/pi-embedded-runner/history.ts index 68e1f651503..921bd47e3f9 100644 --- a/src/agents/pi-embedded-runner/history.ts +++ b/src/agents/pi-embedded-runner/history.ts @@ -38,8 +38,7 @@ export function getDmHistoryLimitFromSessionKey( if (!sessionKey || !config) return undefined; const parts = sessionKey.split(":").filter(Boolean); - const providerParts = - parts.length >= 3 && parts[0] === "agent" ? parts.slice(2) : parts; + const providerParts = parts.length >= 3 && parts[0] === "agent" ? parts.slice(2) : parts; const provider = providerParts[0]?.toLowerCase(); if (!provider) return undefined; diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 2dec21affec..5e2ffa540bf 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -1,8 +1,5 @@ import type { Api, Model } from "@mariozechner/pi-ai"; -import { - discoverAuthStorage, - discoverModels, -} from "@mariozechner/pi-coding-agent"; +import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; import type { ClawdbotConfig } from "../../config/config.js"; import { resolveClawdbotAgentDir } from "../agent-paths.js"; @@ -15,9 +12,7 @@ export function buildModelAliasLines(cfg?: ClawdbotConfig) { for (const [keyRaw, entryRaw] of Object.entries(models)) { const model = String(keyRaw ?? "").trim(); if (!model) continue; - const alias = String( - (entryRaw as { alias?: string } | undefined)?.alias ?? "", - ).trim(); + const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); if (!alias) continue; entries.push({ alias, model }); } @@ -67,10 +62,8 @@ export function resolveModel( reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: - providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS, - maxTokens: - providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS, + contextWindow: providerCfg?.models?.[0]?.contextWindow ?? DEFAULT_CONTEXT_TOKENS, + maxTokens: providerCfg?.models?.[0]?.maxTokens ?? DEFAULT_CONTEXT_TOKENS, } as Model); return { model: fallbackModel, authStorage, modelRegistry }; } diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 49c4e4abdb9..8127ed2ecd7 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -14,11 +14,7 @@ import { evaluateContextWindowGuard, resolveContextWindowInfo, } from "../context-window-guard.js"; -import { - DEFAULT_CONTEXT_TOKENS, - DEFAULT_MODEL, - DEFAULT_PROVIDER, -} from "../defaults.js"; +import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { FailoverError, resolveFailoverStatus } from "../failover-error.js"; import { ensureAuthProfileStore, @@ -58,13 +54,10 @@ type ApiKeyInfo = { export async function runEmbeddedPiAgent( params: RunEmbeddedPiAgentParams, ): Promise { - const sessionLane = resolveSessionLane( - params.sessionKey?.trim() || params.sessionId, - ); + const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId); const globalLane = resolveGlobalLane(params.lane); const enqueueGlobal = - params.enqueue ?? - ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); + params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); return enqueueCommandInLane(sessionLane, () => enqueueGlobal(async () => { @@ -72,8 +65,7 @@ export async function runEmbeddedPiAgent( const resolvedWorkspace = resolveUserPath(params.workspaceDir); const prevCwd = process.cwd(); - const provider = - (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; + const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER; const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL; const agentDir = params.agentDir ?? resolveClawdbotAgentDir(); await ensureClawdbotModelsJson(params.config, agentDir); @@ -124,12 +116,9 @@ export async function runEmbeddedPiAgent( preferredProfile: explicitProfileId, }); if (explicitProfileId && !profileOrder.includes(explicitProfileId)) { - throw new Error( - `Auth profile "${explicitProfileId}" is not configured for ${provider}.`, - ); + throw new Error(`Auth profile "${explicitProfileId}" is not configured for ${provider}.`); } - const profileCandidates = - profileOrder.length > 0 ? profileOrder : [undefined]; + const profileCandidates = profileOrder.length > 0 ? profileOrder : [undefined]; let profileIndex = 0; const initialThinkLevel = params.thinkLevel ?? "off"; @@ -150,9 +139,8 @@ export async function runEmbeddedPiAgent( const applyApiKeyInfo = async (candidate?: string): Promise => { apiKeyInfo = await resolveApiKeyForCandidate(candidate); if (model.provider === "github-copilot") { - const { resolveCopilotApiToken } = await import( - "../../providers/github-copilot-token.js" - ); + const { resolveCopilotApiToken } = + await import("../../providers/github-copilot-token.js"); const copilotToken = await resolveCopilotApiToken({ githubToken: apiKeyInfo.apiKey, }); @@ -238,13 +226,7 @@ export async function runEmbeddedPiAgent( enforceFinalTag: params.enforceFinalTag, }); - const { - aborted, - promptError, - timedOut, - sessionIdUsed, - lastAssistant, - } = attempt; + const { aborted, promptError, timedOut, sessionIdUsed, lastAssistant } = attempt; if (promptError && !aborted) { const errorText = describeUnknownError(promptError); @@ -273,11 +255,7 @@ export async function runEmbeddedPiAgent( }; } const promptFailoverReason = classifyFailoverReason(errorText); - if ( - promptFailoverReason && - promptFailoverReason !== "timeout" && - lastProfileId - ) { + if (promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId) { await markAuthProfileFailure({ store: authStore, profileId: lastProfileId, @@ -320,14 +298,11 @@ export async function runEmbeddedPiAgent( } const fallbackConfigured = - (params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > - 0; + (params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0; const authFailure = isAuthAssistantError(lastAssistant); const rateLimitFailure = isRateLimitAssistantError(lastAssistant); const failoverFailure = isFailoverAssistantError(lastAssistant); - const assistantFailoverReason = classifyFailoverReason( - lastAssistant?.errorMessage ?? "", - ); + const assistantFailoverReason = classifyFailoverReason(lastAssistant?.errorMessage ?? ""); const cloudCodeAssistFormatError = attempt.cloudCodeAssistFormatError; // Treat timeout as potential rate limit (Antigravity hangs on rate limit) @@ -406,8 +381,7 @@ export async function runEmbeddedPiAgent( sessionKey: params.sessionKey ?? params.sessionId, verboseLevel: params.verboseLevel, reasoningLevel: params.reasoningLevel, - inlineToolResultsAllowed: - !params.onPartialReply && !params.onToolResult, + inlineToolResultsAllowed: !params.onPartialReply && !params.onToolResult, }); log.debug( diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index f3b77416fc2..8b2e9f7107b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -4,11 +4,7 @@ import os from "node:os"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AssistantMessage } from "@mariozechner/pi-ai"; import { streamSimple } from "@mariozechner/pi-ai"; -import { - createAgentSession, - SessionManager, - SettingsManager, -} from "@mariozechner/pi-coding-agent"; +import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js"; @@ -41,19 +37,13 @@ import { loadWorkspaceSkillEntries, resolveSkillsPromptForRun, } from "../../skills.js"; -import { - filterBootstrapFilesForSession, - loadWorkspaceBootstrapFiles, -} from "../../workspace.js"; +import { filterBootstrapFilesForSession, loadWorkspaceBootstrapFiles } from "../../workspace.js"; import { isAbortError } from "../abort.js"; import { buildEmbeddedExtensionPaths } from "../extensions.js"; import { applyExtraParamsToAgent } from "../extra-params.js"; import { logToolSchemasForGoogle, sanitizeSessionHistory } from "../google.js"; -import { - getDmHistoryLimitFromSessionKey, - limitHistoryTurns, -} from "../history.js"; +import { getDmHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js"; import { log } from "../logger.js"; import { buildModelAliasLines } from "../model.js"; import { @@ -62,15 +52,9 @@ import { setActiveEmbeddedRun, } from "../runs.js"; import { buildEmbeddedSandboxInfo } from "../sandbox-info.js"; -import { - prewarmSessionFile, - trackSessionManagerAccess, -} from "../session-manager-cache.js"; +import { prewarmSessionFile, trackSessionManagerAccess } from "../session-manager-cache.js"; import { prepareSessionManagerForRun } from "../session-manager-init.js"; -import { - buildEmbeddedSystemPrompt, - createSystemPromptOverride, -} from "../system-prompt.js"; +import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js"; import { splitSdkTools } from "../tool-split.js"; import { formatUserTime, @@ -79,10 +63,7 @@ import { resolveUserTimezone, } from "../utils.js"; -import type { - EmbeddedRunAttemptParams, - EmbeddedRunAttemptResult, -} from "./types.js"; +import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js"; export async function runEmbeddedAttempt( params: EmbeddedRunAttemptParams, @@ -113,8 +94,7 @@ export async function runEmbeddedAttempt( let restoreSkillEnv: (() => void) | undefined; process.chdir(effectiveWorkspace); try { - const shouldLoadSkillEntries = - !params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills; + const shouldLoadSkillEntries = !params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills; const skillEntries = shouldLoadSkillEntries ? loadWorkspaceSkillEntries(effectiveWorkspace) : []; @@ -171,9 +151,7 @@ export async function runEmbeddedAttempt( logToolSchemasForGoogle({ tools, provider: params.provider }); const machineName = await getMachineDisplayName(); - const runtimeChannel = normalizeMessageChannel( - params.messageChannel ?? params.messageProvider, - ); + const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider); const runtimeCapabilities = runtimeChannel ? (resolveChannelCapabilities({ cfg: params.config, @@ -193,9 +171,7 @@ export async function runEmbeddedAttempt( const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); const reasoningTagHint = isReasoningTagProvider(params.provider); - const userTimezone = resolveUserTimezone( - params.config?.agents?.defaults?.userTimezone, - ); + const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); const userTime = formatUserTime(new Date(), userTimezone); const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, @@ -211,9 +187,7 @@ export async function runEmbeddedAttempt( ownerNumbers: params.ownerNumbers, reasoningTagHint, heartbeatPrompt: isDefaultAgent - ? resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ) + ? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt) : undefined, skillsPrompt, runtimeInfo, @@ -231,9 +205,7 @@ export async function runEmbeddedAttempt( }); let sessionManager: ReturnType | undefined; - let session: - | Awaited>["session"] - | undefined; + let session: Awaited>["session"] | undefined; try { const hadSessionFile = await fs .stat(params.sessionFile) @@ -241,9 +213,7 @@ export async function runEmbeddedAttempt( .catch(() => false); await prewarmSessionFile(params.sessionFile); - sessionManager = guardSessionManager( - SessionManager.open(params.sessionFile), - ); + sessionManager = guardSessionManager(SessionManager.open(params.sessionFile)); trackSessionManagerAccess(params.sessionFile); await prepareSessionManagerForRun({ @@ -254,10 +224,7 @@ export async function runEmbeddedAttempt( cwd: effectiveWorkspace, }); - const settingsManager = SettingsManager.create( - effectiveWorkspace, - agentDir, - ); + const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); ensurePiCompactionReserveTokens({ settingsManager, minReserveTokens: resolveCompactionReserveTokensFloor(params.config), @@ -412,9 +379,7 @@ export async function runEmbeddedAttempt( let promptError: unknown = null; try { const promptStartedAt = Date.now(); - log.debug( - `embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`, - ); + log.debug(`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`); try { await activeSession.prompt(params.prompt, { images: params.images }); } catch (err) { @@ -448,15 +413,12 @@ export async function runEmbeddedAttempt( const lastAssistant = messagesSnapshot .slice() .reverse() - .find((m) => (m as AgentMessage)?.role === "assistant") as - | AssistantMessage - | undefined; + .find((m) => (m as AgentMessage)?.role === "assistant") as AssistantMessage | undefined; const toolMetasNormalized = toolMetas .filter( (entry): entry is { toolName: string; meta?: string } => - typeof entry.toolName === "string" && - entry.toolName.trim().length > 0, + typeof entry.toolName === "string" && entry.toolName.trim().length > 0, ) .map((entry) => ({ toolName: entry.toolName, meta: entry.meta })); @@ -473,8 +435,7 @@ export async function runEmbeddedAttempt( messagingToolSentTexts: getMessagingToolSentTexts(), messagingToolSentTargets: getMessagingToolSentTargets(), cloudCodeAssistFormatError: Boolean( - lastAssistant?.errorMessage && - isCloudCodeAssistFormatError(lastAssistant.errorMessage), + lastAssistant?.errorMessage && isCloudCodeAssistFormatError(lastAssistant.errorMessage), ), }; } finally { diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index 21298860dc3..7b6349ad5d3 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -1,9 +1,5 @@ import type { ImageContent } from "@mariozechner/pi-ai"; -import type { - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../../../auto-reply/thinking.js"; +import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js"; import type { ClawdbotConfig } from "../../../config/config.js"; import type { enqueueCommand } from "../../../process/command-queue.js"; import type { ExecElevatedDefaults } from "../../bash-tools.js"; @@ -42,10 +38,7 @@ export type RunEmbeddedPiAgentParams = { runId: string; abortSignal?: AbortSignal; shouldEmitToolResult?: () => boolean; - onPartialReply?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; + onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onAssistantMessageStart?: () => void | Promise; onBlockReply?: (payload: { text?: string; @@ -55,18 +48,9 @@ export type RunEmbeddedPiAgentParams = { onBlockReplyFlush?: () => void | Promise; blockReplyBreak?: "text_end" | "message_end"; blockReplyChunking?: BlockReplyChunking; - onReasoningStream?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; - onToolResult?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; - onAgentEvent?: (evt: { - stream: string; - data: Record; - }) => void; + onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + onAgentEvent?: (evt: { stream: string; data: Record }) => void; lane?: string; enqueue?: typeof enqueueCommand; extraSystemPrompt?: string; diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 0de4413aa2a..0b8115ac303 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -1,13 +1,7 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { parseReplyDirectives } from "../../../auto-reply/reply/reply-directives.js"; -import type { - ReasoningLevel, - VerboseLevel, -} from "../../../auto-reply/thinking.js"; -import { - isSilentReplyText, - SILENT_REPLY_TOKEN, -} from "../../../auto-reply/tokens.js"; +import type { ReasoningLevel, VerboseLevel } from "../../../auto-reply/thinking.js"; +import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../../auto-reply/tokens.js"; import { formatToolAggregate } from "../../../auto-reply/tool-meta.js"; import type { ClawdbotConfig } from "../../../config/config.js"; import { formatAssistantErrorText } from "../../pi-embedded-helpers.js"; @@ -57,9 +51,7 @@ export function buildEmbeddedRunPayloads(params: { if (errorText) replyItems.push({ text: errorText, isError: true }); const inlineToolResults = - params.inlineToolResultsAllowed && - params.verboseLevel === "on" && - params.toolMetas.length > 0; + params.inlineToolResultsAllowed && params.verboseLevel === "on" && params.toolMetas.length > 0; if (inlineToolResults) { for (const { toolName, meta } of params.toolMetas) { const agg = formatToolAggregate(toolName, meta ? [meta] : []); @@ -90,9 +82,7 @@ export function buildEmbeddedRunPayloads(params: { : ""; if (reasoningText) replyItems.push({ text: reasoningText }); - const fallbackAnswerText = params.lastAssistant - ? extractAssistantText(params.lastAssistant) - : ""; + const fallbackAnswerText = params.lastAssistant ? extractAssistantText(params.lastAssistant) : ""; const answerTexts = params.assistantTexts.length ? params.assistantTexts : fallbackAnswerText @@ -108,11 +98,7 @@ export function buildEmbeddedRunPayloads(params: { replyToTag, replyToCurrent, } = parseReplyDirectives(text); - if ( - !cleanedText && - (!mediaUrls || mediaUrls.length === 0) && - !audioAsVoice - ) { + if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice) { continue; } replyItems.push({ @@ -135,12 +121,10 @@ export function buildEmbeddedRunPayloads(params: { replyToId: item.replyToId, replyToTag: item.replyToTag, replyToCurrent: item.replyToCurrent, - audioAsVoice: - item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), + audioAsVoice: item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), })) .filter((p) => { - if (!p.text && !p.mediaUrl && (!p.mediaUrls || p.mediaUrls.length === 0)) - return false; + if (!p.text && !p.mediaUrl && (!p.mediaUrls || p.mediaUrls.length === 0)) return false; if (p.text && isSilentReplyText(p.text, SILENT_REPLY_TOKEN)) return false; return true; }); diff --git a/src/agents/pi-embedded-runner/run/types.ts b/src/agents/pi-embedded-runner/run/types.ts index f57e81c1d94..e132511f0bf 100644 --- a/src/agents/pi-embedded-runner/run/types.ts +++ b/src/agents/pi-embedded-runner/run/types.ts @@ -1,20 +1,8 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - Api, - AssistantMessage, - ImageContent, - Model, -} from "@mariozechner/pi-ai"; -import type { - discoverAuthStorage, - discoverModels, -} from "@mariozechner/pi-coding-agent"; +import type { Api, AssistantMessage, ImageContent, Model } from "@mariozechner/pi-ai"; +import type { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; -import type { - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../../../auto-reply/thinking.js"; +import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js"; import type { ClawdbotConfig } from "../../../config/config.js"; import type { ExecElevatedDefaults } from "../../bash-tools.js"; import type { MessagingToolSend } from "../../pi-embedded-messaging.js"; @@ -54,10 +42,7 @@ export type EmbeddedRunAttemptParams = { runId: string; abortSignal?: AbortSignal; shouldEmitToolResult?: () => boolean; - onPartialReply?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; + onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onAssistantMessageStart?: () => void | Promise; onBlockReply?: (payload: { text?: string; @@ -67,18 +52,9 @@ export type EmbeddedRunAttemptParams = { onBlockReplyFlush?: () => void | Promise; blockReplyBreak?: "text_end" | "message_end"; blockReplyChunking?: BlockReplyChunking; - onReasoningStream?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; - onToolResult?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; - onAgentEvent?: (evt: { - stream: string; - data: Record; - }) => void; + onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + onAgentEvent?: (evt: { stream: string; data: Record }) => void; extraSystemPrompt?: string; ownerNumbers?: string[]; enforceFinalTag?: boolean; diff --git a/src/agents/pi-embedded-runner/runs.ts b/src/agents/pi-embedded-runner/runs.ts index 838038eb7f1..abc207744a1 100644 --- a/src/agents/pi-embedded-runner/runs.ts +++ b/src/agents/pi-embedded-runner/runs.ts @@ -12,10 +12,7 @@ type EmbeddedRunWaiter = { }; const EMBEDDED_RUN_WAITERS = new Map>(); -export function queueEmbeddedPiMessage( - sessionId: string, - text: string, -): boolean { +export function queueEmbeddedPiMessage(sessionId: string, text: string): boolean { const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId); if (!handle) return false; if (!handle.isStreaming()) return false; @@ -41,12 +38,8 @@ export function isEmbeddedPiRunStreaming(sessionId: string): boolean { return handle.isStreaming(); } -export function waitForEmbeddedPiRunEnd( - sessionId: string, - timeoutMs = 15_000, -): Promise { - if (!sessionId || !ACTIVE_EMBEDDED_RUNS.has(sessionId)) - return Promise.resolve(true); +export function waitForEmbeddedPiRunEnd(sessionId: string, timeoutMs = 15_000): Promise { + if (!sessionId || !ACTIVE_EMBEDDED_RUNS.has(sessionId)) return Promise.resolve(true); return new Promise((resolve) => { const waiters = EMBEDDED_RUN_WAITERS.get(sessionId) ?? new Set(); const waiter: EmbeddedRunWaiter = { @@ -81,17 +74,11 @@ function notifyEmbeddedRunEnded(sessionId: string) { } } -export function setActiveEmbeddedRun( - sessionId: string, - handle: EmbeddedPiQueueHandle, -) { +export function setActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueHandle) { ACTIVE_EMBEDDED_RUNS.set(sessionId, handle); } -export function clearActiveEmbeddedRun( - sessionId: string, - handle: EmbeddedPiQueueHandle, -) { +export function clearActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueHandle) { if (ACTIVE_EMBEDDED_RUNS.get(sessionId) === handle) { ACTIVE_EMBEDDED_RUNS.delete(sessionId); notifyEmbeddedRunEnded(sessionId); diff --git a/src/agents/pi-embedded-runner/sandbox-info.ts b/src/agents/pi-embedded-runner/sandbox-info.ts index 59aad6d8a39..1ffc76f0ce9 100644 --- a/src/agents/pi-embedded-runner/sandbox-info.ts +++ b/src/agents/pi-embedded-runner/sandbox-info.ts @@ -7,15 +7,12 @@ export function buildEmbeddedSandboxInfo( execElevated?: ExecElevatedDefaults, ): EmbeddedSandboxInfo | undefined { if (!sandbox?.enabled) return undefined; - const elevatedAllowed = Boolean( - execElevated?.enabled && execElevated.allowed, - ); + const elevatedAllowed = Boolean(execElevated?.enabled && execElevated.allowed); return { enabled: true, workspaceDir: sandbox.workspaceDir, workspaceAccess: sandbox.workspaceAccess, - agentWorkspaceMount: - sandbox.workspaceAccess === "ro" ? "/agent" : undefined, + agentWorkspaceMount: sandbox.workspaceAccess === "ro" ? "/agent" : undefined, browserControlUrl: sandbox.browser?.controlUrl, browserNoVncUrl: sandbox.browser?.noVncUrl, hostBrowserAllowed: sandbox.browserAllowHostControl, diff --git a/src/agents/pi-embedded-runner/session-manager-init.ts b/src/agents/pi-embedded-runner/session-manager-init.ts index b0ab30c43e2..95c699947bd 100644 --- a/src/agents/pi-embedded-runner/session-manager-init.ts +++ b/src/agents/pi-embedded-runner/session-manager-init.ts @@ -23,21 +23,15 @@ export async function prepareSessionManagerForRun(params: { const sm = params.sessionManager as { sessionId: string; flushed: boolean; - fileEntries: Array< - SessionHeaderEntry | SessionMessageEntry | { type: string } - >; + fileEntries: Array; byId?: Map; labelsById?: Map; leafId?: string | null; }; - const header = sm.fileEntries.find( - (e): e is SessionHeaderEntry => e.type === "session", - ); + const header = sm.fileEntries.find((e): e is SessionHeaderEntry => e.type === "session"); const hasAssistant = sm.fileEntries.some( - (e) => - e.type === "message" && - (e as SessionMessageEntry).message?.role === "assistant", + (e) => e.type === "message" && (e as SessionMessageEntry).message?.role === "assistant", ); if (!params.hadSessionFile && header) { diff --git a/src/agents/pi-embedded-runner/tool-split.ts b/src/agents/pi-embedded-runner/tool-split.ts index a33196efc45..13e440a20ce 100644 --- a/src/agents/pi-embedded-runner/tool-split.ts +++ b/src/agents/pi-embedded-runner/tool-split.ts @@ -6,10 +6,7 @@ import { toToolDefinitions } from "../pi-tool-definition-adapter.js"; // and extended toolset remain consistent across providers. type AnyAgentTool = AgentTool; -export function splitSdkTools(options: { - tools: AnyAgentTool[]; - sandboxEnabled: boolean; -}): { +export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }): { builtInTools: AnyAgentTool[]; customTools: ReturnType; } { diff --git a/src/agents/pi-embedded-runner/utils.ts b/src/agents/pi-embedded-runner/utils.ts index 295b32d6d0d..ebb72ed4c8e 100644 --- a/src/agents/pi-embedded-runner/utils.ts +++ b/src/agents/pi-embedded-runner/utils.ts @@ -9,9 +9,7 @@ export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { return level; } -export function resolveExecToolDefaults( - config?: ClawdbotConfig, -): ExecToolDefaults | undefined { +export function resolveExecToolDefaults(config?: ClawdbotConfig): ExecToolDefaults | undefined { const tools = config?.tools; if (!tools) return undefined; if (!tools.exec) return tools.bash; @@ -23,9 +21,7 @@ export function resolveUserTimezone(configured?: string): string { const trimmed = configured?.trim(); if (trimmed) { try { - new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format( - new Date(), - ); + new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date()); return trimmed; } catch { // ignore invalid timezone @@ -35,10 +31,7 @@ export function resolveUserTimezone(configured?: string): string { return host?.trim() || "UTC"; } -export function formatUserTime( - date: Date, - timeZone: string, -): string | undefined { +export function formatUserTime(date: Date, timeZone: string): string | undefined { try { const parts = new Intl.DateTimeFormat("en-CA", { timeZone, @@ -54,14 +47,7 @@ export function formatUserTime( for (const part of parts) { if (part.type !== "literal") map[part.type] = part.value; } - if ( - !map.weekday || - !map.year || - !map.month || - !map.day || - !map.hour || - !map.minute - ) { + if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) { return undefined; } return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`; diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.ts b/src/agents/pi-embedded-subscribe.handlers.messages.ts index c31631f7366..98f9371413a 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.ts +++ b/src/agents/pi-embedded-subscribe.handlers.messages.ts @@ -47,21 +47,14 @@ export function handleMessageUpdate( assistantEvent && typeof assistantEvent === "object" ? (assistantEvent as Record) : undefined; - const evtType = - typeof assistantRecord?.type === "string" ? assistantRecord.type : ""; + const evtType = typeof assistantRecord?.type === "string" ? assistantRecord.type : ""; - if ( - evtType !== "text_delta" && - evtType !== "text_start" && - evtType !== "text_end" - ) { + if (evtType !== "text_delta" && evtType !== "text_start" && evtType !== "text_end") { return; } - const delta = - typeof assistantRecord?.delta === "string" ? assistantRecord.delta : ""; - const content = - typeof assistantRecord?.content === "string" ? assistantRecord.content : ""; + const delta = typeof assistantRecord?.delta === "string" ? assistantRecord.delta : ""; + const content = typeof assistantRecord?.content === "string" ? assistantRecord.content : ""; appendRawStream({ ts: Date.now(), @@ -103,9 +96,7 @@ export function handleMessageUpdate( if (ctx.state.streamReasoning) { // Handle partial tags: stream whatever reasoning is visible so far. - ctx.emitReasoningStream( - extractThinkingFromTaggedStream(ctx.state.deltaBuffer), - ); + ctx.emitReasoningStream(extractThinkingFromTaggedStream(ctx.state.deltaBuffer)); } const next = ctx @@ -140,11 +131,7 @@ export function handleMessageUpdate( } } - if ( - ctx.params.onBlockReply && - ctx.blockChunking && - ctx.state.blockReplyBreak === "text_end" - ) { + if (ctx.params.onBlockReply && ctx.blockChunking && ctx.state.blockReplyBreak === "text_end") { ctx.blockChunker?.drain({ force: false, emit: ctx.emitBlockChunk }); } @@ -182,29 +169,23 @@ export function handleMessageEnd( const text = ctx.stripBlockTags(rawText, { thinking: false, final: false }); const rawThinking = ctx.state.includeReasoning || ctx.state.streamReasoning - ? extractAssistantThinking(assistantMessage) || - extractThinkingFromTaggedText(rawText) + ? extractAssistantThinking(assistantMessage) || extractThinkingFromTaggedText(rawText) : ""; - const formattedReasoning = rawThinking - ? formatReasoningMessage(rawThinking) - : ""; + const formattedReasoning = rawThinking ? formatReasoningMessage(rawThinking) : ""; - const addedDuringMessage = - ctx.state.assistantTexts.length > ctx.state.assistantTextBaseline; + const addedDuringMessage = ctx.state.assistantTexts.length > ctx.state.assistantTextBaseline; const chunkerHasBuffered = ctx.blockChunker?.hasBuffered() ?? false; ctx.finalizeAssistantTexts({ text, addedDuringMessage, chunkerHasBuffered }); const onBlockReply = ctx.params.onBlockReply; const shouldEmitReasoning = Boolean( ctx.state.includeReasoning && - formattedReasoning && - onBlockReply && - formattedReasoning !== ctx.state.lastReasoningSent, + formattedReasoning && + onBlockReply && + formattedReasoning !== ctx.state.lastReasoningSent, ); const shouldEmitReasoningBeforeAnswer = - shouldEmitReasoning && - ctx.state.blockReplyBreak === "message_end" && - !addedDuringMessage; + shouldEmitReasoning && ctx.state.blockReplyBreak === "message_end" && !addedDuringMessage; const maybeEmitReasoning = () => { if (!shouldEmitReasoning || !formattedReasoning) return; ctx.state.lastReasoningSent = formattedReasoning; @@ -215,9 +196,7 @@ export function handleMessageEnd( if ( (ctx.state.blockReplyBreak === "message_end" || - (ctx.blockChunker - ? ctx.blockChunker.hasBuffered() - : ctx.state.blockBuffer.length > 0)) && + (ctx.blockChunker ? ctx.blockChunker.hasBuffered() : ctx.state.blockBuffer.length > 0)) && text && onBlockReply ) { @@ -238,17 +217,9 @@ export function handleMessageEnd( ); } else { ctx.state.lastBlockReplyText = text; - const { - text: cleanedText, - mediaUrls, - audioAsVoice, - } = parseReplyDirectives(text); + const { text: cleanedText, mediaUrls, audioAsVoice } = parseReplyDirectives(text); // Emit if there's content OR audioAsVoice flag (to propagate the flag). - if ( - cleanedText || - (mediaUrls && mediaUrls.length > 0) || - audioAsVoice - ) { + if (cleanedText || (mediaUrls && mediaUrls.length > 0) || audioAsVoice) { void onBlockReply({ text: cleanedText, mediaUrls: mediaUrls?.length ? mediaUrls : undefined, diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 62a3b5b8109..5476ba1ff2b 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -2,10 +2,7 @@ import type { AgentEvent } from "@mariozechner/pi-agent-core"; import { emitAgentEvent } from "../infra/agent-events.js"; import { normalizeTextForComparison } from "./pi-embedded-helpers.js"; -import { - isMessagingTool, - isMessagingToolSendAction, -} from "./pi-embedded-messaging.js"; +import { isMessagingTool, isMessagingToolSendAction } from "./pi-embedded-messaging.js"; import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; import { extractMessagingToolSend, @@ -29,12 +26,10 @@ export function handleToolExecutionStart( const args = evt.args; if (toolName === "read") { - const record = - args && typeof args === "object" ? (args as Record) : {}; + const record = args && typeof args === "object" ? (args as Record) : {}; const filePath = typeof record.path === "string" ? record.path.trim() : ""; if (!filePath) { - const argsPreview = - typeof args === "string" ? args.slice(0, 200) : undefined; + const argsPreview = typeof args === "string" ? args.slice(0, 200) : undefined; ctx.log.warn( `read tool called without path: toolCallId=${toolCallId} argsType=${typeof args}${argsPreview ? ` argsPreview=${argsPreview}` : ""}`, ); @@ -74,8 +69,7 @@ export function handleToolExecutionStart( // Track messaging tool sends (pending until confirmed in tool_execution_end). if (isMessagingTool(toolName)) { - const argsRecord = - args && typeof args === "object" ? (args as Record) : {}; + const argsRecord = args && typeof args === "object" ? (args as Record) : {}; const isMessagingSend = isMessagingToolSendAction(toolName, argsRecord); if (isMessagingSend) { const sendTarget = extractMessagingToolSend(toolName, argsRecord); @@ -83,13 +77,10 @@ export function handleToolExecutionStart( ctx.state.pendingMessagingTargets.set(toolCallId, sendTarget); } // Field names vary by tool: Discord/Slack use "content", sessions_send uses "message" - const text = - (argsRecord.content as string) ?? (argsRecord.message as string); + const text = (argsRecord.content as string) ?? (argsRecord.message as string); if (text && typeof text === "string") { ctx.state.pendingMessagingTexts.set(toolCallId, text); - ctx.log.debug( - `Tracking pending messaging text: tool=${toolName} len=${text.length}`, - ); + ctx.log.debug(`Tracking pending messaging text: tool=${toolName} len=${text.length}`); } } } @@ -154,12 +145,8 @@ export function handleToolExecutionEnd( ctx.state.pendingMessagingTexts.delete(toolCallId); if (!isToolError) { ctx.state.messagingToolSentTexts.push(pendingText); - ctx.state.messagingToolSentTextsNormalized.push( - normalizeTextForComparison(pendingText), - ); - ctx.log.debug( - `Committed messaging text: tool=${toolName} len=${pendingText.length}`, - ); + ctx.state.messagingToolSentTextsNormalized.push(normalizeTextForComparison(pendingText)); + ctx.log.debug(`Committed messaging text: tool=${toolName} len=${pendingText.length}`); ctx.trimMessagingToolSent(); } } diff --git a/src/agents/pi-embedded-subscribe.handlers.ts b/src/agents/pi-embedded-subscribe.handlers.ts index fdeeaf0d3ef..b3f755d704d 100644 --- a/src/agents/pi-embedded-subscribe.handlers.ts +++ b/src/agents/pi-embedded-subscribe.handlers.ts @@ -19,9 +19,7 @@ import type { EmbeddedPiSubscribeEvent, } from "./pi-embedded-subscribe.handlers.types.js"; -export function createEmbeddedPiSessionEventHandler( - ctx: EmbeddedPiSubscribeContext, -) { +export function createEmbeddedPiSessionEventHandler(ctx: EmbeddedPiSubscribeContext) { return (evt: EmbeddedPiSubscribeEvent) => { switch (evt.type) { case "message_start": diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/pi-embedded-subscribe.handlers.types.ts index cd95c46305b..48963e3ff30 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/pi-embedded-subscribe.handlers.types.ts @@ -56,10 +56,7 @@ export type EmbeddedPiSubscribeContext = { shouldEmitToolResult: () => boolean; emitToolSummary: (toolName?: string, meta?: string) => void; - stripBlockTags: ( - text: string, - state: { thinking: boolean; final: boolean }, - ) => string; + stripBlockTags: (text: string, state: { thinking: boolean; final: boolean }) => string; emitBlockChunk: (text: string) => void; flushBlockReplyBuffer: () => void; emitReasoningStream: (text: string) => void; diff --git a/src/agents/pi-embedded-subscribe.raw-stream.ts b/src/agents/pi-embedded-subscribe.raw-stream.ts index dbf6604d271..69953d1ce24 100644 --- a/src/agents/pi-embedded-subscribe.raw-stream.ts +++ b/src/agents/pi-embedded-subscribe.raw-stream.ts @@ -21,10 +21,7 @@ export function appendRawStream(payload: Record) { } } try { - void fs.promises.appendFile( - RAW_STREAM_PATH, - `${JSON.stringify(payload)}\n`, - ); + void fs.promises.appendFile(RAW_STREAM_PATH, `${JSON.stringify(payload)}\n`); } catch { // ignore raw stream write failures } diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts index 3b412206ce6..30336ed38ec 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts @@ -28,9 +28,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-flush-test", onBlockReply, onBlockReplyFlush, @@ -87,9 +85,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReplyFlush = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-flush-buffer", onBlockReply, onBlockReplyFlush, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts index d3fb534e2d1..964ff5b3ab3 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts @@ -25,9 +25,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", @@ -66,9 +64,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", @@ -107,9 +103,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts index 35244a23408..60460571309 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts @@ -28,9 +28,7 @@ describe("subscribeEmbeddedPiSession", () => { // No onBlockReplyFlush provided subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-no-flush", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts index 6e3c968a99d..00138a7f9ab 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts @@ -25,9 +25,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", @@ -66,9 +64,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts index ec9eb27a7ea..86963f08cad 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts @@ -28,9 +28,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", @@ -74,9 +72,7 @@ describe("subscribeEmbeddedPiSession", () => { }; const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", }); @@ -100,9 +96,7 @@ describe("subscribeEmbeddedPiSession", () => { }; const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", reasoningMode: "on", }); @@ -132,9 +126,7 @@ describe("subscribeEmbeddedPiSession", () => { }; const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", blockReplyChunking: { minChars: 50, maxChars: 200 }, // Chunking enabled }); @@ -149,8 +141,6 @@ describe("subscribeEmbeddedPiSession", () => { handler?.({ type: "message_end", message: assistantMessage }); - expect(subscription.assistantTexts).toEqual([ - "Response from non-streaming model", - ]); + expect(subscription.assistantTexts).toEqual(["Response from non-streaming model"]); }); }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts index fcd7fcda330..d8fcf94c91e 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", @@ -78,9 +76,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts index a01c80acd3e..e7cb7fc3788 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -46,58 +44,50 @@ describe("subscribeEmbeddedPiSession", () => { handler?.({ type: "message_end", message: assistantMessage }); expect(onBlockReply).toHaveBeenCalledTimes(2); - expect(onBlockReply.mock.calls[0][0].text).toBe( - "Reasoning:\n_Because it helps_", - ); + expect(onBlockReply.mock.calls[0][0].text).toBe("Reasoning:\n_Because it helps_"); expect(onBlockReply.mock.calls[1][0].text).toBe("Final answer"); }); - it.each( - THINKING_TAG_CASES, - )("promotes <%s> tags to thinking blocks at write-time", ({ - open, - close, - }) => { - let handler: ((evt: unknown) => void) | undefined; - const session: StubSession = { - subscribe: (fn) => { - handler = fn; - return () => {}; - }, - }; - - const onBlockReply = vi.fn(); - - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], - runId: "run", - onBlockReply, - blockReplyBreak: "message_end", - reasoningMode: "on", - }); - - const assistantMessage = { - role: "assistant", - content: [ - { - type: "text", - text: `${open}\nBecause it helps\n${close}\n\nFinal answer`, + it.each(THINKING_TAG_CASES)( + "promotes <%s> tags to thinking blocks at write-time", + ({ open, close }) => { + let handler: ((evt: unknown) => void) | undefined; + const session: StubSession = { + subscribe: (fn) => { + handler = fn; + return () => {}; }, - ], - } as AssistantMessage; + }; - handler?.({ type: "message_end", message: assistantMessage }); + const onBlockReply = vi.fn(); - expect(onBlockReply).toHaveBeenCalledTimes(2); - expect(onBlockReply.mock.calls[0][0].text).toBe( - "Reasoning:\n_Because it helps_", - ); - expect(onBlockReply.mock.calls[1][0].text).toBe("Final answer"); + subscribeEmbeddedPiSession({ + session: session as unknown as Parameters[0]["session"], + runId: "run", + onBlockReply, + blockReplyBreak: "message_end", + reasoningMode: "on", + }); - expect(assistantMessage.content).toEqual([ - { type: "thinking", thinking: "Because it helps" }, - { type: "text", text: "Final answer" }, - ]); - }); + const assistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `${open}\nBecause it helps\n${close}\n\nFinal answer`, + }, + ], + } as AssistantMessage; + + handler?.({ type: "message_end", message: assistantMessage }); + + expect(onBlockReply).toHaveBeenCalledTimes(2); + expect(onBlockReply.mock.calls[0][0].text).toBe("Reasoning:\n_Because it helps_"); + expect(onBlockReply.mock.calls[1][0].text).toBe("Final answer"); + + expect(assistantMessage.content).toEqual([ + { type: "thinking", thinking: "Because it helps" }, + { type: "text", text: "Final answer" }, + ]); + }, + ); }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts index c7c7f1c0c68..c9d5dd0810c 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts @@ -27,9 +27,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", enforceFinalTag: true, onPartialReply, @@ -76,9 +74,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onPartialReply, }); @@ -107,9 +103,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts index d214f2e433f..398c2829df2 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts @@ -25,9 +25,7 @@ describe("subscribeEmbeddedPiSession", () => { const onToolResult = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-canvas-tool", verboseLevel: "on", onToolResult, @@ -59,9 +57,7 @@ describe("subscribeEmbeddedPiSession", () => { const onToolResult = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-tool-off", shouldEmitToolResult: () => false, onToolResult, @@ -88,9 +84,7 @@ describe("subscribeEmbeddedPiSession", () => { const onToolResult = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-tool-override", verboseLevel: "off", shouldEmitToolResult: () => true, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts index 0c85cfd0808..8b4d539465c 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts @@ -24,9 +24,7 @@ describe("subscribeEmbeddedPiSession", () => { }; const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", reasoningMode: "on", }); @@ -80,9 +78,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", reasoningMode: "on", onPartialReply, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts index 45ac3818456..d8d868541ad 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -58,9 +56,7 @@ describe("subscribeEmbeddedPiSession", () => { handler?.({ type: "message_end", message: assistantMessage }); expect(onBlockReply).toHaveBeenCalledTimes(3); - expect(onBlockReply.mock.calls[1][0].text).toBe( - " ```js\n const x = 1;\n ```", - ); + expect(onBlockReply.mock.calls[1][0].text).toBe(" ```js\n const x = 1;\n ```"); }); it("accepts longer fence markers for close", () => { let handler: ((evt: unknown) => void) | undefined; @@ -74,9 +70,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts index 3d90b946e9f..f786b104f1f 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -77,9 +75,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts index 2436fc9f160..aef772deb0d 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts index b576c5604b8..59973be7e21 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const subscription = subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -60,10 +58,7 @@ describe("subscribeEmbeddedPiSession", () => { expect(onBlockReply).toHaveBeenCalledTimes(2); expect(onBlockReply.mock.calls[0][0].text).toBe("First block line"); expect(onBlockReply.mock.calls[1][0].text).toBe("Second block line"); - expect(subscription.assistantTexts).toEqual([ - "First block line", - "Second block line", - ]); + expect(subscription.assistantTexts).toEqual(["First block line", "Second block line"]); }); it("avoids splitting inside fenced code blocks", () => { let handler: ((evt: unknown) => void) | undefined; @@ -77,9 +72,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -110,9 +103,7 @@ describe("subscribeEmbeddedPiSession", () => { expect(onBlockReply).toHaveBeenCalledTimes(3); expect(onBlockReply.mock.calls[0][0].text).toBe("Intro"); - expect(onBlockReply.mock.calls[1][0].text).toBe( - "```bash\nline1\nline2\n```", - ); + expect(onBlockReply.mock.calls[1][0].text).toBe("```bash\nline1\nline2\n```"); expect(onBlockReply.mock.calls[2][0].text).toBe("Outro"); }); }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts index f7227031ebc..3af91731f3c 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts @@ -14,141 +14,136 @@ describe("subscribeEmbeddedPiSession", () => { { tag: "antthinking", open: "", close: "" }, ] as const; - it.each( - THINKING_TAG_CASES, - )("streams <%s> reasoning via onReasoningStream without leaking into final text", ({ - open, - close, - }) => { - let handler: ((evt: unknown) => void) | undefined; - const session: StubSession = { - subscribe: (fn) => { - handler = fn; - return () => {}; - }, - }; - - const onReasoningStream = vi.fn(); - const onBlockReply = vi.fn(); - - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], - runId: "run", - onReasoningStream, - onBlockReply, - blockReplyBreak: "message_end", - reasoningMode: "stream", - }); - - handler?.({ - type: "message_update", - message: { role: "assistant" }, - assistantMessageEvent: { - type: "text_delta", - delta: `${open}\nBecause`, - }, - }); - - handler?.({ - type: "message_update", - message: { role: "assistant" }, - assistantMessageEvent: { - type: "text_delta", - delta: ` it helps\n${close}\n\nFinal answer`, - }, - }); - - const assistantMessage = { - role: "assistant", - content: [ - { - type: "text", - text: `${open}\nBecause it helps\n${close}\n\nFinal answer`, + it.each(THINKING_TAG_CASES)( + "streams <%s> reasoning via onReasoningStream without leaking into final text", + ({ open, close }) => { + let handler: ((evt: unknown) => void) | undefined; + const session: StubSession = { + subscribe: (fn) => { + handler = fn; + return () => {}; }, - ], - } as AssistantMessage; + }; - handler?.({ type: "message_end", message: assistantMessage }); + const onReasoningStream = vi.fn(); + const onBlockReply = vi.fn(); - expect(onBlockReply).toHaveBeenCalledTimes(1); - expect(onBlockReply.mock.calls[0][0].text).toBe("Final answer"); + subscribeEmbeddedPiSession({ + session: session as unknown as Parameters[0]["session"], + runId: "run", + onReasoningStream, + onBlockReply, + blockReplyBreak: "message_end", + reasoningMode: "stream", + }); - const streamTexts = onReasoningStream.mock.calls - .map((call) => call[0]?.text) - .filter((value): value is string => typeof value === "string"); - expect(streamTexts.at(-1)).toBe("Reasoning:\n_Because it helps_"); + handler?.({ + type: "message_update", + message: { role: "assistant" }, + assistantMessageEvent: { + type: "text_delta", + delta: `${open}\nBecause`, + }, + }); - expect(assistantMessage.content).toEqual([ - { type: "thinking", thinking: "Because it helps" }, - { type: "text", text: "Final answer" }, - ]); - }); - it.each( - THINKING_TAG_CASES, - )("suppresses <%s> blocks across chunk boundaries", ({ open, close }) => { - let handler: ((evt: unknown) => void) | undefined; - const session: StubSession = { - subscribe: (fn) => { - handler = fn; - return () => {}; - }, - }; + handler?.({ + type: "message_update", + message: { role: "assistant" }, + assistantMessageEvent: { + type: "text_delta", + delta: ` it helps\n${close}\n\nFinal answer`, + }, + }); - const onBlockReply = vi.fn(); + const assistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `${open}\nBecause it helps\n${close}\n\nFinal answer`, + }, + ], + } as AssistantMessage; - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], - runId: "run", - onBlockReply, - blockReplyBreak: "text_end", - blockReplyChunking: { - minChars: 5, - maxChars: 50, - breakPreference: "newline", - }, - }); + handler?.({ type: "message_end", message: assistantMessage }); - handler?.({ type: "message_start", message: { role: "assistant" } }); + expect(onBlockReply).toHaveBeenCalledTimes(1); + expect(onBlockReply.mock.calls[0][0].text).toBe("Final answer"); - handler?.({ - type: "message_update", - message: { role: "assistant" }, - assistantMessageEvent: { - type: "text_delta", - delta: `${open}Reasoning chunk that should not leak`, - }, - }); + const streamTexts = onReasoningStream.mock.calls + .map((call) => call[0]?.text) + .filter((value): value is string => typeof value === "string"); + expect(streamTexts.at(-1)).toBe("Reasoning:\n_Because it helps_"); - expect(onBlockReply).not.toHaveBeenCalled(); + expect(assistantMessage.content).toEqual([ + { type: "thinking", thinking: "Because it helps" }, + { type: "text", text: "Final answer" }, + ]); + }, + ); + it.each(THINKING_TAG_CASES)( + "suppresses <%s> blocks across chunk boundaries", + ({ open, close }) => { + let handler: ((evt: unknown) => void) | undefined; + const session: StubSession = { + subscribe: (fn) => { + handler = fn; + return () => {}; + }, + }; - handler?.({ - type: "message_update", - message: { role: "assistant" }, - assistantMessageEvent: { - type: "text_delta", - delta: `${close}\n\nFinal answer`, - }, - }); + const onBlockReply = vi.fn(); - handler?.({ - type: "message_update", - message: { role: "assistant" }, - assistantMessageEvent: { type: "text_end" }, - }); + subscribeEmbeddedPiSession({ + session: session as unknown as Parameters[0]["session"], + runId: "run", + onBlockReply, + blockReplyBreak: "text_end", + blockReplyChunking: { + minChars: 5, + maxChars: 50, + breakPreference: "newline", + }, + }); - const payloadTexts = onBlockReply.mock.calls - .map((call) => call[0]?.text) - .filter((value): value is string => typeof value === "string"); - expect(payloadTexts.length).toBeGreaterThan(0); - for (const text of payloadTexts) { - expect(text).not.toContain("Reasoning"); - expect(text).not.toContain(open); - } - const combined = payloadTexts.join(" ").replace(/\s+/g, " ").trim(); - expect(combined).toBe("Final answer"); - }); + handler?.({ type: "message_start", message: { role: "assistant" } }); + + handler?.({ + type: "message_update", + message: { role: "assistant" }, + assistantMessageEvent: { + type: "text_delta", + delta: `${open}Reasoning chunk that should not leak`, + }, + }); + + expect(onBlockReply).not.toHaveBeenCalled(); + + handler?.({ + type: "message_update", + message: { role: "assistant" }, + assistantMessageEvent: { + type: "text_delta", + delta: `${close}\n\nFinal answer`, + }, + }); + + handler?.({ + type: "message_update", + message: { role: "assistant" }, + assistantMessageEvent: { type: "text_end" }, + }); + + const payloadTexts = onBlockReply.mock.calls + .map((call) => call[0]?.text) + .filter((value): value is string => typeof value === "string"); + expect(payloadTexts.length).toBeGreaterThan(0); + for (const text of payloadTexts) { + expect(text).not.toContain("Reasoning"); + expect(text).not.toContain(open); + } + const combined = payloadTexts.join(" ").replace(/\s+/g, " ").trim(); + expect(combined).toBe("Final answer"); + }, + ); }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts index bf12a2a8896..6e017899e4f 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts @@ -26,9 +26,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -72,9 +70,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "message_end", @@ -118,9 +114,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts index a21ea1b6585..05622834cda 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts +++ b/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts @@ -66,9 +66,7 @@ describe("subscribeEmbeddedPiSession", () => { const onToolResult = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-tool", verboseLevel: "on", onToolResult, @@ -107,9 +105,7 @@ describe("subscribeEmbeddedPiSession", () => { const onToolResult = vi.fn(); subscribeEmbeddedPiSession({ - session: session as unknown as Parameters< - typeof subscribeEmbeddedPiSession - >[0]["session"], + session: session as unknown as Parameters[0]["session"], runId: "run-browser-tool", verboseLevel: "on", onToolResult, diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index 3b76bc46dd0..6f43e0d6548 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -1,12 +1,6 @@ -import { - getChannelPlugin, - normalizeChannelId, -} from "../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js"; import { truncateUtf16Safe } from "../utils.js"; -import { - type MessagingToolSend, - normalizeTargetForProvider, -} from "./pi-embedded-messaging.js"; +import { type MessagingToolSend, normalizeTargetForProvider } from "./pi-embedded-messaging.js"; const TOOL_RESULT_MAX_CHARS = 8000; @@ -56,18 +50,15 @@ export function extractMessagingToolSend( ): MessagingToolSend | undefined { // Provider docking: new provider tools must implement plugin.actions.extractToolSend. const action = typeof args.action === "string" ? args.action.trim() : ""; - const accountIdRaw = - typeof args.accountId === "string" ? args.accountId.trim() : undefined; + const accountIdRaw = typeof args.accountId === "string" ? args.accountId.trim() : undefined; const accountId = accountIdRaw ? accountIdRaw : undefined; if (toolName === "message") { if (action !== "send" && action !== "thread-reply") return undefined; const toRaw = typeof args.to === "string" ? args.to : undefined; if (!toRaw) return undefined; - const providerRaw = - typeof args.provider === "string" ? args.provider.trim() : ""; + const providerRaw = typeof args.provider === "string" ? args.provider.trim() : ""; const providerId = providerRaw ? normalizeChannelId(providerRaw) : null; - const provider = - providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message"); + const provider = providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message"); const to = normalizeTargetForProvider(provider, toRaw); return to ? { tool: toolName, provider, accountId, to } : undefined; } diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index 82b4aebb55c..0475a19f8a7 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -14,8 +14,7 @@ import type { import type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.types.js"; import { formatReasoningMessage } from "./pi-embedded-utils.js"; -const THINKING_TAG_SCAN_RE = - /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi; +const THINKING_TAG_SCAN_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi; const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi; const log = createSubsystemLogger("agent/embedded"); @@ -24,9 +23,7 @@ export type { SubscribeEmbeddedPiSessionParams, } from "./pi-embedded-subscribe.types.js"; -export function subscribeEmbeddedPiSession( - params: SubscribeEmbeddedPiSessionParams, -) { +export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionParams) { const reasoningMode = params.reasoningMode ?? "off"; const state: EmbeddedPiSubscribeState = { assistantTexts: [], @@ -37,9 +34,7 @@ export function subscribeEmbeddedPiSession( reasoningMode, includeReasoning: reasoningMode === "on", shouldEmitPartialReplies: !(reasoningMode === "on" && !params.onBlockReply), - streamReasoning: - reasoningMode === "stream" && - typeof params.onReasoningStream === "function", + streamReasoning: reasoningMode === "stream" && typeof params.onReasoningStream === "function", deltaBuffer: "", blockBuffer: "", // Track if a streamed chunk opened a block (stateful across chunks). @@ -66,8 +61,7 @@ export function subscribeEmbeddedPiSession( const toolMetaById = state.toolMetaById; const toolSummaryById = state.toolSummaryById; const messagingToolSentTexts = state.messagingToolSentTexts; - const messagingToolSentTextsNormalized = - state.messagingToolSentTextsNormalized; + const messagingToolSentTextsNormalized = state.messagingToolSentTextsNormalized; const messagingToolSentTargets = state.messagingToolSentTargets; const pendingMessagingTexts = state.pendingMessagingTexts; const pendingMessagingTargets = state.pendingMessagingTargets; @@ -131,8 +125,7 @@ export function subscribeEmbeddedPiSession( messagingToolSentTextsNormalized.splice(0, overflow); } if (messagingToolSentTargets.length > MAX_MESSAGING_SENT_TARGETS) { - const overflow = - messagingToolSentTargets.length - MAX_MESSAGING_SENT_TARGETS; + const overflow = messagingToolSentTargets.length - MAX_MESSAGING_SENT_TARGETS; messagingToolSentTargets.splice(0, overflow); } }; @@ -169,9 +162,7 @@ export function subscribeEmbeddedPiSession( }; const blockChunking = params.blockReplyChunking; - const blockChunker = blockChunking - ? new EmbeddedBlockChunker(blockChunking) - : null; + const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null; // KNOWN: Provider streams are not strictly once-only or perfectly ordered. // `text_end` can repeat full content; late `text_end` can arrive after `message_end`. // Tests: `src/agents/pi-embedded-subscribe.test.ts` (e.g. late text_end cases). @@ -194,10 +185,7 @@ export function subscribeEmbeddedPiSession( } }; - const stripBlockTags = ( - text: string, - state: { thinking: boolean; final: boolean }, - ): string => { + const stripBlockTags = (text: string, state: { thinking: boolean; final: boolean }): string => { if (!text) return text; // 1. Handle blocks (stateful, strip content inside) @@ -279,15 +267,8 @@ export function subscribeEmbeddedPiSession( // Only check committed (successful) messaging tool texts - checking pending texts // is risky because if the tool fails after suppression, the user gets no response const normalizedChunk = normalizeTextForComparison(chunk); - if ( - isMessagingToolDuplicateNormalized( - normalizedChunk, - messagingToolSentTextsNormalized, - ) - ) { - log.debug( - `Skipping block reply - already sent via messaging tool: ${chunk.slice(0, 50)}...`, - ); + if (isMessagingToolDuplicateNormalized(normalizedChunk, messagingToolSentTextsNormalized)) { + log.debug(`Skipping block reply - already sent via messaging tool: ${chunk.slice(0, 50)}...`); return; } @@ -297,8 +278,7 @@ export function subscribeEmbeddedPiSession( const splitResult = parseReplyDirectives(chunk); const { text: cleanedText, mediaUrls, audioAsVoice } = splitResult; // Skip empty payloads, but always emit if audioAsVoice is set (to propagate the flag) - if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice) - return; + if (!cleanedText && (!mediaUrls || mediaUrls.length === 0) && !audioAsVoice) return; void params.onBlockReply({ text: cleanedText, mediaUrls: mediaUrls?.length ? mediaUrls : undefined, @@ -365,16 +345,13 @@ export function subscribeEmbeddedPiSession( maybeResolveCompactionWait, }; - const unsubscribe = params.session.subscribe( - createEmbeddedPiSessionEventHandler(ctx), - ); + const unsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx)); return { assistantTexts, toolMetas, unsubscribe, - isCompacting: () => - state.compactionInFlight || state.pendingCompactionRetry > 0, + isCompacting: () => state.compactionInFlight || state.pendingCompactionRetry > 0, getMessagingToolSentTexts: () => messagingToolSentTexts.slice(), getMessagingToolSentTargets: () => messagingToolSentTargets.slice(), // Returns true if any messaging tool successfully sent a message. @@ -390,9 +367,7 @@ export function subscribeEmbeddedPiSession( queueMicrotask(() => { if (state.compactionInFlight || state.pendingCompactionRetry > 0) { ensureCompactionPromise(); - void (state.compactionRetryPromise ?? Promise.resolve()).then( - resolve, - ); + void (state.compactionRetryPromise ?? Promise.resolve()).then(resolve); } else { resolve(); } diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/pi-embedded-subscribe.types.ts index 52732e855c1..7237d6ac432 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/pi-embedded-subscribe.types.ts @@ -9,14 +9,8 @@ export type SubscribeEmbeddedPiSessionParams = { verboseLevel?: "off" | "on"; reasoningMode?: ReasoningLevel; shouldEmitToolResult?: () => boolean; - onToolResult?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; - onReasoningStream?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; + onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onBlockReply?: (payload: { text?: string; mediaUrls?: string[]; @@ -26,15 +20,9 @@ export type SubscribeEmbeddedPiSessionParams = { onBlockReplyFlush?: () => void | Promise; blockReplyBreak?: "text_end" | "message_end"; blockReplyChunking?: BlockReplyChunking; - onPartialReply?: (payload: { - text?: string; - mediaUrls?: string[]; - }) => void | Promise; + onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; onAssistantMessageStart?: () => void | Promise; - onAgentEvent?: (evt: { - stream: string; - data: Record; - }) => void; + onAgentEvent?: (evt: { stream: string; data: Record }) => void; enforceFinalTag?: boolean; }; diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index 19e7afd62ff..12594eaa105 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -22,9 +22,7 @@ function stripMinimaxToolCallXml(text: string): string { } export function extractAssistantText(msg: AssistantMessage): string { - const isTextBlock = ( - block: unknown, - ): block is { type: "text"; text: string } => { + const isTextBlock = (block: unknown): block is { type: "text"; text: string } => { if (!block || typeof block !== "object") return false; const rec = block as Record; return rec.type === "text" && typeof rec.text === "string"; @@ -66,9 +64,7 @@ type ThinkTaggedSplitBlock = | { type: "thinking"; thinking: string } | { type: "text"; text: string }; -export function splitThinkingTaggedText( - text: string, -): ThinkTaggedSplitBlock[] | null { +export function splitThinkingTaggedText(text: string): ThinkTaggedSplitBlock[] | null { const trimmedStart = text.trimStart(); // Avoid false positives: only treat it as structured thinking when it begins // with a think tag (common for local/OpenAI-compat providers that emulate @@ -123,9 +119,7 @@ export function splitThinkingTaggedText( export function promoteThinkingTagsToBlocks(message: AssistantMessage): void { if (!Array.isArray(message.content)) return; - const hasThinkingBlock = message.content.some( - (block) => block.type === "thinking", - ); + const hasThinkingBlock = message.content.some((block) => block.type === "thinking"); if (hasThinkingBlock) return; const next: AssistantMessage["content"] = []; @@ -193,10 +187,7 @@ export function extractThinkingFromTaggedStream(text: string): string { return text.slice(start).trim(); } -export function inferToolMetaFromArgs( - toolName: string, - args: unknown, -): string | undefined { +export function inferToolMetaFromArgs(toolName: string, args: unknown): string | undefined { const display = resolveToolDisplay({ name: toolName, args }); return formatToolDetail(display); } diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index 4b374c258b0..af6a6697cc9 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -1,9 +1,5 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - ExtensionAPI, - ExtensionContext, - FileOperations, -} from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext, FileOperations } from "@mariozechner/pi-coding-agent"; import { estimateTokens, generateSummary } from "@mariozechner/pi-coding-agent"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; @@ -25,27 +21,19 @@ function computeFileLists(fileOps: FileOperations): { return { readFiles, modifiedFiles }; } -function formatFileOperations( - readFiles: string[], - modifiedFiles: string[], -): string { +function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string { const sections: string[] = []; if (readFiles.length > 0) { sections.push(`\n${readFiles.join("\n")}\n`); } if (modifiedFiles.length > 0) { - sections.push( - `\n${modifiedFiles.join("\n")}\n`, - ); + sections.push(`\n${modifiedFiles.join("\n")}\n`); } if (sections.length === 0) return ""; return `\n\n${sections.join("\n\n")}`; } -function chunkMessages( - messages: AgentMessage[], - maxTokens: number, -): AgentMessage[][] { +function chunkMessages(messages: AgentMessage[], maxTokens: number): AgentMessage[][] { if (messages.length === 0) return []; const chunks: AgentMessage[][] = []; @@ -146,14 +134,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { 1, Math.floor(model.contextWindow ?? DEFAULT_CONTEXT_TOKENS), ); - const maxChunkTokens = Math.max( - 1, - Math.floor(contextWindowTokens * MAX_CHUNK_RATIO), - ); - const reserveTokens = Math.max( - 1, - Math.floor(preparation.settings.reserveTokens), - ); + const maxChunkTokens = Math.max(1, Math.floor(contextWindowTokens * MAX_CHUNK_RATIO)); + const reserveTokens = Math.max(1, Math.floor(preparation.settings.reserveTokens)); const historySummary = await summarizeChunks({ messages: preparation.messagesToSummarize, @@ -167,10 +149,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { }); let summary = historySummary; - if ( - preparation.isSplitTurn && - preparation.turnPrefixMessages.length > 0 - ) { + if (preparation.isSplitTurn && preparation.turnPrefixMessages.length > 0) { const prefixSummary = await summarizeChunks({ messages: preparation.turnPrefixMessages, model, diff --git a/src/agents/pi-extensions/context-pruning.test.ts b/src/agents/pi-extensions/context-pruning.test.ts index 4df741951ad..7dc8e6c59a4 100644 --- a/src/agents/pi-extensions/context-pruning.test.ts +++ b/src/agents/pi-extensions/context-pruning.test.ts @@ -1,8 +1,5 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - ExtensionAPI, - ExtensionContext, -} from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { describe, expect, it } from "vitest"; import { setContextPruningRuntime } from "./context-pruning/runtime.js"; @@ -21,13 +18,8 @@ function toolText(msg: AgentMessage): string { return first.text; } -function findToolResult( - messages: AgentMessage[], - toolCallId: string, -): AgentMessage { - const msg = messages.find( - (m) => m.role === "toolResult" && m.toolCallId === toolCallId, - ); +function findToolResult(messages: AgentMessage[], toolCallId: string): AgentMessage { + const msg = messages.find((m) => m.role === "toolResult" && m.toolCallId === toolCallId); if (!msg) throw new Error(`missing toolResult: ${toolCallId}`); return msg; } diff --git a/src/agents/pi-extensions/context-pruning/extension.ts b/src/agents/pi-extensions/context-pruning/extension.ts index 13b9a8d4b17..94da729e42f 100644 --- a/src/agents/pi-extensions/context-pruning/extension.ts +++ b/src/agents/pi-extensions/context-pruning/extension.ts @@ -1,9 +1,5 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - ContextEvent, - ExtensionAPI, - ExtensionContext, -} from "@mariozechner/pi-coding-agent"; +import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { pruneContextMessages } from "./pruner.js"; import { getContextPruningRuntime } from "./runtime.js"; diff --git a/src/agents/pi-extensions/context-pruning/pruner.ts b/src/agents/pi-extensions/context-pruning/pruner.ts index 589cf1bb40c..61d5154d40c 100644 --- a/src/agents/pi-extensions/context-pruning/pruner.ts +++ b/src/agents/pi-extensions/context-pruning/pruner.ts @@ -1,9 +1,5 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - ImageContent, - TextContent, - ToolResultMessage, -} from "@mariozechner/pi-ai"; +import type { ImageContent, TextContent, ToolResultMessage } from "@mariozechner/pi-ai"; import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; import type { EffectiveContextPruningSettings } from "./settings.js"; @@ -18,9 +14,7 @@ function asText(text: string): TextContent { return { type: "text", text }; } -function collectTextSegments( - content: ReadonlyArray, -): string[] { +function collectTextSegments(content: ReadonlyArray): string[] { const parts: string[] = []; for (const block of content) { if (block.type === "text") parts.push(block.text); @@ -82,9 +76,7 @@ function takeTailFromJoinedText(parts: string[], maxChars: number): string { return out.join(""); } -function hasImageBlocks( - content: ReadonlyArray, -): boolean { +function hasImageBlocks(content: ReadonlyArray): boolean { for (const block of content) { if (block.type === "image") return true; } @@ -208,21 +200,16 @@ export function pruneContextMessages(params: { const charWindow = contextWindowTokens * CHARS_PER_TOKEN_ESTIMATE; if (charWindow <= 0) return messages; - const cutoffIndex = findAssistantCutoffIndex( - messages, - settings.keepLastAssistants, - ); + const cutoffIndex = findAssistantCutoffIndex(messages, settings.keepLastAssistants); if (cutoffIndex === null) return messages; // Bootstrap safety: never prune anything before the first user message. This protects initial // "identity" reads (SOUL.md, USER.md, etc.) which typically happen before the first inbound user // message exists in the session transcript. const firstUserIndex = findFirstUserIndex(messages); - const pruneStartIndex = - firstUserIndex === null ? messages.length : firstUserIndex; + const pruneStartIndex = firstUserIndex === null ? messages.length : firstUserIndex; - const isToolPrunable = - params.isToolPrunable ?? makeToolPrunablePredicate(settings.tools); + const isToolPrunable = params.isToolPrunable ?? makeToolPrunablePredicate(settings.tools); if (settings.mode === "aggressive") { let next: AgentMessage[] | null = null; diff --git a/src/agents/pi-extensions/context-pruning/settings.ts b/src/agents/pi-extensions/context-pruning/settings.ts index f3bb6de8376..69f9474d1f1 100644 --- a/src/agents/pi-extensions/context-pruning/settings.ts +++ b/src/agents/pi-extensions/context-pruning/settings.ts @@ -41,81 +41,55 @@ export type EffectiveContextPruningSettings = { }; }; -export const DEFAULT_CONTEXT_PRUNING_SETTINGS: EffectiveContextPruningSettings = - { - mode: "adaptive", - keepLastAssistants: 3, - softTrimRatio: 0.3, - hardClearRatio: 0.5, - minPrunableToolChars: 50_000, - tools: {}, - softTrim: { - maxChars: 4_000, - headChars: 1_500, - tailChars: 1_500, - }, - hardClear: { - enabled: true, - placeholder: "[Old tool result content cleared]", - }, - }; +export const DEFAULT_CONTEXT_PRUNING_SETTINGS: EffectiveContextPruningSettings = { + mode: "adaptive", + keepLastAssistants: 3, + softTrimRatio: 0.3, + hardClearRatio: 0.5, + minPrunableToolChars: 50_000, + tools: {}, + softTrim: { + maxChars: 4_000, + headChars: 1_500, + tailChars: 1_500, + }, + hardClear: { + enabled: true, + placeholder: "[Old tool result content cleared]", + }, +}; -export function computeEffectiveSettings( - raw: unknown, -): EffectiveContextPruningSettings | null { +export function computeEffectiveSettings(raw: unknown): EffectiveContextPruningSettings | null { if (!raw || typeof raw !== "object") return null; const cfg = raw as ContextPruningConfig; if (cfg.mode !== "adaptive" && cfg.mode !== "aggressive") return null; - const s: EffectiveContextPruningSettings = structuredClone( - DEFAULT_CONTEXT_PRUNING_SETTINGS, - ); + const s: EffectiveContextPruningSettings = structuredClone(DEFAULT_CONTEXT_PRUNING_SETTINGS); s.mode = cfg.mode; - if ( - typeof cfg.keepLastAssistants === "number" && - Number.isFinite(cfg.keepLastAssistants) - ) { + if (typeof cfg.keepLastAssistants === "number" && Number.isFinite(cfg.keepLastAssistants)) { s.keepLastAssistants = Math.max(0, Math.floor(cfg.keepLastAssistants)); } - if ( - typeof cfg.softTrimRatio === "number" && - Number.isFinite(cfg.softTrimRatio) - ) { + if (typeof cfg.softTrimRatio === "number" && Number.isFinite(cfg.softTrimRatio)) { s.softTrimRatio = Math.min(1, Math.max(0, cfg.softTrimRatio)); } - if ( - typeof cfg.hardClearRatio === "number" && - Number.isFinite(cfg.hardClearRatio) - ) { + if (typeof cfg.hardClearRatio === "number" && Number.isFinite(cfg.hardClearRatio)) { s.hardClearRatio = Math.min(1, Math.max(0, cfg.hardClearRatio)); } - if ( - typeof cfg.minPrunableToolChars === "number" && - Number.isFinite(cfg.minPrunableToolChars) - ) { + if (typeof cfg.minPrunableToolChars === "number" && Number.isFinite(cfg.minPrunableToolChars)) { s.minPrunableToolChars = Math.max(0, Math.floor(cfg.minPrunableToolChars)); } if (cfg.tools) { s.tools = cfg.tools; } if (cfg.softTrim) { - if ( - typeof cfg.softTrim.maxChars === "number" && - Number.isFinite(cfg.softTrim.maxChars) - ) { + if (typeof cfg.softTrim.maxChars === "number" && Number.isFinite(cfg.softTrim.maxChars)) { s.softTrim.maxChars = Math.max(0, Math.floor(cfg.softTrim.maxChars)); } - if ( - typeof cfg.softTrim.headChars === "number" && - Number.isFinite(cfg.softTrim.headChars) - ) { + if (typeof cfg.softTrim.headChars === "number" && Number.isFinite(cfg.softTrim.headChars)) { s.softTrim.headChars = Math.max(0, Math.floor(cfg.softTrim.headChars)); } - if ( - typeof cfg.softTrim.tailChars === "number" && - Number.isFinite(cfg.softTrim.tailChars) - ) { + if (typeof cfg.softTrim.tailChars === "number" && Number.isFinite(cfg.softTrim.tailChars)) { s.softTrim.tailChars = Math.max(0, Math.floor(cfg.softTrim.tailChars)); } } @@ -123,10 +97,7 @@ export function computeEffectiveSettings( if (s.mode === "adaptive" && typeof cfg.hardClear.enabled === "boolean") { s.hardClear.enabled = cfg.hardClear.enabled; } - if ( - typeof cfg.hardClear.placeholder === "string" && - cfg.hardClear.placeholder.trim() - ) { + if (typeof cfg.hardClear.placeholder === "string" && cfg.hardClear.placeholder.trim()) { s.hardClear.placeholder = cfg.hardClear.placeholder.trim(); } } diff --git a/src/agents/pi-extensions/transcript-sanitize.ts b/src/agents/pi-extensions/transcript-sanitize.ts index 81af767e392..da57e9f2879 100644 --- a/src/agents/pi-extensions/transcript-sanitize.ts +++ b/src/agents/pi-extensions/transcript-sanitize.ts @@ -7,11 +7,7 @@ */ import type { AgentMessage } from "@mariozechner/pi-agent-core"; -import type { - ContextEvent, - ExtensionAPI, - ExtensionContext, -} from "@mariozechner/pi-coding-agent"; +import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { isGoogleModelApi } from "../pi-embedded-helpers.js"; import { repairToolUseResultPairing } from "../session-transcript-repair.js"; diff --git a/src/agents/pi-settings.test.ts b/src/agents/pi-settings.test.ts index 9a733254038..6a55dce2af4 100644 --- a/src/agents/pi-settings.test.ts +++ b/src/agents/pi-settings.test.ts @@ -39,9 +39,7 @@ describe("ensurePiCompactionReserveTokens", () => { describe("resolveCompactionReserveTokensFloor", () => { it("returns the default when config is missing", () => { - expect(resolveCompactionReserveTokensFloor()).toBe( - DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, - ); + expect(resolveCompactionReserveTokensFloor()).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); }); it("accepts configured floors, including zero", () => { diff --git a/src/agents/pi-settings.ts b/src/agents/pi-settings.ts index 6ed6fcc22a4..2d6c0c20ceb 100644 --- a/src/agents/pi-settings.ts +++ b/src/agents/pi-settings.ts @@ -4,17 +4,14 @@ export const DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000; type PiSettingsManagerLike = { getCompactionReserveTokens: () => number; - applyOverrides: (overrides: { - compaction: { reserveTokens: number }; - }) => void; + applyOverrides: (overrides: { compaction: { reserveTokens: number } }) => void; }; export function ensurePiCompactionReserveTokens(params: { settingsManager: PiSettingsManagerLike; minReserveTokens?: number; }): { didOverride: boolean; reserveTokens: number } { - const minReserveTokens = - params.minReserveTokens ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; + const minReserveTokens = params.minReserveTokens ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; const current = params.settingsManager.getCompactionReserveTokens(); if (current >= minReserveTokens) { @@ -28,9 +25,7 @@ export function ensurePiCompactionReserveTokens(params: { return { didOverride: true, reserveTokens: minReserveTokens }; } -export function resolveCompactionReserveTokensFloor( - cfg?: ClawdbotConfig, -): number { +export function resolveCompactionReserveTokensFloor(cfg?: ClawdbotConfig): number { const raw = cfg?.agents?.defaults?.compaction?.reserveTokensFloor; if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) { return Math.floor(raw); diff --git a/src/agents/pi-tools.abort.ts b/src/agents/pi-tools.abort.ts index 9a88c79b8d4..8a7502609c1 100644 --- a/src/agents/pi-tools.abort.ts +++ b/src/agents/pi-tools.abort.ts @@ -6,10 +6,7 @@ function throwAbortError(): never { throw err; } -function combineAbortSignals( - a?: AbortSignal, - b?: AbortSignal, -): AbortSignal | undefined { +function combineAbortSignals(a?: AbortSignal, b?: AbortSignal): AbortSignal | undefined { if (!a && !b) return undefined; if (a && !b) return a; if (b && !a) return b; diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts index 13c71a62883..a3575180bb3 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-a.test.ts @@ -48,9 +48,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -63,9 +61,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts index bc5f1a691b6..8680422dc2a 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-b.test.ts @@ -48,9 +48,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -63,27 +61,17 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); it("preserves action enums in normalized schemas", () => { const tools = createClawdbotCodingTools(); - const toolNames = [ - "browser", - "canvas", - "nodes", - "cron", - "gateway", - "message", - ]; + const toolNames = ["browser", "canvas", "nodes", "cron", "gateway", "message"]; - const collectActionValues = ( - schema: unknown, - values: Set, - ): void => { + const collectActionValues = (schema: unknown, values: Set): void => { if (!schema || typeof schema !== "object") return; const record = schema as Record; if (typeof record.const === "string") values.add(record.const); @@ -145,9 +133,7 @@ describe("createClawdbotCodingTools", () => { modelProvider: "anthropic", modelId: "claude-opus-4-5", }); - expect(anthropicTools.some((tool) => tool.name === "apply_patch")).toBe( - false, - ); + expect(anthropicTools.some((tool) => tool.name === "apply_patch")).toBe(false); }); it("respects apply_patch allowModels", () => { const config: ClawdbotConfig = { diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts index 574e4e75a32..3fd5b81d7a5 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-d.test.ts @@ -51,9 +51,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -66,9 +64,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); @@ -96,9 +94,7 @@ describe("createClawdbotCodingTools", () => { path: imagePath, }); - expect(result?.content?.some((block) => block.type === "image")).toBe( - true, - ); + expect(result?.content?.some((block) => block.type === "image")).toBe(true); const text = result?.content?.find((block) => block.type === "text") as | { text?: string } | undefined; @@ -126,16 +122,12 @@ describe("createClawdbotCodingTools", () => { path: textPath, }); - expect(result?.content?.some((block) => block.type === "image")).toBe( - false, - ); - const textBlocks = result?.content?.filter( - (block) => block.type === "text", - ) as Array<{ text?: string }> | undefined; + expect(result?.content?.some((block) => block.type === "image")).toBe(false); + const textBlocks = result?.content?.filter((block) => block.type === "text") as + | Array<{ text?: string }> + | undefined; expect(textBlocks?.length ?? 0).toBeGreaterThan(0); - const combinedText = textBlocks - ?.map((block) => block.text ?? "") - .join("\n"); + const combinedText = textBlocks?.map((block) => block.text ?? "").join("\n"); expect(combinedText).toContain(contents); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts index cfbf9399490..3a34a318e70 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-e.test.ts @@ -47,9 +47,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -62,9 +60,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); @@ -142,10 +140,7 @@ describe("createClawdbotCodingTools", () => { "maxProperties", ]); - const findUnsupportedKeywords = ( - schema: unknown, - path: string, - ): string[] => { + const findUnsupportedKeywords = (schema: unknown, path: string): string[] => { const found: string[] = []; if (!schema || typeof schema !== "object") return found; if (Array.isArray(schema)) { @@ -164,9 +159,7 @@ describe("createClawdbotCodingTools", () => { : undefined; if (properties) { for (const [key, value] of Object.entries(properties)) { - found.push( - ...findUnsupportedKeywords(value, `${path}.properties.${key}`), - ); + found.push(...findUnsupportedKeywords(value, `${path}.properties.${key}`)); } } @@ -183,10 +176,7 @@ describe("createClawdbotCodingTools", () => { }; for (const tool of tools) { - const violations = findUnsupportedKeywords( - tool.parameters, - `${tool.name}.parameters`, - ); + const violations = findUnsupportedKeywords(tool.parameters, `${tool.name}.parameters`); expect(violations).toEqual([]); } }); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts index c81a312fccb..d01d1973548 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-f.test.ts @@ -50,9 +50,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -65,9 +63,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); @@ -89,12 +87,10 @@ describe("createClawdbotCodingTools", () => { path: testFile, }); - const textBlocks = result?.content?.filter( - (block) => block.type === "text", - ) as Array<{ text?: string }> | undefined; - const combinedText = textBlocks - ?.map((block) => block.text ?? "") - .join("\n"); + const textBlocks = result?.content?.filter((block) => block.type === "text") as + | Array<{ text?: string }> + | undefined; + const combinedText = textBlocks?.map((block) => block.text ?? "").join("\n"); expect(combinedText).toContain(testContent); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); @@ -178,12 +174,10 @@ describe("createClawdbotCodingTools", () => { file_path: filePath, }); - const textBlocks = result?.content?.filter( - (block) => block.type === "text", - ) as Array<{ text?: string }> | undefined; - const combinedText = textBlocks - ?.map((block) => block.text ?? "") - .join("\n"); + const textBlocks = result?.content?.filter((block) => block.type === "text") as + | Array<{ text?: string }> + | undefined; + const combinedText = textBlocks?.map((block) => block.text ?? "").join("\n"); expect(combinedText).toContain("hello universe"); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts index f57b3b93652..4b29ece654e 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts @@ -50,9 +50,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -65,9 +63,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); @@ -106,9 +104,7 @@ describe("createClawdbotCodingTools", () => { const readTool = tools.find((tool) => tool.name === "read"); expect(readTool).toBeDefined(); - await expect( - readTool?.execute("tool-sbx-1", { file_path: outsidePath }), - ).rejects.toThrow(); + await expect(readTool?.execute("tool-sbx-1", { file_path: outsidePath })).rejects.toThrow(); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); await fs.rm(outsidePath, { force: true }); diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts index 2a4028e19d6..5c8e3c0bd0d 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts @@ -48,9 +48,7 @@ describe("createClawdbotCodingTools", () => { execute, }; - const wrapped = __testing.wrapToolParamNormalization(tool, [ - { keys: ["path", "file_path"] }, - ]); + const wrapped = __testing.wrapToolParamNormalization(tool, [{ keys: ["path", "file_path"] }]); await wrapped.execute("tool-1", { file_path: "foo.txt", content: "x" }); expect(execute).toHaveBeenCalledWith( @@ -63,9 +61,9 @@ describe("createClawdbotCodingTools", () => { await expect(wrapped.execute("tool-2", { content: "x" })).rejects.toThrow( /Missing required parameter/, ); - await expect( - wrapped.execute("tool-3", { file_path: " ", content: "x" }), - ).rejects.toThrow(/Missing required parameter/); + await expect(wrapped.execute("tool-3", { file_path: " ", content: "x" })).rejects.toThrow( + /Missing required parameter/, + ); }); }); diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 2b9c89be2f4..05e0dfc0f15 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -1,8 +1,5 @@ import type { ClawdbotConfig } from "../config/config.js"; -import { - resolveAgentConfig, - resolveAgentIdFromSessionKey, -} from "./agent-scope.js"; +import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; import type { SandboxToolPolicy } from "./sandbox.js"; import { expandToolGroups, normalizeToolName } from "./tool-policy.js"; @@ -14,9 +11,7 @@ const DEFAULT_SUBAGENT_TOOL_DENY = [ "sessions_spawn", ]; -export function resolveSubagentToolPolicy( - cfg?: ClawdbotConfig, -): SandboxToolPolicy { +export function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPolicy { const configured = cfg?.tools?.subagents?.tools; const deny = [ ...DEFAULT_SUBAGENT_TOOL_DENY, @@ -26,10 +21,7 @@ export function resolveSubagentToolPolicy( return { allow, deny }; } -export function isToolAllowedByPolicyName( - name: string, - policy?: SandboxToolPolicy, -): boolean { +export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean { if (!policy) return true; const deny = new Set(expandToolGroups(policy.deny)); const allowRaw = expandToolGroups(policy.allow); @@ -44,10 +36,7 @@ export function isToolAllowedByPolicyName( return true; } -export function filterToolsByPolicy( - tools: AnyAgentTool[], - policy?: SandboxToolPolicy, -) { +export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) { if (!policy) return tools; return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy)); } @@ -56,13 +45,9 @@ export function resolveEffectiveToolPolicy(params: { config?: ClawdbotConfig; sessionKey?: string; }) { - const agentId = params.sessionKey - ? resolveAgentIdFromSessionKey(params.sessionKey) - : undefined; + const agentId = params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined; const agentConfig = - params.config && agentId - ? resolveAgentConfig(params.config, agentId) - : undefined; + params.config && agentId ? resolveAgentConfig(params.config, agentId) : undefined; const agentTools = agentConfig?.tools; const hasAgentToolPolicy = Array.isArray(agentTools?.allow) || diff --git a/src/agents/pi-tools.read.ts b/src/agents/pi-tools.read.ts index e1bc93cbbdc..45bd925f77c 100644 --- a/src/agents/pi-tools.read.ts +++ b/src/agents/pi-tools.read.ts @@ -1,9 +1,5 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; -import { - createEditTool, - createReadTool, - createWriteTool, -} from "@mariozechner/pi-coding-agent"; +import { createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent"; import { detectMime } from "../media/mime.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; @@ -16,9 +12,7 @@ type ToolContentBlock = AgentToolResult["content"][number]; type ImageContentBlock = Extract; type TextContentBlock = Extract; -async function sniffMimeFromBase64( - base64: string, -): Promise { +async function sniffMimeFromBase64(base64: string): Promise { const trimmed = base64.trim(); if (!trimmed) return undefined; @@ -74,11 +68,7 @@ async function normalizeReadImageResult( if (sniffed === image.mimeType) return result; const nextContent = content.map((block) => { - if ( - block && - typeof block === "object" && - (block as { type?: unknown }).type === "image" - ) { + if (block && typeof block === "object" && (block as { type?: unknown }).type === "image") { const b = block as ImageContentBlock & { mimeType: string }; return { ...b, mimeType: sniffed } satisfies ImageContentBlock; } @@ -125,9 +115,7 @@ export const CLAUDE_PARAM_GROUPS = { // Normalize tool parameters from Claude Code conventions to pi-coding-agent conventions. // Claude Code uses file_path/old_string/new_string while pi-coding-agent uses path/oldText/newText. // This prevents models trained on Claude Code from getting stuck in tool-call loops. -export function normalizeToolParams( - params: unknown, -): Record | undefined { +export function normalizeToolParams(params: unknown): Record | undefined { if (!params || typeof params !== "object") return undefined; const record = params as Record; const normalized = { ...record }; @@ -149,9 +137,7 @@ export function normalizeToolParams( return normalized; } -export function patchToolSchemaForClaudeCompatibility( - tool: AnyAgentTool, -): AnyAgentTool { +export function patchToolSchemaForClaudeCompatibility(tool: AnyAgentTool): AnyAgentTool { const schema = tool.parameters && typeof tool.parameters === "object" ? (tool.parameters as Record) @@ -235,9 +221,7 @@ export function wrapToolParamNormalization( const normalized = normalizeToolParams(params); const record = normalized ?? - (params && typeof params === "object" - ? (params as Record) - : undefined); + (params && typeof params === "object" ? (params as Record) : undefined); if (requiredParamGroups?.length) { assertRequiredParams(record, requiredParamGroups, tool.name); } @@ -253,9 +237,7 @@ function wrapSandboxPathGuard(tool: AnyAgentTool, root: string): AnyAgentTool { const normalized = normalizeToolParams(args); const record = normalized ?? - (args && typeof args === "object" - ? (args as Record) - : undefined); + (args && typeof args === "object" ? (args as Record) : undefined); const filePath = record?.path; if (typeof filePath === "string" && filePath.trim()) { await assertSandboxPath({ filePath, cwd: root, root }); @@ -272,18 +254,12 @@ export function createSandboxedReadTool(root: string) { export function createSandboxedWriteTool(root: string) { const base = createWriteTool(root) as unknown as AnyAgentTool; - return wrapSandboxPathGuard( - wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.write), - root, - ); + return wrapSandboxPathGuard(wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.write), root); } export function createSandboxedEditTool(root: string) { const base = createEditTool(root) as unknown as AnyAgentTool; - return wrapSandboxPathGuard( - wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.edit), - root, - ); + return wrapSandboxPathGuard(wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.edit), root); } export function createClawdbotReadTool(base: AnyAgentTool): AnyAgentTool { @@ -294,17 +270,14 @@ export function createClawdbotReadTool(base: AnyAgentTool): AnyAgentTool { const normalized = normalizeToolParams(params); const record = normalized ?? - (params && typeof params === "object" - ? (params as Record) - : undefined); + (params && typeof params === "object" ? (params as Record) : undefined); assertRequiredParams(record, CLAUDE_PARAM_GROUPS.read, base.name); const result = (await base.execute( toolCallId, normalized ?? params, signal, )) as AgentToolResult; - const filePath = - typeof record?.path === "string" ? String(record.path) : ""; + const filePath = typeof record?.path === "string" ? String(record.path) : ""; const normalizedResult = await normalizeReadImageResult(result, filePath); return sanitizeToolResultImages(normalizedResult, `read:${filePath}`); }, diff --git a/src/agents/pi-tools.schema.ts b/src/agents/pi-tools.schema.ts index 3db3750600b..1910f3eef49 100644 --- a/src/agents/pi-tools.schema.ts +++ b/src/agents/pi-tools.schema.ts @@ -28,9 +28,7 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown { const existingEnum = extractEnumValues(existing); const incomingEnum = extractEnumValues(incoming); if (existingEnum || incomingEnum) { - const values = Array.from( - new Set([...(existingEnum ?? []), ...(incomingEnum ?? [])]), - ); + const values = Array.from(new Set([...(existingEnum ?? []), ...(incomingEnum ?? [])])); const merged: Record = {}; for (const source of [existing, incoming]) { if (!source || typeof source !== "object") continue; @@ -64,11 +62,7 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { // If schema already has type + properties (no top-level anyOf to merge), // still clean it for Gemini compatibility - if ( - "type" in schema && - "properties" in schema && - !Array.isArray(schema.anyOf) - ) { + if ("type" in schema && "properties" in schema && !Array.isArray(schema.anyOf)) { return { ...tool, parameters: cleanSchemaForGemini(schema), @@ -105,17 +99,12 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { const props = (entry as { properties?: unknown }).properties; if (!props || typeof props !== "object") continue; objectVariants += 1; - for (const [key, value] of Object.entries( - props as Record, - )) { + for (const [key, value] of Object.entries(props as Record)) { if (!(key in mergedProperties)) { mergedProperties[key] = value; continue; } - mergedProperties[key] = mergePropertySchemas( - mergedProperties[key], - value, - ); + mergedProperties[key] = mergePropertySchemas(mergedProperties[key], value); } const required = Array.isArray((entry as { required?: unknown }).required) ? (entry as { required: unknown[] }).required @@ -147,27 +136,18 @@ export function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool { // Merging properties preserves useful enums like `action` while keeping schemas portable. parameters: cleanSchemaForGemini({ type: "object", - ...(typeof nextSchema.title === "string" - ? { title: nextSchema.title } - : {}), + ...(typeof nextSchema.title === "string" ? { title: nextSchema.title } : {}), ...(typeof nextSchema.description === "string" ? { description: nextSchema.description } : {}), properties: - Object.keys(mergedProperties).length > 0 - ? mergedProperties - : (schema.properties ?? {}), - ...(mergedRequired && mergedRequired.length > 0 - ? { required: mergedRequired } - : {}), - additionalProperties: - "additionalProperties" in schema ? schema.additionalProperties : true, + Object.keys(mergedProperties).length > 0 ? mergedProperties : (schema.properties ?? {}), + ...(mergedRequired && mergedRequired.length > 0 ? { required: mergedRequired } : {}), + additionalProperties: "additionalProperties" in schema ? schema.additionalProperties : true, }), }; } -export function cleanToolSchemaForGemini( - schema: Record, -): unknown { +export function cleanToolSchemaForGemini(schema: Record): unknown { return cleanSchemaForGemini(schema); } diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index a7d93c2def8..512ee856481 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -36,10 +36,7 @@ import { patchToolSchemaForClaudeCompatibility, wrapToolParamNormalization, } from "./pi-tools.read.js"; -import { - cleanToolSchemaForGemini, - normalizeToolParameters, -} from "./pi-tools.schema.js"; +import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; import type { SandboxContext } from "./sandbox.js"; import { resolveToolProfilePolicy } from "./tool-policy.js"; @@ -54,9 +51,7 @@ function isApplyPatchAllowedForModel(params: { modelId?: string; allowModels?: string[]; }) { - const allowModels = Array.isArray(params.allowModels) - ? params.allowModels - : []; + const allowModels = Array.isArray(params.allowModels) ? params.allowModels : []; if (allowModels.length === 0) return true; const modelId = params.modelId?.trim(); if (!modelId) return false; @@ -123,8 +118,7 @@ export function createClawdbotCodingTools(options?: { sessionKey: options?.sessionKey, }); const profilePolicy = resolveToolProfilePolicy(profile); - const scopeKey = - options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); + const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = isSubagentSessionKey(options?.sessionKey) && options?.sessionKey ? resolveSubagentToolPolicy(options.config) @@ -161,21 +155,13 @@ export function createClawdbotCodingTools(options?: { if (sandboxRoot) return []; // Wrap with param normalization for Claude Code compatibility return [ - wrapToolParamNormalization( - createWriteTool(workspaceRoot), - CLAUDE_PARAM_GROUPS.write, - ), + wrapToolParamNormalization(createWriteTool(workspaceRoot), CLAUDE_PARAM_GROUPS.write), ]; } if (tool.name === "edit") { if (sandboxRoot) return []; // Wrap with param normalization for Claude Code compatibility - return [ - wrapToolParamNormalization( - createEditTool(workspaceRoot), - CLAUDE_PARAM_GROUPS.edit, - ), - ]; + return [wrapToolParamNormalization(createEditTool(workspaceRoot), CLAUDE_PARAM_GROUPS.edit)]; } return [tool as AnyAgentTool]; }); @@ -207,17 +193,13 @@ export function createClawdbotCodingTools(options?: { ? null : createApplyPatchTool({ cwd: sandboxRoot ?? workspaceRoot, - sandboxRoot: - sandboxRoot && allowWorkspaceWrites ? sandboxRoot : undefined, + sandboxRoot: sandboxRoot && allowWorkspaceWrites ? sandboxRoot : undefined, }); const tools: AnyAgentTool[] = [ ...base, ...(sandboxRoot ? allowWorkspaceWrites - ? [ - createSandboxedEditTool(sandboxRoot), - createSandboxedWriteTool(sandboxRoot), - ] + ? [createSandboxedEditTool(sandboxRoot), createSandboxedWriteTool(sandboxRoot)] : [] : []), ...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []), @@ -246,15 +228,11 @@ export function createClawdbotCodingTools(options?: { hasRepliedRef: options?.hasRepliedRef, }), ]; - const toolsFiltered = profilePolicy - ? filterToolsByPolicy(tools, profilePolicy) - : tools; + const toolsFiltered = profilePolicy ? filterToolsByPolicy(tools, profilePolicy) : tools; const policyFiltered = effectiveToolsPolicy ? filterToolsByPolicy(toolsFiltered, effectiveToolsPolicy) : toolsFiltered; - const sandboxed = sandbox - ? filterToolsByPolicy(policyFiltered, sandbox.tools) - : policyFiltered; + const sandboxed = sandbox ? filterToolsByPolicy(policyFiltered, sandbox.tools) : policyFiltered; const subagentFiltered = subagentPolicy ? filterToolsByPolicy(sandboxed, subagentPolicy) : sandboxed; @@ -262,9 +240,7 @@ export function createClawdbotCodingTools(options?: { // Without this, some providers (notably OpenAI) will reject root-level union schemas. const normalized = subagentFiltered.map(normalizeToolParameters); const withAbort = options?.abortSignal - ? normalized.map((tool) => - wrapToolWithAbortSignal(tool, options.abortSignal), - ) + ? normalized.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal)) : normalized; // NOTE: Keep canonical (lowercase) tool names here. diff --git a/src/agents/pi-tools.workspace-paths.test.ts b/src/agents/pi-tools.workspace-paths.test.ts index 1d159008981..cdac954fd6e 100644 --- a/src/agents/pi-tools.workspace-paths.test.ts +++ b/src/agents/pi-tools.workspace-paths.test.ts @@ -14,9 +14,7 @@ async function withTempDir(prefix: string, fn: (dir: string) => Promise) { } } -function getTextContent(result?: { - content?: Array<{ type: string; text?: string }>; -}) { +function getTextContent(result?: { content?: Array<{ type: string; text?: string }> }) { const textBlock = result?.content?.find((block) => block.type === "text"); return textBlock?.text ?? ""; } @@ -63,10 +61,7 @@ describe("workspace path resolution", () => { content: contents, }); - const written = await fs.readFile( - path.join(workspaceDir, testFile), - "utf8", - ); + const written = await fs.readFile(path.join(workspaceDir, testFile), "utf8"); expect(written).toBe(contents); } finally { process.chdir(prevCwd); @@ -80,11 +75,7 @@ describe("workspace path resolution", () => { await withTempDir("clawdbot-cwd-", async (otherDir) => { const prevCwd = process.cwd(); const testFile = "edit.txt"; - await fs.writeFile( - path.join(workspaceDir, testFile), - "hello world", - "utf8", - ); + await fs.writeFile(path.join(workspaceDir, testFile), "hello world", "utf8"); process.chdir(otherDir); try { @@ -98,10 +89,7 @@ describe("workspace path resolution", () => { newText: "clawdbot", }); - const updated = await fs.readFile( - path.join(workspaceDir, testFile), - "utf8", - ); + const updated = await fs.readFile(path.join(workspaceDir, testFile), "utf8"); expect(updated).toBe("hello clawdbot"); } finally { process.chdir(prevCwd); @@ -120,9 +108,7 @@ describe("workspace path resolution", () => { command: "echo ok", }); const cwd = - result?.details && - typeof result.details === "object" && - "cwd" in result.details + result?.details && typeof result.details === "object" && "cwd" in result.details ? (result.details as { cwd?: string }).cwd : undefined; expect(cwd).toBeTruthy(); @@ -146,9 +132,7 @@ describe("workspace path resolution", () => { workdir: overrideDir, }); const cwd = - result?.details && - typeof result.details === "object" && - "cwd" in result.details + result?.details && typeof result.details === "object" && "cwd" in result.details ? (result.details as { cwd?: string }).cwd : undefined; expect(cwd).toBeTruthy(); @@ -190,16 +174,8 @@ describe("sandboxed workspace paths", () => { }; const testFile = "sandbox.txt"; - await fs.writeFile( - path.join(sandboxDir, testFile), - "sandbox read", - "utf8", - ); - await fs.writeFile( - path.join(workspaceDir, testFile), - "workspace read", - "utf8", - ); + await fs.writeFile(path.join(sandboxDir, testFile), "sandbox read", "utf8"); + await fs.writeFile(path.join(workspaceDir, testFile), "workspace read", "utf8"); const tools = createClawdbotCodingTools({ workspaceDir, sandbox }); const readTool = tools.find((tool) => tool.name === "read"); @@ -217,10 +193,7 @@ describe("sandboxed workspace paths", () => { path: "new.txt", content: "sandbox write", }); - const written = await fs.readFile( - path.join(sandboxDir, "new.txt"), - "utf8", - ); + const written = await fs.readFile(path.join(sandboxDir, "new.txt"), "utf8"); expect(written).toBe("sandbox write"); await editTool?.execute("sbx-edit", { @@ -228,10 +201,7 @@ describe("sandboxed workspace paths", () => { oldText: "write", newText: "edit", }); - const edited = await fs.readFile( - path.join(sandboxDir, "new.txt"), - "utf8", - ); + const edited = await fs.readFile(path.join(sandboxDir, "new.txt"), "utf8"); expect(edited).toBe("sandbox edit"); }); }); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts index 6892f7f9700..2e06bde035e 100644 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts +++ b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.includes-session-status-default-sandbox-allowlist.test.ts @@ -32,8 +32,7 @@ vi.mock("node:child_process", async (importOriginal) => { dockerArgs[0] === "inspect" && dockerArgs[1] === "-f" && dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = - dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; const code = shouldFailContainerInspect ? 1 : 0; if (shouldSucceedImageInspect) { diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts index f9f9f53cd9d..6df4bc23a6a 100644 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts +++ b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-allow-agent-specific-docker-settings-beyond.test.ts @@ -32,8 +32,7 @@ vi.mock("node:child_process", async (importOriginal) => { dockerArgs[0] === "inspect" && dockerArgs[1] === "-f" && dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = - dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; const code = shouldFailContainerInspect ? 1 : 0; if (shouldSucceedImageInspect) { diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts index 98ff6e27c8b..4eaf19a6dc9 100644 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts +++ b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-agent-specific-workspaceroot.test.ts @@ -33,8 +33,7 @@ vi.mock("node:child_process", async (importOriginal) => { dockerArgs[0] === "inspect" && dockerArgs[1] === "-f" && dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = - dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; const code = shouldFailContainerInspect ? 1 : 0; if (shouldSucceedImageInspect) { @@ -85,9 +84,7 @@ describe("Agent-specific sandbox config", () => { }); expect(context).toBeDefined(); - expect(context?.workspaceDir).toContain( - path.resolve("/tmp/isolated-sandboxes"), - ); + expect(context?.workspaceDir).toContain(path.resolve("/tmp/isolated-sandboxes")); }); it("should prefer agent config over global for multiple agents", async () => { const { resolveSandboxContext } = await import("./sandbox.js"); diff --git a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts index c9b813fe4fc..24e1ef4a948 100644 --- a/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts +++ b/src/agents/sandbox-agent-config.agent-specific-sandbox-config.should-use-global-sandbox-config-no-agent.test.ts @@ -32,8 +32,7 @@ vi.mock("node:child_process", async (importOriginal) => { dockerArgs[0] === "inspect" && dockerArgs[1] === "-f" && dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = - dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; const code = shouldFailContainerInspect ? 1 : 0; if (shouldSucceedImageInspect) { diff --git a/src/agents/sandbox-create-args.test.ts b/src/agents/sandbox-create-args.test.ts index 8e468fc3f6b..32cf4d7dd9e 100644 --- a/src/agents/sandbox-create-args.test.ts +++ b/src/agents/sandbox-create-args.test.ts @@ -101,10 +101,7 @@ describe("buildSandboxCreateArgs", () => { tmpfs: [], network: "none", capDrop: [], - binds: [ - "/home/user/source:/source:rw", - "/var/run/docker.sock:/var/run/docker.sock", - ], + binds: ["/home/user/source:/source:rw", "/var/run/docker.sock:/var/run/docker.sock"], }; const args = buildSandboxCreateArgs({ diff --git a/src/agents/sandbox-merge.test.ts b/src/agents/sandbox-merge.test.ts index bf0bfd0042e..10cdb44dc7a 100644 --- a/src/agents/sandbox-merge.test.ts +++ b/src/agents/sandbox-merge.test.ts @@ -1,20 +1,14 @@ import { describe, expect, it } from "vitest"; describe("sandbox config merges", () => { - it( - "resolves sandbox scope deterministically", - { timeout: 15_000 }, - async () => { - const { resolveSandboxScope } = await import("./sandbox.js"); + it("resolves sandbox scope deterministically", { timeout: 15_000 }, async () => { + const { resolveSandboxScope } = await import("./sandbox.js"); - expect(resolveSandboxScope({})).toBe("agent"); - expect(resolveSandboxScope({ perSession: true })).toBe("session"); - expect(resolveSandboxScope({ perSession: false })).toBe("shared"); - expect(resolveSandboxScope({ perSession: true, scope: "agent" })).toBe( - "agent", - ); - }, - ); + expect(resolveSandboxScope({})).toBe("agent"); + expect(resolveSandboxScope({ perSession: true })).toBe("session"); + expect(resolveSandboxScope({ perSession: false })).toBe("shared"); + expect(resolveSandboxScope({ perSession: true, scope: "agent" })).toBe("agent"); + }); it("merges sandbox docker env and ulimits (agent wins)", async () => { const { resolveSandboxDockerConfig } = await import("./sandbox.js"); @@ -82,9 +76,7 @@ describe("sandbox config merges", () => { }, }); - expect(resolved.binds).toEqual([ - "/var/run/docker.sock:/var/run/docker.sock", - ]); + expect(resolved.binds).toEqual(["/var/run/docker.sock:/var/run/docker.sock"]); }); it("ignores agent docker overrides under shared scope", async () => { @@ -100,8 +92,7 @@ describe("sandbox config merges", () => { }); it("applies per-agent browser and prune overrides (ignored under shared scope)", async () => { - const { resolveSandboxBrowserConfig, resolveSandboxPruneConfig } = - await import("./sandbox.js"); + const { resolveSandboxBrowserConfig, resolveSandboxPruneConfig } = await import("./sandbox.js"); const browser = resolveSandboxBrowserConfig({ scope: "agent", diff --git a/src/agents/sandbox-paths.ts b/src/agents/sandbox-paths.ts index 40c0522c1bb..f3dc787b796 100644 --- a/src/agents/sandbox-paths.ts +++ b/src/agents/sandbox-paths.ts @@ -25,11 +25,10 @@ function resolveToCwd(filePath: string, cwd: string): string { return path.resolve(cwd, expanded); } -export function resolveSandboxPath(params: { - filePath: string; - cwd: string; - root: string; -}): { resolved: string; relative: string } { +export function resolveSandboxPath(params: { filePath: string; cwd: string; root: string }): { + resolved: string; + relative: string; +} { const resolved = resolveToCwd(params.filePath, params.cwd); const rootResolved = path.resolve(params.root); const relative = path.relative(rootResolved, resolved); @@ -37,18 +36,12 @@ export function resolveSandboxPath(params: { return { resolved, relative: "" }; } if (relative.startsWith("..") || path.isAbsolute(relative)) { - throw new Error( - `Path escapes sandbox root (${shortPath(rootResolved)}): ${params.filePath}`, - ); + throw new Error(`Path escapes sandbox root (${shortPath(rootResolved)}): ${params.filePath}`); } return { resolved, relative }; } -export async function assertSandboxPath(params: { - filePath: string; - cwd: string; - root: string; -}) { +export async function assertSandboxPath(params: { filePath: string; cwd: string; root: string }) { const resolved = resolveSandboxPath(params); await assertNoSymlink(resolved.relative, path.resolve(params.root)); return resolved; diff --git a/src/agents/sandbox-skills.test.ts b/src/agents/sandbox-skills.test.ts index e6a6caf7db9..7a11ff76f02 100644 --- a/src/agents/sandbox-skills.test.ts +++ b/src/agents/sandbox-skills.test.ts @@ -32,8 +32,7 @@ vi.mock("node:child_process", async (importOriginal) => { dockerArgs[0] === "inspect" && dockerArgs[1] === "-f" && dockerArgs[2] === "{{.State.Running}}"; - const shouldSucceedImageInspect = - dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; + const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect"; const code = shouldFailContainerInspect ? 1 : 0; if (shouldSucceedImageInspect) { @@ -46,11 +45,7 @@ vi.mock("node:child_process", async (importOriginal) => { }; }); -async function writeSkill(params: { - dir: string; - name: string; - description: string; -}) { +async function writeSkill(params: { dir: string; name: string; description: string }) { const { dir, name, description } = params; await fs.mkdir(dir, { recursive: true }); await fs.writeFile( @@ -87,9 +82,7 @@ describe("sandbox skill mirroring", () => { }); const runContext = async (workspaceAccess: "none" | "ro") => { - const stateDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-state-"), - ); + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-state-")); const bundledDir = path.join(stateDir, "bundled-skills"); await fs.mkdir(bundledDir, { recursive: true }); @@ -99,9 +92,7 @@ describe("sandbox skill mirroring", () => { const { resolveSandboxContext } = await import("./sandbox.js"); - const workspaceDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-workspace-"), - ); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-")); await writeSkill({ dir: path.join(workspaceDir, "skills", "demo-skill"), name: "demo-skill", @@ -134,29 +125,15 @@ describe("sandbox skill mirroring", () => { const { context } = await runContext("ro"); expect(context?.enabled).toBe(true); - const skillPath = path.join( - context?.workspaceDir ?? "", - "skills", - "demo-skill", - "SKILL.md", - ); - await expect(fs.readFile(skillPath, "utf-8")).resolves.toContain( - "demo-skill", - ); + const skillPath = path.join(context?.workspaceDir ?? "", "skills", "demo-skill", "SKILL.md"); + await expect(fs.readFile(skillPath, "utf-8")).resolves.toContain("demo-skill"); }, 20_000); it("copies skills into the sandbox when workspaceAccess is none", async () => { const { context } = await runContext("none"); expect(context?.enabled).toBe(true); - const skillPath = path.join( - context?.workspaceDir ?? "", - "skills", - "demo-skill", - "SKILL.md", - ); - await expect(fs.readFile(skillPath, "utf-8")).resolves.toContain( - "demo-skill", - ); + const skillPath = path.join(context?.workspaceDir ?? "", "skills", "demo-skill", "SKILL.md"); + await expect(fs.readFile(skillPath, "utf-8")).resolves.toContain("demo-skill"); }, 20_000); }); diff --git a/src/agents/sandbox.resolveSandboxContext.test.ts b/src/agents/sandbox.resolveSandboxContext.test.ts index 7228f177d98..6d2e645167a 100644 --- a/src/agents/sandbox.resolveSandboxContext.test.ts +++ b/src/agents/sandbox.resolveSandboxContext.test.ts @@ -9,8 +9,7 @@ describe("resolveSandboxContext", () => { throw new Error("spawn should not be called"); }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, spawn }; }); @@ -44,8 +43,7 @@ describe("resolveSandboxContext", () => { throw new Error("spawn should not be called"); }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, spawn }; }); @@ -79,8 +77,7 @@ describe("resolveSandboxContext", () => { throw new Error("spawn should not be called"); }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, spawn }; }); diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 1b66f609fe4..8ac65795d0f 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -10,10 +10,7 @@ export { DEFAULT_SANDBOX_COMMON_IMAGE, DEFAULT_SANDBOX_IMAGE, } from "./sandbox/constants.js"; -export { - ensureSandboxWorkspaceForSession, - resolveSandboxContext, -} from "./sandbox/context.js"; +export { ensureSandboxWorkspaceForSession, resolveSandboxContext } from "./sandbox/context.js"; export { buildSandboxCreateArgs } from "./sandbox/docker.js"; export { diff --git a/src/agents/sandbox/browser-bridges.ts b/src/agents/sandbox/browser-bridges.ts index b7688b12c20..aceb713f990 100644 --- a/src/agents/sandbox/browser-bridges.ts +++ b/src/agents/sandbox/browser-bridges.ts @@ -1,6 +1,3 @@ import type { BrowserBridge } from "../../browser/bridge-server.js"; -export const BROWSER_BRIDGES = new Map< - string, - { bridge: BrowserBridge; containerName: string } ->(); +export const BROWSER_BRIDGES = new Map(); diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index ac312cd63f8..e3975e9b5d9 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -1,17 +1,8 @@ -import { - startBrowserBridgeServer, - stopBrowserBridgeServer, -} from "../../browser/bridge-server.js"; -import { - type ResolvedBrowserConfig, - resolveProfile, -} from "../../browser/config.js"; +import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js"; +import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js"; import { DEFAULT_CLAWD_BROWSER_COLOR } from "../../browser/constants.js"; import { BROWSER_BRIDGES } from "./browser-bridges.js"; -import { - DEFAULT_SANDBOX_BROWSER_IMAGE, - SANDBOX_AGENT_WORKSPACE_MOUNT, -} from "./constants.js"; +import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { buildSandboxCreateArgs, dockerContainerState, @@ -23,10 +14,7 @@ import { slugifySessionKey } from "./shared.js"; import { isToolAllowed } from "./tool-policy.js"; import type { SandboxBrowserContext, SandboxConfig } from "./types.js"; -async function waitForSandboxCdp(params: { - cdpPort: number; - timeoutMs: number; -}): Promise { +async function waitForSandboxCdp(params: { cdpPort: number; timeoutMs: number }): Promise { const deadline = Date.now() + Math.max(0, params.timeoutMs); const url = `http://127.0.0.1:${params.cdpPort}/json/version`; while (Date.now() < deadline) { @@ -94,17 +82,12 @@ export async function ensureSandboxBrowser(params: { if (!params.cfg.browser.enabled) return null; if (!isToolAllowed(params.cfg.tools, "browser")) return null; - const slug = - params.cfg.scope === "shared" - ? "shared" - : slugifySessionKey(params.scopeKey); + const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey); const name = `${params.cfg.browser.containerPrefix}${slug}`; const containerName = name.slice(0, 63); const state = await dockerContainerState(containerName); if (!state.exists) { - await ensureSandboxBrowserImage( - params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE, - ); + await ensureSandboxBrowserImage(params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE); const args = buildSandboxCreateArgs({ name: containerName, cfg: params.cfg.docker, @@ -112,18 +95,11 @@ export async function ensureSandboxBrowser(params: { labels: { "clawdbot.sandboxBrowser": "1" }, }); const mainMountSuffix = - params.cfg.workspaceAccess === "ro" && - params.workspaceDir === params.agentWorkspaceDir + params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; - args.push( - "-v", - `${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`, - ); - if ( - params.cfg.workspaceAccess !== "none" && - params.workspaceDir !== params.agentWorkspaceDir - ) { + args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`); + if (params.cfg.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : ""; args.push( "-v", @@ -134,22 +110,11 @@ export async function ensureSandboxBrowser(params: { if (params.cfg.browser.enableNoVnc && !params.cfg.browser.headless) { args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`); } - args.push( - "-e", - `CLAWDBOT_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`, - ); - args.push( - "-e", - `CLAWDBOT_BROWSER_ENABLE_NOVNC=${ - params.cfg.browser.enableNoVnc ? "1" : "0" - }`, - ); + args.push("-e", `CLAWDBOT_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`); + args.push("-e", `CLAWDBOT_BROWSER_ENABLE_NOVNC=${params.cfg.browser.enableNoVnc ? "1" : "0"}`); args.push("-e", `CLAWDBOT_BROWSER_CDP_PORT=${params.cfg.browser.cdpPort}`); args.push("-e", `CLAWDBOT_BROWSER_VNC_PORT=${params.cfg.browser.vncPort}`); - args.push( - "-e", - `CLAWDBOT_BROWSER_NOVNC_PORT=${params.cfg.browser.noVncPort}`, - ); + args.push("-e", `CLAWDBOT_BROWSER_NOVNC_PORT=${params.cfg.browser.noVncPort}`); args.push(params.cfg.browser.image); await execDocker(args); await execDocker(["start", containerName]); @@ -157,10 +122,7 @@ export async function ensureSandboxBrowser(params: { await execDocker(["start", containerName]); } - const mappedCdp = await readDockerPort( - containerName, - params.cfg.browser.cdpPort, - ); + const mappedCdp = await readDockerPort(containerName, params.cfg.browser.cdpPort); if (!mappedCdp) { throw new Error(`Failed to resolve CDP port mapping for ${containerName}.`); } @@ -171,17 +133,11 @@ export async function ensureSandboxBrowser(params: { : null; const existing = BROWSER_BRIDGES.get(params.scopeKey); - const existingProfile = existing - ? resolveProfile(existing.bridge.state.resolved, "clawd") - : null; + const existingProfile = existing ? resolveProfile(existing.bridge.state.resolved, "clawd") : null; const shouldReuse = - existing && - existing.containerName === containerName && - existingProfile?.cdpPort === mappedCdp; + existing && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp; if (existing && !shouldReuse) { - await stopBrowserBridgeServer(existing.bridge.server).catch( - () => undefined, - ); + await stopBrowserBridgeServer(existing.bridge.server).catch(() => undefined); BROWSER_BRIDGES.delete(params.scopeKey); } @@ -241,9 +197,7 @@ export async function ensureSandboxBrowser(params: { }); const noVncUrl = - mappedNoVnc && - params.cfg.browser.enableNoVnc && - !params.cfg.browser.headless + mappedNoVnc && params.cfg.browser.enableNoVnc && !params.cfg.browser.headless ? `http://127.0.0.1:${mappedNoVnc}/vnc.html?autoconnect=1&resize=remote` : undefined; diff --git a/src/agents/sandbox/config.ts b/src/agents/sandbox/config.ts index 9d8ff9ae3f5..a633a0fd98e 100644 --- a/src/agents/sandbox/config.ts +++ b/src/agents/sandbox/config.ts @@ -39,8 +39,7 @@ export function resolveSandboxDockerConfig(params: { globalDocker?: Partial; agentDocker?: Partial; }): SandboxDockerConfig { - const agentDocker = - params.scope === "shared" ? undefined : params.agentDocker; + const agentDocker = params.scope === "shared" ? undefined : params.agentDocker; const globalDocker = params.globalDocker; const env = agentDocker?.env @@ -59,12 +58,9 @@ export function resolveSandboxDockerConfig(params: { agentDocker?.containerPrefix ?? globalDocker?.containerPrefix ?? DEFAULT_SANDBOX_CONTAINER_PREFIX, - workdir: - agentDocker?.workdir ?? globalDocker?.workdir ?? DEFAULT_SANDBOX_WORKDIR, - readOnlyRoot: - agentDocker?.readOnlyRoot ?? globalDocker?.readOnlyRoot ?? true, - tmpfs: agentDocker?.tmpfs ?? - globalDocker?.tmpfs ?? ["/tmp", "/var/tmp", "/run"], + workdir: agentDocker?.workdir ?? globalDocker?.workdir ?? DEFAULT_SANDBOX_WORKDIR, + readOnlyRoot: agentDocker?.readOnlyRoot ?? globalDocker?.readOnlyRoot ?? true, + tmpfs: agentDocker?.tmpfs ?? globalDocker?.tmpfs ?? ["/tmp", "/var/tmp", "/run"], network: agentDocker?.network ?? globalDocker?.network ?? "none", user: agentDocker?.user ?? globalDocker?.user, capDrop: agentDocker?.capDrop ?? globalDocker?.capDrop ?? ["ALL"], @@ -76,8 +72,7 @@ export function resolveSandboxDockerConfig(params: { cpus: agentDocker?.cpus ?? globalDocker?.cpus, ulimits, seccompProfile: agentDocker?.seccompProfile ?? globalDocker?.seccompProfile, - apparmorProfile: - agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile, + apparmorProfile: agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile, dns: agentDocker?.dns ?? globalDocker?.dns, extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts, binds: binds.length ? binds : undefined, @@ -89,44 +84,27 @@ export function resolveSandboxBrowserConfig(params: { globalBrowser?: Partial; agentBrowser?: Partial; }): SandboxBrowserConfig { - const agentBrowser = - params.scope === "shared" ? undefined : params.agentBrowser; + const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser; const globalBrowser = params.globalBrowser; - const allowedControlUrls = - agentBrowser?.allowedControlUrls ?? globalBrowser?.allowedControlUrls; + const allowedControlUrls = agentBrowser?.allowedControlUrls ?? globalBrowser?.allowedControlUrls; const allowedControlHosts = agentBrowser?.allowedControlHosts ?? globalBrowser?.allowedControlHosts; const allowedControlPorts = agentBrowser?.allowedControlPorts ?? globalBrowser?.allowedControlPorts; return { enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false, - image: - agentBrowser?.image ?? - globalBrowser?.image ?? - DEFAULT_SANDBOX_BROWSER_IMAGE, + image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE, containerPrefix: agentBrowser?.containerPrefix ?? globalBrowser?.containerPrefix ?? DEFAULT_SANDBOX_BROWSER_PREFIX, - cdpPort: - agentBrowser?.cdpPort ?? - globalBrowser?.cdpPort ?? - DEFAULT_SANDBOX_BROWSER_CDP_PORT, - vncPort: - agentBrowser?.vncPort ?? - globalBrowser?.vncPort ?? - DEFAULT_SANDBOX_BROWSER_VNC_PORT, + cdpPort: agentBrowser?.cdpPort ?? globalBrowser?.cdpPort ?? DEFAULT_SANDBOX_BROWSER_CDP_PORT, + vncPort: agentBrowser?.vncPort ?? globalBrowser?.vncPort ?? DEFAULT_SANDBOX_BROWSER_VNC_PORT, noVncPort: - agentBrowser?.noVncPort ?? - globalBrowser?.noVncPort ?? - DEFAULT_SANDBOX_BROWSER_NOVNC_PORT, + agentBrowser?.noVncPort ?? globalBrowser?.noVncPort ?? DEFAULT_SANDBOX_BROWSER_NOVNC_PORT, headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false, - enableNoVnc: - agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true, - allowHostControl: - agentBrowser?.allowHostControl ?? - globalBrowser?.allowHostControl ?? - false, + enableNoVnc: agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true, + allowHostControl: agentBrowser?.allowHostControl ?? globalBrowser?.allowHostControl ?? false, allowedControlUrls: Array.isArray(allowedControlUrls) && allowedControlUrls.length > 0 ? allowedControlUrls @@ -155,14 +133,8 @@ export function resolveSandboxPruneConfig(params: { const agentPrune = params.scope === "shared" ? undefined : params.agentPrune; const globalPrune = params.globalPrune; return { - idleHours: - agentPrune?.idleHours ?? - globalPrune?.idleHours ?? - DEFAULT_SANDBOX_IDLE_HOURS, - maxAgeDays: - agentPrune?.maxAgeDays ?? - globalPrune?.maxAgeDays ?? - DEFAULT_SANDBOX_MAX_AGE_DAYS, + idleHours: agentPrune?.idleHours ?? globalPrune?.idleHours ?? DEFAULT_SANDBOX_IDLE_HOURS, + maxAgeDays: agentPrune?.maxAgeDays ?? globalPrune?.maxAgeDays ?? DEFAULT_SANDBOX_MAX_AGE_DAYS, }; } @@ -174,8 +146,7 @@ export function resolveSandboxConfigForAgent( // Agent-specific sandbox config overrides global let agentSandbox: typeof agent | undefined; - const agentConfig = - cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined; + const agentConfig = cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined; if (agentConfig?.sandbox) { agentSandbox = agentConfig.sandbox; } @@ -190,12 +161,9 @@ export function resolveSandboxConfigForAgent( return { mode: agentSandbox?.mode ?? agent?.mode ?? "off", scope, - workspaceAccess: - agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none", + workspaceAccess: agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none", workspaceRoot: - agentSandbox?.workspaceRoot ?? - agent?.workspaceRoot ?? - DEFAULT_SANDBOX_WORKSPACE_ROOT, + agentSandbox?.workspaceRoot ?? agent?.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT, docker: resolveSandboxDockerConfig({ scope, globalDocker: agent?.docker, diff --git a/src/agents/sandbox/constants.ts b/src/agents/sandbox/constants.ts index d2409e56b87..7b6cb97392f 100644 --- a/src/agents/sandbox/constants.ts +++ b/src/agents/sandbox/constants.ts @@ -4,11 +4,7 @@ import path from "node:path"; import { CHANNEL_IDS } from "../../channels/registry.js"; import { STATE_DIR_CLAWDBOT } from "../../config/config.js"; -export const DEFAULT_SANDBOX_WORKSPACE_ROOT = path.join( - os.homedir(), - ".clawdbot", - "sandboxes", -); +export const DEFAULT_SANDBOX_WORKSPACE_ROOT = path.join(os.homedir(), ".clawdbot", "sandboxes"); export const DEFAULT_SANDBOX_IMAGE = "clawdbot-sandbox:bookworm-slim"; export const DEFAULT_SANDBOX_CONTAINER_PREFIX = "clawdbot-sbx-"; @@ -41,10 +37,8 @@ export const DEFAULT_TOOL_DENY = [ ...CHANNEL_IDS, ] as const; -export const DEFAULT_SANDBOX_BROWSER_IMAGE = - "clawdbot-sandbox-browser:bookworm-slim"; -export const DEFAULT_SANDBOX_COMMON_IMAGE = - "clawdbot-sandbox-common:bookworm-slim"; +export const DEFAULT_SANDBOX_BROWSER_IMAGE = "clawdbot-sandbox-browser:bookworm-slim"; +export const DEFAULT_SANDBOX_COMMON_IMAGE = "clawdbot-sandbox-common:bookworm-slim"; export const DEFAULT_SANDBOX_BROWSER_PREFIX = "clawdbot-sbx-browser-"; export const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222; @@ -55,11 +49,5 @@ export const DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS = 12_000; export const SANDBOX_AGENT_WORKSPACE_MOUNT = "/agent"; export const SANDBOX_STATE_DIR = path.join(STATE_DIR_CLAWDBOT, "sandbox"); -export const SANDBOX_REGISTRY_PATH = path.join( - SANDBOX_STATE_DIR, - "containers.json", -); -export const SANDBOX_BROWSER_REGISTRY_PATH = path.join( - SANDBOX_STATE_DIR, - "browsers.json", -); +export const SANDBOX_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "containers.json"); +export const SANDBOX_BROWSER_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "browsers.json"); diff --git a/src/agents/sandbox/context.ts b/src/agents/sandbox/context.ts index 89e2f2768ee..ff6372e693a 100644 --- a/src/agents/sandbox/context.ts +++ b/src/agents/sandbox/context.ts @@ -10,10 +10,7 @@ import { resolveSandboxConfigForAgent } from "./config.js"; import { ensureSandboxContainer } from "./docker.js"; import { maybePruneSandboxes } from "./prune.js"; import { resolveSandboxRuntimeStatus } from "./runtime-status.js"; -import { - resolveSandboxScopeKey, - resolveSandboxWorkspaceDir, -} from "./shared.js"; +import { resolveSandboxScopeKey, resolveSandboxWorkspaceDir } from "./shared.js"; import type { SandboxContext, SandboxWorkspaceInfo } from "./types.js"; import { ensureSandboxWorkspace } from "./workspace.js"; @@ -41,11 +38,8 @@ export async function resolveSandboxContext(params: { const workspaceRoot = resolveUserPath(cfg.workspaceRoot); const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey); const sandboxWorkspaceDir = - cfg.scope === "shared" - ? workspaceRoot - : resolveSandboxWorkspaceDir(workspaceRoot, scopeKey); - const workspaceDir = - cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir; + cfg.scope === "shared" ? workspaceRoot : resolveSandboxWorkspaceDir(workspaceRoot, scopeKey); + const workspaceDir = cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir; if (workspaceDir === sandboxWorkspaceDir) { await ensureSandboxWorkspace( sandboxWorkspaceDir, @@ -60,8 +54,7 @@ export async function resolveSandboxContext(params: { config: params.config, }); } catch (error) { - const message = - error instanceof Error ? error.message : JSON.stringify(error); + const message = error instanceof Error ? error.message : JSON.stringify(error); defaultRuntime.error?.(`Sandbox skill sync failed: ${message}`); } } @@ -123,11 +116,8 @@ export async function ensureSandboxWorkspaceForSession(params: { const workspaceRoot = resolveUserPath(cfg.workspaceRoot); const scopeKey = resolveSandboxScopeKey(cfg.scope, rawSessionKey); const sandboxWorkspaceDir = - cfg.scope === "shared" - ? workspaceRoot - : resolveSandboxWorkspaceDir(workspaceRoot, scopeKey); - const workspaceDir = - cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir; + cfg.scope === "shared" ? workspaceRoot : resolveSandboxWorkspaceDir(workspaceRoot, scopeKey); + const workspaceDir = cfg.workspaceAccess === "rw" ? agentWorkspaceDir : sandboxWorkspaceDir; if (workspaceDir === sandboxWorkspaceDir) { await ensureSandboxWorkspace( sandboxWorkspaceDir, @@ -142,8 +132,7 @@ export async function ensureSandboxWorkspaceForSession(params: { config: params.config, }); } catch (error) { - const message = - error instanceof Error ? error.message : JSON.stringify(error); + const message = error instanceof Error ? error.message : JSON.stringify(error); defaultRuntime.error?.(`Sandbox skill sync failed: ${message}`); } } diff --git a/src/agents/sandbox/docker.ts b/src/agents/sandbox/docker.ts index d91f8bc206c..59ccb127e64 100644 --- a/src/agents/sandbox/docker.ts +++ b/src/agents/sandbox/docker.ts @@ -1,41 +1,32 @@ import { spawn } from "node:child_process"; -import { - DEFAULT_SANDBOX_IMAGE, - SANDBOX_AGENT_WORKSPACE_MOUNT, -} from "./constants.js"; +import { DEFAULT_SANDBOX_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { updateRegistry } from "./registry.js"; import { resolveSandboxScopeKey, slugifySessionKey } from "./shared.js"; -import type { - SandboxConfig, - SandboxDockerConfig, - SandboxWorkspaceAccess, -} from "./types.js"; +import type { SandboxConfig, SandboxDockerConfig, SandboxWorkspaceAccess } from "./types.js"; export function execDocker(args: string[], opts?: { allowFailure?: boolean }) { - return new Promise<{ stdout: string; stderr: string; code: number }>( - (resolve, reject) => { - const child = spawn("docker", args, { - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (chunk) => { - stdout += chunk.toString(); - }); - child.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); - }); - child.on("close", (code) => { - const exitCode = code ?? 0; - if (exitCode !== 0 && !opts?.allowFailure) { - reject(new Error(stderr.trim() || `docker ${args.join(" ")} failed`)); - return; - } - resolve({ stdout, stderr, code: exitCode }); - }); - }, - ); + return new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => { + const child = spawn("docker", args, { + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout?.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr?.on("data", (chunk) => { + stderr += chunk.toString(); + }); + child.on("close", (code) => { + const exitCode = code ?? 0; + if (exitCode !== 0 && !opts?.allowFailure) { + reject(new Error(stderr.trim() || `docker ${args.join(" ")} failed`)); + return; + } + resolve({ stdout, stderr, code: exitCode }); + }); + }); } export async function readDockerPort(containerName: string, port: number) { @@ -69,10 +60,9 @@ export async function ensureDockerImage(image: string) { } export async function dockerContainerState(name: string) { - const result = await execDocker( - ["inspect", "-f", "{{.State.Running}}", name], - { allowFailure: true }, - ); + const result = await execDocker(["inspect", "-f", "{{.State.Running}}", name], { + allowFailure: true, + }); if (result.code !== 0) return { exists: false, running: false }; return { exists: true, running: result.stdout.trim() === "true" }; } @@ -95,10 +85,8 @@ function formatUlimitValue( const raw = String(value).trim(); return raw ? `${name}=${raw}` : null; } - const soft = - typeof value.soft === "number" ? Math.max(0, value.soft) : undefined; - const hard = - typeof value.hard === "number" ? Math.max(0, value.hard) : undefined; + const soft = typeof value.soft === "number" ? Math.max(0, value.soft) : undefined; + const hard = typeof value.hard === "number" ? Math.max(0, value.hard) : undefined; if (soft === undefined && hard === undefined) return null; if (soft === undefined) return `${name}=${hard}`; if (hard === undefined) return `${name}=${soft}`; @@ -184,14 +172,9 @@ async function createSandboxContainer(params: { }); args.push("--workdir", cfg.workdir); const mainMountSuffix = - params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir - ? ":ro" - : ""; + params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; args.push("-v", `${workspaceDir}:${cfg.workdir}${mainMountSuffix}`); - if ( - params.workspaceAccess !== "none" && - workspaceDir !== params.agentWorkspaceDir - ) { + if (params.workspaceAccess !== "none" && workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : ""; args.push( "-v", @@ -215,8 +198,7 @@ export async function ensureSandboxContainer(params: { cfg: SandboxConfig; }) { const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey); - const slug = - params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey); + const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey); const name = `${params.cfg.docker.containerPrefix}${slug}`; const containerName = name.slice(0, 63); const state = await dockerContainerState(containerName); diff --git a/src/agents/sandbox/manage.ts b/src/agents/sandbox/manage.ts index c050718411f..89c80f95bd8 100644 --- a/src/agents/sandbox/manage.ts +++ b/src/agents/sandbox/manage.ts @@ -46,8 +46,7 @@ export async function listSandboxContainers(): Promise { } } const agentId = resolveSandboxAgentId(entry.sessionKey); - const configuredImage = resolveSandboxConfigForAgent(config, agentId).docker - .image; + const configuredImage = resolveSandboxConfigForAgent(config, agentId).docker.image; results.push({ ...entry, image: actualImage, @@ -81,8 +80,7 @@ export async function listSandboxBrowsers(): Promise { } } const agentId = resolveSandboxAgentId(entry.sessionKey); - const configuredImage = resolveSandboxConfigForAgent(config, agentId) - .browser.image; + const configuredImage = resolveSandboxConfigForAgent(config, agentId).browser.image; results.push({ ...entry, image: actualImage, @@ -94,9 +92,7 @@ export async function listSandboxBrowsers(): Promise { return results; } -export async function removeSandboxContainer( - containerName: string, -): Promise { +export async function removeSandboxContainer(containerName: string): Promise { try { await execDocker(["rm", "-f", containerName], { allowFailure: true }); } catch { @@ -105,9 +101,7 @@ export async function removeSandboxContainer( await removeRegistryEntry(containerName); } -export async function removeSandboxBrowserContainer( - containerName: string, -): Promise { +export async function removeSandboxBrowserContainer(containerName: string): Promise { try { await execDocker(["rm", "-f", containerName], { allowFailure: true }); } catch { @@ -118,9 +112,7 @@ export async function removeSandboxBrowserContainer( // Stop browser bridge if active for (const [sessionKey, bridge] of BROWSER_BRIDGES.entries()) { if (bridge.containerName === containerName) { - await stopBrowserBridgeServer(bridge.bridge.server).catch( - () => undefined, - ); + await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined); BROWSER_BRIDGES.delete(sessionKey); } } diff --git a/src/agents/sandbox/prune.ts b/src/agents/sandbox/prune.ts index 0ed3487e919..5c23e58e6db 100644 --- a/src/agents/sandbox/prune.ts +++ b/src/agents/sandbox/prune.ts @@ -61,9 +61,7 @@ async function pruneSandboxBrowsers(cfg: SandboxConfig) { await removeBrowserRegistryEntry(entry.containerName); const bridge = BROWSER_BRIDGES.get(entry.sessionKey); if (bridge?.containerName === entry.containerName) { - await stopBrowserBridgeServer(bridge.bridge.server).catch( - () => undefined, - ); + await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined); BROWSER_BRIDGES.delete(entry.sessionKey); } } @@ -85,9 +83,7 @@ export async function maybePruneSandboxes(cfg: SandboxConfig) { : typeof error === "string" ? error : JSON.stringify(error); - defaultRuntime.error?.( - `Sandbox prune failed: ${message ?? "unknown error"}`, - ); + defaultRuntime.error?.(`Sandbox prune failed: ${message ?? "unknown error"}`); } } diff --git a/src/agents/sandbox/registry.ts b/src/agents/sandbox/registry.ts index dcd503644cf..698a39a9427 100644 --- a/src/agents/sandbox/registry.ts +++ b/src/agents/sandbox/registry.ts @@ -45,21 +45,13 @@ export async function readRegistry(): Promise { async function writeRegistry(registry: SandboxRegistry) { await fs.mkdir(SANDBOX_STATE_DIR, { recursive: true }); - await fs.writeFile( - SANDBOX_REGISTRY_PATH, - `${JSON.stringify(registry, null, 2)}\n`, - "utf-8", - ); + await fs.writeFile(SANDBOX_REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, "utf-8"); } export async function updateRegistry(entry: SandboxRegistryEntry) { const registry = await readRegistry(); - const existing = registry.entries.find( - (item) => item.containerName === entry.containerName, - ); - const next = registry.entries.filter( - (item) => item.containerName !== entry.containerName, - ); + const existing = registry.entries.find((item) => item.containerName === entry.containerName); + const next = registry.entries.filter((item) => item.containerName !== entry.containerName); next.push({ ...entry, createdAtMs: existing?.createdAtMs ?? entry.createdAtMs, @@ -70,9 +62,7 @@ export async function updateRegistry(entry: SandboxRegistryEntry) { export async function removeRegistryEntry(containerName: string) { const registry = await readRegistry(); - const next = registry.entries.filter( - (item) => item.containerName !== containerName, - ); + const next = registry.entries.filter((item) => item.containerName !== containerName); if (next.length === registry.entries.length) return; await writeRegistry({ entries: next }); } @@ -97,16 +87,10 @@ async function writeBrowserRegistry(registry: SandboxBrowserRegistry) { ); } -export async function updateBrowserRegistry( - entry: SandboxBrowserRegistryEntry, -) { +export async function updateBrowserRegistry(entry: SandboxBrowserRegistryEntry) { const registry = await readBrowserRegistry(); - const existing = registry.entries.find( - (item) => item.containerName === entry.containerName, - ); - const next = registry.entries.filter( - (item) => item.containerName !== entry.containerName, - ); + const existing = registry.entries.find((item) => item.containerName === entry.containerName); + const next = registry.entries.filter((item) => item.containerName !== entry.containerName); next.push({ ...entry, createdAtMs: existing?.createdAtMs ?? entry.createdAtMs, @@ -117,9 +101,7 @@ export async function updateBrowserRegistry( export async function removeBrowserRegistryEntry(containerName: string) { const registry = await readBrowserRegistry(); - const next = registry.entries.filter( - (item) => item.containerName !== containerName, - ); + const next = registry.entries.filter((item) => item.containerName !== containerName); if (next.length === registry.entries.length) return; await writeBrowserRegistry({ entries: next }); } diff --git a/src/agents/sandbox/runtime-status.ts b/src/agents/sandbox/runtime-status.ts index 26304280f3d..5bf693532e3 100644 --- a/src/agents/sandbox/runtime-status.ts +++ b/src/agents/sandbox/runtime-status.ts @@ -1,19 +1,12 @@ import type { ClawdbotConfig } from "../../config/config.js"; -import { - canonicalizeMainSessionAlias, - resolveAgentMainSessionKey, -} from "../../config/sessions.js"; +import { canonicalizeMainSessionAlias, resolveAgentMainSessionKey } from "../../config/sessions.js"; import { resolveSessionAgentId } from "../agent-scope.js"; import { expandToolGroups } from "../tool-policy.js"; import { resolveSandboxConfigForAgent } from "./config.js"; import { resolveSandboxToolPolicyForAgent } from "./tool-policy.js"; import type { SandboxConfig, SandboxToolPolicyResolved } from "./types.js"; -function shouldSandboxSession( - cfg: SandboxConfig, - sessionKey: string, - mainSessionKey: string, -) { +function shouldSandboxSession(cfg: SandboxConfig, sessionKey: string, mainSessionKey: string) { if (cfg.mode === "off") return false; if (cfg.mode === "all") return true; return sessionKey.trim() !== mainSessionKey.trim(); @@ -113,9 +106,7 @@ export function formatSandboxToolPolicyBlockedMessage(params: { } const lines: string[] = []; - lines.push( - `Tool "${tool}" blocked by sandbox tool policy (mode=${runtime.mode}).`, - ); + lines.push(`Tool "${tool}" blocked by sandbox tool policy (mode=${runtime.mode}).`); lines.push(`Session: ${runtime.sessionKey || "(unknown)"}`); lines.push(`Reason: ${reasons.join(" + ")}`); lines.push("Fix:"); diff --git a/src/agents/sandbox/shared.ts b/src/agents/sandbox/shared.ts index dc067e9b427..fe9a80bcd5c 100644 --- a/src/agents/sandbox/shared.ts +++ b/src/agents/sandbox/shared.ts @@ -7,11 +7,7 @@ import { resolveAgentIdFromSessionKey } from "../agent-scope.js"; export function slugifySessionKey(value: string) { const trimmed = value.trim() || "session"; - const hash = crypto - .createHash("sha1") - .update(trimmed) - .digest("hex") - .slice(0, 8); + const hash = crypto.createHash("sha1").update(trimmed).digest("hex").slice(0, 8); const safe = trimmed .toLowerCase() .replace(/[^a-z0-9._-]+/g, "-") @@ -26,10 +22,7 @@ export function resolveSandboxWorkspaceDir(root: string, sessionKey: string) { return path.join(resolvedRoot, slug); } -export function resolveSandboxScopeKey( - scope: "session" | "agent" | "shared", - sessionKey: string, -) { +export function resolveSandboxScopeKey(scope: "session" | "agent" | "shared", sessionKey: string) { const trimmed = sessionKey.trim() || "main"; if (scope === "shared") return "shared"; if (scope === "session") return trimmed; diff --git a/src/agents/sandbox/tool-policy.ts b/src/agents/sandbox/tool-policy.ts index aac03fc24a8..09fcba9b8ea 100644 --- a/src/agents/sandbox/tool-policy.ts +++ b/src/agents/sandbox/tool-policy.ts @@ -20,8 +20,7 @@ export function resolveSandboxToolPolicyForAgent( cfg?: ClawdbotConfig, agentId?: string, ): SandboxToolPolicyResolved { - const agentConfig = - cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined; + const agentConfig = cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined; const agentAllow = agentConfig?.tools?.sandbox?.tools?.allow; const agentDeny = agentConfig?.tools?.sandbox?.tools?.deny; const globalAllow = cfg?.tools?.sandbox?.tools?.allow; diff --git a/src/agents/schema/clean-for-gemini.ts b/src/agents/schema/clean-for-gemini.ts index 2b12f4c236c..ecfc6660999 100644 --- a/src/agents/schema/clean-for-gemini.ts +++ b/src/agents/schema/clean-for-gemini.ts @@ -33,9 +33,7 @@ const UNSUPPORTED_SCHEMA_KEYWORDS = new Set([ // TypeBox Type.Literal generates { const: "value", type: "string" }. // Some schemas may use { enum: ["value"], type: "string" }. // Both patterns are flattened to { type: "string", enum: ["a", "b", ...] }. -function tryFlattenLiteralAnyOf( - variants: unknown[], -): { type: string; enum: unknown[] } | null { +function tryFlattenLiteralAnyOf(variants: unknown[]): { type: string; enum: unknown[] } | null { if (variants.length === 0) return null; const allValues: unknown[] = []; @@ -62,8 +60,7 @@ function tryFlattenLiteralAnyOf( allValues.push(literalValue); } - if (commonType && allValues.length > 0) - return { type: commonType, enum: allValues }; + if (commonType && allValues.length > 0) return { type: commonType, enum: allValues }; return null; } @@ -78,11 +75,7 @@ function isNullSchema(variant: unknown): boolean { } const typeValue = record.type; if (typeValue === "null") return true; - if ( - Array.isArray(typeValue) && - typeValue.length === 1 && - typeValue[0] === "null" - ) { + if (Array.isArray(typeValue) && typeValue.length === 1 && typeValue[0] === "null") { return true; } return false; @@ -107,9 +100,7 @@ function extendSchemaDefs( schema: Record, ): SchemaDefs | undefined { const defsEntry = - schema.$defs && - typeof schema.$defs === "object" && - !Array.isArray(schema.$defs) + schema.$defs && typeof schema.$defs === "object" && !Array.isArray(schema.$defs) ? (schema.$defs as Record) : undefined; const legacyDefsEntry = @@ -126,8 +117,7 @@ function extendSchemaDefs( for (const [key, value] of Object.entries(defsEntry)) next.set(key, value); } if (legacyDefsEntry) { - for (const [key, value] of Object.entries(legacyDefsEntry)) - next.set(key, value); + for (const [key, value] of Object.entries(legacyDefsEntry)) next.set(key, value); } return next; } @@ -136,10 +126,7 @@ function decodeJsonPointerSegment(segment: string): string { return segment.replaceAll("~1", "/").replaceAll("~0", "~"); } -function tryResolveLocalRef( - ref: string, - defs: SchemaDefs | undefined, -): unknown { +function tryResolveLocalRef(ref: string, defs: SchemaDefs | undefined): unknown { if (!defs) return undefined; const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/); if (!match) return undefined; @@ -155,9 +142,7 @@ function cleanSchemaForGeminiWithDefs( ): unknown { if (!schema || typeof schema !== "object") return schema; if (Array.isArray(schema)) { - return schema.map((item) => - cleanSchemaForGeminiWithDefs(item, defs, refStack), - ); + return schema.map((item) => cleanSchemaForGeminiWithDefs(item, defs, refStack)); } const obj = schema as Record; @@ -172,11 +157,7 @@ function cleanSchemaForGeminiWithDefs( const nextRefStack = refStack ? new Set(refStack) : new Set(); nextRefStack.add(refValue); - const cleaned = cleanSchemaForGeminiWithDefs( - resolved, - nextDefs, - nextRefStack, - ); + const cleaned = cleanSchemaForGeminiWithDefs(resolved, nextDefs, nextRefStack); if (!cleaned || typeof cleaned !== "object" || Array.isArray(cleaned)) { return cleaned; } @@ -211,9 +192,7 @@ function cleanSchemaForGeminiWithDefs( : undefined; if (hasAnyOf) { - const { variants: nonNullVariants, stripped } = stripNullVariants( - cleanedAnyOf ?? [], - ); + const { variants: nonNullVariants, stripped } = stripNullVariants(cleanedAnyOf ?? []); if (stripped) cleanedAnyOf = nonNullVariants; const flattened = tryFlattenLiteralAnyOf(nonNullVariants); @@ -243,9 +222,7 @@ function cleanSchemaForGeminiWithDefs( } if (hasOneOf) { - const { variants: nonNullVariants, stripped } = stripNullVariants( - cleanedOneOf ?? [], - ); + const { variants: nonNullVariants, stripped } = stripNullVariants(cleanedOneOf ?? []); if (stripped) cleanedOneOf = nonNullVariants; const flattened = tryFlattenLiteralAnyOf(nonNullVariants); @@ -308,15 +285,11 @@ function cleanSchemaForGeminiWithDefs( } else if (key === "anyOf" && Array.isArray(value)) { cleaned[key] = cleanedAnyOf ?? - value.map((variant) => - cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack), - ); + value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)); } else if (key === "oneOf" && Array.isArray(value)) { cleaned[key] = cleanedOneOf ?? - value.map((variant) => - cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack), - ); + value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack)); } else if (key === "allOf" && Array.isArray(value)) { cleaned[key] = value.map((variant) => cleanSchemaForGeminiWithDefs(variant, nextDefs, refStack), diff --git a/src/agents/session-tool-result-guard-wrapper.ts b/src/agents/session-tool-result-guard-wrapper.ts index 1fc253d7567..d386f85af22 100644 --- a/src/agents/session-tool-result-guard-wrapper.ts +++ b/src/agents/session-tool-result-guard-wrapper.ts @@ -11,18 +11,12 @@ export type GuardedSessionManager = SessionManager & { * Apply the tool-result guard to a SessionManager exactly once and expose * a flush method on the instance for easy teardown handling. */ -export function guardSessionManager( - sessionManager: SessionManager, -): GuardedSessionManager { - if ( - typeof (sessionManager as GuardedSessionManager).flushPendingToolResults === - "function" - ) { +export function guardSessionManager(sessionManager: SessionManager): GuardedSessionManager { + if (typeof (sessionManager as GuardedSessionManager).flushPendingToolResults === "function") { return sessionManager as GuardedSessionManager; } const guard = installSessionToolResultGuard(sessionManager); - (sessionManager as GuardedSessionManager).flushPendingToolResults = - guard.flushPendingToolResults; + (sessionManager as GuardedSessionManager).flushPendingToolResults = guard.flushPendingToolResults; return sessionManager as GuardedSessionManager; } diff --git a/src/agents/session-tool-result-guard.test.ts b/src/agents/session-tool-result-guard.test.ts index ef7eefca5cb..1bfcb31ed4e 100644 --- a/src/agents/session-tool-result-guard.test.ts +++ b/src/agents/session-tool-result-guard.test.ts @@ -26,11 +26,7 @@ describe("installSessionToolResultGuard", () => { .filter((e) => e.type === "message") .map((e) => (e as { message: AgentMessage }).message); - expect(entries.map((m) => m.role)).toEqual([ - "assistant", - "toolResult", - "assistant", - ]); + expect(entries.map((m) => m.role)).toEqual(["assistant", "toolResult", "assistant"]); const synthetic = entries[1] as { toolCallId?: string; isError?: boolean; diff --git a/src/agents/session-tool-result-guard.ts b/src/agents/session-tool-result-guard.ts index c37d0b2e96f..11d281fee13 100644 --- a/src/agents/session-tool-result-guard.ts +++ b/src/agents/session-tool-result-guard.ts @@ -5,9 +5,7 @@ import { makeMissingToolResult } from "./session-transcript-repair.js"; type ToolCall = { id: string; name?: string }; -function extractAssistantToolCalls( - msg: Extract, -): ToolCall[] { +function extractAssistantToolCalls(msg: Extract): ToolCall[] { const content = msg.content; if (!Array.isArray(content)) return []; @@ -16,11 +14,7 @@ function extractAssistantToolCalls( if (!block || typeof block !== "object") continue; const rec = block as { type?: unknown; id?: unknown; name?: unknown }; if (typeof rec.id !== "string" || !rec.id) continue; - if ( - rec.type === "toolCall" || - rec.type === "toolUse" || - rec.type === "functionCall" - ) { + if (rec.type === "toolCall" || rec.type === "toolUse" || rec.type === "functionCall") { toolCalls.push({ id: rec.id, name: typeof rec.name === "string" ? rec.name : undefined, @@ -30,9 +24,7 @@ function extractAssistantToolCalls( return toolCalls; } -function extractToolResultId( - msg: Extract, -): string | null { +function extractToolResultId(msg: Extract): string | null { const toolCallId = (msg as { toolCallId?: unknown }).toolCallId; if (typeof toolCallId === "string" && toolCallId) return toolCallId; const toolUseId = (msg as { toolUseId?: unknown }).toolUseId; @@ -59,18 +51,14 @@ export function installSessionToolResultGuard(sessionManager: SessionManager): { const role = (message as { role?: unknown }).role; if (role === "toolResult") { - const id = extractToolResultId( - message as Extract, - ); + const id = extractToolResultId(message as Extract); if (id) pending.delete(id); return originalAppend(message as never); } const toolCalls = role === "assistant" - ? extractAssistantToolCalls( - message as Extract, - ) + ? extractAssistantToolCalls(message as Extract) : []; // If previous tool calls are still pending, flush before non-tool results. @@ -94,8 +82,7 @@ export function installSessionToolResultGuard(sessionManager: SessionManager): { }; // Monkey-patch appendMessage with our guarded version. - sessionManager.appendMessage = - guardedAppend as SessionManager["appendMessage"]; + sessionManager.appendMessage = guardedAppend as SessionManager["appendMessage"]; return { flushPendingToolResults, diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index 7d736f01410..ccc63ec7f14 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -35,9 +35,7 @@ describe("sanitizeToolUseResultPairing", () => { const input = [ { role: "assistant", - content: [ - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], }, { role: "toolResult", @@ -64,9 +62,7 @@ describe("sanitizeToolUseResultPairing", () => { const input = [ { role: "assistant", - content: [ - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], }, { role: "toolResult", diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index b214f996451..e608a8d7373 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -17,11 +17,7 @@ function extractToolCallsFromAssistant( const rec = block as { type?: unknown; id?: unknown; name?: unknown }; if (typeof rec.id !== "string" || !rec.id) continue; - if ( - rec.type === "toolCall" || - rec.type === "toolUse" || - rec.type === "functionCall" - ) { + if (rec.type === "toolCall" || rec.type === "toolUse" || rec.type === "functionCall") { toolCalls.push({ id: rec.id, name: typeof rec.name === "string" ? rec.name : undefined, @@ -31,9 +27,7 @@ function extractToolCallsFromAssistant( return toolCalls; } -function extractToolResultId( - msg: Extract, -): string | null { +function extractToolResultId(msg: Extract): string | null { const toolCallId = (msg as { toolCallId?: unknown }).toolCallId; if (typeof toolCallId === "string" && toolCallId) return toolCallId; const toolUseId = (msg as { toolUseId?: unknown }).toolUseId; @@ -62,9 +56,7 @@ function makeMissingToolResult(params: { export { makeMissingToolResult }; -export function sanitizeToolUseResultPairing( - messages: AgentMessage[], -): AgentMessage[] { +export function sanitizeToolUseResultPairing(messages: AgentMessage[]): AgentMessage[] { return repairToolUseResultPairing(messages).messages; } @@ -76,9 +68,7 @@ export type ToolUseRepairReport = { moved: boolean; }; -export function repairToolUseResultPairing( - messages: AgentMessage[], -): ToolUseRepairReport { +export function repairToolUseResultPairing(messages: AgentMessage[]): ToolUseRepairReport { // Anthropic (and Cloud Code Assist) reject transcripts where assistant tool calls are not // immediately followed by matching tool results. Session files can end up with results // displaced (e.g. after user turns) or duplicated. Repair by: @@ -93,9 +83,7 @@ export function repairToolUseResultPairing( let moved = false; let changed = false; - const pushToolResult = ( - msg: Extract, - ) => { + const pushToolResult = (msg: Extract) => { const id = extractToolResultId(msg); if (id && seenToolResultIds.has(id)) { droppedDuplicateCount += 1; @@ -136,10 +124,7 @@ export function repairToolUseResultPairing( const toolCallIds = new Set(toolCalls.map((t) => t.id)); - const spanResultsById = new Map< - string, - Extract - >(); + const spanResultsById = new Map>(); const remainder: AgentMessage[] = []; let j = i + 1; @@ -154,10 +139,7 @@ export function repairToolUseResultPairing( if (nextRole === "assistant") break; if (nextRole === "toolResult") { - const toolResult = next as Extract< - AgentMessage, - { role: "toolResult" } - >; + const toolResult = next as Extract; const id = extractToolResultId(toolResult); if (id && toolCallIds.has(id)) { if (seenToolResultIds.has(id)) { diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index fd0283ddf59..767fb9c4ea7 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -23,9 +23,7 @@ function isAlive(pid: number): boolean { } } -async function readLockPayload( - lockPath: string, -): Promise { +async function readLockPayload(lockPath: string): Promise { try { const raw = await fs.readFile(lockPath, "utf8"); const parsed = JSON.parse(raw) as Partial; @@ -72,11 +70,7 @@ export async function acquireSessionWriteLock(params: { try { const handle = await fs.open(lockPath, "wx"); await handle.writeFile( - JSON.stringify( - { pid: process.pid, createdAt: new Date().toISOString() }, - null, - 2, - ), + JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2), "utf8", ); HELD_LOCKS.set(sessionFile, { count: 1, handle, lockPath }); @@ -95,11 +89,8 @@ export async function acquireSessionWriteLock(params: { const code = (err as { code?: unknown }).code; if (code !== "EEXIST") throw err; const payload = await readLockPayload(lockPath); - const createdAt = payload?.createdAt - ? Date.parse(payload.createdAt) - : NaN; - const stale = - !Number.isFinite(createdAt) || Date.now() - createdAt > staleMs; + const createdAt = payload?.createdAt ? Date.parse(payload.createdAt) : NaN; + const stale = !Number.isFinite(createdAt) || Date.now() - createdAt > staleMs; const alive = payload?.pid ? isAlive(payload.pid) : false; if (stale || !alive) { await fs.rm(lockPath, { force: true }); @@ -113,7 +104,5 @@ export async function acquireSessionWriteLock(params: { const payload = await readLockPayload(lockPath); const owner = payload?.pid ? `pid=${payload.pid}` : "unknown"; - throw new Error( - `session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`, - ); + throw new Error(`session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`); } diff --git a/src/agents/skills-install.ts b/src/agents/skills-install.ts index 12e4af812d8..3f30cf66aeb 100644 --- a/src/agents/skills-install.ts +++ b/src/agents/skills-install.ts @@ -47,9 +47,7 @@ function summarizeInstallOutput(text: string): string | undefined { if (!preferred) return undefined; const normalized = preferred.replace(/\s+/g, " ").trim(); const maxLen = 200; - return normalized.length > maxLen - ? `${normalized.slice(0, maxLen - 1)}…` - : normalized; + return normalized.length > maxLen ? `${normalized.slice(0, maxLen - 1)}…` : normalized; } function formatInstallFailureMessage(result: { @@ -57,11 +55,8 @@ function formatInstallFailureMessage(result: { stdout: string; stderr: string; }): string { - const code = - typeof result.code === "number" ? `exit ${result.code}` : "unknown exit"; - const summary = - summarizeInstallOutput(result.stderr) ?? - summarizeInstallOutput(result.stdout); + const code = typeof result.code === "number" ? `exit ${result.code}` : "unknown exit"; + const summary = summarizeInstallOutput(result.stderr) ?? summarizeInstallOutput(result.stdout); if (!summary) return `Install failed (${code})`; return `Install failed (${code}): ${summary}`; } @@ -70,10 +65,7 @@ function resolveInstallId(spec: SkillInstallSpec, index: number): string { return (spec.id ?? `${spec.kind}-${index}`).trim(); } -function findInstallSpec( - entry: SkillEntry, - installId: string, -): SkillInstallSpec | undefined { +function findInstallSpec(entry: SkillEntry, installId: string): SkillInstallSpec | undefined { const specs = entry.clawdbot?.install ?? []; for (const [index, spec] of specs.entries()) { if (resolveInstallId(spec, index) === installId) return spec; @@ -81,10 +73,7 @@ function findInstallSpec( return undefined; } -function buildNodeInstallCommand( - packageName: string, - prefs: SkillsInstallPreferences, -): string[] { +function buildNodeInstallCommand(packageName: string, prefs: SkillsInstallPreferences): string[] { switch (prefs.nodeManager) { case "pnpm": return ["pnpm", "add", "-g", packageName]; @@ -128,10 +117,7 @@ function buildInstallCommand( } } -async function resolveBrewBinDir( - timeoutMs: number, - brewExe?: string, -): Promise { +async function resolveBrewBinDir(timeoutMs: number, brewExe?: string): Promise { const exe = brewExe ?? (hasBinary("brew") ? "brew" : resolveBrewExecutable()); if (!exe) return undefined; @@ -156,13 +142,8 @@ async function resolveBrewBinDir( return undefined; } -export async function installSkill( - params: SkillInstallRequest, -): Promise { - const timeoutMs = Math.min( - Math.max(params.timeoutMs ?? 300_000, 1_000), - 900_000, - ); +export async function installSkill(params: SkillInstallRequest): Promise { + const timeoutMs = Math.min(Math.max(params.timeoutMs ?? 300_000, 1_000), 900_000); const workspaceDir = resolveUserPath(params.workspaceDir); const entries = loadWorkspaceSkillEntries(workspaceDir); const entry = entries.find((item) => item.skill.name === params.skillName); @@ -211,12 +192,9 @@ export async function installSkill( } if (spec.kind === "uv" && !hasBinary("uv")) { if (brewExe) { - const brewResult = await runCommandWithTimeout( - [brewExe, "install", "uv"], - { - timeoutMs, - }, - ); + const brewResult = await runCommandWithTimeout([brewExe, "install", "uv"], { + timeoutMs, + }); if (brewResult.code !== 0) { return { ok: false, @@ -252,12 +230,9 @@ export async function installSkill( if (spec.kind === "go" && !hasBinary("go")) { if (brewExe) { - const brewResult = await runCommandWithTimeout( - [brewExe, "install", "go"], - { - timeoutMs, - }, - ); + const brewResult = await runCommandWithTimeout([brewExe, "install", "go"], { + timeoutMs, + }); if (brewResult.code !== 0) { return { ok: false, diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index 55336f75c40..e618b0c474d 100644 --- a/src/agents/skills-status.ts +++ b/src/agents/skills-status.ts @@ -162,9 +162,7 @@ function buildSkillStatus( ? requiredAnyBins : []; const missingOs = - requiredOs.length > 0 && !requiredOs.includes(process.platform) - ? requiredOs - : []; + requiredOs.length > 0 && !requiredOs.includes(process.platform) ? requiredOs : []; const missingEnv: string[] = []; for (const envName of requiredEnv) { @@ -176,16 +174,12 @@ function buildSkillStatus( missingEnv.push(envName); } - const configChecks: SkillStatusConfigCheck[] = requiredConfig.map( - (pathStr) => { - const value = resolveConfigPath(config, pathStr); - const satisfied = isConfigPathTruthy(config, pathStr); - return { path: pathStr, value, satisfied }; - }, - ); - const missingConfig = configChecks - .filter((check) => !check.satisfied) - .map((check) => check.path); + const configChecks: SkillStatusConfigCheck[] = requiredConfig.map((pathStr) => { + const value = resolveConfigPath(config, pathStr); + const satisfied = isConfigPathTruthy(config, pathStr); + return { path: pathStr, value, satisfied }; + }); + const missingConfig = configChecks.filter((check) => !check.satisfied).map((check) => check.path); const missing = always ? { bins: [], anyBins: [], env: [], config: [], os: [] } @@ -229,10 +223,7 @@ function buildSkillStatus( }, missing, configChecks, - install: normalizeInstallOptions( - entry, - prefs ?? resolveSkillsInstallPreferences(config), - ), + install: normalizeInstallOptions(entry, prefs ?? resolveSkillsInstallPreferences(config)), }; } @@ -244,16 +235,12 @@ export function buildWorkspaceSkillStatus( entries?: SkillEntry[]; }, ): SkillStatusReport { - const managedSkillsDir = - opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills"); - const skillEntries = - opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts); + const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills"); + const skillEntries = opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts); const prefs = resolveSkillsInstallPreferences(opts?.config); return { workspaceDir, managedSkillsDir, - skills: skillEntries.map((entry) => - buildSkillStatus(entry, opts?.config, prefs), - ), + skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config, prefs)), }; } diff --git a/src/agents/skills.applyskillenvoverrides.test.ts b/src/agents/skills.applyskillenvoverrides.test.ts index 5049bb5c6b9..311bd8819e5 100644 --- a/src/agents/skills.applyskillenvoverrides.test.ts +++ b/src/agents/skills.applyskillenvoverrides.test.ts @@ -39,8 +39,7 @@ describe("applySkillEnvOverrides", () => { dir: skillDir, name: "env-skill", description: "Needs env", - metadata: - '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', + metadata: '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', }); const entries = loadWorkspaceSkillEntries(workspaceDir, { @@ -73,8 +72,7 @@ describe("applySkillEnvOverrides", () => { dir: skillDir, name: "env-skill", description: "Needs env", - metadata: - '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', + metadata: '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', }); const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, { diff --git a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts index e3ab0110b5f..050d3d631ee 100644 --- a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts @@ -80,8 +80,7 @@ describe("buildWorkspaceSkillsPrompt", () => { dir: path.join(skillsDir, "anybin-skill"), name: "anybin-skill", description: "Needs any bin", - metadata: - '{"clawdbot":{"requires":{"anyBins":["missingbin","fakebin"]}}}', + metadata: '{"clawdbot":{"requires":{"anyBins":["missingbin","fakebin"]}}}', }); await writeSkill({ dir: path.join(skillsDir, "config-skill"), @@ -99,8 +98,7 @@ describe("buildWorkspaceSkillsPrompt", () => { dir: path.join(skillsDir, "env-skill"), name: "env-skill", description: "Needs env", - metadata: - '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', + metadata: '{"clawdbot":{"requires":{"env":["ENV_KEY"]},"primaryEnv":"ENV_KEY"}}', }); try { diff --git a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts index cca8566d17c..d40e66a374b 100644 --- a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts @@ -28,12 +28,8 @@ ${body ?? `# ${name}\n`} describe("buildWorkspaceSkillsPrompt", () => { it("syncs merged skills into a target workspace", async () => { - const sourceWorkspace = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-"), - ); - const targetWorkspace = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-"), - ); + const sourceWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-")); + const targetWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-")); const extraDir = path.join(sourceWorkspace, ".extra"); const bundledDir = path.join(sourceWorkspace, ".bundled"); const managedDir = path.join(sourceWorkspace, ".managed"); @@ -76,9 +72,7 @@ describe("buildWorkspaceSkillsPrompt", () => { expect(prompt).not.toContain("Managed version"); expect(prompt).not.toContain("Bundled version"); expect(prompt).not.toContain("Extra version"); - expect(prompt).toContain( - path.join(targetWorkspace, "skills", "demo-skill", "SKILL.md"), - ); + expect(prompt).toContain(path.join(targetWorkspace, "skills", "demo-skill", "SKILL.md")); }); it("filters skills based on env/config gates", async () => { const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-")); diff --git a/src/agents/skills.ts b/src/agents/skills.ts index 354f0ea8270..7ef5cfe1636 100644 --- a/src/agents/skills.ts +++ b/src/agents/skills.ts @@ -32,14 +32,10 @@ export { export function resolveSkillsInstallPreferences(config?: ClawdbotConfig) { const raw = config?.skills?.install; const preferBrew = raw?.preferBrew ?? true; - const managerRaw = - typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : ""; + const managerRaw = typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : ""; const manager = managerRaw.toLowerCase(); const nodeManager = - manager === "pnpm" || - manager === "yarn" || - manager === "bun" || - manager === "npm" + manager === "pnpm" || manager === "yarn" || manager === "bun" || manager === "npm" ? (manager as "npm" | "pnpm" | "yarn" | "bun") : "npm"; return { preferBrew, nodeManager }; diff --git a/src/agents/skills/config.ts b/src/agents/skills/config.ts index 2aede1d391d..3bf5131723b 100644 --- a/src/agents/skills/config.ts +++ b/src/agents/skills/config.ts @@ -16,10 +16,7 @@ function isTruthy(value: unknown): boolean { return true; } -export function resolveConfigPath( - config: ClawdbotConfig | undefined, - pathStr: string, -) { +export function resolveConfigPath(config: ClawdbotConfig | undefined, pathStr: string) { const parts = pathStr.split(".").filter(Boolean); let current: unknown = config; for (const part of parts) { @@ -29,10 +26,7 @@ export function resolveConfigPath( return current; } -export function isConfigPathTruthy( - config: ClawdbotConfig | undefined, - pathStr: string, -): boolean { +export function isConfigPathTruthy(config: ClawdbotConfig | undefined, pathStr: string): boolean { const value = resolveConfigPath(config, pathStr); if (value === undefined && pathStr in DEFAULT_CONFIG_VALUES) { return DEFAULT_CONFIG_VALUES[pathStr] === true; @@ -66,16 +60,11 @@ function isBundledSkill(entry: SkillEntry): boolean { return entry.skill.source === "clawdbot-bundled"; } -export function resolveBundledAllowlist( - config?: ClawdbotConfig, -): string[] | undefined { +export function resolveBundledAllowlist(config?: ClawdbotConfig): string[] | undefined { return normalizeAllowlist(config?.skills?.allowBundled); } -export function isBundledSkillAllowed( - entry: SkillEntry, - allowlist?: string[], -): boolean { +export function isBundledSkillAllowed(entry: SkillEntry, allowlist?: string[]): boolean { if (!allowlist || allowlist.length === 0) return true; if (!isBundledSkill(entry)) return true; const key = resolveSkillKey(entry.skill, entry); diff --git a/src/agents/skills/env-overrides.ts b/src/agents/skills/env-overrides.ts index 0b374d691a9..17e4f3ed84d 100644 --- a/src/agents/skills/env-overrides.ts +++ b/src/agents/skills/env-overrides.ts @@ -3,10 +3,7 @@ import { resolveSkillConfig } from "./config.js"; import { resolveSkillKey } from "./frontmatter.js"; import type { SkillEntry, SkillSnapshot } from "./types.js"; -export function applySkillEnvOverrides(params: { - skills: SkillEntry[]; - config?: ClawdbotConfig; -}) { +export function applySkillEnvOverrides(params: { skills: SkillEntry[]; config?: ClawdbotConfig }) { const { skills, config } = params; const updates: Array<{ key: string; prev: string | undefined }> = []; @@ -58,11 +55,7 @@ export function applySkillEnvOverridesFromSnapshot(params: { } } - if ( - skill.primaryEnv && - skillConfig.apiKey && - !process.env[skill.primaryEnv] - ) { + if (skill.primaryEnv && skillConfig.apiKey && !process.env[skill.primaryEnv]) { updates.push({ key: skill.primaryEnv, prev: process.env[skill.primaryEnv], diff --git a/src/agents/skills/frontmatter.ts b/src/agents/skills/frontmatter.ts index e4ce9384d64..c145fad8b48 100644 --- a/src/agents/skills/frontmatter.ts +++ b/src/agents/skills/frontmatter.ts @@ -53,11 +53,7 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { if (!input || typeof input !== "object") return undefined; const raw = input as Record; const kindRaw = - typeof raw.kind === "string" - ? raw.kind - : typeof raw.type === "string" - ? raw.type - : ""; + typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : ""; const kind = kindRaw.trim().toLowerCase(); if (kind !== "brew" && kind !== "node" && kind !== "go" && kind !== "uv") { return undefined; @@ -78,10 +74,7 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { return spec; } -function getFrontmatterValue( - frontmatter: ParsedSkillFrontmatter, - key: string, -): string | undefined { +function getFrontmatterValue(frontmatter: ParsedSkillFrontmatter, key: string): string | undefined { const raw = frontmatter[key]; return typeof raw === "string" ? raw : undefined; } @@ -101,32 +94,17 @@ export function resolveClawdbotMetadata( typeof clawdbotObj.requires === "object" && clawdbotObj.requires !== null ? (clawdbotObj.requires as Record) : undefined; - const installRaw = Array.isArray(clawdbotObj.install) - ? (clawdbotObj.install as unknown[]) - : []; + const installRaw = Array.isArray(clawdbotObj.install) ? (clawdbotObj.install as unknown[]) : []; const install = installRaw .map((entry) => parseInstallSpec(entry)) .filter((entry): entry is SkillInstallSpec => Boolean(entry)); const osRaw = normalizeStringList(clawdbotObj.os); return { - always: - typeof clawdbotObj.always === "boolean" - ? clawdbotObj.always - : undefined, - emoji: - typeof clawdbotObj.emoji === "string" ? clawdbotObj.emoji : undefined, - homepage: - typeof clawdbotObj.homepage === "string" - ? clawdbotObj.homepage - : undefined, - skillKey: - typeof clawdbotObj.skillKey === "string" - ? clawdbotObj.skillKey - : undefined, - primaryEnv: - typeof clawdbotObj.primaryEnv === "string" - ? clawdbotObj.primaryEnv - : undefined, + always: typeof clawdbotObj.always === "boolean" ? clawdbotObj.always : undefined, + emoji: typeof clawdbotObj.emoji === "string" ? clawdbotObj.emoji : undefined, + homepage: typeof clawdbotObj.homepage === "string" ? clawdbotObj.homepage : undefined, + skillKey: typeof clawdbotObj.skillKey === "string" ? clawdbotObj.skillKey : undefined, + primaryEnv: typeof clawdbotObj.primaryEnv === "string" ? clawdbotObj.primaryEnv : undefined, os: osRaw.length > 0 ? osRaw : undefined, requires: requiresRaw ? { diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index 2cbea38710d..22e7bac8bec 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -13,11 +13,7 @@ import { resolveBundledSkillsDir } from "./bundled-dir.js"; import { shouldIncludeSkill } from "./config.js"; import { parseFrontmatter, resolveClawdbotMetadata } from "./frontmatter.js"; import { serializeByKey } from "./serialize.js"; -import type { - ParsedSkillFrontmatter, - SkillEntry, - SkillSnapshot, -} from "./types.js"; +import type { ParsedSkillFrontmatter, SkillEntry, SkillSnapshot } from "./types.js"; const fsp = fs.promises; @@ -26,23 +22,17 @@ function filterSkillEntries( config?: ClawdbotConfig, skillFilter?: string[], ): SkillEntry[] { - let filtered = entries.filter((entry) => - shouldIncludeSkill({ entry, config }), - ); + let filtered = entries.filter((entry) => shouldIncludeSkill({ entry, config })); // If skillFilter is provided, only include skills in the filter list. if (skillFilter !== undefined) { - const normalized = skillFilter - .map((entry) => String(entry).trim()) - .filter(Boolean); + const normalized = skillFilter.map((entry) => String(entry).trim()).filter(Boolean); const label = normalized.length > 0 ? normalized.join(", ") : "(none)"; console.log(`[skills] Applying skill filter: ${label}`); filtered = normalized.length > 0 ? filtered.filter((entry) => normalized.includes(entry.skill.name)) : []; - console.log( - `[skills] After filter: ${filtered.map((entry) => entry.skill.name).join(", ")}`, - ); + console.log(`[skills] After filter: ${filtered.map((entry) => entry.skill.name).join(", ")}`); } return filtered; } @@ -69,8 +59,7 @@ function loadSkillEntries( return []; }; - const managedSkillsDir = - opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills"); + const managedSkillsDir = opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills"); const workspaceSkillsDir = path.join(workspaceDir, "skills"); const bundledSkillsDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir(); const extraDirsRaw = opts?.config?.skills?.load?.extraDirs ?? []; @@ -107,22 +96,20 @@ function loadSkillEntries( for (const skill of managedSkills) merged.set(skill.name, skill); for (const skill of workspaceSkills) merged.set(skill.name, skill); - const skillEntries: SkillEntry[] = Array.from(merged.values()).map( - (skill) => { - let frontmatter: ParsedSkillFrontmatter = {}; - try { - const raw = fs.readFileSync(skill.filePath, "utf-8"); - frontmatter = parseFrontmatter(raw); - } catch { - // ignore malformed skills - } - return { - skill, - frontmatter, - clawdbot: resolveClawdbotMetadata(frontmatter), - }; - }, - ); + const skillEntries: SkillEntry[] = Array.from(merged.values()).map((skill) => { + let frontmatter: ParsedSkillFrontmatter = {}; + try { + const raw = fs.readFileSync(skill.filePath, "utf-8"); + frontmatter = parseFrontmatter(raw); + } catch { + // ignore malformed skills + } + return { + skill, + frontmatter, + clawdbot: resolveClawdbotMetadata(frontmatter), + }; + }); return skillEntries; } @@ -138,11 +125,7 @@ export function buildWorkspaceSkillSnapshot( }, ): SkillSnapshot { const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts); - const eligible = filterSkillEntries( - skillEntries, - opts?.config, - opts?.skillFilter, - ); + const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter); const resolvedSkills = eligible.map((entry) => entry.skill); return { prompt: formatSkillsForPrompt(resolvedSkills), @@ -166,11 +149,7 @@ export function buildWorkspaceSkillsPrompt( }, ): string { const skillEntries = opts?.entries ?? loadSkillEntries(workspaceDir, opts); - const eligible = filterSkillEntries( - skillEntries, - opts?.config, - opts?.skillFilter, - ); + const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter); return formatSkillsForPrompt(eligible.map((entry) => entry.skill)); } @@ -234,11 +213,8 @@ export async function syncSkillsToWorkspace(params: { force: true, }); } catch (error) { - const message = - error instanceof Error ? error.message : JSON.stringify(error); - console.warn( - `[skills] Failed to copy ${entry.skill.name} to sandbox: ${message}`, - ); + const message = error instanceof Error ? error.message : JSON.stringify(error); + console.warn(`[skills] Failed to copy ${entry.skill.name} to sandbox: ${message}`); } } }); diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index cae87cd647d..93a011b7cb0 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -91,17 +91,13 @@ async function buildSubagentStatsLine(params: { const sessionId = entry?.sessionId; const transcriptPath = - sessionId && storePath - ? path.join(path.dirname(storePath), `${sessionId}.jsonl`) - : undefined; + sessionId && storePath ? path.join(path.dirname(storePath), `${sessionId}.jsonl`) : undefined; const input = entry?.inputTokens; const output = entry?.outputTokens; const total = entry?.totalTokens ?? - (typeof input === "number" && typeof output === "number" - ? input + output - : undefined); + (typeof input === "number" && typeof output === "number" ? input + output : undefined); const runtimeMs = typeof params.startedAt === "number" && typeof params.endedAt === "number" ? Math.max(0, params.endedAt - params.startedAt) @@ -119,10 +115,8 @@ async function buildSubagentStatsLine(params: { const runtime = formatDurationShort(runtimeMs); parts.push(`runtime ${runtime ?? "n/a"}`); if (typeof total === "number") { - const inputText = - typeof input === "number" ? formatTokenCount(input) : "n/a"; - const outputText = - typeof output === "number" ? formatTokenCount(output) : "n/a"; + const inputText = typeof input === "number" ? formatTokenCount(input) : "n/a"; + const outputText = typeof output === "number" ? formatTokenCount(output) : "n/a"; const totalText = formatTokenCount(total); parts.push(`tokens ${totalText} (in ${inputText} / out ${outputText})`); } else { @@ -179,12 +173,8 @@ export function buildSubagentSystemPrompt(params: { "", "## Session Context", params.label ? `- Label: ${params.label}` : undefined, - params.requesterSessionKey - ? `- Requester session: ${params.requesterSessionKey}.` - : undefined, - params.requesterChannel - ? `- Requester channel: ${params.requesterChannel}.` - : undefined, + params.requesterSessionKey ? `- Requester session: ${params.requesterSessionKey}.` : undefined, + params.requesterChannel ? `- Requester channel: ${params.requesterChannel}.` : undefined, `- Your session: ${params.childSessionKey}.`, "", "Run the task. Provide a clear final answer (plain text).", @@ -202,12 +192,8 @@ function buildSubagentAnnouncePrompt(params: { }) { const lines = [ "Sub-agent announce step:", - params.requesterSessionKey - ? `Requester session: ${params.requesterSessionKey}.` - : undefined, - params.requesterChannel - ? `Requester channel: ${params.requesterChannel}.` - : undefined, + params.requesterSessionKey ? `Requester session: ${params.requesterSessionKey}.` : undefined, + params.requesterChannel ? `Requester channel: ${params.requesterChannel}.` : undefined, `Post target channel: ${params.announceChannel}.`, `Original task: ${params.task}`, params.subagentReply @@ -285,21 +271,14 @@ export async function runSubagentAnnounceFlow(params: { lane: AGENT_LANE_NESTED, }); - if ( - !announceReply || - !announceReply.trim() || - isAnnounceSkip(announceReply) - ) - return false; + if (!announceReply || !announceReply.trim() || isAnnounceSkip(announceReply)) return false; const statsLine = await buildSubagentStatsLine({ sessionKey: params.childSessionKey, startedAt: params.startedAt, endedAt: params.endedAt, }); - const message = statsLine - ? `${announceReply.trim()}\n\n${statsLine}` - : announceReply.trim(); + const message = statsLine ? `${announceReply.trim()}\n\n${statsLine}` : announceReply.trim(); await callGateway({ method: "send", diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.test.ts index 942685d01ec..995e202cc3d 100644 --- a/src/agents/subagent-registry.persistence.test.ts +++ b/src/agents/subagent-registry.persistence.test.ts @@ -42,9 +42,7 @@ describe("subagent registry persistence", () => { }); it("persists runs to disk and resumes after restart", async () => { - tempStateDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-subagent-"), - ); + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-subagent-")); process.env.CLAWDBOT_STATE_DIR = tempStateDir; vi.resetModules(); @@ -85,9 +83,7 @@ describe("subagent registry persistence", () => { }); it("retries announce even when announceHandled was persisted", async () => { - tempStateDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-subagent-"), - ); + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-subagent-")); process.env.CLAWDBOT_STATE_DIR = tempStateDir; const registryPath = path.join(tempStateDir, "subagents", "runs.json"); diff --git a/src/agents/subagent-registry.store.ts b/src/agents/subagent-registry.store.ts index 24c9d6e15d9..6312698f807 100644 --- a/src/agents/subagent-registry.store.ts +++ b/src/agents/subagent-registry.store.ts @@ -33,9 +33,7 @@ export function loadSubagentRegistryFromDisk(): Map { const typed = entry as PersistedSubagentRunRecord; if (!typed.runId || typeof typed.runId !== "string") continue; const announceCompletedAt = - typeof typed.announceCompletedAt === "number" - ? typed.announceCompletedAt - : undefined; + typeof typed.announceCompletedAt === "number" ? typed.announceCompletedAt : undefined; out.set(runId, { ...typed, announceCompletedAt, @@ -45,9 +43,7 @@ export function loadSubagentRegistryFromDisk(): Map { return out; } -export function saveSubagentRegistryToDisk( - runs: Map, -) { +export function saveSubagentRegistryToDisk(runs: Map) { const pathname = resolveSubagentRegistryPath(); const serialized: Record = {}; for (const [runId, entry] of runs.entries()) { diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index d37ce0453ae..5604ce590dd 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -165,9 +165,7 @@ function ensureListener() { const phase = evt.data?.phase; if (phase === "start") { const startedAt = - typeof evt.data?.startedAt === "number" - ? (evt.data.startedAt as number) - : undefined; + typeof evt.data?.startedAt === "number" ? (evt.data.startedAt as number) : undefined; if (startedAt) { entry.startedAt = startedAt; persistSubagentRuns(); @@ -176,9 +174,7 @@ function ensureListener() { } if (phase !== "end" && phase !== "error") return; const endedAt = - typeof evt.data?.endedAt === "number" - ? (evt.data.endedAt as number) - : Date.now(); + typeof evt.data?.endedAt === "number" ? (evt.data.endedAt as number) : Date.now(); entry.endedAt = endedAt; persistSubagentRuns(); @@ -205,11 +201,7 @@ function ensureListener() { }); } -function finalizeSubagentAnnounce( - runId: string, - cleanup: "delete" | "keep", - didAnnounce: boolean, -) { +function finalizeSubagentAnnounce(runId: string, cleanup: "delete" | "keep", didAnnounce: boolean) { const entry = subagentRuns.get(runId); if (!entry) return; if (cleanup === "delete") { @@ -247,10 +239,7 @@ export function registerSubagentRun(params: { const cfg = loadConfig(); const archiveAfterMs = resolveArchiveAfterMs(cfg); const archiveAtMs = archiveAfterMs ? now + archiveAfterMs : undefined; - const waitTimeoutMs = resolveSubagentWaitTimeoutMs( - cfg, - params.runTimeoutSeconds, - ); + const waitTimeoutMs = resolveSubagentWaitTimeoutMs(cfg, params.runTimeoutSeconds); subagentRuns.set(params.runId, { runId: params.runId, childSessionKey: params.childSessionKey, diff --git a/src/agents/synthetic-models.ts b/src/agents/synthetic-models.ts index 077c00564f9..e31f3c795c7 100644 --- a/src/agents/synthetic-models.ts +++ b/src/agents/synthetic-models.ts @@ -167,9 +167,7 @@ export const SYNTHETIC_MODEL_CATALOG = [ export type SyntheticCatalogEntry = (typeof SYNTHETIC_MODEL_CATALOG)[number]; -export function buildSyntheticModelDefinition( - entry: SyntheticCatalogEntry, -): ModelDefinitionConfig { +export function buildSyntheticModelDefinition(entry: SyntheticCatalogEntry): ModelDefinitionConfig { return { id: entry.id, name: entry.name, diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index b4b17c8aa68..cdb0c345383 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -62,8 +62,7 @@ export function buildAgentSystemPrompt(params: { nodes: "List/describe/notify/camera/screen on paired nodes", cron: "Manage cron jobs and wake events (use for reminders)", message: "Send messages and channel actions", - gateway: - "Restart, apply config, or run updates on the running Clawdbot process", + gateway: "Restart, apply config, or run updates on the running Clawdbot process", agents_list: "List agent ids allowed for sessions_spawn", sessions_list: "List other sessions (incl. sub-agents) with filters/last", sessions_history: "Fetch history for another session/sub-agent", @@ -138,9 +137,7 @@ export function buildAgentSystemPrompt(params: { const execToolName = resolveToolName("exec"); const processToolName = resolveToolName("process"); const extraSystemPrompt = params.extraSystemPrompt?.trim(); - const ownerNumbers = (params.ownerNumbers ?? []) - .map((value) => value.trim()) - .filter(Boolean); + const ownerNumbers = (params.ownerNumbers ?? []).map((value) => value.trim()).filter(Boolean); const ownerLine = ownerNumbers.length > 0 ? `Owner numbers: ${ownerNumbers.join(", ")}. Treat messages from these numbers as the user.` @@ -170,9 +167,7 @@ export function buildAgentSystemPrompt(params: { const runtimeCapabilities = (runtimeInfo?.capabilities ?? []) .map((cap) => String(cap).trim()) .filter(Boolean); - const runtimeCapabilitiesLower = new Set( - runtimeCapabilities.map((cap) => cap.toLowerCase()), - ); + const runtimeCapabilitiesLower = new Set(runtimeCapabilities.map((cap) => cap.toLowerCase())); const inlineButtonsEnabled = runtimeCapabilitiesLower.has("inlinebuttons"); const skillsLines = skillsPrompt ? [skillsPrompt, ""] : []; const skillsSection = skillsPrompt @@ -232,9 +227,7 @@ export function buildAgentSystemPrompt(params: { : "", hasGateway ? "" : "", "", - params.modelAliasLines && params.modelAliasLines.length > 0 - ? "## Model Aliases" - : "", + params.modelAliasLines && params.modelAliasLines.length > 0 ? "## Model Aliases" : "", params.modelAliasLines && params.modelAliasLines.length > 0 ? "Prefer aliases when specifying model overrides; full provider/model is also accepted." : "", @@ -274,26 +267,18 @@ export function buildAgentSystemPrompt(params: { ? "Host browser control: blocked." : "", params.sandboxInfo.allowedControlUrls?.length - ? `Browser control URL allowlist: ${params.sandboxInfo.allowedControlUrls.join( - ", ", - )}` + ? `Browser control URL allowlist: ${params.sandboxInfo.allowedControlUrls.join(", ")}` : "", params.sandboxInfo.allowedControlHosts?.length - ? `Browser control host allowlist: ${params.sandboxInfo.allowedControlHosts.join( - ", ", - )}` + ? `Browser control host allowlist: ${params.sandboxInfo.allowedControlHosts.join(", ")}` : "", params.sandboxInfo.allowedControlPorts?.length - ? `Browser control port allowlist: ${params.sandboxInfo.allowedControlPorts.join( - ", ", - )}` + ? `Browser control port allowlist: ${params.sandboxInfo.allowedControlPorts.join(", ")}` : "", params.sandboxInfo.elevated?.allowed ? "Elevated exec is available for this session." : "", - params.sandboxInfo.elevated?.allowed - ? "User can toggle with /elevated on|off." - : "", + params.sandboxInfo.elevated?.allowed ? "User can toggle with /elevated on|off." : "", params.sandboxInfo.elevated?.allowed ? "You may also send /elevated on|off when needed." : "", @@ -399,11 +384,7 @@ export function buildAgentSystemPrompt(params: { runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", runtimeChannel ? `channel=${runtimeChannel}` : "", runtimeChannel - ? `capabilities=${ - runtimeCapabilities.length > 0 - ? runtimeCapabilities.join(",") - : "none" - }` + ? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}` : "", `thinking=${params.defaultThinkLevel ?? "off"}`, ] diff --git a/src/agents/timeout.ts b/src/agents/timeout.ts index 5cb5dbca364..95ad4970f6d 100644 --- a/src/agents/timeout.ts +++ b/src/agents/timeout.ts @@ -3,9 +3,7 @@ import type { ClawdbotConfig } from "../config/config.js"; const DEFAULT_AGENT_TIMEOUT_SECONDS = 600; const normalizeNumber = (value: unknown): number | undefined => - typeof value === "number" && Number.isFinite(value) - ? Math.floor(value) - : undefined; + typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : undefined; export function resolveAgentTimeoutSeconds(cfg?: ClawdbotConfig): number { const raw = normalizeNumber(cfg?.agents?.defaults?.timeoutSeconds); diff --git a/src/agents/tool-call-id.test.ts b/src/agents/tool-call-id.test.ts index 9215652e8db..6ffe2b85e53 100644 --- a/src/agents/tool-call-id.test.ts +++ b/src/agents/tool-call-id.test.ts @@ -11,9 +11,7 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => { const input = [ { role: "assistant", - content: [ - { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - ], + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], }, { role: "toolResult", diff --git a/src/agents/tool-call-id.ts b/src/agents/tool-call-id.ts index e20863f6895..a3bfab13163 100644 --- a/src/agents/tool-call-id.ts +++ b/src/agents/tool-call-id.ts @@ -11,9 +11,7 @@ export function sanitizeToolCallId(id: string): string { "", ); - return trimmedInvalidStartChars.length > 0 - ? trimmedInvalidStartChars - : "sanitized_tool_id"; + return trimmedInvalidStartChars.length > 0 ? trimmedInvalidStartChars : "sanitized_tool_id"; } export function isValidCloudCodeAssistToolId(id: string): boolean { @@ -31,8 +29,7 @@ function makeUniqueToolId(params: { id: string; used: Set }): string { const hash = shortHash(params.id); const maxBaseLen = 64 - 1 - hash.length; - const clippedBase = - base.length > maxBaseLen ? base.slice(0, maxBaseLen) : base; + const clippedBase = base.length > maxBaseLen ? base.slice(0, maxBaseLen) : base; const candidate = `${clippedBase}_${hash}`; if (!params.used.has(candidate)) return candidate; @@ -83,8 +80,7 @@ function rewriteToolResultIds(params: { ? params.message.toolCallId : undefined; const toolUseId = (params.message as { toolUseId?: unknown }).toolUseId; - const toolUseIdStr = - typeof toolUseId === "string" && toolUseId ? toolUseId : undefined; + const toolUseIdStr = typeof toolUseId === "string" && toolUseId ? toolUseId : undefined; const nextToolCallId = toolCallId ? params.resolve(toolCallId) : undefined; const nextToolUseId = toolUseIdStr ? params.resolve(toolUseIdStr) : undefined; @@ -100,9 +96,7 @@ function rewriteToolResultIds(params: { } as Extract; } -export function sanitizeToolCallIdsForCloudCodeAssist( - messages: AgentMessage[], -): AgentMessage[] { +export function sanitizeToolCallIdsForCloudCodeAssist(messages: AgentMessage[]): AgentMessage[] { // Cloud Code Assist requires tool IDs matching ^[a-zA-Z0-9_-]+$. // Sanitization can introduce collisions (e.g. `a|b` and `a:b` -> `a_b`). // Fix by applying a stable, transcript-wide mapping and de-duping via suffix. diff --git a/src/agents/tool-display.json b/src/agents/tool-display.json index b3609d5c737..e593a97d4e3 100644 --- a/src/agents/tool-display.json +++ b/src/agents/tool-display.json @@ -95,7 +95,13 @@ }, "act": { "label": "act", - "detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"] + "detailKeys": [ + "request.kind", + "request.ref", + "request.selector", + "request.text", + "request.value" + ] } } }, @@ -122,9 +128,15 @@ "approve": { "label": "approve", "detailKeys": ["requestId"] }, "reject": { "label": "reject", "detailKeys": ["requestId"] }, "notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] }, - "camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] }, + "camera_snap": { + "label": "camera snap", + "detailKeys": ["node", "nodeId", "facing", "deviceId"] + }, "camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] }, - "camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] }, + "camera_clip": { + "label": "camera clip", + "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] + }, "screen_record": { "label": "screen record", "detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"] @@ -159,10 +171,19 @@ "emoji": "✉️", "title": "Message", "actions": { - "send": { "label": "send", "detailKeys": ["provider", "to", "media", "replyTo", "threadId"] }, + "send": { + "label": "send", + "detailKeys": ["provider", "to", "media", "replyTo", "threadId"] + }, "poll": { "label": "poll", "detailKeys": ["provider", "to", "pollQuestion"] }, - "react": { "label": "react", "detailKeys": ["provider", "to", "messageId", "emoji", "remove"] }, - "reactions": { "label": "reactions", "detailKeys": ["provider", "to", "messageId", "limit"] }, + "react": { + "label": "react", + "detailKeys": ["provider", "to", "messageId", "emoji", "remove"] + }, + "reactions": { + "label": "reactions", + "detailKeys": ["provider", "to", "messageId", "limit"] + }, "read": { "label": "read", "detailKeys": ["provider", "to", "limit"] }, "edit": { "label": "edit", "detailKeys": ["provider", "to", "messageId"] }, "delete": { "label": "delete", "detailKeys": ["provider", "to", "messageId"] }, @@ -170,23 +191,47 @@ "unpin": { "label": "unpin", "detailKeys": ["provider", "to", "messageId"] }, "list-pins": { "label": "list pins", "detailKeys": ["provider", "to"] }, "permissions": { "label": "permissions", "detailKeys": ["provider", "channelId", "to"] }, - "thread-create": { "label": "thread create", "detailKeys": ["provider", "channelId", "threadName"] }, - "thread-list": { "label": "thread list", "detailKeys": ["provider", "guildId", "channelId"] }, - "thread-reply": { "label": "thread reply", "detailKeys": ["provider", "channelId", "messageId"] }, + "thread-create": { + "label": "thread create", + "detailKeys": ["provider", "channelId", "threadName"] + }, + "thread-list": { + "label": "thread list", + "detailKeys": ["provider", "guildId", "channelId"] + }, + "thread-reply": { + "label": "thread reply", + "detailKeys": ["provider", "channelId", "messageId"] + }, "search": { "label": "search", "detailKeys": ["provider", "guildId", "query"] }, "sticker": { "label": "sticker", "detailKeys": ["provider", "to", "stickerId"] }, "member-info": { "label": "member", "detailKeys": ["provider", "guildId", "userId"] }, "role-info": { "label": "roles", "detailKeys": ["provider", "guildId"] }, "emoji-list": { "label": "emoji list", "detailKeys": ["provider", "guildId"] }, - "emoji-upload": { "label": "emoji upload", "detailKeys": ["provider", "guildId", "emojiName"] }, - "sticker-upload": { "label": "sticker upload", "detailKeys": ["provider", "guildId", "stickerName"] }, - "role-add": { "label": "role add", "detailKeys": ["provider", "guildId", "userId", "roleId"] }, - "role-remove": { "label": "role remove", "detailKeys": ["provider", "guildId", "userId", "roleId"] }, + "emoji-upload": { + "label": "emoji upload", + "detailKeys": ["provider", "guildId", "emojiName"] + }, + "sticker-upload": { + "label": "sticker upload", + "detailKeys": ["provider", "guildId", "stickerName"] + }, + "role-add": { + "label": "role add", + "detailKeys": ["provider", "guildId", "userId", "roleId"] + }, + "role-remove": { + "label": "role remove", + "detailKeys": ["provider", "guildId", "userId", "roleId"] + }, "channel-info": { "label": "channel", "detailKeys": ["provider", "channelId"] }, "channel-list": { "label": "channels", "detailKeys": ["provider", "guildId"] }, "voice-status": { "label": "voice", "detailKeys": ["provider", "guildId", "userId"] }, "event-list": { "label": "events", "detailKeys": ["provider", "guildId"] }, - "event-create": { "label": "event create", "detailKeys": ["provider", "guildId", "eventName"] }, + "event-create": { + "label": "event create", + "detailKeys": ["provider", "guildId", "eventName"] + }, "timeout": { "label": "timeout", "detailKeys": ["provider", "guildId", "userId"] }, "kick": { "label": "kick", "detailKeys": ["provider", "guildId", "userId"] }, "ban": { "label": "ban", "detailKeys": ["provider", "guildId", "userId"] } diff --git a/src/agents/tool-display.ts b/src/agents/tool-display.ts index 9e9a5a9e076..1b125d376a0 100644 --- a/src/agents/tool-display.ts +++ b/src/agents/tool-display.ts @@ -92,10 +92,7 @@ function lookupValueByPath(args: unknown, path: string): unknown { return current; } -function resolveDetailFromKeys( - args: unknown, - keys: string[], -): string | undefined { +function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined { for (const key of keys) { const value = lookupValueByPath(args, key); const display = coerceDisplayValue(value); @@ -157,8 +154,7 @@ export function resolveToolDisplay(params: { detail = resolveWriteDetail(params.args); } - const detailKeys = - actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; + const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? []; if (!detail && detailKeys.length > 0) { detail = resolveDetailFromKeys(params.args, detailKeys); } diff --git a/src/agents/tool-images.ts b/src/agents/tool-images.ts index 11425e0afb3..274b832fdb4 100644 --- a/src/agents/tool-images.ts +++ b/src/agents/tool-images.ts @@ -18,11 +18,7 @@ const MAX_IMAGE_BYTES = 5 * 1024 * 1024; function isImageBlock(block: unknown): block is ImageContentBlock { if (!block || typeof block !== "object") return false; const rec = block as Record; - return ( - rec.type === "image" && - typeof rec.data === "string" && - typeof rec.mimeType === "string" - ); + return rec.type === "image" && typeof rec.data === "string" && typeof rec.mimeType === "string"; } function isTextBlock(block: unknown): block is TextContentBlock { @@ -60,19 +56,12 @@ async function resizeImageBase64IfNeeded(params: { resized: false, }; } - } else if ( - !overBytes && - width <= params.maxDimensionPx && - height <= params.maxDimensionPx - ) { + } else if (!overBytes && width <= params.maxDimensionPx && height <= params.maxDimensionPx) { return { base64: params.base64, mimeType: params.mimeType, resized: false }; } const qualities = [85, 75, 65, 55, 45, 35]; - const sideStart = - maxDim > 0 - ? Math.min(params.maxDimensionPx, maxDim) - : params.maxDimensionPx; + const sideStart = maxDim > 0 ? Math.min(params.maxDimensionPx, maxDim) : params.maxDimensionPx; const sideGrid = [sideStart, 1800, 1600, 1400, 1200, 1000, 800] .map((v) => Math.min(params.maxDimensionPx, v)) .filter((v, i, arr) => v > 0 && arr.indexOf(v) === i) @@ -103,9 +92,7 @@ async function resizeImageBase64IfNeeded(params: { const best = smallest?.buffer ?? buf; const maxMb = (params.maxBytes / (1024 * 1024)).toFixed(0); const gotMb = (best.byteLength / (1024 * 1024)).toFixed(2); - throw new Error( - `Image could not be reduced below ${maxMb}MB (got ${gotMb}MB)`, - ); + throw new Error(`Image could not be reduced below ${maxMb}MB (got ${gotMb}MB)`); } export async function sanitizeContentBlocksImages( @@ -113,10 +100,7 @@ export async function sanitizeContentBlocksImages( label: string, opts: { maxDimensionPx?: number; maxBytes?: number } = {}, ): Promise { - const maxDimensionPx = Math.max( - opts.maxDimensionPx ?? MAX_IMAGE_DIMENSION_PX, - 1, - ); + const maxDimensionPx = Math.max(opts.maxDimensionPx ?? MAX_IMAGE_DIMENSION_PX, 1); const maxBytes = Math.max(opts.maxBytes ?? MAX_IMAGE_BYTES, 1); const out: ToolContentBlock[] = []; diff --git a/src/agents/tool-policy.test.ts b/src/agents/tool-policy.test.ts index 385075afb42..a0a234c9fa8 100644 --- a/src/agents/tool-policy.test.ts +++ b/src/agents/tool-policy.test.ts @@ -1,18 +1,9 @@ import { describe, expect, it } from "vitest"; -import { - expandToolGroups, - resolveToolProfilePolicy, - TOOL_GROUPS, -} from "./tool-policy.js"; +import { expandToolGroups, resolveToolProfilePolicy, TOOL_GROUPS } from "./tool-policy.js"; describe("tool-policy", () => { it("expands groups and normalizes aliases", () => { - const expanded = expandToolGroups([ - "group:runtime", - "BASH", - "apply-patch", - "group:fs", - ]); + const expanded = expandToolGroups(["group:runtime", "BASH", "apply-patch", "group:fs"]); const set = new Set(expanded); expect(set.has("exec")).toBe(true); expect(set.has("bash")).toBe(true); diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index 55dfd761202..e771c71049b 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -58,13 +58,7 @@ const TOOL_PROFILES: Record = { allow: ["session_status"], }, coding: { - allow: [ - "group:fs", - "group:runtime", - "group:sessions", - "group:memory", - "image", - ], + allow: ["group:fs", "group:runtime", "group:sessions", "group:memory", "image"], }, messaging: { allow: [ @@ -102,9 +96,7 @@ export function expandToolGroups(list?: string[]) { return Array.from(new Set(expanded)); } -export function resolveToolProfilePolicy( - profile?: string, -): ToolProfilePolicy | undefined { +export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined { if (!profile) return undefined; const resolved = TOOL_PROFILES[profile as ToolProfileId]; if (!resolved) return undefined; diff --git a/src/agents/tool-summaries.ts b/src/agents/tool-summaries.ts index 325966b414f..09671efbf66 100644 --- a/src/agents/tool-summaries.ts +++ b/src/agents/tool-summaries.ts @@ -1,8 +1,6 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; -export function buildToolSummaryMap( - tools: AgentTool[], -): Record { +export function buildToolSummaryMap(tools: AgentTool[]): Record { const summaries: Record = {}; for (const tool of tools) { const summary = tool.description?.trim() || tool.label?.trim(); diff --git a/src/agents/tools/agent-step.ts b/src/agents/tools/agent-step.ts index 2ad76ada61c..e55c16a781f 100644 --- a/src/agents/tools/agent-step.ts +++ b/src/agents/tools/agent-step.ts @@ -13,9 +13,7 @@ export async function readLatestAssistantReply(params: { method: "chat.history", params: { sessionKey: params.sessionKey, limit: params.limit ?? 50 }, })) as { messages?: unknown[] }; - const filtered = stripToolMessages( - Array.isArray(history?.messages) ? history.messages : [], - ); + const filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []); const last = filtered.length > 0 ? filtered[filtered.length - 1] : undefined; return last ? extractAssistantText(last) : undefined; } @@ -43,8 +41,7 @@ export async function runAgentStep(params: { timeoutMs: 10_000, })) as { runId?: string; acceptedAt?: number }; - const stepRunId = - typeof response?.runId === "string" && response.runId ? response.runId : ""; + const stepRunId = typeof response?.runId === "string" && response.runId ? response.runId : ""; const resolvedRunId = stepRunId || stepIdem; const stepWaitMs = Math.min(params.timeoutMs, 60_000); const wait = (await callGateway({ diff --git a/src/agents/tools/agents-list-tool.ts b/src/agents/tools/agents-list-tool.ts index b94208af9a8..40c7172cbba 100644 --- a/src/agents/tools/agents-list-tool.ts +++ b/src/agents/tools/agents-list-tool.ts @@ -9,10 +9,7 @@ import { import { resolveAgentConfig } from "../agent-scope.js"; import type { AnyAgentTool } from "./common.js"; import { jsonResult } from "./common.js"; -import { - resolveInternalSessionKey, - resolveMainSessionAlias, -} from "./sessions-helpers.js"; +import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js"; const AgentsListToolSchema = Type.Object({}); @@ -22,14 +19,11 @@ type AgentListEntry = { configured: boolean; }; -export function createAgentsListTool(opts?: { - agentSessionKey?: string; -}): AnyAgentTool { +export function createAgentsListTool(opts?: { agentSessionKey?: string }): AnyAgentTool { return { label: "Agents", name: "agents_list", - description: - "List agent ids you can target with sessions_spawn (based on allowlists).", + description: "List agent ids you can target with sessions_spawn (based on allowlists).", parameters: AgentsListToolSchema, execute: async () => { const cfg = loadConfig(); @@ -46,8 +40,7 @@ export function createAgentsListTool(opts?: { parseAgentSessionKey(requesterInternalKey)?.agentId ?? DEFAULT_AGENT_ID, ); - const allowAgents = - resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? []; + const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? []; const allowAny = allowAgents.some((value) => value.trim() === "*"); const allowSet = new Set( allowAgents @@ -55,12 +48,8 @@ export function createAgentsListTool(opts?: { .map((value) => normalizeAgentId(value)), ); - const configuredAgents = Array.isArray(cfg.agents?.list) - ? cfg.agents?.list - : []; - const configuredIds = configuredAgents.map((entry) => - normalizeAgentId(entry.id), - ); + const configuredAgents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : []; + const configuredIds = configuredAgents.map((entry) => normalizeAgentId(entry.id)); const configuredNameMap = new Map(); for (const entry of configuredAgents) { const name = entry?.name?.trim() ?? ""; @@ -77,9 +66,7 @@ export function createAgentsListTool(opts?: { } const all = Array.from(allowed); - const rest = all - .filter((id) => id !== requesterAgentId) - .sort((a, b) => a.localeCompare(b)); + const rest = all.filter((id) => id !== requesterAgentId).sort((a, b) => a.localeCompare(b)); const ordered = [requesterAgentId, ...rest]; const agents: AgentListEntry[] = ordered.map((id) => ({ id, diff --git a/src/agents/tools/browser-tool.schema.ts b/src/agents/tools/browser-tool.schema.ts index 3d3bbf728a3..1fd869810fd 100644 --- a/src/agents/tools/browser-tool.schema.ts +++ b/src/agents/tools/browser-tool.schema.ts @@ -64,9 +64,7 @@ const BrowserActSchema = Type.Object({ // select values: Type.Optional(Type.Array(Type.String())), // fill - use permissive array of objects - fields: Type.Optional( - Type.Array(Type.Object({}, { additionalProperties: true })), - ), + fields: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }))), // resize width: Type.Optional(Type.Number()), height: Type.Optional(Type.Number()), diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index 656e73db572..15444d196ae 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -21,12 +21,7 @@ import { resolveBrowserConfig } from "../../browser/config.js"; import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js"; import { loadConfig } from "../../config/config.js"; import { BrowserToolSchema } from "./browser-tool.schema.js"; -import { - type AnyAgentTool, - imageResultFromFile, - jsonResult, - readStringParam, -} from "./common.js"; +import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js"; function resolveBrowserBaseUrl(params: { target?: "sandbox" | "host" | "custom"; @@ -42,22 +37,13 @@ function resolveBrowserBaseUrl(params: { const normalizedControlUrl = params.controlUrl?.trim() ?? ""; const normalizedDefault = params.defaultControlUrl?.trim() ?? ""; const target = - params.target ?? - (normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host"); + params.target ?? (normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host"); const assertAllowedControlUrl = (url: string) => { - const allowedUrls = params.allowedControlUrls?.map((entry) => - entry.trim().replace(/\/$/, ""), - ); - const allowedHosts = params.allowedControlHosts?.map((entry) => - entry.trim().toLowerCase(), - ); + const allowedUrls = params.allowedControlUrls?.map((entry) => entry.trim().replace(/\/$/, "")); + const allowedHosts = params.allowedControlHosts?.map((entry) => entry.trim().toLowerCase()); const allowedPorts = params.allowedControlPorts; - if ( - !allowedUrls?.length && - !allowedHosts?.length && - !allowedPorts?.length - ) { + if (!allowedUrls?.length && !allowedHosts?.length && !allowedPorts?.length) { return; } let parsed: URL; @@ -71,21 +57,13 @@ function resolveBrowserBaseUrl(params: { throw new Error("Browser controlUrl is not in the allowed URL list."); } if (allowedHosts?.length && !allowedHosts.includes(parsed.hostname)) { - throw new Error( - "Browser controlUrl hostname is not in the allowed host list.", - ); + throw new Error("Browser controlUrl hostname is not in the allowed host list."); } if (allowedPorts?.length) { const port = - parsed.port?.trim() !== "" - ? Number(parsed.port) - : parsed.protocol === "https:" - ? 443 - : 80; + parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80; if (!Number.isFinite(port) || !allowedPorts.includes(port)) { - throw new Error( - "Browser controlUrl port is not in the allowed port list.", - ); + throw new Error("Browser controlUrl port is not in the allowed port list."); } } }; @@ -134,9 +112,7 @@ export function createBrowserTool(opts?: { }): AnyAgentTool { const targetDefault = opts?.defaultControlUrl ? "sandbox" : "host"; const hostHint = - opts?.allowHostControl === false - ? "Host target blocked by policy." - : "Host target allowed."; + opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed."; const allowlistHint = opts?.allowedControlUrls?.length || opts?.allowedControlHosts?.length || @@ -159,11 +135,7 @@ export function createBrowserTool(opts?: { const params = args as Record; const action = readStringParam(params, "action", { required: true }); const controlUrl = readStringParam(params, "controlUrl"); - const target = readStringParam(params, "target") as - | "sandbox" - | "host" - | "custom" - | undefined; + const target = readStringParam(params, "target") as "sandbox" | "host" | "custom" | undefined; const profile = readStringParam(params, "profile"); const baseUrl = resolveBrowserBaseUrl({ target, @@ -190,9 +162,7 @@ export function createBrowserTool(opts?: { const targetUrl = readStringParam(params, "targetUrl", { required: true, }); - return jsonResult( - await browserOpenTab(baseUrl, targetUrl, { profile }), - ); + return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile })); } case "focus": { const targetId = readStringParam(params, "targetId", { @@ -213,10 +183,7 @@ export function createBrowserTool(opts?: { ? (params.format as "ai" | "aria") : "ai"; const hasMaxChars = Object.hasOwn(params, "maxChars"); - const targetId = - typeof params.targetId === "string" - ? params.targetId.trim() - : undefined; + const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined; const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit @@ -228,34 +195,21 @@ export function createBrowserTool(opts?: { ? Math.floor(params.maxChars) : undefined; const resolvedMaxChars = - format === "ai" - ? hasMaxChars - ? maxChars - : DEFAULT_AI_SNAPSHOT_MAX_CHARS - : undefined; + format === "ai" ? (hasMaxChars ? maxChars : DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined; const interactive = - typeof params.interactive === "boolean" - ? params.interactive - : undefined; - const compact = - typeof params.compact === "boolean" ? params.compact : undefined; + typeof params.interactive === "boolean" ? params.interactive : undefined; + const compact = typeof params.compact === "boolean" ? params.compact : undefined; const depth = typeof params.depth === "number" && Number.isFinite(params.depth) ? params.depth : undefined; - const selector = - typeof params.selector === "string" - ? params.selector.trim() - : undefined; - const frame = - typeof params.frame === "string" ? params.frame.trim() : undefined; + const selector = typeof params.selector === "string" ? params.selector.trim() : undefined; + const frame = typeof params.frame === "string" ? params.frame.trim() : undefined; const snapshot = await browserSnapshot(baseUrl, { format, targetId, limit, - ...(typeof resolvedMaxChars === "number" - ? { maxChars: resolvedMaxChars } - : {}), + ...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}), interactive, compact, depth, @@ -305,21 +259,12 @@ export function createBrowserTool(opts?: { ); } case "console": { - const level = - typeof params.level === "string" ? params.level.trim() : undefined; - const targetId = - typeof params.targetId === "string" - ? params.targetId.trim() - : undefined; - return jsonResult( - await browserConsoleMessages(baseUrl, { level, targetId, profile }), - ); + const level = typeof params.level === "string" ? params.level.trim() : undefined; + const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined; + return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile })); } case "pdf": { - const targetId = - typeof params.targetId === "string" - ? params.targetId.trim() - : undefined; + const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined; const result = await browserPdfSave(baseUrl, { targetId, profile }); return { content: [{ type: "text", text: `FILE:${result.path}` }], @@ -327,20 +272,14 @@ export function createBrowserTool(opts?: { }; } case "upload": { - const paths = Array.isArray(params.paths) - ? params.paths.map((p) => String(p)) - : []; + const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : []; if (paths.length === 0) throw new Error("paths required"); const ref = readStringParam(params, "ref"); const inputRef = readStringParam(params, "inputRef"); const element = readStringParam(params, "element"); - const targetId = - typeof params.targetId === "string" - ? params.targetId.trim() - : undefined; + const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined; const timeoutMs = - typeof params.timeoutMs === "number" && - Number.isFinite(params.timeoutMs) + typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : undefined; return jsonResult( @@ -357,17 +296,10 @@ export function createBrowserTool(opts?: { } case "dialog": { const accept = Boolean(params.accept); - const promptText = - typeof params.promptText === "string" - ? params.promptText - : undefined; - const targetId = - typeof params.targetId === "string" - ? params.targetId.trim() - : undefined; + const promptText = typeof params.promptText === "string" ? params.promptText : undefined; + const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined; const timeoutMs = - typeof params.timeoutMs === "number" && - Number.isFinite(params.timeoutMs) + typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? params.timeoutMs : undefined; return jsonResult( @@ -385,11 +317,9 @@ export function createBrowserTool(opts?: { if (!request || typeof request !== "object") { throw new Error("request required"); } - const result = await browserAct( - baseUrl, - request as Parameters[1], - { profile }, - ); + const result = await browserAct(baseUrl, request as Parameters[1], { + profile, + }); return jsonResult(result); } default: diff --git a/src/agents/tools/canvas-tool.ts b/src/agents/tools/canvas-tool.ts index 66c4f869d43..247d69fb075 100644 --- a/src/agents/tools/canvas-tool.ts +++ b/src/agents/tools/canvas-tool.ts @@ -3,18 +3,10 @@ import fs from "node:fs/promises"; import { Type } from "@sinclair/typebox"; import { writeBase64ToFile } from "../../cli/nodes-camera.js"; -import { - canvasSnapshotTempPath, - parseCanvasSnapshotPayload, -} from "../../cli/nodes-canvas.js"; +import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../../cli/nodes-canvas.js"; import { imageMimeFromFormat } from "../../media/mime.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; -import { - type AnyAgentTool, - imageResult, - jsonResult, - readStringParam, -} from "./common.js"; +import { type AnyAgentTool, imageResult, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; import { resolveNodeId } from "./nodes-utils.js"; @@ -70,8 +62,7 @@ export function createCanvasTool(): AnyAgentTool { const gatewayOpts: GatewayCallOptions = { gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), - timeoutMs: - typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, + timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, }; const nodeId = await resolveNodeId( @@ -80,10 +71,7 @@ export function createCanvasTool(): AnyAgentTool { true, ); - const invoke = async ( - command: string, - invokeParams?: Record, - ) => + const invoke = async (command: string, invokeParams?: Record) => await callGatewayTool("node.invoke", gatewayOpts, { nodeId, command, @@ -97,8 +85,7 @@ export function createCanvasTool(): AnyAgentTool { x: typeof params.x === "number" ? params.x : undefined, y: typeof params.y === "number" ? params.y : undefined, width: typeof params.width === "number" ? params.width : undefined, - height: - typeof params.height === "number" ? params.height : undefined, + height: typeof params.height === "number" ? params.height : undefined, }; const invokeParams: Record = {}; if (typeof params.target === "string" && params.target.trim()) { @@ -140,20 +127,14 @@ export function createCanvasTool(): AnyAgentTool { return jsonResult({ ok: true }); } case "snapshot": { - const formatRaw = - typeof params.format === "string" - ? params.format.toLowerCase() - : "png"; - const format = - formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png"; + const formatRaw = typeof params.format === "string" ? params.format.toLowerCase() : "png"; + const format = formatRaw === "jpg" || formatRaw === "jpeg" ? "jpeg" : "png"; const maxWidth = - typeof params.maxWidth === "number" && - Number.isFinite(params.maxWidth) + typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) ? params.maxWidth : undefined; const quality = - typeof params.quality === "number" && - Number.isFinite(params.quality) + typeof params.quality === "number" && Number.isFinite(params.quality) ? params.quality : undefined; const raw = (await invoke("canvas.snapshot", { diff --git a/src/agents/tools/common.test.ts b/src/agents/tools/common.test.ts index 86d2a5ea87b..c8a572b8877 100644 --- a/src/agents/tools/common.test.ts +++ b/src/agents/tools/common.test.ts @@ -38,9 +38,9 @@ describe("readStringOrNumberParam", () => { }); it("throws when required and missing", () => { - expect(() => - readStringOrNumberParam({}, "chatId", { required: true }), - ).toThrow(/chatId required/); + expect(() => readStringOrNumberParam({}, "chatId", { required: true })).toThrow( + /chatId required/, + ); }); }); diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index c5dfade135f..7e5446d064d 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -45,12 +45,7 @@ export function readStringParam( key: string, options: StringParamOptions = {}, ) { - const { - required = false, - trim = true, - label = key, - allowEmpty = false, - } = options; + const { required = false, trim = true, label = key, allowEmpty = false } = options; const raw = params[key]; if (typeof raw !== "string") { if (required) throw new Error(`${label} required`); @@ -162,8 +157,7 @@ export function readReactionParams( ): ReactionParams { const emojiKey = options.emojiKey ?? "emoji"; const removeKey = options.removeKey ?? "remove"; - const remove = - typeof params[removeKey] === "boolean" ? params[removeKey] : false; + const remove = typeof params[removeKey] === "boolean" ? params[removeKey] : false; const emoji = readStringParam(params, emojiKey, { required: true, allowEmpty: true, @@ -219,8 +213,7 @@ export async function imageResultFromFile(params: { details?: Record; }): Promise> { const buf = await fs.readFile(params.path); - const mimeType = - (await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png"; + const mimeType = (await detectMime({ buffer: buf.slice(0, 256) })) ?? "image/png"; return await imageResult({ label: params.label, path: params.path, diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 7d331a78cae..cee9aed2711 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -1,8 +1,5 @@ import { Type } from "@sinclair/typebox"; -import { - normalizeCronJobCreate, - normalizeCronJobPatch, -} from "../../cron/normalize.js"; +import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; @@ -12,16 +9,7 @@ import { callGatewayTool, type GatewayCallOptions } from "./gateway.js"; // contain nested unions. Tool schemas need to stay provider-friendly, so we // accept "any object" here and validate at runtime. -const CRON_ACTIONS = [ - "status", - "list", - "add", - "update", - "remove", - "run", - "runs", - "wake", -] as const; +const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs", "wake"] as const; const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const; @@ -53,15 +41,12 @@ export function createCronTool(): AnyAgentTool { const gatewayOpts: GatewayCallOptions = { gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), - timeoutMs: - typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, + timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, }; switch (action) { case "status": - return jsonResult( - await callGatewayTool("cron.status", gatewayOpts, {}), - ); + return jsonResult(await callGatewayTool("cron.status", gatewayOpts, {})); case "list": return jsonResult( await callGatewayTool("cron.list", gatewayOpts, { @@ -73,17 +58,12 @@ export function createCronTool(): AnyAgentTool { throw new Error("job required"); } const job = normalizeCronJobCreate(params.job) ?? params.job; - return jsonResult( - await callGatewayTool("cron.add", gatewayOpts, job), - ); + return jsonResult(await callGatewayTool("cron.add", gatewayOpts, job)); } case "update": { - const id = - readStringParam(params, "jobId") ?? readStringParam(params, "id"); + const id = readStringParam(params, "jobId") ?? readStringParam(params, "id"); if (!id) { - throw new Error( - "jobId required (id accepted for backward compatibility)", - ); + throw new Error("jobId required (id accepted for backward compatibility)"); } if (!params.patch || typeof params.patch !== "object") { throw new Error("patch required"); @@ -97,40 +77,25 @@ export function createCronTool(): AnyAgentTool { ); } case "remove": { - const id = - readStringParam(params, "jobId") ?? readStringParam(params, "id"); + const id = readStringParam(params, "jobId") ?? readStringParam(params, "id"); if (!id) { - throw new Error( - "jobId required (id accepted for backward compatibility)", - ); + throw new Error("jobId required (id accepted for backward compatibility)"); } - return jsonResult( - await callGatewayTool("cron.remove", gatewayOpts, { id }), - ); + return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, { id })); } case "run": { - const id = - readStringParam(params, "jobId") ?? readStringParam(params, "id"); + const id = readStringParam(params, "jobId") ?? readStringParam(params, "id"); if (!id) { - throw new Error( - "jobId required (id accepted for backward compatibility)", - ); + throw new Error("jobId required (id accepted for backward compatibility)"); } - return jsonResult( - await callGatewayTool("cron.run", gatewayOpts, { id }), - ); + return jsonResult(await callGatewayTool("cron.run", gatewayOpts, { id })); } case "runs": { - const id = - readStringParam(params, "jobId") ?? readStringParam(params, "id"); + const id = readStringParam(params, "jobId") ?? readStringParam(params, "id"); if (!id) { - throw new Error( - "jobId required (id accepted for backward compatibility)", - ); + throw new Error("jobId required (id accepted for backward compatibility)"); } - return jsonResult( - await callGatewayTool("cron.runs", gatewayOpts, { id }), - ); + return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, { id })); } case "wake": { const text = readStringParam(params, "text", { required: true }); @@ -139,12 +104,7 @@ export function createCronTool(): AnyAgentTool { ? params.mode : "next-heartbeat"; return jsonResult( - await callGatewayTool( - "wake", - gatewayOpts, - { mode, text }, - { expectFinal: false }, - ), + await callGatewayTool("wake", gatewayOpts, { mode, text }, { expectFinal: false }), ); } default: diff --git a/src/agents/tools/discord-actions-guild.ts b/src/agents/tools/discord-actions-guild.ts index 8be6680cd26..87778143af6 100644 --- a/src/agents/tools/discord-actions-guild.ts +++ b/src/agents/tools/discord-actions-guild.ts @@ -28,9 +28,7 @@ import { readStringParam, } from "./common.js"; -function readParentIdParam( - params: Record, -): string | null | undefined { +function readParentIdParam(params: Record): string | null | undefined { if (params.clearParent === true) return null; if (params.parentId === null) return null; return readStringParam(params, "parentId"); @@ -206,8 +204,7 @@ export async function handleDiscordGuildAction( const channelId = readStringParam(params, "channelId"); const location = readStringParam(params, "location"); const entityTypeRaw = readStringParam(params, "entityType"); - const entityType = - entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2; + const entityType = entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2; const payload = { name, description, @@ -215,8 +212,7 @@ export async function handleDiscordGuildAction( scheduled_end_time: endTime, entity_type: entityType, channel_id: channelId, - entity_metadata: - entityType === 3 && location ? { location } : undefined, + entity_metadata: entityType === 3 && location ? { location } : undefined, privacy_level: 2, }; const event = await createScheduledEventDiscord(guildId, payload); diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index 7fcae8c6828..80843bfbb68 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -113,9 +113,7 @@ export async function handleDiscordMessagingAction( }); const limitRaw = params.limit; const limit = - typeof limitRaw === "number" && Number.isFinite(limitRaw) - ? limitRaw - : undefined; + typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; const reactions = await fetchReactionsDiscord(channelId, messageId, { limit, }); @@ -149,14 +147,10 @@ export async function handleDiscordMessagingAction( }); const allowMultiselectRaw = params.allowMultiselect; const allowMultiselect = - typeof allowMultiselectRaw === "boolean" - ? allowMultiselectRaw - : undefined; + typeof allowMultiselectRaw === "boolean" ? allowMultiselectRaw : undefined; const durationRaw = params.durationHours; const durationHours = - typeof durationRaw === "number" && Number.isFinite(durationRaw) - ? durationRaw - : undefined; + typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined; const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1; await sendPollDiscord( to, @@ -215,8 +209,7 @@ export async function handleDiscordMessagingAction( }); const formattedMessages = messages.map((message) => ({ ...message, - timestamp: - formatDiscordTimestamp(message.timestamp) ?? message.timestamp, + timestamp: formatDiscordTimestamp(message.timestamp) ?? message.timestamp, })); return jsonResult({ ok: true, messages: formattedMessages }); } @@ -278,8 +271,7 @@ export async function handleDiscordMessagingAction( const messageId = readStringParam(params, "messageId"); const autoArchiveMinutesRaw = params.autoArchiveMinutes; const autoArchiveMinutes = - typeof autoArchiveMinutesRaw === "number" && - Number.isFinite(autoArchiveMinutesRaw) + typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw) ? autoArchiveMinutesRaw : undefined; const thread = await createThreadDiscord(channelId, { @@ -298,9 +290,7 @@ export async function handleDiscordMessagingAction( }); const channelId = readStringParam(params, "channelId"); const includeArchived = - typeof params.includeArchived === "boolean" - ? params.includeArchived - : undefined; + typeof params.includeArchived === "boolean" ? params.includeArchived : undefined; const before = readStringParam(params, "before"); const limit = typeof params.limit === "number" && Number.isFinite(params.limit) @@ -387,14 +377,8 @@ export async function handleDiscordMessagingAction( typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit : undefined; - const channelIdList = [ - ...(channelIds ?? []), - ...(channelId ? [channelId] : []), - ]; - const authorIdList = [ - ...(authorIds ?? []), - ...(authorId ? [authorId] : []), - ]; + const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])]; + const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])]; const results = await searchMessagesDiscord({ guildId, content, diff --git a/src/agents/tools/discord-actions-moderation.ts b/src/agents/tools/discord-actions-moderation.ts index 7b2803f2c76..260ce85ea44 100644 --- a/src/agents/tools/discord-actions-moderation.ts +++ b/src/agents/tools/discord-actions-moderation.ts @@ -1,10 +1,6 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { DiscordActionConfig } from "../../config/config.js"; -import { - banMemberDiscord, - kickMemberDiscord, - timeoutMemberDiscord, -} from "../../discord/send.js"; +import { banMemberDiscord, kickMemberDiscord, timeoutMemberDiscord } from "../../discord/send.js"; import { type ActionGate, jsonResult, readStringParam } from "./common.js"; export async function handleDiscordModerationAction( @@ -24,8 +20,7 @@ export async function handleDiscordModerationAction( required: true, }); const durationMinutes = - typeof params.durationMinutes === "number" && - Number.isFinite(params.durationMinutes) + typeof params.durationMinutes === "number" && Number.isFinite(params.durationMinutes) ? params.durationMinutes : undefined; const until = readStringParam(params, "until"); @@ -65,8 +60,7 @@ export async function handleDiscordModerationAction( }); const reason = readStringParam(params, "reason"); const deleteMessageDays = - typeof params.deleteMessageDays === "number" && - Number.isFinite(params.deleteMessageDays) + typeof params.deleteMessageDays === "number" && Number.isFinite(params.deleteMessageDays) ? params.deleteMessageDays : undefined; await banMemberDiscord({ diff --git a/src/agents/tools/discord-actions.test.ts b/src/agents/tools/discord-actions.test.ts index ecc8424c35c..a49c4e72430 100644 --- a/src/agents/tools/discord-actions.test.ts +++ b/src/agents/tools/discord-actions.test.ts @@ -42,8 +42,7 @@ vi.mock("../../discord/send.js", () => ({ deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args), editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args), editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args), - fetchChannelPermissionsDiscord: (...args: unknown[]) => - fetchChannelPermissionsDiscord(...args), + fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args), fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args), listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args), listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args), @@ -51,17 +50,14 @@ vi.mock("../../discord/send.js", () => ({ pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args), reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args), readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args), - removeChannelPermissionDiscord: (...args: unknown[]) => - removeChannelPermissionDiscord(...args), - removeOwnReactionsDiscord: (...args: unknown[]) => - removeOwnReactionsDiscord(...args), + removeChannelPermissionDiscord: (...args: unknown[]) => removeChannelPermissionDiscord(...args), + removeOwnReactionsDiscord: (...args: unknown[]) => removeOwnReactionsDiscord(...args), removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args), searchMessagesDiscord: (...args: unknown[]) => searchMessagesDiscord(...args), sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args), sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args), sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args), - setChannelPermissionDiscord: (...args: unknown[]) => - setChannelPermissionDiscord(...args), + setChannelPermissionDiscord: (...args: unknown[]) => setChannelPermissionDiscord(...args), unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args), })); @@ -169,11 +165,7 @@ describe("handleDiscordGuildAction - channel management", () => { it("respects channel gating for channelCreate", async () => { await expect( - handleDiscordGuildAction( - "channelCreate", - { guildId: "G1", name: "test" }, - channelsDisabled, - ), + handleDiscordGuildAction("channelCreate", { guildId: "G1", name: "test" }, channelsDisabled), ).rejects.toThrow(/Discord channel management is disabled/); }); @@ -239,11 +231,7 @@ describe("handleDiscordGuildAction - channel management", () => { }); it("deletes a channel", async () => { - await handleDiscordGuildAction( - "channelDelete", - { channelId: "C1" }, - channelsEnabled, - ); + await handleDiscordGuildAction("channelDelete", { channelId: "C1" }, channelsEnabled); expect(deleteChannelDiscord).toHaveBeenCalledWith("C1"); }); @@ -330,11 +318,7 @@ describe("handleDiscordGuildAction - channel management", () => { }); it("deletes a category", async () => { - await handleDiscordGuildAction( - "categoryDelete", - { categoryId: "CAT1" }, - channelsEnabled, - ); + await handleDiscordGuildAction("categoryDelete", { categoryId: "CAT1" }, channelsEnabled); expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1"); }); diff --git a/src/agents/tools/gateway-tool.ts b/src/agents/tools/gateway-tool.ts index 914f793189b..5055d2dc574 100644 --- a/src/agents/tools/gateway-tool.ts +++ b/src/agents/tools/gateway-tool.ts @@ -58,9 +58,7 @@ export function createGatewayTool(opts?: { const action = readStringParam(params, "action", { required: true }); if (action === "restart") { if (opts?.config?.commands?.restart !== true) { - throw new Error( - "Gateway restart is disabled. Set commands.restart=true to enable.", - ); + throw new Error("Gateway restart is disabled. Set commands.restart=true to enable."); } const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim() @@ -75,9 +73,7 @@ export function createGatewayTool(opts?: { ? params.reason.trim().slice(0, 200) : undefined; const note = - typeof params.note === "string" && params.note.trim() - ? params.note.trim() - : undefined; + typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined; const payload: RestartSentinelPayload = { kind: "restart", status: "ok", @@ -114,8 +110,7 @@ export function createGatewayTool(opts?: { ? params.gatewayToken.trim() : undefined; const timeoutMs = - typeof params.timeoutMs === "number" && - Number.isFinite(params.timeoutMs) + typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? Math.max(1, Math.floor(params.timeoutMs)) : undefined; const gatewayOpts = { gatewayUrl, gatewayToken, timeoutMs }; @@ -135,12 +130,9 @@ export function createGatewayTool(opts?: { ? params.sessionKey.trim() : opts?.agentSessionKey?.trim() || undefined; const note = - typeof params.note === "string" && params.note.trim() - ? params.note.trim() - : undefined; + typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined; const restartDelayMs = - typeof params.restartDelayMs === "number" && - Number.isFinite(params.restartDelayMs) + typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : undefined; const result = await callGatewayTool("config.apply", gatewayOpts, { @@ -157,12 +149,9 @@ export function createGatewayTool(opts?: { ? params.sessionKey.trim() : opts?.agentSessionKey?.trim() || undefined; const note = - typeof params.note === "string" && params.note.trim() - ? params.note.trim() - : undefined; + typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined; const restartDelayMs = - typeof params.restartDelayMs === "number" && - Number.isFinite(params.restartDelayMs) + typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : undefined; const result = await callGatewayTool("update.run", gatewayOpts, { diff --git a/src/agents/tools/gateway.ts b/src/agents/tools/gateway.ts index 84defc22e69..ca7a2b49058 100644 --- a/src/agents/tools/gateway.ts +++ b/src/agents/tools/gateway.ts @@ -1,8 +1,5 @@ import { callGateway } from "../../gateway/call.js"; -import { - GATEWAY_CLIENT_MODES, - GATEWAY_CLIENT_NAMES, -} from "../../utils/message-channel.js"; +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789"; diff --git a/src/agents/tools/image-tool.helpers.ts b/src/agents/tools/image-tool.helpers.ts index f53d9dae169..739cc5ccb19 100644 --- a/src/agents/tools/image-tool.helpers.ts +++ b/src/agents/tools/image-tool.helpers.ts @@ -40,15 +40,11 @@ export function coerceImageAssistantText(params: { ); } if (errorMessage) { - throw new Error( - `Image model failed (${params.provider}/${params.model}): ${errorMessage}`, - ); + throw new Error(`Image model failed (${params.provider}/${params.model}): ${errorMessage}`); } const text = extractAssistantText(params.message); if (text.trim()) return text.trim(); - throw new Error( - `Image model returned no text (${params.provider}/${params.model}).`, - ); + throw new Error(`Image model returned no text (${params.provider}/${params.model}).`); } export function coerceImageModelConfig(cfg?: ClawdbotConfig): ImageModelConfig { @@ -56,10 +52,8 @@ export function coerceImageModelConfig(cfg?: ClawdbotConfig): ImageModelConfig { | { primary?: string; fallbacks?: string[] } | string | undefined; - const primary = - typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary; - const fallbacks = - typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : []; + const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary; + const fallbacks = typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : []; return { ...(primary?.trim() ? { primary: primary.trim() } : {}), ...(fallbacks.length > 0 ? { fallbacks } : {}), @@ -70,9 +64,7 @@ export function resolveProviderVisionModelFromConfig(params: { cfg?: ClawdbotConfig; provider: string; }): string | null { - const providerCfg = params.cfg?.models?.providers?.[ - params.provider - ] as unknown as + const providerCfg = params.cfg?.models?.providers?.[params.provider] as unknown as | { models?: Array<{ id?: string; input?: string[] }> } | undefined; const models = providerCfg?.models ?? []; @@ -87,9 +79,7 @@ export function resolveProviderVisionModelFromConfig(params: { : null; const picked = preferMinimaxVl ?? - models.find( - (m) => Boolean((m?.id ?? "").trim()) && m.input?.includes("image"), - ); + models.find((m) => Boolean((m?.id ?? "").trim()) && m.input?.includes("image")); const id = (picked?.id ?? "").trim(); return id ? `${params.provider}/${id}` : null; } diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 3064798be12..5e22f7353ab 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -5,11 +5,7 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../../config/config.js"; -import { - __testing, - createImageTool, - resolveImageModelConfigForTool, -} from "./image-tool.js"; +import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js"; async function writeAuthProfiles(agentDir: string, profiles: unknown) { await fs.mkdir(agentDir, { recursive: true }); @@ -41,9 +37,7 @@ describe("image tool implicit imageModel config", () => { }); it("stays disabled without auth when no pairing is possible", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-")); const cfg: ClawdbotConfig = { agents: { defaults: { model: { primary: "openai/gpt-5.2" } } }, }; @@ -52,9 +46,7 @@ describe("image tool implicit imageModel config", () => { }); it("pairs minimax primary with MiniMax-VL-01 (and fallbacks) when auth exists", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-")); vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); vi.stubEnv("OPENAI_API_KEY", "openai-test"); vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test"); @@ -69,9 +61,7 @@ describe("image tool implicit imageModel config", () => { }); it("pairs a custom provider when it declares an image-capable model", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-")); await writeAuthProfiles(agentDir, { version: 1, profiles: { @@ -98,9 +88,7 @@ describe("image tool implicit imageModel config", () => { }); it("prefers explicit agents.defaults.imageModel", async () => { - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-")); const cfg: ClawdbotConfig = { agents: { defaults: { @@ -115,9 +103,7 @@ describe("image tool implicit imageModel config", () => { }); it("sandboxes image paths like the read tool", async () => { - const stateDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-sandbox-"), - ); + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-sandbox-")); const agentDir = path.join(stateDir, "agent"); const sandboxRoot = path.join(stateDir, "sandbox"); await fs.mkdir(agentDir, { recursive: true }); @@ -132,19 +118,17 @@ describe("image tool implicit imageModel config", () => { expect(tool).not.toBeNull(); if (!tool) throw new Error("expected image tool"); - await expect( - tool.execute("t1", { image: "https://example.com/a.png" }), - ).rejects.toThrow(/Sandboxed image tool does not allow remote URLs/i); + await expect(tool.execute("t1", { image: "https://example.com/a.png" })).rejects.toThrow( + /Sandboxed image tool does not allow remote URLs/i, + ); - await expect( - tool.execute("t2", { image: "../escape.png" }), - ).rejects.toThrow(/escapes sandbox root/i); + await expect(tool.execute("t2", { image: "../escape.png" })).rejects.toThrow( + /escapes sandbox root/i, + ); }); it("rewrites inbound absolute paths into sandbox media/inbound", async () => { - const stateDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-image-sandbox-"), - ); + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-image-sandbox-")); const agentDir = path.join(stateDir, "agent"); const sandboxRoot = path.join(stateDir, "sandbox"); await fs.mkdir(agentDir, { recursive: true }); @@ -190,9 +174,7 @@ describe("image tool implicit imageModel config", () => { }); expect(fetch).toHaveBeenCalledTimes(1); - expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain( - "photo.png", - ); + expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain("photo.png"); }); }); @@ -207,9 +189,9 @@ describe("image tool data URL support", () => { }); it("rejects non-image data URLs", () => { - expect(() => - __testing.decodeDataUrl("data:text/plain;base64,SGVsbG8="), - ).toThrow(/Unsupported data URL type/i); + expect(() => __testing.decodeDataUrl("data:text/plain;base64,SGVsbG8=")).toThrow( + /Unsupported data URL type/i, + ); }); }); @@ -245,9 +227,7 @@ describe("image tool MiniMax VLM routing", () => { // @ts-expect-error partial global global.fetch = fetch; - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-minimax-vlm-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-minimax-vlm-")); vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); const cfg: ClawdbotConfig = { agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } }, @@ -265,9 +245,9 @@ describe("image tool MiniMax VLM routing", () => { const [url, init] = fetch.mock.calls[0]; expect(String(url)).toBe("https://api.minimax.io/v1/coding_plan/vlm"); expect(init?.method).toBe("POST"); - expect( - String((init?.headers as Record)?.Authorization), - ).toBe("Bearer minimax-test"); + expect(String((init?.headers as Record)?.Authorization)).toBe( + "Bearer minimax-test", + ); expect(String(init?.body)).toContain('"prompt":"Describe the image."'); expect(String(init?.body)).toContain('"image_url":"data:image/png;base64,'); @@ -289,9 +269,7 @@ describe("image tool MiniMax VLM routing", () => { // @ts-expect-error partial global global.fetch = fetch; - const agentDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-minimax-vlm-"), - ); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-minimax-vlm-")); vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); const cfg: ClawdbotConfig = { agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } }, diff --git a/src/agents/tools/image-tool.ts b/src/agents/tools/image-tool.ts index 9e6a662a62f..a013e98e442 100644 --- a/src/agents/tools/image-tool.ts +++ b/src/agents/tools/image-tool.ts @@ -8,19 +8,13 @@ import { complete, type Model, } from "@mariozechner/pi-ai"; -import { - discoverAuthStorage, - discoverModels, -} from "@mariozechner/pi-coding-agent"; +import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ClawdbotConfig } from "../../config/config.js"; import { resolveUserPath } from "../../utils.js"; import { loadWebMedia } from "../../web/media.js"; -import { - ensureAuthProfileStore, - listProfilesForProvider, -} from "../auth-profiles.js"; +import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { minimaxUnderstandImage } from "../minimax-vlm.js"; import { getApiKeyForModel, resolveEnvApiKey } from "../model-auth.js"; @@ -48,24 +42,15 @@ function resolveDefaultModelRef(cfg?: ClawdbotConfig): { provider: string; model: string; } { - const modelConfig = cfg?.agents?.defaults?.model as - | { primary?: string } - | string - | undefined; - const raw = - typeof modelConfig === "string" - ? modelConfig.trim() - : modelConfig?.primary?.trim(); + const modelConfig = cfg?.agents?.defaults?.model as { primary?: string } | string | undefined; + const raw = typeof modelConfig === "string" ? modelConfig.trim() : modelConfig?.primary?.trim(); const parsed = parseModelRef(raw ?? "", DEFAULT_PROVIDER) ?? ({ provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL } as const); return { provider: parsed.provider, model: parsed.model }; } -function hasAuthForProvider(params: { - provider: string; - agentDir: string; -}): boolean { +function hasAuthForProvider(params: { provider: string; agentDir: string }): boolean { if (resolveEnvApiKey(params.provider)?.apiKey) return true; const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, @@ -156,33 +141,18 @@ export function resolveImageModelConfigForTool(params: { return null; } -function pickMaxBytes( - cfg?: ClawdbotConfig, - maxBytesMb?: number, -): number | undefined { - if ( - typeof maxBytesMb === "number" && - Number.isFinite(maxBytesMb) && - maxBytesMb > 0 - ) { +function pickMaxBytes(cfg?: ClawdbotConfig, maxBytesMb?: number): number | undefined { + if (typeof maxBytesMb === "number" && Number.isFinite(maxBytesMb) && maxBytesMb > 0) { return Math.floor(maxBytesMb * 1024 * 1024); } const configured = cfg?.agents?.defaults?.mediaMaxMb; - if ( - typeof configured === "number" && - Number.isFinite(configured) && - configured > 0 - ) { + if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) { return Math.floor(configured * 1024 * 1024); } return undefined; } -function buildImageContext( - prompt: string, - base64: string, - mimeType: string, -): Context { +function buildImageContext(prompt: string, base64: string, mimeType: string): Context { return { messages: [ { @@ -201,8 +171,7 @@ async function resolveSandboxedImagePath(params: { sandboxRoot: string; imagePath: string; }): Promise<{ resolved: string; rewrittenFrom?: string }> { - const normalize = (p: string) => - p.startsWith("file://") ? p.slice("file://".length) : p; + const normalize = (p: string) => (p.startsWith("file://") ? p.slice("file://".length) : p); const filePath = normalize(params.imagePath); try { const out = await assertSandboxPath({ @@ -269,9 +238,7 @@ async function runImagePrompt(params: { throw new Error(`Unknown model: ${provider}/${modelId}`); } if (!model.input?.includes("image")) { - throw new Error( - `Model does not support images: ${provider}/${modelId}`, - ); + throw new Error(`Model does not support images: ${provider}/${modelId}`); } const apiKeyInfo = await getApiKeyForModel({ model, @@ -291,11 +258,7 @@ async function runImagePrompt(params: { return { text, provider: model.provider, model: model.id }; } - const context = buildImageContext( - params.prompt, - params.base64, - params.mimeType, - ); + const context = buildImageContext(params.prompt, params.base64, params.mimeType); const message = (await complete(model, context, { apiKey: apiKeyInfo.apiKey, maxTokens: 512, @@ -351,12 +314,8 @@ export function createImageTool(options?: { maxBytesMb: Type.Optional(Type.Number()), }), execute: async (_toolCallId, args) => { - const record = - args && typeof args === "object" - ? (args as Record) - : {}; - const imageRawInput = - typeof record.image === "string" ? record.image.trim() : ""; + const record = args && typeof args === "object" ? (args as Record) : {}; + const imageRawInput = typeof record.image === "string" ? record.image.trim() : ""; const imageRaw = imageRawInput.startsWith("@") ? imageRawInput.slice(1).trim() : imageRawInput; @@ -372,13 +331,7 @@ export function createImageTool(options?: { const isFileUrl = /^file:/i.test(imageRaw); const isHttpUrl = /^https?:\/\//i.test(imageRaw); const isDataUrl = /^data:/i.test(imageRaw); - if ( - hasScheme && - !looksLikeWindowsDrivePath && - !isFileUrl && - !isHttpUrl && - !isDataUrl - ) { + if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) { return { content: [ { @@ -397,11 +350,8 @@ export function createImageTool(options?: { ? record.prompt.trim() : DEFAULT_PROMPT; const modelOverride = - typeof record.model === "string" && record.model.trim() - ? record.model.trim() - : undefined; - const maxBytesMb = - typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; + typeof record.model === "string" && record.model.trim() ? record.model.trim() : undefined; + const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; const maxBytes = pickMaxBytes(options?.config, maxBytesMb); const sandboxRoot = options?.sandboxRoot?.trim(); @@ -415,19 +365,18 @@ export function createImageTool(options?: { if (imageRaw.startsWith("~")) return resolveUserPath(imageRaw); return imageRaw; })(); - const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = - isDataUrl - ? { resolved: "" } - : sandboxRoot - ? await resolveSandboxedImagePath({ - sandboxRoot, - imagePath: resolvedImage, - }) - : { - resolved: resolvedImage.startsWith("file://") - ? resolvedImage.slice("file://".length) - : resolvedImage, - }; + const resolvedPathInfo: { resolved: string; rewrittenFrom?: string } = isDataUrl + ? { resolved: "" } + : sandboxRoot + ? await resolveSandboxedImagePath({ + sandboxRoot, + imagePath: resolvedImage, + }) + : { + resolved: resolvedImage.startsWith("file://") + ? resolvedImage.slice("file://".length) + : resolvedImage, + }; const resolvedPath = isDataUrl ? null : resolvedPathInfo.resolved; const media = isDataUrl diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 0a6c90dd732..4b784eb9e76 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -9,10 +9,7 @@ import { } from "../../channels/plugins/types.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; -import { - GATEWAY_CLIENT_IDS, - GATEWAY_CLIENT_MODES, -} from "../../gateway/protocol/client-info.js"; +import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js"; import { runMessageAction } from "../../infra/outbound/message-action-runner.js"; import { normalizeAccountId } from "../../routing/session-key.js"; import { stringEnum } from "../schema/typebox.js"; @@ -132,10 +129,9 @@ type MessageToolOptions = { function buildMessageToolSchema(cfg: ClawdbotConfig) { const actions = listChannelMessageActions(cfg); const includeButtons = supportsChannelMessageButtons(cfg); - return buildMessageToolSchemaFromActions( - actions.length > 0 ? actions : ["send"], - { includeButtons }, - ); + return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], { + includeButtons, + }); } function resolveAgentAccountId(value?: string): string | undefined { @@ -146,9 +142,7 @@ function resolveAgentAccountId(value?: string): string | undefined { export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { const agentAccountId = resolveAgentAccountId(options?.agentAccountId); - const schema = options?.config - ? buildMessageToolSchema(options.config) - : MessageToolSchema; + const schema = options?.config ? buildMessageToolSchema(options.config) : MessageToolSchema; return { label: "Message", diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index ee0ad68287f..07416567297 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -99,26 +99,19 @@ export function createNodesTool(): AnyAgentTool { const gatewayOpts: GatewayCallOptions = { gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), - timeoutMs: - typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, + timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined, }; switch (action) { case "status": - return jsonResult( - await callGatewayTool("node.list", gatewayOpts, {}), - ); + return jsonResult(await callGatewayTool("node.list", gatewayOpts, {})); case "describe": { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); - return jsonResult( - await callGatewayTool("node.describe", gatewayOpts, { nodeId }), - ); + return jsonResult(await callGatewayTool("node.describe", gatewayOpts, { nodeId })); } case "pending": - return jsonResult( - await callGatewayTool("node.pair.list", gatewayOpts, {}), - ); + return jsonResult(await callGatewayTool("node.pair.list", gatewayOpts, {})); case "approve": { const requestId = readStringParam(params, "requestId", { required: true, @@ -153,16 +146,9 @@ export function createNodesTool(): AnyAgentTool { params: { title: title.trim() || undefined, body: body.trim() || undefined, - sound: - typeof params.sound === "string" ? params.sound : undefined, - priority: - typeof params.priority === "string" - ? params.priority - : undefined, - delivery: - typeof params.delivery === "string" - ? params.delivery - : undefined, + sound: typeof params.sound === "string" ? params.sound : undefined, + priority: typeof params.priority === "string" ? params.priority : undefined, + delivery: typeof params.delivery === "string" ? params.delivery : undefined, }, idempotencyKey: crypto.randomUUID(), }); @@ -172,9 +158,7 @@ export function createNodesTool(): AnyAgentTool { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); const facingRaw = - typeof params.facing === "string" - ? params.facing.toLowerCase() - : "both"; + typeof params.facing === "string" ? params.facing.toLowerCase() : "both"; const facings: CameraFacing[] = facingRaw === "both" ? ["front", "back"] @@ -184,18 +168,15 @@ export function createNodesTool(): AnyAgentTool { throw new Error("invalid facing (front|back|both)"); })(); const maxWidth = - typeof params.maxWidth === "number" && - Number.isFinite(params.maxWidth) + typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) ? params.maxWidth : undefined; const quality = - typeof params.quality === "number" && - Number.isFinite(params.quality) + typeof params.quality === "number" && Number.isFinite(params.quality) ? params.quality : undefined; const delayMs = - typeof params.delayMs === "number" && - Number.isFinite(params.delayMs) + typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? params.delayMs : undefined; const deviceId = @@ -227,13 +208,10 @@ export function createNodesTool(): AnyAgentTool { normalizedFormat !== "jpeg" && normalizedFormat !== "png" ) { - throw new Error( - `unsupported camera.snap format: ${payload.format}`, - ); + throw new Error(`unsupported camera.snap format: ${payload.format}`); } - const isJpeg = - normalizedFormat === "jpg" || normalizedFormat === "jpeg"; + const isJpeg = normalizedFormat === "jpg" || normalizedFormat === "jpeg"; const filePath = cameraTempPath({ kind: "snap", facing, @@ -245,8 +223,7 @@ export function createNodesTool(): AnyAgentTool { type: "image", data: payload.base64, mimeType: - imageMimeFromFormat(payload.format) ?? - (isJpeg ? "image/jpeg" : "image/png"), + imageMimeFromFormat(payload.format) ?? (isJpeg ? "image/jpeg" : "image/png"), }); details.push({ facing, @@ -269,32 +246,24 @@ export function createNodesTool(): AnyAgentTool { idempotencyKey: crypto.randomUUID(), })) as { payload?: unknown }; const payload = - raw && typeof raw.payload === "object" && raw.payload !== null - ? raw.payload - : {}; + raw && typeof raw.payload === "object" && raw.payload !== null ? raw.payload : {}; return jsonResult(payload); } case "camera_clip": { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); - const facing = - typeof params.facing === "string" - ? params.facing.toLowerCase() - : "front"; + const facing = typeof params.facing === "string" ? params.facing.toLowerCase() : "front"; if (facing !== "front" && facing !== "back") { throw new Error("invalid facing (front|back)"); } const durationMs = - typeof params.durationMs === "number" && - Number.isFinite(params.durationMs) + typeof params.durationMs === "number" && Number.isFinite(params.durationMs) ? params.durationMs : typeof params.duration === "string" ? parseDurationMs(params.duration) : 3000; const includeAudio = - typeof params.includeAudio === "boolean" - ? params.includeAudio - : true; + typeof params.includeAudio === "boolean" ? params.includeAudio : true; const deviceId = typeof params.deviceId === "string" && params.deviceId.trim() ? params.deviceId.trim() @@ -332,25 +301,19 @@ export function createNodesTool(): AnyAgentTool { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); const durationMs = - typeof params.durationMs === "number" && - Number.isFinite(params.durationMs) + typeof params.durationMs === "number" && Number.isFinite(params.durationMs) ? params.durationMs : typeof params.duration === "string" ? parseDurationMs(params.duration) : 10_000; const fps = - typeof params.fps === "number" && Number.isFinite(params.fps) - ? params.fps - : 10; + typeof params.fps === "number" && Number.isFinite(params.fps) ? params.fps : 10; const screenIndex = - typeof params.screenIndex === "number" && - Number.isFinite(params.screenIndex) + typeof params.screenIndex === "number" && Number.isFinite(params.screenIndex) ? params.screenIndex : 0; const includeAudio = - typeof params.includeAudio === "boolean" - ? params.includeAudio - : true; + typeof params.includeAudio === "boolean" ? params.includeAudio : true; const raw = (await callGatewayTool("node.invoke", gatewayOpts, { nodeId, command: "screen.record", @@ -368,10 +331,7 @@ export function createNodesTool(): AnyAgentTool { typeof params.outPath === "string" && params.outPath.trim() ? params.outPath.trim() : screenRecordTempPath({ ext: payload.format || "mp4" }); - const written = await writeScreenRecordToFile( - filePath, - payload.base64, - ); + const written = await writeScreenRecordToFile(filePath, payload.base64); return { content: [{ type: "text", text: `FILE:${written.path}` }], details: { @@ -387,8 +347,7 @@ export function createNodesTool(): AnyAgentTool { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); const maxAgeMs = - typeof params.maxAgeMs === "number" && - Number.isFinite(params.maxAgeMs) + typeof params.maxAgeMs === "number" && Number.isFinite(params.maxAgeMs) ? params.maxAgeMs : undefined; const desiredAccuracy = @@ -419,23 +378,17 @@ export function createNodesTool(): AnyAgentTool { const nodeId = await resolveNodeId(gatewayOpts, node); const commandRaw = params.command; if (!commandRaw) { - throw new Error( - "command required (argv array, e.g. ['echo', 'Hello'])", - ); + throw new Error("command required (argv array, e.g. ['echo', 'Hello'])"); } if (!Array.isArray(commandRaw)) { - throw new Error( - "command must be an array of strings (argv), e.g. ['echo', 'Hello']", - ); + throw new Error("command must be an array of strings (argv), e.g. ['echo', 'Hello']"); } const command = commandRaw.map((c) => String(c)); if (command.length === 0) { throw new Error("command must not be empty"); } const cwd = - typeof params.cwd === "string" && params.cwd.trim() - ? params.cwd.trim() - : undefined; + typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined; const env = parseEnvPairs(params.env); const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs); const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs); diff --git a/src/agents/tools/nodes-utils.ts b/src/agents/tools/nodes-utils.ts index a2fd2213475..2430a00dd56 100644 --- a/src/agents/tools/nodes-utils.ts +++ b/src/agents/tools/nodes-utils.ts @@ -43,21 +43,13 @@ type PairingList = { }; function parseNodeList(value: unknown): NodeListNode[] { - const obj = - typeof value === "object" && value !== null - ? (value as Record) - : {}; + const obj = typeof value === "object" && value !== null ? (value as Record) : {}; return Array.isArray(obj.nodes) ? (obj.nodes as NodeListNode[]) : []; } function parsePairingList(value: unknown): PairingList { - const obj = - typeof value === "object" && value !== null - ? (value as Record) - : {}; - const pending = Array.isArray(obj.pending) - ? (obj.pending as PendingRequest[]) - : []; + const obj = typeof value === "object" && value !== null ? (value as Record) : {}; + const pending = Array.isArray(obj.pending) ? (obj.pending as PendingRequest[]) : []; const paired = Array.isArray(obj.paired) ? (obj.paired as PairedNode[]) : []; return { pending, paired }; } diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index 7ef36a88c51..6a3250130c2 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -6,10 +6,7 @@ import { resolveAuthProfileOrder, } from "../../agents/auth-profiles.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; -import { - getCustomProviderApiKey, - resolveEnvApiKey, -} from "../../agents/model-auth.js"; +import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js"; import { loadModelCatalog } from "../../agents/model-catalog.js"; import { buildAllowedModelSet, @@ -20,10 +17,7 @@ import { resolveModelRefFromString, } from "../../agents/model-selection.js"; import { normalizeGroupActivation } from "../../auto-reply/group-activation.js"; -import { - getFollowupQueueDepth, - resolveQueueSettings, -} from "../../auto-reply/reply/queue.js"; +import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js"; import { buildStatusMessage } from "../../auto-reply/status.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js"; @@ -45,10 +39,7 @@ import { } from "../../routing/session-key.js"; import type { AnyAgentTool } from "./common.js"; import { readStringParam } from "./common.js"; -import { - resolveInternalSessionKey, - resolveMainSessionAlias, -} from "./sessions-helpers.js"; +import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js"; const SessionStatusToolSchema = Type.Object({ sessionKey: Type.Optional(Type.String()), @@ -179,10 +170,8 @@ async function resolveModelOverride(params: { defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); - const currentProvider = - params.sessionEntry?.providerOverride?.trim() || configDefault.provider; - const currentModel = - params.sessionEntry?.modelOverride?.trim() || configDefault.model; + const currentProvider = params.sessionEntry?.providerOverride?.trim() || configDefault.provider; + const currentModel = params.sessionEntry?.modelOverride?.trim() || configDefault.model; const aliasIndex = buildModelAliasIndex({ cfg: params.cfg, @@ -209,8 +198,7 @@ async function resolveModelOverride(params: { throw new Error(`Model "${key}" is not allowed.`); } const isDefault = - resolved.ref.provider === configDefault.provider && - resolved.ref.model === configDefault.model; + resolved.ref.provider === configDefault.provider && resolved.ref.model === configDefault.model; return { kind: "set", provider: resolved.ref.provider, @@ -234,15 +222,12 @@ export function createSessionStatusTool(opts?: { const cfg = opts?.config ?? loadConfig(); const { mainKey, alias } = resolveMainSessionAlias(cfg); - const requestedKeyRaw = - readStringParam(params, "sessionKey") ?? opts?.agentSessionKey; + const requestedKeyRaw = readStringParam(params, "sessionKey") ?? opts?.agentSessionKey; if (!requestedKeyRaw?.trim()) { throw new Error("sessionKey required"); } - const agentId = resolveAgentIdFromSessionKey( - opts?.agentSessionKey ?? requestedKeyRaw, - ); + const agentId = resolveAgentIdFromSessionKey(opts?.agentSessionKey ?? requestedKeyRaw); const storePath = resolveStorePath(cfg.session?.store, { agentId }); const store = loadSessionStore(storePath); @@ -289,8 +274,7 @@ export function createSessionStatusTool(opts?: { defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); - const providerForCard = - resolved.entry.providerOverride?.trim() || configured.provider; + const providerForCard = resolved.entry.providerOverride?.trim() || configured.provider; const usageProvider = resolveUsageProviderId(providerForCard); let usageLine: string | undefined; if (usageProvider) { @@ -316,22 +300,18 @@ export function createSessionStatusTool(opts?: { resolved.key.includes(":group:") || resolved.key.includes(":channel:"); const groupActivation = isGroup - ? (normalizeGroupActivation(resolved.entry.groupActivation) ?? - "mention") + ? (normalizeGroupActivation(resolved.entry.groupActivation) ?? "mention") : undefined; const queueSettings = resolveQueueSettings({ cfg, - channel: - resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown", + channel: resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown", sessionEntry: resolved.entry, }); const queueKey = resolved.key ?? resolved.entry.sessionId; const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0; const queueOverrides = Boolean( - resolved.entry.queueDebounceMs ?? - resolved.entry.queueCap ?? - resolved.entry.queueDrop, + resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop, ); const statusText = buildStatusMessage({ diff --git a/src/agents/tools/sessions-announce-target.test.ts b/src/agents/tools/sessions-announce-target.test.ts index bffe5e8da3b..bc34eecd05d 100644 --- a/src/agents/tools/sessions-announce-target.test.ts +++ b/src/agents/tools/sessions-announce-target.test.ts @@ -43,9 +43,7 @@ describe("resolveAnnounceTarget", () => { accountId: "work", }); expect(callGatewayMock).toHaveBeenCalledTimes(1); - const first = callGatewayMock.mock.calls[0]?.[0] as - | { method?: string } - | undefined; + const first = callGatewayMock.mock.calls[0]?.[0] as { method?: string } | undefined; expect(first).toBeDefined(); expect(first?.method).toBe("sessions.list"); }); diff --git a/src/agents/tools/sessions-announce-target.ts b/src/agents/tools/sessions-announce-target.ts index 336c425a3ff..0b5d3adb7d0 100644 --- a/src/agents/tools/sessions-announce-target.ts +++ b/src/agents/tools/sessions-announce-target.ts @@ -1,7 +1,4 @@ -import { - getChannelPlugin, - normalizeChannelId, -} from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { callGateway } from "../../gateway/call.js"; import type { AnnounceTarget } from "./sessions-send-helpers.js"; import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js"; @@ -35,13 +32,9 @@ export async function resolveAnnounceTarget(params: { const match = sessions.find((entry) => entry?.key === params.sessionKey) ?? sessions.find((entry) => entry?.key === params.displayKey); - const channel = - typeof match?.lastChannel === "string" ? match.lastChannel : undefined; + const channel = typeof match?.lastChannel === "string" ? match.lastChannel : undefined; const to = typeof match?.lastTo === "string" ? match.lastTo : undefined; - const accountId = - typeof match?.lastAccountId === "string" - ? match.lastAccountId - : undefined; + const accountId = typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined; if (channel && to) return { channel, to, accountId }; } catch { // ignore diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index e6f10cb49e2..cc63d545d67 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -15,21 +15,13 @@ export function resolveMainSessionAlias(cfg: ClawdbotConfig) { return { mainKey, alias, scope }; } -export function resolveDisplaySessionKey(params: { - key: string; - alias: string; - mainKey: string; -}) { +export function resolveDisplaySessionKey(params: { key: string; alias: string; mainKey: string }) { if (params.key === params.alias) return "main"; if (params.key === params.mainKey) return "main"; return params.key; } -export function resolveInternalSessionKey(params: { - key: string; - alias: string; - mainKey: string; -}) { +export function resolveInternalSessionKey(params: { key: string; alias: string; mainKey: string }) { if (params.key === "main") return params.alias; return params.key; } @@ -46,11 +38,7 @@ export function classifySessionKind(params: { if (key.startsWith("hook:")) return "hook"; if (key.startsWith("node-") || key.startsWith("node:")) return "node"; if (params.gatewayKind === "group") return "group"; - if ( - key.startsWith("group:") || - key.includes(":group:") || - key.includes(":channel:") - ) { + if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) { return "group"; } return "other"; @@ -62,12 +50,7 @@ export function deriveChannel(params: { channel?: string | null; lastChannel?: string | null; }): string { - if ( - params.kind === "cron" || - params.kind === "hook" || - params.kind === "node" - ) - return "internal"; + if (params.kind === "cron" || params.kind === "hook" || params.kind === "node") return "internal"; const channel = normalizeKey(params.channel ?? undefined); if (channel) return channel; const lastChannel = normalizeKey(params.lastChannel ?? undefined); diff --git a/src/agents/tools/sessions-history-tool.ts b/src/agents/tools/sessions-history-tool.ts index 5288fc8390f..76dad4963ad 100644 --- a/src/agents/tools/sessions-history-tool.ts +++ b/src/agents/tools/sessions-history-tool.ts @@ -22,9 +22,7 @@ const SessionsHistoryToolSchema = Type.Object({ includeTools: Type.Optional(Type.Boolean()), }); -function resolveSandboxSessionToolsVisibility( - cfg: ReturnType, -) { +function resolveSandboxSessionToolsVisibility(cfg: ReturnType) { return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; } @@ -99,9 +97,7 @@ export function createSessionsHistoryTool(opts?: { const routingA2A = cfg.tools?.agentToAgent; const a2aEnabled = routingA2A?.enabled === true; - const allowPatterns = Array.isArray(routingA2A?.allow) - ? routingA2A.allow - : []; + const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : []; const matchesAllow = (agentId: string) => { if (allowPatterns.length === 0) return true; return allowPatterns.some((pattern) => { @@ -117,9 +113,7 @@ export function createSessionsHistoryTool(opts?: { const requesterAgentId = normalizeAgentId( parseAgentSessionKey(requesterInternalKey)?.agentId, ); - const targetAgentId = normalizeAgentId( - parseAgentSessionKey(resolvedKey)?.agentId, - ); + const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId); const isCrossAgent = requesterAgentId !== targetAgentId; if (isCrossAgent) { if (!a2aEnabled) { @@ -146,12 +140,8 @@ export function createSessionsHistoryTool(opts?: { method: "chat.history", params: { sessionKey: resolvedKey, limit }, })) as { messages?: unknown[] }; - const rawMessages = Array.isArray(result?.messages) - ? result.messages - : []; - const messages = includeTools - ? rawMessages - : stripToolMessages(rawMessages); + const rawMessages = Array.isArray(result?.messages) ? result.messages : []; + const messages = includeTools ? rawMessages : stripToolMessages(rawMessages); return jsonResult({ sessionKey: resolveDisplaySessionKey({ key: sessionKey, diff --git a/src/agents/tools/sessions-list-tool.gating.test.ts b/src/agents/tools/sessions-list-tool.gating.test.ts index c5e94da93c7..636c2c5a1c3 100644 --- a/src/agents/tools/sessions-list-tool.gating.test.ts +++ b/src/agents/tools/sessions-list-tool.gating.test.ts @@ -6,8 +6,7 @@ vi.mock("../../gateway/call.js", () => ({ })); vi.mock("../../config/config.js", async (importOriginal) => { - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, loadConfig: () => diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 1e628d5cb91..dc8bae6de94 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -51,9 +51,7 @@ const SessionsListToolSchema = Type.Object({ messageLimit: Type.Optional(Type.Number({ minimum: 0 })), }); -function resolveSandboxSessionToolsVisibility( - cfg: ReturnType, -) { +function resolveSandboxSessionToolsVisibility(cfg: ReturnType) { return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; } @@ -91,22 +89,18 @@ export function createSessionsListTool(opts?: { const allowedKindsList = (kindsRaw ?? []).filter((value) => ["main", "group", "cron", "hook", "node", "other"].includes(value), ); - const allowedKinds = allowedKindsList.length - ? new Set(allowedKindsList) - : undefined; + const allowedKinds = allowedKindsList.length ? new Set(allowedKindsList) : undefined; const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : undefined; const activeMinutes = - typeof params.activeMinutes === "number" && - Number.isFinite(params.activeMinutes) + typeof params.activeMinutes === "number" && Number.isFinite(params.activeMinutes) ? Math.max(1, Math.floor(params.activeMinutes)) : undefined; const messageLimitRaw = - typeof params.messageLimit === "number" && - Number.isFinite(params.messageLimit) + typeof params.messageLimit === "number" && Number.isFinite(params.messageLimit) ? Math.max(0, Math.floor(params.messageLimit)) : 0; const messageLimit = Math.min(messageLimitRaw, 20); @@ -129,9 +123,7 @@ export function createSessionsListTool(opts?: { const storePath = typeof list?.path === "string" ? list.path : undefined; const routingA2A = cfg.tools?.agentToAgent; const a2aEnabled = routingA2A?.enabled === true; - const allowPatterns = Array.isArray(routingA2A?.allow) - ? routingA2A.allow - : []; + const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : []; const matchesAllow = (agentId: string) => { if (allowPatterns.length === 0) return true; return allowPatterns.some((pattern) => { @@ -154,21 +146,17 @@ export function createSessionsListTool(opts?: { const key = typeof entry.key === "string" ? entry.key : ""; if (!key) continue; - const entryAgentId = normalizeAgentId( - parseAgentSessionKey(key)?.agentId, - ); + const entryAgentId = normalizeAgentId(parseAgentSessionKey(key)?.agentId); const crossAgent = entryAgentId !== requesterAgentId; if (crossAgent) { if (!a2aEnabled) continue; - if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId)) - continue; + if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId)) continue; } if (key === "unknown") continue; if (key === "global" && alias !== "global") continue; - const gatewayKind = - typeof entry.kind === "string" ? entry.kind : undefined; + const gatewayKind = typeof entry.kind === "string" ? entry.kind : undefined; const kind = classifySessionKind({ key, gatewayKind, alias, mainKey }); if (allowedKinds && !allowedKinds.has(kind)) continue; @@ -178,14 +166,10 @@ export function createSessionsListTool(opts?: { mainKey, }); - const entryChannel = - typeof entry.channel === "string" ? entry.channel : undefined; - const lastChannel = - typeof entry.lastChannel === "string" ? entry.lastChannel : undefined; + const entryChannel = typeof entry.channel === "string" ? entry.channel : undefined; + const lastChannel = typeof entry.lastChannel === "string" ? entry.lastChannel : undefined; const lastAccountId = - typeof entry.lastAccountId === "string" - ? entry.lastAccountId - : undefined; + typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined; const derivedChannel = deriveChannel({ key, kind, @@ -193,8 +177,7 @@ export function createSessionsListTool(opts?: { lastChannel, }); - const sessionId = - typeof entry.sessionId === "string" ? entry.sessionId : undefined; + const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : undefined; const transcriptPath = sessionId && storePath ? path.join(path.dirname(storePath), `${sessionId}.jsonl`) @@ -205,40 +188,18 @@ export function createSessionsListTool(opts?: { kind, channel: derivedChannel, label: typeof entry.label === "string" ? entry.label : undefined, - displayName: - typeof entry.displayName === "string" - ? entry.displayName - : undefined, - updatedAt: - typeof entry.updatedAt === "number" ? entry.updatedAt : undefined, + displayName: typeof entry.displayName === "string" ? entry.displayName : undefined, + updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined, sessionId, model: typeof entry.model === "string" ? entry.model : undefined, - contextTokens: - typeof entry.contextTokens === "number" - ? entry.contextTokens - : undefined, - totalTokens: - typeof entry.totalTokens === "number" - ? entry.totalTokens - : undefined, - thinkingLevel: - typeof entry.thinkingLevel === "string" - ? entry.thinkingLevel - : undefined, - verboseLevel: - typeof entry.verboseLevel === "string" - ? entry.verboseLevel - : undefined, - systemSent: - typeof entry.systemSent === "boolean" - ? entry.systemSent - : undefined, + contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : undefined, + totalTokens: typeof entry.totalTokens === "number" ? entry.totalTokens : undefined, + thinkingLevel: typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined, + verboseLevel: typeof entry.verboseLevel === "string" ? entry.verboseLevel : undefined, + systemSent: typeof entry.systemSent === "boolean" ? entry.systemSent : undefined, abortedLastRun: - typeof entry.abortedLastRun === "boolean" - ? entry.abortedLastRun - : undefined, - sendPolicy: - typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined, + typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined, + sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined, lastChannel, lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined, lastAccountId, @@ -255,14 +216,9 @@ export function createSessionsListTool(opts?: { method: "chat.history", params: { sessionKey: resolvedKey, limit: messageLimit }, })) as { messages?: unknown[] }; - const rawMessages = Array.isArray(history?.messages) - ? history.messages - : []; + const rawMessages = Array.isArray(history?.messages) ? history.messages : []; const filtered = stripToolMessages(rawMessages); - row.messages = - filtered.length > messageLimit - ? filtered.slice(-messageLimit) - : filtered; + row.messages = filtered.length > messageLimit ? filtered.slice(-messageLimit) : filtered; } rows.push(row); diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 8cdeb243b42..0e2611b8548 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -1,7 +1,4 @@ -import { - getChannelPlugin, - normalizeChannelId, -} from "../../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import type { ClawdbotConfig } from "../../config/config.js"; const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP"; @@ -15,14 +12,9 @@ export type AnnounceTarget = { accountId?: string; }; -export function resolveAnnounceTargetFromKey( - sessionKey: string, -): AnnounceTarget | null { +export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget | null { const rawParts = sessionKey.split(":").filter(Boolean); - const parts = - rawParts.length >= 3 && rawParts[0] === "agent" - ? rawParts.slice(2) - : rawParts; + const parts = rawParts.length >= 3 && rawParts[0] === "agent" ? rawParts.slice(2) : rawParts; if (parts.length < 3) return null; const [channelRaw, kind, ...rest] = parts; if (kind !== "group" && kind !== "channel") return null; @@ -37,9 +29,7 @@ export function resolveAnnounceTargetFromKey( : `group:${id}` : id; const normalized = normalizedChannel - ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.( - kindTarget, - ) + ? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget) : undefined; return { channel, to: normalized ?? kindTarget }; } @@ -72,9 +62,7 @@ export function buildAgentToAgentReplyContext(params: { maxTurns: number; }) { const currentLabel = - params.currentRole === "requester" - ? "Agent 1 (requester)" - : "Agent 2 (target)"; + params.currentRole === "requester" ? "Agent 1 (requester)" : "Agent 2 (target)"; const lines = [ "Agent-to-agent reply step:", `Current agent: ${currentLabel}.`, @@ -86,9 +74,7 @@ export function buildAgentToAgentReplyContext(params: { ? `Agent 1 (requester) channel: ${params.requesterChannel}.` : undefined, `Agent 2 (target) session: ${params.targetSessionKey}.`, - params.targetChannel - ? `Agent 2 (target) channel: ${params.targetChannel}.` - : undefined, + params.targetChannel ? `Agent 2 (target) channel: ${params.targetChannel}.` : undefined, `If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`, ].filter(Boolean); return lines.join("\n"); @@ -112,16 +98,12 @@ export function buildAgentToAgentAnnounceContext(params: { ? `Agent 1 (requester) channel: ${params.requesterChannel}.` : undefined, `Agent 2 (target) session: ${params.targetSessionKey}.`, - params.targetChannel - ? `Agent 2 (target) channel: ${params.targetChannel}.` - : undefined, + params.targetChannel ? `Agent 2 (target) channel: ${params.targetChannel}.` : undefined, `Original request: ${params.originalMessage}`, params.roundOneReply ? `Round 1 reply: ${params.roundOneReply}` : "Round 1 reply: (not available).", - params.latestReply - ? `Latest reply: ${params.latestReply}` - : "Latest reply: (not available).", + params.latestReply ? `Latest reply: ${params.latestReply}` : "Latest reply: (not available).", `If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`, "Any other reply will be posted to the target channel.", "After this reply, the agent-to-agent conversation is over.", diff --git a/src/agents/tools/sessions-send-tool.a2a.ts b/src/agents/tools/sessions-send-tool.a2a.ts index 62b65824dc1..ab52b70a15d 100644 --- a/src/agents/tools/sessions-send-tool.a2a.ts +++ b/src/agents/tools/sessions-send-tool.a2a.ts @@ -66,9 +66,7 @@ export async function runSessionsSendA2AFlow(params: { let incomingMessage = latestReply; for (let turn = 1; turn <= params.maxPingPongTurns; turn += 1) { const currentRole = - currentSessionKey === params.requesterSessionKey - ? "requester" - : "target"; + currentSessionKey === params.requesterSessionKey ? "requester" : "target"; const replyPrompt = buildAgentToAgentReplyContext({ requesterSessionKey: params.requesterSessionKey, requesterChannel: params.requesterChannel, @@ -112,12 +110,7 @@ export async function runSessionsSendA2AFlow(params: { timeoutMs: params.announceTimeoutMs, lane: AGENT_LANE_NESTED, }); - if ( - announceTarget && - announceReply && - announceReply.trim() && - !isAnnounceSkip(announceReply) - ) { + if (announceTarget && announceReply && announceReply.trim() && !isAnnounceSkip(announceReply)) { try { await callGateway({ method: "send", diff --git a/src/agents/tools/sessions-send-tool.gating.test.ts b/src/agents/tools/sessions-send-tool.gating.test.ts index 8e823666e98..76a242c9898 100644 --- a/src/agents/tools/sessions-send-tool.gating.test.ts +++ b/src/agents/tools/sessions-send-tool.gating.test.ts @@ -6,8 +6,7 @@ vi.mock("../../gateway/call.js", () => ({ })); vi.mock("../../config/config.js", async (importOriginal) => { - const actual = - await importOriginal(); + const actual = await importOriginal(); return { ...actual, loadConfig: () => diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index 594ee306258..5366edc1631 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -24,17 +24,12 @@ import { resolveMainSessionAlias, stripToolMessages, } from "./sessions-helpers.js"; -import { - buildAgentToAgentMessageContext, - resolvePingPongTurns, -} from "./sessions-send-helpers.js"; +import { buildAgentToAgentMessageContext, resolvePingPongTurns } from "./sessions-send-helpers.js"; import { runSessionsSendA2AFlow } from "./sessions-send-tool.a2a.js"; const SessionsSendToolSchema = Type.Object({ sessionKey: Type.Optional(Type.String()), - label: Type.Optional( - Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH }), - ), + label: Type.Optional(Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH })), agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })), message: Type.String(), timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })), @@ -56,8 +51,7 @@ export function createSessionsSendTool(opts?: { const message = readStringParam(params, "message", { required: true }); const cfg = loadConfig(); const { mainKey, alias } = resolveMainSessionAlias(cfg); - const visibility = - cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; + const visibility = cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; const requesterInternalKey = typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim() ? resolveInternalSessionKey({ @@ -74,9 +68,7 @@ export function createSessionsSendTool(opts?: { const routingA2A = cfg.tools?.agentToAgent; const a2aEnabled = routingA2A?.enabled === true; - const allowPatterns = Array.isArray(routingA2A?.allow) - ? routingA2A.allow - : []; + const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : []; const matchesAllow = (agentId: string) => { if (allowPatterns.length === 0) return true; return allowPatterns.some((pattern) => { @@ -92,8 +84,7 @@ export function createSessionsSendTool(opts?: { const sessionKeyParam = readStringParam(params, "sessionKey"); const labelParam = readStringParam(params, "label")?.trim() || undefined; - const labelAgentIdParam = - readStringParam(params, "agentId")?.trim() || undefined; + const labelAgentIdParam = readStringParam(params, "agentId")?.trim() || undefined; if (sessionKeyParam && labelParam) { return jsonResult({ runId: crypto.randomUUID(), @@ -114,9 +105,7 @@ export function createSessionsSendTool(opts?: { let sessionKey = sessionKeyParam; if (!sessionKey && labelParam) { const requesterAgentId = requesterInternalKey - ? normalizeAgentId( - parseAgentSessionKey(requesterInternalKey)?.agentId, - ) + ? normalizeAgentId(parseAgentSessionKey(requesterInternalKey)?.agentId) : undefined; const requestedAgentId = labelAgentIdParam ? normalizeAgentId(labelAgentIdParam) @@ -131,16 +120,11 @@ export function createSessionsSendTool(opts?: { return jsonResult({ runId: crypto.randomUUID(), status: "forbidden", - error: - "Sandboxed sessions_send label lookup is limited to this agent", + error: "Sandboxed sessions_send label lookup is limited to this agent", }); } - if ( - requesterAgentId && - requestedAgentId && - requestedAgentId !== requesterAgentId - ) { + if (requesterAgentId && requestedAgentId && requestedAgentId !== requesterAgentId) { if (!a2aEnabled) { return jsonResult({ runId: crypto.randomUUID(), @@ -149,15 +133,11 @@ export function createSessionsSendTool(opts?: { "Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.", }); } - if ( - !matchesAllow(requesterAgentId) || - !matchesAllow(requestedAgentId) - ) { + if (!matchesAllow(requesterAgentId) || !matchesAllow(requestedAgentId)) { return jsonResult({ runId: crypto.randomUUID(), status: "forbidden", - error: - "Agent-to-agent messaging denied by tools.agentToAgent.allow.", + error: "Agent-to-agent messaging denied by tools.agentToAgent.allow.", }); } } @@ -174,8 +154,7 @@ export function createSessionsSendTool(opts?: { params: resolveParams, timeoutMs: 10_000, })) as { key?: unknown }; - resolvedKey = - typeof resolved?.key === "string" ? resolved.key.trim() : ""; + resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : ""; } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (restrictToSpawned) { @@ -245,8 +224,7 @@ export function createSessionsSendTool(opts?: { } } const timeoutSeconds = - typeof params.timeoutSeconds === "number" && - Number.isFinite(params.timeoutSeconds) + typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds) ? Math.max(0, Math.floor(params.timeoutSeconds)) : 30; const timeoutMs = timeoutSeconds * 1000; @@ -261,9 +239,7 @@ export function createSessionsSendTool(opts?: { const requesterAgentId = normalizeAgentId( parseAgentSessionKey(requesterInternalKey)?.agentId, ); - const targetAgentId = normalizeAgentId( - parseAgentSessionKey(resolvedKey)?.agentId, - ); + const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId); const isCrossAgent = requesterAgentId !== targetAgentId; if (isCrossAgent) { if (!a2aEnabled) { @@ -279,8 +255,7 @@ export function createSessionsSendTool(opts?: { return jsonResult({ runId: crypto.randomUUID(), status: "forbidden", - error: - "Agent-to-agent messaging denied by tools.agentToAgent.allow.", + error: "Agent-to-agent messaging denied by tools.agentToAgent.allow.", sessionKey: displayKey, }); } @@ -337,11 +312,7 @@ export function createSessionsSendTool(opts?: { }); } catch (err) { const messageText = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "error"; + err instanceof Error ? err.message : typeof err === "string" ? err : "error"; return jsonResult({ runId, status: "error", @@ -362,11 +333,7 @@ export function createSessionsSendTool(opts?: { } } catch (err) { const messageText = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "error"; + err instanceof Error ? err.message : typeof err === "string" ? err : "error"; return jsonResult({ runId, status: "error", @@ -390,11 +357,7 @@ export function createSessionsSendTool(opts?: { waitError = typeof wait?.error === "string" ? wait.error : undefined; } catch (err) { const messageText = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "error"; + err instanceof Error ? err.message : typeof err === "string" ? err : "error"; return jsonResult({ runId, status: messageText.includes("gateway timeout") ? "timeout" : "error", @@ -424,11 +387,8 @@ export function createSessionsSendTool(opts?: { method: "chat.history", params: { sessionKey: resolvedKey, limit: 50 }, })) as { messages?: unknown[] }; - const filtered = stripToolMessages( - Array.isArray(history?.messages) ? history.messages : [], - ); - const last = - filtered.length > 0 ? filtered[filtered.length - 1] : undefined; + const filtered = stripToolMessages(Array.isArray(history?.messages) ? history.messages : []); + const last = filtered.length > 0 ? filtered[filtered.length - 1] : undefined; const reply = last ? extractAssistantText(last) : undefined; startA2AFlow(reply ?? undefined); diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 01678956018..6ae8b8dc322 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -68,14 +68,12 @@ export function createSessionsSpawnTool(opts?: { : "keep"; const runTimeoutSeconds = (() => { const explicit = - typeof params.runTimeoutSeconds === "number" && - Number.isFinite(params.runTimeoutSeconds) + typeof params.runTimeoutSeconds === "number" && Number.isFinite(params.runTimeoutSeconds) ? Math.max(0, Math.floor(params.runTimeoutSeconds)) : undefined; if (explicit !== undefined) return explicit; const legacy = - typeof params.timeoutSeconds === "number" && - Number.isFinite(params.timeoutSeconds) + typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds) ? Math.max(0, Math.floor(params.timeoutSeconds)) : undefined; return legacy ?? 0; @@ -86,10 +84,7 @@ export function createSessionsSpawnTool(opts?: { const cfg = loadConfig(); const { mainKey, alias } = resolveMainSessionAlias(cfg); const requesterSessionKey = opts?.agentSessionKey; - if ( - typeof requesterSessionKey === "string" && - isSubagentSessionKey(requesterSessionKey) - ) { + if (typeof requesterSessionKey === "string" && isSubagentSessionKey(requesterSessionKey)) { return jsonResult({ status: "forbidden", error: "sessions_spawn is not allowed from sub-agent sessions", @@ -115,9 +110,7 @@ export function createSessionsSpawnTool(opts?: { ? normalizeAgentId(requestedAgentId) : requesterAgentId; if (targetAgentId !== requesterAgentId) { - const allowAgents = - resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? - []; + const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? []; const allowAny = allowAgents.some((value) => value.trim() === "*"); const normalizedTargetId = targetAgentId.toLowerCase(); const allowSet = new Set( @@ -154,14 +147,9 @@ export function createSessionsSpawnTool(opts?: { modelApplied = true; } catch (err) { const messageText = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "error"; + err instanceof Error ? err.message : typeof err === "string" ? err : "error"; const recoverable = - messageText.includes("invalid model") || - messageText.includes("model not allowed"); + messageText.includes("invalid model") || messageText.includes("model not allowed"); if (!recoverable) { return jsonResult({ status: "error", @@ -204,11 +192,7 @@ export function createSessionsSpawnTool(opts?: { } } catch (err) { const messageText = - err instanceof Error - ? err.message - : typeof err === "string" - ? err - : "error"; + err instanceof Error ? err.message : typeof err === "string" ? err : "error"; return jsonResult({ status: "error", error: messageText, diff --git a/src/agents/tools/slack-actions.test.ts b/src/agents/tools/slack-actions.test.ts index 9dc91f49f65..bba2f1d078d 100644 --- a/src/agents/tools/slack-actions.test.ts +++ b/src/agents/tools/slack-actions.test.ts @@ -27,8 +27,7 @@ vi.mock("../../slack/actions.js", () => ({ pinSlackMessage: (...args: unknown[]) => pinSlackMessage(...args), reactSlackMessage: (...args: unknown[]) => reactSlackMessage(...args), readSlackMessages: (...args: unknown[]) => readSlackMessages(...args), - removeOwnSlackReactions: (...args: unknown[]) => - removeOwnSlackReactions(...args), + removeOwnSlackReactions: (...args: unknown[]) => removeOwnSlackReactions(...args), removeSlackReaction: (...args: unknown[]) => removeSlackReaction(...args), sendSlackMessage: (...args: unknown[]) => sendSlackMessage(...args), unpinSlackMessage: (...args: unknown[]) => unpinSlackMessage(...args), @@ -122,14 +121,10 @@ describe("handleSlackAction", () => { }, cfg, ); - expect(sendSlackMessage).toHaveBeenCalledWith( - "channel:C123", - "Hello thread", - { - mediaUrl: undefined, - threadTs: "1234567890.123456", - }, - ); + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Hello thread", { + mediaUrl: undefined, + threadTs: "1234567890.123456", + }); }); it("auto-injects threadTs from context when replyToMode=all", async () => { @@ -148,14 +143,10 @@ describe("handleSlackAction", () => { replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith( - "channel:C123", - "Auto-threaded", - { - mediaUrl: undefined, - threadTs: "1111111111.111111", - }, - ); + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Auto-threaded", { + mediaUrl: undefined, + threadTs: "1111111111.111111", + }); }); it("replyToMode=first threads first message then stops", async () => { @@ -187,14 +178,10 @@ describe("handleSlackAction", () => { cfg, context, ); - expect(sendSlackMessage).toHaveBeenLastCalledWith( - "channel:C123", - "Second", - { - mediaUrl: undefined, - threadTs: undefined, - }, - ); + expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Second", { + mediaUrl: undefined, + threadTs: undefined, + }); }); it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => { @@ -218,14 +205,10 @@ describe("handleSlackAction", () => { cfg, context, ); - expect(sendSlackMessage).toHaveBeenLastCalledWith( - "channel:C123", - "Explicit", - { - mediaUrl: undefined, - threadTs: "2222222222.222222", - }, - ); + expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Explicit", { + mediaUrl: undefined, + threadTs: "2222222222.222222", + }); expect(hasRepliedRef.value).toBe(true); await handleSlackAction( @@ -233,29 +216,21 @@ describe("handleSlackAction", () => { cfg, context, ); - expect(sendSlackMessage).toHaveBeenLastCalledWith( - "channel:C123", - "Second", - { - mediaUrl: undefined, - threadTs: undefined, - }, - ); + expect(sendSlackMessage).toHaveBeenLastCalledWith("channel:C123", "Second", { + mediaUrl: undefined, + threadTs: undefined, + }); }); it("replyToMode=first without hasRepliedRef does not thread", async () => { const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig; sendSlackMessage.mockClear(); - await handleSlackAction( - { action: "sendMessage", to: "channel:C123", content: "No ref" }, - cfg, - { - currentChannelId: "C123", - currentThreadTs: "1111111111.111111", - replyToMode: "first", - // no hasRepliedRef - }, - ); + await handleSlackAction({ action: "sendMessage", to: "channel:C123", content: "No ref" }, cfg, { + currentChannelId: "C123", + currentThreadTs: "1111111111.111111", + replyToMode: "first", + // no hasRepliedRef + }); expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "No ref", { mediaUrl: undefined, threadTs: undefined, @@ -300,14 +275,10 @@ describe("handleSlackAction", () => { replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith( - "channel:C999", - "Different channel", - { - mediaUrl: undefined, - threadTs: undefined, - }, - ); + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C999", "Different channel", { + mediaUrl: undefined, + threadTs: undefined, + }); }); it("explicit threadTs overrides context threadTs", async () => { @@ -327,14 +298,10 @@ describe("handleSlackAction", () => { replyToMode: "all", }, ); - expect(sendSlackMessage).toHaveBeenCalledWith( - "channel:C123", - "Explicit thread", - { - mediaUrl: undefined, - threadTs: "2222222222.222222", - }, - ); + expect(sendSlackMessage).toHaveBeenCalledWith("channel:C123", "Explicit thread", { + mediaUrl: undefined, + threadTs: "2222222222.222222", + }); }); it("handles channel target without prefix when replyToMode=all", async () => { diff --git a/src/agents/tools/slack-actions.ts b/src/agents/tools/slack-actions.ts index f2d16169004..a4e6e0addd8 100644 --- a/src/agents/tools/slack-actions.ts +++ b/src/agents/tools/slack-actions.ts @@ -17,19 +17,9 @@ import { sendSlackMessage, unpinSlackMessage, } from "../../slack/actions.js"; -import { - createActionGate, - jsonResult, - readReactionParams, - readStringParam, -} from "./common.js"; +import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js"; -const messagingActions = new Set([ - "sendMessage", - "editMessage", - "deleteMessage", - "readMessages", -]); +const messagingActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]); const reactionsActions = new Set(["react", "reactions"]); const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]); @@ -73,11 +63,7 @@ function resolveThreadTsFromContext( if (context.replyToMode === "all") { return context.currentThreadTs; } - if ( - context.replyToMode === "first" && - context.hasRepliedRef && - !context.hasRepliedRef.value - ) { + if (context.replyToMode === "first" && context.hasRepliedRef && !context.hasRepliedRef.value) { context.hasRepliedRef.value = true; return context.currentThreadTs; } @@ -157,9 +143,7 @@ export async function handleSlackAction( // threadTs: once we send a message to the current channel, consider the // first reply "used" so later tool calls don't auto-thread again. if (context?.hasRepliedRef && context.currentChannelId) { - const normalizedTarget = to.startsWith("channel:") - ? to.slice("channel:".length) - : to; + const normalizedTarget = to.startsWith("channel:") ? to.slice("channel:".length) : to; if (normalizedTarget === context.currentChannelId) { context.hasRepliedRef.value = true; } @@ -204,9 +188,7 @@ export async function handleSlackAction( }); const limitRaw = params.limit; const limit = - typeof limitRaw === "number" && Number.isFinite(limitRaw) - ? limitRaw - : undefined; + typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; const before = readStringParam(params, "before"); const after = readStringParam(params, "after"); const result = await readSlackMessages(channelId, { @@ -270,9 +252,7 @@ export async function handleSlackAction( if (!isActionEnabled("emojiList")) { throw new Error("Slack emoji list is disabled."); } - const emojis = accountOpts - ? await listSlackEmojis(accountOpts) - : await listSlackEmojis(); + const emojis = accountOpts ? await listSlackEmojis(accountOpts) : await listSlackEmojis(); return jsonResult({ ok: true, emojis }); } diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 637d779eda7..08ad175ac86 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -1,10 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../../config/config.js"; -import { - handleTelegramAction, - readTelegramButtons, -} from "./telegram-actions.js"; +import { handleTelegramAction, readTelegramButtons } from "./telegram-actions.js"; const reactMessageTelegram = vi.fn(async () => ({ ok: true })); const sendMessageTelegram = vi.fn(async () => ({ diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 49cce680f95..02c0df8037d 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -1,10 +1,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import { resolveChannelCapabilities } from "../../config/channel-capabilities.js"; import type { ClawdbotConfig } from "../../config/config.js"; -import { - reactMessageTelegram, - sendMessageTelegram, -} from "../../telegram/send.js"; +import { reactMessageTelegram, sendMessageTelegram } from "../../telegram/send.js"; import { resolveTelegramToken } from "../../telegram/token.js"; import { createActionGate, @@ -47,23 +44,18 @@ export function readTelegramButtons( } return row.map((button, buttonIndex) => { if (!button || typeof button !== "object") { - throw new Error( - `buttons[${rowIndex}][${buttonIndex}] must be an object`, - ); + throw new Error(`buttons[${rowIndex}][${buttonIndex}] must be an object`); } const text = typeof (button as { text?: unknown }).text === "string" ? (button as { text: string }).text.trim() : ""; const callbackData = - typeof (button as { callback_data?: unknown }).callback_data === - "string" + typeof (button as { callback_data?: unknown }).callback_data === "string" ? (button as { callback_data: string }).callback_data.trim() : ""; if (!text || !callbackData) { - throw new Error( - `buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`, - ); + throw new Error(`buttons[${rowIndex}][${buttonIndex}] requires text and callback_data`); } if (callbackData.length > 64) { throw new Error( @@ -124,10 +116,7 @@ export async function handleTelegramAction( const content = readStringParam(params, "content", { required: true }); const mediaUrl = readStringParam(params, "mediaUrl"); const buttons = readTelegramButtons(params); - if ( - buttons && - !hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined }) - ) { + if (buttons && !hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })) { throw new Error( 'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts..capabilities).', ); diff --git a/src/agents/tools/whatsapp-actions.test.ts b/src/agents/tools/whatsapp-actions.test.ts index 4902c9740ea..63b4a1adc47 100644 --- a/src/agents/tools/whatsapp-actions.test.ts +++ b/src/agents/tools/whatsapp-actions.test.ts @@ -24,17 +24,12 @@ describe("handleWhatsAppAction", () => { }, enabledConfig, ); - expect(sendReactionWhatsApp).toHaveBeenCalledWith( - "123@s.whatsapp.net", - "msg1", - "✅", - { - verbose: false, - fromMe: undefined, - participant: undefined, - accountId: undefined, - }, - ); + expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "✅", { + verbose: false, + fromMe: undefined, + participant: undefined, + accountId: undefined, + }); }); it("removes reactions on empty emoji", async () => { @@ -47,17 +42,12 @@ describe("handleWhatsAppAction", () => { }, enabledConfig, ); - expect(sendReactionWhatsApp).toHaveBeenCalledWith( - "123@s.whatsapp.net", - "msg1", - "", - { - verbose: false, - fromMe: undefined, - participant: undefined, - accountId: undefined, - }, - ); + expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "", { + verbose: false, + fromMe: undefined, + participant: undefined, + accountId: undefined, + }); }); it("removes reactions when remove flag set", async () => { @@ -71,17 +61,12 @@ describe("handleWhatsAppAction", () => { }, enabledConfig, ); - expect(sendReactionWhatsApp).toHaveBeenCalledWith( - "123@s.whatsapp.net", - "msg1", - "", - { - verbose: false, - fromMe: undefined, - participant: undefined, - accountId: undefined, - }, - ); + expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "", { + verbose: false, + fromMe: undefined, + participant: undefined, + accountId: undefined, + }); }); it("passes account scope and sender flags", async () => { @@ -97,17 +82,12 @@ describe("handleWhatsAppAction", () => { }, enabledConfig, ); - expect(sendReactionWhatsApp).toHaveBeenCalledWith( - "123@s.whatsapp.net", - "msg1", - "🎉", - { - verbose: false, - fromMe: true, - participant: "999@s.whatsapp.net", - accountId: "work", - }, - ); + expect(sendReactionWhatsApp).toHaveBeenCalledWith("123@s.whatsapp.net", "msg1", "🎉", { + verbose: false, + fromMe: true, + participant: "999@s.whatsapp.net", + accountId: "work", + }); }); it("respects reaction gating", async () => { diff --git a/src/agents/tools/whatsapp-actions.ts b/src/agents/tools/whatsapp-actions.ts index 01ab8483611..0e1651738ac 100644 --- a/src/agents/tools/whatsapp-actions.ts +++ b/src/agents/tools/whatsapp-actions.ts @@ -2,12 +2,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { ClawdbotConfig } from "../../config/config.js"; import { sendReactionWhatsApp } from "../../web/outbound.js"; -import { - createActionGate, - jsonResult, - readReactionParams, - readStringParam, -} from "./common.js"; +import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js"; export async function handleWhatsAppAction( params: Record, diff --git a/src/agents/usage.ts b/src/agents/usage.ts index 76697bcda63..aa7dce767b9 100644 --- a/src/agents/usage.ts +++ b/src/agents/usage.ts @@ -36,30 +36,18 @@ const asFiniteNumber = (value: unknown): number | undefined => { return value; }; -export function hasNonzeroUsage( - usage?: NormalizedUsage | null, -): usage is NormalizedUsage { +export function hasNonzeroUsage(usage?: NormalizedUsage | null): usage is NormalizedUsage { if (!usage) return false; - return [ - usage.input, - usage.output, - usage.cacheRead, - usage.cacheWrite, - usage.total, - ].some((v) => typeof v === "number" && Number.isFinite(v) && v > 0); + return [usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.total].some( + (v) => typeof v === "number" && Number.isFinite(v) && v > 0, + ); } -export function normalizeUsage( - raw?: UsageLike | null, -): NormalizedUsage | undefined { +export function normalizeUsage(raw?: UsageLike | null): NormalizedUsage | undefined { if (!raw) return undefined; const input = asFiniteNumber( - raw.input ?? - raw.inputTokens ?? - raw.input_tokens ?? - raw.promptTokens ?? - raw.prompt_tokens, + raw.input ?? raw.inputTokens ?? raw.input_tokens ?? raw.promptTokens ?? raw.prompt_tokens, ); const output = asFiniteNumber( raw.output ?? @@ -68,15 +56,11 @@ export function normalizeUsage( raw.completionTokens ?? raw.completion_tokens, ); - const cacheRead = asFiniteNumber( - raw.cacheRead ?? raw.cache_read ?? raw.cache_read_input_tokens, - ); + const cacheRead = asFiniteNumber(raw.cacheRead ?? raw.cache_read ?? raw.cache_read_input_tokens); const cacheWrite = asFiniteNumber( raw.cacheWrite ?? raw.cache_write ?? raw.cache_creation_input_tokens, ); - const total = asFiniteNumber( - raw.total ?? raw.totalTokens ?? raw.total_tokens, - ); + const total = asFiniteNumber(raw.total ?? raw.totalTokens ?? raw.total_tokens); if ( input === undefined && diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index feeb1b064f7..e14022fde07 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -24,9 +24,7 @@ describe("ensureAgentWorkspace", () => { ensureBootstrapFiles: true, }); expect(result.dir).toBe(path.resolve(nested)); - expect(result.agentsPath).toBe( - path.join(path.resolve(nested), "AGENTS.md"), - ); + expect(result.agentsPath).toBe(path.join(path.resolve(nested), "AGENTS.md")); expect(result.agentsPath).toBeDefined(); if (!result.agentsPath) throw new Error("agentsPath missing"); const content = await fs.readFile(result.agentsPath, "utf-8"); @@ -111,20 +109,12 @@ describe("filterBootstrapFilesForSession", () => { ]; it("keeps full bootstrap set for non-subagent sessions", () => { - const result = filterBootstrapFilesForSession( - files, - "agent:main:session:abc", - ); - expect(result.map((file) => file.name)).toEqual( - files.map((file) => file.name), - ); + const result = filterBootstrapFilesForSession(files, "agent:main:session:abc"); + expect(result.map((file) => file.name)).toEqual(files.map((file) => file.name)); }); it("limits bootstrap files for subagent sessions", () => { - const result = filterBootstrapFilesForSession( - files, - "agent:main:subagent:abc", - ); + const result = filterBootstrapFilesForSession(files, "agent:main:subagent:abc"); expect(result.map((file) => file.name)).toEqual([ DEFAULT_AGENTS_FILENAME, DEFAULT_TOOLS_FILENAME, diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 19657699054..fc2c2cae92e 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -219,9 +219,7 @@ export async function ensureAgentWorkspace(params?: { heartbeatPath?: string; bootstrapPath?: string; }> { - const rawDir = params?.dir?.trim() - ? params.dir.trim() - : DEFAULT_AGENT_WORKSPACE_DIR; + const rawDir = params?.dir?.trim() ? params.dir.trim() : DEFAULT_AGENT_WORKSPACE_DIR; const dir = resolveUserPath(rawDir); await fs.mkdir(dir, { recursive: true }); @@ -236,14 +234,7 @@ export async function ensureAgentWorkspace(params?: { const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME); const isBrandNewWorkspace = await (async () => { - const paths = [ - agentsPath, - soulPath, - toolsPath, - identityPath, - userPath, - heartbeatPath, - ]; + const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath, heartbeatPath]; const existing = await Promise.all( paths.map(async (p) => { try { @@ -257,26 +248,11 @@ export async function ensureAgentWorkspace(params?: { return existing.every((v) => !v); })(); - const agentsTemplate = await loadTemplate( - DEFAULT_AGENTS_FILENAME, - DEFAULT_AGENTS_TEMPLATE, - ); - const soulTemplate = await loadTemplate( - DEFAULT_SOUL_FILENAME, - DEFAULT_SOUL_TEMPLATE, - ); - const toolsTemplate = await loadTemplate( - DEFAULT_TOOLS_FILENAME, - DEFAULT_TOOLS_TEMPLATE, - ); - const identityTemplate = await loadTemplate( - DEFAULT_IDENTITY_FILENAME, - DEFAULT_IDENTITY_TEMPLATE, - ); - const userTemplate = await loadTemplate( - DEFAULT_USER_FILENAME, - DEFAULT_USER_TEMPLATE, - ); + const agentsTemplate = await loadTemplate(DEFAULT_AGENTS_FILENAME, DEFAULT_AGENTS_TEMPLATE); + const soulTemplate = await loadTemplate(DEFAULT_SOUL_FILENAME, DEFAULT_SOUL_TEMPLATE); + const toolsTemplate = await loadTemplate(DEFAULT_TOOLS_FILENAME, DEFAULT_TOOLS_TEMPLATE); + const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME, DEFAULT_IDENTITY_TEMPLATE); + const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME, DEFAULT_USER_TEMPLATE); const heartbeatTemplate = await loadTemplate( DEFAULT_HEARTBEAT_FILENAME, DEFAULT_HEARTBEAT_TEMPLATE, @@ -308,9 +284,7 @@ export async function ensureAgentWorkspace(params?: { }; } -export async function loadWorkspaceBootstrapFiles( - dir: string, -): Promise { +export async function loadWorkspaceBootstrapFiles(dir: string): Promise { const resolvedDir = resolveUserPath(dir); const entries: Array<{ @@ -364,10 +338,7 @@ export async function loadWorkspaceBootstrapFiles( return result; } -const SUBAGENT_BOOTSTRAP_ALLOWLIST = new Set([ - DEFAULT_AGENTS_FILENAME, - DEFAULT_TOOLS_FILENAME, -]); +const SUBAGENT_BOOTSTRAP_ALLOWLIST = new Set([DEFAULT_AGENTS_FILENAME, DEFAULT_TOOLS_FILENAME]); export function filterBootstrapFilesForSession( files: WorkspaceBootstrapFile[], diff --git a/src/auto-reply/chunk.test.ts b/src/auto-reply/chunk.test.ts index 28e8f8c6707..17e98739c68 100644 --- a/src/auto-reply/chunk.test.ts +++ b/src/auto-reply/chunk.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - chunkMarkdownText, - chunkText, - resolveTextChunkLimit, -} from "./chunk.js"; +import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "./chunk.js"; function expectFencesBalanced(chunks: string[]) { for (const chunk of chunks) { @@ -32,10 +28,7 @@ type ChunkCase = { expected: string[]; }; -function runChunkCases( - chunker: (text: string, limit: number) => string[], - cases: ChunkCase[], -) { +function runChunkCases(chunker: (text: string, limit: number) => string[], cases: ChunkCase[]) { for (const { name, text, limit, expected } of cases) { it(name, () => { expect(chunker(text, limit)).toEqual(expected); @@ -84,21 +77,15 @@ describe("chunkText", () => { it("prefers breaking at a newline before the limit", () => { const text = `paragraph one line\n\nparagraph two starts here and continues`; const chunks = chunkText(text, 40); - expect(chunks).toEqual([ - "paragraph one line", - "paragraph two starts here and continues", - ]); + expect(chunks).toEqual(["paragraph one line", "paragraph two starts here and continues"]); }); it("otherwise breaks at the last whitespace under the limit", () => { - const text = - "This is a message that should break nicely near a word boundary."; + const text = "This is a message that should break nicely near a word boundary."; const chunks = chunkText(text, 30); expect(chunks[0].length).toBeLessThanOrEqual(30); expect(chunks[1].length).toBeLessThanOrEqual(30); - expect(chunks.join(" ").replace(/\s+/g, " ").trim()).toBe( - text.replace(/\s+/g, " ").trim(), - ); + expect(chunks.join(" ").replace(/\s+/g, " ").trim()).toBe(text.replace(/\s+/g, " ").trim()); }); it("falls back to a hard break when no whitespace is present", () => { diff --git a/src/auto-reply/chunk.ts b/src/auto-reply/chunk.ts index 02f84378986..abbd830a205 100644 --- a/src/auto-reply/chunk.ts +++ b/src/auto-reply/chunk.ts @@ -4,11 +4,7 @@ import type { ChannelId } from "../channels/plugins/types.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { - findFenceSpanAt, - isSafeFenceBreak, - parseFenceSpans, -} from "../markdown/fences.js"; +import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js"; import { normalizeAccountId } from "../routing/session-key.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js"; @@ -58,9 +54,7 @@ export function resolveTextChunkLimit( if (!provider || provider === INTERNAL_MESSAGE_CHANNEL) return undefined; const channelsConfig = cfg?.channels as Record | undefined; const providerConfig = (channelsConfig?.[provider] ?? - (cfg as Record | undefined)?.[provider]) as - | ProviderChunkConfig - | undefined; + (cfg as Record | undefined)?.[provider]) as ProviderChunkConfig | undefined; return resolveChunkLimitForProvider(providerConfig, accountId); })(); if (typeof providerOverride === "number" && providerOverride > 0) { @@ -96,12 +90,8 @@ export function chunkText(text: string, limit: number): string[] { } // If we broke on whitespace/newline, skip that separator; for hard breaks keep it. - const brokeOnSeparator = - breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); - const nextStart = Math.min( - remaining.length, - breakIdx + (brokeOnSeparator ? 1 : 0), - ); + const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); + const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); remaining = remaining.slice(nextStart).trimStart(); } @@ -145,10 +135,7 @@ export function chunkMarkdownText(text: string, limit: number): string[] { const maxIdxIfAlreadyNewline = limit - closeLine.length; let pickedNewline = false; - let lastNewline = remaining.lastIndexOf( - "\n", - Math.max(0, maxIdxIfAlreadyNewline - 1), - ); + let lastNewline = remaining.lastIndexOf("\n", Math.max(0, maxIdxIfAlreadyNewline - 1)); while (lastNewline !== -1) { const candidateBreak = lastNewline + 1; if (candidateBreak < minProgressIdx) break; @@ -173,27 +160,19 @@ export function chunkMarkdownText(text: string, limit: number): string[] { const fenceAtBreak = findFenceSpanAt(spans, breakIdx); fenceToSplit = - fenceAtBreak && fenceAtBreak.start === initialFence.start - ? fenceAtBreak - : undefined; + fenceAtBreak && fenceAtBreak.start === initialFence.start ? fenceAtBreak : undefined; } let rawChunk = remaining.slice(0, breakIdx); if (!rawChunk) break; - const brokeOnSeparator = - breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); - const nextStart = Math.min( - remaining.length, - breakIdx + (brokeOnSeparator ? 1 : 0), - ); + const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]); + const nextStart = Math.min(remaining.length, breakIdx + (brokeOnSeparator ? 1 : 0)); let next = remaining.slice(nextStart); if (fenceToSplit) { const closeLine = `${fenceToSplit.indent}${fenceToSplit.marker}`; - rawChunk = rawChunk.endsWith("\n") - ? `${rawChunk}${closeLine}` - : `${rawChunk}\n${closeLine}`; + rawChunk = rawChunk.endsWith("\n") ? `${rawChunk}${closeLine}` : `${rawChunk}\n${closeLine}`; next = `${fenceToSplit.openLine}\n${next}`; } else { next = stripLeadingNewlines(next); @@ -213,13 +192,9 @@ function stripLeadingNewlines(value: string): string { return i > 0 ? value.slice(i) : value; } -function pickSafeBreakIndex( - window: string, - spans: ReturnType, -): number { - const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints( - window, - (index) => isSafeFenceBreak(spans, index), +function pickSafeBreakIndex(window: string, spans: ReturnType): number { + const { lastNewline, lastWhitespace } = scanParenAwareBreakpoints(window, (index) => + isSafeFenceBreak(spans, index), ); if (lastNewline > 0) return lastNewline; diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index e9b772a4ea8..39c752bc5ac 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -14,10 +14,7 @@ export type CommandAuthorization = { to?: string; }; -function resolveProviderFromContext( - ctx: MsgContext, - cfg: ClawdbotConfig, -): ChannelId | undefined { +function resolveProviderFromContext(ctx: MsgContext, cfg: ClawdbotConfig): ChannelId | undefined { const direct = normalizeChannelId(ctx.Provider) ?? normalizeChannelId(ctx.Surface) ?? @@ -79,12 +76,9 @@ export function resolveCommandAuthorization(params: { allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [], }); const allowAll = - allowFromList.length === 0 || - allowFromList.some((entry) => entry.trim() === "*"); + allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*"); - const ownerCandidates = allowAll - ? [] - : allowFromList.filter((entry) => entry !== "*"); + const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*"); if (!allowAll && ownerCandidates.length === 0 && to) { const normalizedTo = formatAllowFromList({ dock, diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 307d4640d58..879307f27c6 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -1,8 +1,5 @@ import { listChannelDocks } from "../channels/dock.js"; -import type { - ChatCommandDefinition, - CommandScope, -} from "./commands-registry.types.js"; +import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js"; type DefineChatCommandInput = { key: string; @@ -14,17 +11,12 @@ type DefineChatCommandInput = { scope?: CommandScope; }; -function defineChatCommand( - command: DefineChatCommandInput, -): ChatCommandDefinition { - const aliases = ( - command.textAliases ?? (command.textAlias ? [command.textAlias] : []) - ) +function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition { + const aliases = (command.textAliases ?? (command.textAlias ? [command.textAlias] : [])) .map((alias) => alias.trim()) .filter(Boolean); const scope = - command.scope ?? - (command.nativeName ? (aliases.length ? "both" : "native") : "text"); + command.scope ?? (command.nativeName ? (aliases.length ? "both" : "native") : "text"); return { key: command.key, nativeName: command.nativeName, @@ -35,18 +27,12 @@ function defineChatCommand( }; } -function registerAlias( - commands: ChatCommandDefinition[], - key: string, - ...aliases: string[] -): void { +function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliases: string[]): void { const command = commands.find((entry) => entry.key === key); if (!command) { throw new Error(`registerAlias: unknown command key: ${key}`); } - const existing = new Set( - command.textAliases.map((alias) => alias.trim().toLowerCase()), - ); + const existing = new Set(command.textAliases.map((alias) => alias.trim().toLowerCase())); for (const alias of aliases) { const trimmed = alias.trim(); if (!trimmed) continue; diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index c07d226e56f..8112c49f562 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -95,9 +95,7 @@ describe("commands registry", () => { }); it("normalizes telegram-style command mentions for the current bot", () => { - expect( - normalizeCommandBody("/help@clawdbot", { botUsername: "clawdbot" }), - ).toBe("/help"); + expect(normalizeCommandBody("/help@clawdbot", { botUsername: "clawdbot" })).toBe("/help"); expect( normalizeCommandBody("/help@clawdbot args", { botUsername: "clawdbot", @@ -111,8 +109,8 @@ describe("commands registry", () => { }); it("keeps telegram-style command mentions for other bots", () => { - expect( - normalizeCommandBody("/help@otherbot", { botUsername: "clawdbot" }), - ).toBe("/help@otherbot"); + expect(normalizeCommandBody("/help@otherbot", { botUsername: "clawdbot" })).toBe( + "/help@otherbot", + ); }); }); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index bd74c5073a9..46f51d64a59 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -1,8 +1,5 @@ import type { ClawdbotConfig } from "../config/types.js"; -import { - CHAT_COMMANDS, - getNativeCommandSurfaces, -} from "./commands-registry.data.js"; +import { CHAT_COMMANDS, getNativeCommandSurfaces } from "./commands-registry.data.js"; import type { ChatCommandDefinition, CommandDetection, @@ -56,35 +53,28 @@ export function listChatCommands(): ChatCommandDefinition[] { return [...CHAT_COMMANDS]; } -export function isCommandEnabled( - cfg: ClawdbotConfig, - commandKey: string, -): boolean { +export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boolean { if (commandKey === "config") return cfg.commands?.config === true; if (commandKey === "debug") return cfg.commands?.debug === true; if (commandKey === "bash") return cfg.commands?.bash === true; return true; } -export function listChatCommandsForConfig( - cfg: ClawdbotConfig, -): ChatCommandDefinition[] { +export function listChatCommandsForConfig(cfg: ClawdbotConfig): ChatCommandDefinition[] { return CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key)); } export function listNativeCommandSpecs(): NativeCommandSpec[] { - return CHAT_COMMANDS.filter( - (command) => command.scope !== "text" && command.nativeName, - ).map((command) => ({ - name: command.nativeName ?? command.key, - description: command.description, - acceptsArgs: Boolean(command.acceptsArgs), - })); + return CHAT_COMMANDS.filter((command) => command.scope !== "text" && command.nativeName).map( + (command) => ({ + name: command.nativeName ?? command.key, + description: command.description, + acceptsArgs: Boolean(command.acceptsArgs), + }), + ); } -export function listNativeCommandSpecsForConfig( - cfg: ClawdbotConfig, -): NativeCommandSpec[] { +export function listNativeCommandSpecsForConfig(cfg: ClawdbotConfig): NativeCommandSpec[] { return listChatCommandsForConfig(cfg) .filter((command) => command.scope !== "text" && command.nativeName) .map((command) => ({ @@ -94,14 +84,10 @@ export function listNativeCommandSpecsForConfig( })); } -export function findCommandByNativeName( - name: string, -): ChatCommandDefinition | undefined { +export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined { const normalized = name.trim().toLowerCase(); return CHAT_COMMANDS.find( - (command) => - command.scope !== "text" && - command.nativeName?.toLowerCase() === normalized, + (command) => command.scope !== "text" && command.nativeName?.toLowerCase() === normalized, ); } @@ -110,16 +96,12 @@ export function buildCommandText(commandName: string, args?: string): string { return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`; } -export function normalizeCommandBody( - raw: string, - options?: CommandNormalizeOptions, -): string { +export function normalizeCommandBody(raw: string, options?: CommandNormalizeOptions): string { const trimmed = raw.trim(); if (!trimmed.startsWith("/")) return trimmed; const newline = trimmed.indexOf("\n"); - const singleLine = - newline === -1 ? trimmed : trimmed.slice(0, newline).trim(); + const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim(); const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/); const normalized = colonMatch @@ -151,9 +133,7 @@ export function normalizeCommandBody( if (!tokenSpec) return commandBody; if (rest && !tokenSpec.acceptsArgs) return commandBody; const normalizedRest = rest?.trimStart(); - return normalizedRest - ? `${tokenSpec.canonical} ${normalizedRest}` - : tokenSpec.canonical; + return normalizedRest ? `${tokenSpec.canonical} ${normalizedRest}` : tokenSpec.canonical; } export function isCommandMessage(raw: string): boolean { @@ -181,9 +161,7 @@ export function getCommandDetection(_cfg?: ClawdbotConfig): CommandDetection { } cachedDetection = { exact, - regex: patterns.length - ? new RegExp(`^(?:${patterns.join("|")})$`, "i") - : /$^/, + regex: patterns.length ? new RegExp(`^(?:${patterns.join("|")})$`, "i") : /$^/, }; return cachedDetection; } @@ -225,9 +203,7 @@ export function isNativeCommandSurface(surface?: string): boolean { return getNativeCommandSurfaces().has(surface.toLowerCase()); } -export function shouldHandleTextCommands( - params: ShouldHandleTextCommandsParams, -): boolean { +export function shouldHandleTextCommands(params: ShouldHandleTextCommandsParams): boolean { if (params.commandSource === "native") return true; if (params.cfg.commands?.text !== false) return true; return !isNativeCommandSurface(params.surface); diff --git a/src/auto-reply/envelope.test.ts b/src/auto-reply/envelope.test.ts index 12e8b2ce27c..e5b0f145519 100644 --- a/src/auto-reply/envelope.test.ts +++ b/src/auto-reply/envelope.test.ts @@ -19,9 +19,7 @@ describe("formatAgentEnvelope", () => { process.env.TZ = originalTz; - expect(body).toBe( - "[WebChat user1 mac-mini 10.0.0.5 2025-01-02T03:04Z] hello", - ); + expect(body).toBe("[WebChat user1 mac-mini 10.0.0.5 2025-01-02T03:04Z] hello"); }); it("formats timestamps in UTC regardless of local timezone", () => { diff --git a/src/auto-reply/group-activation.ts b/src/auto-reply/group-activation.ts index fd64194da75..7dcd2e69671 100644 --- a/src/auto-reply/group-activation.ts +++ b/src/auto-reply/group-activation.ts @@ -2,9 +2,7 @@ import { normalizeCommandBody } from "./commands-registry.js"; export type GroupActivationMode = "mention" | "always"; -export function normalizeGroupActivation( - raw?: string | null, -): GroupActivationMode | undefined { +export function normalizeGroupActivation(raw?: string | null): GroupActivationMode | undefined { const value = raw?.trim().toLowerCase(); if (value === "mention") return "mention"; if (value === "always") return "always"; diff --git a/src/auto-reply/heartbeat.test.ts b/src/auto-reply/heartbeat.test.ts index 6bc066d1da0..b9141605f86 100644 --- a/src/auto-reply/heartbeat.test.ts +++ b/src/auto-reply/heartbeat.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - DEFAULT_HEARTBEAT_ACK_MAX_CHARS, - stripHeartbeatToken, -} from "./heartbeat.js"; +import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "./heartbeat.js"; import { HEARTBEAT_TOKEN } from "./tokens.js"; describe("stripHeartbeatToken", () => { @@ -18,26 +15,20 @@ describe("stripHeartbeatToken", () => { text: "", didStrip: false, }); - expect(stripHeartbeatToken(HEARTBEAT_TOKEN, { mode: "heartbeat" })).toEqual( - { - shouldSkip: true, - text: "", - didStrip: true, - }, - ); - }); - - it("drops heartbeats with small junk in heartbeat mode", () => { - expect( - stripHeartbeatToken("HEARTBEAT_OK 🦞", { mode: "heartbeat" }), - ).toEqual({ + expect(stripHeartbeatToken(HEARTBEAT_TOKEN, { mode: "heartbeat" })).toEqual({ shouldSkip: true, text: "", didStrip: true, }); - expect( - stripHeartbeatToken(`🦞 ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }), - ).toEqual({ + }); + + it("drops heartbeats with small junk in heartbeat mode", () => { + expect(stripHeartbeatToken("HEARTBEAT_OK 🦞", { mode: "heartbeat" })).toEqual({ + shouldSkip: true, + text: "", + didStrip: true, + }); + expect(stripHeartbeatToken(`🦞 ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({ shouldSkip: true, text: "", didStrip: true, @@ -45,9 +36,7 @@ describe("stripHeartbeatToken", () => { }); it("drops short remainder in heartbeat mode", () => { - expect( - stripHeartbeatToken(`ALERT ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }), - ).toEqual({ + expect(stripHeartbeatToken(`ALERT ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({ shouldSkip: true, text: "", didStrip: true, @@ -56,9 +45,7 @@ describe("stripHeartbeatToken", () => { it("keeps heartbeat replies when remaining content exceeds threshold", () => { const long = "A".repeat(DEFAULT_HEARTBEAT_ACK_MAX_CHARS + 1); - expect( - stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }), - ).toEqual({ + expect(stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({ shouldSkip: false, text: long, didStrip: true, @@ -66,16 +53,12 @@ describe("stripHeartbeatToken", () => { }); it("strips token at edges for normal messages", () => { - expect( - stripHeartbeatToken(`${HEARTBEAT_TOKEN} hello`, { mode: "message" }), - ).toEqual({ + expect(stripHeartbeatToken(`${HEARTBEAT_TOKEN} hello`, { mode: "message" })).toEqual({ shouldSkip: false, text: "hello", didStrip: true, }); - expect( - stripHeartbeatToken(`hello ${HEARTBEAT_TOKEN}`, { mode: "message" }), - ).toEqual({ + expect(stripHeartbeatToken(`hello ${HEARTBEAT_TOKEN}`, { mode: "message" })).toEqual({ shouldSkip: false, text: "hello", didStrip: true, @@ -95,9 +78,7 @@ describe("stripHeartbeatToken", () => { }); it("strips HTML-wrapped heartbeat tokens", () => { - expect( - stripHeartbeatToken(`${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }), - ).toEqual({ + expect(stripHeartbeatToken(`${HEARTBEAT_TOKEN}`, { mode: "heartbeat" })).toEqual({ shouldSkip: true, text: "", didStrip: true, @@ -105,9 +86,7 @@ describe("stripHeartbeatToken", () => { }); it("strips markdown-wrapped heartbeat tokens", () => { - expect( - stripHeartbeatToken(`**${HEARTBEAT_TOKEN}**`, { mode: "heartbeat" }), - ).toEqual({ + expect(stripHeartbeatToken(`**${HEARTBEAT_TOKEN}**`, { mode: "heartbeat" })).toEqual({ shouldSkip: true, text: "", didStrip: true, diff --git a/src/auto-reply/heartbeat.ts b/src/auto-reply/heartbeat.ts index e1d4f60420d..47e34b04e6c 100644 --- a/src/auto-reply/heartbeat.ts +++ b/src/auto-reply/heartbeat.ts @@ -54,9 +54,7 @@ export function stripHeartbeatToken( const mode: StripHeartbeatMode = opts.mode ?? "message"; const maxAckCharsRaw = opts.maxAckChars; const parsedAckChars = - typeof maxAckCharsRaw === "string" - ? Number(maxAckCharsRaw) - : maxAckCharsRaw; + typeof maxAckCharsRaw === "string" ? Number(maxAckCharsRaw) : maxAckCharsRaw; const maxAckChars = Math.max( 0, typeof parsedAckChars === "number" && Number.isFinite(parsedAckChars) @@ -77,9 +75,7 @@ export function stripHeartbeatToken( .replace(/[*`~_]+$/, ""); const trimmedNormalized = stripMarkup(trimmed); - const hasToken = - trimmed.includes(HEARTBEAT_TOKEN) || - trimmedNormalized.includes(HEARTBEAT_TOKEN); + const hasToken = trimmed.includes(HEARTBEAT_TOKEN) || trimmedNormalized.includes(HEARTBEAT_TOKEN); if (!hasToken) { return { shouldSkip: false, text: trimmed, didStrip: false }; } @@ -87,9 +83,7 @@ export function stripHeartbeatToken( const strippedOriginal = stripTokenAtEdges(trimmed); const strippedNormalized = stripTokenAtEdges(trimmedNormalized); const picked = - strippedOriginal.didStrip && strippedOriginal.text - ? strippedOriginal - : strippedNormalized; + strippedOriginal.didStrip && strippedOriginal.text ? strippedOriginal : strippedNormalized; if (!picked.didStrip) { return { shouldSkip: false, text: trimmed, didStrip: false }; } diff --git a/src/auto-reply/media-note.ts b/src/auto-reply/media-note.ts index 7917c9d7a5b..d08e7011d06 100644 --- a/src/auto-reply/media-note.ts +++ b/src/auto-reply/media-note.ts @@ -18,9 +18,7 @@ function formatMediaAttachedLine(params: { } export function buildInboundMediaNote(ctx: MsgContext): string | undefined { - const pathsFromArray = Array.isArray(ctx.MediaPaths) - ? ctx.MediaPaths - : undefined; + const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : undefined; const paths = pathsFromArray && pathsFromArray.length > 0 ? pathsFromArray diff --git a/src/auto-reply/model.ts b/src/auto-reply/model.ts index c40a618ddbc..46ea44db159 100644 --- a/src/auto-reply/model.ts +++ b/src/auto-reply/model.ts @@ -17,9 +17,7 @@ export function extractModelDirective( /(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i, ); - const aliases = (options?.aliases ?? []) - .map((alias) => alias.trim()) - .filter(Boolean); + const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean); const aliasMatch = modelMatch || aliases.length === 0 ? null @@ -41,9 +39,7 @@ export function extractModelDirective( rawProfile = parts.slice(1).join("@").trim() || undefined; } - const cleaned = match - ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() - : body.trim(); + const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim(); return { cleaned, diff --git a/src/auto-reply/reply.block-streaming.test.ts b/src/auto-reply/reply.block-streaming.test.ts index de8139fe4fe..21b892b46d6 100644 --- a/src/auto-reply/reply.block-streaming.test.ts +++ b/src/auto-reply/reply.block-streaming.test.ts @@ -6,19 +6,14 @@ import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.j import { loadModelCatalog } from "../agents/model-catalog.js"; import { getReplyFromConfig } from "./reply.js"; -type RunEmbeddedPiAgent = - typeof import("../agents/pi-embedded.js").runEmbeddedPiAgent; +type RunEmbeddedPiAgent = typeof import("../agents/pi-embedded.js").runEmbeddedPiAgent; type RunEmbeddedPiAgentParams = Parameters[0]; const piEmbeddedMock = vi.hoisted(() => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn< - ReturnType, - Parameters - >(), + runEmbeddedPiAgent: vi.fn, Parameters>(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); diff --git a/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts b/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts index 188e04e82e9..0fe82b0d77e 100644 --- a/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.accepts-thinking-xhigh-codex-models.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -84,9 +83,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("Thinking level set to xhigh."); }); }); @@ -113,9 +110,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("Thinking level set to xhigh."); }); }); @@ -142,9 +137,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain( 'Thinking level "xhigh" is only supported for openai/gpt-5.2, openai-codex/gpt-5.2-codex or openai-codex/gpt-5.1-codex.', ); diff --git a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts index b3c3e80404d..0a96e53a69a 100644 --- a/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.applies-inline-reasoning-mixed-messages-acks-immediately.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -98,9 +97,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("done"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); diff --git a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts index 39bdf9ea2ef..953e5e4d870 100644 --- a/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.defaults-think-low-reasoning-capable-models-no.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -263,9 +262,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("done"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); }); diff --git a/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts b/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts index 8488b35aa3b..d1ff259ad48 100644 --- a/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.ignores-inline-model-uses-default-model.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -95,9 +94,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("done"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]; diff --git a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts index 4ade6a1a61b..c959ce36286 100644 --- a/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.lists-allowlisted-models-model-list.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); diff --git a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts index 5531e49b2a5..a2e226485b9 100644 --- a/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.prefers-alias-matches-fuzzy-selection-is-ambiguous.test.ts @@ -14,8 +14,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -96,9 +95,7 @@ describe("directive behavior", () => { baseUrl: "http://127.0.0.1:1234/v1", apiKey: "lmstudio", api: "openai-responses", - models: [ - { id: "kimi-k2-0905-preview", name: "Kimi K2 (Local)" }, - ], + models: [{ id: "kimi-k2-0905-preview", name: "Kimi K2 (Local)" }], }, }, }, @@ -188,9 +185,7 @@ describe("directive behavior", () => { ); const events = drainSystemEvents(MAIN_SESSION_KEY); - expect(events).toContain( - "Model switched to Opus (anthropic/claude-opus-4-5).", - ); + expect(events).toContain("Model switched to Opus (anthropic/claude-opus-4-5)."); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts index 67bc81d0ddd..3e5699204cb 100644 --- a/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.requires-per-agent-allowlist-addition-global.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); diff --git a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts b/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts index 1a5f0a47eab..3209a80c3b4 100644 --- a/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.returns-status-alongside-directive-only-acks.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -95,9 +94,7 @@ describe("directive behavior", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Elevated mode disabled."); expect(text).toContain("Session: agent:main:main"); - const optionsLine = text - ?.split("\n") - .find((line) => line.trim().startsWith("⚙️")); + const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️")); expect(optionsLine).toBeTruthy(); expect(optionsLine).not.toContain("elevated"); diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts index f6112c9b524..554ed71d230 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-elevated-level-as-off-after.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -183,9 +182,7 @@ describe("directive behavior", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - const optionsLine = text - ?.split("\n") - .find((line) => line.trim().startsWith("⚙️")); + const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️")); expect(optionsLine).toBeTruthy(); expect(optionsLine).toContain("elevated"); diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index 09c6b61ea04..189238dc6b5 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -181,9 +180,7 @@ describe("directive behavior", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Elevated mode disabled."); - const optionsLine = text - ?.split("\n") - .find((line) => line.trim().startsWith("⚙️")); + const optionsLine = text?.split("\n").find((line) => line.trim().startsWith("⚙️")); expect(optionsLine).toBeTruthy(); expect(optionsLine).not.toContain("elevated"); diff --git a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts index 86a174d723b..cfb6d8e8021 100644 --- a/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.supports-fuzzy-model-matches-model-directive.test.ts @@ -12,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -217,9 +216,7 @@ describe("directive behavior", () => { baseUrl: "http://127.0.0.1:1234/v1", apiKey: "lmstudio", api: "openai-responses", - models: [ - { id: "minimax-m2.1-gs32", name: "MiniMax M2.1 GS32" }, - ], + models: [{ id: "minimax-m2.1-gs32", name: "MiniMax M2.1 GS32" }], }, }, }, diff --git a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts index 3287052afd2..8844bf846b8 100644 --- a/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.updates-tool-verbose-during-flight-run-toggle.test.ts @@ -3,11 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; -import { - loadSessionStore, - resolveSessionKey, - saveSessionStore, -} from "../config/sessions.js"; +import { loadSessionStore, resolveSessionKey, saveSessionStore } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; const MAIN_SESSION_KEY = "agent:main:main"; @@ -16,8 +12,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -114,9 +109,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("done"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); }); @@ -189,9 +182,7 @@ describe("directive behavior", () => { }, ); - const texts = (Array.isArray(res) ? res : [res]) - .map((entry) => entry?.text) - .filter(Boolean); + const texts = (Array.isArray(res) ? res : [res]).map((entry) => entry?.text).filter(Boolean); expect(texts).toContain("done"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); }); diff --git a/src/auto-reply/reply.directive.parse.test.ts b/src/auto-reply/reply.directive.parse.test.ts index 6d036ccbca7..cbbea25d0db 100644 --- a/src/auto-reply/reply.directive.parse.test.ts +++ b/src/auto-reply/reply.directive.parse.test.ts @@ -187,10 +187,7 @@ describe("directive parsing", () => { }); it("preserves newlines when stripping reply tags", () => { - const res = extractReplyToTag( - "line 1\nline 2 [[reply_to_current]]\n\nline 3", - "msg-2", - ); + const res = extractReplyToTag("line 1\nline 2 [[reply_to_current]]\n\nline 3", "msg-2"); expect(res.replyToId).toBe("msg-2"); expect(res.cleaned).toBe("line 1\nline 2\n\nline 3"); }); diff --git a/src/auto-reply/reply.heartbeat-typing.test.ts b/src/auto-reply/reply.heartbeat-typing.test.ts index 32abb91686a..1818f1c74b2 100644 --- a/src/auto-reply/reply.heartbeat-typing.test.ts +++ b/src/auto-reply/reply.heartbeat-typing.test.ts @@ -26,8 +26,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); diff --git a/src/auto-reply/reply.media-note.test.ts b/src/auto-reply/reply.media-note.test.ts index 88a04d34401..4f78c5c6678 100644 --- a/src/auto-reply/reply.media-note.test.ts +++ b/src/auto-reply/reply.media-note.test.ts @@ -10,8 +10,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -34,8 +33,7 @@ async function withTempHome(fn: (home: string) => Promise): Promise { }, { env: { - CLAWDBOT_BUNDLED_SKILLS_DIR: (home) => - path.join(home, "bundled-skills"), + CLAWDBOT_BUNDLED_SKILLS_DIR: (home) => path.join(home, "bundled-skills"), }, prefix: "clawdbot-media-note-", }, diff --git a/src/auto-reply/reply.queue.test.ts b/src/auto-reply/reply.queue.test.ts index 6594ca2303d..15078c5e69c 100644 --- a/src/auto-reply/reply.queue.test.ts +++ b/src/auto-reply/reply.queue.test.ts @@ -14,8 +14,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -105,11 +104,7 @@ describe("queue followups", () => { await Promise.resolve(); expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2); - expect( - prompts.some((p) => - p.includes("[Queued messages while agent was busy]"), - ), - ).toBe(true); + expect(prompts.some((p) => p.includes("[Queued messages while agent was busy]"))).toBe(true); }); }); @@ -132,23 +127,11 @@ describe("queue followups", () => { drop: "summarize", }); - await getReplyFromConfig( - { Body: "one", From: "+1002", To: "+2000" }, - {}, - cfg, - ); - await getReplyFromConfig( - { Body: "two", From: "+1002", To: "+2000" }, - {}, - cfg, - ); + await getReplyFromConfig({ Body: "one", From: "+1002", To: "+2000" }, {}, cfg); + await getReplyFromConfig({ Body: "two", From: "+1002", To: "+2000" }, {}, cfg); vi.mocked(isEmbeddedPiRunActive).mockReturnValue(false); - await getReplyFromConfig( - { Body: "three", From: "+1002", To: "+2000" }, - {}, - cfg, - ); + await getReplyFromConfig({ Body: "three", From: "+1002", To: "+2000" }, {}, cfg); await vi.runAllTimersAsync(); await Promise.resolve(); diff --git a/src/auto-reply/reply.raw-body.test.ts b/src/auto-reply/reply.raw-body.test.ts index fc1508ba2f4..cebe771b519 100644 --- a/src/auto-reply/reply.raw-body.test.ts +++ b/src/auto-reply/reply.raw-body.test.ts @@ -9,8 +9,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -228,11 +227,8 @@ describe("RawBody directive parsing", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; - expect(prompt).toContain( - "[Chat messages since your last reply - for context]", - ); + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(prompt).toContain("[Chat messages since your last reply - for context]"); expect(prompt).toContain("Peter: hello"); expect(prompt).toContain("status please"); expect(prompt).not.toContain("/think:high"); diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.test.ts b/src/auto-reply/reply.triggers.group-intro-prompts.test.ts index b6401ebbc0d..c3b238529e9 100644 --- a/src/auto-reply/reply.triggers.group-intro-prompts.test.ts +++ b/src/auto-reply/reply.triggers.group-intro-prompts.test.ts @@ -7,8 +7,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -48,10 +47,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -126,8 +122,7 @@ describe("group intro prompts", () => { expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); const extraSystemPrompt = - vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] - ?.extraSystemPrompt ?? ""; + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? ""; expect(extraSystemPrompt).toBe( `You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, ); @@ -158,8 +153,7 @@ describe("group intro prompts", () => { expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); const extraSystemPrompt = - vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] - ?.extraSystemPrompt ?? ""; + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? ""; expect(extraSystemPrompt).toBe( `You are replying inside the WhatsApp group "Ops". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant). ${groupParticipationNote} Address the specific sender noted in the message context.`, ); @@ -190,8 +184,7 @@ describe("group intro prompts", () => { expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); const extraSystemPrompt = - vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] - ?.extraSystemPrompt ?? ""; + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? ""; expect(extraSystemPrompt).toBe( `You are replying inside the Telegram group "Dev Chat". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, ); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts index be37d2cdc73..c01a063395f 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -163,9 +159,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const extra = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ?? - ""; + const extra = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.extraSystemPrompt ?? ""; expect(extra).toContain("Test Group"); expect(extra).toContain("Activation: always-on"); }); @@ -207,8 +201,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("hello"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("A new session was started via /new or /reset"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts index a8bbcc53dbe..f4bbc5cbb7e 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-approved-sender-toggle-elevated-mode.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const MAIN_SESSION_KEY = "agent:main:main"; @@ -135,10 +131,7 @@ describe("trigger handling", () => { expect(text).toContain("Elevated mode enabled"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { elevatedLevel?: string } - >; + const store = JSON.parse(storeRaw) as Record; expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on"); }); }); @@ -180,10 +173,7 @@ describe("trigger handling", () => { expect(text).toContain("tools.elevated.enabled"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { elevatedLevel?: string } - >; + const store = JSON.parse(storeRaw) as Record; expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined(); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts index 752d5cd769a..692ad89d2c4 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-elevated-off-groups-without-mention.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { loadSessionStore } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; @@ -146,9 +142,7 @@ describe("trigger handling", () => { expect(text).toContain("Elevated mode disabled."); const store = loadSessionStore(cfg.session.store); - expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe( - "off", - ); + expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off"); }); }); it("allows elevated directive in groups when mentioned", async () => { @@ -191,13 +185,8 @@ describe("trigger handling", () => { expect(text).toContain("Elevated mode enabled"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { elevatedLevel?: string } - >; - expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe( - "on", - ); + const store = JSON.parse(storeRaw) as Record; + expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on"); }); }); it("allows elevated directive in direct chats without mentions", async () => { @@ -237,10 +226,7 @@ describe("trigger handling", () => { expect(text).toContain("Elevated mode enabled"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { elevatedLevel?: string } - >; + const store = JSON.parse(storeRaw) as Record; expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts index a61d3b70906..73b5e5656f8 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -199,8 +195,7 @@ describe("trigger handling", () => { expect(String(blockReplies[0]?.text ?? "")).toContain("Model:"); expect(replies.length).toBe(1); expect(replies[0]?.text).toBe("agent says hi"); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/status"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts index 191eed77849..a5f91ff1ea4 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts @@ -7,8 +7,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -48,10 +47,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -124,8 +120,7 @@ describe("trigger handling", () => { expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Slash commands"); expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/commands"); expect(text).toBe("ok"); }); @@ -158,8 +153,7 @@ describe("trigger handling", () => { expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Identity"); expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/whoami"); expect(text).toBe("ok"); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts index 73bd4d4e1d9..58610203530 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.ignores-inline-elevated-directive-unapproved-sender.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const MAIN_SESSION_KEY = "agent:main:main"; @@ -173,10 +169,7 @@ describe("trigger handling", () => { expect(text).toContain("Elevated mode enabled"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { elevatedLevel?: string } - >; + const store = JSON.parse(storeRaw) as Record; expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on"); }); }); @@ -215,9 +208,7 @@ describe("trigger handling", () => { }); it("returns a context overflow fallback when the embedded agent throws", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockRejectedValue( - new Error("Context window exceeded"), - ); + vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("Context window exceeded")); const res = await getReplyFromConfig( { diff --git a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts index 2523fe9971d..2ee12491ead 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; import { HEARTBEAT_TOKEN } from "./tokens.js"; @@ -101,9 +97,7 @@ afterEach(() => { describe("trigger handling", () => { it("includes the error cause when the embedded agent throws", async () => { await withTempHome(async (home) => { - vi.mocked(runEmbeddedPiAgent).mockRejectedValue( - new Error("sandbox is not defined"), - ); + vi.mocked(runEmbeddedPiAgent).mockRejectedValue(new Error("sandbox is not defined")); const res = await getReplyFromConfig( { @@ -221,12 +215,11 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Group activation set to always"); - const store = JSON.parse( - await fs.readFile(cfg.session.store, "utf-8"), - ) as Record; - expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe( - "always", - ); + const store = JSON.parse(await fs.readFile(cfg.session.store, "utf-8")) as Record< + string, + { groupActivation?: string } + >; + expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts index 3b8e9a8746b..2233b8366a8 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.keeps-inline-status-unauthorized-senders.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const MAIN_SESSION_KEY = "agent:main:main"; @@ -135,8 +131,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; // Not allowlisted: inline /status is treated as plain text and is not stripped. expect(prompt).toContain("/status"); }); @@ -178,8 +173,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("/help"); }); }); @@ -232,10 +226,7 @@ describe("trigger handling", () => { expect(text).toContain("Send policy set to off"); const storeRaw = await fs.readFile(cfg.session.store, "utf-8"); - const store = JSON.parse(storeRaw) as Record< - string, - { sendPolicy?: string } - >; + const store = JSON.parse(storeRaw) as Record; expect(store[MAIN_SESSION_KEY]?.sendPolicy).toBe("deny"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts index 4a30910ad75..f9e6901860a 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.reports-active-auth-profile-key-snippet-status.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { resolveSessionKey } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; @@ -193,8 +189,7 @@ describe("trigger handling", () => { // stripped from the prompt; the remaining text continues through the agent. expect(blockReplies.length).toBe(1); expect(String(blockReplies[0]?.text ?? "").length).toBeGreaterThan(0); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/status"); }); }); @@ -225,8 +220,7 @@ describe("trigger handling", () => { expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain("Help"); expect(runEmbeddedPiAgent).toHaveBeenCalled(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).not.toContain("/help"); expect(text).toBe("ok"); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts index 8e80a188693..7ea06c52343 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -102,10 +101,7 @@ afterEach(() => { describe("trigger handling", () => { it("runs /compact as a gated command", async () => { await withTempHome(async (home) => { - const storePath = join( - tmpdir(), - `clawdbot-session-test-${Date.now()}.json`, - ); + const storePath = join(tmpdir(), `clawdbot-session-test-${Date.now()}.json`); vi.mocked(compactEmbeddedPiSession).mockResolvedValue({ ok: true, compacted: true, @@ -182,8 +178,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("Give me the status"); expect(prompt).not.toContain("/thinking high"); expect(prompt).not.toContain("/think high"); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts index 2b7b7845697..f9c5d239211 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -135,8 +131,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("hello"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain("A new session was started via /new or /reset"); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts index 422fd1c8924..c028d5c0566 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-endpoint-default-model-status-not-configured.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -182,10 +178,7 @@ describe("trigger handling", () => { cfg, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect( - text?.startsWith("⚙️ Restarting") || - text?.startsWith("⚠️ Restart failed"), - ).toBe(true); + expect(text?.startsWith("⚙️ Restarting") || text?.startsWith("⚠️ Restart failed")).toBe(true); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts index 08e6fba9884..248ff9e42e2 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.shows-quick-model-picker-grouped-by-model.test.ts @@ -9,8 +9,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -50,10 +49,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { loadSessionStore } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; @@ -119,12 +115,8 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; const normalized = normalizeTestText(text ?? ""); - expect(normalized).toContain( - "Pick: /model <#> or /model ", - ); - expect(normalized).toContain( - "1) claude-opus-4-5 — anthropic, openrouter", - ); + expect(normalized).toContain("Pick: /model <#> or /model "); + expect(normalized).toContain("1) claude-opus-4-5 — anthropic, openrouter"); expect(normalized).toContain("3) gpt-5.2 — openai, openai-codex"); expect(normalized).toContain("More: /model status"); expect(normalized).not.toContain("reasoning"); @@ -202,9 +194,7 @@ describe("trigger handling", () => { const store = loadSessionStore(cfg.session.store); expect(store[sessionKey]?.providerOverride).toBe("openrouter"); - expect(store[sessionKey]?.modelOverride).toBe( - "anthropic/claude-opus-4-5", - ); + expect(store[sessionKey]?.modelOverride).toBe("anthropic/claude-opus-4-5"); }); }); it("selects a model by index via /model <#>", async () => { @@ -227,9 +217,7 @@ describe("trigger handling", () => { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(normalizeTestText(text ?? "")).toContain( - "Model set to openai/gpt-5.2", - ); + expect(normalizeTestText(text ?? "")).toContain("Model set to openai/gpt-5.2"); const store = loadSessionStore(cfg.session.store); expect(store[sessionKey]?.providerOverride).toBe("openai"); diff --git a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts index 96b7bbec25f..d0d6a5fc1e5 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -50,15 +49,9 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js"; -import { - resolveAgentIdFromSessionKey, - resolveSessionKey, -} from "../config/sessions.js"; +import { resolveAgentIdFromSessionKey, resolveSessionKey } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; const _MAIN_SESSION_KEY = "agent:main:main"; @@ -104,90 +97,80 @@ afterEach(() => { }); describe("trigger handling", () => { - it( - "stages inbound media into the sandbox workspace", - { timeout: 15_000 }, - async () => { - await withTempHome(async (home) => { - const inboundDir = join(home, ".clawdbot", "media", "inbound"); - await fs.mkdir(inboundDir, { recursive: true }); - const mediaPath = join(inboundDir, "photo.jpg"); - await fs.writeFile(mediaPath, "test"); + it("stages inbound media into the sandbox workspace", { timeout: 15_000 }, async () => { + await withTempHome(async (home) => { + const inboundDir = join(home, ".clawdbot", "media", "inbound"); + await fs.mkdir(inboundDir, { recursive: true }); + const mediaPath = join(inboundDir, "photo.jpg"); + await fs.writeFile(mediaPath, "test"); - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); - - const cfg = { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "clawd"), - sandbox: { - mode: "non-main" as const, - workspaceRoot: join(home, "sandboxes"), - }, - }, - }, - channels: { - whatsapp: { - allowFrom: ["*"], - }, - }, - session: { - store: join(home, "sessions.json"), - }, - }; - - const ctx = { - Body: "hi", - From: "group:whatsapp:demo", - To: "+2000", - ChatType: "group" as const, - Provider: "whatsapp" as const, - MediaPath: mediaPath, - MediaType: "image/jpeg", - MediaUrl: mediaPath, - }; - - const res = await getReplyFromConfig(ctx, {}, cfg); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("ok"); - expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); - - const prompt = - vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; - const stagedPath = `media/inbound/${basename(mediaPath)}`; - expect(prompt).toContain(stagedPath); - expect(prompt).not.toContain(mediaPath); - - const sessionKey = resolveSessionKey( - cfg.session?.scope ?? "per-sender", - ctx, - cfg.session?.mainKey, - ); - const agentId = resolveAgentIdFromSessionKey(sessionKey); - const sandbox = await ensureSandboxWorkspaceForSession({ - config: cfg, - sessionKey, - workspaceDir: resolveAgentWorkspaceDir(cfg, agentId), - }); - expect(sandbox).not.toBeNull(); - if (!sandbox) { - throw new Error("Expected sandbox to be set"); - } - const stagedFullPath = join( - sandbox.workspaceDir, - "media", - "inbound", - basename(mediaPath), - ); - await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy(); + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, }); - }, - ); + + const cfg = { + agents: { + defaults: { + model: "anthropic/claude-opus-4-5", + workspace: join(home, "clawd"), + sandbox: { + mode: "non-main" as const, + workspaceRoot: join(home, "sandboxes"), + }, + }, + }, + channels: { + whatsapp: { + allowFrom: ["*"], + }, + }, + session: { + store: join(home, "sessions.json"), + }, + }; + + const ctx = { + Body: "hi", + From: "group:whatsapp:demo", + To: "+2000", + ChatType: "group" as const, + Provider: "whatsapp" as const, + MediaPath: mediaPath, + MediaType: "image/jpeg", + MediaUrl: mediaPath, + }; + + const res = await getReplyFromConfig(ctx, {}, cfg); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toBe("ok"); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + + const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + const stagedPath = `media/inbound/${basename(mediaPath)}`; + expect(prompt).toContain(stagedPath); + expect(prompt).not.toContain(mediaPath); + + const sessionKey = resolveSessionKey( + cfg.session?.scope ?? "per-sender", + ctx, + cfg.session?.mainKey, + ); + const agentId = resolveAgentIdFromSessionKey(sessionKey); + const sandbox = await ensureSandboxWorkspaceForSession({ + config: cfg, + sessionKey, + workspaceDir: resolveAgentWorkspaceDir(cfg, agentId), + }); + expect(sandbox).not.toBeNull(); + if (!sandbox) { + throw new Error("Expected sandbox to be set"); + } + const stagedFullPath = join(sandbox.workspaceDir, "media", "inbound", basename(mediaPath)); + await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy(); + }); + }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts index b95ecef77f8..7fd774dfde5 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts @@ -8,8 +8,7 @@ vi.mock("../agents/pi-embedded.js", () => ({ compactEmbeddedPiSession: vi.fn(), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => - `session:${key.trim() || "main"}`, + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); @@ -49,10 +48,7 @@ const modelCatalogMocks = vi.hoisted(() => ({ vi.mock("../agents/model-catalog.js", () => modelCatalogMocks); -import { - abortEmbeddedPiRun, - runEmbeddedPiAgent, -} from "../agents/pi-embedded.js"; +import { abortEmbeddedPiRun, runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { loadSessionStore } from "../config/sessions.js"; import { getReplyFromConfig } from "./reply.js"; @@ -137,9 +133,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("⚙️ Agent was aborted."); - expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith( - targetSessionId, - ); + expect(vi.mocked(abortEmbeddedPiRun)).toHaveBeenCalledWith(targetSessionId); const store = loadSessionStore(cfg.session.store); expect(store[targetSessionKey]?.abortedLastRun).toBe(true); }); diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 97d09235406..2af6dc232d7 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -9,10 +9,7 @@ import { } from "../../config/sessions.js"; import { parseAgentSessionKey } from "../../routing/session-key.js"; import { resolveCommandAuthorization } from "../command-auth.js"; -import { - normalizeCommandBody, - shouldHandleTextCommands, -} from "../commands-registry.js"; +import { normalizeCommandBody, shouldHandleTextCommands } from "../commands-registry.js"; import type { MsgContext } from "../templating.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; @@ -82,9 +79,7 @@ export async function tryFastAbortFromMessage(params: { config: cfg, }); // Use RawBody/CommandBody for abort detection (clean message without structural context). - const raw = stripStructuralPrefixes( - ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "", - ); + const raw = stripStructuralPrefixes(ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? ""); const isGroup = ctx.ChatType?.trim().toLowerCase() === "group"; const stripped = isGroup ? stripMentions(raw, ctx, cfg, agentId) : raw; const normalized = normalizeCommandBody(stripped); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 648359cb825..fbc7a7451b6 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -17,27 +17,18 @@ import { saveSessionStore, } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; -import { - emitAgentEvent, - registerAgentRunContext, -} from "../../infra/agent-events.js"; +import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js"; import { defaultRuntime } from "../../runtime.js"; import { stripHeartbeatToken } from "../heartbeat.js"; import type { TemplateContext } from "../templating.js"; import type { VerboseLevel } from "../thinking.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; -import { - buildThreadingToolContext, - resolveEnforceFinalTag, -} from "./agent-runner-utils.js"; +import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js"; import type { BlockReplyPipeline } from "./block-reply-pipeline.js"; import type { FollowupRun } from "./queue.js"; import { parseReplyDirectives } from "./reply-directives.js"; -import { - applyReplyTagsToPayload, - isRenderablePayload, -} from "./reply-payloads.js"; +import { applyReplyTagsToPayload, isRenderablePayload } from "./reply-payloads.js"; import type { TypingSignaler } from "./typing-mode.js"; export type AgentRunLoopResult = @@ -94,12 +85,9 @@ export async function runAgentTurnWithFallback(params: { while (true) { try { const allowPartialStream = !( - params.followupRun.run.reasoningLevel === "stream" && - params.opts?.onReasoningStream + params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream ); - const normalizeStreamingText = ( - payload: ReplyPayload, - ): { text?: string; skip: boolean } => { + const normalizeStreamingText = (payload: ReplyPayload): { text?: string; skip: boolean } => { if (!allowPartialStream) return { skip: true }; let text = payload.text; if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) { @@ -120,9 +108,7 @@ export async function runAgentTurnWithFallback(params: { } return { text, skip: false }; }; - const handlePartialForTyping = async ( - payload: ReplyPayload, - ): Promise => { + const handlePartialForTyping = async (payload: ReplyPayload): Promise => { const { text, skip } = normalizeStreamingText(payload); if (skip || !text) return undefined; await params.typingSignals.signalTextDelta(text); @@ -149,10 +135,7 @@ export async function runAgentTurnWithFallback(params: { startedAt, }, }); - const cliSessionId = getCliSessionId( - params.getActiveSessionEntry(), - provider, - ); + const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider); return runCliAgent({ sessionId: params.followupRun.run.sessionId, sessionKey: params.sessionKey, @@ -198,8 +181,7 @@ export async function runAgentTurnWithFallback(params: { return runEmbeddedPiAgent({ sessionId: params.followupRun.run.sessionId, sessionKey: params.sessionKey, - messageProvider: - params.sessionCtx.Provider?.trim().toLowerCase() || undefined, + messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined, agentAccountId: params.sessionCtx.AccountId, // Provider threading context for tool auto-injection ...buildThreadingToolContext({ @@ -215,10 +197,7 @@ export async function runAgentTurnWithFallback(params: { prompt: params.commandBody, extraSystemPrompt: params.followupRun.run.extraSystemPrompt, ownerNumbers: params.followupRun.run.ownerNumbers, - enforceFinalTag: resolveEnforceFinalTag( - params.followupRun.run, - provider, - ), + enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider), provider, model, authProfileId: params.followupRun.run.authProfileId, @@ -233,11 +212,7 @@ export async function runAgentTurnWithFallback(params: { onPartialReply: allowPartialStream ? async (payload) => { const textForTyping = await handlePartialForTyping(payload); - if ( - !params.opts?.onPartialReply || - textForTyping === undefined - ) - return; + if (!params.opts?.onPartialReply || textForTyping === undefined) return; await params.opts.onPartialReply({ text: textForTyping, mediaUrls: payload.mediaUrls, @@ -248,8 +223,7 @@ export async function runAgentTurnWithFallback(params: { await params.typingSignals.signalMessageStart(); }, onReasoningStream: - params.typingSignals.shouldStartOnReasoning || - params.opts?.onReasoningStream + params.typingSignals.shouldStartOnReasoning || params.opts?.onReasoningStream ? async (payload) => { await params.typingSignals.signalReasoningDelta(); await params.opts?.onReasoningStream?.({ @@ -261,16 +235,14 @@ export async function runAgentTurnWithFallback(params: { onAgentEvent: (evt) => { // Trigger typing when tools start executing if (evt.stream === "tool") { - const phase = - typeof evt.data.phase === "string" ? evt.data.phase : ""; + const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; if (phase === "start" || phase === "update") { void params.typingSignals.signalToolStart(); } } // Track auto-compaction completion if (evt.stream === "compaction") { - const phase = - typeof evt.data.phase === "string" ? evt.data.phase : ""; + const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; const willRetry = Boolean(evt.data.willRetry); if (phase === "end" && !willRetry) { autoCompactionCompleted = true; @@ -281,8 +253,7 @@ export async function runAgentTurnWithFallback(params: { params.blockStreamingEnabled && params.opts?.onBlockReply ? async (payload) => { const { text, skip } = normalizeStreamingText(payload); - const hasPayloadMedia = - (payload.mediaUrls?.length ?? 0) > 0; + const hasPayloadMedia = (payload.mediaUrls?.length ?? 0) > 0; if (skip && !hasPayloadMedia) return; const taggedPayload = applyReplyTagsToPayload( { @@ -293,22 +264,14 @@ export async function runAgentTurnWithFallback(params: { params.sessionCtx.MessageSid, ); // Let through payloads with audioAsVoice flag even if empty (need to track it) - if ( - !isRenderablePayload(taggedPayload) && - !payload.audioAsVoice - ) - return; - const parsed = parseReplyDirectives( - taggedPayload.text ?? "", - { - currentMessageId: params.sessionCtx.MessageSid, - silentToken: SILENT_REPLY_TOKEN, - }, - ); + if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return; + const parsed = parseReplyDirectives(taggedPayload.text ?? "", { + currentMessageId: params.sessionCtx.MessageSid, + silentToken: SILENT_REPLY_TOKEN, + }); const cleaned = parsed.text || undefined; const hasRenderableMedia = - Boolean(taggedPayload.mediaUrl) || - (taggedPayload.mediaUrls?.length ?? 0) > 0; + Boolean(taggedPayload.mediaUrl) || (taggedPayload.mediaUrls?.length ?? 0) > 0; // Skip empty payloads unless they have audioAsVoice flag (need to track it) if ( !cleaned && @@ -322,21 +285,16 @@ export async function runAgentTurnWithFallback(params: { const blockPayload: ReplyPayload = params.applyReplyToMode({ ...taggedPayload, text: cleaned, - audioAsVoice: Boolean( - parsed.audioAsVoice || payload.audioAsVoice, - ), + audioAsVoice: Boolean(parsed.audioAsVoice || payload.audioAsVoice), replyToId: taggedPayload.replyToId ?? parsed.replyToId, replyToTag: taggedPayload.replyToTag || parsed.replyToTag, - replyToCurrent: - taggedPayload.replyToCurrent || parsed.replyToCurrent, + replyToCurrent: taggedPayload.replyToCurrent || parsed.replyToCurrent, }); void params.typingSignals .signalTextDelta(cleaned ?? taggedPayload.text) .catch((err) => { - logVerbose( - `block reply typing signal failed: ${String(err)}`, - ); + logVerbose(`block reply typing signal failed: ${String(err)}`); }); params.blockReplyPipeline?.enqueue(blockPayload); @@ -399,8 +357,7 @@ export async function runAgentTurnWithFallback(params: { isContextOverflowError(message) || /context.*overflow|too large|context window/i.test(message); const isCompactionFailure = isCompactionFailureError(message); - const isSessionCorruption = - /function call turn comes immediately after/i.test(message); + const isSessionCorruption = /function call turn comes immediately after/i.test(message); if ( isCompactionFailure && @@ -426,8 +383,7 @@ export async function runAgentTurnWithFallback(params: { try { // Delete transcript file if it exists if (corruptedSessionId) { - const transcriptPath = - resolveSessionTranscriptPath(corruptedSessionId); + const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId); try { fs.unlinkSync(transcriptPath); } catch { diff --git a/src/auto-reply/reply/agent-runner-helpers.ts b/src/auto-reply/reply/agent-runner-helpers.ts index 402a860af5c..f783442da31 100644 --- a/src/auto-reply/reply/agent-runner-helpers.ts +++ b/src/auto-reply/reply/agent-runner-helpers.ts @@ -9,9 +9,7 @@ const hasAudioMedia = (urls?: string[]): boolean => Boolean(urls?.some((url) => isAudioFileName(url))); export const isAudioPayload = (payload: ReplyPayload): boolean => - hasAudioMedia( - payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined), - ); + hasAudioMedia(payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined)); export const createShouldEmitToolResult = (params: { sessionKey?: string; diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 224b8ffbdae..96d7fc88adc 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -3,10 +3,7 @@ import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js" import { runWithModelFallback } from "../../agents/model-fallback.js"; import { isCliProvider } from "../../agents/model-selection.js"; import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; -import { - resolveSandboxConfigForAgent, - resolveSandboxRuntimeStatus, -} from "../../agents/sandbox.js"; +import { resolveSandboxConfigForAgent, resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { resolveAgentIdFromSessionKey, @@ -18,10 +15,7 @@ import { registerAgentRunContext } from "../../infra/agent-events.js"; import type { TemplateContext } from "../templating.js"; import type { VerboseLevel } from "../thinking.js"; import type { GetReplyOptions } from "../types.js"; -import { - buildThreadingToolContext, - resolveEnforceFinalTag, -} from "./agent-runner-utils.js"; +import { buildThreadingToolContext, resolveEnforceFinalTag } from "./agent-runner-utils.js"; import { resolveMemoryFlushContextWindowTokens, resolveMemoryFlushSettings, @@ -54,10 +48,7 @@ export async function runMemoryFlushIfNeeded(params: { sessionKey: params.sessionKey, }); if (!runtime.sandboxed) return true; - const sandboxCfg = resolveSandboxConfigForAgent( - params.cfg, - runtime.agentId, - ); + const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, runtime.agentId); return sandboxCfg.workspaceAccess === "rw"; })(); @@ -69,9 +60,7 @@ export async function runMemoryFlushIfNeeded(params: { shouldRunMemoryFlush({ entry: params.sessionEntry ?? - (params.sessionKey - ? params.sessionStore?.[params.sessionKey] - : undefined), + (params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined), contextWindowTokens: resolveMemoryFlushContextWindowTokens({ modelId: params.followupRun.run.model ?? params.defaultModel, agentCfgContextTokens: params.agentCfgContextTokens, @@ -111,8 +100,7 @@ export async function runMemoryFlushIfNeeded(params: { runEmbeddedPiAgent({ sessionId: params.followupRun.run.sessionId, sessionKey: params.sessionKey, - messageProvider: - params.sessionCtx.Provider?.trim().toLowerCase() || undefined, + messageProvider: params.sessionCtx.Provider?.trim().toLowerCase() || undefined, agentAccountId: params.sessionCtx.AccountId, // Provider threading context for tool auto-injection ...buildThreadingToolContext({ @@ -128,10 +116,7 @@ export async function runMemoryFlushIfNeeded(params: { prompt: memoryFlushSettings.prompt, extraSystemPrompt: flushSystemPrompt, ownerNumbers: params.followupRun.run.ownerNumbers, - enforceFinalTag: resolveEnforceFinalTag( - params.followupRun.run, - provider, - ), + enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider), provider, model, authProfileId: params.followupRun.run.authProfileId, @@ -143,8 +128,7 @@ export async function runMemoryFlushIfNeeded(params: { runId: flushRunId, onAgentEvent: (evt) => { if (evt.stream === "compaction") { - const phase = - typeof evt.data.phase === "string" ? evt.data.phase : ""; + const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; const willRetry = Boolean(evt.data.willRetry); if (phase === "end" && !willRetry) { memoryCompactionCompleted = true; @@ -155,9 +139,7 @@ export async function runMemoryFlushIfNeeded(params: { }); let memoryFlushCompactionCount = activeSessionEntry?.compactionCount ?? - (params.sessionKey - ? activeSessionStore?.[params.sessionKey]?.compactionCount - : 0) ?? + (params.sessionKey ? activeSessionStore?.[params.sessionKey]?.compactionCount : 0) ?? 0; if (memoryCompactionCompleted) { const nextCount = await incrementCompactionCount({ diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index caf0a237eb1..582ed53f37c 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -4,10 +4,7 @@ import { stripHeartbeatToken } from "../heartbeat.js"; import type { OriginatingChannelType } from "../templating.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; -import { - formatBunFetchSocketError, - isBunFetchSocketError, -} from "./agent-runner-utils.js"; +import { formatBunFetchSocketError, isBunFetchSocketError } from "./agent-runner-utils.js"; import type { BlockReplyPipeline } from "./block-reply-pipeline.js"; import { parseReplyDirectives } from "./reply-directives.js"; import { @@ -52,8 +49,7 @@ export function buildReplyPayloads(params: { didLogHeartbeatStrip = true; logVerbose("Stripped stray HEARTBEAT_OK token from reply"); } - const hasMedia = - Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; + const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; if (stripped.shouldSkip && !hasMedia) return []; return [{ ...payload, text: stripped.text }]; }); @@ -105,9 +101,7 @@ export function buildReplyPayloads(params: { const filteredPayloads = shouldDropFinalPayloads ? [] : params.blockStreamingEnabled - ? dedupedPayloads.filter( - (payload) => !params.blockReplyPipeline?.hasSentPayload(payload), - ) + ? dedupedPayloads.filter((payload) => !params.blockReplyPipeline?.hasSentPayload(payload)) : dedupedPayloads; const replyPayloads = suppressMessagingToolReplies ? [] : filteredPayloads; diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index ebbfdc0a0b7..b4f1b2bc29a 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -4,11 +4,7 @@ import type { ChannelThreadingToolContext } from "../../channels/plugins/types.j import { normalizeChannelId } from "../../channels/registry.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; -import { - estimateUsageCost, - formatTokenCount, - formatUsd, -} from "../../utils/usage-format.js"; +import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js"; import type { TemplateContext } from "../templating.js"; import type { ReplyPayload } from "../types.js"; import type { FollowupRun } from "./queue.js"; @@ -73,8 +69,7 @@ export const formatResponseUsageLine = (params: { const output = usage.output; if (typeof input !== "number" && typeof output !== "number") return null; const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?"; - const outputLabel = - typeof output === "number" ? formatTokenCount(output) : "?"; + const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?"; const cost = params.showCost && typeof input === "number" && typeof output === "number" ? estimateUsageCost({ @@ -92,10 +87,7 @@ export const formatResponseUsageLine = (params: { return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`; }; -export const appendUsageLine = ( - payloads: ReplyPayload[], - line: string, -): ReplyPayload[] => { +export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPayload[] => { let index = -1; for (let i = payloads.length - 1; i >= 0; i -= 1) { if (payloads[i]?.text) { @@ -116,7 +108,5 @@ export const appendUsageLine = ( return updated; }; -export const resolveEnforceFinalTag = ( - run: FollowupRun["run"], - provider: string, -) => Boolean(run.enforceFinalTag || isReasoningTagProvider(provider)); +export const resolveEnforceFinalTag = (run: FollowupRun["run"], provider: string) => + Boolean(run.enforceFinalTag || isReasoningTagProvider(provider)); diff --git a/src/auto-reply/reply/agent-runner.block-streaming.test.ts b/src/auto-reply/reply/agent-runner.block-streaming.test.ts index 91c665682cf..313f8916c60 100644 --- a/src/auto-reply/reply/agent-runner.block-streaming.test.ts +++ b/src/auto-reply/reply/agent-runner.block-streaming.test.ts @@ -28,8 +28,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -43,9 +42,7 @@ describe("runReplyAgent block streaming", () => { it("coalesces duplicate text_end block replies", async () => { const onBlockReply = vi.fn(); runEmbeddedPiAgentMock.mockImplementationOnce(async (params) => { - const block = params.onBlockReply as - | ((payload: { text?: string }) => void) - | undefined; + const block = params.onBlockReply as ((payload: { text?: string }) => void) | undefined; block?.({ text: "Hello" }); block?.({ text: "Hello" }); return { diff --git a/src/auto-reply/reply/agent-runner.claude-cli.test.ts b/src/auto-reply/reply/agent-runner.claude-cli.test.ts index d306824c7dc..546af159f4e 100644 --- a/src/auto-reply/reply/agent-runner.claude-cli.test.ts +++ b/src/auto-reply/reply/agent-runner.claude-cli.test.ts @@ -34,8 +34,7 @@ vi.mock("../../agents/cli-runner.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts index 6e15cb2f463..cf9c6a8238c 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts @@ -34,8 +34,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -124,9 +123,7 @@ function createMinimalRun(params?: { describe("runReplyAgent typing (heartbeat)", () => { it("resets corrupted Gemini sessions and deletes transcripts", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const stateDir = await fs.mkdtemp( - path.join(tmpdir(), "clawdbot-session-reset-"), - ); + const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-reset-")); process.env.CLAWDBOT_STATE_DIR = stateDir; try { const sessionId = "session-corrupt"; @@ -173,9 +170,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("keeps sessions intact on other errors", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const stateDir = await fs.mkdtemp( - path.join(tmpdir(), "clawdbot-session-noreset-"), - ); + const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-noreset-")); process.env.CLAWDBOT_STATE_DIR = stateDir; try { const sessionId = "session-ok"; diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts index 58e5ceeba59..cc490f6d13c 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts @@ -33,8 +33,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -123,9 +122,7 @@ function createMinimalRun(params?: { describe("runReplyAgent typing (heartbeat)", () => { it("retries after compaction failure by resetting the session", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const stateDir = await fs.mkdtemp( - path.join(tmpdir(), "clawdbot-session-compaction-reset-"), - ); + const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-compaction-reset-")); process.env.CLAWDBOT_STATE_DIR = stateDir; try { const sessionId = "session"; @@ -173,9 +170,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("retries after context overflow payload by resetting the session", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const stateDir = await fs.mkdtemp( - path.join(tmpdir(), "clawdbot-session-overflow-reset-"), - ); + const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-overflow-reset-")); process.env.CLAWDBOT_STATE_DIR = stateDir; try { const sessionId = "session"; @@ -188,9 +183,7 @@ describe("runReplyAgent typing (heartbeat)", () => { runEmbeddedPiAgentMock .mockImplementationOnce(async () => ({ - payloads: [ - { text: "Context overflow: prompt too large", isError: true }, - ], + payloads: [{ text: "Context overflow: prompt too large", isError: true }], meta: { durationMs: 1, error: { diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts index bd089a19020..f5ac30d34ae 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts @@ -33,8 +33,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -123,12 +122,10 @@ function createMinimalRun(params?: { describe("runReplyAgent typing (heartbeat)", () => { it("signals typing on block replies", async () => { const onBlockReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onBlockReply?.({ text: "chunk", mediaUrls: [] }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onBlockReply?.({ text: "chunk", mediaUrls: [] }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ typingMode: "message", @@ -148,12 +145,10 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("signals typing on tool results", async () => { const onToolResult = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onToolResult?.({ text: "tooling", mediaUrls: [] }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onToolResult?.({ text: "tooling", mediaUrls: [] }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ typingMode: "message", @@ -169,12 +164,10 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("skips typing for silent tool results", async () => { const onToolResult = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onToolResult?.({ text: "NO_REPLY", mediaUrls: [] }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onToolResult?.({ text: "NO_REPLY", mediaUrls: [] }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ typingMode: "message", @@ -195,10 +188,7 @@ describe("runReplyAgent typing (heartbeat)", () => { runEmbeddedPiAgentMock.mockImplementationOnce( async (params: { - onAgentEvent?: (evt: { - stream: string; - data: Record; - }) => void; + onAgentEvent?: (evt: { stream: string; data: Record }) => void; }) => { params.onAgentEvent?.({ stream: "compaction", diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts index 584364e9bc1..d5a984a4256 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts @@ -30,8 +30,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -120,12 +119,10 @@ function createMinimalRun(params?: { describe("runReplyAgent typing (heartbeat)", () => { it("signals typing for normal runs", async () => { const onPartialReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onPartialReply?.({ text: "hi" }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onPartialReply?.({ text: "hi" }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ opts: { isHeartbeat: false, onPartialReply }, @@ -137,12 +134,10 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(typing.startTypingLoop).toHaveBeenCalled(); }); it("signals typing even without consumer partial handler", async () => { - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onPartialReply?.({ text: "hi" }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onPartialReply?.({ text: "hi" }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ typingMode: "message", @@ -154,12 +149,10 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("never signals typing for heartbeat runs", async () => { const onPartialReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onPartialReply?.({ text: "hi" }); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onPartialReply?.({ text: "hi" }); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ opts: { isHeartbeat: true, onPartialReply }, @@ -172,12 +165,10 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("suppresses partial streaming for NO_REPLY", async () => { const onPartialReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onPartialReply?.({ text: "NO_REPLY" }); - return { payloads: [{ text: "NO_REPLY" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onPartialReply?.({ text: "NO_REPLY" }); + return { payloads: [{ text: "NO_REPLY" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ opts: { isHeartbeat: false, onPartialReply }, @@ -189,12 +180,10 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(typing.startTypingLoop).not.toHaveBeenCalled(); }); it("starts typing on assistant message start in message mode", async () => { - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: EmbeddedPiAgentParams) => { - await params.onAssistantMessageStart?.(); - return { payloads: [{ text: "final" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { + await params.onAssistantMessageStart?.(); + return { payloads: [{ text: "final" }], meta: {} }; + }); const { run, typing } = createMinimalRun({ typingMode: "message", @@ -208,9 +197,7 @@ describe("runReplyAgent typing (heartbeat)", () => { runEmbeddedPiAgentMock.mockImplementationOnce( async (params: { onPartialReply?: (payload: { text?: string }) => Promise | void; - onReasoningStream?: (payload: { - text?: string; - }) => Promise | void; + onReasoningStream?: (payload: { text?: string }) => Promise | void; }) => { await params.onReasoningStream?.({ text: "Reasoning:\n_step_" }); await params.onPartialReply?.({ text: "hi" }); @@ -228,9 +215,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("suppresses typing in never mode", async () => { runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: { - onPartialReply?: (payload: { text?: string }) => void; - }) => { + async (params: { onPartialReply?: (payload: { text?: string }) => void }) => { params.onPartialReply?.({ text: "hi" }); return { payloads: [{ text: "final" }], meta: {} }; }, diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts index 68ae59d0505..ea45c4ddcae 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts @@ -34,8 +34,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -124,13 +123,9 @@ function createMinimalRun(params?: { describe("runReplyAgent typing (heartbeat)", () => { it("still replies even if session reset fails to persist", async () => { const prevStateDir = process.env.CLAWDBOT_STATE_DIR; - const stateDir = await fs.mkdtemp( - path.join(tmpdir(), "clawdbot-session-reset-fail-"), - ); + const stateDir = await fs.mkdtemp(path.join(tmpdir(), "clawdbot-session-reset-fail-")); process.env.CLAWDBOT_STATE_DIR = stateDir; - const saveSpy = vi - .spyOn(sessions, "saveSessionStore") - .mockRejectedValueOnce(new Error("boom")); + const saveSpy = vi.spyOn(sessions, "saveSessionStore").mockRejectedValueOnce(new Error("boom")); try { const sessionId = "session-corrupt"; const storePath = path.join(stateDir, "sessions", "sessions.json"); @@ -185,9 +180,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const payloads = Array.isArray(res) ? res : res ? [res] : []; expect(payloads.length).toBe(1); expect(payloads[0]?.text).toContain("LLM connection failed"); - expect(payloads[0]?.text).toContain( - "socket connection was closed unexpectedly", - ); + expect(payloads[0]?.text).toContain("socket connection was closed unexpectedly"); expect(payloads[0]?.text).toContain("```"); }); }); diff --git a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.increments-compaction-count-flush-compaction-completes.test.ts b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.increments-compaction-count-flush-compaction-completes.test.ts index 8989330f0d0..f3c77bb84f0 100644 --- a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.increments-compaction-count-flush-compaction-completes.test.ts +++ b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.increments-compaction-count-flush-compaction-completes.test.ts @@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn(); type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - onAgentEvent?: (evt: { - stream?: string; - data?: { phase?: string; willRetry?: boolean }; - }) => void; + onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; }; vi.mock("../../agents/model-fallback.js", () => ({ @@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -140,21 +136,19 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { - params.onAgentEvent?.({ - stream: "compaction", - data: { phase: "end", willRetry: false }, - }); - return { payloads: [], meta: {} }; - } - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { + params.onAgentEvent?.({ + stream: "compaction", + data: { phase: "end", willRetry: false }, + }); + return { payloads: [], meta: {} }; + } + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, diff --git a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.runs-memory-flush-turn-updates-session-metadata.test.ts b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.runs-memory-flush-turn-updates-session-metadata.test.ts index 032742509e6..88d51ee1041 100644 --- a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.runs-memory-flush-turn-updates-session-metadata.test.ts +++ b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.runs-memory-flush-turn-updates-session-metadata.test.ts @@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn(); type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - onAgentEvent?: (evt: { - stream?: string; - data?: { phase?: string; willRetry?: boolean }; - }) => void; + onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; }; vi.mock("../../agents/model-fallback.js", () => ({ @@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -141,18 +137,16 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array<{ prompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push({ prompt: params.prompt }); - if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { - return { payloads: [], meta: {} }; - } - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push({ prompt: params.prompt }); + if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { + return { payloads: [], meta: {} }; + } + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, @@ -184,10 +178,7 @@ describe("runReplyAgent memory flush", () => { typingMode: "instant", }); - expect(calls.map((call) => call.prompt)).toEqual([ - DEFAULT_MEMORY_FLUSH_PROMPT, - "hello", - ]); + expect(calls.map((call) => call.prompt)).toEqual([DEFAULT_MEMORY_FLUSH_PROMPT, "hello"]); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); expect(stored[sessionKey].memoryFlushAt).toBeTypeOf("number"); @@ -207,12 +198,10 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); - runEmbeddedPiAgentMock.mockImplementation( - async (_params: EmbeddedRunParams) => ({ - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }), - ); + runEmbeddedPiAgentMock.mockImplementation(async (_params: EmbeddedRunParams) => ({ + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + })); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, @@ -250,9 +239,7 @@ describe("runReplyAgent memory flush", () => { }); expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as - | { prompt?: string } - | undefined; + const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as { prompt?: string } | undefined; expect(call?.prompt).toBe("hello"); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); diff --git a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-cli-providers.test.ts b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-cli-providers.test.ts index d8b7989a4d9..5f4a9df016b 100644 --- a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-cli-providers.test.ts +++ b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-cli-providers.test.ts @@ -12,10 +12,7 @@ const runCliAgentMock = vi.fn(); type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - onAgentEvent?: (evt: { - stream?: string; - data?: { phase?: string; willRetry?: boolean }; - }) => void; + onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; }; vi.mock("../../agents/model-fallback.js", () => ({ @@ -44,8 +41,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -141,15 +137,13 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array<{ prompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push({ prompt: params.prompt }); - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push({ prompt: params.prompt }); + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); runCliAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: { usage: { input: 1, output: 1 } } }, @@ -187,9 +181,7 @@ describe("runReplyAgent memory flush", () => { }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); - const call = runCliAgentMock.mock.calls[0]?.[0] as - | { prompt?: string } - | undefined; + const call = runCliAgentMock.mock.calls[0]?.[0] as { prompt?: string } | undefined; expect(call?.prompt).toBe("hello"); expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); diff --git a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-sandbox-workspace-is-read.test.ts b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-sandbox-workspace-is-read.test.ts index 45a5c683798..be2e1bb5fcd 100644 --- a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-sandbox-workspace-is-read.test.ts +++ b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-sandbox-workspace-is-read.test.ts @@ -12,10 +12,7 @@ const runCliAgentMock = vi.fn(); type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - onAgentEvent?: (evt: { - stream?: string; - data?: { phase?: string; willRetry?: boolean }; - }) => void; + onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; }; vi.mock("../../agents/model-fallback.js", () => ({ @@ -44,8 +41,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -140,15 +136,13 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array<{ prompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push({ prompt: params.prompt }); - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push({ prompt: params.prompt }); + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, @@ -207,15 +201,13 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array<{ prompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push({ prompt: params.prompt }); - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push({ prompt: params.prompt }); + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, diff --git a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.uses-configured-prompts-memory-flush-runs.test.ts b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.uses-configured-prompts-memory-flush-runs.test.ts index 950d0f8bb33..717ffbdc4bb 100644 --- a/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.uses-configured-prompts-memory-flush-runs.test.ts +++ b/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.uses-configured-prompts-memory-flush-runs.test.ts @@ -13,10 +13,7 @@ const runCliAgentMock = vi.fn(); type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - onAgentEvent?: (evt: { - stream?: string; - data?: { phase?: string; willRetry?: boolean }; - }) => void; + onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; }; vi.mock("../../agents/model-fallback.js", () => ({ @@ -45,8 +42,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -141,18 +137,16 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push(params); - if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { - return { payloads: [], meta: {} }; - } - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push(params); + if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { + return { payloads: [], meta: {} }; + } + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, @@ -221,15 +215,13 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); const calls: Array<{ prompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedRunParams) => { - calls.push({ prompt: params.prompt }); - return { - payloads: [{ text: "ok" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { + calls.push({ prompt: params.prompt }); + return { + payloads: [{ text: "ok" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({ storePath, diff --git a/src/auto-reply/reply/agent-runner.messaging-tools.test.ts b/src/auto-reply/reply/agent-runner.messaging-tools.test.ts index 0e2673efb48..ecbcd8e18c9 100644 --- a/src/auto-reply/reply/agent-runner.messaging-tools.test.ts +++ b/src/auto-reply/reply/agent-runner.messaging-tools.test.ts @@ -28,8 +28,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -101,9 +100,7 @@ describe("runReplyAgent messaging tool suppression", () => { runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], - messagingToolSentTargets: [ - { tool: "slack", provider: "slack", to: "channel:C1" }, - ], + messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }], meta: {}, }); @@ -116,9 +113,7 @@ describe("runReplyAgent messaging tool suppression", () => { runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], - messagingToolSentTargets: [ - { tool: "discord", provider: "discord", to: "channel:C1" }, - ], + messagingToolSentTargets: [{ tool: "discord", provider: "discord", to: "channel:C1" }], meta: {}, }); diff --git a/src/auto-reply/reply/agent-runner.reasoning-tags.test.ts b/src/auto-reply/reply/agent-runner.reasoning-tags.test.ts index 1bd9085b454..2859439f940 100644 --- a/src/auto-reply/reply/agent-runner.reasoning-tags.test.ts +++ b/src/auto-reply/reply/agent-runner.reasoning-tags.test.ts @@ -23,8 +23,7 @@ vi.mock("../../agents/pi-embedded.js", () => ({ })); vi.mock("./queue.js", async () => { - const actual = - await vi.importActual("./queue.js"); + const actual = await vi.importActual("./queue.js"); return { ...actual, enqueueFollowupRun: vi.fn(), @@ -118,11 +117,7 @@ describe("runReplyAgent fallback reasoning tags", () => { meta: {}, }); runWithModelFallbackMock.mockImplementationOnce( - async ({ - run, - }: { - run: (provider: string, model: string) => Promise; - }) => ({ + async ({ run }: { run: (provider: string, model: string) => Promise }) => ({ result: await run("google-antigravity", "gemini-3"), provider: "google-antigravity", model: "gemini-3", @@ -131,27 +126,19 @@ describe("runReplyAgent fallback reasoning tags", () => { await createRun(); - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as - | EmbeddedPiAgentParams - | undefined; + const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as EmbeddedPiAgentParams | undefined; expect(call?.enforceFinalTag).toBe(true); }); it("enforces during memory flush on fallback providers", async () => { - runEmbeddedPiAgentMock.mockImplementation( - async (params: EmbeddedPiAgentParams) => { - if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { - return { payloads: [], meta: {} }; - } - return { payloads: [{ text: "ok" }], meta: {} }; - }, - ); + runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedPiAgentParams) => { + if (params.prompt === DEFAULT_MEMORY_FLUSH_PROMPT) { + return { payloads: [], meta: {} }; + } + return { payloads: [{ text: "ok" }], meta: {} }; + }); runWithModelFallbackMock.mockImplementation( - async ({ - run, - }: { - run: (provider: string, model: string) => Promise; - }) => ({ + async ({ run }: { run: (provider: string, model: string) => Promise }) => ({ result: await run("google-antigravity", "gemini-3"), provider: "google-antigravity", model: "gemini-3", @@ -169,8 +156,7 @@ describe("runReplyAgent fallback reasoning tags", () => { const flushCall = runEmbeddedPiAgentMock.mock.calls.find( ([params]) => - (params as EmbeddedPiAgentParams | undefined)?.prompt === - DEFAULT_MEMORY_FLUSH_PROMPT, + (params as EmbeddedPiAgentParams | undefined)?.prompt === DEFAULT_MEMORY_FLUSH_PROMPT, )?.[0] as EmbeddedPiAgentParams | undefined; expect(flushCall?.enforceFinalTag).toBe(true); diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index fbe23e0d5f6..084dc619ee5 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -29,25 +29,12 @@ import { } from "./agent-runner-helpers.js"; import { runMemoryFlushIfNeeded } from "./agent-runner-memory.js"; import { buildReplyPayloads } from "./agent-runner-payloads.js"; -import { - appendUsageLine, - formatResponseUsageLine, -} from "./agent-runner-utils.js"; -import { - createAudioAsVoiceBuffer, - createBlockReplyPipeline, -} from "./block-reply-pipeline.js"; +import { appendUsageLine, formatResponseUsageLine } from "./agent-runner-utils.js"; +import { createAudioAsVoiceBuffer, createBlockReplyPipeline } from "./block-reply-pipeline.js"; import { resolveBlockStreamingCoalescing } from "./block-streaming.js"; import { createFollowupRunner } from "./followup-runner.js"; -import { - enqueueFollowupRun, - type FollowupRun, - type QueueSettings, -} from "./queue.js"; -import { - createReplyToModeFilterForChannel, - resolveReplyToMode, -} from "./reply-threading.js"; +import { enqueueFollowupRun, type FollowupRun, type QueueSettings } from "./queue.js"; +import { createReplyToModeFilterForChannel, resolveReplyToMode } from "./reply-threading.js"; import { incrementCompactionCount } from "./session-updates.js"; import type { TypingController } from "./typing.js"; import { createTypingSignaler } from "./typing-mode.js"; @@ -129,8 +116,7 @@ export async function runReplyAgent(params: { }); const pendingToolTasks = new Set>(); - const blockReplyTimeoutMs = - opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS; + const blockReplyTimeoutMs = opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS; const replyToChannel = sessionCtx.OriginatingChannel ?? @@ -142,10 +128,7 @@ export async function runReplyAgent(params: { replyToChannel, sessionCtx.AccountId, ); - const applyReplyToMode = createReplyToModeFilterForChannel( - replyToMode, - replyToChannel, - ); + const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel); const cfg = followupRun.run.config; const blockReplyCoalescing = blockStreamingEnabled && opts?.onBlockReply @@ -167,10 +150,7 @@ export async function runReplyAgent(params: { : null; if (shouldSteer && isStreaming) { - const steered = queueEmbeddedPiMessage( - followupRun.run.sessionId, - followupRun.prompt, - ); + const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt); if (steered && !shouldFollowup) { if (activeSessionEntry && activeSessionStore && sessionKey) { activeSessionEntry.updatedAt = Date.now(); @@ -225,9 +205,7 @@ export async function runReplyAgent(params: { }); let responseUsageLine: string | undefined; - const resetSessionAfterCompactionFailure = async ( - reason: string, - ): Promise => { + const resetSessionAfterCompactionFailure = async (reason: string): Promise => { if (!sessionKey || !activeSessionStore || !storePath) return false; const nextSessionId = crypto.randomUUID(); const nextEntry: SessionEntry = { @@ -239,14 +217,8 @@ export async function runReplyAgent(params: { }; const agentId = resolveAgentIdFromSessionKey(sessionKey); const topicId = - typeof sessionCtx.MessageThreadId === "number" - ? sessionCtx.MessageThreadId - : undefined; - const nextSessionFile = resolveSessionTranscriptPath( - nextSessionId, - agentId, - topicId, - ); + typeof sessionCtx.MessageThreadId === "number" ? sessionCtx.MessageThreadId : undefined; + const nextSessionFile = resolveSessionTranscriptPath(nextSessionId, agentId, topicId); nextEntry.sessionFile = nextSessionFile; activeSessionStore[sessionKey] = nextEntry; try { @@ -289,11 +261,7 @@ export async function runReplyAgent(params: { }); if (runOutcome.kind === "final") { - return finalizeWithFollowup( - runOutcome.payload, - queueKey, - runFollowupTurn, - ); + return finalizeWithFollowup(runOutcome.payload, queueKey, runFollowupTurn); } const { runResult, fallbackProvider, fallbackModel } = runOutcome; @@ -354,12 +322,9 @@ export async function runReplyAgent(params: { await signalTypingIfNeeded(replyPayloads, typingSignals); const usage = runResult.meta.agentMeta?.usage; - const modelUsed = - runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel; + const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel; const providerUsed = - runResult.meta.agentMeta?.provider ?? - fallbackProvider ?? - followupRun.run.provider; + runResult.meta.agentMeta?.provider ?? fallbackProvider ?? followupRun.run.provider; const cliSessionId = isCliProvider(providerUsed, cfg) ? runResult.meta.agentMeta?.sessionId?.trim() : undefined; @@ -378,13 +343,11 @@ export async function runReplyAgent(params: { update: async (entry) => { const input = usage.input ?? 0; const output = usage.output ?? 0; - const promptTokens = - input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); + const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); const patch: Partial = { inputTokens: input, outputTokens: output, - totalTokens: - promptTokens > 0 ? promptTokens : (usage.total ?? input), + totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input), modelProvider: providerUsed, model: modelUsed, contextTokens: contextTokensUsed ?? entry.contextTokens, @@ -437,9 +400,7 @@ export async function runReplyAgent(params: { const responseUsageEnabled = (activeSessionEntry?.responseUsage ?? - (sessionKey - ? activeSessionStore?.[sessionKey]?.responseUsage - : undefined)) === "on"; + (sessionKey ? activeSessionStore?.[sessionKey]?.responseUsage : undefined)) === "on"; if (responseUsageEnabled && hasNonzeroUsage(usage)) { const authMode = resolveModelAuthMode(providerUsed, cfg); const showCost = authMode === "api-key"; @@ -469,17 +430,11 @@ export async function runReplyAgent(params: { }); if (resolvedVerboseLevel === "on") { const suffix = typeof count === "number" ? ` (count ${count})` : ""; - finalPayloads = [ - { text: `🧹 Auto-compaction complete${suffix}.` }, - ...finalPayloads, - ]; + finalPayloads = [{ text: `🧹 Auto-compaction complete${suffix}.` }, ...finalPayloads]; } } if (resolvedVerboseLevel === "on" && activeIsNewSession) { - finalPayloads = [ - { text: `🧭 New session: ${followupRun.run.sessionId}` }, - ...finalPayloads, - ]; + finalPayloads = [{ text: `🧭 New session: ${followupRun.run.sessionId}` }, ...finalPayloads]; } if (responseUsageLine) { finalPayloads = appendUsageLine(finalPayloads, responseUsageLine); diff --git a/src/auto-reply/reply/bash-command.ts b/src/auto-reply/reply/bash-command.ts index 3ea5451990a..dc87976648b 100644 --- a/src/auto-reply/reply/bash-command.ts +++ b/src/auto-reply/reply/bash-command.ts @@ -1,9 +1,5 @@ import { resolveSessionAgentId } from "../../agents/agent-scope.js"; -import { - getFinishedSession, - getSession, - markExited, -} from "../../agents/bash-process-registry.js"; +import { getFinishedSession, getSession, markExited } from "../../agents/bash-process-registry.js"; import { createExecTool } from "../../agents/bash-tools.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { killProcessTree } from "../../agents/shell-utils.js"; @@ -41,8 +37,7 @@ function clampNumber(value: number, min: number, max: number) { function resolveForegroundMs(cfg: ClawdbotConfig): number { const raw = cfg.commands?.bashForegroundMs; - if (typeof raw !== "number" || Number.isNaN(raw)) - return DEFAULT_FOREGROUND_MS; + if (typeof raw !== "number" || Number.isNaN(raw)) return DEFAULT_FOREGROUND_MS; return clampNumber(Math.floor(raw), 0, MAX_FOREGROUND_MS); } @@ -98,8 +93,7 @@ function resolveRawCommandBody(params: { agentId?: string; isGroup: boolean; }) { - const source = - params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? ""; + const source = params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body ?? ""; const stripped = stripStructuralPrefixes(source); return params.isGroup ? stripMentions(stripped, params.ctx, params.cfg, params.agentId) @@ -110,8 +104,7 @@ function getScopedSession(sessionId: string) { const running = getSession(sessionId); if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) return { running }; const finished = getFinishedSession(sessionId); - if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY) - return { finished }; + if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY) return { finished }; return {}; } @@ -165,11 +158,7 @@ function formatElevatedUnavailableMessage(params: { `elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`, ); if (params.failures.length > 0) { - lines.push( - `Failing gates: ${params.failures - .map((f) => `${f.gate} (${f.key})`) - .join(", ")}`, - ); + lines.push(`Failing gates: ${params.failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`); } else { lines.push( "Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.).", @@ -244,18 +233,14 @@ export async function handleBashChatCommand(params: { if (request.action === "poll") { const sessionId = - request.sessionId?.trim() || - (liveJob?.state === "running" ? liveJob.sessionId : ""); + request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : ""); if (!sessionId) { return { text: "⚙️ No active bash job." }; } const { running, finished } = getScopedSession(sessionId); if (running) { attachActiveWatcher(sessionId); - const runtimeSec = Math.max( - 0, - Math.floor((Date.now() - running.startedAt) / 1000), - ); + const runtimeSec = Math.max(0, Math.floor((Date.now() - running.startedAt) / 1000)); const tail = running.tail || "(no output yet)"; return { text: [ @@ -291,8 +276,7 @@ export async function handleBashChatCommand(params: { if (request.action === "stop") { const sessionId = - request.sessionId?.trim() || - (liveJob?.state === "running" ? liveJob.sessionId : ""); + request.sessionId?.trim() || (liveJob?.state === "running" ? liveJob.sessionId : ""); if (!sessionId) { return { text: "⚙️ No active bash job." }; } @@ -326,9 +310,7 @@ export async function handleBashChatCommand(params: { // request.action === "run" if (liveJob) { const label = - liveJob.state === "running" - ? formatSessionSnippet(liveJob.sessionId) - : "starting"; + liveJob.state === "running" ? formatSessionSnippet(liveJob.sessionId) : "starting"; return { text: `⚠️ A bash job is already running (${label}). Use !poll / !stop (or /bash poll / /bash stop).`, }; @@ -346,8 +328,7 @@ export async function handleBashChatCommand(params: { try { const foregroundMs = resolveForegroundMs(params.cfg); const shouldBackgroundImmediately = foregroundMs <= 0; - const timeoutSec = - params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec; + const timeoutSec = params.cfg.tools?.exec?.timeoutSec ?? params.cfg.tools?.bash?.timeoutSec; const execTool = createExecTool({ scopeKey: CHAT_BASH_SCOPE_KEY, allowBackground: true, @@ -385,14 +366,11 @@ export async function handleBashChatCommand(params: { // Completed in foreground. activeJob = null; - const exitCode = - result.details?.status === "completed" ? result.details.exitCode : 0; + const exitCode = result.details?.status === "completed" ? result.details.exitCode : 0; const output = result.details?.status === "completed" ? result.details.aggregated - : result.content - .map((chunk) => (chunk.type === "text" ? chunk.text : "")) - .join("\n"); + : result.content.map((chunk) => (chunk.type === "text" ? chunk.text : "")).join("\n"); return { text: [ `⚙️ bash: ${commandText}`, @@ -404,9 +382,7 @@ export async function handleBashChatCommand(params: { activeJob = null; const message = err instanceof Error ? err.message : String(err); return { - text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join( - "\n", - ), + text: [`⚠️ bash failed: ${commandText}`, formatOutputBlock(message)].join("\n"), }; } } diff --git a/src/auto-reply/reply/block-reply-coalescer.ts b/src/auto-reply/reply/block-reply-coalescer.ts index 97535e94271..d18a85881c5 100644 --- a/src/auto-reply/reply/block-reply-coalescer.ts +++ b/src/auto-reply/reply/block-reply-coalescer.ts @@ -66,8 +66,7 @@ export function createBlockReplyCoalescer(params: { const enqueue = (payload: ReplyPayload) => { if (shouldAbort()) return; - const hasMedia = - Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; + const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; const text = payload.text ?? ""; const hasText = text.trim().length > 0; if (hasMedia) { @@ -79,8 +78,7 @@ export function createBlockReplyCoalescer(params: { if ( bufferText && - (bufferReplyToId !== payload.replyToId || - bufferAudioAsVoice !== payload.audioAsVoice) + (bufferReplyToId !== payload.replyToId || bufferAudioAsVoice !== payload.audioAsVoice) ) { void flush({ force: true }); } diff --git a/src/auto-reply/reply/block-reply-pipeline.ts b/src/auto-reply/reply/block-reply-pipeline.ts index 2ca263be9d6..00e08acba4c 100644 --- a/src/auto-reply/reply/block-reply-pipeline.ts +++ b/src/auto-reply/reply/block-reply-pipeline.ts @@ -30,8 +30,7 @@ export function createAudioAsVoiceBuffer(params: { } }, shouldBuffer: (payload) => params.isAudioPayload(payload), - finalize: (payload) => - seenAudioAsVoice ? { ...payload, audioAsVoice: true } : payload, + finalize: (payload) => (seenAudioAsVoice ? { ...payload, audioAsVoice: true } : payload), }; } @@ -97,9 +96,7 @@ export function createBlockReplyPipeline(params: { if (sentKeys.has(payloadKey) || pendingKeys.has(payloadKey)) return; pendingKeys.add(payloadKey); - const timeoutError = new Error( - `block reply delivery timed out after ${timeoutMs}ms`, - ); + const timeoutError = new Error(`block reply delivery timed out after ${timeoutMs}ms`); const abortController = new AbortController(); sendChain = sendChain .then(async () => { @@ -180,8 +177,7 @@ export function createBlockReplyPipeline(params: { const enqueue = (payload: ReplyPayload) => { if (aborted) return; if (bufferPayload(payload)) return; - const hasMedia = - Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; + const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; if (hasMedia) { void coalescer?.flush({ force: true }); sendPayload(payload); @@ -189,11 +185,7 @@ export function createBlockReplyPipeline(params: { } if (coalescer) { const payloadKey = createBlockReplyPayloadKey(payload); - if ( - seenKeys.has(payloadKey) || - pendingKeys.has(payloadKey) || - bufferedKeys.has(payloadKey) - ) { + if (seenKeys.has(payloadKey) || pendingKeys.has(payloadKey) || bufferedKeys.has(payloadKey)) { return; } bufferedKeys.add(payloadKey); @@ -217,8 +209,7 @@ export function createBlockReplyPipeline(params: { enqueue, flush, stop, - hasBuffered: () => - Boolean(coalescer?.hasBuffered() || bufferedPayloads.length > 0), + hasBuffered: () => Boolean(coalescer?.hasBuffered() || bufferedPayloads.length > 0), didStream: () => didStream, isAborted: () => aborted, hasSentPayload: (payload) => { diff --git a/src/auto-reply/reply/block-streaming.ts b/src/auto-reply/reply/block-streaming.ts index 05a624b2719..ee8ccc11e3c 100644 --- a/src/auto-reply/reply/block-streaming.ts +++ b/src/auto-reply/reply/block-streaming.ts @@ -14,9 +14,7 @@ const BLOCK_CHUNK_PROVIDERS = new Set([ INTERNAL_MESSAGE_CHANNEL, ]); -function normalizeChunkProvider( - provider?: string, -): TextChunkProvider | undefined { +function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined { if (!provider) return undefined; const cleaned = provider.trim().toLowerCase(); return BLOCK_CHUNK_PROVIDERS.has(cleaned as TextChunkProvider) @@ -26,10 +24,7 @@ function normalizeChunkProvider( type ProviderBlockStreamingConfig = { blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - accounts?: Record< - string, - { blockStreamingCoalesce?: BlockStreamingCoalesceConfig } - >; + accounts?: Record; }; function resolveProviderBlockStreamingCoalesce(params: { @@ -72,20 +67,13 @@ export function resolveBlockStreamingChunking( fallbackLimit: providerChunkLimit, }); const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk; - const maxRequested = Math.max( - 1, - Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX), - ); + const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX)); const maxChars = Math.max(1, Math.min(maxRequested, textLimit)); const minFallback = DEFAULT_BLOCK_STREAM_MIN; - const minRequested = Math.max( - 1, - Math.floor(chunkCfg?.minChars ?? minFallback), - ); + const minRequested = Math.max(1, Math.floor(chunkCfg?.minChars ?? minFallback)); const minChars = Math.min(minRequested, maxChars); const breakPreference = - chunkCfg?.breakPreference === "newline" || - chunkCfg?.breakPreference === "sentence" + chunkCfg?.breakPreference === "newline" || chunkCfg?.breakPreference === "sentence" ? chunkCfg.breakPreference : "paragraph"; return { minChars, maxChars, breakPreference }; @@ -117,8 +105,7 @@ export function resolveBlockStreamingCoalescing( providerKey, accountId, }); - const coalesceCfg = - providerCfg ?? cfg?.agents?.defaults?.blockStreamingCoalesce; + const coalesceCfg = providerCfg ?? cfg?.agents?.defaults?.blockStreamingCoalesce; const minRequested = Math.max( 1, Math.floor( @@ -128,23 +115,17 @@ export function resolveBlockStreamingCoalescing( DEFAULT_BLOCK_STREAM_MIN, ), ); - const maxRequested = Math.max( - 1, - Math.floor(coalesceCfg?.maxChars ?? textLimit), - ); + const maxRequested = Math.max(1, Math.floor(coalesceCfg?.maxChars ?? textLimit)); const maxChars = Math.max(1, Math.min(maxRequested, textLimit)); const minChars = Math.min(minRequested, maxChars); const idleMs = Math.max( 0, Math.floor( - coalesceCfg?.idleMs ?? - providerDefaults?.idleMs ?? - DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS, + coalesceCfg?.idleMs ?? providerDefaults?.idleMs ?? DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS, ), ); const preference = chunking?.breakPreference ?? "paragraph"; - const joiner = - preference === "sentence" ? " " : preference === "newline" ? "\n" : "\n\n"; + const joiner = preference === "sentence" ? " " : preference === "newline" ? "\n" : "\n\n"; return { minChars, maxChars, diff --git a/src/auto-reply/reply/body.ts b/src/auto-reply/reply/body.ts index 436262f44b8..c6f0bcbafd7 100644 --- a/src/auto-reply/reply/body.ts +++ b/src/auto-reply/reply/body.ts @@ -30,9 +30,7 @@ export async function applySessionHints(params: { } } - const messageIdHint = params.messageId?.trim() - ? `[message_id: ${params.messageId.trim()}]` - : ""; + const messageIdHint = params.messageId?.trim() ? `[message_id: ${params.messageId.trim()}]` : ""; if (messageIdHint) { prefixedBodyBase = `${prefixedBodyBase}\n${messageIdHint}`; } diff --git a/src/auto-reply/reply/commands-bash.ts b/src/auto-reply/reply/commands-bash.ts index cb7fab30140..4ef36b7210e 100644 --- a/src/auto-reply/reply/commands-bash.ts +++ b/src/auto-reply/reply/commands-bash.ts @@ -2,26 +2,17 @@ import { logVerbose } from "../../globals.js"; import { handleBashChatCommand } from "./bash-command.js"; import type { CommandHandler } from "./commands-types.js"; -export const handleBashCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleBashCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const { command } = params; const bashSlashRequested = - command.commandBodyNormalized === "/bash" || - command.commandBodyNormalized.startsWith("/bash "); + command.commandBodyNormalized === "/bash" || command.commandBodyNormalized.startsWith("/bash "); const bashBangRequested = command.commandBodyNormalized.startsWith("!"); - if ( - !bashSlashRequested && - !(bashBangRequested && command.isAuthorizedSender) - ) { + if (!bashSlashRequested && !(bashBangRequested && command.isAuthorizedSender)) { return null; } if (!command.isAuthorizedSender) { - logVerbose( - `Ignoring /bash from unauthorized sender: ${command.senderId || ""}`, - ); + logVerbose(`Ignoring /bash from unauthorized sender: ${command.senderId || ""}`); return { shouldContinue: false }; } const reply = await handleBashChatCommand({ diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 7ced15ca9e6..da048ec6523 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -73,24 +73,19 @@ export const handleCompactCommand: CommandHandler = async (params) => { skillsSnapshot: params.sessionEntry.skillsSnapshot, provider: params.provider, model: params.model, - thinkLevel: - params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), + thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), bashElevated: { enabled: false, allowed: false, defaultLevel: "off", }, customInstructions, - ownerNumbers: - params.command.ownerList.length > 0 - ? params.command.ownerList - : undefined, + ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, }); const totalTokens = params.sessionEntry.totalTokens ?? - (params.sessionEntry.inputTokens ?? 0) + - (params.sessionEntry.outputTokens ?? 0); + (params.sessionEntry.inputTokens ?? 0) + (params.sessionEntry.outputTokens ?? 0); const contextSummary = formatContextUsageShort( totalTokens > 0 ? totalTokens : null, params.contextTokens ?? params.sessionEntry.contextTokens ?? null, diff --git a/src/auto-reply/reply/commands-config.ts b/src/auto-reply/reply/commands-config.ts index 1f70a09fd40..88a2647db40 100644 --- a/src/auto-reply/reply/commands-config.ts +++ b/src/auto-reply/reply/commands-config.ts @@ -20,14 +20,9 @@ import type { CommandHandler } from "./commands-types.js"; import { parseConfigCommand } from "./config-commands.js"; import { parseDebugCommand } from "./debug-commands.js"; -export const handleConfigCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; - const configCommand = parseConfigCommand( - params.command.commandBodyNormalized, - ); + const configCommand = parseConfigCommand(params.command.commandBodyNormalized); if (!configCommand) return null; if (!params.command.isAuthorizedSender) { logVerbose( @@ -50,11 +45,7 @@ export const handleConfigCommand: CommandHandler = async ( }; } const snapshot = await readConfigFileSnapshot(); - if ( - !snapshot.valid || - !snapshot.parsed || - typeof snapshot.parsed !== "object" - ) { + if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") { return { shouldContinue: false, reply: { @@ -62,9 +53,7 @@ export const handleConfigCommand: CommandHandler = async ( }, }; } - const parsedBase = structuredClone( - snapshot.parsed as Record, - ); + const parsedBase = structuredClone(snapshot.parsed as Record); if (configCommand.action === "show") { const pathRaw = configCommand.path?.trim(); @@ -159,10 +148,7 @@ export const handleConfigCommand: CommandHandler = async ( return null; }; -export const handleDebugCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleDebugCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const debugCommand = parseDebugCommand(params.command.commandBodyNormalized); if (!debugCommand) return null; diff --git a/src/auto-reply/reply/commands-context.ts b/src/auto-reply/reply/commands-context.ts index 6fa2582a731..d72a268f257 100644 --- a/src/auto-reply/reply/commands-context.ts +++ b/src/auto-reply/reply/commands-context.ts @@ -14,8 +14,7 @@ export function buildCommandContext(params: { triggerBodyNormalized: string; commandAuthorized: boolean; }): CommandContext { - const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } = - params; + const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } = params; const auth = resolveCommandAuthorization({ ctx, cfg, @@ -23,13 +22,10 @@ export function buildCommandContext(params: { }); const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase(); const channel = (ctx.Provider ?? surface).trim().toLowerCase(); - const abortKey = - sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined); + const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined); const rawBodyNormalized = triggerBodyNormalized; const commandBodyNormalized = normalizeCommandBody( - isGroup - ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) - : rawBodyNormalized, + isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized, ); return { diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 0246693c080..6229fd6fd64 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -39,9 +39,7 @@ const HANDLERS: CommandHandler[] = [ handleAbortTrigger, ]; -export async function handleCommands( - params: HandleCommandsParams, -): Promise { +export async function handleCommands(params: HandleCommandsParams): Promise { const resetRequested = params.command.commandBodyNormalized === "/reset" || params.command.commandBodyNormalized === "/new"; @@ -71,9 +69,7 @@ export async function handleCommands( chatType: params.sessionEntry?.chatType, }); if (sendPolicy === "deny") { - logVerbose( - `Send blocked by policy for session ${params.sessionKey ?? "unknown"}`, - ); + logVerbose(`Send blocked by policy for session ${params.sessionKey ?? "unknown"}`); return { shouldContinue: false }; } diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts index 1267a29ae35..97875d2f7bd 100644 --- a/src/auto-reply/reply/commands-info.ts +++ b/src/auto-reply/reply/commands-info.ts @@ -3,10 +3,7 @@ import { buildCommandsMessage, buildHelpMessage } from "../status.js"; import { buildStatusReply } from "./commands-status.js"; import type { CommandHandler } from "./commands-types.js"; -export const handleHelpCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleHelpCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (params.command.commandBodyNormalized !== "/help") return null; if (!params.command.isAuthorizedSender) { @@ -21,10 +18,7 @@ export const handleHelpCommand: CommandHandler = async ( }; }; -export const handleCommandsListCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleCommandsListCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (params.command.commandBodyNormalized !== "/commands") return null; if (!params.command.isAuthorizedSender) { @@ -39,14 +33,10 @@ export const handleCommandsListCommand: CommandHandler = async ( }; }; -export const handleStatusCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const statusRequested = - params.directives.hasStatusDirective || - params.command.commandBodyNormalized === "/status"; + params.directives.hasStatusDirective || params.command.commandBodyNormalized === "/status"; if (!statusRequested) return null; if (!params.command.isAuthorizedSender) { logVerbose( @@ -74,10 +64,7 @@ export const handleStatusCommand: CommandHandler = async ( return { shouldContinue: false, reply }; }; -export const handleWhoamiCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleWhoamiCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (params.command.commandBodyNormalized !== "/whoami") return null; if (!params.command.isAuthorizedSender) { @@ -91,9 +78,7 @@ export const handleWhoamiCommand: CommandHandler = async ( const lines = ["🧭 Identity", `Channel: ${params.command.channel}`]; if (senderId) lines.push(`User id: ${senderId}`); if (senderUsername) { - const handle = senderUsername.startsWith("@") - ? senderUsername - : `@${senderUsername}`; + const handle = senderUsername.startsWith("@") ? senderUsername : `@${senderUsername}`; lines.push(`Username: ${handle}`); } if (params.ctx.ChatType === "group" && params.ctx.From) { diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index 8df6c75cc19..8a3f8fa4ac5 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -2,10 +2,7 @@ import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js"; import type { SessionEntry } from "../../config/sessions.js"; import { saveSessionStore } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; -import { - scheduleGatewaySigusr1Restart, - triggerClawdbotRestart, -} from "../../infra/restart.js"; +import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../infra/restart.js"; import { parseAgentSessionKey } from "../../routing/session-key.js"; import { parseActivationCommand } from "../group-activation.js"; import { parseSendPolicyCommand } from "../send-policy.js"; @@ -33,12 +30,8 @@ function resolveAbortTarget(params: { sessionEntry?: SessionEntry; sessionStore?: Record; }) { - const targetSessionKey = - params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey; - const { entry, key } = resolveSessionEntryForKey( - params.sessionStore, - targetSessionKey, - ); + const targetSessionKey = params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey; + const { entry, key } = resolveSessionEntryForKey(params.sessionStore, targetSessionKey); if (entry && key) return { entry, key, sessionId: entry.sessionId }; if (params.sessionEntry && params.sessionKey) { return { @@ -50,14 +43,9 @@ function resolveAbortTarget(params: { return { entry: undefined, key: targetSessionKey, sessionId: undefined }; } -export const handleActivationCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleActivationCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; - const activationCommand = parseActivationCommand( - params.command.commandBodyNormalized, - ); + const activationCommand = parseActivationCommand(params.command.commandBodyNormalized); if (!activationCommand.hasCommand) return null; if (!params.isGroup) { return { @@ -94,14 +82,9 @@ export const handleActivationCommand: CommandHandler = async ( }; }; -export const handleSendPolicyCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleSendPolicyCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; - const sendPolicyCommand = parseSendPolicyCommand( - params.command.commandBodyNormalized, - ); + const sendPolicyCommand = parseSendPolicyCommand(params.command.commandBodyNormalized); if (!sendPolicyCommand.hasCommand) return null; if (!params.command.isAuthorizedSender) { logVerbose( @@ -139,10 +122,7 @@ export const handleSendPolicyCommand: CommandHandler = async ( }; }; -export const handleRestartCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleRestartCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (params.command.commandBodyNormalized !== "/restart") return null; if (!params.command.isAuthorizedSender) { @@ -171,9 +151,7 @@ export const handleRestartCommand: CommandHandler = async ( } const restartMethod = triggerClawdbotRestart(); if (!restartMethod.ok) { - const detail = restartMethod.detail - ? ` Details: ${restartMethod.detail}` - : ""; + const detail = restartMethod.detail ? ` Details: ${restartMethod.detail}` : ""; return { shouldContinue: false, reply: { @@ -189,10 +167,7 @@ export const handleRestartCommand: CommandHandler = async ( }; }; -export const handleStopCommand: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleStopCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (params.command.commandBodyNormalized !== "/stop") return null; if (!params.command.isAuthorizedSender) { @@ -223,10 +198,7 @@ export const handleStopCommand: CommandHandler = async ( return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } }; }; -export const handleAbortTrigger: CommandHandler = async ( - params, - allowTextCommands, -) => { +export const handleAbortTrigger: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; if (!isAbortTrigger(params.command.rawBodyNormalized)) return null; const abortTarget = resolveAbortTarget({ diff --git a/src/auto-reply/reply/commands-status.ts b/src/auto-reply/reply/commands-status.ts index 800d8b2108d..8a2222bbb90 100644 --- a/src/auto-reply/reply/commands-status.ts +++ b/src/auto-reply/reply/commands-status.ts @@ -8,10 +8,7 @@ import { resolveAuthProfileDisplayLabel, resolveAuthProfileOrder, } from "../../agents/auth-profiles.js"; -import { - getCustomProviderApiKey, - resolveEnvApiKey, -} from "../../agents/model-auth.js"; +import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js"; import { normalizeProviderId } from "../../agents/model-selection.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry, SessionScope } from "../../config/sessions.js"; @@ -23,12 +20,7 @@ import { } from "../../infra/provider-usage.js"; import { normalizeGroupActivation } from "../group-activation.js"; import { buildStatusMessage } from "../status.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../thinking.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import type { CommandContext } from "./commands-types.js"; import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js"; @@ -132,9 +124,7 @@ export async function buildStatusReply(params: { defaultGroupActivation, } = params; if (!command.isAuthorizedSender) { - logVerbose( - `Ignoring /status from unauthorized sender: ${command.senderId || ""}`, - ); + logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || ""}`); return undefined; } const statusAgentId = sessionKey @@ -151,10 +141,7 @@ export async function buildStatusReply(params: { agentDir: statusAgentDir, }); usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() }); - if ( - !usageLine && - (resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on") - ) { + if (!usageLine && (resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")) { const entry = usageSummary.providers[0]; if (entry?.error) { usageLine = `📊 Usage: ${entry.displayName} (${entry.error})`; @@ -172,13 +159,10 @@ export async function buildStatusReply(params: { const queueKey = sessionKey ?? sessionEntry?.sessionId; const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0; const queueOverrides = Boolean( - sessionEntry?.queueDebounceMs ?? - sessionEntry?.queueCap ?? - sessionEntry?.queueDrop, + sessionEntry?.queueDebounceMs ?? sessionEntry?.queueCap ?? sessionEntry?.queueDrop, ); const groupActivation = isGroup - ? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? - defaultGroupActivation()) + ? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation()) : undefined; const agentDefaults = cfg.agents?.defaults ?? {}; const statusText = buildStatusMessage({ @@ -202,12 +186,7 @@ export async function buildStatusReply(params: { resolvedVerbose: resolvedVerboseLevel, resolvedReasoning: resolvedReasoningLevel, resolvedElevated: resolvedElevatedLevel, - modelAuth: resolveModelAuthLabel( - provider, - cfg, - sessionEntry, - statusAgentDir, - ), + modelAuth: resolveModelAuthLabel(provider, cfg, sessionEntry, statusAgentDir), usageLine: usageLine ?? undefined, queue: { mode: queueSettings.mode, diff --git a/src/auto-reply/reply/commands-types.ts b/src/auto-reply/reply/commands-types.ts index b4869b17f40..15f3f0b5ec7 100644 --- a/src/auto-reply/reply/commands-types.ts +++ b/src/auto-reply/reply/commands-types.ts @@ -2,12 +2,7 @@ import type { ChannelId } from "../../channels/plugins/types.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry, SessionScope } from "../../config/sessions.js"; import type { MsgContext } from "../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../thinking.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import type { InlineDirectives } from "./directive-handling.js"; diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index c5ca49ce009..995df0f1e59 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -6,11 +6,7 @@ import { resetBashChatCommandForTests } from "./bash-command.js"; import { buildCommandContext, handleCommands } from "./commands.js"; import { parseInlineDirectives } from "./directive-handling.js"; -function buildParams( - commandBody: string, - cfg: ClawdbotConfig, - ctxOverrides?: Partial, -) { +function buildParams(commandBody: string, cfg: ClawdbotConfig, ctxOverrides?: Partial) { const ctx = { Body: commandBody, CommandBody: commandBody, @@ -71,9 +67,7 @@ describe("handleCommands gating", () => { params.elevated = { enabled: true, allowed: false, - failures: [ - { gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" }, - ], + failures: [{ gate: "allowFrom", key: "tools.elevated.allowFrom.whatsapp" }], }; const result = await handleCommands(params); expect(result.shouldContinue).toBe(false); diff --git a/src/auto-reply/reply/config-commands.ts b/src/auto-reply/reply/config-commands.ts index e5cf099b6ac..cd504f9bf0e 100644 --- a/src/auto-reply/reply/config-commands.ts +++ b/src/auto-reply/reply/config-commands.ts @@ -23,8 +23,7 @@ export function parseConfigCommand(raw: string): ConfigCommand | null { case "get": return { action: "show", path: args || undefined }; case "unset": { - if (!args) - return { action: "error", message: "Usage: /config unset path" }; + if (!args) return { action: "error", message: "Usage: /config unset path" }; return { action: "unset", path: args }; } case "set": { diff --git a/src/auto-reply/reply/debug-commands.ts b/src/auto-reply/reply/debug-commands.ts index 9c9ecde4695..09f7d2e1698 100644 --- a/src/auto-reply/reply/debug-commands.ts +++ b/src/auto-reply/reply/debug-commands.ts @@ -24,8 +24,7 @@ export function parseDebugCommand(raw: string): DebugCommand | null { case "reset": return { action: "reset" }; case "unset": { - if (!args) - return { action: "error", message: "Usage: /debug unset path" }; + if (!args) return { action: "error", message: "Usage: /debug unset path" }; return { action: "unset", path: args }; } case "set": { diff --git a/src/auto-reply/reply/directive-handling.auth.ts b/src/auto-reply/reply/directive-handling.auth.ts index 453c32cfe24..8a3602401fc 100644 --- a/src/auto-reply/reply/directive-handling.auth.ts +++ b/src/auto-reply/reply/directive-handling.auth.ts @@ -65,8 +65,7 @@ export const resolveAuthLabel = async ( const configProfile = cfg.auth?.profiles?.[profileId]; const missing = !profile || - (configProfile?.provider && - configProfile.provider !== profile.provider) || + (configProfile?.provider && configProfile.provider !== profile.provider) || (configProfile?.mode && configProfile.mode !== profile.type && !(configProfile.mode === "oauth" && profile.type === "token")); @@ -115,11 +114,7 @@ export const resolveAuthLabel = async ( if (lastGood && profileId === lastGood) flags.push("lastGood"); if (isProfileInCooldown(store, profileId)) { const until = store.usageStats?.[profileId]?.cooldownUntil; - if ( - typeof until === "number" && - Number.isFinite(until) && - until > now - ) { + if (typeof until === "number" && Number.isFinite(until) && until > now) { flags.push(`cooldown ${formatUntil(until)}`); } else { flags.push("cooldown"); @@ -127,8 +122,7 @@ export const resolveAuthLabel = async ( } if ( !profile || - (configProfile?.provider && - configProfile.provider !== profile.provider) || + (configProfile?.provider && configProfile.provider !== profile.provider) || (configProfile?.mode && configProfile.mode !== profile.type && !(configProfile.mode === "oauth" && profile.type === "token")) @@ -146,11 +140,7 @@ export const resolveAuthLabel = async ( Number.isFinite(profile.expires) && profile.expires > 0 ) { - flags.push( - profile.expires <= now - ? "expired" - : `exp ${formatUntil(profile.expires)}`, - ); + flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`); } const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : ""; return `${profileId}=token:${maskApiKey(profile.token)}${suffix}`; @@ -171,11 +161,7 @@ export const resolveAuthLabel = async ( Number.isFinite(profile.expires) && profile.expires > 0 ) { - flags.push( - profile.expires <= now - ? "expired" - : `exp ${formatUntil(profile.expires)}`, - ); + flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`); } const suffixLabel = suffix ? ` ${suffix}` : ""; const suffixFlags = flags.length > 0 ? ` (${flags.join(", ")})` : ""; @@ -199,8 +185,7 @@ export const resolveAuthLabel = async ( if (customKey) { return { label: maskApiKey(customKey), - source: - mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "", + source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "", }; } return { label: "missing", source: "missing" }; diff --git a/src/auto-reply/reply/directive-handling.fast-lane.ts b/src/auto-reply/reply/directive-handling.fast-lane.ts index d63f18ef444..54f3f0375d0 100644 --- a/src/auto-reply/reply/directive-handling.fast-lane.ts +++ b/src/auto-reply/reply/directive-handling.fast-lane.ts @@ -6,12 +6,7 @@ import type { ReplyPayload } from "../types.js"; import { handleDirectiveOnly } from "./directive-handling.impl.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; import { isDirectiveOnly } from "./directive-handling.parse.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "./directives.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js"; export async function applyInlineDirectivesFastLane(params: { directives: InlineDirectives; @@ -45,9 +40,7 @@ export async function applyInlineDirectivesFastLane(params: { resolveDefaultThinkingLevel: () => Promise; allowedModelKeys: Set; allowedModelCatalog: Awaited< - ReturnType< - typeof import("../../agents/model-catalog.js").loadModelCatalog - > + ReturnType >; resetModelOverride: boolean; }; diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 7e78546623f..752e8984e79 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -1,18 +1,11 @@ -import { - resolveAgentDir, - resolveSessionAgentId, -} from "../../agents/agent-scope.js"; +import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js"; import type { ModelAliasIndex } from "../../agents/model-selection.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { type SessionEntry, saveSessionStore } from "../../config/sessions.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { applyVerboseOverride } from "../../sessions/level-overrides.js"; -import { - formatThinkingLevels, - formatXHighModelHint, - supportsXHighThinking, -} from "../thinking.js"; +import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import { maybeHandleModelDirectiveInfo, @@ -28,12 +21,7 @@ import { formatReasoningEvent, withOptions, } from "./directive-handling.shared.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "./directives.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js"; export async function handleDirectiveOnly(params: { cfg: ClawdbotConfig; @@ -95,8 +83,7 @@ export async function handleDirectiveOnly(params: { cfg: params.cfg, sessionKey: params.sessionKey, }).sandboxed; - const shouldHintDirectRuntime = - directives.hasElevatedDirective && !runtimeIsSandboxed; + const shouldHintDirectRuntime = directives.hasElevatedDirective && !runtimeIsSandboxed; const modelInfo = await maybeHandleModelDirectiveInfo({ directives, @@ -161,10 +148,7 @@ export async function handleDirectiveOnly(params: { if (!directives.rawReasoningLevel) { const level = currentReasoningLevel ?? "off"; return { - text: withOptions( - `Current reasoning level: ${level}.`, - "on, off, stream", - ), + text: withOptions(`Current reasoning level: ${level}.`, "on, off, stream"), }; } return { @@ -196,10 +180,7 @@ export async function handleDirectiveOnly(params: { text: `Unrecognized elevated level "${directives.rawElevatedLevel}". Valid levels: off, on.`, }; } - if ( - directives.hasElevatedDirective && - (!elevatedEnabled || !elevatedAllowed) - ) { + if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) { return { text: formatElevatedUnavailableText({ runtimeSandboxed: runtimeIsSandboxed, @@ -229,8 +210,7 @@ export async function handleDirectiveOnly(params: { const nextThinkLevel = directives.hasThinkDirective ? directives.thinkLevel - : ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? - currentThinkLevel); + : ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? currentThinkLevel); const shouldDowngradeXHigh = !directives.hasThinkDirective && nextThinkLevel === "xhigh" && @@ -242,17 +222,14 @@ export async function handleDirectiveOnly(params: { (sessionEntry.elevatedLevel as ElevatedLevel | undefined) ?? (elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel)); const prevReasoningLevel = - currentReasoningLevel ?? - (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? - "off"; + currentReasoningLevel ?? (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off"; let elevatedChanged = directives.hasElevatedDirective && directives.elevatedLevel !== undefined && elevatedEnabled && elevatedAllowed; let reasoningChanged = - directives.hasReasoningDirective && - directives.reasoningLevel !== undefined; + directives.hasReasoningDirective && directives.reasoningLevel !== undefined; if (directives.hasThinkDirective && directives.thinkLevel) { if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel; else sessionEntry.thinkingLevel = directives.thinkLevel; @@ -264,12 +241,10 @@ export async function handleDirectiveOnly(params: { applyVerboseOverride(sessionEntry, directives.verboseLevel); } if (directives.hasReasoningDirective && directives.reasoningLevel) { - if (directives.reasoningLevel === "off") - delete sessionEntry.reasoningLevel; + if (directives.reasoningLevel === "off") delete sessionEntry.reasoningLevel; else sessionEntry.reasoningLevel = directives.reasoningLevel; reasoningChanged = - directives.reasoningLevel !== prevReasoningLevel && - directives.reasoningLevel !== undefined; + directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined; } if (directives.hasElevatedDirective && directives.elevatedLevel) { // Unlike other toggles, elevated defaults can be "on". @@ -277,8 +252,7 @@ export async function handleDirectiveOnly(params: { sessionEntry.elevatedLevel = directives.elevatedLevel; elevatedChanged = elevatedChanged || - (directives.elevatedLevel !== prevElevatedLevel && - directives.elevatedLevel !== undefined); + (directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined); } if (modelSelection) { if (modelSelection.isDefault) { @@ -319,26 +293,21 @@ export async function handleDirectiveOnly(params: { if (modelSelection) { const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; if (nextLabel !== initialModelLabel) { - enqueueSystemEvent( - formatModelSwitchEvent(nextLabel, modelSelection.alias), - { - sessionKey, - contextKey: `model:${nextLabel}`, - }, - ); + enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), { + sessionKey, + contextKey: `model:${nextLabel}`, + }); } } if (elevatedChanged) { - const nextElevated = (sessionEntry.elevatedLevel ?? - "off") as ElevatedLevel; + const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel; enqueueSystemEvent(formatElevatedEvent(nextElevated), { sessionKey, contextKey: "mode:elevated", }); } if (reasoningChanged) { - const nextReasoning = (sessionEntry.reasoningLevel ?? - "off") as ReasoningLevel; + const nextReasoning = (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel; enqueueSystemEvent(formatReasoningEvent(nextReasoning), { sessionKey, contextKey: "mode:reasoning", @@ -385,9 +354,7 @@ export async function handleDirectiveOnly(params: { } if (modelSelection) { const label = `${modelSelection.provider}/${modelSelection.model}`; - const labelWithAlias = modelSelection.alias - ? `${modelSelection.alias} (${label})` - : label; + const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label; parts.push( modelSelection.isDefault ? `Model reset to default (${labelWithAlias}).` @@ -398,27 +365,18 @@ export async function handleDirectiveOnly(params: { } } if (directives.hasQueueDirective && directives.queueMode) { - parts.push( - formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`), - ); + parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`)); } else if (directives.hasQueueDirective && directives.queueReset) { parts.push(formatDirectiveAck("Queue mode reset to default.")); } - if ( - directives.hasQueueDirective && - typeof directives.debounceMs === "number" - ) { - parts.push( - formatDirectiveAck(`Queue debounce set to ${directives.debounceMs}ms.`), - ); + if (directives.hasQueueDirective && typeof directives.debounceMs === "number") { + parts.push(formatDirectiveAck(`Queue debounce set to ${directives.debounceMs}ms.`)); } if (directives.hasQueueDirective && typeof directives.cap === "number") { parts.push(formatDirectiveAck(`Queue cap set to ${directives.cap}.`)); } if (directives.hasQueueDirective && directives.dropPolicy) { - parts.push( - formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`), - ); + parts.push(formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`)); } const ack = parts.join(" ").trim(); if (!ack && directives.hasStatusDirective) return undefined; diff --git a/src/auto-reply/reply/directive-handling.model-picker.ts b/src/auto-reply/reply/directive-handling.model-picker.ts index d09c3bbecf1..98c91775257 100644 --- a/src/auto-reply/reply/directive-handling.model-picker.ts +++ b/src/auto-reply/reply/directive-handling.model-picker.ts @@ -52,9 +52,7 @@ function sortProvidersForPicker(providers: string[]): string[] { }); } -export function buildModelPickerItems( - catalog: ModelPickerCatalogEntry[], -): ModelPickerItem[] { +export function buildModelPickerItems(catalog: ModelPickerCatalogEntry[]): ModelPickerItem[] { const byModel = new Map }>(); for (const entry of catalog) { const provider = normalizeProviderId(entry.provider); @@ -72,9 +70,7 @@ export function buildModelPickerItems( const providers = sortProvidersForPicker(Object.keys(data.providerModels)); out.push({ model, providers, providerModels: data.providerModels }); } - out.sort((a, b) => - a.model.toLowerCase().localeCompare(b.model.toLowerCase()), - ); + out.sort((a, b) => a.model.toLowerCase().localeCompare(b.model.toLowerCase())); return out; } diff --git a/src/auto-reply/reply/directive-handling.model.ts b/src/auto-reply/reply/directive-handling.model.ts index ec311b23a79..b63f2c6d199 100644 --- a/src/auto-reply/reply/directive-handling.model.ts +++ b/src/auto-reply/reply/directive-handling.model.ts @@ -22,10 +22,7 @@ import { resolveProviderEndpointLabel, } from "./directive-handling.model-picker.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; -import { - type ModelDirectiveSelection, - resolveModelDirectiveSelection, -} from "./model-selection.js"; +import { type ModelDirectiveSelection, resolveModelDirectiveSelection } from "./model-selection.js"; function buildModelPickerCatalog(params: { cfg: ClawdbotConfig; @@ -127,10 +124,7 @@ export async function maybeHandleModelDirectiveInfo(params: { const items = buildModelPickerItems(pickerCatalog); if (items.length === 0) return { text: "No models available." }; const current = `${params.provider}/${params.model}`; - const lines: string[] = [ - `Current: ${current}`, - "Pick: /model <#> or /model ", - ]; + const lines: string[] = [`Current: ${current}`, "Pick: /model <#> or /model "]; for (const [idx, item] of items.entries()) { lines.push(`${idx + 1}) ${item.model} — ${item.providers.join(", ")}`); } @@ -194,8 +188,7 @@ export async function maybeHandleModelDirectiveInfo(params: { for (const entry of models) { const label = `${provider}/${entry.id}`; const aliases = params.aliasIndex.byKey.get(label); - const aliasSuffix = - aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : ""; + const aliasSuffix = aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : ""; lines.push(` • ${label}${aliasSuffix}`); } } @@ -217,10 +210,7 @@ export function resolveModelSelectionFromDirective(params: { profileOverride?: string; errorText?: string; } { - if ( - !params.directives.hasModelDirective || - !params.directives.rawModelDirective - ) { + if (!params.directives.hasModelDirective || !params.directives.rawModelDirective) { if (params.directives.rawModelProfile) { return { errorText: "Auth profile override requires a model selection." }; } @@ -261,9 +251,7 @@ export function resolveModelSelectionFromDirective(params: { modelSelection = { provider: picked.provider, model: picked.model, - isDefault: - picked.provider === params.defaultProvider && - picked.model === params.defaultModel, + isDefault: picked.provider === params.defaultProvider && picked.model === params.defaultModel, ...(alias ? { alias } : {}), }; } else { diff --git a/src/auto-reply/reply/directive-handling.parse.ts b/src/auto-reply/reply/directive-handling.parse.ts index 23bf4ee211b..ef2deb32f7a 100644 --- a/src/auto-reply/reply/directive-handling.parse.ts +++ b/src/auto-reply/reply/directive-handling.parse.ts @@ -1,12 +1,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { extractModelDirective } from "../model.js"; import type { MsgContext } from "../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "./directives.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js"; import { extractElevatedDirective, extractReasoningDirective, @@ -89,10 +84,9 @@ export function parseInlineDirectives( } : extractElevatedDirective(reasoningCleaned); const allowStatusDirective = options?.allowStatusDirective !== false; - const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = - allowStatusDirective - ? extractStatusDirective(elevatedCleaned) - : { cleaned: elevatedCleaned, hasDirective: false }; + const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = allowStatusDirective + ? extractStatusDirective(elevatedCleaned) + : { cleaned: elevatedCleaned, hasDirective: false }; const { cleaned: modelCleaned, rawModel, @@ -167,8 +161,6 @@ export function isDirectiveOnly(params: { ) return false; const stripped = stripStructuralPrefixes(cleanedBody ?? ""); - const noMentions = isGroup - ? stripMentions(stripped, ctx, cfg, agentId) - : stripped; + const noMentions = isGroup ? stripMentions(stripped, ctx, cfg, agentId) : stripped; return noMentions.length === 0; } diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 8764f0c7840..27bfdcc28ca 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -5,11 +5,7 @@ import { resolveSessionAgentId, } from "../../agents/agent-scope.js"; import { lookupContextTokens } from "../../agents/context.js"; -import { - DEFAULT_CONTEXT_TOKENS, - DEFAULT_MODEL, - DEFAULT_PROVIDER, -} from "../../agents/defaults.js"; +import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; import { buildModelAliasIndex, type ModelAliasIndex, @@ -23,10 +19,7 @@ import { enqueueSystemEvent } from "../../infra/system-events.js"; import { applyVerboseOverride } from "../../sessions/level-overrides.js"; import { resolveProfileOverride } from "./directive-handling.auth.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; -import { - formatElevatedEvent, - formatReasoningEvent, -} from "./directive-handling.shared.js"; +import { formatElevatedEvent, formatReasoningEvent } from "./directive-handling.shared.js"; import type { ElevatedLevel, ReasoningLevel } from "./directives.js"; export async function persistInlineDirectives(params: { @@ -78,16 +71,14 @@ export async function persistInlineDirectives(params: { (sessionEntry.elevatedLevel as ElevatedLevel | undefined) ?? (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ?? (elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel)); - const prevReasoningLevel = - (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off"; + const prevReasoningLevel = (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off"; let elevatedChanged = directives.hasElevatedDirective && directives.elevatedLevel !== undefined && elevatedEnabled && elevatedAllowed; let reasoningChanged = - directives.hasReasoningDirective && - directives.reasoningLevel !== undefined; + directives.hasReasoningDirective && directives.reasoningLevel !== undefined; let updated = false; if (directives.hasThinkDirective && directives.thinkLevel) { @@ -124,8 +115,7 @@ export async function persistInlineDirectives(params: { sessionEntry.elevatedLevel = directives.elevatedLevel; elevatedChanged = elevatedChanged || - (directives.elevatedLevel !== prevElevatedLevel && - directives.elevatedLevel !== undefined); + (directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined); updated = true; } @@ -156,8 +146,7 @@ export async function persistInlineDirectives(params: { profileOverride = profileResolved.profileId; } const isDefault = - resolved.ref.provider === defaultProvider && - resolved.ref.model === defaultModel; + resolved.ref.provider === defaultProvider && resolved.ref.model === defaultModel; if (isDefault) { delete sessionEntry.providerOverride; delete sessionEntry.modelOverride; @@ -174,13 +163,10 @@ export async function persistInlineDirectives(params: { model = resolved.ref.model; const nextLabel = `${provider}/${model}`; if (nextLabel !== initialModelLabel) { - enqueueSystemEvent( - formatModelSwitchEvent(nextLabel, resolved.alias), - { - sessionKey, - contextKey: `model:${nextLabel}`, - }, - ); + enqueueSystemEvent(formatModelSwitchEvent(nextLabel, resolved.alias), { + sessionKey, + contextKey: `model:${nextLabel}`, + }); } updated = true; } @@ -201,16 +187,14 @@ export async function persistInlineDirectives(params: { await saveSessionStore(storePath, sessionStore); } if (elevatedChanged) { - const nextElevated = (sessionEntry.elevatedLevel ?? - "off") as ElevatedLevel; + const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel; enqueueSystemEvent(formatElevatedEvent(nextElevated), { sessionKey, contextKey: "mode:elevated", }); } if (reasoningChanged) { - const nextReasoning = (sessionEntry.reasoningLevel ?? - "off") as ReasoningLevel; + const nextReasoning = (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel; enqueueSystemEvent(formatReasoningEvent(nextReasoning), { sessionKey, contextKey: "mode:reasoning", @@ -222,17 +206,11 @@ export async function persistInlineDirectives(params: { return { provider, model, - contextTokens: - agentCfg?.contextTokens ?? - lookupContextTokens(model) ?? - DEFAULT_CONTEXT_TOKENS, + contextTokens: agentCfg?.contextTokens ?? lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS, }; } -export function resolveDefaultModel(params: { - cfg: ClawdbotConfig; - agentId?: string; -}): { +export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: string }): { defaultProvider: string; defaultModel: string; aliasIndex: ModelAliasIndex; diff --git a/src/auto-reply/reply/directive-handling.queue-validation.ts b/src/auto-reply/reply/directive-handling.queue-validation.ts index f5d3581e7bf..55c0174d61b 100644 --- a/src/auto-reply/reply/directive-handling.queue-validation.ts +++ b/src/auto-reply/reply/directive-handling.queue-validation.ts @@ -29,11 +29,8 @@ export function maybeHandleQueueDirective(params: { sessionEntry: params.sessionEntry, }); const debounceLabel = - typeof settings.debounceMs === "number" - ? `${settings.debounceMs}ms` - : "default"; - const capLabel = - typeof settings.cap === "number" ? String(settings.cap) : "default"; + typeof settings.debounceMs === "number" ? `${settings.debounceMs}ms` : "default"; + const capLabel = typeof settings.cap === "number" ? String(settings.cap) : "default"; const dropLabel = settings.dropPolicy ?? "default"; return { text: withOptions( @@ -44,23 +41,13 @@ export function maybeHandleQueueDirective(params: { } const queueModeInvalid = - !directives.queueMode && - !directives.queueReset && - Boolean(directives.rawQueueMode); + !directives.queueMode && !directives.queueReset && Boolean(directives.rawQueueMode); const queueDebounceInvalid = - directives.rawDebounce !== undefined && - typeof directives.debounceMs !== "number"; - const queueCapInvalid = - directives.rawCap !== undefined && typeof directives.cap !== "number"; - const queueDropInvalid = - directives.rawDrop !== undefined && !directives.dropPolicy; + directives.rawDebounce !== undefined && typeof directives.debounceMs !== "number"; + const queueCapInvalid = directives.rawCap !== undefined && typeof directives.cap !== "number"; + const queueDropInvalid = directives.rawDrop !== undefined && !directives.dropPolicy; - if ( - queueModeInvalid || - queueDebounceInvalid || - queueCapInvalid || - queueDropInvalid - ) { + if (queueModeInvalid || queueDebounceInvalid || queueCapInvalid || queueDropInvalid) { const errors: string[] = []; if (queueModeInvalid) { errors.push( diff --git a/src/auto-reply/reply/directive-handling.shared.ts b/src/auto-reply/reply/directive-handling.shared.ts index b389ff41b18..07da3cb31b1 100644 --- a/src/auto-reply/reply/directive-handling.shared.ts +++ b/src/auto-reply/reply/directive-handling.shared.ts @@ -37,9 +37,7 @@ export function formatElevatedUnavailableText(params: { ); const failures = params.failures ?? []; if (failures.length > 0) { - lines.push( - `Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`, - ); + lines.push(`Failing gates: ${failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`); } else { lines.push( "Fix-it keys: tools.elevated.enabled, tools.elevated.allowFrom., agents.list[].tools.elevated.*", diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 0e529357840..92c1783bcc1 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -1,12 +1,6 @@ export { applyInlineDirectivesFastLane } from "./directive-handling.fast-lane.js"; export * from "./directive-handling.impl.js"; export type { InlineDirectives } from "./directive-handling.parse.js"; -export { - isDirectiveOnly, - parseInlineDirectives, -} from "./directive-handling.parse.js"; -export { - persistInlineDirectives, - resolveDefaultModel, -} from "./directive-handling.persist.js"; +export { isDirectiveOnly, parseInlineDirectives } from "./directive-handling.parse.js"; +export { persistInlineDirectives, resolveDefaultModel } from "./directive-handling.persist.js"; export { formatDirectiveAck } from "./directive-handling.shared.js"; diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index c6f431b3018..95aea371e50 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -16,17 +16,14 @@ type ExtractedLevel = { hasDirective: boolean; }; -const escapeRegExp = (value: string) => - value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const matchLevelDirective = ( body: string, names: string[], ): { start: number; end: number; rawLevel?: string } | null => { const namePattern = names.map(escapeRegExp).join("|"); - const match = body.match( - new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"), - ); + const match = body.match(new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i")); if (!match || match.index === undefined) return null; const start = match.index; let end = match.index + match[0].length; @@ -76,9 +73,7 @@ const extractSimpleDirective = ( const match = body.match( new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"), ); - const cleaned = match - ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() - : body.trim(); + const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim(); return { cleaned, hasDirective: Boolean(match), @@ -92,11 +87,7 @@ export function extractThinkDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const extracted = extractLevelDirective( - body, - ["thinking", "think", "t"], - normalizeThinkLevel, - ); + const extracted = extractLevelDirective(body, ["thinking", "think", "t"], normalizeThinkLevel); return { cleaned: extracted.cleaned, thinkLevel: extracted.level, @@ -112,11 +103,7 @@ export function extractVerboseDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const extracted = extractLevelDirective( - body, - ["verbose", "v"], - normalizeVerboseLevel, - ); + const extracted = extractLevelDirective(body, ["verbose", "v"], normalizeVerboseLevel); return { cleaned: extracted.cleaned, verboseLevel: extracted.level, @@ -132,11 +119,7 @@ export function extractElevatedDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const extracted = extractLevelDirective( - body, - ["elevated", "elev"], - normalizeElevatedLevel, - ); + const extracted = extractLevelDirective(body, ["elevated", "elev"], normalizeElevatedLevel); return { cleaned: extracted.cleaned, elevatedLevel: extracted.level, @@ -152,11 +135,7 @@ export function extractReasoningDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const extracted = extractLevelDirective( - body, - ["reasoning", "reason"], - normalizeReasoningLevel, - ); + const extracted = extractLevelDirective(body, ["reasoning", "reason"], normalizeReasoningLevel); return { cleaned: extracted.cleaned, reasoningLevel: extracted.level, diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index 7feb7915863..96cd2942d2c 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -17,14 +17,7 @@ vi.mock("./route-reply.js", () => ({ isRoutableChannel: (channel: string | undefined) => Boolean( channel && - [ - "telegram", - "slack", - "discord", - "signal", - "imessage", - "whatsapp", - ].includes(channel), + ["telegram", "slack", "discord", "signal", "imessage", "whatsapp"].includes(channel), ), routeReply: mocks.routeReply, })); diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 18b562e1ab1..7b81d9c949a 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -37,9 +37,7 @@ export async function dispatchReplyFromConfig(params: { const originatingTo = ctx.OriginatingTo; const currentSurface = (ctx.Surface ?? ctx.Provider)?.toLowerCase(); const shouldRouteToOriginating = - isRoutableChannel(originatingChannel) && - originatingTo && - originatingChannel !== currentSurface; + isRoutableChannel(originatingChannel) && originatingTo && originatingChannel !== currentSurface; /** * Helper to send a payload via route-reply (async). @@ -66,9 +64,7 @@ export async function dispatchReplyFromConfig(params: { abortSignal, }); if (!result.ok) { - logVerbose( - `dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`, - ); + logVerbose(`dispatch-from-config: route-reply failed: ${result.error ?? "unknown error"}`); } }; @@ -129,11 +125,7 @@ export async function dispatchReplyFromConfig(params: { cfg, ); - const replies = replyResult - ? Array.isArray(replyResult) - ? replyResult - : [replyResult] - : []; + const replies = replyResult ? (Array.isArray(replyResult) ? replyResult : [replyResult]) : []; let queuedFinal = false; let routedFinalCount = 0; diff --git a/src/auto-reply/reply/followup-runner.compaction.test.ts b/src/auto-reply/reply/followup-runner.compaction.test.ts index 1e4e6336b2d..7ea021764fb 100644 --- a/src/auto-reply/reply/followup-runner.compaction.test.ts +++ b/src/auto-reply/reply/followup-runner.compaction.test.ts @@ -48,10 +48,7 @@ describe("createFollowupRunner compaction", () => { runEmbeddedPiAgentMock.mockImplementationOnce( async (params: { - onAgentEvent?: (evt: { - stream: string; - data: Record; - }) => void; + onAgentEvent?: (evt: { stream: string; data: Record }) => void; }) => { params.onAgentEvent?.({ stream: "compaction", @@ -102,9 +99,7 @@ describe("createFollowupRunner compaction", () => { await runner(queued); expect(onBlockReply).toHaveBeenCalled(); - expect(onBlockReply.mock.calls[0][0].text).toContain( - "Auto-compaction complete", - ); + expect(onBlockReply.mock.calls[0][0].text).toContain("Auto-compaction complete"); expect(sessionStore.main.compactionCount).toBe(1); }); }); diff --git a/src/auto-reply/reply/followup-runner.messaging-tools.test.ts b/src/auto-reply/reply/followup-runner.messaging-tools.test.ts index 1b0962221b0..dd080eedc95 100644 --- a/src/auto-reply/reply/followup-runner.messaging-tools.test.ts +++ b/src/auto-reply/reply/followup-runner.messaging-tools.test.ts @@ -103,9 +103,7 @@ describe("createFollowupRunner messaging tool dedupe", () => { runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], - messagingToolSentTargets: [ - { tool: "slack", provider: "slack", to: "channel:C1" }, - ], + messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }], meta: {}, }); diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index e0ae80b8ba4..a8df22e5d21 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -66,14 +66,10 @@ export function createFollowupRunner(params: { * session's current dispatcher. This ensures replies go back to * where the message originated. */ - const sendFollowupPayloads = async ( - payloads: ReplyPayload[], - queued: FollowupRun, - ) => { + const sendFollowupPayloads = async (payloads: ReplyPayload[], queued: FollowupRun) => { // Check if we should route to originating channel. const { originatingChannel, originatingTo } = queued; - const shouldRouteToOriginating = - isRoutableChannel(originatingChannel) && originatingTo; + const shouldRouteToOriginating = isRoutableChannel(originatingChannel) && originatingTo; if (!shouldRouteToOriginating && !opts?.onBlockReply) { logVerbose("followup queue: no onBlockReply handler; dropping payloads"); @@ -168,8 +164,7 @@ export function createFollowupRunner(params: { blockReplyBreak: queued.run.blockReplyBreak, onAgentEvent: (evt) => { if (evt.stream !== "compaction") return; - const phase = - typeof evt.data.phase === "string" ? evt.data.phase : ""; + const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; const willRetry = Boolean(evt.data.willRetry); if (phase === "end" && !willRetry) { autoCompactionCompleted = true; @@ -182,9 +177,7 @@ export function createFollowupRunner(params: { fallbackModel = fallbackResult.model; } catch (err) { const message = err instanceof Error ? err.message : String(err); - defaultRuntime.error?.( - `Followup agent failed before reply: ${message}`, - ); + defaultRuntime.error?.(`Followup agent failed before reply: ${message}`); return; } @@ -194,16 +187,13 @@ export function createFollowupRunner(params: { const text = payload.text; if (!text || !text.includes("HEARTBEAT_OK")) return [payload]; const stripped = stripHeartbeatToken(text, { mode: "message" }); - const hasMedia = - Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; + const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; if (stripped.shouldSkip && !hasMedia) return []; return [{ ...payload, text: stripped.text }]; }); const replyToChannel = queued.originatingChannel ?? - (queued.run.messageProvider?.toLowerCase() as - | OriginatingChannelType - | undefined); + (queued.run.messageProvider?.toLowerCase() as OriginatingChannelType | undefined); const replyToMode = resolveReplyToMode( queued.run.config, replyToChannel, @@ -247,8 +237,7 @@ export function createFollowupRunner(params: { if (storePath && sessionKey) { const usage = runResult.meta.agentMeta?.usage; - const modelUsed = - runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel; + const modelUsed = runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel; const contextTokensUsed = agentCfgContextTokens ?? lookupContextTokens(modelUsed) ?? @@ -263,13 +252,11 @@ export function createFollowupRunner(params: { update: async (entry) => { const input = usage.input ?? 0; const output = usage.output ?? 0; - const promptTokens = - input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); + const promptTokens = input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); return { inputTokens: input, outputTokens: output, - totalTokens: - promptTokens > 0 ? promptTokens : (usage.total ?? input), + totalTokens: promptTokens > 0 ? promptTokens : (usage.total ?? input), modelProvider: fallbackProvider ?? entry.modelProvider, model: modelUsed, contextTokens: contextTokensUsed ?? entry.contextTokens, @@ -278,9 +265,7 @@ export function createFollowupRunner(params: { }, }); } catch (err) { - logVerbose( - `failed to persist followup usage update: ${String(err)}`, - ); + logVerbose(`failed to persist followup usage update: ${String(err)}`); } } else if (modelUsed || contextTokensUsed) { try { @@ -295,9 +280,7 @@ export function createFollowupRunner(params: { }), }); } catch (err) { - logVerbose( - `failed to persist followup model/context update: ${String(err)}`, - ); + logVerbose(`failed to persist followup model/context update: ${String(err)}`); } } } diff --git a/src/auto-reply/reply/get-reply-directives-apply.ts b/src/auto-reply/reply/get-reply-directives-apply.ts index a167e0a8645..dfc1b69233b 100644 --- a/src/auto-reply/reply/get-reply-directives-apply.ts +++ b/src/auto-reply/reply/get-reply-directives-apply.ts @@ -1,12 +1,7 @@ import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { MsgContext } from "../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../thinking.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import { buildStatusReply } from "./commands.js"; import { @@ -176,11 +171,7 @@ export async function applyInlineDirectiveOverrides(params: { currentElevatedLevel, }); let statusReply: ReplyPayload | undefined; - if ( - directives.hasStatusDirective && - allowTextCommands && - command.isAuthorizedSender - ) { + if (directives.hasStatusDirective && allowTextCommands && command.isAuthorizedSender) { statusReply = await buildStatusReply({ cfg, command, @@ -192,8 +183,7 @@ export async function applyInlineDirectiveOverrides(params: { contextTokens, resolvedThinkLevel: resolvedDefaultThinkLevel, resolvedVerboseLevel: (currentVerboseLevel ?? "off") as VerboseLevel, - resolvedReasoningLevel: (currentReasoningLevel ?? - "off") as ReasoningLevel, + resolvedReasoningLevel: (currentReasoningLevel ?? "off") as ReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel: async () => resolvedDefaultThinkLevel, isGroup, @@ -284,9 +274,7 @@ export async function applyInlineDirectiveOverrides(params: { contextTokens = persisted.contextTokens; const perMessageQueueMode = - directives.hasQueueDirective && !directives.queueReset - ? directives.queueMode - : undefined; + directives.hasQueueDirective && !directives.queueReset ? directives.queueMode : undefined; const perMessageQueueOptions = directives.hasQueueDirective && !directives.queueReset ? { diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index bee7124741b..788c6c3ccd7 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -2,43 +2,19 @@ import type { ModelAliasIndex } from "../../agents/model-selection.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; -import { - listChatCommands, - shouldHandleTextCommands, -} from "../commands-registry.js"; +import { listChatCommands, shouldHandleTextCommands } from "../commands-registry.js"; import type { MsgContext, TemplateContext } from "../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../thinking.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { resolveBlockStreamingChunking } from "./block-streaming.js"; import { buildCommandContext } from "./commands.js"; -import { - type InlineDirectives, - parseInlineDirectives, -} from "./directive-handling.js"; +import { type InlineDirectives, parseInlineDirectives } from "./directive-handling.js"; import { applyInlineDirectiveOverrides } from "./get-reply-directives-apply.js"; import { clearInlineDirectives } from "./get-reply-directives-utils.js"; -import { - defaultGroupActivation, - resolveGroupRequireMention, -} from "./groups.js"; -import { - CURRENT_MESSAGE_MARKER, - stripMentions, - stripStructuralPrefixes, -} from "./mentions.js"; -import { - createModelSelectionState, - resolveContextTokens, -} from "./model-selection.js"; -import { - formatElevatedUnavailableMessage, - resolveElevatedPermissions, -} from "./reply-elevated.js"; +import { defaultGroupActivation, resolveGroupRequireMention } from "./groups.js"; +import { CURRENT_MESSAGE_MARKER, stripMentions, stripStructuralPrefixes } from "./mentions.js"; +import { createModelSelectionState, resolveContextTokens } from "./model-selection.js"; +import { formatElevatedUnavailableMessage, resolveElevatedPermissions } from "./reply-elevated.js"; import { stripInlineStatus } from "./reply-inline.js"; import type { TypingController } from "./typing.js"; @@ -95,12 +71,8 @@ export async function resolveReplyDirectives(params: { sessionStore?: Record; sessionKey: string; storePath?: string; - sessionScope: Parameters< - typeof applyInlineDirectiveOverrides - >[0]["sessionScope"]; - groupResolution: Parameters< - typeof resolveGroupRequireMention - >[0]["groupResolution"]; + sessionScope: Parameters[0]["sessionScope"]; + groupResolution: Parameters[0]["groupResolution"]; isGroup: boolean; triggerBodyNormalized: string; commandAuthorized: boolean; @@ -175,19 +147,14 @@ export async function resolveReplyDirectives(params: { allowStatusDirective, }); const hasInlineStatus = - parsedDirectives.hasStatusDirective && - parsedDirectives.cleaned.trim().length > 0; + parsedDirectives.hasStatusDirective && parsedDirectives.cleaned.trim().length > 0; if (hasInlineStatus) { parsedDirectives = { ...parsedDirectives, hasStatusDirective: false, }; } - if ( - isGroup && - ctx.WasMentioned !== true && - parsedDirectives.hasElevatedDirective - ) { + if (isGroup && ctx.WasMentioned !== true && parsedDirectives.hasElevatedDirective) { if (parsedDirectives.elevatedLevel !== "off") { parsedDirectives = { ...parsedDirectives, @@ -206,18 +173,14 @@ export async function resolveReplyDirectives(params: { parsedDirectives.hasQueueDirective; if (hasInlineDirective) { const stripped = stripStructuralPrefixes(parsedDirectives.cleaned); - const noMentions = isGroup - ? stripMentions(stripped, ctx, cfg, agentId) - : stripped; + const noMentions = isGroup ? stripMentions(stripped, ctx, cfg, agentId) : stripped; if (noMentions.trim().length > 0) { const directiveOnlyCheck = parseInlineDirectives(noMentions, { modelAliases: configuredAliases, }); if (directiveOnlyCheck.cleaned.trim().length > 0) { const allowInlineStatus = - parsedDirectives.hasStatusDirective && - allowTextCommands && - command.isAuthorizedSender; + parsedDirectives.hasStatusDirective && allowTextCommands && command.isAuthorizedSender; parsedDirectives = allowInlineStatus ? { ...clearInlineDirectives(parsedDirectives.cleaned), @@ -257,13 +220,8 @@ export async function resolveReplyDirectives(params: { }).cleaned; } - const head = existingBody.slice( - 0, - markerIndex + CURRENT_MESSAGE_MARKER.length, - ); - const tail = existingBody.slice( - markerIndex + CURRENT_MESSAGE_MARKER.length, - ); + const head = existingBody.slice(0, markerIndex + CURRENT_MESSAGE_MARKER.length); + const tail = existingBody.slice(markerIndex + CURRENT_MESSAGE_MARKER.length); const cleanedTail = parseInlineDirectives(tail, { modelAliases: configuredAliases, allowStatusDirective, @@ -279,9 +237,7 @@ export async function resolveReplyDirectives(params: { sessionCtx.BodyStripped = cleanedBody; const messageProviderKey = - sessionCtx.Provider?.trim().toLowerCase() ?? - ctx.Provider?.trim().toLowerCase() ?? - ""; + sessionCtx.Provider?.trim().toLowerCase() ?? ctx.Provider?.trim().toLowerCase() ?? ""; const elevated = resolveElevatedPermissions({ cfg, agentId, @@ -291,10 +247,7 @@ export async function resolveReplyDirectives(params: { const elevatedEnabled = elevated.enabled; const elevatedAllowed = elevated.allowed; const elevatedFailures = elevated.failures; - if ( - directives.hasElevatedDirective && - (!elevatedEnabled || !elevatedAllowed) - ) { + if (directives.hasElevatedDirective && (!elevatedEnabled || !elevatedAllowed)) { typing.cleanup(); const runtimeSandboxed = resolveSandboxRuntimeStatus({ cfg, @@ -346,17 +299,11 @@ export async function resolveReplyDirectives(params: { ? "on" : "off"; const resolvedBlockStreamingBreak: "text_end" | "message_end" = - agentCfg?.blockStreamingBreak === "message_end" - ? "message_end" - : "text_end"; + agentCfg?.blockStreamingBreak === "message_end" ? "message_end" : "text_end"; const blockStreamingEnabled = resolvedBlockStreaming === "on" && opts?.disableBlockStreaming !== true; const blockReplyChunking = blockStreamingEnabled - ? resolveBlockStreamingChunking( - cfg, - sessionCtx.Provider, - sessionCtx.AccountId, - ) + ? resolveBlockStreamingChunking(cfg, sessionCtx.Provider, sessionCtx.AccountId) : undefined; const modelState = await createModelSelectionState({ @@ -382,20 +329,13 @@ export async function resolveReplyDirectives(params: { const initialModelLabel = `${provider}/${model}`; const formatModelSwitchEvent = (label: string, alias?: string) => - alias - ? `Model switched to ${alias} (${label}).` - : `Model switched to ${label}.`; + alias ? `Model switched to ${alias} (${label}).` : `Model switched to ${label}.`; const isModelListAlias = directives.hasModelDirective && - ["status", "list"].includes( - directives.rawModelDirective?.trim().toLowerCase() ?? "", - ); - const effectiveModelDirective = isModelListAlias - ? undefined - : directives.rawModelDirective; + ["status", "list"].includes(directives.rawModelDirective?.trim().toLowerCase() ?? ""); + const effectiveModelDirective = isModelListAlias ? undefined : directives.rawModelDirective; - const inlineStatusRequested = - hasInlineStatus && allowTextCommands && command.isAuthorizedSender; + const inlineStatusRequested = hasInlineStatus && allowTextCommands && command.isAuthorizedSender; const applyResult = await applyInlineDirectiveOverrides({ ctx, @@ -437,8 +377,7 @@ export async function resolveReplyDirectives(params: { provider = applyResult.provider; model = applyResult.model; contextTokens = applyResult.contextTokens; - const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = - applyResult; + const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult; return { kind: "continue", diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index 7886ba0f21f..6c9d72c241d 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -2,12 +2,7 @@ import { getChannelDock } from "../../channels/dock.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import type { MsgContext, TemplateContext } from "../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../thinking.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { getAbortMemory } from "./abort.js"; import { buildStatusReply, handleCommands } from "./commands.js"; @@ -47,9 +42,7 @@ export async function handleInlineActions(params: { elevatedEnabled: boolean; elevatedAllowed: boolean; elevatedFailures: Array<{ gate: string; key: string }>; - defaultActivation: Parameters< - typeof buildStatusReply - >[0]["defaultGroupActivation"]; + defaultActivation: Parameters[0]["defaultGroupActivation"]; resolvedThinkLevel: ThinkLevel | undefined; resolvedVerboseLevel: VerboseLevel | undefined; resolvedReasoningLevel: ReasoningLevel; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 93c910f0681..c5287bca882 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -163,8 +163,7 @@ export async function runPreparedReply( isHeartbeat, }); const shouldInjectGroupIntro = Boolean( - isGroupChat && - (isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro), + isGroupChat && (isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro), ); const groupIntro = shouldInjectGroupIntro ? buildGroupIntro({ @@ -176,17 +175,10 @@ export async function runPreparedReply( }) : ""; const groupSystemPrompt = sessionCtx.GroupSystemPrompt?.trim() ?? ""; - const extraSystemPrompt = [groupIntro, groupSystemPrompt] - .filter(Boolean) - .join("\n\n"); + const extraSystemPrompt = [groupIntro, groupSystemPrompt].filter(Boolean).join("\n\n"); const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""; // Use CommandBody/RawBody for bare reset detection (clean message without structural context). - const rawBodyTrimmed = ( - ctx.CommandBody ?? - ctx.RawBody ?? - ctx.Body ?? - "" - ).trim(); + const rawBodyTrimmed = (ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "").trim(); const baseBodyTrimmedRaw = baseBody.trim(); if ( allowTextCommands && @@ -198,12 +190,8 @@ export async function runPreparedReply( return undefined; } const isBareSessionReset = - isNewSession && - baseBodyTrimmedRaw.length === 0 && - rawBodyTrimmed.length > 0; - const baseBodyFinal = isBareSessionReset - ? BARE_SESSION_RESET_PROMPT - : baseBody; + isNewSession && baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0; + const baseBodyFinal = isBareSessionReset ? BARE_SESSION_RESET_PROMPT : baseBody; const baseBodyTrimmed = baseBodyFinal.trim(); if (!baseBodyTrimmed) { await typing.onReplyStart(); @@ -223,10 +211,8 @@ export async function runPreparedReply( abortKey: command.abortKey, messageId: sessionCtx.MessageSid, }); - const isGroupSession = - sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room"; - const isMainSession = - !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey); + const isGroupSession = sessionEntry?.chatType === "group" || sessionEntry?.chatType === "room"; + const isMainSession = !isGroupSession && sessionKey === normalizeMainKey(sessionCfg?.mainKey); prefixedBodyBase = await prependSystemEvents({ cfg, sessionKey, @@ -263,18 +249,12 @@ export async function runPreparedReply( ? "To send an image back, add a line like: MEDIA:https://example.com/image.jpg (no spaces). Keep caption in the text body." : undefined; let prefixedCommandBody = mediaNote - ? [mediaNote, mediaReplyHint, prefixedBody ?? ""] - .filter(Boolean) - .join("\n") - .trim() + ? [mediaNote, mediaReplyHint, prefixedBody ?? ""].filter(Boolean).join("\n").trim() : prefixedBody; if (!resolvedThinkLevel && prefixedCommandBody) { const parts = prefixedCommandBody.split(/\s+/); const maybeLevel = normalizeThinkLevel(parts[0]); - if ( - maybeLevel && - (maybeLevel !== "xhigh" || supportsXHighThinking(provider, model)) - ) { + if (maybeLevel && (maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))) { resolvedThinkLevel = maybeLevel; prefixedCommandBody = parts.slice(1).join(" ").trim(); } @@ -282,12 +262,8 @@ export async function runPreparedReply( if (!resolvedThinkLevel) { resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel(); } - if ( - resolvedThinkLevel === "xhigh" && - !supportsXHighThinking(provider, model) - ) { - const explicitThink = - directives.hasThinkDirective && directives.thinkLevel !== undefined; + if (resolvedThinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) { + const explicitThink = directives.hasThinkDirective && directives.thinkLevel !== undefined; if (explicitThink) { typing.cleanup(); return { @@ -295,12 +271,7 @@ export async function runPreparedReply( }; } resolvedThinkLevel = "high"; - if ( - sessionEntry && - sessionStore && - sessionKey && - sessionEntry.thinkingLevel === "xhigh" - ) { + if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "xhigh") { sessionEntry.thinkingLevel = "high"; sessionEntry.updatedAt = Date.now(); sessionStore[sessionKey] = sessionEntry; @@ -317,10 +288,7 @@ export async function runPreparedReply( .join("\n\n") : [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n"); const queuedBody = mediaNote - ? [mediaNote, mediaReplyHint, queueBodyBase] - .filter(Boolean) - .join("\n") - .trim() + ? [mediaNote, mediaReplyHint, queueBodyBase].filter(Boolean).join("\n").trim() : queueBodyBase; const resolvedQueue = resolveQueueSettings({ cfg, @@ -329,22 +297,17 @@ export async function runPreparedReply( inlineMode: perMessageQueueMode, inlineOptions: perMessageQueueOptions, }); - const sessionLaneKey = resolveEmbeddedSessionLane( - sessionKey ?? sessionIdFinal, - ); + const sessionLaneKey = resolveEmbeddedSessionLane(sessionKey ?? sessionIdFinal); const laneSize = getQueueSize(sessionLaneKey); if (resolvedQueue.mode === "interrupt" && laneSize > 0) { const cleared = clearCommandLane(sessionLaneKey); const aborted = abortEmbeddedPiRun(sessionIdFinal); - logVerbose( - `Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`, - ); + logVerbose(`Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`); } const queueKey = sessionKey ?? sessionIdFinal; const isActive = isEmbeddedPiRunActive(sessionIdFinal); const isStreaming = isEmbeddedPiRunStreaming(sessionIdFinal); - const shouldSteer = - resolvedQueue.mode === "steer" || resolvedQueue.mode === "steer-backlog"; + const shouldSteer = resolvedQueue.mode === "steer" || resolvedQueue.mode === "steer-backlog"; const shouldFollowup = resolvedQueue.mode === "followup" || resolvedQueue.mode === "collect" || @@ -385,8 +348,7 @@ export async function runPreparedReply( }, timeoutMs, blockReplyBreak: resolvedBlockStreamingBreak, - ownerNumbers: - command.ownerList.length > 0 ? command.ownerList : undefined, + ownerNumbers: command.ownerList.length > 0 ? command.ownerList : undefined, extraSystemPrompt: extraSystemPrompt || undefined, ...(isReasoningTagProvider(provider) ? { enforceFinalTag: true } : {}), }, diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index 2f398c23961..292d4a628f1 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -5,21 +5,14 @@ import { } from "../../agents/agent-scope.js"; import { resolveModelRefFromString } from "../../agents/model-selection.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; -import { - DEFAULT_AGENT_WORKSPACE_DIR, - ensureAgentWorkspace, -} from "../../agents/workspace.js"; +import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js"; import { type ClawdbotConfig, loadConfig } from "../../config/config.js"; import { logVerbose } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveCommandAuthorization } from "../command-auth.js"; import type { MsgContext } from "../templating.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js"; -import { - hasAudioTranscriptionConfig, - isAudio, - transcribeInboundAudio, -} from "../transcription.js"; +import { hasAudioTranscriptionConfig, isAudio, transcribeInboundAudio } from "../transcription.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { resolveDefaultModel } from "./directive-handling.js"; import { resolveReplyDirectives } from "./get-reply-directives.js"; @@ -62,8 +55,7 @@ export async function getReplyFromConfig( } } - const workspaceDirRaw = - resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR; + const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR; const workspace = await ensureAgentWorkspace({ dir: workspaceDirRaw, ensureBootstrapFiles: !agentCfg?.skipBootstrap, diff --git a/src/auto-reply/reply/groups.test.ts b/src/auto-reply/reply/groups.test.ts index 7c700faf4a5..831a4ac4ef6 100644 --- a/src/auto-reply/reply/groups.test.ts +++ b/src/auto-reply/reply/groups.test.ts @@ -32,9 +32,7 @@ describe("resolveGroupRequireMention", () => { chatType: "group", }; - expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe( - false, - ); + expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false); }); it("respects Slack channel requireMention settings", () => { @@ -58,8 +56,6 @@ describe("resolveGroupRequireMention", () => { chatType: "group", }; - expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe( - false, - ); + expect(resolveGroupRequireMention({ cfg, ctx, groupResolution })).toBe(false); }); }); diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index 746ac6f767d..be9c5edc829 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -1,13 +1,7 @@ import { getChannelDock } from "../../channels/dock.js"; -import { - getChatChannelMeta, - normalizeChannelId, -} from "../../channels/registry.js"; +import { getChatChannelMeta, normalizeChannelId } from "../../channels/registry.js"; import type { ClawdbotConfig } from "../../config/config.js"; -import type { - GroupKeyResolution, - SessionEntry, -} from "../../config/sessions.js"; +import type { GroupKeyResolution, SessionEntry } from "../../config/sessions.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; import { normalizeGroupActivation } from "../group-activation.js"; import type { TemplateContext } from "../templating.js"; @@ -24,9 +18,7 @@ export function resolveGroupRequireMention(params: { const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, ""); const groupRoom = ctx.GroupRoom?.trim() ?? ctx.GroupSubject?.trim(); const groupSpace = ctx.GroupSpace?.trim(); - const requireMention = getChannelDock( - channel, - )?.groups?.resolveRequireMention?.({ + const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({ cfg, groupId, groupRoom, @@ -37,9 +29,7 @@ export function resolveGroupRequireMention(params: { return true; } -export function defaultGroupActivation( - requireMention: boolean, -): "always" | "mention" { +export function defaultGroupActivation(requireMention: boolean): "always" | "mention" { return requireMention === false ? "always" : "mention"; } @@ -51,8 +41,7 @@ export function buildGroupIntro(params: { silentToken: string; }): string { const activation = - normalizeGroupActivation(params.sessionEntry?.groupActivation) ?? - params.defaultActivation; + normalizeGroupActivation(params.sessionEntry?.groupActivation) ?? params.defaultActivation; const subject = params.sessionCtx.GroupSubject?.trim(); const members = params.sessionCtx.GroupMembers?.trim(); const rawProvider = params.sessionCtx.Provider?.trim(); diff --git a/src/auto-reply/reply/history.test.ts b/src/auto-reply/reply/history.test.ts index c249d6b29af..027320acb2d 100644 --- a/src/auto-reply/reply/history.test.ts +++ b/src/auto-reply/reply/history.test.ts @@ -56,10 +56,7 @@ describe("history helpers", () => { entry: { sender: "C", body: "three" }, }); - expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual([ - "two", - "three", - ]); + expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["two", "three"]); }); it("builds context from map and appends entry", () => { @@ -78,11 +75,7 @@ describe("history helpers", () => { formatEntry: (entry) => `${entry.sender}: ${entry.body}`, }); - expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual([ - "one", - "two", - "three", - ]); + expect(historyMap.get("room")?.map((entry) => entry.body)).toEqual(["one", "two", "three"]); expect(result).toContain(HISTORY_CONTEXT_MARKER); expect(result).toContain("A: one"); expect(result).toContain("B: two"); diff --git a/src/auto-reply/reply/history.ts b/src/auto-reply/reply/history.ts index 2e30c1d470a..632d71f1f73 100644 --- a/src/auto-reply/reply/history.ts +++ b/src/auto-reply/reply/history.ts @@ -1,7 +1,6 @@ import { CURRENT_MESSAGE_MARKER } from "./mentions.js"; -export const HISTORY_CONTEXT_MARKER = - "[Chat messages since your last reply - for context]"; +export const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]"; export const DEFAULT_GROUP_HISTORY_LIMIT = 50; export type HistoryEntry = { @@ -19,13 +18,9 @@ export function buildHistoryContext(params: { const { historyText, currentMessage } = params; const lineBreak = params.lineBreak ?? "\n"; if (!historyText.trim()) return currentMessage; - return [ - HISTORY_CONTEXT_MARKER, - historyText, - "", - CURRENT_MESSAGE_MARKER, - currentMessage, - ].join(lineBreak); + return [HISTORY_CONTEXT_MARKER, historyText, "", CURRENT_MESSAGE_MARKER, currentMessage].join( + lineBreak, + ); } export function appendHistoryEntry(params: { @@ -86,8 +81,7 @@ export function buildHistoryContextFromEntries(params: { excludeLast?: boolean; }): string { const lineBreak = params.lineBreak ?? "\n"; - const entries = - params.excludeLast === false ? params.entries : params.entries.slice(0, -1); + const entries = params.excludeLast === false ? params.entries : params.entries.slice(0, -1); if (entries.length === 0) return params.currentMessage; const historyText = entries.map(params.formatEntry).join(lineBreak); return buildHistoryContext({ diff --git a/src/auto-reply/reply/inbound-dedupe.test.ts b/src/auto-reply/reply/inbound-dedupe.test.ts index 9a5588ce31f..d9dbd148a8a 100644 --- a/src/auto-reply/reply/inbound-dedupe.test.ts +++ b/src/auto-reply/reply/inbound-dedupe.test.ts @@ -38,16 +38,10 @@ describe("inbound dedupe", () => { MessageSid: "msg-1", }; expect( - shouldSkipDuplicateInbound( - { ...base, OriginatingTo: "whatsapp:+1000" }, - { now: 100 }, - ), + shouldSkipDuplicateInbound({ ...base, OriginatingTo: "whatsapp:+1000" }, { now: 100 }), ).toBe(false); expect( - shouldSkipDuplicateInbound( - { ...base, OriginatingTo: "whatsapp:+2000" }, - { now: 200 }, - ), + shouldSkipDuplicateInbound({ ...base, OriginatingTo: "whatsapp:+2000" }, { now: 200 }), ).toBe(false); }); @@ -60,22 +54,13 @@ describe("inbound dedupe", () => { MessageSid: "msg-1", }; expect( - shouldSkipDuplicateInbound( - { ...base, SessionKey: "agent:alpha:main" }, - { now: 100 }, - ), + shouldSkipDuplicateInbound({ ...base, SessionKey: "agent:alpha:main" }, { now: 100 }), ).toBe(false); expect( - shouldSkipDuplicateInbound( - { ...base, SessionKey: "agent:bravo:main" }, - { now: 200 }, - ), + shouldSkipDuplicateInbound({ ...base, SessionKey: "agent:bravo:main" }, { now: 200 }), ).toBe(false); expect( - shouldSkipDuplicateInbound( - { ...base, SessionKey: "agent:alpha:main" }, - { now: 300 }, - ), + shouldSkipDuplicateInbound({ ...base, SessionKey: "agent:alpha:main" }, { now: 300 }), ).toBe(true); }); }); diff --git a/src/auto-reply/reply/inbound-dedupe.ts b/src/auto-reply/reply/inbound-dedupe.ts index 2246b6b799f..00f10500146 100644 --- a/src/auto-reply/reply/inbound-dedupe.ts +++ b/src/auto-reply/reply/inbound-dedupe.ts @@ -10,27 +10,21 @@ const inboundDedupeCache = createDedupeCache({ maxSize: DEFAULT_INBOUND_DEDUPE_MAX, }); -const normalizeProvider = (value?: string | null) => - value?.trim().toLowerCase() || ""; +const normalizeProvider = (value?: string | null) => value?.trim().toLowerCase() || ""; const resolveInboundPeerId = (ctx: MsgContext) => ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? ctx.SessionKey; export function buildInboundDedupeKey(ctx: MsgContext): string | null { - const provider = normalizeProvider( - ctx.OriginatingChannel ?? ctx.Provider ?? ctx.Surface, - ); + const provider = normalizeProvider(ctx.OriginatingChannel ?? ctx.Provider ?? ctx.Surface); const messageId = ctx.MessageSid?.trim(); if (!provider || !messageId) return null; const peerId = resolveInboundPeerId(ctx); if (!peerId) return null; const sessionKey = ctx.SessionKey?.trim() ?? ""; const accountId = ctx.AccountId?.trim() ?? ""; - const threadId = - typeof ctx.MessageThreadId === "number" ? String(ctx.MessageThreadId) : ""; - return [provider, accountId, sessionKey, peerId, threadId, messageId] - .filter(Boolean) - .join("|"); + const threadId = typeof ctx.MessageThreadId === "number" ? String(ctx.MessageThreadId) : ""; + return [provider, accountId, sessionKey, peerId, threadId, messageId].filter(Boolean).join("|"); } export function shouldSkipDuplicateInbound( diff --git a/src/auto-reply/reply/memory-flush.test.ts b/src/auto-reply/reply/memory-flush.test.ts index a9d056cb326..6d04b21e310 100644 --- a/src/auto-reply/reply/memory-flush.test.ts +++ b/src/auto-reply/reply/memory-flush.test.ts @@ -118,8 +118,6 @@ describe("shouldRunMemoryFlush", () => { describe("resolveMemoryFlushContextWindowTokens", () => { it("falls back to agent config or default tokens", () => { - expect( - resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 }), - ).toBe(42_000); + expect(resolveMemoryFlushContextWindowTokens({ agentCfgContextTokens: 42_000 })).toBe(42_000); }); }); diff --git a/src/auto-reply/reply/memory-flush.ts b/src/auto-reply/reply/memory-flush.ts index d1b0a5d325c..867f7da8898 100644 --- a/src/auto-reply/reply/memory-flush.ts +++ b/src/auto-reply/reply/memory-flush.ts @@ -33,22 +33,17 @@ const normalizeNonNegativeInt = (value: unknown): number | null => { return int >= 0 ? int : null; }; -export function resolveMemoryFlushSettings( - cfg?: ClawdbotConfig, -): MemoryFlushSettings | null { +export function resolveMemoryFlushSettings(cfg?: ClawdbotConfig): MemoryFlushSettings | null { const defaults = cfg?.agents?.defaults?.compaction?.memoryFlush; const enabled = defaults?.enabled ?? true; if (!enabled) return null; const softThresholdTokens = - normalizeNonNegativeInt(defaults?.softThresholdTokens) ?? - DEFAULT_MEMORY_FLUSH_SOFT_TOKENS; + normalizeNonNegativeInt(defaults?.softThresholdTokens) ?? DEFAULT_MEMORY_FLUSH_SOFT_TOKENS; const prompt = defaults?.prompt?.trim() || DEFAULT_MEMORY_FLUSH_PROMPT; - const systemPrompt = - defaults?.systemPrompt?.trim() || DEFAULT_MEMORY_FLUSH_SYSTEM_PROMPT; + const systemPrompt = defaults?.systemPrompt?.trim() || DEFAULT_MEMORY_FLUSH_SYSTEM_PROMPT; const reserveTokensFloor = - normalizeNonNegativeInt( - cfg?.agents?.defaults?.compaction?.reserveTokensFloor, - ) ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; + normalizeNonNegativeInt(cfg?.agents?.defaults?.compaction?.reserveTokensFloor) ?? + DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; return { enabled, @@ -69,17 +64,12 @@ export function resolveMemoryFlushContextWindowTokens(params: { agentCfgContextTokens?: number; }): number { return ( - lookupContextTokens(params.modelId) ?? - params.agentCfgContextTokens ?? - DEFAULT_CONTEXT_TOKENS + lookupContextTokens(params.modelId) ?? params.agentCfgContextTokens ?? DEFAULT_CONTEXT_TOKENS ); } export function shouldRunMemoryFlush(params: { - entry?: Pick< - SessionEntry, - "totalTokens" | "compactionCount" | "memoryFlushCompactionCount" - >; + entry?: Pick; contextWindowTokens: number; reserveTokensFloor: number; softThresholdTokens: number; diff --git a/src/auto-reply/reply/mentions.test.ts b/src/auto-reply/reply/mentions.test.ts index 88f3172c41c..d0c16977ad7 100644 --- a/src/auto-reply/reply/mentions.test.ts +++ b/src/auto-reply/reply/mentions.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - buildMentionRegexes, - matchesMentionPatterns, - normalizeMentionText, -} from "./mentions.js"; +import { buildMentionRegexes, matchesMentionPatterns, normalizeMentionText } from "./mentions.js"; describe("mention helpers", () => { it("builds regexes and skips invalid patterns", () => { diff --git a/src/auto-reply/reply/mentions.ts b/src/auto-reply/reply/mentions.ts index 61f8513124f..de637156ed9 100644 --- a/src/auto-reply/reply/mentions.ts +++ b/src/auto-reply/reply/mentions.ts @@ -36,10 +36,7 @@ function normalizeMentionPatterns(patterns: string[]): string[] { return patterns.map(normalizeMentionPattern); } -function resolveMentionPatterns( - cfg: ClawdbotConfig | undefined, - agentId?: string, -): string[] { +function resolveMentionPatterns(cfg: ClawdbotConfig | undefined, agentId?: string): string[] { if (!cfg) return []; const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined; const agentGroupChat = agentConfig?.groupChat; @@ -54,13 +51,8 @@ function resolveMentionPatterns( return derived.length > 0 ? derived : []; } -export function buildMentionRegexes( - cfg: ClawdbotConfig | undefined, - agentId?: string, -): RegExp[] { - const patterns = normalizeMentionPatterns( - resolveMentionPatterns(cfg, agentId), - ); +export function buildMentionRegexes(cfg: ClawdbotConfig | undefined, agentId?: string): RegExp[] { + const patterns = normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId)); return patterns .map((pattern) => { try { @@ -73,15 +65,10 @@ export function buildMentionRegexes( } export function normalizeMentionText(text: string): string { - return (text ?? "") - .replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "") - .toLowerCase(); + return (text ?? "").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "").toLowerCase(); } -export function matchesMentionPatterns( - text: string, - mentionRegexes: RegExp[], -): boolean { +export function matchesMentionPatterns(text: string, mentionRegexes: RegExp[]): boolean { if (mentionRegexes.length === 0) return false; const cleaned = normalizeMentionText(text ?? ""); if (!cleaned) return false; @@ -92,11 +79,7 @@ export function stripStructuralPrefixes(text: string): string { // Ignore wrapper labels, timestamps, and sender prefixes so directive-only // detection still works in group batches that include history/context. const afterMarker = text.includes(CURRENT_MESSAGE_MARKER) - ? text - .slice( - text.indexOf(CURRENT_MESSAGE_MARKER) + CURRENT_MESSAGE_MARKER.length, - ) - .trimStart() + ? text.slice(text.indexOf(CURRENT_MESSAGE_MARKER) + CURRENT_MESSAGE_MARKER.length).trimStart() : text; return afterMarker @@ -115,9 +98,7 @@ export function stripMentions( ): string { let result = text; const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null; - const providerMentions = providerId - ? getChannelDock(providerId)?.mentions - : undefined; + const providerMentions = providerId ? getChannelDock(providerId)?.mentions : undefined; const patterns = normalizeMentionPatterns([ ...resolveMentionPatterns(cfg, agentId), ...(providerMentions?.stripPatterns?.({ ctx, cfg, agentId }) ?? []), diff --git a/src/auto-reply/reply/model-selection.ts b/src/auto-reply/reply/model-selection.ts index 5d90ad9b111..96929ee62f2 100644 --- a/src/auto-reply/reply/model-selection.ts +++ b/src/auto-reply/reply/model-selection.ts @@ -105,15 +105,9 @@ function scoreFuzzyMatch(params: { score += 30; } - const fragmentVariants = FUZZY_VARIANT_TOKENS.filter((token) => - fragment.includes(token), - ); - const modelVariants = FUZZY_VARIANT_TOKENS.filter((token) => - modelLower.includes(token), - ); - const variantMatchCount = fragmentVariants.filter((token) => - modelLower.includes(token), - ).length; + const fragmentVariants = FUZZY_VARIANT_TOKENS.filter((token) => fragment.includes(token)); + const modelVariants = FUZZY_VARIANT_TOKENS.filter((token) => modelLower.includes(token)); + const variantMatchCount = fragmentVariants.filter((token) => modelLower.includes(token)).length; const variantCount = modelVariants.length; if (fragmentVariants.length === 0 && variantCount > 0) { score -= variantCount * 30; @@ -123,8 +117,7 @@ function scoreFuzzyMatch(params: { } const defaultProvider = normalizeProviderId(params.defaultProvider); - const isDefault = - provider === defaultProvider && model === params.defaultModel; + const isDefault = provider === defaultProvider && model === params.defaultModel; if (isDefault) score += 20; return { @@ -139,9 +132,7 @@ function scoreFuzzyMatch(params: { export async function createModelSelectionState(params: { cfg: ClawdbotConfig; - agentCfg: - | NonNullable["defaults"]> - | undefined; + agentCfg: NonNullable["defaults"]> | undefined; sessionEntry?: SessionEntry; sessionStore?: Record; sessionKey?: string; @@ -166,13 +157,9 @@ export async function createModelSelectionState(params: { let provider = params.provider; let model = params.model; - const hasAllowlist = - agentCfg?.models && Object.keys(agentCfg.models).length > 0; - const hasStoredOverride = Boolean( - sessionEntry?.modelOverride || sessionEntry?.providerOverride, - ); - const needsModelCatalog = - params.hasModelDirective || hasAllowlist || hasStoredOverride; + const hasAllowlist = agentCfg?.models && Object.keys(agentCfg.models).length > 0; + const hasStoredOverride = Boolean(sessionEntry?.modelOverride || sessionEntry?.providerOverride); + const needsModelCatalog = params.hasModelDirective || hasAllowlist || hasStoredOverride; let allowedModelKeys = new Set(); let allowedModelCatalog: ModelCatalog = []; @@ -192,8 +179,7 @@ export async function createModelSelectionState(params: { } if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) { - const overrideProvider = - sessionEntry.providerOverride?.trim() || defaultProvider; + const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider; const overrideModel = sessionEntry.modelOverride?.trim(); if (overrideModel) { const key = modelKey(overrideProvider, overrideModel); @@ -221,15 +207,8 @@ export async function createModelSelectionState(params: { } } - if ( - sessionEntry && - sessionStore && - sessionKey && - sessionEntry.authProfileOverride - ) { - const { ensureAuthProfileStore } = await import( - "../../agents/auth-profiles.js" - ); + if (sessionEntry && sessionStore && sessionKey && sessionEntry.authProfileOverride) { + const { ensureAuthProfileStore } = await import("../../agents/auth-profiles.js"); const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false, }); @@ -259,9 +238,7 @@ export async function createModelSelectionState(params: { catalog: catalogForThinking, }); defaultThinkingLevel = - resolved ?? - (agentCfg?.thinkingDefault as ThinkLevel | undefined) ?? - "off"; + resolved ?? (agentCfg?.thinkingDefault as ThinkLevel | undefined) ?? "off"; return defaultThinkingLevel; }; @@ -283,21 +260,15 @@ export function resolveModelDirectiveSelection(params: { aliasIndex: ModelAliasIndex; allowedModelKeys: Set; }): { selection?: ModelDirectiveSelection; error?: string } { - const { raw, defaultProvider, defaultModel, aliasIndex, allowedModelKeys } = - params; + const { raw, defaultProvider, defaultModel, aliasIndex, allowedModelKeys } = params; const rawTrimmed = raw.trim(); const rawLower = rawTrimmed.toLowerCase(); - const pickAliasForKey = ( - provider: string, - model: string, - ): string | undefined => aliasIndex.byKey.get(modelKey(provider, model))?.[0]; + const pickAliasForKey = (provider: string, model: string): string | undefined => + aliasIndex.byKey.get(modelKey(provider, model))?.[0]; - const buildSelection = ( - provider: string, - model: string, - ): ModelDirectiveSelection => { + const buildSelection = (provider: string, model: string): ModelDirectiveSelection => { const alias = pickAliasForKey(provider, model); return { provider, @@ -320,13 +291,9 @@ export function resolveModelDirectiveSelection(params: { if (slash <= 0) continue; const provider = normalizeProviderId(key.slice(0, slash)); const model = key.slice(slash + 1); - if (params.provider && provider !== normalizeProviderId(params.provider)) - continue; + if (params.provider && provider !== normalizeProviderId(params.provider)) continue; const haystack = `${provider}/${model}`.toLowerCase(); - if ( - haystack.includes(fragment) || - model.toLowerCase().includes(fragment) - ) { + if (haystack.includes(fragment) || model.toLowerCase().includes(fragment)) { candidates.push({ provider, model }); } } @@ -344,11 +311,7 @@ export function resolveModelDirectiveSelection(params: { for (const match of aliasMatches) { const key = modelKey(match.provider, match.model); if (!allowedModelKeys.has(key)) continue; - if ( - !candidates.some( - (c) => c.provider === match.provider && c.model === match.model, - ) - ) { + if (!candidates.some((c) => c.provider === match.provider && c.model === match.model)) { candidates.push(match); } } @@ -378,10 +341,8 @@ export function resolveModelDirectiveSelection(params: { if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; if (a.variantMatchCount !== b.variantMatchCount) return b.variantMatchCount - a.variantMatchCount; - if (a.variantCount !== b.variantCount) - return a.variantCount - b.variantCount; - if (a.modelLength !== b.modelLength) - return a.modelLength - b.modelLength; + if (a.variantCount !== b.variantCount) return a.variantCount - b.variantCount; + if (a.modelLength !== b.modelLength) return a.modelLength - b.modelLength; return a.key.localeCompare(b.key); }); @@ -410,9 +371,7 @@ export function resolveModelDirectiveSelection(params: { selection: { provider: resolved.ref.provider, model: resolved.ref.model, - isDefault: - resolved.ref.provider === defaultProvider && - resolved.ref.model === defaultModel, + isDefault: resolved.ref.provider === defaultProvider && resolved.ref.model === defaultModel, alias: resolved.alias, }, }; @@ -438,14 +397,10 @@ export function resolveModelDirectiveSelection(params: { } export function resolveContextTokens(params: { - agentCfg: - | NonNullable["defaults"]> - | undefined; + agentCfg: NonNullable["defaults"]> | undefined; model: string; }): number { return ( - params.agentCfg?.contextTokens ?? - lookupContextTokens(params.model) ?? - DEFAULT_CONTEXT_TOKENS + params.agentCfg?.contextTokens ?? lookupContextTokens(params.model) ?? DEFAULT_CONTEXT_TOKENS ); } diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index 7c451bbb38d..f46a49b9ff9 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -1,9 +1,5 @@ import { stripHeartbeatToken } from "../heartbeat.js"; -import { - HEARTBEAT_TOKEN, - isSilentReplyText, - SILENT_REPLY_TOKEN, -} from "../tokens.js"; +import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js"; import type { ReplyPayload } from "../types.js"; export type NormalizeReplyOptions = { @@ -17,9 +13,7 @@ export function normalizeReplyPayload( payload: ReplyPayload, opts: NormalizeReplyOptions = {}, ): ReplyPayload | null { - const hasMedia = Boolean( - payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0, - ); + const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0); const trimmed = payload.text?.trim() ?? ""; if (!trimmed && !hasMedia) return null; diff --git a/src/auto-reply/reply/provider-dispatcher.ts b/src/auto-reply/reply/provider-dispatcher.ts index 5c9da13fb68..2da188722ec 100644 --- a/src/auto-reply/reply/provider-dispatcher.ts +++ b/src/auto-reply/reply/provider-dispatcher.ts @@ -17,8 +17,9 @@ export async function dispatchReplyWithBufferedBlockDispatcher(params: { replyOptions?: Omit; replyResolver?: typeof import("../reply.js").getReplyFromConfig; }): Promise { - const { dispatcher, replyOptions, markDispatchIdle } = - createReplyDispatcherWithTyping(params.dispatcherOptions); + const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping( + params.dispatcherOptions, + ); const result = await dispatchReplyFromConfig({ ctx: params.ctx, diff --git a/src/auto-reply/reply/queue.collect-routing.test.ts b/src/auto-reply/reply/queue.collect-routing.test.ts index 58a957c6240..5abfa83b3ca 100644 --- a/src/auto-reply/reply/queue.collect-routing.test.ts +++ b/src/auto-reply/reply/queue.collect-routing.test.ts @@ -91,9 +91,7 @@ describe("followup queue deduplication", () => { scheduleFollowupDrain(key, runFollowup); await expect.poll(() => calls.length).toBe(1); // Should collect both unique messages - expect(calls[0]?.prompt).toContain( - "[Queued messages while agent was busy]", - ); + expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); }); it("deduplicates exact prompt when routing matches and no message id", async () => { @@ -282,9 +280,7 @@ describe("followup queue collect routing", () => { scheduleFollowupDrain(key, runFollowup); await expect.poll(() => calls.length).toBe(1); - expect(calls[0]?.prompt).toContain( - "[Queued messages while agent was busy]", - ); + expect(calls[0]?.prompt).toContain("[Queued messages while agent was busy]"); expect(calls[0]?.originatingChannel).toBe("slack"); expect(calls[0]?.originatingTo).toBe("channel:A"); }); diff --git a/src/auto-reply/reply/queue/drain.ts b/src/auto-reply/reply/queue/drain.ts index 1c4303bc520..e76f1b71d4d 100644 --- a/src/auto-reply/reply/queue/drain.ts +++ b/src/auto-reply/reply/queue/drain.ts @@ -3,10 +3,7 @@ import { isRoutableChannel } from "../route-reply.js"; import { FOLLOWUP_QUEUES } from "./state.js"; import type { FollowupRun } from "./types.js"; -async function waitForQueueDebounce(queue: { - debounceMs: number; - lastEnqueuedAt: number; -}) { +async function waitForQueueDebounce(queue: { debounceMs: number; lastEnqueuedAt: number }) { const debounceMs = Math.max(0, queue.debounceMs); if (debounceMs <= 0) return; while (true) { @@ -71,12 +68,9 @@ function hasCrossChannelItems(items: FollowupRun[]): boolean { return true; } keys.add( - [ - channel, - to, - accountId || "", - typeof threadId === "number" ? String(threadId) : "", - ].join("|"), + [channel, to, accountId || "", typeof threadId === "number" ? String(threadId) : ""].join( + "|", + ), ); } @@ -127,12 +121,8 @@ export function scheduleFollowupDrain( if (!run) break; // Preserve originating channel from items when collecting same-channel. - const originatingChannel = items.find( - (i) => i.originatingChannel, - )?.originatingChannel; - const originatingTo = items.find( - (i) => i.originatingTo, - )?.originatingTo; + const originatingChannel = items.find((i) => i.originatingChannel)?.originatingChannel; + const originatingTo = items.find((i) => i.originatingTo)?.originatingTo; const originatingAccountId = items.find( (i) => i.originatingAccountId, )?.originatingAccountId; @@ -170,9 +160,7 @@ export function scheduleFollowupDrain( await runFollowup(next); } } catch (err) { - defaultRuntime.error?.( - `followup queue drain failed for ${key}: ${String(err)}`, - ); + defaultRuntime.error?.(`followup queue drain failed for ${key}: ${String(err)}`); } finally { queue.draining = false; if (queue.items.length === 0 && queue.droppedCount === 0) { diff --git a/src/auto-reply/reply/queue/enqueue.ts b/src/auto-reply/reply/queue/enqueue.ts index 619298e1110..4c830d3a3db 100644 --- a/src/auto-reply/reply/queue/enqueue.ts +++ b/src/auto-reply/reply/queue/enqueue.ts @@ -25,14 +25,10 @@ function isRunAlreadyQueued( const messageId = run.messageId?.trim(); if (messageId) { - return items.some( - (item) => item.messageId?.trim() === messageId && hasSameRouting(item), - ); + return items.some((item) => item.messageId?.trim() === messageId && hasSameRouting(item)); } if (!allowPromptFallback) return false; - return items.some( - (item) => item.prompt === run.prompt && hasSameRouting(item), - ); + return items.some((item) => item.prompt === run.prompt && hasSameRouting(item)); } export function enqueueFollowupRun( diff --git a/src/auto-reply/reply/queue/normalize.ts b/src/auto-reply/reply/queue/normalize.ts index a6c148b3d83..eac9b3bd1e6 100644 --- a/src/auto-reply/reply/queue/normalize.ts +++ b/src/auto-reply/reply/queue/normalize.ts @@ -4,32 +4,18 @@ export function normalizeQueueMode(raw?: string): QueueMode | undefined { if (!raw) return undefined; const cleaned = raw.trim().toLowerCase(); if (cleaned === "queue" || cleaned === "queued") return "steer"; - if ( - cleaned === "interrupt" || - cleaned === "interrupts" || - cleaned === "abort" - ) + if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort") return "interrupt"; if (cleaned === "steer" || cleaned === "steering") return "steer"; - if ( - cleaned === "followup" || - cleaned === "follow-ups" || - cleaned === "followups" - ) + if (cleaned === "followup" || cleaned === "follow-ups" || cleaned === "followups") return "followup"; if (cleaned === "collect" || cleaned === "coalesce") return "collect"; - if ( - cleaned === "steer+backlog" || - cleaned === "steer-backlog" || - cleaned === "steer_backlog" - ) + if (cleaned === "steer+backlog" || cleaned === "steer-backlog" || cleaned === "steer_backlog") return "steer-backlog"; return undefined; } -export function normalizeQueueDropPolicy( - raw?: string, -): QueueDropPolicy | undefined { +export function normalizeQueueDropPolicy(raw?: string): QueueDropPolicy | undefined { if (!raw) return undefined; const cleaned = raw.trim().toLowerCase(); if (cleaned === "old" || cleaned === "oldest") return "old"; diff --git a/src/auto-reply/reply/queue/settings.ts b/src/auto-reply/reply/queue/settings.ts index f451c980919..5fd9797dc52 100644 --- a/src/auto-reply/reply/queue/settings.ts +++ b/src/auto-reply/reply/queue/settings.ts @@ -1,22 +1,12 @@ import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js"; -import { - DEFAULT_QUEUE_CAP, - DEFAULT_QUEUE_DEBOUNCE_MS, - DEFAULT_QUEUE_DROP, -} from "./state.js"; -import type { - QueueMode, - QueueSettings, - ResolveQueueSettingsParams, -} from "./types.js"; +import { DEFAULT_QUEUE_CAP, DEFAULT_QUEUE_DEBOUNCE_MS, DEFAULT_QUEUE_DROP } from "./state.js"; +import type { QueueMode, QueueSettings, ResolveQueueSettingsParams } from "./types.js"; function defaultQueueModeForChannel(_channel?: string): QueueMode { return "collect"; } -export function resolveQueueSettings( - params: ResolveQueueSettingsParams, -): QueueSettings { +export function resolveQueueSettings(params: ResolveQueueSettingsParams): QueueSettings { const channelKey = params.channel?.trim().toLowerCase(); const queueCfg = params.cfg.messages?.queue; const providerModeRaw = @@ -46,10 +36,8 @@ export function resolveQueueSettings( DEFAULT_QUEUE_DROP; return { mode: resolvedMode, - debounceMs: - typeof debounceRaw === "number" ? Math.max(0, debounceRaw) : undefined, - cap: - typeof capRaw === "number" ? Math.max(1, Math.floor(capRaw)) : undefined, + debounceMs: typeof debounceRaw === "number" ? Math.max(0, debounceRaw) : undefined, + cap: typeof capRaw === "number" ? Math.max(1, Math.floor(capRaw)) : undefined, dropPolicy: dropRaw, }; } diff --git a/src/auto-reply/reply/queue/state.ts b/src/auto-reply/reply/queue/state.ts index e939ce2d5f9..d7977ae57aa 100644 --- a/src/auto-reply/reply/queue/state.ts +++ b/src/auto-reply/reply/queue/state.ts @@ -1,9 +1,4 @@ -import type { - FollowupRun, - QueueDropPolicy, - QueueMode, - QueueSettings, -} from "./types.js"; +import type { FollowupRun, QueueDropPolicy, QueueMode, QueueSettings } from "./types.js"; export type FollowupQueueState = { items: FollowupRun[]; @@ -24,10 +19,7 @@ export const DEFAULT_QUEUE_DROP: QueueDropPolicy = "summarize"; export const FOLLOWUP_QUEUES = new Map(); -export function getFollowupQueue( - key: string, - settings: QueueSettings, -): FollowupQueueState { +export function getFollowupQueue(key: string, settings: QueueSettings): FollowupQueueState { const existing = FOLLOWUP_QUEUES.get(key); if (existing) { existing.mode = settings.mode; diff --git a/src/auto-reply/reply/queue/types.ts b/src/auto-reply/reply/queue/types.ts index fe91d214e34..f04828a1c19 100644 --- a/src/auto-reply/reply/queue/types.ts +++ b/src/auto-reply/reply/queue/types.ts @@ -2,20 +2,9 @@ import type { SkillSnapshot } from "../../../agents/skills.js"; import type { ClawdbotConfig } from "../../../config/config.js"; import type { SessionEntry } from "../../../config/sessions.js"; import type { OriginatingChannelType } from "../../templating.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "../directives.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../directives.js"; -export type QueueMode = - | "steer" - | "followup" - | "collect" - | "steer-backlog" - | "interrupt" - | "queue"; +export type QueueMode = "steer" | "followup" | "collect" | "steer-backlog" | "interrupt" | "queue"; export type QueueDropPolicy = "old" | "new" | "summarize"; diff --git a/src/auto-reply/reply/reply-dispatcher.test.ts b/src/auto-reply/reply/reply-dispatcher.test.ts index 37456c6b0e8..cc99130cf2d 100644 --- a/src/auto-reply/reply/reply-dispatcher.test.ts +++ b/src/auto-reply/reply/reply-dispatcher.test.ts @@ -10,9 +10,7 @@ describe("createReplyDispatcher", () => { expect(dispatcher.sendFinalReply({})).toBe(false); expect(dispatcher.sendFinalReply({ text: " " })).toBe(false); expect(dispatcher.sendFinalReply({ text: SILENT_REPLY_TOKEN })).toBe(false); - expect( - dispatcher.sendFinalReply({ text: `${SILENT_REPLY_TOKEN} -- nope` }), - ).toBe(false); + expect(dispatcher.sendFinalReply({ text: `${SILENT_REPLY_TOKEN} -- nope` })).toBe(false); await dispatcher.waitForIdle(); expect(deliver).not.toHaveBeenCalled(); @@ -28,9 +26,7 @@ describe("createReplyDispatcher", () => { }); expect(dispatcher.sendFinalReply({ text: HEARTBEAT_TOKEN })).toBe(false); - expect( - dispatcher.sendToolResult({ text: `${HEARTBEAT_TOKEN} hello` }), - ).toBe(true); + expect(dispatcher.sendToolResult({ text: `${HEARTBEAT_TOKEN} hello` })).toBe(true); await dispatcher.waitForIdle(); expect(deliver).toHaveBeenCalledTimes(1); @@ -91,9 +87,7 @@ describe("createReplyDispatcher", () => { }); it("fires onIdle when the queue drains", async () => { - const deliver = vi.fn( - async () => await new Promise((resolve) => setTimeout(resolve, 5)), - ); + const deliver = vi.fn(async () => await new Promise((resolve) => setTimeout(resolve, 5))); const onIdle = vi.fn(); const dispatcher = createReplyDispatcher({ deliver, onIdle }); diff --git a/src/auto-reply/reply/reply-dispatcher.ts b/src/auto-reply/reply/reply-dispatcher.ts index 2bda83a9e8d..5cc3a97facc 100644 --- a/src/auto-reply/reply/reply-dispatcher.ts +++ b/src/auto-reply/reply/reply-dispatcher.ts @@ -5,10 +5,7 @@ import type { TypingController } from "./typing.js"; export type ReplyDispatchKind = "tool" | "block" | "final"; -type ReplyDispatchErrorHandler = ( - err: unknown, - info: { kind: ReplyDispatchKind }, -) => void; +type ReplyDispatchErrorHandler = (err: unknown, info: { kind: ReplyDispatchKind }) => void; type ReplyDispatchDeliverer = ( payload: ReplyPayload, @@ -23,13 +20,9 @@ function getHumanDelay(config: HumanDelayConfig | undefined): number { const mode = config?.mode ?? "off"; if (mode === "off") return 0; const min = - mode === "custom" - ? (config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS) - : DEFAULT_HUMAN_DELAY_MIN_MS; + mode === "custom" ? (config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS) : DEFAULT_HUMAN_DELAY_MIN_MS; const max = - mode === "custom" - ? (config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS) - : DEFAULT_HUMAN_DELAY_MAX_MS; + mode === "custom" ? (config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS) : DEFAULT_HUMAN_DELAY_MAX_MS; if (max <= min) return min; return Math.floor(Math.random() * (max - min + 1)) + min; } @@ -47,10 +40,7 @@ export type ReplyDispatcherOptions = { humanDelay?: HumanDelayConfig; }; -export type ReplyDispatcherWithTypingOptions = Omit< - ReplyDispatcherOptions, - "onIdle" -> & { +export type ReplyDispatcherWithTypingOptions = Omit & { onReplyStart?: () => Promise | void; onIdle?: () => void; }; @@ -79,9 +69,7 @@ function normalizeReplyPayloadInternal( }); } -export function createReplyDispatcher( - options: ReplyDispatcherOptions, -): ReplyDispatcher { +export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDispatcher { let sendChain: Promise = Promise.resolve(); // Track in-flight deliveries so we can emit a reliable "idle" signal. let pending = 0; diff --git a/src/auto-reply/reply/reply-elevated.ts b/src/auto-reply/reply/reply-elevated.ts index 0f28830f4be..fc7bd7c8698 100644 --- a/src/auto-reply/reply/reply-elevated.ts +++ b/src/auto-reply/reply/reply-elevated.ts @@ -1,13 +1,7 @@ import { resolveAgentConfig } from "../../agents/agent-scope.js"; import { getChannelDock } from "../../channels/dock.js"; -import { - CHAT_CHANNEL_ORDER, - normalizeChannelId, -} from "../../channels/registry.js"; -import type { - AgentElevatedAllowFromConfig, - ClawdbotConfig, -} from "../../config/config.js"; +import { CHAT_CHANNEL_ORDER, normalizeChannelId } from "../../channels/registry.js"; +import type { AgentElevatedAllowFromConfig, ClawdbotConfig } from "../../config/config.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; import type { MsgContext } from "../templating.js"; @@ -64,9 +58,7 @@ function isApprovedElevatedSender(params: { ); if (!rawAllow || rawAllow.length === 0) return false; - const allowTokens = rawAllow - .map((entry) => String(entry).trim()) - .filter(Boolean); + const allowTokens = rawAllow.map((entry) => String(entry).trim()).filter(Boolean); if (allowTokens.length === 0) return false; if (allowTokens.some((entry) => entry === "*")) return true; @@ -116,14 +108,12 @@ export function resolveElevatedPermissions(params: { failures: Array<{ gate: string; key: string }>; } { const globalConfig = params.cfg.tools?.elevated; - const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools - ?.elevated; + const agentConfig = resolveAgentConfig(params.cfg, params.agentId)?.tools?.elevated; const globalEnabled = globalConfig?.enabled !== false; const agentEnabled = agentConfig?.enabled !== false; const enabled = globalEnabled && agentEnabled; const failures: Array<{ gate: string; key: string }> = []; - if (!globalEnabled) - failures.push({ gate: "enabled", key: "tools.elevated.enabled" }); + if (!globalEnabled) failures.push({ gate: "enabled", key: "tools.elevated.enabled" }); if (!agentEnabled) failures.push({ gate: "enabled", @@ -184,11 +174,7 @@ export function formatElevatedUnavailableMessage(params: { `elevated is not available right now (runtime=${params.runtimeSandboxed ? "sandboxed" : "direct"}).`, ); if (params.failures.length > 0) { - lines.push( - `Failing gates: ${params.failures - .map((f) => `${f.gate} (${f.key})`) - .join(", ")}`, - ); + lines.push(`Failing gates: ${params.failures.map((f) => `${f.gate} (${f.key})`).join(", ")}`); } else { lines.push( "Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.).", diff --git a/src/auto-reply/reply/reply-inline.ts b/src/auto-reply/reply/reply-inline.ts index e35ef220221..811584fc490 100644 --- a/src/auto-reply/reply/reply-inline.ts +++ b/src/auto-reply/reply/reply-inline.ts @@ -4,8 +4,7 @@ const INLINE_SIMPLE_COMMAND_ALIASES = new Map([ ["/whoami", "/whoami"], ["/id", "/whoami"], ]); -const INLINE_SIMPLE_COMMAND_RE = - /(?:^|\s)\/(help|commands|whoami|id)(?=$|\s|:)/i; +const INLINE_SIMPLE_COMMAND_RE = /(?:^|\s)\/(help|commands|whoami|id)(?=$|\s|:)/i; const INLINE_STATUS_RE = /(?:^|\s)\/(?:status|usage)(?=$|\s|:)(?:\s*:\s*)?/gi; @@ -29,9 +28,6 @@ export function stripInlineStatus(body: string): { } { const trimmed = body.trim(); if (!trimmed) return { cleaned: "", didStrip: false }; - const cleaned = trimmed - .replace(INLINE_STATUS_RE, " ") - .replace(/\s+/g, " ") - .trim(); + const cleaned = trimmed.replace(INLINE_STATUS_RE, " ").replace(/\s+/g, " ").trim(); return { cleaned, didStrip: cleaned !== trimmed }; } diff --git a/src/auto-reply/reply/reply-payloads.ts b/src/auto-reply/reply/reply-payloads.ts index 54b3db58cb6..94551fed6f4 100644 --- a/src/auto-reply/reply/reply-payloads.ts +++ b/src/auto-reply/reply/reply-payloads.ts @@ -42,9 +42,7 @@ export function applyReplyTagsToPayload( export function isRenderablePayload(payload: ReplyPayload): boolean { return Boolean( - payload.text || - payload.mediaUrl || - (payload.mediaUrls && payload.mediaUrls.length > 0), + payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0), ); } @@ -55,10 +53,7 @@ export function applyReplyThreading(params: { currentMessageId?: string; }): ReplyPayload[] { const { payloads, replyToMode, replyToChannel, currentMessageId } = params; - const applyReplyToMode = createReplyToModeFilterForChannel( - replyToMode, - replyToChannel, - ); + const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel); return payloads .map((payload) => applyReplyTagsToPayload(payload, currentMessageId)) .filter(isRenderablePayload) @@ -71,9 +66,7 @@ export function filterMessagingToolDuplicates(params: { }): ReplyPayload[] { const { payloads, sentTexts } = params; if (sentTexts.length === 0) return payloads; - return payloads.filter( - (payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts), - ); + return payloads.filter((payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts)); } function normalizeAccountId(value?: string): string | undefined { @@ -89,10 +82,7 @@ export function shouldSuppressMessagingToolReplies(params: { }): boolean { const provider = params.messageProvider?.trim().toLowerCase(); if (!provider) return false; - const originTarget = normalizeTargetForProvider( - provider, - params.originatingTo, - ); + const originTarget = normalizeTargetForProvider(provider, params.originatingTo); if (!originTarget) return false; const originAccount = normalizeAccountId(params.accountId); const sentTargets = params.messagingToolSentTargets ?? []; diff --git a/src/auto-reply/reply/reply-threading.test.ts b/src/auto-reply/reply/reply-threading.test.ts index 69a5058b5c0..38f28b11e9b 100644 --- a/src/auto-reply/reply/reply-threading.test.ts +++ b/src/auto-reply/reply/reply-threading.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from "vitest"; import type { ClawdbotConfig } from "../../config/config.js"; -import { - createReplyToModeFilter, - resolveReplyToMode, -} from "./reply-threading.js"; +import { createReplyToModeFilter, resolveReplyToMode } from "./reply-threading.js"; const emptyCfg = {} as ClawdbotConfig; @@ -44,9 +41,7 @@ describe("createReplyToModeFilter", () => { it("keeps replyToId when mode is off and reply tags are allowed", () => { const filter = createReplyToModeFilter("off", { allowTagsWhenOff: true }); - expect( - filter({ text: "hi", replyToId: "1", replyToTag: true }).replyToId, - ).toBe("1"); + expect(filter({ text: "hi", replyToId: "1", replyToTag: true }).replyToId).toBe("1"); }); it("keeps replyToId when mode is all", () => { diff --git a/src/auto-reply/reply/route-reply.test.ts b/src/auto-reply/reply/route-reply.test.ts index c675a9c13fb..3ae1be3e22b 100644 --- a/src/auto-reply/reply/route-reply.test.ts +++ b/src/auto-reply/reply/route-reply.test.ts @@ -131,11 +131,7 @@ describe("routeReply", () => { sessionKey: "agent:rich:main", cfg, }); - expect(mocks.sendMessageSlack).toHaveBeenCalledWith( - "channel:C123", - "hi", - expect.any(Object), - ); + expect(mocks.sendMessageSlack).toHaveBeenCalledWith("channel:C123", "hi", expect.any(Object)); }); it("passes thread id to Telegram sends", async () => { diff --git a/src/auto-reply/reply/route-reply.ts b/src/auto-reply/reply/route-reply.ts index bacaf134d21..ce4de8219c9 100644 --- a/src/auto-reply/reply/route-reply.ts +++ b/src/auto-reply/reply/route-reply.ts @@ -52,11 +52,8 @@ export type RouteReplyResult = { * back to the originating channel when OriginatingChannel/OriginatingTo * are set. */ -export async function routeReply( - params: RouteReplyParams, -): Promise { - const { payload, channel, to, accountId, threadId, cfg, abortSignal } = - params; +export async function routeReply(params: RouteReplyParams): Promise { + const { payload, channel, to, accountId, threadId, cfg, abortSignal } = params; // Debug: `pnpm test src/auto-reply/reply/route-reply.test.ts` const responsePrefix = params.sessionKey @@ -106,9 +103,7 @@ export async function routeReply( try { // Provider docking: this is an execution boundary (we're about to send). // Keep the module cheap to import by loading outbound plumbing lazily. - const { deliverOutboundPayloads } = await import( - "../../infra/outbound/deliver.js" - ); + const { deliverOutboundPayloads } = await import("../../infra/outbound/deliver.js"); const results = await deliverOutboundPayloads({ cfg, channel: channelId, diff --git a/src/auto-reply/reply/session-updates.test.ts b/src/auto-reply/reply/session-updates.test.ts index 6f243a53286..1583e887a4d 100644 --- a/src/auto-reply/reply/session-updates.test.ts +++ b/src/auto-reply/reply/session-updates.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../../config/config.js"; -import { - enqueueSystemEvent, - resetSystemEventsForTest, -} from "../../infra/system-events.js"; +import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system-events.js"; import { prependSystemEvents } from "./session-updates.js"; describe("prependSystemEvents", () => { diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index 4835437ef2f..8960449089f 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -156,13 +156,7 @@ export async function incrementCompactionCount(params: { storePath?: string; now?: number; }): Promise { - const { - sessionEntry, - sessionStore, - sessionKey, - storePath, - now = Date.now(), - } = params; + const { sessionEntry, sessionStore, sessionKey, storePath, now = Date.now() } = params; if (!sessionStore || !sessionKey) return undefined; const entry = sessionStore[sessionKey] ?? sessionEntry; if (!entry) return undefined; diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 6fe748e20cd..2a3a7d13257 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -10,9 +10,7 @@ import { initSessionState } from "./session.js"; describe("initSessionState thread forking", () => { it("forks a new session from the parent session file", async () => { - const root = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-thread-session-"), - ); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-thread-session-")); const sessionsDir = path.join(root, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -84,9 +82,7 @@ describe("initSessionState thread forking", () => { }); it("records topic-specific session files when MessageThreadId is present", async () => { - const root = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-topic-session-"), - ); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-topic-session-")); const storePath = path.join(root, "sessions.json"); const cfg = { @@ -134,9 +130,7 @@ describe("initSessionState RawBody", () => { }); it("Reset triggers (/new, /reset) work with RawBody", async () => { - const root = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-rawbody-reset-"), - ); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-rawbody-reset-")); const storePath = path.join(root, "sessions.json"); const cfg = { session: { store: storePath } } as ClawdbotConfig; @@ -158,9 +152,7 @@ describe("initSessionState RawBody", () => { }); it("falls back to Body when RawBody is undefined", async () => { - const root = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-rawbody-fallback-"), - ); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-rawbody-fallback-")); const storePath = path.join(root, "sessions.json"); const cfg = { session: { store: storePath } } as ClawdbotConfig; diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 97d84500f0f..6aa08d2e175 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -2,10 +2,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { - CURRENT_SESSION_VERSION, - SessionManager, -} from "@mariozechner/pi-coding-agent"; +import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { getChannelDock } from "../../channels/dock.js"; import { normalizeChannelId } from "../../channels/registry.js"; @@ -59,18 +56,14 @@ function forkSessionFromParent(params: { const manager = SessionManager.open(parentSessionFile); const leafId = manager.getLeafId(); if (leafId) { - const sessionFile = - manager.createBranchedSession(leafId) ?? manager.getSessionFile(); + const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile(); const sessionId = manager.getSessionId(); if (sessionFile && sessionId) return { sessionId, sessionFile }; } const sessionId = crypto.randomUUID(); const timestamp = new Date().toISOString(); const fileTimestamp = timestamp.replace(/[:.]/g, "-"); - const sessionFile = path.join( - manager.getSessionDir(), - `${fileTimestamp}_${sessionId}.jsonl`, - ); + const sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`); const header = { type: "session", version: CURRENT_SESSION_VERSION, @@ -95,9 +88,7 @@ export async function initSessionState(params: { // Native slash commands (Telegram/Discord/Slack) are delivered on a separate // "slash session" key, but should mutate the target chat session. const targetSessionKey = - ctx.CommandSource === "native" - ? ctx.CommandTargetSessionKey?.trim() - : undefined; + ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined; const sessionCtxForState = targetSessionKey && targetSessionKey !== ctx.SessionKey ? { ...ctx, SessionKey: targetSessionKey } @@ -111,15 +102,11 @@ export async function initSessionState(params: { const resetTriggers = sessionCfg?.resetTriggers?.length ? sessionCfg.resetTriggers : DEFAULT_RESET_TRIGGERS; - const idleMinutes = Math.max( - sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES, - 1, - ); + const idleMinutes = Math.max(sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES, 1); const sessionScope = sessionCfg?.scope ?? "per-sender"; const storePath = resolveStorePath(sessionCfg?.store, { agentId }); - const sessionStore: Record = - loadSessionStore(storePath); + const sessionStore: Record = loadSessionStore(storePath); let sessionKey: string | undefined; let sessionEntry: SessionEntry; @@ -135,16 +122,12 @@ export async function initSessionState(params: { let persistedModelOverride: string | undefined; let persistedProviderOverride: string | undefined; - const groupResolution = - resolveGroupSessionKey(sessionCtxForState) ?? undefined; - const isGroup = - ctx.ChatType?.trim().toLowerCase() === "group" || Boolean(groupResolution); + const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined; + const isGroup = ctx.ChatType?.trim().toLowerCase() === "group" || Boolean(groupResolution); // Prefer CommandBody/RawBody (clean message) for command detection; fall back // to Body which may contain structural context (history, sender labels). const commandSource = ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? ""; - const triggerBodyNormalized = stripStructuralPrefixes(commandSource) - .trim() - .toLowerCase(); + const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim().toLowerCase(); // Use CommandBody/RawBody for reset trigger matching (clean message without structural context). const rawBody = commandSource; @@ -169,10 +152,7 @@ export async function initSessionState(params: { break; } const triggerPrefix = `${trigger} `; - if ( - trimmedBody.startsWith(triggerPrefix) || - strippedForReset.startsWith(triggerPrefix) - ) { + if (trimmedBody.startsWith(triggerPrefix) || strippedForReset.startsWith(triggerPrefix)) { isNewSession = true; bodyStripped = strippedForReset.slice(trigger.length).trimStart(); break; @@ -241,15 +221,10 @@ export async function initSessionState(params: { const normalizedChannel = normalizeChannelId(channel); const isRoomProvider = Boolean( normalizedChannel && - getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes( - "channel", - ), + getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), ); const nextRoom = - explicitRoom ?? - (isRoomProvider && subject && subject.startsWith("#") - ? subject - : undefined); + explicitRoom ?? (isRoomProvider && subject && subject.startsWith("#") ? subject : undefined); const nextSubject = nextRoom ? undefined : subject; sessionEntry.chatType = groupResolution.chatType ?? "group"; sessionEntry.channel = channel; diff --git a/src/auto-reply/reply/stage-sandbox-media.ts b/src/auto-reply/reply/stage-sandbox-media.ts index 967b6671d7e..4840b3a0505 100644 --- a/src/auto-reply/reply/stage-sandbox-media.ts +++ b/src/auto-reply/reply/stage-sandbox-media.ts @@ -14,11 +14,8 @@ export async function stageSandboxMedia(params: { workspaceDir: string; }) { const { ctx, sessionCtx, cfg, sessionKey, workspaceDir } = params; - const hasPathsArray = - Array.isArray(ctx.MediaPaths) && ctx.MediaPaths.length > 0; - const pathsFromArray = Array.isArray(ctx.MediaPaths) - ? ctx.MediaPaths - : undefined; + const hasPathsArray = Array.isArray(ctx.MediaPaths) && ctx.MediaPaths.length > 0; + const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : undefined; const rawPaths = pathsFromArray && pathsFromArray.length > 0 ? pathsFromArray @@ -86,9 +83,7 @@ export async function stageSandboxMedia(params: { return mapped ?? value; }; - const nextMediaPaths = hasPathsArray - ? rawPaths.map((p) => rewriteIfStaged(p) ?? p) - : undefined; + const nextMediaPaths = hasPathsArray ? rawPaths.map((p) => rewriteIfStaged(p) ?? p) : undefined; if (nextMediaPaths) { ctx.MediaPaths = nextMediaPaths; sessionCtx.MediaPaths = nextMediaPaths; diff --git a/src/auto-reply/reply/typing.ts b/src/auto-reply/reply/typing.ts index 680163a922f..67fd299bd93 100644 --- a/src/auto-reply/reply/typing.ts +++ b/src/auto-reply/reply/typing.ts @@ -70,9 +70,7 @@ export function createTypingController(params: { } typingTtlTimer = setTimeout(() => { if (!typingTimer) return; - log?.( - `typing TTL reached (${formatTypingTtl(typingTtlMs)}); stopping typing indicator`, - ); + log?.(`typing TTL reached (${formatTypingTtl(typingTtlMs)}); stopping typing indicator`); cleanup(); }, typingTtlMs); }; diff --git a/src/auto-reply/send-policy.ts b/src/auto-reply/send-policy.ts index dcbc53f9897..101869573a0 100644 --- a/src/auto-reply/send-policy.ts +++ b/src/auto-reply/send-policy.ts @@ -2,9 +2,7 @@ import { normalizeCommandBody } from "./commands-registry.js"; export type SendPolicyOverride = "allow" | "deny"; -export function normalizeSendPolicyOverride( - raw?: string | null, -): SendPolicyOverride | undefined { +export function normalizeSendPolicyOverride(raw?: string | null): SendPolicyOverride | undefined { const value = raw?.trim().toLowerCase(); if (!value) return undefined; if (value === "allow" || value === "on") return "allow"; diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 9273e187fb9..9020fa30941 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -4,11 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { - buildCommandsMessage, - buildHelpMessage, - buildStatusMessage, -} from "./status.js"; +import { buildCommandsMessage, buildHelpMessage, buildStatusMessage } from "./status.js"; afterEach(() => { vi.restoreAllMocks(); @@ -105,9 +101,7 @@ describe("buildStatusMessage", () => { queue: { mode: "collect", depth: 0 }, }); - const optionsLine = text - .split("\n") - .find((line) => line.trim().startsWith("⚙️")); + const optionsLine = text.split("\n").find((line) => line.trim().startsWith("⚙️")); expect(optionsLine).toBeTruthy(); expect(optionsLine).not.toContain("elevated"); }); @@ -146,9 +140,7 @@ describe("buildStatusMessage", () => { modelAuth: "api-key", }); - expect(normalizeTestText(text)).toContain( - "Model: google-antigravity/claude-sonnet-4-5", - ); + expect(normalizeTestText(text)).toContain("Model: google-antigravity/claude-sonnet-4-5"); }); it("handles missing agent config gracefully", () => { @@ -200,9 +192,7 @@ describe("buildStatusMessage", () => { modelAuth: "api-key", }); - expect(text).toContain( - "Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)", - ); + expect(text).toContain("Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)"); }); it("inserts usage summary beneath context line", () => { @@ -258,9 +248,7 @@ describe("buildStatusMessage", () => { await withTempHome( async (dir) => { vi.resetModules(); - const { buildStatusMessage: buildStatusMessageDynamic } = await import( - "./status.js" - ); + const { buildStatusMessage: buildStatusMessageDynamic } = await import("./status.js"); const sessionId = "sess-1"; const logPath = path.join( @@ -325,12 +313,8 @@ describe("buildCommandsMessage", () => { commands: { config: false, debug: false }, } as ClawdbotConfig); expect(text).toContain("/commands - List all slash commands."); - expect(text).toContain( - "/think (aliases: /thinking, /t) - Set thinking level.", - ); - expect(text).toContain( - "/compact (text-only) - Compact the session context.", - ); + expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level."); + expect(text).toContain("/compact (text-only) - Compact the session context."); expect(text).not.toContain("/config"); expect(text).not.toContain("/debug"); }); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index fd0143272d2..f53300db705 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -1,19 +1,11 @@ import fs from "node:fs"; import { lookupContextTokens } from "../agents/context.js"; -import { - DEFAULT_CONTEXT_TOKENS, - DEFAULT_MODEL, - DEFAULT_PROVIDER, -} from "../agents/defaults.js"; +import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveModelAuthMode } from "../agents/model-auth.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import { resolveSandboxRuntimeStatus } from "../agents/sandbox.js"; -import { - derivePromptTokens, - normalizeUsage, - type UsageLike, -} from "../agents/usage.js"; +import { derivePromptTokens, normalizeUsage, type UsageLike } from "../agents/usage.js"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveMainSessionKey, @@ -29,20 +21,10 @@ import { resolveModelCostConfig, } from "../utils/usage-format.js"; import { VERSION } from "../version.js"; -import { - listChatCommands, - listChatCommandsForConfig, -} from "./commands-registry.js"; -import type { - ElevatedLevel, - ReasoningLevel, - ThinkLevel, - VerboseLevel, -} from "./thinking.js"; +import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js"; +import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; -type AgentConfig = Partial< - NonNullable["defaults"]> ->; +type AgentConfig = Partial["defaults"]>>; export const formatTokenCount = formatTokenCountShared; @@ -73,10 +55,7 @@ type StatusArgs = { now?: number; }; -const formatTokens = ( - total: number | null | undefined, - contextTokens: number | null, -) => { +const formatTokens = (total: number | null | undefined, contextTokens: number | null) => { const ctx = contextTokens ?? null; if (total == null) { const ctxLabel = ctx ? formatTokenCount(ctx) : "?"; @@ -115,9 +94,7 @@ const formatQueueDetails = (queue?: QueueStatus) => { if (typeof queue.debounceMs === "number") { const ms = Math.max(0, Math.round(queue.debounceMs)); const label = - ms >= 1000 - ? `${ms % 1000 === 0 ? ms / 1000 : (ms / 1000).toFixed(1)}s` - : `${ms}ms`; + ms >= 1000 ? `${ms % 1000 === 0 ? ms / 1000 : (ms / 1000).toFixed(1)}s` : `${ms}ms`; detailParts.push(`debounce ${label}`); } if (typeof queue.cap === "number") detailParts.push(`cap ${queue.cap}`); @@ -173,8 +150,7 @@ const readUsageFromSessionLog = ( if (!lastUsage) return undefined; input = lastUsage.input ?? 0; output = lastUsage.output ?? 0; - promptTokens = - derivePromptTokens(lastUsage) ?? lastUsage.total ?? input + output; + promptTokens = derivePromptTokens(lastUsage) ?? lastUsage.total ?? input + output; const total = lastUsage.total ?? promptTokens + output; if (promptTokens === 0 && total === 0) return undefined; return { input, output, promptTokens, total, model }; @@ -186,8 +162,7 @@ const readUsageFromSessionLog = ( const formatUsagePair = (input?: number | null, output?: number | null) => { if (input == null && output == null) return null; const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?"; - const outputLabel = - typeof output === "number" ? formatTokenCount(output) : "?"; + const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?"; return `🧮 Tokens: ${inputLabel} in / ${outputLabel} out`; }; @@ -203,8 +178,7 @@ export function buildStatusMessage(args: StatusArgs): string { defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); - const provider = - entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER; + const provider = entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER; let model = entry?.modelOverride ?? resolved.model ?? DEFAULT_MODEL; let contextTokens = entry?.contextTokens ?? @@ -214,9 +188,7 @@ export function buildStatusMessage(args: StatusArgs): string { let inputTokens = entry?.inputTokens; let outputTokens = entry?.outputTokens; - let totalTokens = - entry?.totalTokens ?? - (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0); + let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0); // Prefer prompt-size tokens from the session transcript when it looks larger // (cached prompt tokens are often missing from agent meta/store). @@ -237,8 +209,7 @@ export function buildStatusMessage(args: StatusArgs): string { } const thinkLevel = args.resolvedThink ?? args.agent?.thinkingDefault ?? "off"; - const verboseLevel = - args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; + const verboseLevel = args.resolvedVerbose ?? args.agent?.verboseDefault ?? "off"; const reasoningLevel = args.resolvedReasoning ?? "off"; const elevatedLevel = args.resolvedElevated ?? @@ -274,9 +245,7 @@ export function buildStatusMessage(args: StatusArgs): string { const updatedAt = entry?.updatedAt; const sessionLine = [ `Session: ${args.sessionKey ?? "unknown"}`, - typeof updatedAt === "number" - ? `updated ${formatAge(now - updatedAt)}` - : "no activity", + typeof updatedAt === "number" ? `updated ${formatAge(now - updatedAt)}` : "no activity", ] .filter(Boolean) .join(" • "); @@ -318,8 +287,7 @@ export function buildStatusMessage(args: StatusArgs): string { const authMode = resolveModelAuthMode(provider, args.config); const authLabelValue = - args.modelAuth ?? - (authMode && authMode !== "unknown" ? authMode : undefined); + args.modelAuth ?? (authMode && authMode !== "unknown" ? authMode : undefined); const showCost = authLabelValue === "api-key" || authLabelValue === "mixed"; const costConfig = showCost ? resolveModelCostConfig({ @@ -328,8 +296,7 @@ export function buildStatusMessage(args: StatusArgs): string { config: args.config, }) : undefined; - const hasUsage = - typeof inputTokens === "number" || typeof outputTokens === "number"; + const hasUsage = typeof inputTokens === "number" || typeof outputTokens === "number"; const cost = showCost && hasUsage ? estimateUsageCost({ @@ -350,9 +317,7 @@ export function buildStatusMessage(args: StatusArgs): string { const usagePair = formatUsagePair(inputTokens, outputTokens); const costLine = costLabel ? `💵 Cost: ${costLabel}` : null; const usageCostLine = - usagePair && costLine - ? `${usagePair} · ${costLine}` - : (usagePair ?? costLine); + usagePair && costLine ? `${usagePair} · ${costLine}` : (usagePair ?? costLine); return [ versionLine, @@ -405,9 +370,7 @@ export function buildCommandsMessage(cfg?: ClawdbotConfig): string { seen.add(key); return true; }); - const aliasLabel = aliases.length - ? ` (aliases: ${aliases.join(", ")})` - : ""; + const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : ""; const scopeLabel = command.scope === "text" ? " (text-only)" : ""; lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`); } diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index 6bc85a93ba9..6d49b7cced8 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - listThinkingLevels, - normalizeReasoningLevel, - normalizeThinkLevel, -} from "./thinking.js"; +import { listThinkingLevels, normalizeReasoningLevel, normalizeThinkLevel } from "./thinking.js"; describe("normalizeThinkLevel", () => { it("accepts mid as medium", () => { @@ -25,9 +21,7 @@ describe("listThinkingLevels", () => { }); it("excludes xhigh for non-codex models", () => { - expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain( - "xhigh", - ); + expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain("xhigh"); }); }); diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 7af93dd403c..8e99c2d46f4 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -1,10 +1,4 @@ -export type ThinkLevel = - | "off" - | "minimal" - | "low" - | "medium" - | "high" - | "xhigh"; +export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; export type VerboseLevel = "off" | "on"; export type ElevatedLevel = "off" | "on"; export type ReasoningLevel = "off" | "on" | "stream"; @@ -16,9 +10,7 @@ export const XHIGH_MODEL_REFS = [ "openai-codex/gpt-5.1-codex", ] as const; -const XHIGH_MODEL_SET = new Set( - XHIGH_MODEL_REFS.map((entry) => entry.toLowerCase()), -); +const XHIGH_MODEL_SET = new Set(XHIGH_MODEL_REFS.map((entry) => entry.toLowerCase())); const XHIGH_MODEL_IDS = new Set( XHIGH_MODEL_REFS.map((entry) => entry.split("/")[1]?.toLowerCase()).filter( (entry): entry is string => Boolean(entry), @@ -26,42 +18,22 @@ const XHIGH_MODEL_IDS = new Set( ); // Normalize user-provided thinking level strings to the canonical enum. -export function normalizeThinkLevel( - raw?: string | null, -): ThinkLevel | undefined { +export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined { if (!raw) return undefined; const key = raw.toLowerCase(); if (["off"].includes(key)) return "off"; if (["min", "minimal"].includes(key)) return "minimal"; - if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) - return "low"; - if ( - ["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes( - key, - ) - ) + if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) return "low"; + if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) return "medium"; - if ( - [ - "high", - "ultra", - "ultrathink", - "think-hard", - "thinkhardest", - "highest", - "max", - ].includes(key) - ) + if (["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key)) return "high"; if (["xhigh", "x-high", "x_high"].includes(key)) return "xhigh"; if (["think"].includes(key)) return "minimal"; return undefined; } -export function supportsXHighThinking( - provider?: string | null, - model?: string | null, -): boolean { +export function supportsXHighThinking(provider?: string | null, model?: string | null): boolean { const modelKey = model?.trim().toLowerCase(); if (!modelKey) return false; const providerKey = provider?.trim().toLowerCase(); @@ -71,10 +43,7 @@ export function supportsXHighThinking( return XHIGH_MODEL_IDS.has(modelKey); } -export function listThinkingLevels( - provider?: string | null, - model?: string | null, -): ThinkLevel[] { +export function listThinkingLevels(provider?: string | null, model?: string | null): ThinkLevel[] { const levels: ThinkLevel[] = ["off", "minimal", "low", "medium", "high"]; if (supportsXHighThinking(provider, model)) levels.push("xhigh"); return levels; @@ -97,9 +66,7 @@ export function formatXHighModelHint(): string { } // Normalize verbose flags used to toggle agent verbosity. -export function normalizeVerboseLevel( - raw?: string | null, -): VerboseLevel | undefined { +export function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undefined { if (!raw) return undefined; const key = raw.toLowerCase(); if (["off", "false", "no", "0"].includes(key)) return "off"; @@ -108,22 +75,16 @@ export function normalizeVerboseLevel( } // Normalize response-usage display flags used to toggle cost/token lines. -export function normalizeUsageDisplay( - raw?: string | null, -): UsageDisplayLevel | undefined { +export function normalizeUsageDisplay(raw?: string | null): UsageDisplayLevel | undefined { if (!raw) return undefined; const key = raw.toLowerCase(); - if (["off", "false", "no", "0", "disable", "disabled"].includes(key)) - return "off"; - if (["on", "true", "yes", "1", "enable", "enabled"].includes(key)) - return "on"; + if (["off", "false", "no", "0", "disable", "disabled"].includes(key)) return "off"; + if (["on", "true", "yes", "1", "enable", "enabled"].includes(key)) return "on"; return undefined; } // Normalize elevated flags used to toggle elevated bash permissions. -export function normalizeElevatedLevel( - raw?: string | null, -): ElevatedLevel | undefined { +export function normalizeElevatedLevel(raw?: string | null): ElevatedLevel | undefined { if (!raw) return undefined; const key = raw.toLowerCase(); if (["off", "false", "no", "0"].includes(key)) return "off"; @@ -132,30 +93,12 @@ export function normalizeElevatedLevel( } // Normalize reasoning visibility flags used to toggle reasoning exposure. -export function normalizeReasoningLevel( - raw?: string | null, -): ReasoningLevel | undefined { +export function normalizeReasoningLevel(raw?: string | null): ReasoningLevel | undefined { if (!raw) return undefined; const key = raw.toLowerCase(); - if ( - [ - "off", - "false", - "no", - "0", - "hide", - "hidden", - "disable", - "disabled", - ].includes(key) - ) + if (["off", "false", "no", "0", "hide", "hidden", "disable", "disabled"].includes(key)) return "off"; - if ( - ["on", "true", "yes", "1", "show", "visible", "enable", "enabled"].includes( - key, - ) - ) - return "on"; + if (["on", "true", "yes", "1", "show", "visible", "enable", "enabled"].includes(key)) return "on"; if (["stream", "streaming", "draft", "live"].includes(key)) return "stream"; return undefined; } diff --git a/src/auto-reply/tool-meta.test.ts b/src/auto-reply/tool-meta.test.ts index b8da3af3344..3961e907321 100644 --- a/src/auto-reply/tool-meta.test.ts +++ b/src/auto-reply/tool-meta.test.ts @@ -1,11 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - formatToolAggregate, - formatToolPrefix, - shortenMeta, - shortenPath, -} from "./tool-meta.js"; +import { formatToolAggregate, formatToolPrefix, shortenMeta, shortenPath } from "./tool-meta.js"; describe("tool meta formatting", () => { beforeEach(() => { diff --git a/src/auto-reply/tool-meta.ts b/src/auto-reply/tool-meta.ts index da8d50a6a78..9e67c444dbb 100644 --- a/src/auto-reply/tool-meta.ts +++ b/src/auto-reply/tool-meta.ts @@ -1,7 +1,4 @@ -import { - formatToolSummary, - resolveToolDisplay, -} from "../agents/tool-display.js"; +import { formatToolSummary, resolveToolDisplay } from "../agents/tool-display.js"; import { shortenHomeInString, shortenHomePath } from "../utils.js"; export function shortenPath(p: string): string { @@ -17,10 +14,7 @@ export function shortenMeta(meta: string): string { return `${shortenHomeInString(base)}${rest}`; } -export function formatToolAggregate( - toolName?: string, - metas?: string[], -): string { +export function formatToolAggregate(toolName?: string, metas?: string[]): string { const filtered = (metas ?? []).filter(Boolean).map(shortenMeta); const display = resolveToolDisplay({ name: toolName }); const prefix = `${display.emoji} ${display.label}`; diff --git a/src/auto-reply/transcription.test.ts b/src/auto-reply/transcription.test.ts index c47d76daf0f..bdd97abe410 100644 --- a/src/auto-reply/transcription.test.ts +++ b/src/auto-reply/transcription.test.ts @@ -54,22 +54,14 @@ describe("transcribeInboundAudio", () => { stderr: "", }); const { transcribeInboundAudio } = await import("./transcription.js"); - const result = await transcribeInboundAudio( - cfg as never, - ctx as never, - runtime as never, - ); + const result = await transcribeInboundAudio(cfg as never, ctx as never, runtime as never); expect(result?.text).toBe("transcribed text"); expect(fetchMock).toHaveBeenCalled(); }); it("returns undefined when no transcription command", async () => { const { transcribeInboundAudio } = await import("./transcription.js"); - const res = await transcribeInboundAudio( - { audio: {} } as never, - {} as never, - runtime as never, - ); + const res = await transcribeInboundAudio({ audio: {} } as never, {} as never, runtime as never); expect(res).toBeUndefined(); }); }); diff --git a/src/auto-reply/transcription.ts b/src/auto-reply/transcription.ts index b303ed1be51..e63e79a6870 100644 --- a/src/auto-reply/transcription.ts +++ b/src/auto-reply/transcription.ts @@ -33,9 +33,7 @@ export async function transcribeInboundAudio( } const timeoutMs = Math.max( - (toolTranscriber?.timeoutSeconds ?? - legacyTranscriber?.timeoutSeconds ?? - 45) * 1000, + (toolTranscriber?.timeoutSeconds ?? legacyTranscriber?.timeoutSeconds ?? 45) * 1000, 1_000, ); let tmpPath: string | undefined; @@ -46,10 +44,7 @@ export async function transcribeInboundAudio( if (!res.ok) throw new Error(`HTTP ${res.status}`); const arrayBuf = await res.arrayBuffer(); const buffer = Buffer.from(arrayBuf); - tmpPath = path.join( - os.tmpdir(), - `clawdbot-audio-${crypto.randomUUID()}.ogg`, - ); + tmpPath = path.join(os.tmpdir(), `clawdbot-audio-${crypto.randomUUID()}.ogg`); await fs.writeFile(tmpPath, buffer); mediaPath = tmpPath; if (shouldLogVerbose()) { @@ -62,12 +57,10 @@ export async function transcribeInboundAudio( const templCtx: MsgContext = { ...ctx, MediaPath: mediaPath }; const argv = hasToolTranscriber - ? [AUDIO_TRANSCRIPTION_BINARY, ...(toolTranscriber?.args ?? [])].map( - (part, index) => (index === 0 ? part : applyTemplate(part, templCtx)), + ? [AUDIO_TRANSCRIPTION_BINARY, ...(toolTranscriber?.args ?? [])].map((part, index) => + index === 0 ? part : applyTemplate(part, templCtx), ) - : (legacyTranscriber?.command ?? []).map((part) => - applyTemplate(part, templCtx), - ); + : (legacyTranscriber?.command ?? []).map((part) => applyTemplate(part, templCtx)); if (shouldLogVerbose()) { logVerbose(`Transcribing audio via command: ${argv.join(" ")}`); } diff --git a/src/auto-reply/types.ts b/src/auto-reply/types.ts index 9344276ccf8..bf6e765e1d8 100644 --- a/src/auto-reply/types.ts +++ b/src/auto-reply/types.ts @@ -11,10 +11,7 @@ export type GetReplyOptions = { isHeartbeat?: boolean; onPartialReply?: (payload: ReplyPayload) => Promise | void; onReasoningStream?: (payload: ReplyPayload) => Promise | void; - onBlockReply?: ( - payload: ReplyPayload, - context?: BlockReplyContext, - ) => Promise | void; + onBlockReply?: (payload: ReplyPayload, context?: BlockReplyContext) => Promise | void; onToolResult?: (payload: ReplyPayload) => Promise | void; disableBlockStreaming?: boolean; /** Timeout for block reply delivery (ms). */ diff --git a/src/browser/cdp.helpers.ts b/src/browser/cdp.helpers.ts index 5b0c509de08..89f7fb5637c 100644 --- a/src/browser/cdp.helpers.ts +++ b/src/browser/cdp.helpers.ts @@ -13,10 +13,7 @@ type Pending = { reject: (err: Error) => void; }; -export type CdpSendFn = ( - method: string, - params?: Record, -) => Promise; +export type CdpSendFn = (method: string, params?: Record) => Promise; export function isLoopbackHost(host: string) { const h = host.trim().toLowerCase(); @@ -35,10 +32,7 @@ function createCdpSender(ws: WebSocket) { let nextId = 1; const pending = new Map(); - const send: CdpSendFn = ( - method: string, - params?: Record, - ) => { + const send: CdpSendFn = (method: string, params?: Record) => { const id = nextId++; const msg = { id, method, params }; ws.send(JSON.stringify(msg)); diff --git a/src/browser/cdp.test.ts b/src/browser/cdp.test.ts index 450ce2b445b..a75cf7134cb 100644 --- a/src/browser/cdp.test.ts +++ b/src/browser/cdp.test.ts @@ -3,12 +3,7 @@ import { createServer } from "node:http"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocketServer } from "ws"; import { rawDataToString } from "../infra/ws.js"; -import { - createTargetViaCdp, - evaluateJavaScript, - normalizeCdpWsUrl, - snapshotAria, -} from "./cdp.js"; +import { createTargetViaCdp, evaluateJavaScript, normalizeCdpWsUrl, snapshotAria } from "./cdp.js"; describe("cdp", () => { let httpServer: ReturnType | null = null; @@ -63,9 +58,7 @@ describe("cdp", () => { res.end("not found"); }); - await new Promise((resolve) => - httpServer?.listen(0, "127.0.0.1", resolve), - ); + await new Promise((resolve) => httpServer?.listen(0, "127.0.0.1", resolve)); const httpPort = (httpServer.address() as { port: number }).port; const created = await createTargetViaCdp({ diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index 338602d7ff4..053cc1acbca 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -32,9 +32,7 @@ export async function captureScreenshot(opts: { return await withCdpSocket(opts.wsUrl, async (send) => { await send("Page.enable"); - let clip: - | { x: number; y: number; width: number; height: number; scale: number } - | undefined; + let clip: { x: number; y: number; width: number; height: number; scale: number } | undefined; if (opts.fullPage) { const metrics = (await send("Page.getLayoutMetrics")) as { cssContentSize?: { width?: number; height?: number }; @@ -50,9 +48,7 @@ export async function captureScreenshot(opts: { const format = opts.format ?? "png"; const quality = - format === "jpeg" - ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) - : undefined; + format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : undefined; const result = (await send("Page.captureScreenshot", { format, @@ -73,10 +69,7 @@ export async function createTargetViaCdp(opts: { url: string; }): Promise<{ targetId: string }> { const base = opts.cdpUrl.replace(/\/$/, ""); - const version = await fetchJson<{ webSocketDebuggerUrl?: string }>( - `${base}/json/version`, - 1500, - ); + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`${base}/json/version`, 1500); const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim(); const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : ""; if (!wsUrl) throw new Error("CDP /json/version missing webSocketDebuggerUrl"); @@ -86,8 +79,7 @@ export async function createTargetViaCdp(opts: { targetId?: string; }; const targetId = String(created?.targetId ?? "").trim(); - if (!targetId) - throw new Error("CDP Target.createTarget returned no targetId"); + if (!targetId) throw new Error("CDP Target.createTarget returned no targetId"); return { targetId }; }); } @@ -167,10 +159,7 @@ function axValue(v: unknown): string { return ""; } -function formatAriaSnapshot( - nodes: RawAXNode[], - limit: number, -): AriaSnapshotNode[] { +function formatAriaSnapshot(nodes: RawAXNode[], limit: number): AriaSnapshotNode[] { const byId = new Map(); for (const n of nodes) { if (n.nodeId) byId.set(n.nodeId, n); @@ -181,14 +170,11 @@ function formatAriaSnapshot( for (const n of nodes) { for (const c of n.childIds ?? []) referenced.add(c); } - const root = - nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0]; + const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0]; if (!root?.nodeId) return []; const out: AriaSnapshotNode[] = []; - const stack: Array<{ id: string; depth: number }> = [ - { id: root.nodeId, depth: 0 }, - ]; + const stack: Array<{ id: string; depth: number }> = [{ id: root.nodeId, depth: 0 }]; while (stack.length && out.length < limit) { const popped = stack.pop(); if (!popped) break; @@ -206,9 +192,7 @@ function formatAriaSnapshot( name: name || "", ...(value ? { value } : {}), ...(description ? { description } : {}), - ...(typeof n.backendDOMNodeId === "number" - ? { backendDOMNodeId: n.backendDOMNodeId } - : {}), + ...(typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {}), depth, }); @@ -245,10 +229,7 @@ export async function snapshotDom(opts: { nodes: DomSnapshotNode[]; }> { const limit = Math.max(1, Math.min(5000, Math.floor(opts.limit ?? 800))); - const maxTextChars = Math.max( - 0, - Math.min(5000, Math.floor(opts.maxTextChars ?? 220)), - ); + const maxTextChars = Math.max(0, Math.min(5000, Math.floor(opts.maxTextChars ?? 220))); const expression = `(() => { const maxNodes = ${JSON.stringify(limit)}; @@ -328,10 +309,7 @@ export async function getDomText(opts: { maxChars?: number; selector?: string; }): Promise<{ text: string }> { - const maxChars = Math.max( - 0, - Math.min(5_000_000, Math.floor(opts.maxChars ?? 200_000)), - ); + const maxChars = Math.max(0, Math.min(5_000_000, Math.floor(opts.maxChars ?? 200_000))); const selectorExpr = opts.selector ? JSON.stringify(opts.selector) : "null"; const expression = `(() => { const fmt = ${JSON.stringify(opts.format)}; @@ -376,14 +354,8 @@ export async function querySelector(opts: { matches: QueryMatch[]; }> { const limit = Math.max(1, Math.min(200, Math.floor(opts.limit ?? 20))); - const maxText = Math.max( - 0, - Math.min(5000, Math.floor(opts.maxTextChars ?? 500)), - ); - const maxHtml = Math.max( - 0, - Math.min(20000, Math.floor(opts.maxHtmlChars ?? 1500)), - ); + const maxText = Math.max(0, Math.min(5000, Math.floor(opts.maxTextChars ?? 500))); + const maxHtml = Math.max(0, Math.min(20000, Math.floor(opts.maxHtmlChars ?? 1500))); const expression = `(() => { const sel = ${JSON.stringify(opts.selector)}; diff --git a/src/browser/chrome.executables.ts b/src/browser/chrome.executables.ts index 756f2bdbb66..7407528b6c2 100644 --- a/src/browser/chrome.executables.ts +++ b/src/browser/chrome.executables.ts @@ -17,9 +17,7 @@ function exists(filePath: string) { } } -function findFirstExecutable( - candidates: Array, -): BrowserExecutable | null { +function findFirstExecutable(candidates: Array): BrowserExecutable | null { for (const candidate of candidates) { if (exists(candidate.path)) return candidate; } @@ -46,10 +44,7 @@ export function findChromeExecutableMac(): BrowserExecutable | null { }, { kind: "chromium", - path: path.join( - os.homedir(), - "Applications/Chromium.app/Contents/MacOS/Chromium", - ), + path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium"), }, { kind: "chrome", @@ -57,10 +52,7 @@ export function findChromeExecutableMac(): BrowserExecutable | null { }, { kind: "chrome", - path: path.join( - os.homedir(), - "Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - ), + path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), }, ]; @@ -84,8 +76,7 @@ export function findChromeExecutableWindows(): BrowserExecutable | null { const localAppData = process.env.LOCALAPPDATA ?? ""; const programFiles = process.env.ProgramFiles ?? "C:\\Program Files"; // Must use bracket notation: variable name contains parentheses - const programFilesX86 = - process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; + const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; const joinWin = path.win32.join; const candidates: Array = []; @@ -94,13 +85,7 @@ export function findChromeExecutableWindows(): BrowserExecutable | null { // Chrome Canary (user install) candidates.push({ kind: "canary", - path: joinWin( - localAppData, - "Google", - "Chrome SxS", - "Application", - "chrome.exe", - ), + path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"), }); // Chromium (user install) candidates.push({ @@ -110,37 +95,19 @@ export function findChromeExecutableWindows(): BrowserExecutable | null { // Chrome (user install) candidates.push({ kind: "chrome", - path: joinWin( - localAppData, - "Google", - "Chrome", - "Application", - "chrome.exe", - ), + path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe"), }); } // Chrome (system install, 64-bit) candidates.push({ kind: "chrome", - path: joinWin( - programFiles, - "Google", - "Chrome", - "Application", - "chrome.exe", - ), + path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe"), }); // Chrome (system install, 32-bit on 64-bit Windows) candidates.push({ kind: "chrome", - path: joinWin( - programFilesX86, - "Google", - "Chrome", - "Application", - "chrome.exe", - ), + path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), }); return findFirstExecutable(candidates); @@ -152,9 +119,7 @@ export function resolveBrowserExecutableForPlatform( ): BrowserExecutable | null { if (resolved.executablePath) { if (!exists(resolved.executablePath)) { - throw new Error( - `browser.executablePath not found: ${resolved.executablePath}`, - ); + throw new Error(`browser.executablePath not found: ${resolved.executablePath}`); } return { kind: "custom", path: resolved.executablePath }; } diff --git a/src/browser/chrome.profile-decoration.ts b/src/browser/chrome.profile-decoration.ts index aa12ae3067a..7c5d96d6fd6 100644 --- a/src/browser/chrome.profile-decoration.ts +++ b/src/browser/chrome.profile-decoration.ts @@ -1,10 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { - DEFAULT_CLAWD_BROWSER_COLOR, - DEFAULT_CLAWD_BROWSER_PROFILE_NAME, -} from "./constants.js"; +import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME } from "./constants.js"; function decoratedMarkerPath(userDataDir: string) { return path.join(userDataDir, ".clawd-profile-decorated"); @@ -15,8 +12,7 @@ function safeReadJson(filePath: string): Record | null { if (!fs.existsSync(filePath)) return null; const raw = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(raw) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) - return null; + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null; return parsed as Record; } catch { return null; @@ -46,9 +42,7 @@ function parseHexRgbToSignedArgbInt(hex: string): number | null { const rgb = Number.parseInt(cleaned, 16); const argbUnsigned = (0xff << 24) | rgb; // Chrome stores colors as signed 32-bit ints (SkColor). - return argbUnsigned > 0x7fffffff - ? argbUnsigned - 0x1_0000_0000 - : argbUnsigned; + return argbUnsigned > 0x7fffffff ? argbUnsigned - 0x1_0000_0000 : argbUnsigned; } export function isProfileDecorated( @@ -74,10 +68,7 @@ export function isProfileDecorated( typeof (infoCache as Record).Default === "object" && (infoCache as Record).Default !== null && !Array.isArray((infoCache as Record).Default) - ? ((infoCache as Record).Default as Record< - string, - unknown - >) + ? ((infoCache as Record).Default as Record) : null; const prefs = safeReadJson(preferencesPath); @@ -95,9 +86,7 @@ export function isProfileDecorated( const autogeneratedTheme = (() => { const autogenerated = prefs?.autogenerated; const theme = - typeof autogenerated === "object" && - autogenerated !== null && - !Array.isArray(autogenerated) + typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated) ? (autogenerated as Record).theme : null; return typeof theme === "object" && theme !== null && !Array.isArray(theme) @@ -105,8 +94,7 @@ export function isProfileDecorated( : null; })(); - const nameOk = - typeof info?.name === "string" ? info.name === desiredName : true; + const nameOk = typeof info?.name === "string" ? info.name === desiredName : true; if (desiredColorInt == null) { // If the user provided a non-#RRGGBB value, we can only do best-effort. @@ -121,8 +109,7 @@ export function isProfileDecorated( const prefOk = (typeof browserTheme?.user_color2 === "number" && browserTheme.user_color2 === desiredColorInt) || - (typeof autogeneratedTheme?.color === "number" && - autogeneratedTheme.color === desiredColorInt); + (typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt); return nameOk && localSeedOk && prefOk; } @@ -136,9 +123,7 @@ export function decorateClawdProfile( opts?: { name?: string; color?: string }, ) { const desiredName = opts?.name ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME; - const desiredColor = ( - opts?.color ?? DEFAULT_CLAWD_BROWSER_COLOR - ).toUpperCase(); + const desiredColor = (opts?.color ?? DEFAULT_CLAWD_BROWSER_COLOR).toUpperCase(); const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor); const localStatePath = path.join(userDataDir, "Local State"); @@ -146,32 +131,12 @@ export function decorateClawdProfile( const localState = safeReadJson(localStatePath) ?? {}; // Common-ish shape: profile.info_cache.Default - setDeep( - localState, - ["profile", "info_cache", "Default", "name"], - desiredName, - ); - setDeep( - localState, - ["profile", "info_cache", "Default", "shortcut_name"], - desiredName, - ); - setDeep( - localState, - ["profile", "info_cache", "Default", "user_name"], - desiredName, - ); + setDeep(localState, ["profile", "info_cache", "Default", "name"], desiredName); + setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], desiredName); + setDeep(localState, ["profile", "info_cache", "Default", "user_name"], desiredName); // Color keys are best-effort (Chrome changes these frequently). - setDeep( - localState, - ["profile", "info_cache", "Default", "profile_color"], - desiredColor, - ); - setDeep( - localState, - ["profile", "info_cache", "Default", "user_color"], - desiredColor, - ); + setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], desiredColor); + setDeep(localState, ["profile", "info_cache", "Default", "user_color"], desiredColor); if (desiredColorInt != null) { // These are the fields Chrome actually uses for profile/avatar tinting. setDeep( @@ -210,11 +175,7 @@ export function decorateClawdProfile( safeWriteJson(preferencesPath, prefs); try { - fs.writeFileSync( - decoratedMarkerPath(userDataDir), - `${Date.now()}\n`, - "utf-8", - ); + fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8"); } catch { // ignore } diff --git a/src/browser/chrome.test.ts b/src/browser/chrome.test.ts index 0e7ec84507a..afc3bcbc872 100644 --- a/src/browser/chrome.test.ts +++ b/src/browser/chrome.test.ts @@ -13,10 +13,7 @@ import { resolveBrowserExecutableForPlatform, stopClawdChrome, } from "./chrome.js"; -import { - DEFAULT_CLAWD_BROWSER_COLOR, - DEFAULT_CLAWD_BROWSER_PROFILE_NAME, -} from "./constants.js"; +import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME } from "./constants.js"; async function readJson(filePath: string): Promise> { const raw = await fsp.readFile(filePath, "utf-8"); @@ -30,9 +27,7 @@ describe("browser chrome profile decoration", () => { }); it("writes expected name + signed ARGB seed to Chrome prefs", async () => { - const userDataDir = await fsp.mkdtemp( - path.join(os.tmpdir(), "clawdbot-chrome-test-"), - ); + const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-")); try { decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR }); @@ -50,9 +45,7 @@ describe("browser chrome profile decoration", () => { expect(def.default_avatar_fill_color).toBe(expectedSignedArgb); expect(def.default_avatar_stroke_color).toBe(expectedSignedArgb); - const prefs = await readJson( - path.join(userDataDir, "Default", "Preferences"), - ); + const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); const browser = prefs.browser as Record; const theme = browser.theme as Record; const autogenerated = prefs.autogenerated as Record; @@ -72,9 +65,7 @@ describe("browser chrome profile decoration", () => { }); it("best-effort writes name when color is invalid", async () => { - const userDataDir = await fsp.mkdtemp( - path.join(os.tmpdir(), "clawdbot-chrome-test-"), - ); + const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-")); try { decorateClawdProfile(userDataDir, { color: "lobster-orange" }); const localState = await readJson(path.join(userDataDir, "Local State")); @@ -90,9 +81,7 @@ describe("browser chrome profile decoration", () => { }); it("recovers from missing/invalid preference files", async () => { - const userDataDir = await fsp.mkdtemp( - path.join(os.tmpdir(), "clawdbot-chrome-test-"), - ); + const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-")); try { await fsp.mkdir(path.join(userDataDir, "Default"), { recursive: true }); await fsp.writeFile(path.join(userDataDir, "Local State"), "{", "utf-8"); // invalid JSON @@ -107,9 +96,7 @@ describe("browser chrome profile decoration", () => { const localState = await readJson(path.join(userDataDir, "Local State")); expect(typeof localState.profile).toBe("object"); - const prefs = await readJson( - path.join(userDataDir, "Default", "Preferences"), - ); + const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); expect(typeof prefs.profile).toBe("object"); } finally { await fsp.rm(userDataDir, { recursive: true, force: true }); @@ -117,16 +104,12 @@ describe("browser chrome profile decoration", () => { }); it("is idempotent when rerun on an existing profile", async () => { - const userDataDir = await fsp.mkdtemp( - path.join(os.tmpdir(), "clawdbot-chrome-test-"), - ); + const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "clawdbot-chrome-test-")); try { decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR }); decorateClawdProfile(userDataDir, { color: DEFAULT_CLAWD_BROWSER_COLOR }); - const prefs = await readJson( - path.join(userDataDir, "Default", "Preferences"), - ); + const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); const profile = prefs.profile as Record; expect(profile.name).toBe(DEFAULT_CLAWD_BROWSER_PROFILE_NAME); } finally { @@ -171,9 +154,7 @@ describe("browser chrome helpers", () => { it("finds Chrome in Program Files on Windows", () => { const marker = path.win32.join("Program Files", "Google", "Chrome"); - const exists = vi - .spyOn(fs, "existsSync") - .mockImplementation((p) => String(p).includes(marker)); + const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker)); const exe = findChromeExecutableWindows(); expect(exe?.kind).toBe("chrome"); expect(exe?.path).toMatch(/chrome\.exe$/); @@ -197,9 +178,7 @@ describe("browser chrome helpers", () => { "Application", "chrome.exe", ); - const exists = vi - .spyOn(fs, "existsSync") - .mockImplementation((p) => String(p).includes(marker)); + const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker)); const exe = resolveBrowserExecutableForPlatform( {} as Parameters[0], "win32", @@ -217,9 +196,7 @@ describe("browser chrome helpers", () => { json: async () => ({ webSocketDebuggerUrl: "ws://127.0.0.1/devtools" }), } as unknown as Response), ); - await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe( - true, - ); + await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(true); vi.stubGlobal( "fetch", @@ -228,14 +205,10 @@ describe("browser chrome helpers", () => { json: async () => ({}), } as unknown as Response), ); - await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe( - false, - ); + await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("boom"))); - await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe( - false, - ); + await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); }); it("stopClawdChrome no-ops when process is already killed", async () => { diff --git a/src/browser/chrome.ts b/src/browser/chrome.ts index 4952994b40c..68ab1f055a9 100644 --- a/src/browser/chrome.ts +++ b/src/browser/chrome.ts @@ -12,18 +12,9 @@ import { type BrowserExecutable, resolveBrowserExecutableForPlatform, } from "./chrome.executables.js"; -import { - decorateClawdProfile, - isProfileDecorated, -} from "./chrome.profile-decoration.js"; -import type { - ResolvedBrowserConfig, - ResolvedBrowserProfile, -} from "./config.js"; -import { - DEFAULT_CLAWD_BROWSER_COLOR, - DEFAULT_CLAWD_BROWSER_PROFILE_NAME, -} from "./constants.js"; +import { decorateClawdProfile, isProfileDecorated } from "./chrome.profile-decoration.js"; +import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js"; +import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_PROFILE_NAME } from "./constants.js"; const log = createSubsystemLogger("browser").child("chrome"); @@ -34,10 +25,7 @@ export { findChromeExecutableWindows, resolveBrowserExecutableForPlatform, } from "./chrome.executables.js"; -export { - decorateClawdProfile, - isProfileDecorated, -} from "./chrome.profile-decoration.js"; +export { decorateClawdProfile, isProfileDecorated } from "./chrome.profile-decoration.js"; function exists(filePath: string) { try { @@ -56,15 +44,11 @@ export type RunningChrome = { proc: ChildProcessWithoutNullStreams; }; -function resolveBrowserExecutable( - resolved: ResolvedBrowserConfig, -): BrowserExecutable | null { +function resolveBrowserExecutable(resolved: ResolvedBrowserConfig): BrowserExecutable | null { return resolveBrowserExecutableForPlatform(resolved, process.platform); } -export function resolveClawdUserDataDir( - profileName = DEFAULT_CLAWD_BROWSER_PROFILE_NAME, -) { +export function resolveClawdUserDataDir(profileName = DEFAULT_CLAWD_BROWSER_PROFILE_NAME) { return path.join(CONFIG_DIR, "browser", profileName, "user-data"); } @@ -72,10 +56,7 @@ function cdpUrlForPort(cdpPort: number) { return `http://127.0.0.1:${cdpPort}`; } -export async function isChromeReachable( - cdpUrl: string, - timeoutMs = 500, -): Promise { +export async function isChromeReachable(cdpUrl: string, timeoutMs = 500): Promise { const version = await fetchChromeVersion(cdpUrl, timeoutMs); return Boolean(version); } @@ -86,10 +67,7 @@ type ChromeVersion = { "User-Agent"?: string; }; -async function fetchChromeVersion( - cdpUrl: string, - timeoutMs = 500, -): Promise { +async function fetchChromeVersion(cdpUrl: string, timeoutMs = 500): Promise { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { @@ -118,10 +96,7 @@ export async function getChromeWebSocketUrl( return normalizeCdpWsUrl(wsUrl, cdpUrl); } -async function canOpenWebSocket( - wsUrl: string, - timeoutMs = 800, -): Promise { +async function canOpenWebSocket(wsUrl: string, timeoutMs = 800): Promise { return await new Promise((resolve) => { const ws = new WebSocket(wsUrl, { handshakeTimeout: timeoutMs }); const timer = setTimeout( @@ -166,17 +141,13 @@ export async function launchClawdChrome( profile: ResolvedBrowserProfile, ): Promise { if (!profile.cdpIsLoopback) { - throw new Error( - `Profile "${profile.name}" is remote; cannot launch local Chrome.`, - ); + throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`); } await ensurePortAvailable(profile.cdpPort); const exe = resolveBrowserExecutable(resolved); if (!exe) { - throw new Error( - "No supported browser found (Chrome/Chromium on macOS, Linux, or Windows).", - ); + throw new Error("No supported browser found (Chrome/Chromium on macOS, Linux, or Windows)."); } const userDataDir = resolveClawdUserDataDir(profile.name); @@ -301,10 +272,7 @@ export async function launchClawdChrome( }; } -export async function stopClawdChrome( - running: RunningChrome, - timeoutMs = 2500, -) { +export async function stopClawdChrome(running: RunningChrome, timeoutMs = 2500) { const proc = running.proc; if (proc.killed) return; try { diff --git a/src/browser/client-actions-core.ts b/src/browser/client-actions-core.ts index 5d4b76218ab..667600ef393 100644 --- a/src/browser/client-actions-core.ts +++ b/src/browser/client-actions-core.ts @@ -96,15 +96,12 @@ export async function browserNavigate( opts: { url: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/navigate${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ url: opts.url, targetId: opts.targetId }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/navigate${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: opts.url, targetId: opts.targetId }), + timeoutMs: 20000, + }); } export async function browserArmDialog( @@ -118,20 +115,17 @@ export async function browserArmDialog( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/hooks/dialog${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - accept: opts.accept, - promptText: opts.promptText, - targetId: opts.targetId, - timeoutMs: opts.timeoutMs, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/hooks/dialog${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + accept: opts.accept, + promptText: opts.promptText, + targetId: opts.targetId, + timeoutMs: opts.timeoutMs, + }), + timeoutMs: 20000, + }); } export async function browserArmFileChooser( @@ -147,22 +141,19 @@ export async function browserArmFileChooser( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/hooks/file-chooser${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - paths: opts.paths, - ref: opts.ref, - inputRef: opts.inputRef, - element: opts.element, - targetId: opts.targetId, - timeoutMs: opts.timeoutMs, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/hooks/file-chooser${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + paths: opts.paths, + ref: opts.ref, + inputRef: opts.inputRef, + element: opts.element, + targetId: opts.targetId, + timeoutMs: opts.timeoutMs, + }), + timeoutMs: 20000, + }); } export async function browserWaitForDownload( @@ -245,19 +236,16 @@ export async function browserScreenshotAction( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/screenshot${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - fullPage: opts.fullPage, - ref: opts.ref, - element: opts.element, - type: opts.type, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/screenshot${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + fullPage: opts.fullPage, + ref: opts.ref, + element: opts.element, + type: opts.type, + }), + timeoutMs: 20000, + }); } diff --git a/src/browser/client-actions-observe.ts b/src/browser/client-actions-observe.ts index 651f597c9d2..4002d48c52e 100644 --- a/src/browser/client-actions-observe.ts +++ b/src/browser/client-actions-observe.ts @@ -1,7 +1,4 @@ -import type { - BrowserActionPathResult, - BrowserActionTargetOk, -} from "./client-actions-types.js"; +import type { BrowserActionPathResult, BrowserActionTargetOk } from "./client-actions-types.js"; import { fetchBrowserJson } from "./client-fetch.js"; import type { BrowserConsoleMessage, @@ -91,20 +88,17 @@ export async function browserTraceStart( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/trace/start${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - screenshots: opts.screenshots, - snapshots: opts.snapshots, - sources: opts.sources, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/trace/start${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + screenshots: opts.screenshots, + snapshots: opts.snapshots, + sources: opts.sources, + }), + timeoutMs: 20000, + }); } export async function browserTraceStop( @@ -112,15 +106,12 @@ export async function browserTraceStop( opts: { targetId?: string; path?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/trace/stop${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, path: opts.path }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/trace/stop${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, path: opts.path }), + timeoutMs: 20000, + }); } export async function browserHighlight( @@ -128,15 +119,12 @@ export async function browserHighlight( opts: { ref: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/highlight${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, ref: opts.ref }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/highlight${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, ref: opts.ref }), + timeoutMs: 20000, + }); } export async function browserResponseBody( diff --git a/src/browser/client-actions-state.ts b/src/browser/client-actions-state.ts index a8463171882..192cad997e6 100644 --- a/src/browser/client-actions-state.ts +++ b/src/browser/client-actions-state.ts @@ -1,7 +1,4 @@ -import type { - BrowserActionOk, - BrowserActionTargetOk, -} from "./client-actions-types.js"; +import type { BrowserActionOk, BrowserActionTargetOk } from "./client-actions-types.js"; import { fetchBrowserJson } from "./client-fetch.js"; function buildProfileQuery(profile?: string): string { @@ -32,15 +29,12 @@ export async function browserCookiesSet( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/cookies/set${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, cookie: opts.cookie }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/cookies/set${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, cookie: opts.cookie }), + timeoutMs: 20000, + }); } export async function browserCookiesClear( @@ -48,15 +42,12 @@ export async function browserCookiesClear( opts: { targetId?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/cookies/clear${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/cookies/clear${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId }), + timeoutMs: 20000, + }); } export async function browserStorageGet( @@ -91,19 +82,16 @@ export async function browserStorageSet( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/storage/${opts.kind}/set${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - key: opts.key, - value: opts.value, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/storage/${opts.kind}/set${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + key: opts.key, + value: opts.value, + }), + timeoutMs: 20000, + }); } export async function browserStorageClear( @@ -127,15 +115,12 @@ export async function browserSetOffline( opts: { offline: boolean; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/offline${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, offline: opts.offline }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/offline${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, offline: opts.offline }), + timeoutMs: 20000, + }); } export async function browserSetHeaders( @@ -147,15 +132,12 @@ export async function browserSetHeaders( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/headers${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, headers: opts.headers }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/headers${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, headers: opts.headers }), + timeoutMs: 20000, + }); } export async function browserSetHttpCredentials( @@ -169,20 +151,17 @@ export async function browserSetHttpCredentials( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/credentials${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - username: opts.username, - password: opts.password, - clear: opts.clear, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/credentials${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + username: opts.username, + password: opts.password, + clear: opts.clear, + }), + timeoutMs: 20000, + }); } export async function browserSetGeolocation( @@ -198,22 +177,19 @@ export async function browserSetGeolocation( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/geolocation${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - latitude: opts.latitude, - longitude: opts.longitude, - accuracy: opts.accuracy, - origin: opts.origin, - clear: opts.clear, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/geolocation${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + latitude: opts.latitude, + longitude: opts.longitude, + accuracy: opts.accuracy, + origin: opts.origin, + clear: opts.clear, + }), + timeoutMs: 20000, + }); } export async function browserSetMedia( @@ -225,18 +201,15 @@ export async function browserSetMedia( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/media${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - colorScheme: opts.colorScheme, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/media${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + colorScheme: opts.colorScheme, + }), + timeoutMs: 20000, + }); } export async function browserSetTimezone( @@ -244,18 +217,15 @@ export async function browserSetTimezone( opts: { timezoneId: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/timezone${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - timezoneId: opts.timezoneId, - }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/timezone${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + timezoneId: opts.timezoneId, + }), + timeoutMs: 20000, + }); } export async function browserSetLocale( @@ -263,15 +233,12 @@ export async function browserSetLocale( opts: { locale: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/locale${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, locale: opts.locale }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/locale${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, locale: opts.locale }), + timeoutMs: 20000, + }); } export async function browserSetDevice( @@ -279,15 +246,12 @@ export async function browserSetDevice( opts: { name: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/device${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, name: opts.name }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/device${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, name: opts.name }), + timeoutMs: 20000, + }); } export async function browserClearPermissions( @@ -295,13 +259,10 @@ export async function browserClearPermissions( opts: { targetId?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson( - `${baseUrl}/set/geolocation${q}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targetId: opts.targetId, clear: true }), - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/set/geolocation${q}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ targetId: opts.targetId, clear: true }), + timeoutMs: 20000, + }); } diff --git a/src/browser/client-fetch.ts b/src/browser/client-fetch.ts index 91c375d9f4b..caf07d8ba0a 100644 --- a/src/browser/client-fetch.ts +++ b/src/browser/client-fetch.ts @@ -6,11 +6,7 @@ function unwrapCause(err: unknown): unknown { return cause ?? null; } -function enhanceBrowserFetchError( - url: string, - err: unknown, - timeoutMs: number, -): Error { +function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error { const cause = unwrapCause(err); const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? ""; @@ -35,9 +31,7 @@ function enhanceBrowserFetchError( ); } - return new Error( - `Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`, - ); + return new Error(`Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`); } export async function fetchBrowserJson( diff --git a/src/browser/client.test.ts b/src/browser/client.test.ts index 4b813fb46ee..4f53dc3ecbf 100644 --- a/src/browser/client.test.ts +++ b/src/browser/client.test.ts @@ -1,11 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { - browserOpenTab, - browserSnapshot, - browserStatus, - browserTabs, -} from "./client.js"; +import { browserOpenTab, browserSnapshot, browserStatus, browserTabs } from "./client.js"; import { browserAct, browserArmDialog, @@ -31,16 +26,12 @@ describe("browser client", () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(fetchFailed)); - await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow( - /Start .*gateway/i, - ); + await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(/Start .*gateway/i); }); it("adds useful timeout messaging for abort-like failures", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("aborted"))); - await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow( - /timed out/i, - ); + await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(/timed out/i); }); it("surfaces non-2xx responses with body text", async () => { @@ -182,16 +173,12 @@ describe("browser client", () => { }), ); - await expect( - browserStatus("http://127.0.0.1:18791"), - ).resolves.toMatchObject({ + await expect(browserStatus("http://127.0.0.1:18791")).resolves.toMatchObject({ running: true, cdpPort: 18792, }); - await expect(browserTabs("http://127.0.0.1:18791")).resolves.toHaveLength( - 1, - ); + await expect(browserTabs("http://127.0.0.1:18791")).resolves.toHaveLength(1); await expect( browserOpenTab("http://127.0.0.1:18791", "https://example.com"), ).resolves.toMatchObject({ targetId: "t2" }); @@ -217,9 +204,10 @@ describe("browser client", () => { await expect( browserConsoleMessages("http://127.0.0.1:18791", { level: "error" }), ).resolves.toMatchObject({ ok: true, targetId: "t1" }); - await expect( - browserPdfSave("http://127.0.0.1:18791"), - ).resolves.toMatchObject({ ok: true, path: "/tmp/a.pdf" }); + await expect(browserPdfSave("http://127.0.0.1:18791")).resolves.toMatchObject({ + ok: true, + path: "/tmp/a.pdf", + }); await expect( browserScreenshotAction("http://127.0.0.1:18791", { fullPage: true }), ).resolves.toMatchObject({ ok: true, path: "/tmp/a.png" }); diff --git a/src/browser/client.ts b/src/browser/client.ts index 279ac5edf4d..5b86073343d 100644 --- a/src/browser/client.ts +++ b/src/browser/client.ts @@ -102,20 +102,14 @@ export async function browserStatus( }); } -export async function browserProfiles( - baseUrl: string, -): Promise { - const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>( - `${baseUrl}/profiles`, - { timeoutMs: 3000 }, - ); +export async function browserProfiles(baseUrl: string): Promise { + const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>(`${baseUrl}/profiles`, { + timeoutMs: 3000, + }); return res.profiles ?? []; } -export async function browserStart( - baseUrl: string, - opts?: { profile?: string }, -): Promise { +export async function browserStart(baseUrl: string, opts?: { profile?: string }): Promise { const q = buildProfileQuery(opts?.profile); await fetchBrowserJson(`${baseUrl}/start${q}`, { method: "POST", @@ -123,10 +117,7 @@ export async function browserStart( }); } -export async function browserStop( - baseUrl: string, - opts?: { profile?: string }, -): Promise { +export async function browserStop(baseUrl: string, opts?: { profile?: string }): Promise { const q = buildProfileQuery(opts?.profile); await fetchBrowserJson(`${baseUrl}/stop${q}`, { method: "POST", @@ -139,13 +130,10 @@ export async function browserResetProfile( opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - return await fetchBrowserJson( - `${baseUrl}/reset-profile${q}`, - { - method: "POST", - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/reset-profile${q}`, { + method: "POST", + timeoutMs: 20000, + }); } export type BrowserCreateProfileResult = { @@ -161,19 +149,16 @@ export async function browserCreateProfile( baseUrl: string, opts: { name: string; color?: string; cdpUrl?: string }, ): Promise { - return await fetchBrowserJson( - `${baseUrl}/profiles/create`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: opts.name, - color: opts.color, - cdpUrl: opts.cdpUrl, - }), - timeoutMs: 10000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/profiles/create`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: opts.name, + color: opts.color, + cdpUrl: opts.cdpUrl, + }), + timeoutMs: 10000, + }); } export type BrowserDeleteProfileResult = { @@ -241,13 +226,10 @@ export async function browserCloseTab( opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - await fetchBrowserJson( - `${baseUrl}/tabs/${encodeURIComponent(targetId)}${q}`, - { - method: "DELETE", - timeoutMs: 5000, - }, - ); + await fetchBrowserJson(`${baseUrl}/tabs/${encodeURIComponent(targetId)}${q}`, { + method: "DELETE", + timeoutMs: 5000, + }); } export async function browserTabAction( @@ -292,20 +274,16 @@ export async function browserSnapshot( if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) { q.set("maxChars", String(opts.maxChars)); } - if (typeof opts.interactive === "boolean") - q.set("interactive", String(opts.interactive)); + if (typeof opts.interactive === "boolean") q.set("interactive", String(opts.interactive)); if (typeof opts.compact === "boolean") q.set("compact", String(opts.compact)); if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) q.set("depth", String(opts.depth)); if (opts.selector?.trim()) q.set("selector", opts.selector.trim()); if (opts.frame?.trim()) q.set("frame", opts.frame.trim()); if (opts.profile) q.set("profile", opts.profile); - return await fetchBrowserJson( - `${baseUrl}/snapshot?${q.toString()}`, - { - timeoutMs: 20000, - }, - ); + return await fetchBrowserJson(`${baseUrl}/snapshot?${q.toString()}`, { + timeoutMs: 20000, + }); } // Actions beyond the basic read-only commands live in client-actions.ts. diff --git a/src/browser/config.test.ts b/src/browser/config.test.ts index d5a96f13016..da54656eab6 100644 --- a/src/browser/config.test.ts +++ b/src/browser/config.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - resolveBrowserConfig, - resolveProfile, - shouldStartLocalBrowserServer, -} from "./config.js"; +import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js"; describe("browser config", () => { it("defaults to enabled with loopback control url and lobster-orange color", () => { @@ -108,8 +104,8 @@ describe("browser config", () => { }); it("rejects unsupported protocols", () => { - expect(() => - resolveBrowserConfig({ controlUrl: "ws://127.0.0.1:18791" }), - ).toThrow(/must be http/i); + expect(() => resolveBrowserConfig({ controlUrl: "ws://127.0.0.1:18791" })).toThrow( + /must be http/i, + ); }); }); diff --git a/src/browser/config.ts b/src/browser/config.ts index 40c8165a751..5d45d6b5f1f 100644 --- a/src/browser/config.ts +++ b/src/browser/config.ts @@ -62,9 +62,7 @@ export function parseHttpUrl(raw: string, label: string) { const trimmed = raw.trim(); const parsed = new URL(trimmed); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new Error( - `${label} must be http(s), got: ${parsed.protocol.replace(":", "")}`, - ); + throw new Error(`${label} must be http(s), got: ${parsed.protocol.replace(":", "")}`); } const port = @@ -104,9 +102,7 @@ function ensureDefaultProfile( } return result; } -export function resolveBrowserConfig( - cfg: BrowserConfig | undefined, -): ResolvedBrowserConfig { +export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBrowserConfig { const enabled = cfg?.enabled ?? DEFAULT_CLAWD_BROWSER_ENABLED; const envControlUrl = process.env.CLAWDBOT_BROWSER_CONTROL_URL?.trim(); const derivedControlPort = (() => { @@ -116,15 +112,10 @@ export function resolveBrowserConfig( if (!Number.isFinite(gatewayPort) || gatewayPort <= 0) return null; return deriveDefaultBrowserControlPort(gatewayPort); })(); - const derivedControlUrl = derivedControlPort - ? `http://127.0.0.1:${derivedControlPort}` - : null; + const derivedControlUrl = derivedControlPort ? `http://127.0.0.1:${derivedControlPort}` : null; const controlInfo = parseHttpUrl( - cfg?.controlUrl ?? - envControlUrl ?? - derivedControlUrl ?? - DEFAULT_CLAWD_BROWSER_CONTROL_URL, + cfg?.controlUrl ?? envControlUrl ?? derivedControlUrl ?? DEFAULT_CLAWD_BROWSER_CONTROL_URL, "browser.controlUrl", ); const controlPort = controlInfo.port; @@ -163,8 +154,7 @@ export function resolveBrowserConfig( const attachOnly = cfg?.attachOnly === true; const executablePath = cfg?.executablePath?.trim() || undefined; - const defaultProfile = - cfg?.defaultProfile ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME; + const defaultProfile = cfg?.defaultProfile ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME; // Use legacy cdpUrl port for backward compatibility when no profiles configured const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined; const profiles = ensureDefaultProfile( @@ -210,10 +200,7 @@ export function resolveProfile( let cdpUrl = ""; if (rawProfileUrl) { - const parsed = parseHttpUrl( - rawProfileUrl, - `browser.profiles.${profileName}.cdpUrl`, - ); + const parsed = parseHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`); cdpHost = parsed.parsed.hostname; cdpPort = parsed.port; cdpUrl = parsed.normalized; diff --git a/src/browser/profiles-service.test.ts b/src/browser/profiles-service.test.ts index 4fa80a8cbd6..4092cf655dc 100644 --- a/src/browser/profiles-service.test.ts +++ b/src/browser/profiles-service.test.ts @@ -5,10 +5,7 @@ import { describe, expect, it, vi } from "vitest"; import { resolveBrowserConfig } from "./config.js"; import { createBrowserProfilesService } from "./profiles-service.js"; -import type { - BrowserRouteContext, - BrowserServerState, -} from "./server-context.js"; +import type { BrowserRouteContext, BrowserServerState } from "./server-context.js"; vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/browser/profiles-service.ts b/src/browser/profiles-service.ts index 55d7187895c..489c56ca310 100644 --- a/src/browser/profiles-service.ts +++ b/src/browser/profiles-service.ts @@ -44,16 +44,12 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) { return await ctx.listProfiles(); }; - const createProfile = async ( - params: CreateProfileParams, - ): Promise => { + const createProfile = async (params: CreateProfileParams): Promise => { const name = params.name.trim(); const rawCdpUrl = params.cdpUrl?.trim() || undefined; if (!isValidProfileName(name)) { - throw new Error( - "invalid profile name: use lowercase letters, numbers, and hyphens only", - ); + throw new Error("invalid profile name: use lowercase letters, numbers, and hyphens only"); } const state = ctx.state(); @@ -70,9 +66,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) { const usedColors = getUsedColors(resolvedProfiles); const profileColor = - params.color && HEX_COLOR_RE.test(params.color) - ? params.color - : allocateColor(usedColors); + params.color && HEX_COLOR_RE.test(params.color) ? params.color : allocateColor(usedColors); let profileConfig: BrowserProfileConfig; if (rawCdpUrl) { @@ -80,9 +74,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) { profileConfig = { cdpUrl: parsed.normalized, color: profileColor }; } else { const usedPorts = getUsedPorts(resolvedProfiles); - const range = deriveDefaultBrowserCdpPortRange( - state.resolved.controlPort, - ); + const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort); const cdpPort = allocateCdpPort(usedPorts, range); if (cdpPort === null) { throw new Error("no available CDP ports in range"); @@ -119,9 +111,7 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) { }; }; - const deleteProfile = async ( - nameRaw: string, - ): Promise => { + const deleteProfile = async (nameRaw: string): Promise => { const name = nameRaw.trim(); if (!name) throw new Error("profile name is required"); if (!isValidProfileName(name)) { diff --git a/src/browser/profiles.test.ts b/src/browser/profiles.test.ts index 328cc50d17e..23bf6403c0a 100644 --- a/src/browser/profiles.test.ts +++ b/src/browser/profiles.test.ts @@ -66,13 +66,9 @@ describe("port allocation", () => { it("allocates within an explicit range", () => { const usedPorts = new Set(); - expect(allocateCdpPort(usedPorts, { start: 20000, end: 20002 })).toBe( - 20000, - ); + expect(allocateCdpPort(usedPorts, { start: 20000, end: 20002 })).toBe(20000); usedPorts.add(20000); - expect(allocateCdpPort(usedPorts, { start: 20000, end: 20002 })).toBe( - 20001, - ); + expect(allocateCdpPort(usedPorts, { start: 20000, end: 20002 })).toBe(20001); }); it("skips used ports and returns next available", () => { diff --git a/src/browser/profiles.ts b/src/browser/profiles.ts index d301669179b..8705dbdad91 100644 --- a/src/browser/profiles.ts +++ b/src/browser/profiles.ts @@ -28,12 +28,7 @@ export function allocateCdpPort( ): number | null { const start = range?.start ?? CDP_PORT_RANGE_START; const end = range?.end ?? CDP_PORT_RANGE_END; - if ( - !Number.isFinite(start) || - !Number.isFinite(end) || - start <= 0 || - end <= 0 - ) { + if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end <= 0) { return null; } if (start > end) return null; diff --git a/src/browser/pw-ai.test.ts b/src/browser/pw-ai.test.ts index ce3411b64db..29c33d77450 100644 --- a/src/browser/pw-ai.test.ts +++ b/src/browser/pw-ai.test.ts @@ -11,11 +11,7 @@ type FakeSession = { detach: ReturnType; }; -function createPage(opts: { - targetId: string; - snapshotFull?: string; - hasSnapshotForAI?: boolean; -}) { +function createPage(opts: { targetId: string; snapshotFull?: string; hasSnapshotForAI?: boolean }) { const session: FakeSession = { send: vi.fn().mockResolvedValue({ targetInfo: { targetId: opts.targetId }, @@ -39,9 +35,7 @@ function createPage(opts: { ...(opts.hasSnapshotForAI === false ? {} : { - _snapshotForAI: vi - .fn() - .mockResolvedValue({ full: opts.snapshotFull ?? "SNAP" }), + _snapshotForAI: vi.fn().mockResolvedValue({ full: opts.snapshotFull ?? "SNAP" }), }), }; @@ -77,9 +71,7 @@ describe("pw-ai", () => { const p2 = createPage({ targetId: "T2", snapshotFull: "TWO" }); const browser = createBrowser([p1.page, p2.page]); - ( - chromium.connectOverCDP as unknown as ReturnType - ).mockResolvedValue(browser); + (chromium.connectOverCDP as unknown as ReturnType).mockResolvedValue(browser); const mod = await importModule(); const res = await mod.snapshotAiViaPlaywright({ @@ -98,9 +90,7 @@ describe("pw-ai", () => { const p1 = createPage({ targetId: "T1", snapshotFull: longSnapshot }); const browser = createBrowser([p1.page]); - ( - chromium.connectOverCDP as unknown as ReturnType - ).mockResolvedValue(browser); + (chromium.connectOverCDP as unknown as ReturnType).mockResolvedValue(browser); const mod = await importModule(); const res = await mod.snapshotAiViaPlaywright({ @@ -118,9 +108,7 @@ describe("pw-ai", () => { const { chromium } = await import("playwright-core"); const p1 = createPage({ targetId: "T1" }); const browser = createBrowser([p1.page]); - ( - chromium.connectOverCDP as unknown as ReturnType - ).mockResolvedValue(browser); + (chromium.connectOverCDP as unknown as ReturnType).mockResolvedValue(browser); const mod = await importModule(); await mod.clickViaPlaywright({ @@ -137,9 +125,7 @@ describe("pw-ai", () => { const { chromium } = await import("playwright-core"); const p1 = createPage({ targetId: "T1", hasSnapshotForAI: false }); const browser = createBrowser([p1.page]); - ( - chromium.connectOverCDP as unknown as ReturnType - ).mockResolvedValue(browser); + (chromium.connectOverCDP as unknown as ReturnType).mockResolvedValue(browser); const mod = await importModule(); await expect( diff --git a/src/browser/pw-role-snapshot.test.ts b/src/browser/pw-role-snapshot.test.ts index 6c6258881c9..abf49c5b9ff 100644 --- a/src/browser/pw-role-snapshot.test.ts +++ b/src/browser/pw-role-snapshot.test.ts @@ -27,9 +27,7 @@ describe("pw-role-snapshot", () => { }); it("uses nth only when duplicates exist", () => { - const aria = ['- button "OK"', '- button "OK"', '- button "Cancel"'].join( - "\n", - ); + const aria = ['- button "OK"', '- button "OK"', '- button "Cancel"'].join("\n"); const res = buildRoleSnapshotFromAriaSnapshot(aria); expect(res.snapshot).toContain("[ref=e1]"); expect(res.snapshot).toContain("[ref=e2] [nth=1]"); @@ -38,9 +36,7 @@ describe("pw-role-snapshot", () => { expect(res.refs.e3?.nth).toBeUndefined(); }); it("respects maxDepth", () => { - const aria = ['- region "Main"', " - group", ' - button "Deep"'].join( - "\n", - ); + const aria = ['- region "Main"', " - group", ' - button "Deep"'].join("\n"); const res = buildRoleSnapshotFromAriaSnapshot(aria, { maxDepth: 1 }); expect(res.snapshot).toContain('- region "Main"'); expect(res.snapshot).toContain(" - group"); diff --git a/src/browser/pw-role-snapshot.ts b/src/browser/pw-role-snapshot.ts index e39fb392ec3..091373ab57e 100644 --- a/src/browser/pw-role-snapshot.ts +++ b/src/browser/pw-role-snapshot.ts @@ -77,13 +77,8 @@ const STRUCTURAL_ROLES = new Set([ "none", ]); -export function getRoleSnapshotStats( - snapshot: string, - refs: RoleRefMap, -): RoleSnapshotStats { - const interactive = Object.values(refs).filter((r) => - INTERACTIVE_ROLES.has(r.role), - ).length; +export function getRoleSnapshotStats(snapshot: string, refs: RoleRefMap): RoleSnapshotStats { + const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length; return { lines: snapshot.split("\n").length, chars: snapshot.length, @@ -137,10 +132,7 @@ function createRoleNameTracker(): RoleNameTracker { }; } -function removeNthFromNonDuplicates( - refs: RoleRefMap, - tracker: RoleNameTracker, -) { +function removeNthFromNonDuplicates(refs: RoleRefMap, tracker: RoleNameTracker) { const duplicates = tracker.getDuplicateKeys(); for (const [ref, data] of Object.entries(refs)) { const key = tracker.getKey(data.role, data.name); diff --git a/src/browser/pw-session.ts b/src/browser/pw-session.ts index 1a3c569d738..df081af8af0 100644 --- a/src/browser/pw-session.ts +++ b/src/browser/pw-session.ts @@ -39,9 +39,7 @@ type SnapshotForAIResult = { full: string; incremental?: string }; type SnapshotForAIOptions = { timeout?: number; track?: string }; export type WithSnapshotForAI = { - _snapshotForAI?: ( - options?: SnapshotForAIOptions, - ) => Promise; + _snapshotForAI?: (options?: SnapshotForAIOptions) => Promise; }; type TargetInfoResponse = { @@ -213,9 +211,7 @@ async function connectBrowser(cdpUrl: string): Promise { for (let attempt = 0; attempt < 3; attempt += 1) { try { const timeout = 5000 + attempt * 2000; - const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch( - () => null, - ); + const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch(() => null); const endpoint = wsUrl ?? normalized; const browser = await chromium.connectOverCDP(endpoint, { timeout }); const connected: ConnectedBrowser = { browser, cdpUrl: normalized }; @@ -234,9 +230,7 @@ async function connectBrowser(cdpUrl: string): Promise { if (lastErr instanceof Error) { throw lastErr; } - const message = lastErr - ? formatErrorMessage(lastErr) - : "CDP connect failed"; + const message = lastErr ? formatErrorMessage(lastErr) : "CDP connect failed"; throw new Error(message); }; @@ -256,9 +250,7 @@ async function getAllPages(browser: Browser): Promise { async function pageTargetId(page: Page): Promise { const session = await page.context().newCDPSession(page); try { - const info = (await session.send( - "Target.getTargetInfo", - )) as TargetInfoResponse; + const info = (await session.send("Target.getTargetInfo")) as TargetInfoResponse; const targetId = String(info?.targetInfo?.targetId ?? "").trim(); return targetId || null; } finally { @@ -266,10 +258,7 @@ async function pageTargetId(page: Page): Promise { } } -async function findPageByTargetId( - browser: Browser, - targetId: string, -): Promise { +async function findPageByTargetId(browser: Browser, targetId: string): Promise { const pages = await getAllPages(browser); for (const page of pages) { const tid = await pageTargetId(page).catch(() => null); @@ -284,8 +273,7 @@ export async function getPageForTargetId(opts: { }): Promise { const { browser } = await connectBrowser(opts.cdpUrl); const pages = await getAllPages(browser); - if (!pages.length) - throw new Error("No pages available in the connected browser."); + if (!pages.length) throw new Error("No pages available in the connected browser."); const first = pages[0]; if (!opts.targetId) return first; const found = await findPageByTargetId(browser, opts.targetId); diff --git a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts index 47e8d3aee68..60713a55116 100644 --- a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts +++ b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts @@ -57,9 +57,7 @@ describe("pw-tools-core", () => { }); it("rewrites strict mode violations for scrollIntoView", async () => { const scrollIntoViewIfNeeded = vi.fn(async () => { - throw new Error( - 'Error: strict mode violation: locator("aria-ref=1") resolved to 2 elements', - ); + throw new Error('Error: strict mode violation: locator("aria-ref=1") resolved to 2 elements'); }); currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; @@ -75,9 +73,7 @@ describe("pw-tools-core", () => { }); it("rewrites not-visible timeouts for scrollIntoView", async () => { const scrollIntoViewIfNeeded = vi.fn(async () => { - throw new Error( - 'Timeout 5000ms exceeded. waiting for locator("aria-ref=1") to be visible', - ); + throw new Error('Timeout 5000ms exceeded. waiting for locator("aria-ref=1") to be visible'); }); currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; @@ -93,9 +89,7 @@ describe("pw-tools-core", () => { }); it("rewrites strict mode violations into snapshot hints", async () => { const click = vi.fn(async () => { - throw new Error( - 'Error: strict mode violation: locator("aria-ref=1") resolved to 2 elements', - ); + throw new Error('Error: strict mode violation: locator("aria-ref=1") resolved to 2 elements'); }); currentRefLocator = { click }; currentPage = {}; @@ -111,9 +105,7 @@ describe("pw-tools-core", () => { }); it("rewrites not-visible timeouts into snapshot hints", async () => { const click = vi.fn(async () => { - throw new Error( - 'Timeout 5000ms exceeded. waiting for locator("aria-ref=1") to be visible', - ); + throw new Error('Timeout 5000ms exceeded. waiting for locator("aria-ref=1") to be visible'); }); currentRefLocator = { click }; currentPage = {}; diff --git a/src/browser/pw-tools-core.downloads.ts b/src/browser/pw-tools-core.downloads.ts index 659d8d74312..bc2b3c5341a 100644 --- a/src/browser/pw-tools-core.downloads.ts +++ b/src/browser/pw-tools-core.downloads.ts @@ -4,11 +4,7 @@ import path from "node:path"; import type { Page } from "playwright-core"; -import { - ensurePageState, - getPageForTargetId, - refLocator, -} from "./pw-session.js"; +import { ensurePageState, getPageForTargetId, refLocator } from "./pw-session.js"; import { bumpDialogArmId, bumpDownloadArmId, diff --git a/src/browser/pw-tools-core.interactions.ts b/src/browser/pw-tools-core.interactions.ts index 89a574b19c7..bd86d1752e1 100644 --- a/src/browser/pw-tools-core.interactions.ts +++ b/src/browser/pw-tools-core.interactions.ts @@ -1,14 +1,6 @@ import type { BrowserFormField } from "./client-actions-core.js"; -import { - ensurePageState, - getPageForTargetId, - refLocator, -} from "./pw-session.js"; -import { - normalizeTimeoutMs, - requireRef, - toAIFriendlyError, -} from "./pw-tools-core.shared.js"; +import { ensurePageState, getPageForTargetId, refLocator } from "./pw-session.js"; +import { normalizeTimeoutMs, requireRef, toAIFriendlyError } from "./pw-tools-core.shared.js"; export async function highlightViaPlaywright(opts: { cdpUrl: string; @@ -41,10 +33,7 @@ export async function clickViaPlaywright(opts: { ensurePageState(page); const ref = requireRef(opts.ref); const locator = refLocator(page, ref); - const timeout = Math.max( - 500, - Math.min(60_000, Math.floor(opts.timeoutMs ?? 8000)), - ); + const timeout = Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs ?? 8000))); try { if (opts.doubleClick) { await locator.dblclick({ @@ -191,10 +180,7 @@ export async function fillFormViaPlaywright(opts: { const locator = refLocator(page, ref); if (type === "checkbox" || type === "radio") { const checked = - rawValue === true || - rawValue === 1 || - rawValue === "1" || - rawValue === "true"; + rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true"; try { await locator.setChecked(checked, { timeout }); } catch (err) { @@ -311,10 +297,7 @@ export async function waitForViaPlaywright(opts: { if (opts.selector) { const selector = String(opts.selector).trim(); if (selector) { - await page - .locator(selector) - .first() - .waitFor({ state: "visible", timeout }); + await page.locator(selector).first().waitFor({ state: "visible", timeout }); } } if (opts.url) { @@ -346,15 +329,13 @@ export async function takeScreenshotViaPlaywright(opts: { ensurePageState(page); const type = opts.type ?? "png"; if (opts.ref) { - if (opts.fullPage) - throw new Error("fullPage is not supported for element screenshots"); + if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots"); const locator = refLocator(page, opts.ref); const buffer = await locator.screenshot({ type }); return { buffer }; } if (opts.element) { - if (opts.fullPage) - throw new Error("fullPage is not supported for element screenshots"); + if (opts.fullPage) throw new Error("fullPage is not supported for element screenshots"); const locator = page.locator(opts.element).first(); const buffer = await locator.screenshot({ type }); return { buffer }; @@ -376,8 +357,7 @@ export async function setInputFilesViaPlaywright(opts: { const page = await getPageForTargetId(opts); ensurePageState(page); if (!opts.paths.length) throw new Error("paths are required"); - const inputRef = - typeof opts.inputRef === "string" ? opts.inputRef.trim() : ""; + const inputRef = typeof opts.inputRef === "string" ? opts.inputRef.trim() : ""; const element = typeof opts.element === "string" ? opts.element.trim() : ""; if (inputRef && element) { throw new Error("inputRef and element are mutually exclusive"); @@ -386,9 +366,7 @@ export async function setInputFilesViaPlaywright(opts: { throw new Error("inputRef or element is required"); } - const locator = inputRef - ? refLocator(page, inputRef) - : page.locator(element).first(); + const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first(); try { await locator.setInputFiles(opts.paths); diff --git a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts index 97fd8c06c9a..7684cb36716 100644 --- a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts +++ b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts @@ -149,9 +149,7 @@ describe("pw-tools-core", () => { }); expect(waitForTimeout).toHaveBeenCalledWith(50); - expect( - currentPage.locator as ReturnType, - ).toHaveBeenCalledWith("#main"); + expect(currentPage.locator as ReturnType).toHaveBeenCalledWith("#main"); expect(waitForSelector).toHaveBeenCalledWith({ state: "visible", timeout: 1234, diff --git a/src/browser/pw-tools-core.responses.ts b/src/browser/pw-tools-core.responses.ts index 2f3aa4617ed..d2e477d2c3e 100644 --- a/src/browser/pw-tools-core.responses.ts +++ b/src/browser/pw-tools-core.responses.ts @@ -7,9 +7,7 @@ function matchUrlPattern(pattern: string, url: string): boolean { if (p === url) return true; if (p.includes("*")) { const escaped = p.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); - const regex = new RegExp( - `^${escaped.replace(/\*\*/g, ".*").replace(/\*/g, ".*")}$`, - ); + const regex = new RegExp(`^${escaped.replace(/\*\*/g, ".*").replace(/\*/g, ".*")}$`); return regex.test(url); } return url.includes(p); @@ -94,13 +92,10 @@ export async function responseBodyViaPlaywright(opts: { bodyText = new TextDecoder("utf-8").decode(buf); } } catch (err) { - throw new Error( - `Failed to read response body for "${url}": ${String(err)}`, - ); + throw new Error(`Failed to read response body for "${url}": ${String(err)}`); } - const trimmed = - bodyText.length > maxChars ? bodyText.slice(0, maxChars) : bodyText; + const trimmed = bodyText.length > maxChars ? bodyText.slice(0, maxChars) : bodyText; return { url, status, diff --git a/src/browser/pw-tools-core.screenshots-element-selector.test.ts b/src/browser/pw-tools-core.screenshots-element-selector.test.ts index 48718b4cc7b..61e9ac8499e 100644 --- a/src/browser/pw-tools-core.screenshots-element-selector.test.ts +++ b/src/browser/pw-tools-core.screenshots-element-selector.test.ts @@ -59,9 +59,7 @@ describe("pw-tools-core", () => { expect(res.buffer.toString()).toBe("E"); expect(sessionMocks.getPageForTargetId).toHaveBeenCalled(); - expect( - currentPage.locator as ReturnType, - ).toHaveBeenCalledWith("#main"); + expect(currentPage.locator as ReturnType).toHaveBeenCalledWith("#main"); expect(elementScreenshot).toHaveBeenCalledWith({ type: "png" }); }); it("screenshots a ref locator", async () => { @@ -115,9 +113,7 @@ describe("pw-tools-core", () => { }); it("arms the next file chooser and sets files (default timeout)", async () => { const fileChooser = { setFiles: vi.fn(async () => {}) }; - const waitForEvent = vi.fn( - async (_event: string, _opts: unknown) => fileChooser, - ); + const waitForEvent = vi.fn(async (_event: string, _opts: unknown) => fileChooser); currentPage = { waitForEvent, keyboard: { press: vi.fn(async () => {}) }, diff --git a/src/browser/pw-tools-core.shared.ts b/src/browser/pw-tools-core.shared.ts index 3f2888d40f2..6cc2521b989 100644 --- a/src/browser/pw-tools-core.shared.ts +++ b/src/browser/pw-tools-core.shared.ts @@ -27,10 +27,7 @@ export function requireRef(value: unknown): string { return ref; } -export function normalizeTimeoutMs( - timeoutMs: number | undefined, - fallback: number, -) { +export function normalizeTimeoutMs(timeoutMs: number | undefined, fallback: number) { return Math.max(500, Math.min(120_000, timeoutMs ?? fallback)); } diff --git a/src/browser/pw-tools-core.snapshot.ts b/src/browser/pw-tools-core.snapshot.ts index 26a84261a0e..41abba84e76 100644 --- a/src/browser/pw-tools-core.snapshot.ts +++ b/src/browser/pw-tools-core.snapshot.ts @@ -5,11 +5,7 @@ import { getRoleSnapshotStats, type RoleSnapshotOptions, } from "./pw-role-snapshot.js"; -import { - ensurePageState, - getPageForTargetId, - type WithSnapshotForAI, -} from "./pw-session.js"; +import { ensurePageState, getPageForTargetId, type WithSnapshotForAI } from "./pw-session.js"; export async function snapshotAiViaPlaywright(opts: { cdpUrl: string; @@ -25,16 +21,11 @@ export async function snapshotAiViaPlaywright(opts: { const maybe = page as unknown as WithSnapshotForAI; if (!maybe._snapshotForAI) { - throw new Error( - "Playwright _snapshotForAI is not available. Upgrade playwright-core.", - ); + throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core."); } const result = await maybe._snapshotForAI({ - timeout: Math.max( - 500, - Math.min(60_000, Math.floor(opts.timeoutMs ?? 5000)), - ), + timeout: Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs ?? 5000))), track: "response", }); let snapshot = String(result?.full ?? ""); @@ -78,10 +69,7 @@ export async function snapshotRoleViaPlaywright(opts: { : page.locator(":root"); const ariaSnapshot = await locator.ariaSnapshot(); - const built = buildRoleSnapshotFromAriaSnapshot( - String(ariaSnapshot ?? ""), - opts.options, - ); + const built = buildRoleSnapshotFromAriaSnapshot(String(ariaSnapshot ?? ""), opts.options); state.roleRefs = built.refs; state.roleRefsFrameSelector = frameSelector || undefined; return { diff --git a/src/browser/pw-tools-core.state.ts b/src/browser/pw-tools-core.state.ts index 9c545c7100b..1781df0f6b6 100644 --- a/src/browser/pw-tools-core.state.ts +++ b/src/browser/pw-tools-core.state.ts @@ -3,10 +3,7 @@ import { devices as playwrightDevices } from "playwright-core"; import { ensurePageState, getPageForTargetId } from "./pw-session.js"; -async function withCdpSession( - page: Page, - fn: (session: CDPSession) => Promise, -): Promise { +async function withCdpSession(page: Page, fn: (session: CDPSession) => Promise): Promise { const session = await page.context().newCDPSession(page); try { return await fn(session); @@ -116,9 +113,7 @@ export async function setLocaleViaPlaywright(opts: { try { await session.send("Emulation.setLocaleOverride", { locale }); } catch (err) { - if ( - String(err).includes("Another locale override is already in effect") - ) { + if (String(err).includes("Another locale override is already in effect")) { return; } throw err; @@ -141,8 +136,7 @@ export async function setTimezoneViaPlaywright(opts: { } catch (err) { const msg = String(err); if (msg.includes("Timezone override is already in effect")) return; - if (msg.includes("Invalid timezone")) - throw new Error(`Invalid timezone ID: ${timezoneId}`); + if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`); throw err; } }); diff --git a/src/browser/pw-tools-core.storage.ts b/src/browser/pw-tools-core.storage.ts index df138406b05..caf2a0d2c74 100644 --- a/src/browser/pw-tools-core.storage.ts +++ b/src/browser/pw-tools-core.storage.ts @@ -66,8 +66,7 @@ export async function storageGetViaPlaywright(opts: { const key = typeof opts.key === "string" ? opts.key : undefined; const values = await page.evaluate( ({ kind: kind2, key: key2 }) => { - const store = - kind2 === "session" ? window.sessionStorage : window.localStorage; + const store = kind2 === "session" ? window.sessionStorage : window.localStorage; if (key2) { const value = store.getItem(key2); return value === null ? {} : { [key2]: value }; @@ -99,8 +98,7 @@ export async function storageSetViaPlaywright(opts: { if (!key) throw new Error("key is required"); await page.evaluate( ({ kind, key: k, value }) => { - const store = - kind === "session" ? window.sessionStorage : window.localStorage; + const store = kind === "session" ? window.sessionStorage : window.localStorage; store.setItem(k, value); }, { kind: opts.kind, key, value: String(opts.value ?? "") }, @@ -116,8 +114,7 @@ export async function storageClearViaPlaywright(opts: { ensurePageState(page); await page.evaluate( ({ kind }) => { - const store = - kind === "session" ? window.sessionStorage : window.localStorage; + const store = kind === "session" ? window.sessionStorage : window.localStorage; store.clear(); }, { kind: opts.kind }, diff --git a/src/browser/pw-tools-core.trace.ts b/src/browser/pw-tools-core.trace.ts index 5475f9b57f6..0efa988cac9 100644 --- a/src/browser/pw-tools-core.trace.ts +++ b/src/browser/pw-tools-core.trace.ts @@ -11,9 +11,7 @@ export async function traceStartViaPlaywright(opts: { const context = page.context(); const ctxState = ensureContextState(context); if (ctxState.traceActive) { - throw new Error( - "Trace already running. Stop the current trace before starting a new one.", - ); + throw new Error("Trace already running. Stop the current trace before starting a new one."); } await context.tracing.start({ screenshots: opts.screenshots ?? true, diff --git a/src/browser/routes/agent.act.shared.ts b/src/browser/routes/agent.act.shared.ts index f093d365c16..977da9c3c89 100644 --- a/src/browser/routes/agent.act.shared.ts +++ b/src/browser/routes/agent.act.shared.ts @@ -21,12 +21,7 @@ export function isActKind(value: unknown): value is ActKind { } export type ClickButton = "left" | "right" | "middle"; -export type ClickModifier = - | "Alt" - | "Control" - | "ControlOrMeta" - | "Meta" - | "Shift"; +export type ClickModifier = "Alt" | "Control" | "ControlOrMeta" | "Meta" | "Shift"; const ALLOWED_CLICK_MODIFIERS = new Set([ "Alt", @@ -45,9 +40,7 @@ export function parseClickModifiers(raw: string[]): { modifiers?: ClickModifier[]; error?: string; } { - const invalid = raw.filter( - (m) => !ALLOWED_CLICK_MODIFIERS.has(m as ClickModifier), - ); + const invalid = raw.filter((m) => !ALLOWED_CLICK_MODIFIERS.has(m as ClickModifier)); if (invalid.length) { return { error: "modifiers must be Alt|Control|ControlOrMeta|Meta|Shift" }; } diff --git a/src/browser/routes/agent.act.ts b/src/browser/routes/agent.act.ts index a58931afbe5..ea681bd258d 100644 --- a/src/browser/routes/agent.act.ts +++ b/src/browser/routes/agent.act.ts @@ -15,18 +15,9 @@ import { resolveProfileContext, SELECTOR_UNSUPPORTED_MESSAGE, } from "./agent.shared.js"; -import { - jsonError, - toBoolean, - toNumber, - toStringArray, - toStringOrEmpty, -} from "./utils.js"; +import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "./utils.js"; -export function registerBrowserAgentActRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserAgentActRoutes(app: express.Express, ctx: BrowserRouteContext) { app.post("/act", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; @@ -55,8 +46,7 @@ export function registerBrowserAgentActRoutes( const timeoutMs = toNumber(body.timeoutMs); const buttonRaw = toStringOrEmpty(body.button) || ""; const button = buttonRaw ? parseClickButton(buttonRaw) : undefined; - if (buttonRaw && !button) - return jsonError(res, 400, "button must be left|right|middle"); + if (buttonRaw && !button) return jsonError(res, 400, "button must be left|right|middle"); const modifiersRaw = toStringArray(body.modifiers) ?? []; const parsedModifiers = parseClickModifiers(modifiersRaw); @@ -79,8 +69,7 @@ export function registerBrowserAgentActRoutes( case "type": { const ref = toStringOrEmpty(body.ref); if (!ref) return jsonError(res, 400, "ref is required"); - if (typeof body.text !== "string") - return jsonError(res, 400, "text is required"); + if (typeof body.text !== "string") return jsonError(res, 400, "text is required"); const text = body.text; const submit = toBoolean(body.submit) ?? false; const slowly = toBoolean(body.slowly) ?? false; @@ -125,9 +114,7 @@ export function registerBrowserAgentActRoutes( const ref = toStringOrEmpty(body.ref); if (!ref) return jsonError(res, 400, "ref is required"); const timeoutMs = toNumber(body.timeoutMs); - const scrollRequest: Parameters< - typeof pw.scrollIntoViewViaPlaywright - >[0] = { + const scrollRequest: Parameters[0] = { cdpUrl, targetId: tab.targetId, ref, @@ -139,8 +126,7 @@ export function registerBrowserAgentActRoutes( case "drag": { const startRef = toStringOrEmpty(body.startRef); const endRef = toStringOrEmpty(body.endRef); - if (!startRef || !endRef) - return jsonError(res, 400, "startRef and endRef are required"); + if (!startRef || !endRef) return jsonError(res, 400, "startRef and endRef are required"); const timeoutMs = toNumber(body.timeoutMs); await pw.dragViaPlaywright({ cdpUrl, @@ -154,8 +140,7 @@ export function registerBrowserAgentActRoutes( case "select": { const ref = toStringOrEmpty(body.ref); const values = toStringArray(body.values); - if (!ref || !values?.length) - return jsonError(res, 400, "ref and values are required"); + if (!ref || !values?.length) return jsonError(res, 400, "ref and values are required"); const timeoutMs = toNumber(body.timeoutMs); await pw.selectOptionViaPlaywright({ cdpUrl, @@ -199,8 +184,7 @@ export function registerBrowserAgentActRoutes( case "resize": { const width = toNumber(body.width); const height = toNumber(body.height); - if (!width || !height) - return jsonError(res, 400, "width and height are required"); + if (!width || !height) return jsonError(res, 400, "width and height are required"); await pw.resizeViewportViaPlaywright({ cdpUrl, targetId: tab.targetId, @@ -300,11 +284,7 @@ export function registerBrowserAgentActRoutes( if (!pw) return; if (inputRef || element) { if (ref) { - return jsonError( - res, - 400, - "ref cannot be combined with inputRef/element", - ); + return jsonError(res, 400, "ref cannot be combined with inputRef/element"); } await pw.setInputFilesViaPlaywright({ cdpUrl: profileCtx.profile.cdpUrl, diff --git a/src/browser/routes/agent.debug.ts b/src/browser/routes/agent.debug.ts index 2eb36f1e275..c71bd7b479e 100644 --- a/src/browser/routes/agent.debug.ts +++ b/src/browser/routes/agent.debug.ts @@ -5,23 +5,14 @@ import path from "node:path"; import type express from "express"; import type { BrowserRouteContext } from "../server-context.js"; -import { - handleRouteError, - readBody, - requirePwAi, - resolveProfileContext, -} from "./agent.shared.js"; +import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; import { toBoolean, toStringOrEmpty } from "./utils.js"; -export function registerBrowserAgentDebugRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserAgentDebugRoutes(app: express.Express, ctx: BrowserRouteContext) { app.get("/console", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const level = typeof req.query.level === "string" ? req.query.level : ""; try { @@ -42,8 +33,7 @@ export function registerBrowserAgentDebugRoutes( app.get("/errors", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const clear = toBoolean(req.query.clear) ?? false; try { @@ -64,8 +54,7 @@ export function registerBrowserAgentDebugRoutes( app.get("/requests", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const filter = typeof req.query.filter === "string" ? req.query.filter : ""; const clear = toBoolean(req.query.clear) ?? false; diff --git a/src/browser/routes/agent.shared.ts b/src/browser/routes/agent.shared.ts index ea625301300..2b778460d23 100644 --- a/src/browser/routes/agent.shared.ts +++ b/src/browser/routes/agent.shared.ts @@ -19,11 +19,7 @@ export function readBody(req: express.Request): Record { return body; } -export function handleRouteError( - ctx: BrowserRouteContext, - res: express.Response, - err: unknown, -) { +export function handleRouteError(ctx: BrowserRouteContext, res: express.Response, err: unknown) { const mapped = ctx.mapTabError(err); if (mapped) return jsonError(res, mapped.status, mapped.message); jsonError(res, 500, String(err)); diff --git a/src/browser/routes/agent.snapshot.ts b/src/browser/routes/agent.snapshot.ts index 7ae3f1b71da..1a13db0175b 100644 --- a/src/browser/routes/agent.snapshot.ts +++ b/src/browser/routes/agent.snapshot.ts @@ -20,10 +20,7 @@ import { } from "./agent.shared.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; -export function registerBrowserAgentSnapshotRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserAgentSnapshotRoutes(app: express.Express, ctx: BrowserRouteContext) { app.post("/navigate", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; @@ -88,11 +85,7 @@ export function registerBrowserAgentSnapshotRoutes( const type = body.type === "jpeg" ? "jpeg" : "png"; if (fullPage && (ref || element)) { - return jsonError( - res, - 400, - "fullPage is not supported for element screenshots", - ); + return jsonError(res, 400, "fullPage is not supported for element screenshots"); } try { @@ -144,8 +137,7 @@ export function registerBrowserAgentSnapshotRoutes( app.get("/snapshot", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const format = req.query.format === "aria" ? "aria" @@ -154,26 +146,17 @@ export function registerBrowserAgentSnapshotRoutes( : (await getPwAiModule()) ? "ai" : "aria"; - const limitRaw = - typeof req.query.limit === "string" ? Number(req.query.limit) : undefined; + const limitRaw = typeof req.query.limit === "string" ? Number(req.query.limit) : undefined; const hasMaxChars = Object.hasOwn(req.query, "maxChars"); const maxCharsRaw = - typeof req.query.maxChars === "string" - ? Number(req.query.maxChars) - : undefined; + typeof req.query.maxChars === "string" ? Number(req.query.maxChars) : undefined; const limit = Number.isFinite(limitRaw) ? limitRaw : undefined; const maxChars = - typeof maxCharsRaw === "number" && - Number.isFinite(maxCharsRaw) && - maxCharsRaw > 0 + typeof maxCharsRaw === "number" && Number.isFinite(maxCharsRaw) && maxCharsRaw > 0 ? Math.floor(maxCharsRaw) : undefined; const resolvedMaxChars = - format === "ai" - ? hasMaxChars - ? maxChars - : DEFAULT_AI_SNAPSHOT_MAX_CHARS - : undefined; + format === "ai" ? (hasMaxChars ? maxChars : DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined; const interactive = toBoolean(req.query.interactive); const compact = toBoolean(req.query.compact); const depth = toNumber(req.query.depth); @@ -208,9 +191,7 @@ export function registerBrowserAgentSnapshotRoutes( .snapshotAiViaPlaywright({ cdpUrl: profileCtx.profile.cdpUrl, targetId: tab.targetId, - ...(typeof resolvedMaxChars === "number" - ? { maxChars: resolvedMaxChars } - : {}), + ...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}), }) .catch(async (err) => { // Public-API fallback when Playwright's private _snapshotForAI is missing. diff --git a/src/browser/routes/agent.storage.ts b/src/browser/routes/agent.storage.ts index 8b3a5f039ed..1f5c137b69d 100644 --- a/src/browser/routes/agent.storage.ts +++ b/src/browser/routes/agent.storage.ts @@ -1,23 +1,14 @@ import type express from "express"; import type { BrowserRouteContext } from "../server-context.js"; -import { - handleRouteError, - readBody, - requirePwAi, - resolveProfileContext, -} from "./agent.shared.js"; +import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; -export function registerBrowserAgentStorageRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserAgentStorageRoutes(app: express.Express, ctx: BrowserRouteContext) { app.get("/cookies", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; try { const tab = await profileCtx.ensureTabAvailable(targetId || undefined); const pw = await requirePwAi(res, "cookies"); @@ -38,9 +29,7 @@ export function registerBrowserAgentStorageRoutes( const body = readBody(req); const targetId = toStringOrEmpty(body.targetId) || undefined; const cookie = - body.cookie && - typeof body.cookie === "object" && - !Array.isArray(body.cookie) + body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie) ? (body.cookie as Record) : null; if (!cookie) return jsonError(res, 400, "cookie is required"); @@ -61,9 +50,7 @@ export function registerBrowserAgentStorageRoutes( httpOnly: toBoolean(cookie.httpOnly) ?? undefined, secure: toBoolean(cookie.secure) ?? undefined, sameSite: - cookie.sameSite === "Lax" || - cookie.sameSite === "None" || - cookie.sameSite === "Strict" + cookie.sameSite === "Lax" || cookie.sameSite === "None" || cookie.sameSite === "Strict" ? (cookie.sameSite as "Lax" | "None" | "Strict") : undefined, }, @@ -99,8 +86,7 @@ export function registerBrowserAgentStorageRoutes( const kind = toStringOrEmpty(req.params.kind); if (kind !== "local" && kind !== "session") return jsonError(res, 400, "kind must be local|session"); - const targetId = - typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; + const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : ""; const key = typeof req.query.key === "string" ? req.query.key : ""; try { const tab = await profileCtx.ensureTabAvailable(targetId || undefined); @@ -175,8 +161,7 @@ export function registerBrowserAgentStorageRoutes( const body = readBody(req); const targetId = toStringOrEmpty(body.targetId) || undefined; const offline = toBoolean(body.offline); - if (offline === undefined) - return jsonError(res, 400, "offline is required"); + if (offline === undefined) return jsonError(res, 400, "offline is required"); try { const tab = await profileCtx.ensureTabAvailable(targetId); const pw = await requirePwAi(res, "offline"); @@ -198,9 +183,7 @@ export function registerBrowserAgentStorageRoutes( const body = readBody(req); const targetId = toStringOrEmpty(body.targetId) || undefined; const headers = - body.headers && - typeof body.headers === "object" && - !Array.isArray(body.headers) + body.headers && typeof body.headers === "object" && !Array.isArray(body.headers) ? (body.headers as Record) : null; if (!headers) return jsonError(res, 400, "headers is required"); @@ -230,8 +213,7 @@ export function registerBrowserAgentStorageRoutes( const targetId = toStringOrEmpty(body.targetId) || undefined; const clear = toBoolean(body.clear) ?? false; const username = toStringOrEmpty(body.username) || undefined; - const password = - typeof body.password === "string" ? body.password : undefined; + const password = typeof body.password === "string" ? body.password : undefined; try { const tab = await profileCtx.ensureTabAvailable(targetId); const pw = await requirePwAi(res, "http credentials"); @@ -285,19 +267,13 @@ export function registerBrowserAgentStorageRoutes( const targetId = toStringOrEmpty(body.targetId) || undefined; const schemeRaw = toStringOrEmpty(body.colorScheme); const colorScheme = - schemeRaw === "dark" || - schemeRaw === "light" || - schemeRaw === "no-preference" + schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference" ? (schemeRaw as "dark" | "light" | "no-preference") : schemeRaw === "none" ? null : undefined; if (colorScheme === undefined) - return jsonError( - res, - 400, - "colorScheme must be dark|light|no-preference|none", - ); + return jsonError(res, 400, "colorScheme must be dark|light|no-preference|none"); try { const tab = await profileCtx.ensureTabAvailable(targetId); const pw = await requirePwAi(res, "media emulation"); diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts index 8ee390cbe88..b2cf4950ce6 100644 --- a/src/browser/routes/agent.ts +++ b/src/browser/routes/agent.ts @@ -6,10 +6,7 @@ import { registerBrowserAgentDebugRoutes } from "./agent.debug.js"; import { registerBrowserAgentSnapshotRoutes } from "./agent.snapshot.js"; import { registerBrowserAgentStorageRoutes } from "./agent.storage.js"; -export function registerBrowserAgentRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserAgentRoutes(app: express.Express, ctx: BrowserRouteContext) { registerBrowserAgentSnapshotRoutes(app, ctx); registerBrowserAgentActRoutes(app, ctx); registerBrowserAgentDebugRoutes(app, ctx); diff --git a/src/browser/routes/basic.ts b/src/browser/routes/basic.ts index 4bc5b2d795c..7fa2515a6bf 100644 --- a/src/browser/routes/basic.ts +++ b/src/browser/routes/basic.ts @@ -4,10 +4,7 @@ import { createBrowserProfilesService } from "../profiles-service.js"; import type { BrowserRouteContext } from "../server-context.js"; import { getProfileContext, jsonError, toStringOrEmpty } from "./utils.js"; -export function registerBrowserBasicRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserBasicRoutes(app: express.Express, ctx: BrowserRouteContext) { // List all profiles with their status app.get("/profiles", async (_req, res) => { try { diff --git a/src/browser/routes/index.ts b/src/browser/routes/index.ts index 3a3dc462326..df435ffb85a 100644 --- a/src/browser/routes/index.ts +++ b/src/browser/routes/index.ts @@ -5,10 +5,7 @@ import { registerBrowserAgentRoutes } from "./agent.js"; import { registerBrowserBasicRoutes } from "./basic.js"; import { registerBrowserTabRoutes } from "./tabs.js"; -export function registerBrowserRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserRoutes(app: express.Express, ctx: BrowserRouteContext) { registerBrowserBasicRoutes(app, ctx); registerBrowserTabRoutes(app, ctx); registerBrowserAgentRoutes(app, ctx); diff --git a/src/browser/routes/tabs.ts b/src/browser/routes/tabs.ts index 5093954149b..44596be85b7 100644 --- a/src/browser/routes/tabs.ts +++ b/src/browser/routes/tabs.ts @@ -1,25 +1,15 @@ import type express from "express"; import type { BrowserRouteContext } from "../server-context.js"; -import { - getProfileContext, - jsonError, - toNumber, - toStringOrEmpty, -} from "./utils.js"; +import { getProfileContext, jsonError, toNumber, toStringOrEmpty } from "./utils.js"; -export function registerBrowserTabRoutes( - app: express.Express, - ctx: BrowserRouteContext, -) { +export function registerBrowserTabRoutes(app: express.Express, ctx: BrowserRouteContext) { app.get("/tabs", async (req, res) => { const profileCtx = getProfileContext(req, ctx); - if ("error" in profileCtx) - return jsonError(res, profileCtx.status, profileCtx.error); + if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); try { const reachable = await profileCtx.isReachable(300); - if (!reachable) - return res.json({ running: false, tabs: [] as unknown[] }); + if (!reachable) return res.json({ running: false, tabs: [] as unknown[] }); const tabs = await profileCtx.listTabs(); res.json({ running: true, tabs }); } catch (err) { @@ -29,8 +19,7 @@ export function registerBrowserTabRoutes( app.post("/tabs/open", async (req, res) => { const profileCtx = getProfileContext(req, ctx); - if ("error" in profileCtx) - return jsonError(res, profileCtx.status, profileCtx.error); + if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); const url = toStringOrEmpty((req.body as { url?: unknown })?.url); if (!url) return jsonError(res, 400, "url is required"); try { @@ -44,15 +33,11 @@ export function registerBrowserTabRoutes( app.post("/tabs/focus", async (req, res) => { const profileCtx = getProfileContext(req, ctx); - if ("error" in profileCtx) - return jsonError(res, profileCtx.status, profileCtx.error); - const targetId = toStringOrEmpty( - (req.body as { targetId?: unknown })?.targetId, - ); + if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); + const targetId = toStringOrEmpty((req.body as { targetId?: unknown })?.targetId); if (!targetId) return jsonError(res, 400, "targetId is required"); try { - if (!(await profileCtx.isReachable(300))) - return jsonError(res, 409, "browser not running"); + if (!(await profileCtx.isReachable(300))) return jsonError(res, 409, "browser not running"); await profileCtx.focusTab(targetId); res.json({ ok: true }); } catch (err) { @@ -64,13 +49,11 @@ export function registerBrowserTabRoutes( app.delete("/tabs/:targetId", async (req, res) => { const profileCtx = getProfileContext(req, ctx); - if ("error" in profileCtx) - return jsonError(res, profileCtx.status, profileCtx.error); + if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); const targetId = toStringOrEmpty(req.params.targetId); if (!targetId) return jsonError(res, 400, "targetId is required"); try { - if (!(await profileCtx.isReachable(300))) - return jsonError(res, 409, "browser not running"); + if (!(await profileCtx.isReachable(300))) return jsonError(res, 409, "browser not running"); await profileCtx.closeTab(targetId); res.json({ ok: true }); } catch (err) { @@ -82,8 +65,7 @@ export function registerBrowserTabRoutes( app.post("/tabs/action", async (req, res) => { const profileCtx = getProfileContext(req, ctx); - if ("error" in profileCtx) - return jsonError(res, profileCtx.status, profileCtx.error); + if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); const action = toStringOrEmpty((req.body as { action?: unknown })?.action); const index = toNumber((req.body as { index?: unknown })?.index); try { @@ -109,8 +91,7 @@ export function registerBrowserTabRoutes( } if (action === "select") { - if (typeof index !== "number") - return jsonError(res, 400, "index is required"); + if (typeof index !== "number") return jsonError(res, 400, "index is required"); const tabs = await profileCtx.listTabs(); const target = tabs[index]; if (!target) return jsonError(res, 404, "tab not found"); diff --git a/src/browser/routes/utils.ts b/src/browser/routes/utils.ts index 0eb7ce7256c..4ad737434bc 100644 --- a/src/browser/routes/utils.ts +++ b/src/browser/routes/utils.ts @@ -32,11 +32,7 @@ export function getProfileContext( } } -export function jsonError( - res: express.Response, - status: number, - message: string, -) { +export function jsonError(res: express.Response, status: number, message: string) { res.status(status).json({ error: message }); } diff --git a/src/browser/screenshot.ts b/src/browser/screenshot.ts index 1c1e3039fdb..6e6466d2f1f 100644 --- a/src/browser/screenshot.ts +++ b/src/browser/screenshot.ts @@ -10,24 +10,15 @@ export async function normalizeBrowserScreenshot( maxBytes?: number; }, ): Promise<{ buffer: Buffer; contentType?: "image/jpeg" }> { - const maxSide = Math.max( - 1, - Math.round(opts?.maxSide ?? DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE), - ); - const maxBytes = Math.max( - 1, - Math.round(opts?.maxBytes ?? DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES), - ); + const maxSide = Math.max(1, Math.round(opts?.maxSide ?? DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE)); + const maxBytes = Math.max(1, Math.round(opts?.maxBytes ?? DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES)); const meta = await getImageMetadata(buffer); const width = Number(meta?.width ?? 0); const height = Number(meta?.height ?? 0); const maxDim = Math.max(width, height); - if ( - buffer.byteLength <= maxBytes && - (maxDim === 0 || (width <= maxSide && height <= maxSide)) - ) { + if (buffer.byteLength <= maxBytes && (maxDim === 0 || (width <= maxSide && height <= maxSide))) { return { buffer }; } diff --git a/src/browser/server-context.ts b/src/browser/server-context.ts index c90c6b69f11..4f8bf8060b7 100644 --- a/src/browser/server-context.ts +++ b/src/browser/server-context.ts @@ -33,10 +33,7 @@ export type { /** * Normalize a CDP WebSocket URL to use the correct base URL. */ -function normalizeWsUrl( - raw: string | undefined, - cdpBaseUrl: string, -): string | undefined { +function normalizeWsUrl(raw: string | undefined, cdpBaseUrl: string): string | undefined { if (!raw) return undefined; try { return normalizeCdpWsUrl(raw, cdpBaseUrl); @@ -45,11 +42,7 @@ function normalizeWsUrl( } } -async function fetchJson( - url: string, - timeoutMs = 1500, - init?: RequestInit, -): Promise { +async function fetchJson(url: string, timeoutMs = 1500, init?: RequestInit): Promise { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { @@ -61,11 +54,7 @@ async function fetchJson( } } -async function fetchOk( - url: string, - timeoutMs = 1500, - init?: RequestInit, -): Promise { +async function fetchOk(url: string, timeoutMs = 1500, init?: RequestInit): Promise { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { @@ -183,9 +172,7 @@ function createProfileContext( return await isChromeReachable(profile.cdpUrl, timeoutMs); }; - const attachRunning = ( - running: NonNullable, - ) => { + const attachRunning = (running: NonNullable) => { setProfileRunning(running); running.proc.on("exit", () => { // Guard against server teardown (e.g., SIGUSR1 restart) @@ -204,10 +191,7 @@ function createProfileContext( const httpReachable = await isHttpReachable(); if (!httpReachable) { - if ( - (current.resolved.attachOnly || remoteCdp) && - opts.onEnsureAttachTarget - ) { + if ((current.resolved.attachOnly || remoteCdp) && opts.onEnsureAttachTarget) { await opts.onEnsureAttachTarget(profile); if (await isHttpReachable(1200)) return; } @@ -374,9 +358,7 @@ function createProfileContext( }; } -export function createBrowserRouteContext( - opts: ContextOptions, -): BrowserRouteContext { +export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteContext { const state = () => { const current = opts.getState(); if (!current) throw new Error("Browser server not started"); @@ -389,9 +371,7 @@ export function createBrowserRouteContext( const profile = resolveProfile(current.resolved, name); if (!profile) { const available = Object.keys(current.resolved.profiles).join(", "); - throw new Error( - `Profile "${name}" not found. Available profiles: ${available || "(none)"}`, - ); + throw new Error(`Profile "${name}" not found. Available profiles: ${available || "(none)"}`); } return createProfileContext(opts, profile); }; @@ -470,10 +450,8 @@ export function createBrowserRouteContext( listProfiles, // Legacy methods delegate to default profile ensureBrowserAvailable: () => getDefaultContext().ensureBrowserAvailable(), - ensureTabAvailable: (targetId) => - getDefaultContext().ensureTabAvailable(targetId), - isHttpReachable: (timeoutMs) => - getDefaultContext().isHttpReachable(timeoutMs), + ensureTabAvailable: (targetId) => getDefaultContext().ensureTabAvailable(targetId), + isHttpReachable: (timeoutMs) => getDefaultContext().isHttpReachable(timeoutMs), isReachable: (timeoutMs) => getDefaultContext().isReachable(timeoutMs), listTabs: () => getDefaultContext().listTabs(), openTab: (url) => getDefaultContext().openTab(url), diff --git a/src/browser/server-context.types.ts b/src/browser/server-context.types.ts index cbe669a5ea5..555427afe7a 100644 --- a/src/browser/server-context.types.ts +++ b/src/browser/server-context.types.ts @@ -2,10 +2,7 @@ import type { Server } from "node:http"; import type { RunningChrome } from "./chrome.js"; import type { BrowserTab } from "./client.js"; -import type { - ResolvedBrowserConfig, - ResolvedBrowserProfile, -} from "./config.js"; +import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js"; export type { BrowserTab }; diff --git a/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/src/browser/server.agent-contract-form-layout-act-commands.test.ts index 34ffbf542b9..be289cd61fc 100644 --- a/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -106,20 +106,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; @@ -390,9 +388,10 @@ describe("browser control server", () => { }); expect(responseBody).toMatchObject({ ok: true }); - const consoleRes = (await realFetch(`${base}/console?level=error`).then( - (r) => r.json(), - )) as { ok: boolean; messages?: unknown[] }; + const consoleRes = (await realFetch(`${base}/console?level=error`).then((r) => r.json())) as { + ok: boolean; + messages?: unknown[]; + }; expect(consoleRes.ok).toBe(true); expect(Array.isArray(consoleRes.messages)).toBe(true); diff --git a/src/browser/server.agent-contract-snapshot-endpoints.test.ts b/src/browser/server.agent-contract-snapshot-endpoints.test.ts index 9b273a149d6..622b6e06a3f 100644 --- a/src/browser/server.agent-contract-snapshot-endpoints.test.ts +++ b/src/browser/server.agent-contract-snapshot-endpoints.test.ts @@ -107,20 +107,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; @@ -269,9 +267,9 @@ describe("browser control server", () => { it("agent contract: snapshot endpoints", async () => { const base = await startServerAndBase(); - const snapAria = (await realFetch( - `${base}/snapshot?format=aria&limit=1`, - ).then((r) => r.json())) as { ok: boolean; format?: string }; + const snapAria = (await realFetch(`${base}/snapshot?format=aria&limit=1`).then((r) => + r.json(), + )) as { ok: boolean; format?: string }; expect(snapAria.ok).toBe(true); expect(snapAria.format).toBe("aria"); expect(cdpMocks.snapshotAria).toHaveBeenCalledWith({ @@ -279,9 +277,10 @@ describe("browser control server", () => { limit: 1, }); - const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) => - r.json(), - )) as { ok: boolean; format?: string }; + const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) => r.json())) as { + ok: boolean; + format?: string; + }; expect(snapAi.ok).toBe(true); expect(snapAi.format).toBe("ai"); expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({ diff --git a/src/browser/server.covers-additional-endpoint-branches.test.ts b/src/browser/server.covers-additional-endpoint-branches.test.ts index 7aa0c5a92ed..d7771e5e0c8 100644 --- a/src/browser/server.covers-additional-endpoint-branches.test.ts +++ b/src/browser/server.covers-additional-endpoint-branches.test.ts @@ -106,20 +106,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; @@ -253,9 +251,10 @@ describe("browser control server", () => { await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; - const tabsWhenStopped = (await realFetch(`${base}/tabs`).then((r) => - r.json(), - )) as { running: boolean; tabs: unknown[] }; + const tabsWhenStopped = (await realFetch(`${base}/tabs`).then((r) => r.json())) as { + running: boolean; + tabs: unknown[]; + }; expect(tabsWhenStopped.running).toBe(false); expect(Array.isArray(tabsWhenStopped.tabs)).toBe(true); @@ -280,9 +279,7 @@ describe("browser control server", () => { }); expect(delAmbiguous.status).toBe(409); - const snapAmbiguous = await realFetch( - `${base}/snapshot?format=aria&targetId=abc`, - ); + const snapAmbiguous = await realFetch(`${base}/snapshot?format=aria&targetId=abc`); expect(snapAmbiguous.status).toBe(409); }); }); @@ -357,9 +354,10 @@ describe("backward compatibility (profile parameter)", () => { await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; - const result = (await realFetch(`${base}/start`, { method: "POST" }).then( - (r) => r.json(), - )) as { ok: boolean; profile?: string }; + const result = (await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json())) as { + ok: boolean; + profile?: string; + }; expect(result.ok).toBe(true); expect(result.profile).toBe("clawd"); }); @@ -371,9 +369,10 @@ describe("backward compatibility (profile parameter)", () => { await realFetch(`${base}/start`, { method: "POST" }); - const result = (await realFetch(`${base}/stop`, { method: "POST" }).then( - (r) => r.json(), - )) as { ok: boolean; profile?: string }; + const result = (await realFetch(`${base}/stop`, { method: "POST" }).then((r) => r.json())) as { + ok: boolean; + profile?: string; + }; expect(result.ok).toBe(true); expect(result.profile).toBe("clawd"); }); @@ -413,9 +412,9 @@ describe("backward compatibility (profile parameter)", () => { await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; - const result = (await realFetch(`${base}/profiles`).then((r) => - r.json(), - )) as { profiles: Array<{ name: string }> }; + const result = (await realFetch(`${base}/profiles`).then((r) => r.json())) as { + profiles: Array<{ name: string }>; + }; expect(Array.isArray(result.profiles)).toBe(true); // Should at least have the default clawd profile expect(result.profiles.some((p) => p.name === "clawd")).toBe(true); @@ -428,9 +427,10 @@ describe("backward compatibility (profile parameter)", () => { await realFetch(`${base}/start`, { method: "POST" }); - const result = (await realFetch(`${base}/tabs?profile=clawd`).then((r) => - r.json(), - )) as { running: boolean; tabs: unknown[] }; + const result = (await realFetch(`${base}/tabs?profile=clawd`).then((r) => r.json())) as { + running: boolean; + tabs: unknown[]; + }; expect(result.running).toBe(true); expect(Array.isArray(result.tabs)).toBe(true); }); diff --git a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts index 040f69e943a..be1800e7f36 100644 --- a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts +++ b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts @@ -106,20 +106,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; diff --git a/src/browser/server.serves-status-starts-browser-requested.test.ts b/src/browser/server.serves-status-starts-browser-requested.test.ts index 6c3706e86fb..c99698ab7f6 100644 --- a/src/browser/server.serves-status-starts-browser-requested.test.ts +++ b/src/browser/server.serves-status-starts-browser-requested.test.ts @@ -106,20 +106,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; diff --git a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts b/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts index b845f9c24d0..a5050298a20 100644 --- a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts +++ b/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts @@ -106,20 +106,18 @@ const launchCalls = vi.hoisted(() => [] as Array<{ port: number }>); vi.mock("./chrome.js", () => ({ isChromeCdpReady: vi.fn(async () => reachable), isChromeReachable: vi.fn(async () => reachable), - launchClawdChrome: vi.fn( - async (_resolved: unknown, profile: { cdpPort: number }) => { - launchCalls.push({ port: profile.cdpPort }); - reachable = true; - return { - pid: 123, - exe: { kind: "chrome", path: "/fake/chrome" }, - userDataDir: "/tmp/clawd", - cdpPort: profile.cdpPort, - startedAt: Date.now(), - proc, - }; - }, - ), + launchClawdChrome: vi.fn(async (_resolved: unknown, profile: { cdpPort: number }) => { + launchCalls.push({ port: profile.cdpPort }); + reachable = true; + return { + pid: 123, + exe: { kind: "chrome", path: "/fake/chrome" }, + userDataDir: "/tmp/clawd", + cdpPort: profile.cdpPort, + startedAt: Date.now(), + proc, + }; + }), resolveClawdUserDataDir: vi.fn(() => "/tmp/clawd"), stopClawdChrome: vi.fn(async () => { reachable = false; @@ -254,9 +252,9 @@ describe("browser control server", () => { const base = `http://127.0.0.1:${testPort}`; await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json()); - const snapAi = (await realFetch( - `${base}/snapshot?format=ai&maxChars=0`, - ).then((r) => r.json())) as { ok: boolean; format?: string }; + const snapAi = (await realFetch(`${base}/snapshot?format=ai&maxChars=0`).then((r) => + r.json(), + )) as { ok: boolean; format?: string }; expect(snapAi.ok).toBe(true); expect(snapAi.format).toBe("ai"); @@ -343,9 +341,10 @@ describe("browser control server", () => { }); expect(dialogMissingAccept.status).toBe(400); - const snapDefault = (await realFetch(`${base}/snapshot?format=wat`).then( - (r) => r.json(), - )) as { ok: boolean; format?: string }; + const snapDefault = (await realFetch(`${base}/snapshot?format=wat`).then((r) => r.json())) as { + ok: boolean; + format?: string; + }; expect(snapDefault.ok).toBe(true); expect(snapDefault.format).toBe("ai"); @@ -412,9 +411,9 @@ describe("browser control server", () => { }).then((r) => r.json())) as { ok?: boolean; error?: string }; expect(started.error).toBeUndefined(); expect(started.ok).toBe(true); - const status = (await realFetch(`${bridge.baseUrl}/`).then((r) => - r.json(), - )) as { running?: boolean }; + const status = (await realFetch(`${bridge.baseUrl}/`).then((r) => r.json())) as { + running?: boolean; + }; expect(status.running).toBe(true); expect(ensured).toHaveBeenCalledTimes(1); diff --git a/src/browser/server.ts b/src/browser/server.ts index f382b22472a..d2a96387ff6 100644 --- a/src/browser/server.ts +++ b/src/browser/server.ts @@ -3,15 +3,9 @@ import express from "express"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging.js"; -import { - resolveBrowserConfig, - shouldStartLocalBrowserServer, -} from "./config.js"; +import { resolveBrowserConfig, shouldStartLocalBrowserServer } from "./config.js"; import { registerBrowserRoutes } from "./routes/index.js"; -import { - type BrowserServerState, - createBrowserRouteContext, -} from "./server-context.js"; +import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js"; let state: BrowserServerState | null = null; const log = createSubsystemLogger("browser"); @@ -44,9 +38,7 @@ export async function startBrowserControlServerFromConfig(): Promise resolve(s)); s.once("error", reject); }).catch((err) => { - logServer.error( - `clawd browser server failed to bind 127.0.0.1:${port}: ${String(err)}`, - ); + logServer.error(`clawd browser server failed to bind 127.0.0.1:${port}: ${String(err)}`); return null; }); diff --git a/src/browser/target-id.test.ts b/src/browser/target-id.test.ts index d000365329f..120e782777a 100644 --- a/src/browser/target-id.test.ts +++ b/src/browser/target-id.test.ts @@ -4,10 +4,7 @@ import { resolveTargetIdFromTabs } from "./target-id.js"; describe("browser target id resolution", () => { it("resolves exact ids", () => { - const res = resolveTargetIdFromTabs("FULL", [ - { targetId: "AAA" }, - { targetId: "FULL" }, - ]); + const res = resolveTargetIdFromTabs("FULL", [{ targetId: "AAA" }, { targetId: "FULL" }]); expect(res).toEqual({ ok: true, targetId: "FULL" }); }); diff --git a/src/browser/target-id.ts b/src/browser/target-id.ts index 4e53ea30094..4721aaf37b1 100644 --- a/src/browser/target-id.ts +++ b/src/browser/target-id.ts @@ -13,9 +13,7 @@ export function resolveTargetIdFromTabs( if (exact) return { ok: true, targetId: exact.targetId }; const lower = needle.toLowerCase(); - const matches = tabs - .map((t) => t.targetId) - .filter((id) => id.toLowerCase().startsWith(lower)); + const matches = tabs.map((t) => t.targetId).filter((id) => id.toLowerCase().startsWith(lower)); const only = matches.length === 1 ? matches[0] : undefined; if (only) return { ok: true, targetId: only }; diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index ff871aec287..876436c5728 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -78,9 +78,7 @@ async function resolveA2uiFilePath(rootReal: string, urlPath: string) { // ignore } - const rootPrefix = rootReal.endsWith(path.sep) - ? rootReal - : `${rootReal}${path.sep}`; + const rootPrefix = rootReal.endsWith(path.sep) ? rootReal : `${rootReal}${path.sep}`; try { const lstat = await fs.lstat(candidate); if (lstat.isSymbolicLink()) return null; diff --git a/src/canvas-host/a2ui/a2ui.bundle.js b/src/canvas-host/a2ui/a2ui.bundle.js index 3b20b5cba14..19bf137e3be 100644 --- a/src/canvas-host/a2ui/a2ui.bundle.js +++ b/src/canvas-host/a2ui/a2ui.bundle.js @@ -1,978 +1,1396 @@ var __defProp$1 = Object.defineProperty; var __exportAll = (all, symbols) => { - let target = {}; - for (var name in all) { - __defProp$1(target, name, { - get: all[name], - enumerable: true - }); - } - if (symbols) { - __defProp$1(target, Symbol.toStringTag, { value: "Module" }); - } - return target; + let target = {}; + for (var name in all) { + __defProp$1(target, name, { + get: all[name], + enumerable: true, + }); + } + if (symbols) { + __defProp$1(target, Symbol.toStringTag, { value: "Module" }); + } + return target; }; /** -* @license -* Copyright 2019 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$6 = globalThis, e$13 = t$6.ShadowRoot && (void 0 === t$6.ShadyCSS || t$6.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$8 = Symbol(), o$14 = new WeakMap(); + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const t$6 = globalThis, + e$13 = + t$6.ShadowRoot && + (void 0 === t$6.ShadyCSS || t$6.ShadyCSS.nativeShadow) && + "adoptedStyleSheets" in Document.prototype && + "replace" in CSSStyleSheet.prototype, + s$8 = Symbol(), + o$14 = new WeakMap(); var n$12 = class { - constructor(t$7, e$14, o$15) { - if (this._$cssResult$ = !0, o$15 !== s$8) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); - this.cssText = t$7, this.t = e$14; - } - get styleSheet() { - let t$7 = this.o; - const s$9 = this.t; - if (e$13 && void 0 === t$7) { - const e$14 = void 0 !== s$9 && 1 === s$9.length; - e$14 && (t$7 = o$14.get(s$9)), void 0 === t$7 && ((this.o = t$7 = new CSSStyleSheet()).replaceSync(this.cssText), e$14 && o$14.set(s$9, t$7)); - } - return t$7; - } - toString() { - return this.cssText; - } + constructor(t$7, e$14, o$15) { + if (((this._$cssResult$ = !0), o$15 !== s$8)) + throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + ((this.cssText = t$7), (this.t = e$14)); + } + get styleSheet() { + let t$7 = this.o; + const s$9 = this.t; + if (e$13 && void 0 === t$7) { + const e$14 = void 0 !== s$9 && 1 === s$9.length; + (e$14 && (t$7 = o$14.get(s$9)), + void 0 === t$7 && + ((this.o = t$7 = new CSSStyleSheet()).replaceSync(this.cssText), + e$14 && o$14.set(s$9, t$7))); + } + return t$7; + } + toString() { + return this.cssText; + } }; -const r$11 = (t$7) => new n$12("string" == typeof t$7 ? t$7 : t$7 + "", void 0, s$8), i$9 = (t$7, ...e$14) => { - const o$15 = 1 === t$7.length ? t$7[0] : e$14.reduce((e$15, s$9, o$16) => e$15 + ((t$8) => { - if (!0 === t$8._$cssResult$) return t$8.cssText; - if ("number" == typeof t$8) return t$8; - throw Error("Value passed to 'css' function must be a 'css' function result: " + t$8 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(s$9) + t$7[o$16 + 1], t$7[0]); - return new n$12(o$15, t$7, s$8); -}, S$1 = (s$9, o$15) => { - if (e$13) s$9.adoptedStyleSheets = o$15.map((t$7) => t$7 instanceof CSSStyleSheet ? t$7 : t$7.styleSheet); - else for (const e$14 of o$15) { - const o$16 = document.createElement("style"), n$13 = t$6.litNonce; - void 0 !== n$13 && o$16.setAttribute("nonce", n$13), o$16.textContent = e$14.cssText, s$9.appendChild(o$16); - } -}, c$6 = e$13 ? (t$7) => t$7 : (t$7) => t$7 instanceof CSSStyleSheet ? ((t$8) => { - let e$14 = ""; - for (const s$9 of t$8.cssRules) e$14 += s$9.cssText; - return r$11(e$14); -})(t$7) : t$7; +const r$11 = (t$7) => new n$12("string" == typeof t$7 ? t$7 : t$7 + "", void 0, s$8), + i$9 = (t$7, ...e$14) => { + const o$15 = + 1 === t$7.length + ? t$7[0] + : e$14.reduce( + (e$15, s$9, o$16) => + e$15 + + ((t$8) => { + if (!0 === t$8._$cssResult$) return t$8.cssText; + if ("number" == typeof t$8) return t$8; + throw Error( + "Value passed to 'css' function must be a 'css' function result: " + + t$8 + + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.", + ); + })(s$9) + + t$7[o$16 + 1], + t$7[0], + ); + return new n$12(o$15, t$7, s$8); + }, + S$1 = (s$9, o$15) => { + if (e$13) + s$9.adoptedStyleSheets = o$15.map((t$7) => + t$7 instanceof CSSStyleSheet ? t$7 : t$7.styleSheet, + ); + else + for (const e$14 of o$15) { + const o$16 = document.createElement("style"), + n$13 = t$6.litNonce; + (void 0 !== n$13 && o$16.setAttribute("nonce", n$13), + (o$16.textContent = e$14.cssText), + s$9.appendChild(o$16)); + } + }, + c$6 = e$13 + ? (t$7) => t$7 + : (t$7) => + t$7 instanceof CSSStyleSheet + ? ((t$8) => { + let e$14 = ""; + for (const s$9 of t$8.cssRules) e$14 += s$9.cssText; + return r$11(e$14); + })(t$7) + : t$7; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { is: i$8, defineProperty: e$12, getOwnPropertyDescriptor: h$6, getOwnPropertyNames: r$10, getOwnPropertySymbols: o$13, getPrototypeOf: n$11 } = Object, a$1 = globalThis, c$5 = a$1.trustedTypes, l$4 = c$5 ? c$5.emptyScript : "", p$2 = a$1.reactiveElementPolyfillSupport, d$2 = (t$7, s$9) => t$7, u$3 = { - toAttribute(t$7, s$9) { - switch (s$9) { - case Boolean: - t$7 = t$7 ? l$4 : null; - break; - case Object: - case Array: t$7 = null == t$7 ? t$7 : JSON.stringify(t$7); - } - return t$7; - }, - fromAttribute(t$7, s$9) { - let i$10 = t$7; - switch (s$9) { - case Boolean: - i$10 = null !== t$7; - break; - case Number: - i$10 = null === t$7 ? null : Number(t$7); - break; - case Object: - case Array: try { - i$10 = JSON.parse(t$7); - } catch (t$8) { - i$10 = null; - } - } - return i$10; - } -}, f$3 = (t$7, s$9) => !i$8(t$7, s$9), b$1 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - useDefault: !1, - hasChanged: f$3 -}; -Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= new WeakMap(); + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const { + is: i$8, + defineProperty: e$12, + getOwnPropertyDescriptor: h$6, + getOwnPropertyNames: r$10, + getOwnPropertySymbols: o$13, + getPrototypeOf: n$11, + } = Object, + a$1 = globalThis, + c$5 = a$1.trustedTypes, + l$4 = c$5 ? c$5.emptyScript : "", + p$2 = a$1.reactiveElementPolyfillSupport, + d$2 = (t$7, s$9) => t$7, + u$3 = { + toAttribute(t$7, s$9) { + switch (s$9) { + case Boolean: + t$7 = t$7 ? l$4 : null; + break; + case Object: + case Array: + t$7 = null == t$7 ? t$7 : JSON.stringify(t$7); + } + return t$7; + }, + fromAttribute(t$7, s$9) { + let i$10 = t$7; + switch (s$9) { + case Boolean: + i$10 = null !== t$7; + break; + case Number: + i$10 = null === t$7 ? null : Number(t$7); + break; + case Object: + case Array: + try { + i$10 = JSON.parse(t$7); + } catch (t$8) { + i$10 = null; + } + } + return i$10; + }, + }, + f$3 = (t$7, s$9) => !i$8(t$7, s$9), + b$1 = { + attribute: !0, + type: String, + converter: u$3, + reflect: !1, + useDefault: !1, + hasChanged: f$3, + }; +((Symbol.metadata ??= Symbol("metadata")), (a$1.litPropertyMetadata ??= new WeakMap())); var y$1 = class extends HTMLElement { - static addInitializer(t$7) { - this._$Ei(), (this.l ??= []).push(t$7); - } - static get observedAttributes() { - return this.finalize(), this._$Eh && [...this._$Eh.keys()]; - } - static createProperty(t$7, s$9 = b$1) { - if (s$9.state && (s$9.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t$7) && ((s$9 = Object.create(s$9)).wrapped = !0), this.elementProperties.set(t$7, s$9), !s$9.noAccessor) { - const i$10 = Symbol(), h$7 = this.getPropertyDescriptor(t$7, i$10, s$9); - void 0 !== h$7 && e$12(this.prototype, t$7, h$7); - } - } - static getPropertyDescriptor(t$7, s$9, i$10) { - const { get: e$14, set: r$12 } = h$6(this.prototype, t$7) ?? { - get() { - return this[s$9]; - }, - set(t$8) { - this[s$9] = t$8; - } - }; - return { - get: e$14, - set(s$10) { - const h$7 = e$14?.call(this); - r$12?.call(this, s$10), this.requestUpdate(t$7, h$7, i$10); - }, - configurable: !0, - enumerable: !0 - }; - } - static getPropertyOptions(t$7) { - return this.elementProperties.get(t$7) ?? b$1; - } - static _$Ei() { - if (this.hasOwnProperty(d$2("elementProperties"))) return; - const t$7 = n$11(this); - t$7.finalize(), void 0 !== t$7.l && (this.l = [...t$7.l]), this.elementProperties = new Map(t$7.elementProperties); - } - static finalize() { - if (this.hasOwnProperty(d$2("finalized"))) return; - if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(d$2("properties"))) { - const t$8 = this.properties, s$9 = [...r$10(t$8), ...o$13(t$8)]; - for (const i$10 of s$9) this.createProperty(i$10, t$8[i$10]); - } - const t$7 = this[Symbol.metadata]; - if (null !== t$7) { - const s$9 = litPropertyMetadata.get(t$7); - if (void 0 !== s$9) for (const [t$8, i$10] of s$9) this.elementProperties.set(t$8, i$10); - } - this._$Eh = new Map(); - for (const [t$8, s$9] of this.elementProperties) { - const i$10 = this._$Eu(t$8, s$9); - void 0 !== i$10 && this._$Eh.set(i$10, t$8); - } - this.elementStyles = this.finalizeStyles(this.styles); - } - static finalizeStyles(s$9) { - const i$10 = []; - if (Array.isArray(s$9)) { - const e$14 = new Set(s$9.flat(1 / 0).reverse()); - for (const s$10 of e$14) i$10.unshift(c$6(s$10)); - } else void 0 !== s$9 && i$10.push(c$6(s$9)); - return i$10; - } - static _$Eu(t$7, s$9) { - const i$10 = s$9.attribute; - return !1 === i$10 ? void 0 : "string" == typeof i$10 ? i$10 : "string" == typeof t$7 ? t$7.toLowerCase() : void 0; - } - constructor() { - super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); - } - _$Ev() { - this._$ES = new Promise((t$7) => this.enableUpdating = t$7), this._$AL = new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t$7) => t$7(this)); - } - addController(t$7) { - (this._$EO ??= new Set()).add(t$7), void 0 !== this.renderRoot && this.isConnected && t$7.hostConnected?.(); - } - removeController(t$7) { - this._$EO?.delete(t$7); - } - _$E_() { - const t$7 = new Map(), s$9 = this.constructor.elementProperties; - for (const i$10 of s$9.keys()) this.hasOwnProperty(i$10) && (t$7.set(i$10, this[i$10]), delete this[i$10]); - t$7.size > 0 && (this._$Ep = t$7); - } - createRenderRoot() { - const t$7 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return S$1(t$7, this.constructor.elementStyles), t$7; - } - connectedCallback() { - this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t$7) => t$7.hostConnected?.()); - } - enableUpdating(t$7) {} - disconnectedCallback() { - this._$EO?.forEach((t$7) => t$7.hostDisconnected?.()); - } - attributeChangedCallback(t$7, s$9, i$10) { - this._$AK(t$7, i$10); - } - _$ET(t$7, s$9) { - const i$10 = this.constructor.elementProperties.get(t$7), e$14 = this.constructor._$Eu(t$7, i$10); - if (void 0 !== e$14 && !0 === i$10.reflect) { - const h$7 = (void 0 !== i$10.converter?.toAttribute ? i$10.converter : u$3).toAttribute(s$9, i$10.type); - this._$Em = t$7, null == h$7 ? this.removeAttribute(e$14) : this.setAttribute(e$14, h$7), this._$Em = null; - } - } - _$AK(t$7, s$9) { - const i$10 = this.constructor, e$14 = i$10._$Eh.get(t$7); - if (void 0 !== e$14 && this._$Em !== e$14) { - const t$8 = i$10.getPropertyOptions(e$14), h$7 = "function" == typeof t$8.converter ? { fromAttribute: t$8.converter } : void 0 !== t$8.converter?.fromAttribute ? t$8.converter : u$3; - this._$Em = e$14; - const r$12 = h$7.fromAttribute(s$9, t$8.type); - this[e$14] = r$12 ?? this._$Ej?.get(e$14) ?? r$12, this._$Em = null; - } - } - requestUpdate(t$7, s$9, i$10, e$14 = !1, h$7) { - if (void 0 !== t$7) { - const r$12 = this.constructor; - if (!1 === e$14 && (h$7 = this[t$7]), i$10 ??= r$12.getPropertyOptions(t$7), !((i$10.hasChanged ?? f$3)(h$7, s$9) || i$10.useDefault && i$10.reflect && h$7 === this._$Ej?.get(t$7) && !this.hasAttribute(r$12._$Eu(t$7, i$10)))) return; - this.C(t$7, s$9, i$10); - } - !1 === this.isUpdatePending && (this._$ES = this._$EP()); - } - C(t$7, s$9, { useDefault: i$10, reflect: e$14, wrapped: h$7 }, r$12) { - i$10 && !(this._$Ej ??= new Map()).has(t$7) && (this._$Ej.set(t$7, r$12 ?? s$9 ?? this[t$7]), !0 !== h$7 || void 0 !== r$12) || (this._$AL.has(t$7) || (this.hasUpdated || i$10 || (s$9 = void 0), this._$AL.set(t$7, s$9)), !0 === e$14 && this._$Em !== t$7 && (this._$Eq ??= new Set()).add(t$7)); - } - async _$EP() { - this.isUpdatePending = !0; - try { - await this._$ES; - } catch (t$8) { - Promise.reject(t$8); - } - const t$7 = this.scheduleUpdate(); - return null != t$7 && await t$7, !this.isUpdatePending; - } - scheduleUpdate() { - return this.performUpdate(); - } - performUpdate() { - if (!this.isUpdatePending) return; - if (!this.hasUpdated) { - if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [t$9, s$10] of this._$Ep) this[t$9] = s$10; - this._$Ep = void 0; - } - const t$8 = this.constructor.elementProperties; - if (t$8.size > 0) for (const [s$10, i$10] of t$8) { - const { wrapped: t$9 } = i$10, e$14 = this[s$10]; - !0 !== t$9 || this._$AL.has(s$10) || void 0 === e$14 || this.C(s$10, void 0, i$10, e$14); - } - } - let t$7 = !1; - const s$9 = this._$AL; - try { - t$7 = this.shouldUpdate(s$9), t$7 ? (this.willUpdate(s$9), this._$EO?.forEach((t$8) => t$8.hostUpdate?.()), this.update(s$9)) : this._$EM(); - } catch (s$10) { - throw t$7 = !1, this._$EM(), s$10; - } - t$7 && this._$AE(s$9); - } - willUpdate(t$7) {} - _$AE(t$7) { - this._$EO?.forEach((t$8) => t$8.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t$7)), this.updated(t$7); - } - _$EM() { - this._$AL = new Map(), this.isUpdatePending = !1; - } - get updateComplete() { - return this.getUpdateComplete(); - } - getUpdateComplete() { - return this._$ES; - } - shouldUpdate(t$7) { - return !0; - } - update(t$7) { - this._$Eq &&= this._$Eq.forEach((t$8) => this._$ET(t$8, this[t$8])), this._$EM(); - } - updated(t$7) {} - firstUpdated(t$7) {} + static addInitializer(t$7) { + (this._$Ei(), (this.l ??= []).push(t$7)); + } + static get observedAttributes() { + return (this.finalize(), this._$Eh && [...this._$Eh.keys()]); + } + static createProperty(t$7, s$9 = b$1) { + if ( + (s$9.state && (s$9.attribute = !1), + this._$Ei(), + this.prototype.hasOwnProperty(t$7) && ((s$9 = Object.create(s$9)).wrapped = !0), + this.elementProperties.set(t$7, s$9), + !s$9.noAccessor) + ) { + const i$10 = Symbol(), + h$7 = this.getPropertyDescriptor(t$7, i$10, s$9); + void 0 !== h$7 && e$12(this.prototype, t$7, h$7); + } + } + static getPropertyDescriptor(t$7, s$9, i$10) { + const { get: e$14, set: r$12 } = h$6(this.prototype, t$7) ?? { + get() { + return this[s$9]; + }, + set(t$8) { + this[s$9] = t$8; + }, + }; + return { + get: e$14, + set(s$10) { + const h$7 = e$14?.call(this); + (r$12?.call(this, s$10), this.requestUpdate(t$7, h$7, i$10)); + }, + configurable: !0, + enumerable: !0, + }; + } + static getPropertyOptions(t$7) { + return this.elementProperties.get(t$7) ?? b$1; + } + static _$Ei() { + if (this.hasOwnProperty(d$2("elementProperties"))) return; + const t$7 = n$11(this); + (t$7.finalize(), + void 0 !== t$7.l && (this.l = [...t$7.l]), + (this.elementProperties = new Map(t$7.elementProperties))); + } + static finalize() { + if (this.hasOwnProperty(d$2("finalized"))) return; + if (((this.finalized = !0), this._$Ei(), this.hasOwnProperty(d$2("properties")))) { + const t$8 = this.properties, + s$9 = [...r$10(t$8), ...o$13(t$8)]; + for (const i$10 of s$9) this.createProperty(i$10, t$8[i$10]); + } + const t$7 = this[Symbol.metadata]; + if (null !== t$7) { + const s$9 = litPropertyMetadata.get(t$7); + if (void 0 !== s$9) for (const [t$8, i$10] of s$9) this.elementProperties.set(t$8, i$10); + } + this._$Eh = new Map(); + for (const [t$8, s$9] of this.elementProperties) { + const i$10 = this._$Eu(t$8, s$9); + void 0 !== i$10 && this._$Eh.set(i$10, t$8); + } + this.elementStyles = this.finalizeStyles(this.styles); + } + static finalizeStyles(s$9) { + const i$10 = []; + if (Array.isArray(s$9)) { + const e$14 = new Set(s$9.flat(1 / 0).reverse()); + for (const s$10 of e$14) i$10.unshift(c$6(s$10)); + } else void 0 !== s$9 && i$10.push(c$6(s$9)); + return i$10; + } + static _$Eu(t$7, s$9) { + const i$10 = s$9.attribute; + return !1 === i$10 + ? void 0 + : "string" == typeof i$10 + ? i$10 + : "string" == typeof t$7 + ? t$7.toLowerCase() + : void 0; + } + constructor() { + (super(), + (this._$Ep = void 0), + (this.isUpdatePending = !1), + (this.hasUpdated = !1), + (this._$Em = null), + this._$Ev()); + } + _$Ev() { + ((this._$ES = new Promise((t$7) => (this.enableUpdating = t$7))), + (this._$AL = new Map()), + this._$E_(), + this.requestUpdate(), + this.constructor.l?.forEach((t$7) => t$7(this))); + } + addController(t$7) { + ((this._$EO ??= new Set()).add(t$7), + void 0 !== this.renderRoot && this.isConnected && t$7.hostConnected?.()); + } + removeController(t$7) { + this._$EO?.delete(t$7); + } + _$E_() { + const t$7 = new Map(), + s$9 = this.constructor.elementProperties; + for (const i$10 of s$9.keys()) + this.hasOwnProperty(i$10) && (t$7.set(i$10, this[i$10]), delete this[i$10]); + t$7.size > 0 && (this._$Ep = t$7); + } + createRenderRoot() { + const t$7 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); + return (S$1(t$7, this.constructor.elementStyles), t$7); + } + connectedCallback() { + ((this.renderRoot ??= this.createRenderRoot()), + this.enableUpdating(!0), + this._$EO?.forEach((t$7) => t$7.hostConnected?.())); + } + enableUpdating(t$7) {} + disconnectedCallback() { + this._$EO?.forEach((t$7) => t$7.hostDisconnected?.()); + } + attributeChangedCallback(t$7, s$9, i$10) { + this._$AK(t$7, i$10); + } + _$ET(t$7, s$9) { + const i$10 = this.constructor.elementProperties.get(t$7), + e$14 = this.constructor._$Eu(t$7, i$10); + if (void 0 !== e$14 && !0 === i$10.reflect) { + const h$7 = (void 0 !== i$10.converter?.toAttribute ? i$10.converter : u$3).toAttribute( + s$9, + i$10.type, + ); + ((this._$Em = t$7), + null == h$7 ? this.removeAttribute(e$14) : this.setAttribute(e$14, h$7), + (this._$Em = null)); + } + } + _$AK(t$7, s$9) { + const i$10 = this.constructor, + e$14 = i$10._$Eh.get(t$7); + if (void 0 !== e$14 && this._$Em !== e$14) { + const t$8 = i$10.getPropertyOptions(e$14), + h$7 = + "function" == typeof t$8.converter + ? { fromAttribute: t$8.converter } + : void 0 !== t$8.converter?.fromAttribute + ? t$8.converter + : u$3; + this._$Em = e$14; + const r$12 = h$7.fromAttribute(s$9, t$8.type); + ((this[e$14] = r$12 ?? this._$Ej?.get(e$14) ?? r$12), (this._$Em = null)); + } + } + requestUpdate(t$7, s$9, i$10, e$14 = !1, h$7) { + if (void 0 !== t$7) { + const r$12 = this.constructor; + if ( + (!1 === e$14 && (h$7 = this[t$7]), + (i$10 ??= r$12.getPropertyOptions(t$7)), + !( + (i$10.hasChanged ?? f$3)(h$7, s$9) || + (i$10.useDefault && + i$10.reflect && + h$7 === this._$Ej?.get(t$7) && + !this.hasAttribute(r$12._$Eu(t$7, i$10))) + )) + ) + return; + this.C(t$7, s$9, i$10); + } + !1 === this.isUpdatePending && (this._$ES = this._$EP()); + } + C(t$7, s$9, { useDefault: i$10, reflect: e$14, wrapped: h$7 }, r$12) { + (i$10 && + !(this._$Ej ??= new Map()).has(t$7) && + (this._$Ej.set(t$7, r$12 ?? s$9 ?? this[t$7]), !0 !== h$7 || void 0 !== r$12)) || + (this._$AL.has(t$7) || (this.hasUpdated || i$10 || (s$9 = void 0), this._$AL.set(t$7, s$9)), + !0 === e$14 && this._$Em !== t$7 && (this._$Eq ??= new Set()).add(t$7)); + } + async _$EP() { + this.isUpdatePending = !0; + try { + await this._$ES; + } catch (t$8) { + Promise.reject(t$8); + } + const t$7 = this.scheduleUpdate(); + return (null != t$7 && (await t$7), !this.isUpdatePending); + } + scheduleUpdate() { + return this.performUpdate(); + } + performUpdate() { + if (!this.isUpdatePending) return; + if (!this.hasUpdated) { + if (((this.renderRoot ??= this.createRenderRoot()), this._$Ep)) { + for (const [t$9, s$10] of this._$Ep) this[t$9] = s$10; + this._$Ep = void 0; + } + const t$8 = this.constructor.elementProperties; + if (t$8.size > 0) + for (const [s$10, i$10] of t$8) { + const { wrapped: t$9 } = i$10, + e$14 = this[s$10]; + !0 !== t$9 || this._$AL.has(s$10) || void 0 === e$14 || this.C(s$10, void 0, i$10, e$14); + } + } + let t$7 = !1; + const s$9 = this._$AL; + try { + ((t$7 = this.shouldUpdate(s$9)), + t$7 + ? (this.willUpdate(s$9), + this._$EO?.forEach((t$8) => t$8.hostUpdate?.()), + this.update(s$9)) + : this._$EM()); + } catch (s$10) { + throw ((t$7 = !1), this._$EM(), s$10); + } + t$7 && this._$AE(s$9); + } + willUpdate(t$7) {} + _$AE(t$7) { + (this._$EO?.forEach((t$8) => t$8.hostUpdated?.()), + this.hasUpdated || ((this.hasUpdated = !0), this.firstUpdated(t$7)), + this.updated(t$7)); + } + _$EM() { + ((this._$AL = new Map()), (this.isUpdatePending = !1)); + } + get updateComplete() { + return this.getUpdateComplete(); + } + getUpdateComplete() { + return this._$ES; + } + shouldUpdate(t$7) { + return !0; + } + update(t$7) { + ((this._$Eq &&= this._$Eq.forEach((t$8) => this._$ET(t$8, this[t$8]))), this._$EM()); + } + updated(t$7) {} + firstUpdated(t$7) {} }; -y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$2("elementProperties")] = new Map(), y$1[d$2("finalized")] = new Map(), p$2?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.2"); +((y$1.elementStyles = []), + (y$1.shadowRootOptions = { mode: "open" }), + (y$1[d$2("elementProperties")] = new Map()), + (y$1[d$2("finalized")] = new Map()), + p$2?.({ ReactiveElement: y$1 }), + (a$1.reactiveElementVersions ??= []).push("2.1.2")); /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$5 = globalThis, i$7 = (t$7) => t$7, s$7 = t$5.trustedTypes, e$11 = s$7 ? s$7.createPolicy("lit-html", { createHTML: (t$7) => t$7 }) : void 0, h$5 = "$lit$", o$12 = `lit$${Math.random().toFixed(9).slice(2)}$`, n$10 = "?" + o$12, r$9 = `<${n$10}>`, l$3 = document, c$4 = () => l$3.createComment(""), a = (t$7) => null === t$7 || "object" != typeof t$7 && "function" != typeof t$7, u$2 = Array.isArray, d$1 = (t$7) => u$2(t$7) || "function" == typeof t$7?.[Symbol.iterator], f$2 = "[ \n\f\r]", v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m$2 = />/g, p$1 = RegExp(`>|${f$2}(?:([^\\s"'>=/]+)(${f$2}*=${f$2}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y = /^(?:script|style|textarea|title)$/i, x = (t$7) => (i$10, ...s$9) => ({ - _$litType$: t$7, - strings: i$10, - values: s$9 -}), b = x(1), w = x(2), T = x(3), E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = new WeakMap(), P = l$3.createTreeWalker(l$3, 129); + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const t$5 = globalThis, + i$7 = (t$7) => t$7, + s$7 = t$5.trustedTypes, + e$11 = s$7 ? s$7.createPolicy("lit-html", { createHTML: (t$7) => t$7 }) : void 0, + h$5 = "$lit$", + o$12 = `lit$${Math.random().toFixed(9).slice(2)}$`, + n$10 = "?" + o$12, + r$9 = `<${n$10}>`, + l$3 = document, + c$4 = () => l$3.createComment(""), + a = (t$7) => null === t$7 || ("object" != typeof t$7 && "function" != typeof t$7), + u$2 = Array.isArray, + d$1 = (t$7) => u$2(t$7) || "function" == typeof t$7?.[Symbol.iterator], + f$2 = "[ \n\f\r]", + v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, + _ = /-->/g, + m$2 = />/g, + p$1 = RegExp(`>|${f$2}(?:([^\\s"'>=/]+)(${f$2}*=${f$2}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"), + g = /'/g, + $ = /"/g, + y = /^(?:script|style|textarea|title)$/i, + x = + (t$7) => + (i$10, ...s$9) => ({ + _$litType$: t$7, + strings: i$10, + values: s$9, + }), + b = x(1), + w = x(2), + T = x(3), + E = Symbol.for("lit-noChange"), + A = Symbol.for("lit-nothing"), + C = new WeakMap(), + P = l$3.createTreeWalker(l$3, 129); function V(t$7, i$10) { - if (!u$2(t$7) || !t$7.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return void 0 !== e$11 ? e$11.createHTML(i$10) : i$10; + if (!u$2(t$7) || !t$7.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return void 0 !== e$11 ? e$11.createHTML(i$10) : i$10; } const N = (t$7, i$10) => { - const s$9 = t$7.length - 1, e$14 = []; - let n$13, l$5 = 2 === i$10 ? "" : 3 === i$10 ? "" : "", c$7 = v$1; - for (let i$11 = 0; i$11 < s$9; i$11++) { - const s$10 = t$7[i$11]; - let a$2, u$4, d$3 = -1, f$4 = 0; - for (; f$4 < s$10.length && (c$7.lastIndex = f$4, u$4 = c$7.exec(s$10), null !== u$4);) f$4 = c$7.lastIndex, c$7 === v$1 ? "!--" === u$4[1] ? c$7 = _ : void 0 !== u$4[1] ? c$7 = m$2 : void 0 !== u$4[2] ? (y.test(u$4[2]) && (n$13 = RegExp("" === u$4[0] ? (c$7 = n$13 ?? v$1, d$3 = -1) : void 0 === u$4[1] ? d$3 = -2 : (d$3 = c$7.lastIndex - u$4[2].length, a$2 = u$4[1], c$7 = void 0 === u$4[3] ? p$1 : "\"" === u$4[3] ? $ : g) : c$7 === $ || c$7 === g ? c$7 = p$1 : c$7 === _ || c$7 === m$2 ? c$7 = v$1 : (c$7 = p$1, n$13 = void 0); - const x$1 = c$7 === p$1 && t$7[i$11 + 1].startsWith("/>") ? " " : ""; - l$5 += c$7 === v$1 ? s$10 + r$9 : d$3 >= 0 ? (e$14.push(a$2), s$10.slice(0, d$3) + h$5 + s$10.slice(d$3) + o$12 + x$1) : s$10 + o$12 + (-2 === d$3 ? i$11 : x$1); - } - return [V(t$7, l$5 + (t$7[s$9] || "") + (2 === i$10 ? "" : 3 === i$10 ? "" : "")), e$14]; + const s$9 = t$7.length - 1, + e$14 = []; + let n$13, + l$5 = 2 === i$10 ? "" : 3 === i$10 ? "" : "", + c$7 = v$1; + for (let i$11 = 0; i$11 < s$9; i$11++) { + const s$10 = t$7[i$11]; + let a$2, + u$4, + d$3 = -1, + f$4 = 0; + for (; f$4 < s$10.length && ((c$7.lastIndex = f$4), (u$4 = c$7.exec(s$10)), null !== u$4); ) + ((f$4 = c$7.lastIndex), + c$7 === v$1 + ? "!--" === u$4[1] + ? (c$7 = _) + : void 0 !== u$4[1] + ? (c$7 = m$2) + : void 0 !== u$4[2] + ? (y.test(u$4[2]) && (n$13 = RegExp("" === u$4[0] + ? ((c$7 = n$13 ?? v$1), (d$3 = -1)) + : void 0 === u$4[1] + ? (d$3 = -2) + : ((d$3 = c$7.lastIndex - u$4[2].length), + (a$2 = u$4[1]), + (c$7 = void 0 === u$4[3] ? p$1 : '"' === u$4[3] ? $ : g)) + : c$7 === $ || c$7 === g + ? (c$7 = p$1) + : c$7 === _ || c$7 === m$2 + ? (c$7 = v$1) + : ((c$7 = p$1), (n$13 = void 0))); + const x$1 = c$7 === p$1 && t$7[i$11 + 1].startsWith("/>") ? " " : ""; + l$5 += + c$7 === v$1 + ? s$10 + r$9 + : d$3 >= 0 + ? (e$14.push(a$2), s$10.slice(0, d$3) + h$5 + s$10.slice(d$3) + o$12 + x$1) + : s$10 + o$12 + (-2 === d$3 ? i$11 : x$1); + } + return [ + V(t$7, l$5 + (t$7[s$9] || "") + (2 === i$10 ? "" : 3 === i$10 ? "" : "")), + e$14, + ]; }; var S = class S { - constructor({ strings: t$7, _$litType$: i$10 }, e$14) { - let r$12; - this.parts = []; - let l$5 = 0, a$2 = 0; - const u$4 = t$7.length - 1, d$3 = this.parts, [f$4, v$2] = N(t$7, i$10); - if (this.el = S.createElement(f$4, e$14), P.currentNode = this.el.content, 2 === i$10 || 3 === i$10) { - const t$8 = this.el.content.firstChild; - t$8.replaceWith(...t$8.childNodes); - } - for (; null !== (r$12 = P.nextNode()) && d$3.length < u$4;) { - if (1 === r$12.nodeType) { - if (r$12.hasAttributes()) for (const t$8 of r$12.getAttributeNames()) if (t$8.endsWith(h$5)) { - const i$11 = v$2[a$2++], s$9 = r$12.getAttribute(t$8).split(o$12), e$15 = /([.?@])?(.*)/.exec(i$11); - d$3.push({ - type: 1, - index: l$5, - name: e$15[2], - strings: s$9, - ctor: "." === e$15[1] ? I : "?" === e$15[1] ? L : "@" === e$15[1] ? z : H - }), r$12.removeAttribute(t$8); - } else t$8.startsWith(o$12) && (d$3.push({ - type: 6, - index: l$5 - }), r$12.removeAttribute(t$8)); - if (y.test(r$12.tagName)) { - const t$8 = r$12.textContent.split(o$12), i$11 = t$8.length - 1; - if (i$11 > 0) { - r$12.textContent = s$7 ? s$7.emptyScript : ""; - for (let s$9 = 0; s$9 < i$11; s$9++) r$12.append(t$8[s$9], c$4()), P.nextNode(), d$3.push({ - type: 2, - index: ++l$5 - }); - r$12.append(t$8[i$11], c$4()); - } - } - } else if (8 === r$12.nodeType) if (r$12.data === n$10) d$3.push({ - type: 2, - index: l$5 - }); - else { - let t$8 = -1; - for (; -1 !== (t$8 = r$12.data.indexOf(o$12, t$8 + 1));) d$3.push({ - type: 7, - index: l$5 - }), t$8 += o$12.length - 1; - } - l$5++; - } - } - static createElement(t$7, i$10) { - const s$9 = l$3.createElement("template"); - return s$9.innerHTML = t$7, s$9; - } + constructor({ strings: t$7, _$litType$: i$10 }, e$14) { + let r$12; + this.parts = []; + let l$5 = 0, + a$2 = 0; + const u$4 = t$7.length - 1, + d$3 = this.parts, + [f$4, v$2] = N(t$7, i$10); + if ( + ((this.el = S.createElement(f$4, e$14)), + (P.currentNode = this.el.content), + 2 === i$10 || 3 === i$10) + ) { + const t$8 = this.el.content.firstChild; + t$8.replaceWith(...t$8.childNodes); + } + for (; null !== (r$12 = P.nextNode()) && d$3.length < u$4; ) { + if (1 === r$12.nodeType) { + if (r$12.hasAttributes()) + for (const t$8 of r$12.getAttributeNames()) + if (t$8.endsWith(h$5)) { + const i$11 = v$2[a$2++], + s$9 = r$12.getAttribute(t$8).split(o$12), + e$15 = /([.?@])?(.*)/.exec(i$11); + (d$3.push({ + type: 1, + index: l$5, + name: e$15[2], + strings: s$9, + ctor: "." === e$15[1] ? I : "?" === e$15[1] ? L : "@" === e$15[1] ? z : H, + }), + r$12.removeAttribute(t$8)); + } else + t$8.startsWith(o$12) && + (d$3.push({ + type: 6, + index: l$5, + }), + r$12.removeAttribute(t$8)); + if (y.test(r$12.tagName)) { + const t$8 = r$12.textContent.split(o$12), + i$11 = t$8.length - 1; + if (i$11 > 0) { + r$12.textContent = s$7 ? s$7.emptyScript : ""; + for (let s$9 = 0; s$9 < i$11; s$9++) + (r$12.append(t$8[s$9], c$4()), + P.nextNode(), + d$3.push({ + type: 2, + index: ++l$5, + })); + r$12.append(t$8[i$11], c$4()); + } + } + } else if (8 === r$12.nodeType) + if (r$12.data === n$10) + d$3.push({ + type: 2, + index: l$5, + }); + else { + let t$8 = -1; + for (; -1 !== (t$8 = r$12.data.indexOf(o$12, t$8 + 1)); ) + (d$3.push({ + type: 7, + index: l$5, + }), + (t$8 += o$12.length - 1)); + } + l$5++; + } + } + static createElement(t$7, i$10) { + const s$9 = l$3.createElement("template"); + return ((s$9.innerHTML = t$7), s$9); + } }; function M$1(t$7, i$10, s$9 = t$7, e$14) { - if (i$10 === E) return i$10; - let h$7 = void 0 !== e$14 ? s$9._$Co?.[e$14] : s$9._$Cl; - const o$15 = a(i$10) ? void 0 : i$10._$litDirective$; - return h$7?.constructor !== o$15 && (h$7?._$AO?.(!1), void 0 === o$15 ? h$7 = void 0 : (h$7 = new o$15(t$7), h$7._$AT(t$7, s$9, e$14)), void 0 !== e$14 ? (s$9._$Co ??= [])[e$14] = h$7 : s$9._$Cl = h$7), void 0 !== h$7 && (i$10 = M$1(t$7, h$7._$AS(t$7, i$10.values), h$7, e$14)), i$10; + if (i$10 === E) return i$10; + let h$7 = void 0 !== e$14 ? s$9._$Co?.[e$14] : s$9._$Cl; + const o$15 = a(i$10) ? void 0 : i$10._$litDirective$; + return ( + h$7?.constructor !== o$15 && + (h$7?._$AO?.(!1), + void 0 === o$15 ? (h$7 = void 0) : ((h$7 = new o$15(t$7)), h$7._$AT(t$7, s$9, e$14)), + void 0 !== e$14 ? ((s$9._$Co ??= [])[e$14] = h$7) : (s$9._$Cl = h$7)), + void 0 !== h$7 && (i$10 = M$1(t$7, h$7._$AS(t$7, i$10.values), h$7, e$14)), + i$10 + ); } var R = class { - constructor(t$7, i$10) { - this._$AV = [], this._$AN = void 0, this._$AD = t$7, this._$AM = i$10; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t$7) { - const { el: { content: i$10 }, parts: s$9 } = this._$AD, e$14 = (t$7?.creationScope ?? l$3).importNode(i$10, !0); - P.currentNode = e$14; - let h$7 = P.nextNode(), o$15 = 0, n$13 = 0, r$12 = s$9[0]; - for (; void 0 !== r$12;) { - if (o$15 === r$12.index) { - let i$11; - 2 === r$12.type ? i$11 = new k(h$7, h$7.nextSibling, this, t$7) : 1 === r$12.type ? i$11 = new r$12.ctor(h$7, r$12.name, r$12.strings, this, t$7) : 6 === r$12.type && (i$11 = new Z(h$7, this, t$7)), this._$AV.push(i$11), r$12 = s$9[++n$13]; - } - o$15 !== r$12?.index && (h$7 = P.nextNode(), o$15++); - } - return P.currentNode = l$3, e$14; - } - p(t$7) { - let i$10 = 0; - for (const s$9 of this._$AV) void 0 !== s$9 && (void 0 !== s$9.strings ? (s$9._$AI(t$7, s$9, i$10), i$10 += s$9.strings.length - 2) : s$9._$AI(t$7[i$10])), i$10++; - } + constructor(t$7, i$10) { + ((this._$AV = []), (this._$AN = void 0), (this._$AD = t$7), (this._$AM = i$10)); + } + get parentNode() { + return this._$AM.parentNode; + } + get _$AU() { + return this._$AM._$AU; + } + u(t$7) { + const { + el: { content: i$10 }, + parts: s$9, + } = this._$AD, + e$14 = (t$7?.creationScope ?? l$3).importNode(i$10, !0); + P.currentNode = e$14; + let h$7 = P.nextNode(), + o$15 = 0, + n$13 = 0, + r$12 = s$9[0]; + for (; void 0 !== r$12; ) { + if (o$15 === r$12.index) { + let i$11; + (2 === r$12.type + ? (i$11 = new k(h$7, h$7.nextSibling, this, t$7)) + : 1 === r$12.type + ? (i$11 = new r$12.ctor(h$7, r$12.name, r$12.strings, this, t$7)) + : 6 === r$12.type && (i$11 = new Z(h$7, this, t$7)), + this._$AV.push(i$11), + (r$12 = s$9[++n$13])); + } + o$15 !== r$12?.index && ((h$7 = P.nextNode()), o$15++); + } + return ((P.currentNode = l$3), e$14); + } + p(t$7) { + let i$10 = 0; + for (const s$9 of this._$AV) + (void 0 !== s$9 && + (void 0 !== s$9.strings + ? (s$9._$AI(t$7, s$9, i$10), (i$10 += s$9.strings.length - 2)) + : s$9._$AI(t$7[i$10])), + i$10++); + } }; var k = class k { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t$7, i$10, s$9, e$14) { - this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t$7, this._$AB = i$10, this._$AM = s$9, this.options = e$14, this._$Cv = e$14?.isConnected ?? !0; - } - get parentNode() { - let t$7 = this._$AA.parentNode; - const i$10 = this._$AM; - return void 0 !== i$10 && 11 === t$7?.nodeType && (t$7 = i$10.parentNode), t$7; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t$7, i$10 = this) { - t$7 = M$1(this, t$7, i$10), a(t$7) ? t$7 === A || null == t$7 || "" === t$7 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t$7 !== this._$AH && t$7 !== E && this._(t$7) : void 0 !== t$7._$litType$ ? this.$(t$7) : void 0 !== t$7.nodeType ? this.T(t$7) : d$1(t$7) ? this.k(t$7) : this._(t$7); - } - O(t$7) { - return this._$AA.parentNode.insertBefore(t$7, this._$AB); - } - T(t$7) { - this._$AH !== t$7 && (this._$AR(), this._$AH = this.O(t$7)); - } - _(t$7) { - this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t$7 : this.T(l$3.createTextNode(t$7)), this._$AH = t$7; - } - $(t$7) { - const { values: i$10, _$litType$: s$9 } = t$7, e$14 = "number" == typeof s$9 ? this._$AC(t$7) : (void 0 === s$9.el && (s$9.el = S.createElement(V(s$9.h, s$9.h[0]), this.options)), s$9); - if (this._$AH?._$AD === e$14) this._$AH.p(i$10); - else { - const t$8 = new R(e$14, this), s$10 = t$8.u(this.options); - t$8.p(i$10), this.T(s$10), this._$AH = t$8; - } - } - _$AC(t$7) { - let i$10 = C.get(t$7.strings); - return void 0 === i$10 && C.set(t$7.strings, i$10 = new S(t$7)), i$10; - } - k(t$7) { - u$2(this._$AH) || (this._$AH = [], this._$AR()); - const i$10 = this._$AH; - let s$9, e$14 = 0; - for (const h$7 of t$7) e$14 === i$10.length ? i$10.push(s$9 = new k(this.O(c$4()), this.O(c$4()), this, this.options)) : s$9 = i$10[e$14], s$9._$AI(h$7), e$14++; - e$14 < i$10.length && (this._$AR(s$9 && s$9._$AB.nextSibling, e$14), i$10.length = e$14); - } - _$AR(t$7 = this._$AA.nextSibling, s$9) { - for (this._$AP?.(!1, !0, s$9); t$7 !== this._$AB;) { - const s$10 = i$7(t$7).nextSibling; - i$7(t$7).remove(), t$7 = s$10; - } - } - setConnected(t$7) { - void 0 === this._$AM && (this._$Cv = t$7, this._$AP?.(t$7)); - } + get _$AU() { + return this._$AM?._$AU ?? this._$Cv; + } + constructor(t$7, i$10, s$9, e$14) { + ((this.type = 2), + (this._$AH = A), + (this._$AN = void 0), + (this._$AA = t$7), + (this._$AB = i$10), + (this._$AM = s$9), + (this.options = e$14), + (this._$Cv = e$14?.isConnected ?? !0)); + } + get parentNode() { + let t$7 = this._$AA.parentNode; + const i$10 = this._$AM; + return (void 0 !== i$10 && 11 === t$7?.nodeType && (t$7 = i$10.parentNode), t$7); + } + get startNode() { + return this._$AA; + } + get endNode() { + return this._$AB; + } + _$AI(t$7, i$10 = this) { + ((t$7 = M$1(this, t$7, i$10)), + a(t$7) + ? t$7 === A || null == t$7 || "" === t$7 + ? (this._$AH !== A && this._$AR(), (this._$AH = A)) + : t$7 !== this._$AH && t$7 !== E && this._(t$7) + : void 0 !== t$7._$litType$ + ? this.$(t$7) + : void 0 !== t$7.nodeType + ? this.T(t$7) + : d$1(t$7) + ? this.k(t$7) + : this._(t$7)); + } + O(t$7) { + return this._$AA.parentNode.insertBefore(t$7, this._$AB); + } + T(t$7) { + this._$AH !== t$7 && (this._$AR(), (this._$AH = this.O(t$7))); + } + _(t$7) { + (this._$AH !== A && a(this._$AH) + ? (this._$AA.nextSibling.data = t$7) + : this.T(l$3.createTextNode(t$7)), + (this._$AH = t$7)); + } + $(t$7) { + const { values: i$10, _$litType$: s$9 } = t$7, + e$14 = + "number" == typeof s$9 + ? this._$AC(t$7) + : (void 0 === s$9.el && (s$9.el = S.createElement(V(s$9.h, s$9.h[0]), this.options)), + s$9); + if (this._$AH?._$AD === e$14) this._$AH.p(i$10); + else { + const t$8 = new R(e$14, this), + s$10 = t$8.u(this.options); + (t$8.p(i$10), this.T(s$10), (this._$AH = t$8)); + } + } + _$AC(t$7) { + let i$10 = C.get(t$7.strings); + return (void 0 === i$10 && C.set(t$7.strings, (i$10 = new S(t$7))), i$10); + } + k(t$7) { + u$2(this._$AH) || ((this._$AH = []), this._$AR()); + const i$10 = this._$AH; + let s$9, + e$14 = 0; + for (const h$7 of t$7) + (e$14 === i$10.length + ? i$10.push((s$9 = new k(this.O(c$4()), this.O(c$4()), this, this.options))) + : (s$9 = i$10[e$14]), + s$9._$AI(h$7), + e$14++); + e$14 < i$10.length && (this._$AR(s$9 && s$9._$AB.nextSibling, e$14), (i$10.length = e$14)); + } + _$AR(t$7 = this._$AA.nextSibling, s$9) { + for (this._$AP?.(!1, !0, s$9); t$7 !== this._$AB; ) { + const s$10 = i$7(t$7).nextSibling; + (i$7(t$7).remove(), (t$7 = s$10)); + } + } + setConnected(t$7) { + void 0 === this._$AM && ((this._$Cv = t$7), this._$AP?.(t$7)); + } }; var H = class { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t$7, i$10, s$9, e$14, h$7) { - this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t$7, this.name = i$10, this._$AM = e$14, this.options = h$7, s$9.length > 2 || "" !== s$9[0] || "" !== s$9[1] ? (this._$AH = Array(s$9.length - 1).fill(new String()), this.strings = s$9) : this._$AH = A; - } - _$AI(t$7, i$10 = this, s$9, e$14) { - const h$7 = this.strings; - let o$15 = !1; - if (void 0 === h$7) t$7 = M$1(this, t$7, i$10, 0), o$15 = !a(t$7) || t$7 !== this._$AH && t$7 !== E, o$15 && (this._$AH = t$7); - else { - const e$15 = t$7; - let n$13, r$12; - for (t$7 = h$7[0], n$13 = 0; n$13 < h$7.length - 1; n$13++) r$12 = M$1(this, e$15[s$9 + n$13], i$10, n$13), r$12 === E && (r$12 = this._$AH[n$13]), o$15 ||= !a(r$12) || r$12 !== this._$AH[n$13], r$12 === A ? t$7 = A : t$7 !== A && (t$7 += (r$12 ?? "") + h$7[n$13 + 1]), this._$AH[n$13] = r$12; - } - o$15 && !e$14 && this.j(t$7); - } - j(t$7) { - t$7 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t$7 ?? ""); - } + get tagName() { + return this.element.tagName; + } + get _$AU() { + return this._$AM._$AU; + } + constructor(t$7, i$10, s$9, e$14, h$7) { + ((this.type = 1), + (this._$AH = A), + (this._$AN = void 0), + (this.element = t$7), + (this.name = i$10), + (this._$AM = e$14), + (this.options = h$7), + s$9.length > 2 || "" !== s$9[0] || "" !== s$9[1] + ? ((this._$AH = Array(s$9.length - 1).fill(new String())), (this.strings = s$9)) + : (this._$AH = A)); + } + _$AI(t$7, i$10 = this, s$9, e$14) { + const h$7 = this.strings; + let o$15 = !1; + if (void 0 === h$7) + ((t$7 = M$1(this, t$7, i$10, 0)), + (o$15 = !a(t$7) || (t$7 !== this._$AH && t$7 !== E)), + o$15 && (this._$AH = t$7)); + else { + const e$15 = t$7; + let n$13, r$12; + for (t$7 = h$7[0], n$13 = 0; n$13 < h$7.length - 1; n$13++) + ((r$12 = M$1(this, e$15[s$9 + n$13], i$10, n$13)), + r$12 === E && (r$12 = this._$AH[n$13]), + (o$15 ||= !a(r$12) || r$12 !== this._$AH[n$13]), + r$12 === A ? (t$7 = A) : t$7 !== A && (t$7 += (r$12 ?? "") + h$7[n$13 + 1]), + (this._$AH[n$13] = r$12)); + } + o$15 && !e$14 && this.j(t$7); + } + j(t$7) { + t$7 === A + ? this.element.removeAttribute(this.name) + : this.element.setAttribute(this.name, t$7 ?? ""); + } }; var I = class extends H { - constructor() { - super(...arguments), this.type = 3; - } - j(t$7) { - this.element[this.name] = t$7 === A ? void 0 : t$7; - } + constructor() { + (super(...arguments), (this.type = 3)); + } + j(t$7) { + this.element[this.name] = t$7 === A ? void 0 : t$7; + } }; var L = class extends H { - constructor() { - super(...arguments), this.type = 4; - } - j(t$7) { - this.element.toggleAttribute(this.name, !!t$7 && t$7 !== A); - } + constructor() { + (super(...arguments), (this.type = 4)); + } + j(t$7) { + this.element.toggleAttribute(this.name, !!t$7 && t$7 !== A); + } }; var z = class extends H { - constructor(t$7, i$10, s$9, e$14, h$7) { - super(t$7, i$10, s$9, e$14, h$7), this.type = 5; - } - _$AI(t$7, i$10 = this) { - if ((t$7 = M$1(this, t$7, i$10, 0) ?? A) === E) return; - const s$9 = this._$AH, e$14 = t$7 === A && s$9 !== A || t$7.capture !== s$9.capture || t$7.once !== s$9.once || t$7.passive !== s$9.passive, h$7 = t$7 !== A && (s$9 === A || e$14); - e$14 && this.element.removeEventListener(this.name, this, s$9), h$7 && this.element.addEventListener(this.name, this, t$7), this._$AH = t$7; - } - handleEvent(t$7) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t$7) : this._$AH.handleEvent(t$7); - } + constructor(t$7, i$10, s$9, e$14, h$7) { + (super(t$7, i$10, s$9, e$14, h$7), (this.type = 5)); + } + _$AI(t$7, i$10 = this) { + if ((t$7 = M$1(this, t$7, i$10, 0) ?? A) === E) return; + const s$9 = this._$AH, + e$14 = + (t$7 === A && s$9 !== A) || + t$7.capture !== s$9.capture || + t$7.once !== s$9.once || + t$7.passive !== s$9.passive, + h$7 = t$7 !== A && (s$9 === A || e$14); + (e$14 && this.element.removeEventListener(this.name, this, s$9), + h$7 && this.element.addEventListener(this.name, this, t$7), + (this._$AH = t$7)); + } + handleEvent(t$7) { + "function" == typeof this._$AH + ? this._$AH.call(this.options?.host ?? this.element, t$7) + : this._$AH.handleEvent(t$7); + } }; var Z = class { - constructor(t$7, i$10, s$9) { - this.element = t$7, this.type = 6, this._$AN = void 0, this._$AM = i$10, this.options = s$9; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t$7) { - M$1(this, t$7); - } + constructor(t$7, i$10, s$9) { + ((this.element = t$7), + (this.type = 6), + (this._$AN = void 0), + (this._$AM = i$10), + (this.options = s$9)); + } + get _$AU() { + return this._$AM._$AU; + } + _$AI(t$7) { + M$1(this, t$7); + } }; const j$1 = { - M: h$5, - P: o$12, - A: n$10, - C: 1, - L: N, - R, - D: d$1, - V: M$1, - I: k, - H, - N: L, - U: z, - B: I, - F: Z -}, B = t$5.litHtmlPolyfillSupport; -B?.(S, k), (t$5.litHtmlVersions ??= []).push("3.3.2"); + M: h$5, + P: o$12, + A: n$10, + C: 1, + L: N, + R, + D: d$1, + V: M$1, + I: k, + H, + N: L, + U: z, + B: I, + F: Z, + }, + B = t$5.litHtmlPolyfillSupport; +(B?.(S, k), (t$5.litHtmlVersions ??= []).push("3.3.2")); const D = (t$7, i$10, s$9) => { - const e$14 = s$9?.renderBefore ?? i$10; - let h$7 = e$14._$litPart$; - if (void 0 === h$7) { - const t$8 = s$9?.renderBefore ?? null; - e$14._$litPart$ = h$7 = new k(i$10.insertBefore(c$4(), t$8), t$8, void 0, s$9 ?? {}); - } - return h$7._$AI(t$7), h$7; + const e$14 = s$9?.renderBefore ?? i$10; + let h$7 = e$14._$litPart$; + if (void 0 === h$7) { + const t$8 = s$9?.renderBefore ?? null; + e$14._$litPart$ = h$7 = new k(i$10.insertBefore(c$4(), t$8), t$8, void 0, s$9 ?? {}); + } + return (h$7._$AI(t$7), h$7); }; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s$6 = globalThis; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const s$6 = globalThis; var i$6 = class extends y$1 { - constructor() { - super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; - } - createRenderRoot() { - const t$7 = super.createRenderRoot(); - return this.renderOptions.renderBefore ??= t$7.firstChild, t$7; - } - update(t$7) { - const r$12 = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t$7), this._$Do = D(r$12, this.renderRoot, this.renderOptions); - } - connectedCallback() { - super.connectedCallback(), this._$Do?.setConnected(!0); - } - disconnectedCallback() { - super.disconnectedCallback(), this._$Do?.setConnected(!1); - } - render() { - return E; - } + constructor() { + (super(...arguments), (this.renderOptions = { host: this }), (this._$Do = void 0)); + } + createRenderRoot() { + const t$7 = super.createRenderRoot(); + return ((this.renderOptions.renderBefore ??= t$7.firstChild), t$7); + } + update(t$7) { + const r$12 = this.render(); + (this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), + super.update(t$7), + (this._$Do = D(r$12, this.renderRoot, this.renderOptions))); + } + connectedCallback() { + (super.connectedCallback(), this._$Do?.setConnected(!0)); + } + disconnectedCallback() { + (super.disconnectedCallback(), this._$Do?.setConnected(!1)); + } + render() { + return E; + } }; -i$6._$litElement$ = !0, i$6["finalized"] = !0, s$6.litElementHydrateSupport?.({ LitElement: i$6 }); +((i$6._$litElement$ = !0), + (i$6["finalized"] = !0), + s$6.litElementHydrateSupport?.({ LitElement: i$6 })); const o$11 = s$6.litElementPolyfillSupport; o$11?.({ LitElement: i$6 }); const n$9 = { - _$AK: (t$7, e$14, r$12) => { - t$7._$AK(e$14, r$12); - }, - _$AL: (t$7) => t$7._$AL + _$AK: (t$7, e$14, r$12) => { + t$7._$AK(e$14, r$12); + }, + _$AL: (t$7) => t$7._$AL, }; (s$6.litElementVersions ??= []).push("4.2.2"); /** -* @license -* Copyright 2022 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const o$10 = !1; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const t$4 = { - ATTRIBUTE: 1, - CHILD: 2, - PROPERTY: 3, - BOOLEAN_ATTRIBUTE: 4, - EVENT: 5, - ELEMENT: 6 -}, e$10 = (t$7) => (...e$14) => ({ - _$litDirective$: t$7, - values: e$14 -}); + ATTRIBUTE: 1, + CHILD: 2, + PROPERTY: 3, + BOOLEAN_ATTRIBUTE: 4, + EVENT: 5, + ELEMENT: 6, + }, + e$10 = + (t$7) => + (...e$14) => ({ + _$litDirective$: t$7, + values: e$14, + }); var i$5 = class { - constructor(t$7) {} - get _$AU() { - return this._$AM._$AU; - } - _$AT(t$7, e$14, i$10) { - this._$Ct = t$7, this._$AM = e$14, this._$Ci = i$10; - } - _$AS(t$7, e$14) { - return this.update(t$7, e$14); - } - update(t$7, e$14) { - return this.render(...e$14); - } + constructor(t$7) {} + get _$AU() { + return this._$AM._$AU; + } + _$AT(t$7, e$14, i$10) { + ((this._$Ct = t$7), (this._$AM = e$14), (this._$Ci = i$10)); + } + _$AS(t$7, e$14) { + return this.update(t$7, e$14); + } + update(t$7, e$14) { + return this.render(...e$14); + } }; /** -* @license -* Copyright 2020 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { I: t$3 } = j$1, i$4 = (o$15) => o$15, n$8 = (o$15) => null === o$15 || "object" != typeof o$15 && "function" != typeof o$15, e$9 = { - HTML: 1, - SVG: 2, - MATHML: 3 -}, l$2 = (o$15, t$7) => void 0 === t$7 ? void 0 !== o$15?._$litType$ : o$15?._$litType$ === t$7, d = (o$15) => null != o$15?._$litType$?.h, c$3 = (o$15) => void 0 !== o$15?._$litDirective$, f$1 = (o$15) => o$15?._$litDirective$, r$8 = (o$15) => void 0 === o$15.strings, s$5 = () => document.createComment(""), v = (o$15, n$13, e$14) => { - const l$5 = o$15._$AA.parentNode, d$3 = void 0 === n$13 ? o$15._$AB : n$13._$AA; - if (void 0 === e$14) { - const i$10 = l$5.insertBefore(s$5(), d$3), n$14 = l$5.insertBefore(s$5(), d$3); - e$14 = new t$3(i$10, n$14, o$15, o$15.options); - } else { - const t$7 = e$14._$AB.nextSibling, n$14 = e$14._$AM, c$7 = n$14 !== o$15; - if (c$7) { - let t$8; - e$14._$AQ?.(o$15), e$14._$AM = o$15, void 0 !== e$14._$AP && (t$8 = o$15._$AU) !== n$14._$AU && e$14._$AP(t$8); - } - if (t$7 !== d$3 || c$7) { - let o$16 = e$14._$AA; - for (; o$16 !== t$7;) { - const t$8 = i$4(o$16).nextSibling; - i$4(l$5).insertBefore(o$16, d$3), o$16 = t$8; - } - } - } - return e$14; -}, u$1 = (o$15, t$7, i$10 = o$15) => (o$15._$AI(t$7, i$10), o$15), m$1 = {}, p = (o$15, t$7 = m$1) => o$15._$AH = t$7, M = (o$15) => o$15._$AH, h$4 = (o$15) => { - o$15._$AR(), o$15._$AA.remove(); -}, j = (o$15) => { - o$15._$AR(); -}; + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const { I: t$3 } = j$1, + i$4 = (o$15) => o$15, + n$8 = (o$15) => null === o$15 || ("object" != typeof o$15 && "function" != typeof o$15), + e$9 = { + HTML: 1, + SVG: 2, + MATHML: 3, + }, + l$2 = (o$15, t$7) => (void 0 === t$7 ? void 0 !== o$15?._$litType$ : o$15?._$litType$ === t$7), + d = (o$15) => null != o$15?._$litType$?.h, + c$3 = (o$15) => void 0 !== o$15?._$litDirective$, + f$1 = (o$15) => o$15?._$litDirective$, + r$8 = (o$15) => void 0 === o$15.strings, + s$5 = () => document.createComment(""), + v = (o$15, n$13, e$14) => { + const l$5 = o$15._$AA.parentNode, + d$3 = void 0 === n$13 ? o$15._$AB : n$13._$AA; + if (void 0 === e$14) { + const i$10 = l$5.insertBefore(s$5(), d$3), + n$14 = l$5.insertBefore(s$5(), d$3); + e$14 = new t$3(i$10, n$14, o$15, o$15.options); + } else { + const t$7 = e$14._$AB.nextSibling, + n$14 = e$14._$AM, + c$7 = n$14 !== o$15; + if (c$7) { + let t$8; + (e$14._$AQ?.(o$15), + (e$14._$AM = o$15), + void 0 !== e$14._$AP && (t$8 = o$15._$AU) !== n$14._$AU && e$14._$AP(t$8)); + } + if (t$7 !== d$3 || c$7) { + let o$16 = e$14._$AA; + for (; o$16 !== t$7; ) { + const t$8 = i$4(o$16).nextSibling; + (i$4(l$5).insertBefore(o$16, d$3), (o$16 = t$8)); + } + } + } + return e$14; + }, + u$1 = (o$15, t$7, i$10 = o$15) => (o$15._$AI(t$7, i$10), o$15), + m$1 = {}, + p = (o$15, t$7 = m$1) => (o$15._$AH = t$7), + M = (o$15) => o$15._$AH, + h$4 = (o$15) => { + (o$15._$AR(), o$15._$AA.remove()); + }, + j = (o$15) => { + o$15._$AR(); + }; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const u = (e$14, s$9, t$7) => { - const r$12 = new Map(); - for (let l$5 = s$9; l$5 <= t$7; l$5++) r$12.set(e$14[l$5], l$5); - return r$12; -}, c$2 = e$10(class extends i$5 { - constructor(e$14) { - if (super(e$14), e$14.type !== t$4.CHILD) throw Error("repeat() can only be used in text expressions"); - } - dt(e$14, s$9, t$7) { - let r$12; - void 0 === t$7 ? t$7 = s$9 : void 0 !== s$9 && (r$12 = s$9); - const l$5 = [], o$15 = []; - let i$10 = 0; - for (const s$10 of e$14) l$5[i$10] = r$12 ? r$12(s$10, i$10) : i$10, o$15[i$10] = t$7(s$10, i$10), i$10++; - return { - values: o$15, - keys: l$5 - }; - } - render(e$14, s$9, t$7) { - return this.dt(e$14, s$9, t$7).values; - } - update(s$9, [t$7, r$12, c$7]) { - const d$3 = M(s$9), { values: p$3, keys: a$2 } = this.dt(t$7, r$12, c$7); - if (!Array.isArray(d$3)) return this.ut = a$2, p$3; - const h$7 = this.ut ??= [], v$2 = []; - let m$3, y$2, x$1 = 0, j$2 = d$3.length - 1, k$1 = 0, w$1 = p$3.length - 1; - for (; x$1 <= j$2 && k$1 <= w$1;) if (null === d$3[x$1]) x$1++; - else if (null === d$3[j$2]) j$2--; - else if (h$7[x$1] === a$2[k$1]) v$2[k$1] = u$1(d$3[x$1], p$3[k$1]), x$1++, k$1++; - else if (h$7[j$2] === a$2[w$1]) v$2[w$1] = u$1(d$3[j$2], p$3[w$1]), j$2--, w$1--; - else if (h$7[x$1] === a$2[w$1]) v$2[w$1] = u$1(d$3[x$1], p$3[w$1]), v(s$9, v$2[w$1 + 1], d$3[x$1]), x$1++, w$1--; - else if (h$7[j$2] === a$2[k$1]) v$2[k$1] = u$1(d$3[j$2], p$3[k$1]), v(s$9, d$3[x$1], d$3[j$2]), j$2--, k$1++; - else if (void 0 === m$3 && (m$3 = u(a$2, k$1, w$1), y$2 = u(h$7, x$1, j$2)), m$3.has(h$7[x$1])) if (m$3.has(h$7[j$2])) { - const e$14 = y$2.get(a$2[k$1]), t$8 = void 0 !== e$14 ? d$3[e$14] : null; - if (null === t$8) { - const e$15 = v(s$9, d$3[x$1]); - u$1(e$15, p$3[k$1]), v$2[k$1] = e$15; - } else v$2[k$1] = u$1(t$8, p$3[k$1]), v(s$9, d$3[x$1], t$8), d$3[e$14] = null; - k$1++; - } else h$4(d$3[j$2]), j$2--; - else h$4(d$3[x$1]), x$1++; - for (; k$1 <= w$1;) { - const e$14 = v(s$9, v$2[w$1 + 1]); - u$1(e$14, p$3[k$1]), v$2[k$1++] = e$14; - } - for (; x$1 <= j$2;) { - const e$14 = d$3[x$1++]; - null !== e$14 && h$4(e$14); - } - return this.ut = a$2, p(s$9, v$2), E; - } -}); + const r$12 = new Map(); + for (let l$5 = s$9; l$5 <= t$7; l$5++) r$12.set(e$14[l$5], l$5); + return r$12; + }, + c$2 = e$10( + class extends i$5 { + constructor(e$14) { + if ((super(e$14), e$14.type !== t$4.CHILD)) + throw Error("repeat() can only be used in text expressions"); + } + dt(e$14, s$9, t$7) { + let r$12; + void 0 === t$7 ? (t$7 = s$9) : void 0 !== s$9 && (r$12 = s$9); + const l$5 = [], + o$15 = []; + let i$10 = 0; + for (const s$10 of e$14) + ((l$5[i$10] = r$12 ? r$12(s$10, i$10) : i$10), (o$15[i$10] = t$7(s$10, i$10)), i$10++); + return { + values: o$15, + keys: l$5, + }; + } + render(e$14, s$9, t$7) { + return this.dt(e$14, s$9, t$7).values; + } + update(s$9, [t$7, r$12, c$7]) { + const d$3 = M(s$9), + { values: p$3, keys: a$2 } = this.dt(t$7, r$12, c$7); + if (!Array.isArray(d$3)) return ((this.ut = a$2), p$3); + const h$7 = (this.ut ??= []), + v$2 = []; + let m$3, + y$2, + x$1 = 0, + j$2 = d$3.length - 1, + k$1 = 0, + w$1 = p$3.length - 1; + for (; x$1 <= j$2 && k$1 <= w$1; ) + if (null === d$3[x$1]) x$1++; + else if (null === d$3[j$2]) j$2--; + else if (h$7[x$1] === a$2[k$1]) ((v$2[k$1] = u$1(d$3[x$1], p$3[k$1])), x$1++, k$1++); + else if (h$7[j$2] === a$2[w$1]) ((v$2[w$1] = u$1(d$3[j$2], p$3[w$1])), j$2--, w$1--); + else if (h$7[x$1] === a$2[w$1]) + ((v$2[w$1] = u$1(d$3[x$1], p$3[w$1])), v(s$9, v$2[w$1 + 1], d$3[x$1]), x$1++, w$1--); + else if (h$7[j$2] === a$2[k$1]) + ((v$2[k$1] = u$1(d$3[j$2], p$3[k$1])), v(s$9, d$3[x$1], d$3[j$2]), j$2--, k$1++); + else if ( + (void 0 === m$3 && ((m$3 = u(a$2, k$1, w$1)), (y$2 = u(h$7, x$1, j$2))), + m$3.has(h$7[x$1])) + ) + if (m$3.has(h$7[j$2])) { + const e$14 = y$2.get(a$2[k$1]), + t$8 = void 0 !== e$14 ? d$3[e$14] : null; + if (null === t$8) { + const e$15 = v(s$9, d$3[x$1]); + (u$1(e$15, p$3[k$1]), (v$2[k$1] = e$15)); + } else ((v$2[k$1] = u$1(t$8, p$3[k$1])), v(s$9, d$3[x$1], t$8), (d$3[e$14] = null)); + k$1++; + } else (h$4(d$3[j$2]), j$2--); + else (h$4(d$3[x$1]), x$1++); + for (; k$1 <= w$1; ) { + const e$14 = v(s$9, v$2[w$1 + 1]); + (u$1(e$14, p$3[k$1]), (v$2[k$1++] = e$14)); + } + for (; x$1 <= j$2; ) { + const e$14 = d$3[x$1++]; + null !== e$14 && h$4(e$14); + } + return ((this.ut = a$2), p(s$9, v$2), E); + } + }, + ); /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var s$4 = class extends Event { - constructor(s$9, t$7, e$14, o$15) { - super("context-request", { - bubbles: !0, - composed: !0 - }), this.context = s$9, this.contextTarget = t$7, this.callback = e$14, this.subscribe = o$15 ?? !1; - } + constructor(s$9, t$7, e$14, o$15) { + (super("context-request", { + bubbles: !0, + composed: !0, + }), + (this.context = s$9), + (this.contextTarget = t$7), + (this.callback = e$14), + (this.subscribe = o$15 ?? !1)); + } }; /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function n$7(n$13) { - return n$13; + return n$13; } /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var s$3 = class { - constructor(t$7, s$9, i$10, h$7) { - if (this.subscribe = !1, this.provided = !1, this.value = void 0, this.t = (t$8, s$10) => { - this.unsubscribe && (this.unsubscribe !== s$10 && (this.provided = !1, this.unsubscribe()), this.subscribe || this.unsubscribe()), this.value = t$8, this.host.requestUpdate(), this.provided && !this.subscribe || (this.provided = !0, this.callback && this.callback(t$8, s$10)), this.unsubscribe = s$10; - }, this.host = t$7, void 0 !== s$9.context) { - const t$8 = s$9; - this.context = t$8.context, this.callback = t$8.callback, this.subscribe = t$8.subscribe ?? !1; - } else this.context = s$9, this.callback = i$10, this.subscribe = h$7 ?? !1; - this.host.addController(this); - } - hostConnected() { - this.dispatchRequest(); - } - hostDisconnected() { - this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0); - } - dispatchRequest() { - this.host.dispatchEvent(new s$4(this.context, this.host, this.t, this.subscribe)); - } + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var s$3 = class { + constructor(t$7, s$9, i$10, h$7) { + if ( + ((this.subscribe = !1), + (this.provided = !1), + (this.value = void 0), + (this.t = (t$8, s$10) => { + (this.unsubscribe && + (this.unsubscribe !== s$10 && ((this.provided = !1), this.unsubscribe()), + this.subscribe || this.unsubscribe()), + (this.value = t$8), + this.host.requestUpdate(), + (this.provided && !this.subscribe) || + ((this.provided = !0), this.callback && this.callback(t$8, s$10)), + (this.unsubscribe = s$10)); + }), + (this.host = t$7), + void 0 !== s$9.context) + ) { + const t$8 = s$9; + ((this.context = t$8.context), + (this.callback = t$8.callback), + (this.subscribe = t$8.subscribe ?? !1)); + } else ((this.context = s$9), (this.callback = i$10), (this.subscribe = h$7 ?? !1)); + this.host.addController(this); + } + hostConnected() { + this.dispatchRequest(); + } + hostDisconnected() { + this.unsubscribe && (this.unsubscribe(), (this.unsubscribe = void 0)); + } + dispatchRequest() { + this.host.dispatchEvent(new s$4(this.context, this.host, this.t, this.subscribe)); + } }; /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var s$2 = class { - get value() { - return this.o; - } - set value(s$9) { - this.setValue(s$9); - } - setValue(s$9, t$7 = !1) { - const i$10 = t$7 || !Object.is(s$9, this.o); - this.o = s$9, i$10 && this.updateObservers(); - } - constructor(s$9) { - this.subscriptions = new Map(), this.updateObservers = () => { - for (const [s$10, { disposer: t$7 }] of this.subscriptions) s$10(this.o, t$7); - }, void 0 !== s$9 && (this.value = s$9); - } - addCallback(s$9, t$7, i$10) { - if (!i$10) return void s$9(this.value); - this.subscriptions.has(s$9) || this.subscriptions.set(s$9, { - disposer: () => { - this.subscriptions.delete(s$9); - }, - consumerHost: t$7 - }); - const { disposer: h$7 } = this.subscriptions.get(s$9); - s$9(this.value, h$7); - } - clearCallbacks() { - this.subscriptions.clear(); - } + get value() { + return this.o; + } + set value(s$9) { + this.setValue(s$9); + } + setValue(s$9, t$7 = !1) { + const i$10 = t$7 || !Object.is(s$9, this.o); + ((this.o = s$9), i$10 && this.updateObservers()); + } + constructor(s$9) { + ((this.subscriptions = new Map()), + (this.updateObservers = () => { + for (const [s$10, { disposer: t$7 }] of this.subscriptions) s$10(this.o, t$7); + }), + void 0 !== s$9 && (this.value = s$9)); + } + addCallback(s$9, t$7, i$10) { + if (!i$10) return void s$9(this.value); + this.subscriptions.has(s$9) || + this.subscriptions.set(s$9, { + disposer: () => { + this.subscriptions.delete(s$9); + }, + consumerHost: t$7, + }); + const { disposer: h$7 } = this.subscriptions.get(s$9); + s$9(this.value, h$7); + } + clearCallbacks() { + this.subscriptions.clear(); + } }; /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var e$8 = class extends Event { - constructor(t$7, s$9) { - super("context-provider", { - bubbles: !0, - composed: !0 - }), this.context = t$7, this.contextTarget = s$9; - } + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var e$8 = class extends Event { + constructor(t$7, s$9) { + (super("context-provider", { + bubbles: !0, + composed: !0, + }), + (this.context = t$7), + (this.contextTarget = s$9)); + } }; var i$3 = class extends s$2 { - constructor(s$9, e$14, i$10) { - super(void 0 !== e$14.context ? e$14.initialValue : i$10), this.onContextRequest = (t$7) => { - if (t$7.context !== this.context) return; - const s$10 = t$7.contextTarget ?? t$7.composedPath()[0]; - s$10 !== this.host && (t$7.stopPropagation(), this.addCallback(t$7.callback, s$10, t$7.subscribe)); - }, this.onProviderRequest = (s$10) => { - if (s$10.context !== this.context) return; - if ((s$10.contextTarget ?? s$10.composedPath()[0]) === this.host) return; - const e$15 = new Set(); - for (const [s$11, { consumerHost: i$11 }] of this.subscriptions) e$15.has(s$11) || (e$15.add(s$11), i$11.dispatchEvent(new s$4(this.context, i$11, s$11, !0))); - s$10.stopPropagation(); - }, this.host = s$9, void 0 !== e$14.context ? this.context = e$14.context : this.context = e$14, this.attachListeners(), this.host.addController?.(this); - } - attachListeners() { - this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest); - } - hostConnected() { - this.host.dispatchEvent(new e$8(this.context, this.host)); - } + constructor(s$9, e$14, i$10) { + (super(void 0 !== e$14.context ? e$14.initialValue : i$10), + (this.onContextRequest = (t$7) => { + if (t$7.context !== this.context) return; + const s$10 = t$7.contextTarget ?? t$7.composedPath()[0]; + s$10 !== this.host && + (t$7.stopPropagation(), this.addCallback(t$7.callback, s$10, t$7.subscribe)); + }), + (this.onProviderRequest = (s$10) => { + if (s$10.context !== this.context) return; + if ((s$10.contextTarget ?? s$10.composedPath()[0]) === this.host) return; + const e$15 = new Set(); + for (const [s$11, { consumerHost: i$11 }] of this.subscriptions) + e$15.has(s$11) || + (e$15.add(s$11), i$11.dispatchEvent(new s$4(this.context, i$11, s$11, !0))); + s$10.stopPropagation(); + }), + (this.host = s$9), + void 0 !== e$14.context ? (this.context = e$14.context) : (this.context = e$14), + this.attachListeners(), + this.host.addController?.(this)); + } + attachListeners() { + (this.host.addEventListener("context-request", this.onContextRequest), + this.host.addEventListener("context-provider", this.onProviderRequest)); + } + hostConnected() { + this.host.dispatchEvent(new e$8(this.context, this.host)); + } }; /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var t$2 = class { - constructor() { - this.pendingContextRequests = new Map(), this.onContextProvider = (t$7) => { - const s$9 = this.pendingContextRequests.get(t$7.context); - if (void 0 === s$9) return; - this.pendingContextRequests.delete(t$7.context); - const { requests: o$15 } = s$9; - for (const { elementRef: s$10, callbackRef: n$13 } of o$15) { - const o$16 = s$10.deref(), c$7 = n$13.deref(); - void 0 === o$16 || void 0 === c$7 || o$16.dispatchEvent(new s$4(t$7.context, o$16, c$7, !0)); - } - }, this.onContextRequest = (e$14) => { - if (!0 !== e$14.subscribe) return; - const t$7 = e$14.contextTarget ?? e$14.composedPath()[0], s$9 = e$14.callback; - let o$15 = this.pendingContextRequests.get(e$14.context); - void 0 === o$15 && this.pendingContextRequests.set(e$14.context, o$15 = { - callbacks: new WeakMap(), - requests: [] - }); - let n$13 = o$15.callbacks.get(t$7); - void 0 === n$13 && o$15.callbacks.set(t$7, n$13 = new WeakSet()), n$13.has(s$9) || (n$13.add(s$9), o$15.requests.push({ - elementRef: new WeakRef(t$7), - callbackRef: new WeakRef(s$9) - })); - }; - } - attach(e$14) { - e$14.addEventListener("context-request", this.onContextRequest), e$14.addEventListener("context-provider", this.onContextProvider); - } - detach(e$14) { - e$14.removeEventListener("context-request", this.onContextRequest), e$14.removeEventListener("context-provider", this.onContextProvider); - } + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var t$2 = class { + constructor() { + ((this.pendingContextRequests = new Map()), + (this.onContextProvider = (t$7) => { + const s$9 = this.pendingContextRequests.get(t$7.context); + if (void 0 === s$9) return; + this.pendingContextRequests.delete(t$7.context); + const { requests: o$15 } = s$9; + for (const { elementRef: s$10, callbackRef: n$13 } of o$15) { + const o$16 = s$10.deref(), + c$7 = n$13.deref(); + void 0 === o$16 || + void 0 === c$7 || + o$16.dispatchEvent(new s$4(t$7.context, o$16, c$7, !0)); + } + }), + (this.onContextRequest = (e$14) => { + if (!0 !== e$14.subscribe) return; + const t$7 = e$14.contextTarget ?? e$14.composedPath()[0], + s$9 = e$14.callback; + let o$15 = this.pendingContextRequests.get(e$14.context); + void 0 === o$15 && + this.pendingContextRequests.set( + e$14.context, + (o$15 = { + callbacks: new WeakMap(), + requests: [], + }), + ); + let n$13 = o$15.callbacks.get(t$7); + (void 0 === n$13 && o$15.callbacks.set(t$7, (n$13 = new WeakSet())), + n$13.has(s$9) || + (n$13.add(s$9), + o$15.requests.push({ + elementRef: new WeakRef(t$7), + callbackRef: new WeakRef(s$9), + }))); + })); + } + attach(e$14) { + (e$14.addEventListener("context-request", this.onContextRequest), + e$14.addEventListener("context-provider", this.onContextProvider)); + } + detach(e$14) { + (e$14.removeEventListener("context-request", this.onContextRequest), + e$14.removeEventListener("context-provider", this.onContextProvider)); + } }; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function e$7({ context: e$14 }) { - return (n$13, i$10) => { - const r$12 = new WeakMap(); - if ("object" == typeof i$10) return { - get() { - return n$13.get.call(this); - }, - set(t$7) { - return r$12.get(this).setValue(t$7), n$13.set.call(this, t$7); - }, - init(n$14) { - return r$12.set(this, new i$3(this, { - context: e$14, - initialValue: n$14 - })), n$14; - } - }; - { - n$13.constructor.addInitializer(((n$14) => { - r$12.set(n$14, new i$3(n$14, { context: e$14 })); - })); - const o$15 = Object.getOwnPropertyDescriptor(n$13, i$10); - let s$9; - if (void 0 === o$15) { - const t$7 = new WeakMap(); - s$9 = { - get() { - return t$7.get(this); - }, - set(e$15) { - r$12.get(this).setValue(e$15), t$7.set(this, e$15); - }, - configurable: !0, - enumerable: !0 - }; - } else { - const t$7 = o$15.set; - s$9 = { - ...o$15, - set(e$15) { - r$12.get(this).setValue(e$15), t$7?.call(this, e$15); - } - }; - } - return void Object.defineProperty(n$13, i$10, s$9); - } - }; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function e$7({ context: e$14 }) { + return (n$13, i$10) => { + const r$12 = new WeakMap(); + if ("object" == typeof i$10) + return { + get() { + return n$13.get.call(this); + }, + set(t$7) { + return (r$12.get(this).setValue(t$7), n$13.set.call(this, t$7)); + }, + init(n$14) { + return ( + r$12.set( + this, + new i$3(this, { + context: e$14, + initialValue: n$14, + }), + ), + n$14 + ); + }, + }; + { + n$13.constructor.addInitializer((n$14) => { + r$12.set(n$14, new i$3(n$14, { context: e$14 })); + }); + const o$15 = Object.getOwnPropertyDescriptor(n$13, i$10); + let s$9; + if (void 0 === o$15) { + const t$7 = new WeakMap(); + s$9 = { + get() { + return t$7.get(this); + }, + set(e$15) { + (r$12.get(this).setValue(e$15), t$7.set(this, e$15)); + }, + configurable: !0, + enumerable: !0, + }; + } else { + const t$7 = o$15.set; + s$9 = { + ...o$15, + set(e$15) { + (r$12.get(this).setValue(e$15), t$7?.call(this, e$15)); + }, + }; + } + return void Object.defineProperty(n$13, i$10, s$9); + } + }; } /** -* @license -* Copyright 2022 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function c$1({ context: c$7, subscribe: e$14 }) { - return (o$15, n$13) => { - "object" == typeof n$13 ? n$13.addInitializer((function() { - new s$3(this, { - context: c$7, - callback: (t$7) => { - o$15.set.call(this, t$7); - }, - subscribe: e$14 - }); - })) : o$15.constructor.addInitializer(((o$16) => { - new s$3(o$16, { - context: c$7, - callback: (t$7) => { - o$16[n$13] = t$7; - }, - subscribe: e$14 - }); - })); - }; + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function c$1({ context: c$7, subscribe: e$14 }) { + return (o$15, n$13) => { + "object" == typeof n$13 + ? n$13.addInitializer(function () { + new s$3(this, { + context: c$7, + callback: (t$7) => { + o$15.set.call(this, t$7); + }, + subscribe: e$14, + }); + }) + : o$15.constructor.addInitializer((o$16) => { + new s$3(o$16, { + context: c$7, + callback: (t$7) => { + o$16[n$13] = t$7; + }, + subscribe: e$14, + }); + }); + }; } const eventInit = { - bubbles: true, - cancelable: true, - composed: true + bubbles: true, + cancelable: true, + composed: true, }; var StateEvent = class StateEvent extends CustomEvent { - static { - this.eventName = "a2uiaction"; - } - constructor(payload) { - super(StateEvent.eventName, { - detail: payload, - ...eventInit - }); - this.payload = payload; - } + static { + this.eventName = "a2uiaction"; + } + constructor(payload) { + super(StateEvent.eventName, { + detail: payload, + ...eventInit, + }); + this.payload = payload; + } }; const opacityBehavior = ` @@ -987,12 +1405,15 @@ const opacityBehavior = ` } }`; const behavior = ` - ${new Array(21).fill(0).map((_$1, idx) => { - return `.behavior-ho-${idx * 5} { + ${new Array(21) + .fill(0) + .map((_$1, idx) => { + return `.behavior-ho-${idx * 5} { --opacity: ${idx / 20}; ${opacityBehavior} }`; -}).join("\n")} + }) + .join("\n")} .behavior-o-s { overflow: scroll; @@ -1014,8 +1435,10 @@ const behavior = ` const grid = 4; const border = ` - ${new Array(25).fill(0).map((_$1, idx) => { - return ` + ${new Array(25) + .fill(0) + .map((_$1, idx) => { + return ` .border-bw-${idx} { border-width: ${idx}px; } .border-btw-${idx} { border-top-width: ${idx}px; } .border-bbw-${idx} { border-bottom-width: ${idx}px; } @@ -1024,7 +1447,8 @@ const border = ` .border-ow-${idx} { outline-width: ${idx}px; } .border-br-${idx} { border-radius: ${idx * grid}px; overflow: hidden;}`; -}).join("\n")} + }) + .join("\n")} .border-br-50pc { border-radius: 50%; @@ -1035,125 +1459,117 @@ const border = ` } `; -const shades = [ - 0, - 5, - 10, - 15, - 20, - 25, - 30, - 35, - 40, - 50, - 60, - 70, - 80, - 90, - 95, - 98, - 99, - 100 -]; +const shades = [0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100]; function merge(...classes) { - const styles = {}; - for (const clazz of classes) { - for (const [key, val] of Object.entries(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - const existingKeys = Object.keys(styles).filter((key$1) => key$1.startsWith(prefix)); - for (const existingKey of existingKeys) { - delete styles[existingKey]; - } - styles[key] = val; - } - } - return styles; + const styles = {}; + for (const clazz of classes) { + for (const [key, val] of Object.entries(clazz)) { + const prefix = key.split("-").with(-1, "").join("-"); + const existingKeys = Object.keys(styles).filter((key$1) => key$1.startsWith(prefix)); + for (const existingKey of existingKeys) { + delete styles[existingKey]; + } + styles[key] = val; + } + } + return styles; } function appendToAll(target, exclusions, ...classes) { - const updatedTarget = structuredClone(target); - for (const clazz of classes) { - for (const key of Object.keys(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - for (const [tagName, classesToAdd] of Object.entries(updatedTarget)) { - if (exclusions.includes(tagName)) { - continue; - } - let found = false; - for (let t$7 = 0; t$7 < classesToAdd.length; t$7++) { - if (classesToAdd[t$7].startsWith(prefix)) { - found = true; - classesToAdd[t$7] = key; - } - } - if (!found) { - classesToAdd.push(key); - } - } - } - } - return updatedTarget; + const updatedTarget = structuredClone(target); + for (const clazz of classes) { + for (const key of Object.keys(clazz)) { + const prefix = key.split("-").with(-1, "").join("-"); + for (const [tagName, classesToAdd] of Object.entries(updatedTarget)) { + if (exclusions.includes(tagName)) { + continue; + } + let found = false; + for (let t$7 = 0; t$7 < classesToAdd.length; t$7++) { + if (classesToAdd[t$7].startsWith(prefix)) { + found = true; + classesToAdd[t$7] = key; + } + } + if (!found) { + classesToAdd.push(key); + } + } + } + } + return updatedTarget; } function createThemeStyles(palettes) { - const styles = {}; - for (const palette of Object.values(palettes)) { - for (const [key, val] of Object.entries(palette)) { - const prop = toProp(key); - styles[prop] = val; - } - } - return styles; + const styles = {}; + for (const palette of Object.values(palettes)) { + for (const [key, val] of Object.entries(palette)) { + const prop = toProp(key); + styles[prop] = val; + } + } + return styles; } function toProp(key) { - if (key.startsWith("nv")) { - return `--nv-${key.slice(2)}`; - } - return `--${key[0]}-${key.slice(1)}`; + if (key.startsWith("nv")) { + return `--nv-${key.slice(2)}`; + } + return `--${key[0]}-${key.slice(1)}`; } const color = (src) => ` - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-bc-${key} { border-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} + ${src + .map((key) => { + const inverseKey = getInverseKey(key); + return `.color-bc-${key} { border-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; + }) + .join("\n")} - ${src.map((key) => { - const inverseKey = getInverseKey(key); - const vals = [`.color-bgc-${key} { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`, `.color-bbgc-${key}::backdrop { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`]; - for (let o$15 = .1; o$15 < 1; o$15 += .1) { - vals.push(`.color-bbgc-${key}_${(o$15 * 100).toFixed(0)}::backdrop { + ${src + .map((key) => { + const inverseKey = getInverseKey(key); + const vals = [ + `.color-bgc-${key} { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`, + `.color-bbgc-${key}::backdrop { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`, + ]; + for (let o$15 = 0.1; o$15 < 1; o$15 += 0.1) { + vals.push(`.color-bbgc-${key}_${(o$15 * 100).toFixed(0)}::backdrop { background-color: light-dark(oklch(from var(${toProp(key)}) l c h / calc(alpha * ${o$15.toFixed(1)})), oklch(from var(${toProp(inverseKey)}) l c h / calc(alpha * ${o$15.toFixed(1)})) ); } `); - } - return vals.join("\n"); -}).join("\n")} + } + return vals.join("\n"); + }) + .join("\n")} - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-c-${key} { color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} + ${src + .map((key) => { + const inverseKey = getInverseKey(key); + return `.color-c-${key} { color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; + }) + .join("\n")} `; const getInverseKey = (key) => { - const match = key.match(/^([a-z]+)(\d+)$/); - if (!match) return key; - const [, prefix, shadeStr] = match; - const shade = parseInt(shadeStr, 10); - const target = 100 - shade; - const inverseShade = shades.reduce((prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev); - return `${prefix}${inverseShade}`; + const match = key.match(/^([a-z]+)(\d+)$/); + if (!match) return key; + const [, prefix, shadeStr] = match; + const shade = parseInt(shadeStr, 10); + const target = 100 - shade; + const inverseShade = shades.reduce((prev, curr) => + Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev, + ); + return `${prefix}${inverseShade}`; }; const keyFactory = (prefix) => { - return shades.map((v$2) => `${prefix}${v$2}`); + return shades.map((v$2) => `${prefix}${v$2}`); }; const colors = [ - color(keyFactory("p")), - color(keyFactory("s")), - color(keyFactory("t")), - color(keyFactory("n")), - color(keyFactory("nv")), - color(keyFactory("e")), - ` + color(keyFactory("p")), + color(keyFactory("s")), + color(keyFactory("t")), + color(keyFactory("n")), + color(keyFactory("nv")), + color(keyFactory("e")), + ` .color-bgc-transparent { background-color: transparent; } @@ -1161,18 +1577,18 @@ const colors = [ :host { color-scheme: var(--color-scheme); } - ` + `, ]; /** -* CSS classes for Google Symbols. -* -* Usage: -* -* ```html -* pen_spark -* ``` -*/ + * CSS classes for Google Symbols. + * + * Usage: + * + * ```html + * pen_spark + * ``` + */ const icons = ` .g-icon { font-family: "Material Symbols Outlined", "Google Symbols"; @@ -1211,15 +1627,20 @@ const icons = ` const layout = ` :host { - ${new Array(16).fill(0).map((_$1, idx) => { - return `--g-${idx + 1}: ${(idx + 1) * grid}px;`; -}).join("\n")} + ${new Array(16) + .fill(0) + .map((_$1, idx) => { + return `--g-${idx + 1}: ${(idx + 1) * grid}px;`; + }) + .join("\n")} } - ${new Array(49).fill(0).map((_$1, index) => { - const idx = index - 24; - const lbl = idx < 0 ? `n${Math.abs(idx)}` : idx.toString(); - return ` + ${new Array(49) + .fill(0) + .map((_$1, index) => { + const idx = index - 24; + const lbl = idx < 0 ? `n${Math.abs(idx)}` : idx.toString(); + return ` .layout-p-${lbl} { --padding: ${idx * grid}px; padding: var(--padding); } .layout-pt-${lbl} { padding-top: ${idx * grid}px; } .layout-pr-${lbl} { padding-right: ${idx * grid}px; } @@ -1236,17 +1657,24 @@ const layout = ` .layout-r-${lbl} { right: ${idx * grid}px; } .layout-b-${lbl} { bottom: ${idx * grid}px; } .layout-l-${lbl} { left: ${idx * grid}px; }`; -}).join("\n")} + }) + .join("\n")} - ${new Array(25).fill(0).map((_$1, idx) => { - return ` + ${new Array(25) + .fill(0) + .map((_$1, idx) => { + return ` .layout-g-${idx} { gap: ${idx * grid}px; }`; -}).join("\n")} + }) + .join("\n")} - ${new Array(8).fill(0).map((_$1, idx) => { - return ` + ${new Array(8) + .fill(0) + .map((_$1, idx) => { + return ` .layout-grd-col${idx + 1} { grid-template-columns: ${"1fr ".repeat(idx + 1).trim()}; }`; -}).join("\n")} + }) + .join("\n")} .layout-pos-a { position: absolute; @@ -1356,27 +1784,39 @@ const layout = ` /** Widths **/ - ${new Array(10).fill(0).map((_$1, idx) => { - const weight = (idx + 1) * 10; - return `.layout-w-${weight} { width: ${weight}%; max-width: ${weight}%; }`; -}).join("\n")} + ${new Array(10) + .fill(0) + .map((_$1, idx) => { + const weight = (idx + 1) * 10; + return `.layout-w-${weight} { width: ${weight}%; max-width: ${weight}%; }`; + }) + .join("\n")} - ${new Array(16).fill(0).map((_$1, idx) => { - const weight = idx * grid; - return `.layout-wp-${idx} { width: ${weight}px; }`; -}).join("\n")} + ${new Array(16) + .fill(0) + .map((_$1, idx) => { + const weight = idx * grid; + return `.layout-wp-${idx} { width: ${weight}px; }`; + }) + .join("\n")} /** Heights **/ - ${new Array(10).fill(0).map((_$1, idx) => { - const height = (idx + 1) * 10; - return `.layout-h-${height} { height: ${height}%; }`; -}).join("\n")} + ${new Array(10) + .fill(0) + .map((_$1, idx) => { + const height = (idx + 1) * 10; + return `.layout-h-${height} { height: ${height}%; }`; + }) + .join("\n")} - ${new Array(16).fill(0).map((_$1, idx) => { - const height = idx * grid; - return `.layout-hp-${idx} { height: ${height}px; }`; -}).join("\n")} + ${new Array(16) + .fill(0) + .map((_$1, idx) => { + const height = idx * grid; + return `.layout-hp-${idx} { height: ${height}px; }`; + }) + .join("\n")} .layout-el-cv { & img, @@ -1400,9 +1840,12 @@ const layout = ` `; const opacity = ` - ${new Array(21).fill(0).map((_$1, idx) => { - return `.opacity-el-${idx * 5} { opacity: ${idx / 20}; }`; -}).join("\n")} + ${new Array(21) + .fill(0) + .map((_$1, idx) => { + return `.opacity-el-${idx * 5} { opacity: ${idx / 20}; }`; + }) + .join("\n")} `; const type$1 = ` @@ -1537,2749 +1980,3056 @@ const type$1 = ` /** Weights **/ - ${new Array(9).fill(0).map((_$1, idx) => { - const weight = (idx + 1) * 100; - return `.typography-w-${weight} { font-weight: ${weight}; }`; -}).join("\n")} + ${new Array(9) + .fill(0) + .map((_$1, idx) => { + const weight = (idx + 1) * 100; + return `.typography-w-${weight} { font-weight: ${weight}; }`; + }) + .join("\n")} `; -const structuralStyles$1 = [ - behavior, - border, - colors, - icons, - layout, - opacity, - type$1 -].flat(Infinity).join("\n"); +const structuralStyles$1 = [behavior, border, colors, icons, layout, opacity, type$1] + .flat(Infinity) + .join("\n"); var guards_exports = /* @__PURE__ */ __exportAll({ - isComponentArrayReference: () => isComponentArrayReference, - isObject: () => isObject$1, - isPath: () => isPath, - isResolvedAudioPlayer: () => isResolvedAudioPlayer, - isResolvedButton: () => isResolvedButton, - isResolvedCard: () => isResolvedCard, - isResolvedCheckbox: () => isResolvedCheckbox, - isResolvedColumn: () => isResolvedColumn, - isResolvedDateTimeInput: () => isResolvedDateTimeInput, - isResolvedDivider: () => isResolvedDivider, - isResolvedIcon: () => isResolvedIcon, - isResolvedImage: () => isResolvedImage, - isResolvedList: () => isResolvedList, - isResolvedModal: () => isResolvedModal, - isResolvedMultipleChoice: () => isResolvedMultipleChoice, - isResolvedRow: () => isResolvedRow, - isResolvedSlider: () => isResolvedSlider, - isResolvedTabs: () => isResolvedTabs, - isResolvedText: () => isResolvedText, - isResolvedTextField: () => isResolvedTextField, - isResolvedVideo: () => isResolvedVideo, - isValueMap: () => isValueMap + isComponentArrayReference: () => isComponentArrayReference, + isObject: () => isObject$1, + isPath: () => isPath, + isResolvedAudioPlayer: () => isResolvedAudioPlayer, + isResolvedButton: () => isResolvedButton, + isResolvedCard: () => isResolvedCard, + isResolvedCheckbox: () => isResolvedCheckbox, + isResolvedColumn: () => isResolvedColumn, + isResolvedDateTimeInput: () => isResolvedDateTimeInput, + isResolvedDivider: () => isResolvedDivider, + isResolvedIcon: () => isResolvedIcon, + isResolvedImage: () => isResolvedImage, + isResolvedList: () => isResolvedList, + isResolvedModal: () => isResolvedModal, + isResolvedMultipleChoice: () => isResolvedMultipleChoice, + isResolvedRow: () => isResolvedRow, + isResolvedSlider: () => isResolvedSlider, + isResolvedTabs: () => isResolvedTabs, + isResolvedText: () => isResolvedText, + isResolvedTextField: () => isResolvedTextField, + isResolvedVideo: () => isResolvedVideo, + isValueMap: () => isValueMap, }); function isValueMap(value) { - return isObject$1(value) && "key" in value; + return isObject$1(value) && "key" in value; } function isPath(key, value) { - return key === "path" && typeof value === "string"; + return key === "path" && typeof value === "string"; } function isObject$1(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); + return typeof value === "object" && value !== null && !Array.isArray(value); } function isComponentArrayReference(value) { - if (!isObject$1(value)) return false; - return "explicitList" in value || "template" in value; + if (!isObject$1(value)) return false; + return "explicitList" in value || "template" in value; } function isStringValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "string" || "literalString" in value); + return ( + isObject$1(value) && + ("path" in value || + ("literal" in value && typeof value.literal === "string") || + "literalString" in value) + ); } function isNumberValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "number" || "literalNumber" in value); + return ( + isObject$1(value) && + ("path" in value || + ("literal" in value && typeof value.literal === "number") || + "literalNumber" in value) + ); } function isBooleanValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "boolean" || "literalBoolean" in value); + return ( + isObject$1(value) && + ("path" in value || + ("literal" in value && typeof value.literal === "boolean") || + "literalBoolean" in value) + ); } function isAnyComponentNode(value) { - if (!isObject$1(value)) return false; - const hasBaseKeys = "id" in value && "type" in value && "properties" in value; - if (!hasBaseKeys) return false; - return true; + if (!isObject$1(value)) return false; + const hasBaseKeys = "id" in value && "type" in value && "properties" in value; + if (!hasBaseKeys) return false; + return true; } function isResolvedAudioPlayer(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); + return isObject$1(props) && "url" in props && isStringValue(props.url); } function isResolvedButton(props) { - return isObject$1(props) && "child" in props && isAnyComponentNode(props.child) && "action" in props; + return ( + isObject$1(props) && "child" in props && isAnyComponentNode(props.child) && "action" in props + ); } function isResolvedCard(props) { - if (!isObject$1(props)) return false; - if (!("child" in props)) { - if (!("children" in props)) { - return false; - } else { - return Array.isArray(props.children) && props.children.every(isAnyComponentNode); - } - } - return isAnyComponentNode(props.child); + if (!isObject$1(props)) return false; + if (!("child" in props)) { + if (!("children" in props)) { + return false; + } else { + return Array.isArray(props.children) && props.children.every(isAnyComponentNode); + } + } + return isAnyComponentNode(props.child); } function isResolvedCheckbox(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label) && "value" in props && isBooleanValue(props.value); + return ( + isObject$1(props) && + "label" in props && + isStringValue(props.label) && + "value" in props && + isBooleanValue(props.value) + ); } function isResolvedColumn(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); + return ( + isObject$1(props) && + "children" in props && + Array.isArray(props.children) && + props.children.every(isAnyComponentNode) + ); } function isResolvedDateTimeInput(props) { - return isObject$1(props) && "value" in props && isStringValue(props.value); + return isObject$1(props) && "value" in props && isStringValue(props.value); } function isResolvedDivider(props) { - return isObject$1(props); + return isObject$1(props); } function isResolvedImage(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); + return isObject$1(props) && "url" in props && isStringValue(props.url); } function isResolvedIcon(props) { - return isObject$1(props) && "name" in props && isStringValue(props.name); + return isObject$1(props) && "name" in props && isStringValue(props.name); } function isResolvedList(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); + return ( + isObject$1(props) && + "children" in props && + Array.isArray(props.children) && + props.children.every(isAnyComponentNode) + ); } function isResolvedModal(props) { - return isObject$1(props) && "entryPointChild" in props && isAnyComponentNode(props.entryPointChild) && "contentChild" in props && isAnyComponentNode(props.contentChild); + return ( + isObject$1(props) && + "entryPointChild" in props && + isAnyComponentNode(props.entryPointChild) && + "contentChild" in props && + isAnyComponentNode(props.contentChild) + ); } function isResolvedMultipleChoice(props) { - return isObject$1(props) && "selections" in props; + return isObject$1(props) && "selections" in props; } function isResolvedRow(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); + return ( + isObject$1(props) && + "children" in props && + Array.isArray(props.children) && + props.children.every(isAnyComponentNode) + ); } function isResolvedSlider(props) { - return isObject$1(props) && "value" in props && isNumberValue(props.value); + return isObject$1(props) && "value" in props && isNumberValue(props.value); } function isResolvedTabItem(item) { - return isObject$1(item) && "title" in item && isStringValue(item.title) && "child" in item && isAnyComponentNode(item.child); + return ( + isObject$1(item) && + "title" in item && + isStringValue(item.title) && + "child" in item && + isAnyComponentNode(item.child) + ); } function isResolvedTabs(props) { - return isObject$1(props) && "tabItems" in props && Array.isArray(props.tabItems) && props.tabItems.every(isResolvedTabItem); + return ( + isObject$1(props) && + "tabItems" in props && + Array.isArray(props.tabItems) && + props.tabItems.every(isResolvedTabItem) + ); } function isResolvedText(props) { - return isObject$1(props) && "text" in props && isStringValue(props.text); + return isObject$1(props) && "text" in props && isStringValue(props.text); } function isResolvedTextField(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label); + return isObject$1(props) && "label" in props && isStringValue(props.label); } function isResolvedVideo(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); + return isObject$1(props) && "url" in props && isStringValue(props.url); } /** -* Processes and consolidates A2UIProtocolMessage objects into a structured, -* hierarchical model of UI surfaces. -*/ + * Processes and consolidates A2UIProtocolMessage objects into a structured, + * hierarchical model of UI surfaces. + */ var A2uiMessageProcessor = class A2uiMessageProcessor { - static { - this.DEFAULT_SURFACE_ID = "@default"; - } - #mapCtor = Map; - #arrayCtor = Array; - #setCtor = Set; - #objCtor = Object; - #surfaces; - constructor(opts = { - mapCtor: Map, - arrayCtor: Array, - setCtor: Set, - objCtor: Object - }) { - this.opts = opts; - this.#arrayCtor = opts.arrayCtor; - this.#mapCtor = opts.mapCtor; - this.#setCtor = opts.setCtor; - this.#objCtor = opts.objCtor; - this.#surfaces = new opts.mapCtor(); - } - getSurfaces() { - return this.#surfaces; - } - clearSurfaces() { - this.#surfaces.clear(); - } - processMessages(messages) { - for (const message of messages) { - if (message.beginRendering) { - this.#handleBeginRendering(message.beginRendering, message.beginRendering.surfaceId); - } - if (message.surfaceUpdate) { - this.#handleSurfaceUpdate(message.surfaceUpdate, message.surfaceUpdate.surfaceId); - } - if (message.dataModelUpdate) { - this.#handleDataModelUpdate(message.dataModelUpdate, message.dataModelUpdate.surfaceId); - } - if (message.deleteSurface) { - this.#handleDeleteSurface(message.deleteSurface); - } - } - } - /** - * Retrieves the data for a given component node and a relative path string. - * This correctly handles the special `.` path, which refers to the node's - * own data context. - */ - getData(node, relativePath, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return null; - let finalPath; - if (relativePath === "." || relativePath === "") { - finalPath = node.dataContextPath ?? "/"; - } else { - finalPath = this.resolvePath(relativePath, node.dataContextPath); - } - return this.#getDataByPath(surface.dataModel, finalPath); - } - setData(node, relativePath, value, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - if (!node) { - console.warn("No component node set"); - return; - } - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return; - let finalPath; - if (relativePath === "." || relativePath === "") { - finalPath = node.dataContextPath ?? "/"; - } else { - finalPath = this.resolvePath(relativePath, node.dataContextPath); - } - this.#setDataByPath(surface.dataModel, finalPath, value); - } - resolvePath(path, dataContextPath) { - if (path.startsWith("/")) { - return path; - } - if (dataContextPath && dataContextPath !== "/") { - return dataContextPath.endsWith("/") ? `${dataContextPath}${path}` : `${dataContextPath}/${path}`; - } - return `/${path}`; - } - #parseIfJsonString(value) { - if (typeof value !== "string") { - return value; - } - const trimmedValue = value.trim(); - if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}") || trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) { - try { - return JSON.parse(value); - } catch (e$14) { - console.warn(`Failed to parse potential JSON string: "${value.substring(0, 50)}..."`, e$14); - return value; - } - } - return value; - } - /** - * Converts a specific array format [{key: "...", value_string: "..."}, ...] - * into a standard Map. It also attempts to parse any string values that - * appear to be stringified JSON. - */ - #convertKeyValueArrayToMap(arr) { - const map$1 = new this.#mapCtor(); - for (const item of arr) { - if (!isObject$1(item) || !("key" in item)) continue; - const key = item.key; - const valueKey = this.#findValueKey(item); - if (!valueKey) continue; - let value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) { - value = this.#convertKeyValueArrayToMap(value); - } else if (typeof value === "string") { - value = this.#parseIfJsonString(value); - } - this.#setDataByPath(map$1, key, value); - } - return map$1; - } - #setDataByPath(root, path, value) { - if (Array.isArray(value) && (value.length === 0 || isObject$1(value[0]) && "key" in value[0])) { - if (value.length === 1 && isObject$1(value[0]) && value[0].key === ".") { - const item = value[0]; - const valueKey = this.#findValueKey(item); - if (valueKey) { - value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) { - value = this.#convertKeyValueArrayToMap(value); - } else if (typeof value === "string") { - value = this.#parseIfJsonString(value); - } - } else { - value = this.#convertKeyValueArrayToMap(value); - } - } else { - value = this.#convertKeyValueArrayToMap(value); - } - } - const segments = this.#normalizePath(path).split("/").filter((s$9) => s$9); - if (segments.length === 0) { - if (value instanceof Map || isObject$1(value)) { - if (!(value instanceof Map) && isObject$1(value)) { - value = new this.#mapCtor(Object.entries(value)); - } - root.clear(); - for (const [key, v$2] of value.entries()) { - root.set(key, v$2); - } - } else { - console.error("Cannot set root of DataModel to a non-Map value."); - } - return; - } - let current = root; - for (let i$10 = 0; i$10 < segments.length - 1; i$10++) { - const segment = segments[i$10]; - let target; - if (current instanceof Map) { - target = current.get(segment); - } else if (Array.isArray(current) && /^\d+$/.test(segment)) { - target = current[parseInt(segment, 10)]; - } - if (target === undefined || typeof target !== "object" || target === null) { - target = new this.#mapCtor(); - if (current instanceof this.#mapCtor) { - current.set(segment, target); - } else if (Array.isArray(current)) { - current[parseInt(segment, 10)] = target; - } - } - current = target; - } - const finalSegment = segments[segments.length - 1]; - const storedValue = value; - if (current instanceof this.#mapCtor) { - current.set(finalSegment, storedValue); - } else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) { - current[parseInt(finalSegment, 10)] = storedValue; - } - } - /** - * Normalizes a path string into a consistent, slash-delimited format. - * Converts bracket notation and dot notation in a two-pass. - * e.g., "bookRecommendations[0].title" -> "/bookRecommendations/0/title" - * e.g., "book.0.title" -> "/book/0/title" - */ - #normalizePath(path) { - const dotPath = path.replace(/\[(\d+)\]/g, ".$1"); - const segments = dotPath.split("."); - return "/" + segments.filter((s$9) => s$9.length > 0).join("/"); - } - #getDataByPath(root, path) { - const segments = this.#normalizePath(path).split("/").filter((s$9) => s$9); - let current = root; - for (const segment of segments) { - if (current === undefined || current === null) return null; - if (current instanceof Map) { - current = current.get(segment); - } else if (Array.isArray(current) && /^\d+$/.test(segment)) { - current = current[parseInt(segment, 10)]; - } else if (isObject$1(current)) { - current = current[segment]; - } else { - return null; - } - } - return current; - } - #getOrCreateSurface(surfaceId) { - let surface = this.#surfaces.get(surfaceId); - if (!surface) { - surface = new this.#objCtor({ - rootComponentId: null, - componentTree: null, - dataModel: new this.#mapCtor(), - components: new this.#mapCtor(), - styles: new this.#objCtor() - }); - this.#surfaces.set(surfaceId, surface); - } - return surface; - } - #handleBeginRendering(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - surface.rootComponentId = message.root; - surface.styles = message.styles ?? {}; - this.#rebuildComponentTree(surface); - } - #handleSurfaceUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - for (const component of message.components) { - surface.components.set(component.id, component); - } - this.#rebuildComponentTree(surface); - } - #handleDataModelUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - const path = message.path ?? "/"; - this.#setDataByPath(surface.dataModel, path, message.contents); - this.#rebuildComponentTree(surface); - } - #handleDeleteSurface(message) { - this.#surfaces.delete(message.surfaceId); - } - /** - * Starts at the root component of the surface and builds out the tree - * recursively. This process involves resolving all properties of the child - * components, and expanding on any explicit children lists or templates - * found in the structure. - * - * @param surface The surface to be built. - */ - #rebuildComponentTree(surface) { - if (!surface.rootComponentId) { - surface.componentTree = null; - return; - } - const visited = new this.#setCtor(); - surface.componentTree = this.#buildNodeRecursive(surface.rootComponentId, surface, visited, "/", ""); - } - /** Finds a value key in a map. */ - #findValueKey(value) { - return Object.keys(value).find((k$1) => k$1.startsWith("value")); - } - /** - * Builds out the nodes recursively. - */ - #buildNodeRecursive(baseComponentId, surface, visited, dataContextPath, idSuffix = "") { - const fullId = `${baseComponentId}${idSuffix}`; - const { components } = surface; - if (!components.has(baseComponentId)) { - return null; - } - if (visited.has(fullId)) { - throw new Error(`Circular dependency for component "${fullId}".`); - } - visited.add(fullId); - const componentData = components.get(baseComponentId); - const componentProps = componentData.component ?? {}; - const componentType = Object.keys(componentProps)[0]; - const unresolvedProperties = componentProps[componentType]; - const resolvedProperties = new this.#objCtor(); - if (isObject$1(unresolvedProperties)) { - for (const [key, value] of Object.entries(unresolvedProperties)) { - resolvedProperties[key] = this.#resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix, key); - } - } - visited.delete(fullId); - const baseNode = { - id: fullId, - dataContextPath, - weight: componentData.weight ?? "initial" - }; - switch (componentType) { - case "Text": - if (!isResolvedText(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Text", - properties: resolvedProperties - }); - case "Image": - if (!isResolvedImage(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Image", - properties: resolvedProperties - }); - case "Icon": - if (!isResolvedIcon(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Icon", - properties: resolvedProperties - }); - case "Video": - if (!isResolvedVideo(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Video", - properties: resolvedProperties - }); - case "AudioPlayer": - if (!isResolvedAudioPlayer(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "AudioPlayer", - properties: resolvedProperties - }); - case "Row": - if (!isResolvedRow(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Row", - properties: resolvedProperties - }); - case "Column": - if (!isResolvedColumn(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Column", - properties: resolvedProperties - }); - case "List": - if (!isResolvedList(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "List", - properties: resolvedProperties - }); - case "Card": - if (!isResolvedCard(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Card", - properties: resolvedProperties - }); - case "Tabs": - if (!isResolvedTabs(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Tabs", - properties: resolvedProperties - }); - case "Divider": - if (!isResolvedDivider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Divider", - properties: resolvedProperties - }); - case "Modal": - if (!isResolvedModal(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Modal", - properties: resolvedProperties - }); - case "Button": - if (!isResolvedButton(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Button", - properties: resolvedProperties - }); - case "CheckBox": - if (!isResolvedCheckbox(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "CheckBox", - properties: resolvedProperties - }); - case "TextField": - if (!isResolvedTextField(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "TextField", - properties: resolvedProperties - }); - case "DateTimeInput": - if (!isResolvedDateTimeInput(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "DateTimeInput", - properties: resolvedProperties - }); - case "MultipleChoice": - if (!isResolvedMultipleChoice(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "MultipleChoice", - properties: resolvedProperties - }); - case "Slider": - if (!isResolvedSlider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Slider", - properties: resolvedProperties - }); - default: return new this.#objCtor({ - ...baseNode, - type: componentType, - properties: resolvedProperties - }); - } - } - /** - * Recursively resolves an individual property value. If a property indicates - * a child node (a string that matches a component ID), an explicitList of - * children, or a template, these will be built out here. - */ - #resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix = "", propertyKey = null) { - const isComponentIdReferenceKey = (key) => key === "child" || key.endsWith("Child"); - if (typeof value === "string" && propertyKey && isComponentIdReferenceKey(propertyKey) && surface.components.has(value)) { - return this.#buildNodeRecursive(value, surface, visited, dataContextPath, idSuffix); - } - if (isComponentArrayReference(value)) { - if (value.explicitList) { - return value.explicitList.map((id) => this.#buildNodeRecursive(id, surface, visited, dataContextPath, idSuffix)); - } - if (value.template) { - const fullDataPath = this.resolvePath(value.template.dataBinding, dataContextPath); - const data = this.#getDataByPath(surface.dataModel, fullDataPath); - const template = value.template; - if (Array.isArray(data)) { - return data.map((_$1, index) => { - const parentIndices = dataContextPath.split("/").filter((segment) => /^\d+$/.test(segment)); - const newIndices = [...parentIndices, index]; - const newSuffix = `:${newIndices.join(":")}`; - const childDataContextPath = `${fullDataPath}/${index}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - } - const mapCtor = this.#mapCtor; - if (data instanceof mapCtor) { - return Array.from(data.keys(), (key) => { - const newSuffix = `:${key}`; - const childDataContextPath = `${fullDataPath}/${key}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - } - return new this.#arrayCtor(); - } - } - if (Array.isArray(value)) { - return value.map((item) => this.#resolvePropertyValue(item, surface, visited, dataContextPath, idSuffix, propertyKey)); - } - if (isObject$1(value)) { - const newObj = new this.#objCtor(); - for (const [key, propValue] of Object.entries(value)) { - let propertyValue = propValue; - if (isPath(key, propValue) && dataContextPath !== "/") { - propertyValue = propValue.replace(/^\.?\/item/, "").replace(/^\.?\/text/, "").replace(/^\.?\/label/, "").replace(/^\.?\//, ""); - newObj[key] = propertyValue; - continue; - } - newObj[key] = this.#resolvePropertyValue(propertyValue, surface, visited, dataContextPath, idSuffix, key); - } - return newObj; - } - return value; - } + static { + this.DEFAULT_SURFACE_ID = "@default"; + } + #mapCtor = Map; + #arrayCtor = Array; + #setCtor = Set; + #objCtor = Object; + #surfaces; + constructor( + opts = { + mapCtor: Map, + arrayCtor: Array, + setCtor: Set, + objCtor: Object, + }, + ) { + this.opts = opts; + this.#arrayCtor = opts.arrayCtor; + this.#mapCtor = opts.mapCtor; + this.#setCtor = opts.setCtor; + this.#objCtor = opts.objCtor; + this.#surfaces = new opts.mapCtor(); + } + getSurfaces() { + return this.#surfaces; + } + clearSurfaces() { + this.#surfaces.clear(); + } + processMessages(messages) { + for (const message of messages) { + if (message.beginRendering) { + this.#handleBeginRendering(message.beginRendering, message.beginRendering.surfaceId); + } + if (message.surfaceUpdate) { + this.#handleSurfaceUpdate(message.surfaceUpdate, message.surfaceUpdate.surfaceId); + } + if (message.dataModelUpdate) { + this.#handleDataModelUpdate(message.dataModelUpdate, message.dataModelUpdate.surfaceId); + } + if (message.deleteSurface) { + this.#handleDeleteSurface(message.deleteSurface); + } + } + } + /** + * Retrieves the data for a given component node and a relative path string. + * This correctly handles the special `.` path, which refers to the node's + * own data context. + */ + getData(node, relativePath, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { + const surface = this.#getOrCreateSurface(surfaceId); + if (!surface) return null; + let finalPath; + if (relativePath === "." || relativePath === "") { + finalPath = node.dataContextPath ?? "/"; + } else { + finalPath = this.resolvePath(relativePath, node.dataContextPath); + } + return this.#getDataByPath(surface.dataModel, finalPath); + } + setData(node, relativePath, value, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { + if (!node) { + console.warn("No component node set"); + return; + } + const surface = this.#getOrCreateSurface(surfaceId); + if (!surface) return; + let finalPath; + if (relativePath === "." || relativePath === "") { + finalPath = node.dataContextPath ?? "/"; + } else { + finalPath = this.resolvePath(relativePath, node.dataContextPath); + } + this.#setDataByPath(surface.dataModel, finalPath, value); + } + resolvePath(path, dataContextPath) { + if (path.startsWith("/")) { + return path; + } + if (dataContextPath && dataContextPath !== "/") { + return dataContextPath.endsWith("/") + ? `${dataContextPath}${path}` + : `${dataContextPath}/${path}`; + } + return `/${path}`; + } + #parseIfJsonString(value) { + if (typeof value !== "string") { + return value; + } + const trimmedValue = value.trim(); + if ( + (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) || + (trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) + ) { + try { + return JSON.parse(value); + } catch (e$14) { + console.warn(`Failed to parse potential JSON string: "${value.substring(0, 50)}..."`, e$14); + return value; + } + } + return value; + } + /** + * Converts a specific array format [{key: "...", value_string: "..."}, ...] + * into a standard Map. It also attempts to parse any string values that + * appear to be stringified JSON. + */ + #convertKeyValueArrayToMap(arr) { + const map$1 = new this.#mapCtor(); + for (const item of arr) { + if (!isObject$1(item) || !("key" in item)) continue; + const key = item.key; + const valueKey = this.#findValueKey(item); + if (!valueKey) continue; + let value = item[valueKey]; + if (valueKey === "valueMap" && Array.isArray(value)) { + value = this.#convertKeyValueArrayToMap(value); + } else if (typeof value === "string") { + value = this.#parseIfJsonString(value); + } + this.#setDataByPath(map$1, key, value); + } + return map$1; + } + #setDataByPath(root, path, value) { + if ( + Array.isArray(value) && + (value.length === 0 || (isObject$1(value[0]) && "key" in value[0])) + ) { + if (value.length === 1 && isObject$1(value[0]) && value[0].key === ".") { + const item = value[0]; + const valueKey = this.#findValueKey(item); + if (valueKey) { + value = item[valueKey]; + if (valueKey === "valueMap" && Array.isArray(value)) { + value = this.#convertKeyValueArrayToMap(value); + } else if (typeof value === "string") { + value = this.#parseIfJsonString(value); + } + } else { + value = this.#convertKeyValueArrayToMap(value); + } + } else { + value = this.#convertKeyValueArrayToMap(value); + } + } + const segments = this.#normalizePath(path) + .split("/") + .filter((s$9) => s$9); + if (segments.length === 0) { + if (value instanceof Map || isObject$1(value)) { + if (!(value instanceof Map) && isObject$1(value)) { + value = new this.#mapCtor(Object.entries(value)); + } + root.clear(); + for (const [key, v$2] of value.entries()) { + root.set(key, v$2); + } + } else { + console.error("Cannot set root of DataModel to a non-Map value."); + } + return; + } + let current = root; + for (let i$10 = 0; i$10 < segments.length - 1; i$10++) { + const segment = segments[i$10]; + let target; + if (current instanceof Map) { + target = current.get(segment); + } else if (Array.isArray(current) && /^\d+$/.test(segment)) { + target = current[parseInt(segment, 10)]; + } + if (target === undefined || typeof target !== "object" || target === null) { + target = new this.#mapCtor(); + if (current instanceof this.#mapCtor) { + current.set(segment, target); + } else if (Array.isArray(current)) { + current[parseInt(segment, 10)] = target; + } + } + current = target; + } + const finalSegment = segments[segments.length - 1]; + const storedValue = value; + if (current instanceof this.#mapCtor) { + current.set(finalSegment, storedValue); + } else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) { + current[parseInt(finalSegment, 10)] = storedValue; + } + } + /** + * Normalizes a path string into a consistent, slash-delimited format. + * Converts bracket notation and dot notation in a two-pass. + * e.g., "bookRecommendations[0].title" -> "/bookRecommendations/0/title" + * e.g., "book.0.title" -> "/book/0/title" + */ + #normalizePath(path) { + const dotPath = path.replace(/\[(\d+)\]/g, ".$1"); + const segments = dotPath.split("."); + return "/" + segments.filter((s$9) => s$9.length > 0).join("/"); + } + #getDataByPath(root, path) { + const segments = this.#normalizePath(path) + .split("/") + .filter((s$9) => s$9); + let current = root; + for (const segment of segments) { + if (current === undefined || current === null) return null; + if (current instanceof Map) { + current = current.get(segment); + } else if (Array.isArray(current) && /^\d+$/.test(segment)) { + current = current[parseInt(segment, 10)]; + } else if (isObject$1(current)) { + current = current[segment]; + } else { + return null; + } + } + return current; + } + #getOrCreateSurface(surfaceId) { + let surface = this.#surfaces.get(surfaceId); + if (!surface) { + surface = new this.#objCtor({ + rootComponentId: null, + componentTree: null, + dataModel: new this.#mapCtor(), + components: new this.#mapCtor(), + styles: new this.#objCtor(), + }); + this.#surfaces.set(surfaceId, surface); + } + return surface; + } + #handleBeginRendering(message, surfaceId) { + const surface = this.#getOrCreateSurface(surfaceId); + surface.rootComponentId = message.root; + surface.styles = message.styles ?? {}; + this.#rebuildComponentTree(surface); + } + #handleSurfaceUpdate(message, surfaceId) { + const surface = this.#getOrCreateSurface(surfaceId); + for (const component of message.components) { + surface.components.set(component.id, component); + } + this.#rebuildComponentTree(surface); + } + #handleDataModelUpdate(message, surfaceId) { + const surface = this.#getOrCreateSurface(surfaceId); + const path = message.path ?? "/"; + this.#setDataByPath(surface.dataModel, path, message.contents); + this.#rebuildComponentTree(surface); + } + #handleDeleteSurface(message) { + this.#surfaces.delete(message.surfaceId); + } + /** + * Starts at the root component of the surface and builds out the tree + * recursively. This process involves resolving all properties of the child + * components, and expanding on any explicit children lists or templates + * found in the structure. + * + * @param surface The surface to be built. + */ + #rebuildComponentTree(surface) { + if (!surface.rootComponentId) { + surface.componentTree = null; + return; + } + const visited = new this.#setCtor(); + surface.componentTree = this.#buildNodeRecursive( + surface.rootComponentId, + surface, + visited, + "/", + "", + ); + } + /** Finds a value key in a map. */ + #findValueKey(value) { + return Object.keys(value).find((k$1) => k$1.startsWith("value")); + } + /** + * Builds out the nodes recursively. + */ + #buildNodeRecursive(baseComponentId, surface, visited, dataContextPath, idSuffix = "") { + const fullId = `${baseComponentId}${idSuffix}`; + const { components } = surface; + if (!components.has(baseComponentId)) { + return null; + } + if (visited.has(fullId)) { + throw new Error(`Circular dependency for component "${fullId}".`); + } + visited.add(fullId); + const componentData = components.get(baseComponentId); + const componentProps = componentData.component ?? {}; + const componentType = Object.keys(componentProps)[0]; + const unresolvedProperties = componentProps[componentType]; + const resolvedProperties = new this.#objCtor(); + if (isObject$1(unresolvedProperties)) { + for (const [key, value] of Object.entries(unresolvedProperties)) { + resolvedProperties[key] = this.#resolvePropertyValue( + value, + surface, + visited, + dataContextPath, + idSuffix, + key, + ); + } + } + visited.delete(fullId); + const baseNode = { + id: fullId, + dataContextPath, + weight: componentData.weight ?? "initial", + }; + switch (componentType) { + case "Text": + if (!isResolvedText(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Text", + properties: resolvedProperties, + }); + case "Image": + if (!isResolvedImage(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Image", + properties: resolvedProperties, + }); + case "Icon": + if (!isResolvedIcon(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Icon", + properties: resolvedProperties, + }); + case "Video": + if (!isResolvedVideo(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Video", + properties: resolvedProperties, + }); + case "AudioPlayer": + if (!isResolvedAudioPlayer(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "AudioPlayer", + properties: resolvedProperties, + }); + case "Row": + if (!isResolvedRow(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Row", + properties: resolvedProperties, + }); + case "Column": + if (!isResolvedColumn(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Column", + properties: resolvedProperties, + }); + case "List": + if (!isResolvedList(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "List", + properties: resolvedProperties, + }); + case "Card": + if (!isResolvedCard(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Card", + properties: resolvedProperties, + }); + case "Tabs": + if (!isResolvedTabs(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Tabs", + properties: resolvedProperties, + }); + case "Divider": + if (!isResolvedDivider(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Divider", + properties: resolvedProperties, + }); + case "Modal": + if (!isResolvedModal(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Modal", + properties: resolvedProperties, + }); + case "Button": + if (!isResolvedButton(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Button", + properties: resolvedProperties, + }); + case "CheckBox": + if (!isResolvedCheckbox(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "CheckBox", + properties: resolvedProperties, + }); + case "TextField": + if (!isResolvedTextField(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "TextField", + properties: resolvedProperties, + }); + case "DateTimeInput": + if (!isResolvedDateTimeInput(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "DateTimeInput", + properties: resolvedProperties, + }); + case "MultipleChoice": + if (!isResolvedMultipleChoice(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "MultipleChoice", + properties: resolvedProperties, + }); + case "Slider": + if (!isResolvedSlider(resolvedProperties)) { + throw new Error(`Invalid data; expected ${componentType}`); + } + return new this.#objCtor({ + ...baseNode, + type: "Slider", + properties: resolvedProperties, + }); + default: + return new this.#objCtor({ + ...baseNode, + type: componentType, + properties: resolvedProperties, + }); + } + } + /** + * Recursively resolves an individual property value. If a property indicates + * a child node (a string that matches a component ID), an explicitList of + * children, or a template, these will be built out here. + */ + #resolvePropertyValue( + value, + surface, + visited, + dataContextPath, + idSuffix = "", + propertyKey = null, + ) { + const isComponentIdReferenceKey = (key) => key === "child" || key.endsWith("Child"); + if ( + typeof value === "string" && + propertyKey && + isComponentIdReferenceKey(propertyKey) && + surface.components.has(value) + ) { + return this.#buildNodeRecursive(value, surface, visited, dataContextPath, idSuffix); + } + if (isComponentArrayReference(value)) { + if (value.explicitList) { + return value.explicitList.map((id) => + this.#buildNodeRecursive(id, surface, visited, dataContextPath, idSuffix), + ); + } + if (value.template) { + const fullDataPath = this.resolvePath(value.template.dataBinding, dataContextPath); + const data = this.#getDataByPath(surface.dataModel, fullDataPath); + const template = value.template; + if (Array.isArray(data)) { + return data.map((_$1, index) => { + const parentIndices = dataContextPath + .split("/") + .filter((segment) => /^\d+$/.test(segment)); + const newIndices = [...parentIndices, index]; + const newSuffix = `:${newIndices.join(":")}`; + const childDataContextPath = `${fullDataPath}/${index}`; + return this.#buildNodeRecursive( + template.componentId, + surface, + visited, + childDataContextPath, + newSuffix, + ); + }); + } + const mapCtor = this.#mapCtor; + if (data instanceof mapCtor) { + return Array.from(data.keys(), (key) => { + const newSuffix = `:${key}`; + const childDataContextPath = `${fullDataPath}/${key}`; + return this.#buildNodeRecursive( + template.componentId, + surface, + visited, + childDataContextPath, + newSuffix, + ); + }); + } + return new this.#arrayCtor(); + } + } + if (Array.isArray(value)) { + return value.map((item) => + this.#resolvePropertyValue(item, surface, visited, dataContextPath, idSuffix, propertyKey), + ); + } + if (isObject$1(value)) { + const newObj = new this.#objCtor(); + for (const [key, propValue] of Object.entries(value)) { + let propertyValue = propValue; + if (isPath(key, propValue) && dataContextPath !== "/") { + propertyValue = propValue + .replace(/^\.?\/item/, "") + .replace(/^\.?\/text/, "") + .replace(/^\.?\/label/, "") + .replace(/^\.?\//, ""); + newObj[key] = propertyValue; + continue; + } + newObj[key] = this.#resolvePropertyValue( + propertyValue, + surface, + visited, + dataContextPath, + idSuffix, + key, + ); + } + return newObj; + } + return value; + } }; var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { - enumerable: true, - configurable: true, - writable: true, - value -}) : obj[key] = value; +var __defNormalProp = (obj, key, value) => + key in obj + ? __defProp(obj, key, { + enumerable: true, + configurable: true, + writable: true, + value, + }) + : (obj[key] = value); var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; }; var __accessCheck = (obj, member, msg) => { - if (!member.has(obj)) throw TypeError("Cannot " + msg); + if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateIn = (member, obj) => { - if (Object(obj) !== obj) throw TypeError("Cannot use the \"in\" operator on this value"); - return member.has(obj); + if (Object(obj) !== obj) throw TypeError('Cannot use the "in" operator on this value'); + return member.has(obj); }; var __privateAdd = (obj, member, value) => { - if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); - member instanceof WeakSet ? member.add(obj) : member.set(obj, value); + if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateMethod = (obj, member, method) => { - __accessCheck(obj, member, "access private method"); - return method; + __accessCheck(obj, member, "access private method"); + return method; }; /** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ function defaultEquals(a$2, b$2) { - return Object.is(a$2, b$2); + return Object.is(a$2, b$2); } /** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ let activeConsumer = null; let inNotificationPhase = false; let epoch = 1; const SIGNAL = /* @__PURE__ */ Symbol("SIGNAL"); function setActiveConsumer(consumer) { - const prev = activeConsumer; - activeConsumer = consumer; - return prev; + const prev = activeConsumer; + activeConsumer = consumer; + return prev; } function getActiveConsumer() { - return activeConsumer; + return activeConsumer; } function isInNotificationPhase() { - return inNotificationPhase; + return inNotificationPhase; } const REACTIVE_NODE = { - version: 0, - lastCleanEpoch: 0, - dirty: false, - producerNode: void 0, - producerLastReadVersion: void 0, - producerIndexOfThis: void 0, - nextProducerIndex: 0, - liveConsumerNode: void 0, - liveConsumerIndexOfThis: void 0, - consumerAllowSignalWrites: false, - consumerIsAlwaysLive: false, - producerMustRecompute: () => false, - producerRecomputeValue: () => {}, - consumerMarkedDirty: () => {}, - consumerOnSignalRead: () => {} + version: 0, + lastCleanEpoch: 0, + dirty: false, + producerNode: void 0, + producerLastReadVersion: void 0, + producerIndexOfThis: void 0, + nextProducerIndex: 0, + liveConsumerNode: void 0, + liveConsumerIndexOfThis: void 0, + consumerAllowSignalWrites: false, + consumerIsAlwaysLive: false, + producerMustRecompute: () => false, + producerRecomputeValue: () => {}, + consumerMarkedDirty: () => {}, + consumerOnSignalRead: () => {}, }; function producerAccessed(node) { - if (inNotificationPhase) { - throw new Error(typeof ngDevMode !== "undefined" && ngDevMode ? `Assertion error: signal read during notification phase` : ""); - } - if (activeConsumer === null) { - return; - } - activeConsumer.consumerOnSignalRead(node); - const idx = activeConsumer.nextProducerIndex++; - assertConsumerNode(activeConsumer); - if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) { - if (consumerIsLive(activeConsumer)) { - const staleProducer = activeConsumer.producerNode[idx]; - producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]); - } - } - if (activeConsumer.producerNode[idx] !== node) { - activeConsumer.producerNode[idx] = node; - activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0; - } - activeConsumer.producerLastReadVersion[idx] = node.version; + if (inNotificationPhase) { + throw new Error( + typeof ngDevMode !== "undefined" && ngDevMode + ? `Assertion error: signal read during notification phase` + : "", + ); + } + if (activeConsumer === null) { + return; + } + activeConsumer.consumerOnSignalRead(node); + const idx = activeConsumer.nextProducerIndex++; + assertConsumerNode(activeConsumer); + if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) { + if (consumerIsLive(activeConsumer)) { + const staleProducer = activeConsumer.producerNode[idx]; + producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]); + } + } + if (activeConsumer.producerNode[idx] !== node) { + activeConsumer.producerNode[idx] = node; + activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) + ? producerAddLiveConsumer(node, activeConsumer, idx) + : 0; + } + activeConsumer.producerLastReadVersion[idx] = node.version; } function producerIncrementEpoch() { - epoch++; + epoch++; } function producerUpdateValueVersion(node) { - if (!node.dirty && node.lastCleanEpoch === epoch) { - return; - } - if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { - node.dirty = false; - node.lastCleanEpoch = epoch; - return; - } - node.producerRecomputeValue(node); - node.dirty = false; - node.lastCleanEpoch = epoch; + if (!node.dirty && node.lastCleanEpoch === epoch) { + return; + } + if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { + node.dirty = false; + node.lastCleanEpoch = epoch; + return; + } + node.producerRecomputeValue(node); + node.dirty = false; + node.lastCleanEpoch = epoch; } function producerNotifyConsumers(node) { - if (node.liveConsumerNode === void 0) { - return; - } - const prev = inNotificationPhase; - inNotificationPhase = true; - try { - for (const consumer of node.liveConsumerNode) { - if (!consumer.dirty) { - consumerMarkDirty(consumer); - } - } - } finally { - inNotificationPhase = prev; - } + if (node.liveConsumerNode === void 0) { + return; + } + const prev = inNotificationPhase; + inNotificationPhase = true; + try { + for (const consumer of node.liveConsumerNode) { + if (!consumer.dirty) { + consumerMarkDirty(consumer); + } + } + } finally { + inNotificationPhase = prev; + } } function producerUpdatesAllowed() { - return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false; + return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false; } function consumerMarkDirty(node) { - var _a$1; - node.dirty = true; - producerNotifyConsumers(node); - (_a$1 = node.consumerMarkedDirty) == null ? void 0 : _a$1.call(node.wrapper ?? node); + var _a$1; + node.dirty = true; + producerNotifyConsumers(node); + (_a$1 = node.consumerMarkedDirty) == null ? void 0 : _a$1.call(node.wrapper ?? node); } function consumerBeforeComputation(node) { - node && (node.nextProducerIndex = 0); - return setActiveConsumer(node); + node && (node.nextProducerIndex = 0); + return setActiveConsumer(node); } function consumerAfterComputation(node, prevConsumer) { - setActiveConsumer(prevConsumer); - if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) { - return; - } - if (consumerIsLive(node)) { - for (let i$10 = node.nextProducerIndex; i$10 < node.producerNode.length; i$10++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - } - } - while (node.producerNode.length > node.nextProducerIndex) { - node.producerNode.pop(); - node.producerLastReadVersion.pop(); - node.producerIndexOfThis.pop(); - } + setActiveConsumer(prevConsumer); + if ( + !node || + node.producerNode === void 0 || + node.producerIndexOfThis === void 0 || + node.producerLastReadVersion === void 0 + ) { + return; + } + if (consumerIsLive(node)) { + for (let i$10 = node.nextProducerIndex; i$10 < node.producerNode.length; i$10++) { + producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); + } + } + while (node.producerNode.length > node.nextProducerIndex) { + node.producerNode.pop(); + node.producerLastReadVersion.pop(); + node.producerIndexOfThis.pop(); + } } function consumerPollProducersForChange(node) { - assertConsumerNode(node); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - const producer = node.producerNode[i$10]; - const seenVersion = node.producerLastReadVersion[i$10]; - if (seenVersion !== producer.version) { - return true; - } - producerUpdateValueVersion(producer); - if (seenVersion !== producer.version) { - return true; - } - } - return false; + assertConsumerNode(node); + for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { + const producer = node.producerNode[i$10]; + const seenVersion = node.producerLastReadVersion[i$10]; + if (seenVersion !== producer.version) { + return true; + } + producerUpdateValueVersion(producer); + if (seenVersion !== producer.version) { + return true; + } + } + return false; } function producerAddLiveConsumer(node, consumer, indexOfThis) { - var _a$1; - assertProducerNode(node); - assertConsumerNode(node); - if (node.liveConsumerNode.length === 0) { - (_a$1 = node.watched) == null ? void 0 : _a$1.call(node.wrapper); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - node.producerIndexOfThis[i$10] = producerAddLiveConsumer(node.producerNode[i$10], node, i$10); - } - } - node.liveConsumerIndexOfThis.push(indexOfThis); - return node.liveConsumerNode.push(consumer) - 1; + var _a$1; + assertProducerNode(node); + assertConsumerNode(node); + if (node.liveConsumerNode.length === 0) { + (_a$1 = node.watched) == null ? void 0 : _a$1.call(node.wrapper); + for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { + node.producerIndexOfThis[i$10] = producerAddLiveConsumer(node.producerNode[i$10], node, i$10); + } + } + node.liveConsumerIndexOfThis.push(indexOfThis); + return node.liveConsumerNode.push(consumer) - 1; } function producerRemoveLiveConsumerAtIndex(node, idx) { - var _a$1; - assertProducerNode(node); - assertConsumerNode(node); - if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) { - throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`); - } - if (node.liveConsumerNode.length === 1) { - (_a$1 = node.unwatched) == null ? void 0 : _a$1.call(node.wrapper); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - } - } - const lastIdx = node.liveConsumerNode.length - 1; - node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx]; - node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx]; - node.liveConsumerNode.length--; - node.liveConsumerIndexOfThis.length--; - if (idx < node.liveConsumerNode.length) { - const idxProducer = node.liveConsumerIndexOfThis[idx]; - const consumer = node.liveConsumerNode[idx]; - assertConsumerNode(consumer); - consumer.producerIndexOfThis[idxProducer] = idx; - } + var _a$1; + assertProducerNode(node); + assertConsumerNode(node); + if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) { + throw new Error( + `Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`, + ); + } + if (node.liveConsumerNode.length === 1) { + (_a$1 = node.unwatched) == null ? void 0 : _a$1.call(node.wrapper); + for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { + producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); + } + } + const lastIdx = node.liveConsumerNode.length - 1; + node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx]; + node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx]; + node.liveConsumerNode.length--; + node.liveConsumerIndexOfThis.length--; + if (idx < node.liveConsumerNode.length) { + const idxProducer = node.liveConsumerIndexOfThis[idx]; + const consumer = node.liveConsumerNode[idx]; + assertConsumerNode(consumer); + consumer.producerIndexOfThis[idxProducer] = idx; + } } function consumerIsLive(node) { - var _a$1; - return node.consumerIsAlwaysLive || (((_a$1 = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a$1.length) ?? 0) > 0; + var _a$1; + return ( + node.consumerIsAlwaysLive || + (((_a$1 = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a$1.length) ?? 0) > + 0 + ); } function assertConsumerNode(node) { - node.producerNode ?? (node.producerNode = []); - node.producerIndexOfThis ?? (node.producerIndexOfThis = []); - node.producerLastReadVersion ?? (node.producerLastReadVersion = []); + node.producerNode ?? (node.producerNode = []); + node.producerIndexOfThis ?? (node.producerIndexOfThis = []); + node.producerLastReadVersion ?? (node.producerLastReadVersion = []); } function assertProducerNode(node) { - node.liveConsumerNode ?? (node.liveConsumerNode = []); - node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []); + node.liveConsumerNode ?? (node.liveConsumerNode = []); + node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []); } /** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ function computedGet(node) { - producerUpdateValueVersion(node); - producerAccessed(node); - if (node.value === ERRORED) { - throw node.error; - } - return node.value; + producerUpdateValueVersion(node); + producerAccessed(node); + if (node.value === ERRORED) { + throw node.error; + } + return node.value; } function createComputed(computation) { - const node = Object.create(COMPUTED_NODE); - node.computation = computation; - const computed = () => computedGet(node); - computed[SIGNAL] = node; - return computed; + const node = Object.create(COMPUTED_NODE); + node.computation = computation; + const computed = () => computedGet(node); + computed[SIGNAL] = node; + return computed; } const UNSET = /* @__PURE__ */ Symbol("UNSET"); const COMPUTING = /* @__PURE__ */ Symbol("COMPUTING"); const ERRORED = /* @__PURE__ */ Symbol("ERRORED"); const COMPUTED_NODE = /* @__PURE__ */ (() => { - return { - ...REACTIVE_NODE, - value: UNSET, - dirty: true, - error: null, - equal: defaultEquals, - producerMustRecompute(node) { - return node.value === UNSET || node.value === COMPUTING; - }, - producerRecomputeValue(node) { - if (node.value === COMPUTING) { - throw new Error("Detected cycle in computations."); - } - const oldValue = node.value; - node.value = COMPUTING; - const prevConsumer = consumerBeforeComputation(node); - let newValue; - let wasEqual = false; - try { - newValue = node.computation.call(node.wrapper); - const oldOk = oldValue !== UNSET && oldValue !== ERRORED; - wasEqual = oldOk && node.equal.call(node.wrapper, oldValue, newValue); - } catch (err) { - newValue = ERRORED; - node.error = err; - } finally { - consumerAfterComputation(node, prevConsumer); - } - if (wasEqual) { - node.value = oldValue; - return; - } - node.value = newValue; - node.version++; - } - }; + return { + ...REACTIVE_NODE, + value: UNSET, + dirty: true, + error: null, + equal: defaultEquals, + producerMustRecompute(node) { + return node.value === UNSET || node.value === COMPUTING; + }, + producerRecomputeValue(node) { + if (node.value === COMPUTING) { + throw new Error("Detected cycle in computations."); + } + const oldValue = node.value; + node.value = COMPUTING; + const prevConsumer = consumerBeforeComputation(node); + let newValue; + let wasEqual = false; + try { + newValue = node.computation.call(node.wrapper); + const oldOk = oldValue !== UNSET && oldValue !== ERRORED; + wasEqual = oldOk && node.equal.call(node.wrapper, oldValue, newValue); + } catch (err) { + newValue = ERRORED; + node.error = err; + } finally { + consumerAfterComputation(node, prevConsumer); + } + if (wasEqual) { + node.value = oldValue; + return; + } + node.value = newValue; + node.version++; + }, + }; })(); /** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ function defaultThrowError() { - throw new Error(); + throw new Error(); } let throwInvalidWriteToSignalErrorFn = defaultThrowError; function throwInvalidWriteToSignalError() { - throwInvalidWriteToSignalErrorFn(); + throwInvalidWriteToSignalErrorFn(); } /** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ function createSignal(initialValue) { - const node = Object.create(SIGNAL_NODE); - node.value = initialValue; - const getter = () => { - producerAccessed(node); - return node.value; - }; - getter[SIGNAL] = node; - return getter; + const node = Object.create(SIGNAL_NODE); + node.value = initialValue; + const getter = () => { + producerAccessed(node); + return node.value; + }; + getter[SIGNAL] = node; + return getter; } function signalGetFn() { - producerAccessed(this); - return this.value; + producerAccessed(this); + return this.value; } function signalSetFn(node, newValue) { - if (!producerUpdatesAllowed()) { - throwInvalidWriteToSignalError(); - } - if (!node.equal.call(node.wrapper, node.value, newValue)) { - node.value = newValue; - signalValueChanged(node); - } + if (!producerUpdatesAllowed()) { + throwInvalidWriteToSignalError(); + } + if (!node.equal.call(node.wrapper, node.value, newValue)) { + node.value = newValue; + signalValueChanged(node); + } } const SIGNAL_NODE = /* @__PURE__ */ (() => { - return { - ...REACTIVE_NODE, - equal: defaultEquals, - value: void 0 - }; + return { + ...REACTIVE_NODE, + equal: defaultEquals, + value: void 0, + }; })(); function signalValueChanged(node) { - node.version++; - producerIncrementEpoch(); - producerNotifyConsumers(node); + node.version++; + producerIncrementEpoch(); + producerNotifyConsumers(node); } /** -* @license -* Copyright 2024 Bloomberg Finance L.P. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * @license + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ const NODE = Symbol("node"); var Signal; ((Signal2) => { - var _a$1, _brand, brand_fn, _b, _brand2, brand_fn2; - class State { - constructor(initialValue, options = {}) { - __privateAdd(this, _brand); - __publicField(this, _a$1); - const ref = createSignal(initialValue); - const node = ref[SIGNAL]; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; - } - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.get"); - return signalGetFn.call(this[NODE]); - } - set(newValue) { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.set"); - if (isInNotificationPhase()) { - throw new Error("Writes to signals not permitted during Watcher callback"); - } - const ref = this[NODE]; - signalSetFn(ref, newValue); - } - } - _a$1 = NODE; - _brand = new WeakSet(); - brand_fn = function() {}; - Signal2.isState = (s$9) => typeof s$9 === "object" && __privateIn(_brand, s$9); - Signal2.State = State; - class Computed { - constructor(computation, options) { - __privateAdd(this, _brand2); - __publicField(this, _b); - const ref = createComputed(computation); - const node = ref[SIGNAL]; - node.consumerAllowSignalWrites = true; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; - } - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isComputed)(this)) throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get"); - return computedGet(this[NODE]); - } - } - _b = NODE; - _brand2 = new WeakSet(); - brand_fn2 = function() {}; - Signal2.isComputed = (c$7) => typeof c$7 === "object" && __privateIn(_brand2, c$7); - Signal2.Computed = Computed; - ((subtle2) => { - var _a2, _brand3, brand_fn3, _assertSignals, assertSignals_fn; - function untrack(cb) { - let output; - let prevActiveConsumer = null; - try { - prevActiveConsumer = setActiveConsumer(null); - output = cb(); - } finally { - setActiveConsumer(prevActiveConsumer); - } - return output; - } - subtle2.untrack = untrack; - function introspectSources(sink) { - var _a3; - if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) { - throw new TypeError("Called introspectSources without a Computed or Watcher argument"); - } - return ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n$13) => n$13.wrapper)) ?? []; - } - subtle2.introspectSources = introspectSources; - function introspectSinks(signal) { - var _a3; - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called introspectSinks without a Signal argument"); - } - return ((_a3 = signal[NODE].liveConsumerNode) == null ? void 0 : _a3.map((n$13) => n$13.wrapper)) ?? []; - } - subtle2.introspectSinks = introspectSinks; - function hasSinks(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called hasSinks without a Signal argument"); - } - const liveConsumerNode = signal[NODE].liveConsumerNode; - if (!liveConsumerNode) return false; - return liveConsumerNode.length > 0; - } - subtle2.hasSinks = hasSinks; - function hasSources(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) { - throw new TypeError("Called hasSources without a Computed or Watcher argument"); - } - const producerNode = signal[NODE].producerNode; - if (!producerNode) return false; - return producerNode.length > 0; - } - subtle2.hasSources = hasSources; - class Watcher { - constructor(notify) { - __privateAdd(this, _brand3); - __privateAdd(this, _assertSignals); - __publicField(this, _a2); - let node = Object.create(REACTIVE_NODE); - node.wrapper = this; - node.consumerMarkedDirty = notify; - node.consumerIsAlwaysLive = true; - node.consumerAllowSignalWrites = false; - node.producerNode = []; - this[NODE] = node; - } - watch(...signals) { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); - } - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - node.dirty = false; - const prev = setActiveConsumer(node); - for (const signal of signals) { - producerAccessed(signal[NODE]); - } - setActiveConsumer(prev); - } - unwatch(...signals) { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); - } - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - assertConsumerNode(node); - for (let i$10 = node.producerNode.length - 1; i$10 >= 0; i$10--) { - if (signals.includes(node.producerNode[i$10].wrapper)) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - const lastIdx = node.producerNode.length - 1; - node.producerNode[i$10] = node.producerNode[lastIdx]; - node.producerIndexOfThis[i$10] = node.producerIndexOfThis[lastIdx]; - node.producerNode.length--; - node.producerIndexOfThis.length--; - node.nextProducerIndex--; - if (i$10 < node.producerNode.length) { - const idxConsumer = node.producerIndexOfThis[i$10]; - const producer = node.producerNode[i$10]; - assertProducerNode(producer); - producer.liveConsumerIndexOfThis[idxConsumer] = i$10; - } - } - } - } - getPending() { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called getPending without Watcher receiver"); - } - const node = this[NODE]; - return node.producerNode.filter((n$13) => n$13.dirty).map((n$13) => n$13.wrapper); - } - } - _a2 = NODE; - _brand3 = new WeakSet(); - brand_fn3 = function() {}; - _assertSignals = new WeakSet(); - assertSignals_fn = function(signals) { - for (const signal of signals) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called watch/unwatch without a Computed or State argument"); - } - } - }; - Signal2.isWatcher = (w$1) => __privateIn(_brand3, w$1); - subtle2.Watcher = Watcher; - function currentComputed() { - var _a3; - return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper; - } - subtle2.currentComputed = currentComputed; - subtle2.watched = Symbol("watched"); - subtle2.unwatched = Symbol("unwatched"); - })(Signal2.subtle || (Signal2.subtle = {})); + var _a$1, _brand, brand_fn, _b, _brand2, brand_fn2; + class State { + constructor(initialValue, options = {}) { + __privateAdd(this, _brand); + __publicField(this, _a$1); + const ref = createSignal(initialValue); + const node = ref[SIGNAL]; + this[NODE] = node; + node.wrapper = this; + if (options) { + const equals = options.equals; + if (equals) { + node.equal = equals; + } + node.watched = options[Signal2.subtle.watched]; + node.unwatched = options[Signal2.subtle.unwatched]; + } + } + get() { + if (!(0, Signal2.isState)(this)) + throw new TypeError("Wrong receiver type for Signal.State.prototype.get"); + return signalGetFn.call(this[NODE]); + } + set(newValue) { + if (!(0, Signal2.isState)(this)) + throw new TypeError("Wrong receiver type for Signal.State.prototype.set"); + if (isInNotificationPhase()) { + throw new Error("Writes to signals not permitted during Watcher callback"); + } + const ref = this[NODE]; + signalSetFn(ref, newValue); + } + } + _a$1 = NODE; + _brand = new WeakSet(); + brand_fn = function () {}; + Signal2.isState = (s$9) => typeof s$9 === "object" && __privateIn(_brand, s$9); + Signal2.State = State; + class Computed { + constructor(computation, options) { + __privateAdd(this, _brand2); + __publicField(this, _b); + const ref = createComputed(computation); + const node = ref[SIGNAL]; + node.consumerAllowSignalWrites = true; + this[NODE] = node; + node.wrapper = this; + if (options) { + const equals = options.equals; + if (equals) { + node.equal = equals; + } + node.watched = options[Signal2.subtle.watched]; + node.unwatched = options[Signal2.subtle.unwatched]; + } + } + get() { + if (!(0, Signal2.isComputed)(this)) + throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get"); + return computedGet(this[NODE]); + } + } + _b = NODE; + _brand2 = new WeakSet(); + brand_fn2 = function () {}; + Signal2.isComputed = (c$7) => typeof c$7 === "object" && __privateIn(_brand2, c$7); + Signal2.Computed = Computed; + ((subtle2) => { + var _a2, _brand3, brand_fn3, _assertSignals, assertSignals_fn; + function untrack(cb) { + let output; + let prevActiveConsumer = null; + try { + prevActiveConsumer = setActiveConsumer(null); + output = cb(); + } finally { + setActiveConsumer(prevActiveConsumer); + } + return output; + } + subtle2.untrack = untrack; + function introspectSources(sink) { + var _a3; + if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) { + throw new TypeError("Called introspectSources without a Computed or Watcher argument"); + } + return ( + ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n$13) => n$13.wrapper)) ?? [] + ); + } + subtle2.introspectSources = introspectSources; + function introspectSinks(signal) { + var _a3; + if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { + throw new TypeError("Called introspectSinks without a Signal argument"); + } + return ( + ((_a3 = signal[NODE].liveConsumerNode) == null + ? void 0 + : _a3.map((n$13) => n$13.wrapper)) ?? [] + ); + } + subtle2.introspectSinks = introspectSinks; + function hasSinks(signal) { + if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { + throw new TypeError("Called hasSinks without a Signal argument"); + } + const liveConsumerNode = signal[NODE].liveConsumerNode; + if (!liveConsumerNode) return false; + return liveConsumerNode.length > 0; + } + subtle2.hasSinks = hasSinks; + function hasSources(signal) { + if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) { + throw new TypeError("Called hasSources without a Computed or Watcher argument"); + } + const producerNode = signal[NODE].producerNode; + if (!producerNode) return false; + return producerNode.length > 0; + } + subtle2.hasSources = hasSources; + class Watcher { + constructor(notify) { + __privateAdd(this, _brand3); + __privateAdd(this, _assertSignals); + __publicField(this, _a2); + let node = Object.create(REACTIVE_NODE); + node.wrapper = this; + node.consumerMarkedDirty = notify; + node.consumerIsAlwaysLive = true; + node.consumerAllowSignalWrites = false; + node.producerNode = []; + this[NODE] = node; + } + watch(...signals) { + if (!(0, Signal2.isWatcher)(this)) { + throw new TypeError("Called unwatch without Watcher receiver"); + } + __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); + const node = this[NODE]; + node.dirty = false; + const prev = setActiveConsumer(node); + for (const signal of signals) { + producerAccessed(signal[NODE]); + } + setActiveConsumer(prev); + } + unwatch(...signals) { + if (!(0, Signal2.isWatcher)(this)) { + throw new TypeError("Called unwatch without Watcher receiver"); + } + __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); + const node = this[NODE]; + assertConsumerNode(node); + for (let i$10 = node.producerNode.length - 1; i$10 >= 0; i$10--) { + if (signals.includes(node.producerNode[i$10].wrapper)) { + producerRemoveLiveConsumerAtIndex( + node.producerNode[i$10], + node.producerIndexOfThis[i$10], + ); + const lastIdx = node.producerNode.length - 1; + node.producerNode[i$10] = node.producerNode[lastIdx]; + node.producerIndexOfThis[i$10] = node.producerIndexOfThis[lastIdx]; + node.producerNode.length--; + node.producerIndexOfThis.length--; + node.nextProducerIndex--; + if (i$10 < node.producerNode.length) { + const idxConsumer = node.producerIndexOfThis[i$10]; + const producer = node.producerNode[i$10]; + assertProducerNode(producer); + producer.liveConsumerIndexOfThis[idxConsumer] = i$10; + } + } + } + } + getPending() { + if (!(0, Signal2.isWatcher)(this)) { + throw new TypeError("Called getPending without Watcher receiver"); + } + const node = this[NODE]; + return node.producerNode.filter((n$13) => n$13.dirty).map((n$13) => n$13.wrapper); + } + } + _a2 = NODE; + _brand3 = new WeakSet(); + brand_fn3 = function () {}; + _assertSignals = new WeakSet(); + assertSignals_fn = function (signals) { + for (const signal of signals) { + if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { + throw new TypeError("Called watch/unwatch without a Computed or State argument"); + } + } + }; + Signal2.isWatcher = (w$1) => __privateIn(_brand3, w$1); + subtle2.Watcher = Watcher; + function currentComputed() { + var _a3; + return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper; + } + subtle2.currentComputed = currentComputed; + subtle2.watched = Symbol("watched"); + subtle2.unwatched = Symbol("unwatched"); + })(Signal2.subtle || (Signal2.subtle = {})); })(Signal || (Signal = {})); /** -* equality check here is always false so that we can dirty the storage -* via setting to _anything_ -* -* -* This is for a pattern where we don't *directly* use signals to back the values used in collections -* so that instanceof checks and getters and other native features "just work" without having -* to do nested proxying. -* -* (though, see deep.ts for nested / deep behavior) -*/ + * equality check here is always false so that we can dirty the storage + * via setting to _anything_ + * + * + * This is for a pattern where we don't *directly* use signals to back the values used in collections + * so that instanceof checks and getters and other native features "just work" without having + * to do nested proxying. + * + * (though, see deep.ts for nested / deep behavior) + */ const createStorage = (initial = null) => new Signal.State(initial, { equals: () => false }); /** -* Just an alias for brevity -*/ + * Just an alias for brevity + */ const BOUND_FUNS = new WeakMap(); function fnCacheFor(context) { - let fnCache = BOUND_FUNS.get(context); - if (!fnCache) { - fnCache = new Map(); - BOUND_FUNS.set(context, fnCache); - } - return fnCache; + let fnCache = BOUND_FUNS.get(context); + if (!fnCache) { + fnCache = new Map(); + BOUND_FUNS.set(context, fnCache); + } + return fnCache; } const ARRAY_GETTER_METHODS = new Set([ - Symbol.iterator, - "concat", - "entries", - "every", - "filter", - "find", - "findIndex", - "flat", - "flatMap", - "forEach", - "includes", - "indexOf", - "join", - "keys", - "lastIndexOf", - "map", - "reduce", - "reduceRight", - "slice", - "some", - "values" -]); -const ARRAY_WRITE_THEN_READ_METHODS = new Set([ - "fill", - "push", - "unshift" + Symbol.iterator, + "concat", + "entries", + "every", + "filter", + "find", + "findIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "reduce", + "reduceRight", + "slice", + "some", + "values", ]); +const ARRAY_WRITE_THEN_READ_METHODS = new Set(["fill", "push", "unshift"]); function convertToInt(prop) { - if (typeof prop === "symbol") return null; - const num = Number(prop); - if (isNaN(num)) return null; - return num % 1 === 0 ? num : null; + if (typeof prop === "symbol") return null; + const num = Number(prop); + if (isNaN(num)) return null; + return num % 1 === 0 ? num : null; } var SignalArray = class SignalArray { - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - */ - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - * @param mapfn A mapping function to call on every element of the array. - * @param thisArg Value of 'this' used to invoke the mapfn. - */ - static from(iterable, mapfn, thisArg) { - return mapfn ? new SignalArray(Array.from(iterable, mapfn, thisArg)) : new SignalArray(Array.from(iterable)); - } - static of(...arr) { - return new SignalArray(arr); - } - constructor(arr = []) { - let clone = arr.slice(); - let self = this; - let boundFns = new Map(); - /** + /** + * Creates an array from an iterable object. + * @param iterable An iterable object to convert to an array. + */ + /** + * Creates an array from an iterable object. + * @param iterable An iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + static from(iterable, mapfn, thisArg) { + return mapfn + ? new SignalArray(Array.from(iterable, mapfn, thisArg)) + : new SignalArray(Array.from(iterable)); + } + static of(...arr) { + return new SignalArray(arr); + } + constructor(arr = []) { + let clone = arr.slice(); + let self = this; + let boundFns = new Map(); + /** Flag to track whether we have *just* intercepted a call to `.push()` or `.unshift()`, since in those cases (and only those cases!) the `Array` itself checks `.length` to return from the function call. */ - let nativelyAccessingLengthFromPushOrUnshift = false; - return new Proxy(clone, { - get(target, prop) { - let index = convertToInt(prop); - if (index !== null) { - self.#readStorageFor(index); - self.#collection.get(); - return target[index]; - } - if (prop === "length") { - if (nativelyAccessingLengthFromPushOrUnshift) { - nativelyAccessingLengthFromPushOrUnshift = false; - } else { - self.#collection.get(); - } - return target[prop]; - } - if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) { - nativelyAccessingLengthFromPushOrUnshift = true; - } - if (ARRAY_GETTER_METHODS.has(prop)) { - let fn = boundFns.get(prop); - if (fn === undefined) { - fn = (...args) => { - self.#collection.get(); - return target[prop](...args); - }; - boundFns.set(prop, fn); - } - return fn; - } - return target[prop]; - }, - set(target, prop, value) { - target[prop] = value; - let index = convertToInt(prop); - if (index !== null) { - self.#dirtyStorageFor(index); - self.#collection.set(null); - } else if (prop === "length") { - self.#collection.set(null); - } - return true; - }, - getPrototypeOf() { - return SignalArray.prototype; - } - }); - } - #collection = createStorage(); - #storages = new Map(); - #readStorageFor(index) { - let storage = this.#storages.get(index); - if (storage === undefined) { - storage = createStorage(); - this.#storages.set(index, storage); - } - storage.get(); - } - #dirtyStorageFor(index) { - const storage = this.#storages.get(index); - if (storage) { - storage.set(null); - } - } + let nativelyAccessingLengthFromPushOrUnshift = false; + return new Proxy(clone, { + get(target, prop) { + let index = convertToInt(prop); + if (index !== null) { + self.#readStorageFor(index); + self.#collection.get(); + return target[index]; + } + if (prop === "length") { + if (nativelyAccessingLengthFromPushOrUnshift) { + nativelyAccessingLengthFromPushOrUnshift = false; + } else { + self.#collection.get(); + } + return target[prop]; + } + if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) { + nativelyAccessingLengthFromPushOrUnshift = true; + } + if (ARRAY_GETTER_METHODS.has(prop)) { + let fn = boundFns.get(prop); + if (fn === undefined) { + fn = (...args) => { + self.#collection.get(); + return target[prop](...args); + }; + boundFns.set(prop, fn); + } + return fn; + } + return target[prop]; + }, + set(target, prop, value) { + target[prop] = value; + let index = convertToInt(prop); + if (index !== null) { + self.#dirtyStorageFor(index); + self.#collection.set(null); + } else if (prop === "length") { + self.#collection.set(null); + } + return true; + }, + getPrototypeOf() { + return SignalArray.prototype; + }, + }); + } + #collection = createStorage(); + #storages = new Map(); + #readStorageFor(index) { + let storage = this.#storages.get(index); + if (storage === undefined) { + storage = createStorage(); + this.#storages.set(index, storage); + } + storage.get(); + } + #dirtyStorageFor(index) { + const storage = this.#storages.get(index); + if (storage) { + storage.set(null); + } + } }; Object.setPrototypeOf(SignalArray.prototype, Array.prototype); function signalArray(x$1) { - return new SignalArray(x$1); + return new SignalArray(x$1); } var SignalMap = class { - collection = createStorage(); - storages = new Map(); - vals; - readStorageFor(key) { - const { storages } = this; - let storage = storages.get(key); - if (storage === undefined) { - storage = createStorage(); - storages.set(key, storage); - } - storage.get(); - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) { - storage.set(null); - } - } - constructor(existing) { - this.vals = existing ? new Map(existing) : new Map(); - } - get(key) { - this.readStorageFor(key); - return this.vals.get(key); - } - has(key) { - this.readStorageFor(key); - return this.vals.has(key); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - set(key, value) { - this.dirtyStorageFor(key); - this.collection.set(null); - this.vals.set(key, value); - return this; - } - delete(key) { - this.dirtyStorageFor(key); - this.collection.set(null); - return this.vals.delete(key); - } - clear() { - this.storages.forEach((s$9) => s$9.set(null)); - this.collection.set(null); - this.vals.clear(); - } + collection = createStorage(); + storages = new Map(); + vals; + readStorageFor(key) { + const { storages } = this; + let storage = storages.get(key); + if (storage === undefined) { + storage = createStorage(); + storages.set(key, storage); + } + storage.get(); + } + dirtyStorageFor(key) { + const storage = this.storages.get(key); + if (storage) { + storage.set(null); + } + } + constructor(existing) { + this.vals = existing ? new Map(existing) : new Map(); + } + get(key) { + this.readStorageFor(key); + return this.vals.get(key); + } + has(key) { + this.readStorageFor(key); + return this.vals.has(key); + } + entries() { + this.collection.get(); + return this.vals.entries(); + } + keys() { + this.collection.get(); + return this.vals.keys(); + } + values() { + this.collection.get(); + return this.vals.values(); + } + forEach(fn) { + this.collection.get(); + this.vals.forEach(fn); + } + get size() { + this.collection.get(); + return this.vals.size; + } + [Symbol.iterator]() { + this.collection.get(); + return this.vals[Symbol.iterator](); + } + get [Symbol.toStringTag]() { + return this.vals[Symbol.toStringTag]; + } + set(key, value) { + this.dirtyStorageFor(key); + this.collection.set(null); + this.vals.set(key, value); + return this; + } + delete(key) { + this.dirtyStorageFor(key); + this.collection.set(null); + return this.vals.delete(key); + } + clear() { + this.storages.forEach((s$9) => s$9.set(null)); + this.collection.set(null); + this.vals.clear(); + } }; Object.setPrototypeOf(SignalMap.prototype, Map.prototype); /** -* Implementation based of tracked-built-ins' TrackedObject -* https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js -*/ + * Implementation based of tracked-built-ins' TrackedObject + * https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js + */ var SignalObjectImpl = class SignalObjectImpl { - static fromEntries(entries) { - return new SignalObjectImpl(Object.fromEntries(entries)); - } - #storages = new Map(); - #collection = createStorage(); - constructor(obj = {}) { - let proto = Object.getPrototypeOf(obj); - let descs = Object.getOwnPropertyDescriptors(obj); - let clone = Object.create(proto); - for (let prop in descs) { - Object.defineProperty(clone, prop, descs[prop]); - } - let self = this; - return new Proxy(clone, { - get(target, prop, receiver) { - self.#readStorageFor(prop); - return Reflect.get(target, prop, receiver); - }, - has(target, prop) { - self.#readStorageFor(prop); - return prop in target; - }, - ownKeys(target) { - self.#collection.get(); - return Reflect.ownKeys(target); - }, - set(target, prop, value, receiver) { - let result = Reflect.set(target, prop, value, receiver); - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - return result; - }, - deleteProperty(target, prop) { - if (prop in target) { - delete target[prop]; - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - } - return true; - }, - getPrototypeOf() { - return SignalObjectImpl.prototype; - } - }); - } - #readStorageFor(key) { - let storage = this.#storages.get(key); - if (storage === undefined) { - storage = createStorage(); - this.#storages.set(key, storage); - } - storage.get(); - } - #dirtyStorageFor(key) { - const storage = this.#storages.get(key); - if (storage) { - storage.set(null); - } - } - #dirtyCollection() { - this.#collection.set(null); - } + static fromEntries(entries) { + return new SignalObjectImpl(Object.fromEntries(entries)); + } + #storages = new Map(); + #collection = createStorage(); + constructor(obj = {}) { + let proto = Object.getPrototypeOf(obj); + let descs = Object.getOwnPropertyDescriptors(obj); + let clone = Object.create(proto); + for (let prop in descs) { + Object.defineProperty(clone, prop, descs[prop]); + } + let self = this; + return new Proxy(clone, { + get(target, prop, receiver) { + self.#readStorageFor(prop); + return Reflect.get(target, prop, receiver); + }, + has(target, prop) { + self.#readStorageFor(prop); + return prop in target; + }, + ownKeys(target) { + self.#collection.get(); + return Reflect.ownKeys(target); + }, + set(target, prop, value, receiver) { + let result = Reflect.set(target, prop, value, receiver); + self.#dirtyStorageFor(prop); + self.#dirtyCollection(); + return result; + }, + deleteProperty(target, prop) { + if (prop in target) { + delete target[prop]; + self.#dirtyStorageFor(prop); + self.#dirtyCollection(); + } + return true; + }, + getPrototypeOf() { + return SignalObjectImpl.prototype; + }, + }); + } + #readStorageFor(key) { + let storage = this.#storages.get(key); + if (storage === undefined) { + storage = createStorage(); + this.#storages.set(key, storage); + } + storage.get(); + } + #dirtyStorageFor(key) { + const storage = this.#storages.get(key); + if (storage) { + storage.set(null); + } + } + #dirtyCollection() { + this.#collection.set(null); + } }; /** -* Create a reactive Object, backed by Signals, using a Proxy. -* This allows dynamic creation and deletion of signals using the object primitive -* APIs that most folks are familiar with -- the only difference is instantiation. -* ```js -* const obj = new SignalObject({ foo: 123 }); -* -* obj.foo // 123 -* obj.foo = 456 -* obj.foo // 456 -* obj.bar = 2 -* obj.bar // 2 -* ``` -*/ + * Create a reactive Object, backed by Signals, using a Proxy. + * This allows dynamic creation and deletion of signals using the object primitive + * APIs that most folks are familiar with -- the only difference is instantiation. + * ```js + * const obj = new SignalObject({ foo: 123 }); + * + * obj.foo // 123 + * obj.foo = 456 + * obj.foo // 456 + * obj.bar = 2 + * obj.bar // 2 + * ``` + */ const SignalObject = SignalObjectImpl; function signalObject(obj) { - return new SignalObject(obj); + return new SignalObject(obj); } var SignalSet = class { - collection = createStorage(); - storages = new Map(); - vals; - storageFor(key) { - const storages = this.storages; - let storage = storages.get(key); - if (storage === undefined) { - storage = createStorage(); - storages.set(key, storage); - } - return storage; - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) { - storage.set(null); - } - } - constructor(existing) { - this.vals = new Set(existing); - } - has(value) { - this.storageFor(value).get(); - return this.vals.has(value); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - add(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - this.vals.add(value); - return this; - } - delete(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - return this.vals.delete(value); - } - clear() { - this.storages.forEach((s$9) => s$9.set(null)); - this.collection.set(null); - this.vals.clear(); - } + collection = createStorage(); + storages = new Map(); + vals; + storageFor(key) { + const storages = this.storages; + let storage = storages.get(key); + if (storage === undefined) { + storage = createStorage(); + storages.set(key, storage); + } + return storage; + } + dirtyStorageFor(key) { + const storage = this.storages.get(key); + if (storage) { + storage.set(null); + } + } + constructor(existing) { + this.vals = new Set(existing); + } + has(value) { + this.storageFor(value).get(); + return this.vals.has(value); + } + entries() { + this.collection.get(); + return this.vals.entries(); + } + keys() { + this.collection.get(); + return this.vals.keys(); + } + values() { + this.collection.get(); + return this.vals.values(); + } + forEach(fn) { + this.collection.get(); + this.vals.forEach(fn); + } + get size() { + this.collection.get(); + return this.vals.size; + } + [Symbol.iterator]() { + this.collection.get(); + return this.vals[Symbol.iterator](); + } + get [Symbol.toStringTag]() { + return this.vals[Symbol.toStringTag]; + } + add(value) { + this.dirtyStorageFor(value); + this.collection.set(null); + this.vals.add(value); + return this; + } + delete(value) { + this.dirtyStorageFor(value); + this.collection.set(null); + return this.vals.delete(value); + } + clear() { + this.storages.forEach((s$9) => s$9.set(null)); + this.collection.set(null); + this.vals.clear(); + } }; Object.setPrototypeOf(SignalSet.prototype, Set.prototype); function create() { - return new A2uiMessageProcessor({ - arrayCtor: SignalArray, - mapCtor: SignalMap, - objCtor: SignalObject, - setCtor: SignalSet - }); + return new A2uiMessageProcessor({ + arrayCtor: SignalArray, + mapCtor: SignalMap, + objCtor: SignalObject, + setCtor: SignalSet, + }); } var server_to_client_with_standard_catalog_default = { - title: "A2UI Message Schema", - description: "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", - type: "object", - additionalProperties: false, - properties: { - "beginRendering": { - "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." - }, - "root": { - "type": "string", - "description": "The ID of the root component to render." - }, - "styles": { - "type": "object", - "description": "Styling information for the UI.", - "additionalProperties": false, - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - } - } - }, - "required": ["root", "surfaceId"] - }, - "surfaceUpdate": { - "type": "object", - "description": "Updates a surface with a new set of components.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." - }, - "components": { - "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, - "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." - }, - "component": { - "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.", - "additionalProperties": false, - "properties": { - "Text": { - "type": "object", - "additionalProperties": false, - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "fit": { - "type": "string", - "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", - "enum": [ - "contain", - "cover", - "fill", - "none", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "additionalProperties": false, - "properties": { "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "additionalProperties": false, - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { "type": "string" } - } - } }, - "required": ["name"] - }, - "Video": { - "type": "object", - "additionalProperties": false, - "properties": { "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - } }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": [ - "start", - "center", - "end", - "stretch" - ] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": [ - "center", - "end", - "start", - "stretch" - ] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": [ - "start", - "center", - "end", - "stretch" - ] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "additionalProperties": false, - "properties": { "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "additionalProperties": false, - "properties": { "tabItems": { - "type": "array", - "description": "An array of objects, where each object defines a tab with a title and a child component.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "child": { "type": "string" } - }, - "required": ["title", "child"] - } - } }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "additionalProperties": false, - "properties": { "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } } - }, - "Modal": { - "type": "object", - "additionalProperties": false, - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "additionalProperties": false, - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "additionalProperties": false, - "properties": { - "name": { "type": "string" }, - "context": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "key": { "type": "string" }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "path": { "type": "string" }, - "literalString": { "type": "string" }, - "literalNumber": { "type": "number" }, - "literalBoolean": { "type": "boolean" } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] - }, - "CheckBox": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "additionalProperties": false, - "properties": { - "literalBoolean": { "type": "boolean" }, - "path": { "type": "string" } - } - } - }, - "required": ["label", "value"] - }, - "TextField": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] - }, - "DateTimeInput": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - }, - "outputFormat": { - "type": "string", - "description": "The desired format for the output string after a date or time is selected." - } - }, - "required": ["value"] - }, - "MultipleChoice": { - "type": "object", - "additionalProperties": false, - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "additionalProperties": false, - "properties": { - "literalArray": { - "type": "array", - "items": { "type": "string" } - }, - "path": { "type": "string" } - } - }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } - }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." - } - }, - "required": ["selections", "options"] - }, - "Slider": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "additionalProperties": false, - "properties": { - "literalNumber": { "type": "number" }, - "path": { "type": "string" } - } - }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." - }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." - } - }, - "required": ["value"] - } - } - } - }, - "required": ["id", "component"] - } - } - }, - "required": ["surfaceId", "components"] - }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." - }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." - }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { - "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", - "additionalProperties": false, - "properties": { - "key": { - "type": "string", - "description": "The key for this data entry." - }, - "valueString": { "type": "string" }, - "valueNumber": { "type": "number" }, - "valueBoolean": { "type": "boolean" }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "additionalProperties": false, - "properties": { - "key": { "type": "string" }, - "valueString": { "type": "string" }, - "valueNumber": { "type": "number" }, - "valueBoolean": { "type": "boolean" } - }, - "required": ["key"] - } - } - }, - "required": ["key"] - } - } - }, - "required": ["contents", "surfaceId"] - }, - "deleteSurface": { - "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", - "additionalProperties": false, - "properties": { "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." - } }, - "required": ["surfaceId"] - } - } + title: "A2UI Message Schema", + description: + "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", + type: "object", + additionalProperties: false, + properties: { + beginRendering: { + type: "object", + description: + "Signals the client to begin rendering a surface with a root component and specific styles.", + additionalProperties: false, + properties: { + surfaceId: { + type: "string", + description: "The unique identifier for the UI surface to be rendered.", + }, + root: { + type: "string", + description: "The ID of the root component to render.", + }, + styles: { + type: "object", + description: "Styling information for the UI.", + additionalProperties: false, + properties: { + font: { + type: "string", + description: "The primary font for the UI.", + }, + primaryColor: { + type: "string", + description: "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", + pattern: "^#[0-9a-fA-F]{6}$", + }, + }, + }, + }, + required: ["root", "surfaceId"], + }, + surfaceUpdate: { + type: "object", + description: "Updates a surface with a new set of components.", + additionalProperties: false, + properties: { + surfaceId: { + type: "string", + description: + "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown.", + }, + components: { + type: "array", + description: "A list containing all UI components for the surface.", + minItems: 1, + items: { + type: "object", + description: + "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", + additionalProperties: false, + properties: { + id: { + type: "string", + description: "The unique identifier for this component.", + }, + weight: { + type: "number", + description: + "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column.", + }, + component: { + type: "object", + description: + "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.", + additionalProperties: false, + properties: { + Text: { + type: "object", + additionalProperties: false, + properties: { + text: { + type: "object", + description: + "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + usageHint: { + type: "string", + description: + "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", + enum: ["h1", "h2", "h3", "h4", "h5", "caption", "body"], + }, + }, + required: ["text"], + }, + Image: { + type: "object", + additionalProperties: false, + properties: { + url: { + type: "object", + description: + "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + fit: { + type: "string", + description: + "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + enum: ["contain", "cover", "fill", "none", "scale-down"], + }, + usageHint: { + type: "string", + description: + "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", + enum: [ + "icon", + "avatar", + "smallFeature", + "mediumFeature", + "largeFeature", + "header", + ], + }, + }, + required: ["url"], + }, + Icon: { + type: "object", + additionalProperties: false, + properties: { + name: { + type: "object", + description: + "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", + additionalProperties: false, + properties: { + literalString: { + type: "string", + enum: [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "payment", + "person", + "phone", + "photo", + "print", + "refresh", + "search", + "send", + "settings", + "share", + "shoppingCart", + "star", + "starHalf", + "starOff", + "upload", + "visibility", + "visibilityOff", + "warning", + ], + }, + path: { type: "string" }, + }, + }, + }, + required: ["name"], + }, + Video: { + type: "object", + additionalProperties: false, + properties: { + url: { + type: "object", + description: + "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + }, + required: ["url"], + }, + AudioPlayer: { + type: "object", + additionalProperties: false, + properties: { + url: { + type: "object", + description: + "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + description: { + type: "object", + description: + "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + }, + required: ["url"], + }, + Row: { + type: "object", + additionalProperties: false, + properties: { + children: { + type: "object", + description: + "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", + additionalProperties: false, + properties: { + explicitList: { + type: "array", + items: { type: "string" }, + }, + template: { + type: "object", + description: + "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", + additionalProperties: false, + properties: { + componentId: { type: "string" }, + dataBinding: { type: "string" }, + }, + required: ["componentId", "dataBinding"], + }, + }, + }, + distribution: { + type: "string", + description: + "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", + enum: [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + ], + }, + alignment: { + type: "string", + description: + "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", + enum: ["start", "center", "end", "stretch"], + }, + }, + required: ["children"], + }, + Column: { + type: "object", + additionalProperties: false, + properties: { + children: { + type: "object", + description: + "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", + additionalProperties: false, + properties: { + explicitList: { + type: "array", + items: { type: "string" }, + }, + template: { + type: "object", + description: + "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", + additionalProperties: false, + properties: { + componentId: { type: "string" }, + dataBinding: { type: "string" }, + }, + required: ["componentId", "dataBinding"], + }, + }, + }, + distribution: { + type: "string", + description: + "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", + enum: [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + ], + }, + alignment: { + type: "string", + description: + "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", + enum: ["center", "end", "start", "stretch"], + }, + }, + required: ["children"], + }, + List: { + type: "object", + additionalProperties: false, + properties: { + children: { + type: "object", + description: + "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", + additionalProperties: false, + properties: { + explicitList: { + type: "array", + items: { type: "string" }, + }, + template: { + type: "object", + description: + "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", + additionalProperties: false, + properties: { + componentId: { type: "string" }, + dataBinding: { type: "string" }, + }, + required: ["componentId", "dataBinding"], + }, + }, + }, + direction: { + type: "string", + description: "The direction in which the list items are laid out.", + enum: ["vertical", "horizontal"], + }, + alignment: { + type: "string", + description: "Defines the alignment of children along the cross axis.", + enum: ["start", "center", "end", "stretch"], + }, + }, + required: ["children"], + }, + Card: { + type: "object", + additionalProperties: false, + properties: { + child: { + type: "string", + description: "The ID of the component to be rendered inside the card.", + }, + }, + required: ["child"], + }, + Tabs: { + type: "object", + additionalProperties: false, + properties: { + tabItems: { + type: "array", + description: + "An array of objects, where each object defines a tab with a title and a child component.", + items: { + type: "object", + additionalProperties: false, + properties: { + title: { + type: "object", + description: + "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + child: { type: "string" }, + }, + required: ["title", "child"], + }, + }, + }, + required: ["tabItems"], + }, + Divider: { + type: "object", + additionalProperties: false, + properties: { + axis: { + type: "string", + description: "The orientation of the divider.", + enum: ["horizontal", "vertical"], + }, + }, + }, + Modal: { + type: "object", + additionalProperties: false, + properties: { + entryPointChild: { + type: "string", + description: + "The ID of the component that opens the modal when interacted with (e.g., a button).", + }, + contentChild: { + type: "string", + description: "The ID of the component to be displayed inside the modal.", + }, + }, + required: ["entryPointChild", "contentChild"], + }, + Button: { + type: "object", + additionalProperties: false, + properties: { + child: { + type: "string", + description: + "The ID of the component to display in the button, typically a Text component.", + }, + primary: { + type: "boolean", + description: + "Indicates if this button should be styled as the primary action.", + }, + action: { + type: "object", + description: + "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", + additionalProperties: false, + properties: { + name: { type: "string" }, + context: { + type: "array", + items: { + type: "object", + additionalProperties: false, + properties: { + key: { type: "string" }, + value: { + type: "object", + description: + "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", + additionalProperties: false, + properties: { + path: { type: "string" }, + literalString: { type: "string" }, + literalNumber: { type: "number" }, + literalBoolean: { type: "boolean" }, + }, + }, + }, + required: ["key", "value"], + }, + }, + }, + required: ["name"], + }, + }, + required: ["child", "action"], + }, + CheckBox: { + type: "object", + additionalProperties: false, + properties: { + label: { + type: "object", + description: + "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + value: { + type: "object", + description: + "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", + additionalProperties: false, + properties: { + literalBoolean: { type: "boolean" }, + path: { type: "string" }, + }, + }, + }, + required: ["label", "value"], + }, + TextField: { + type: "object", + additionalProperties: false, + properties: { + label: { + type: "object", + description: + "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + text: { + type: "object", + description: + "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + textFieldType: { + type: "string", + description: "The type of input field to display.", + enum: ["date", "longText", "number", "shortText", "obscured"], + }, + validationRegexp: { + type: "string", + description: + "A regular expression used for client-side validation of the input.", + }, + }, + required: ["label"], + }, + DateTimeInput: { + type: "object", + additionalProperties: false, + properties: { + value: { + type: "object", + description: + "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + enableDate: { + type: "boolean", + description: "If true, allows the user to select a date.", + }, + enableTime: { + type: "boolean", + description: "If true, allows the user to select a time.", + }, + outputFormat: { + type: "string", + description: + "The desired format for the output string after a date or time is selected.", + }, + }, + required: ["value"], + }, + MultipleChoice: { + type: "object", + additionalProperties: false, + properties: { + selections: { + type: "object", + description: + "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", + additionalProperties: false, + properties: { + literalArray: { + type: "array", + items: { type: "string" }, + }, + path: { type: "string" }, + }, + }, + options: { + type: "array", + description: "An array of available options for the user to choose from.", + items: { + type: "object", + additionalProperties: false, + properties: { + label: { + type: "object", + description: + "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", + additionalProperties: false, + properties: { + literalString: { type: "string" }, + path: { type: "string" }, + }, + }, + value: { + type: "string", + description: + "The value to be associated with this option when selected.", + }, + }, + required: ["label", "value"], + }, + }, + maxAllowedSelections: { + type: "integer", + description: + "The maximum number of options that the user is allowed to select.", + }, + }, + required: ["selections", "options"], + }, + Slider: { + type: "object", + additionalProperties: false, + properties: { + value: { + type: "object", + description: + "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", + additionalProperties: false, + properties: { + literalNumber: { type: "number" }, + path: { type: "string" }, + }, + }, + minValue: { + type: "number", + description: "The minimum value of the slider.", + }, + maxValue: { + type: "number", + description: "The maximum value of the slider.", + }, + }, + required: ["value"], + }, + }, + }, + }, + required: ["id", "component"], + }, + }, + }, + required: ["surfaceId", "components"], + }, + dataModelUpdate: { + type: "object", + description: "Updates the data model for a surface.", + additionalProperties: false, + properties: { + surfaceId: { + type: "string", + description: + "The unique identifier for the UI surface this data model update applies to.", + }, + path: { + type: "string", + description: + "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced.", + }, + contents: { + type: "array", + description: + "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", + items: { + type: "object", + description: + "A single data entry. Exactly one 'value*' property should be provided alongside the key.", + additionalProperties: false, + properties: { + key: { + type: "string", + description: "The key for this data entry.", + }, + valueString: { type: "string" }, + valueNumber: { type: "number" }, + valueBoolean: { type: "boolean" }, + valueMap: { + description: "Represents a map as an adjacency list.", + type: "array", + items: { + type: "object", + description: + "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", + additionalProperties: false, + properties: { + key: { type: "string" }, + valueString: { type: "string" }, + valueNumber: { type: "number" }, + valueBoolean: { type: "boolean" }, + }, + required: ["key"], + }, + }, + }, + required: ["key"], + }, + }, + }, + required: ["contents", "surfaceId"], + }, + deleteSurface: { + type: "object", + description: "Signals the client to delete the surface identified by 'surfaceId'.", + additionalProperties: false, + properties: { + surfaceId: { + type: "string", + description: "The unique identifier for the UI surface to be deleted.", + }, + }, + required: ["surfaceId"], + }, + }, }; const Data = { - createSignalA2uiMessageProcessor: create, - A2uiMessageProcessor, - Guards: guards_exports + createSignalA2uiMessageProcessor: create, + A2uiMessageProcessor, + Guards: guards_exports, }; const Schemas = { A2UIClientEventMessage: server_to_client_with_standard_catalog_default }; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const t$1 = (t$7) => (e$14, o$15) => { - void 0 !== o$15 ? o$15.addInitializer(() => { - customElements.define(t$7, e$14); - }) : customElements.define(t$7, e$14); + void 0 !== o$15 + ? o$15.addInitializer(() => { + customElements.define(t$7, e$14); + }) + : customElements.define(t$7, e$14); }; /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const o$9 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - hasChanged: f$3 -}, r$7 = (t$7 = o$9, e$14, r$12) => { - const { kind: n$13, metadata: i$10 } = r$12; - let s$9 = globalThis.litPropertyMetadata.get(i$10); - if (void 0 === s$9 && globalThis.litPropertyMetadata.set(i$10, s$9 = new Map()), "setter" === n$13 && ((t$7 = Object.create(t$7)).wrapped = !0), s$9.set(r$12.name, t$7), "accessor" === n$13) { - const { name: o$15 } = r$12; - return { - set(r$13) { - const n$14 = e$14.get.call(this); - e$14.set.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13); - }, - init(e$15) { - return void 0 !== e$15 && this.C(o$15, void 0, t$7, e$15), e$15; - } - }; - } - if ("setter" === n$13) { - const { name: o$15 } = r$12; - return function(r$13) { - const n$14 = this[o$15]; - e$14.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13); - }; - } - throw Error("Unsupported decorator location: " + n$13); -}; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const o$9 = { + attribute: !0, + type: String, + converter: u$3, + reflect: !1, + hasChanged: f$3, + }, + r$7 = (t$7 = o$9, e$14, r$12) => { + const { kind: n$13, metadata: i$10 } = r$12; + let s$9 = globalThis.litPropertyMetadata.get(i$10); + if ( + (void 0 === s$9 && globalThis.litPropertyMetadata.set(i$10, (s$9 = new Map())), + "setter" === n$13 && ((t$7 = Object.create(t$7)).wrapped = !0), + s$9.set(r$12.name, t$7), + "accessor" === n$13) + ) { + const { name: o$15 } = r$12; + return { + set(r$13) { + const n$14 = e$14.get.call(this); + (e$14.set.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13)); + }, + init(e$15) { + return (void 0 !== e$15 && this.C(o$15, void 0, t$7, e$15), e$15); + }, + }; + } + if ("setter" === n$13) { + const { name: o$15 } = r$12; + return function (r$13) { + const n$14 = this[o$15]; + (e$14.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13)); + }; + } + throw Error("Unsupported decorator location: " + n$13); + }; function n$6(t$7) { - return (e$14, o$15) => "object" == typeof o$15 ? r$7(t$7, e$14, o$15) : ((t$8, e$15, o$16) => { - const r$12 = e$15.hasOwnProperty(o$16); - return e$15.constructor.createProperty(o$16, t$8), r$12 ? Object.getOwnPropertyDescriptor(e$15, o$16) : void 0; - })(t$7, e$14, o$15); + return (e$14, o$15) => + "object" == typeof o$15 + ? r$7(t$7, e$14, o$15) + : ((t$8, e$15, o$16) => { + const r$12 = e$15.hasOwnProperty(o$16); + return ( + e$15.constructor.createProperty(o$16, t$8), + r$12 ? Object.getOwnPropertyDescriptor(e$15, o$16) : void 0 + ); + })(t$7, e$14, o$15); } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function r$6(r$12) { - return n$6({ - ...r$12, - state: !0, - attribute: !1 - }); + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function r$6(r$12) { + return n$6({ + ...r$12, + state: !0, + attribute: !1, + }); } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function t(t$7) { - return (n$13, o$15) => { - const c$7 = "function" == typeof n$13 ? n$13 : n$13[o$15]; - Object.assign(c$7, t$7); - }; + return (n$13, o$15) => { + const c$7 = "function" == typeof n$13 ? n$13 : n$13[o$15]; + Object.assign(c$7, t$7); + }; } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Reflect.decorate && "object" != typeof t$7 && Object.defineProperty(e$14, t$7, c$7), c$7); + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const e$6 = (e$14, t$7, c$7) => ( + (c$7.configurable = !0), + (c$7.enumerable = !0), + Reflect.decorate && "object" != typeof t$7 && Object.defineProperty(e$14, t$7, c$7), + c$7 +); /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function e$5(e$14, r$12) { - return (n$13, s$9, i$10) => { - const o$15 = (t$7) => t$7.renderRoot?.querySelector(e$14) ?? null; - if (r$12) { - const { get: e$15, set: r$13 } = "object" == typeof s$9 ? n$13 : i$10 ?? (() => { - const t$7 = Symbol(); - return { - get() { - return this[t$7]; - }, - set(e$16) { - this[t$7] = e$16; - } - }; - })(); - return e$6(n$13, s$9, { get() { - let t$7 = e$15.call(this); - return void 0 === t$7 && (t$7 = o$15(this), (null !== t$7 || this.hasUpdated) && r$13.call(this, t$7)), t$7; - } }); - } - return e$6(n$13, s$9, { get() { - return o$15(this); - } }); - }; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function e$5(e$14, r$12) { + return (n$13, s$9, i$10) => { + const o$15 = (t$7) => t$7.renderRoot?.querySelector(e$14) ?? null; + if (r$12) { + const { get: e$15, set: r$13 } = + "object" == typeof s$9 + ? n$13 + : (i$10 ?? + (() => { + const t$7 = Symbol(); + return { + get() { + return this[t$7]; + }, + set(e$16) { + this[t$7] = e$16; + }, + }; + })()); + return e$6(n$13, s$9, { + get() { + let t$7 = e$15.call(this); + return ( + void 0 === t$7 && + ((t$7 = o$15(this)), (null !== t$7 || this.hasUpdated) && r$13.call(this, t$7)), + t$7 + ); + }, + }); + } + return e$6(n$13, s$9, { + get() { + return o$15(this); + }, + }); + }; } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ let e$4; function r$5(r$12) { - return (n$13, o$15) => e$6(n$13, o$15, { get() { - return (this.renderRoot ?? (e$4 ??= document.createDocumentFragment())).querySelectorAll(r$12); - } }); + return (n$13, o$15) => + e$6(n$13, o$15, { + get() { + return (this.renderRoot ?? (e$4 ??= document.createDocumentFragment())).querySelectorAll( + r$12, + ); + }, + }); } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function r$4(r$12) { - return (n$13, e$14) => e$6(n$13, e$14, { async get() { - return await this.updateComplete, this.renderRoot?.querySelector(r$12) ?? null; - } }); + return (n$13, e$14) => + e$6(n$13, e$14, { + async get() { + return (await this.updateComplete, this.renderRoot?.querySelector(r$12) ?? null); + }, + }); } /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function o$8(o$15) { - return (e$14, n$13) => { - const { slot: r$12, selector: s$9 } = o$15 ?? {}, c$7 = "slot" + (r$12 ? `[name=${r$12}]` : ":not([name])"); - return e$6(e$14, n$13, { get() { - const t$7 = this.renderRoot?.querySelector(c$7), e$15 = t$7?.assignedElements(o$15) ?? []; - return void 0 === s$9 ? e$15 : e$15.filter((t$8) => t$8.matches(s$9)); - } }); - }; + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function o$8(o$15) { + return (e$14, n$13) => { + const { slot: r$12, selector: s$9 } = o$15 ?? {}, + c$7 = "slot" + (r$12 ? `[name=${r$12}]` : ":not([name])"); + return e$6(e$14, n$13, { + get() { + const t$7 = this.renderRoot?.querySelector(c$7), + e$15 = t$7?.assignedElements(o$15) ?? []; + return void 0 === s$9 ? e$15 : e$15.filter((t$8) => t$8.matches(s$9)); + }, + }); + }; } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function n$5(n$13) { - return (o$15, r$12) => { - const { slot: e$14 } = n$13 ?? {}, s$9 = "slot" + (e$14 ? `[name=${e$14}]` : ":not([name])"); - return e$6(o$15, r$12, { get() { - const t$7 = this.renderRoot?.querySelector(s$9); - return t$7?.assignedNodes(n$13) ?? []; - } }); - }; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function n$5(n$13) { + return (o$15, r$12) => { + const { slot: e$14 } = n$13 ?? {}, + s$9 = "slot" + (e$14 ? `[name=${e$14}]` : ":not([name])"); + return e$6(o$15, r$12, { + get() { + const t$7 = this.renderRoot?.querySelector(s$9); + return t$7?.assignedNodes(n$13) ?? []; + }, + }); + }; } /** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ let i$2 = !1; + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ let i$2 = !1; const s$1 = new Signal.subtle.Watcher(() => { - i$2 || (i$2 = !0, queueMicrotask(() => { - i$2 = !1; - for (const t$7 of s$1.getPending()) t$7.get(); - s$1.watch(); - })); -}), h$3 = Symbol("SignalWatcherBrand"), e$3 = new FinalizationRegistry((i$10) => { - i$10.unwatch(...Signal.subtle.introspectSources(i$10)); -}), n$4 = new WeakMap(); + i$2 || + ((i$2 = !0), + queueMicrotask(() => { + i$2 = !1; + for (const t$7 of s$1.getPending()) t$7.get(); + s$1.watch(); + })); + }), + h$3 = Symbol("SignalWatcherBrand"), + e$3 = new FinalizationRegistry((i$10) => { + i$10.unwatch(...Signal.subtle.introspectSources(i$10)); + }), + n$4 = new WeakMap(); function o$7(i$10) { - return !0 === i$10[h$3] ? (console.warn("SignalWatcher should not be applied to the same class more than once."), i$10) : class extends i$10 { - constructor() { - super(...arguments), this._$St = new Map(), this._$So = new Signal.State(0), this._$Si = !1; - } - _$Sl() { - var t$7, i$11; - const s$9 = [], h$7 = []; - this._$St.forEach((t$8, i$12) => { - ((null == t$8 ? void 0 : t$8.beforeUpdate) ? s$9 : h$7).push(i$12); - }); - const e$14 = null === (t$7 = this.h) || void 0 === t$7 ? void 0 : t$7.getPending().filter((t$8) => t$8 !== this._$Su && !this._$St.has(t$8)); - s$9.forEach((t$8) => t$8.get()), null === (i$11 = this._$Su) || void 0 === i$11 || i$11.get(), e$14.forEach((t$8) => t$8.get()), h$7.forEach((t$8) => t$8.get()); - } - _$Sv() { - this.isUpdatePending || queueMicrotask(() => { - this.isUpdatePending || this._$Sl(); - }); - } - _$S_() { - if (void 0 !== this.h) return; - this._$Su = new Signal.Computed(() => { - this._$So.get(), super.performUpdate(); - }); - const i$11 = this.h = new Signal.subtle.Watcher(function() { - const t$7 = n$4.get(this); - void 0 !== t$7 && (!1 === t$7._$Si && (new Set(this.getPending()).has(t$7._$Su) ? t$7.requestUpdate() : t$7._$Sv()), this.watch()); - }); - n$4.set(i$11, this), e$3.register(this, i$11), i$11.watch(this._$Su), i$11.watch(...Array.from(this._$St).map(([t$7]) => t$7)); - } - _$Sp() { - if (void 0 === this.h) return; - let i$11 = !1; - this.h.unwatch(...Signal.subtle.introspectSources(this.h).filter((t$7) => { - var s$9; - const h$7 = !0 !== (null === (s$9 = this._$St.get(t$7)) || void 0 === s$9 ? void 0 : s$9.manualDispose); - return h$7 && this._$St.delete(t$7), i$11 || (i$11 = !h$7), h$7; - })), i$11 || (this._$Su = void 0, this.h = void 0, this._$St.clear()); - } - updateEffect(i$11, s$9) { - var h$7; - this._$S_(); - const e$14 = new Signal.Computed(() => { - i$11(); - }); - return this.h.watch(e$14), this._$St.set(e$14, s$9), null !== (h$7 = null == s$9 ? void 0 : s$9.beforeUpdate) && void 0 !== h$7 && h$7 ? Signal.subtle.untrack(() => e$14.get()) : this.updateComplete.then(() => Signal.subtle.untrack(() => e$14.get())), () => { - this._$St.delete(e$14), this.h.unwatch(e$14), !1 === this.isConnected && this._$Sp(); - }; - } - performUpdate() { - this.isUpdatePending && (this._$S_(), this._$Si = !0, this._$So.set(this._$So.get() + 1), this._$Si = !1, this._$Sl()); - } - connectedCallback() { - super.connectedCallback(), this.requestUpdate(); - } - disconnectedCallback() { - super.disconnectedCallback(), queueMicrotask(() => { - !1 === this.isConnected && this._$Sp(); - }); - } - }; + return !0 === i$10[h$3] + ? (console.warn("SignalWatcher should not be applied to the same class more than once."), i$10) + : class extends i$10 { + constructor() { + (super(...arguments), + (this._$St = new Map()), + (this._$So = new Signal.State(0)), + (this._$Si = !1)); + } + _$Sl() { + var t$7, i$11; + const s$9 = [], + h$7 = []; + this._$St.forEach((t$8, i$12) => { + ((null == t$8 ? void 0 : t$8.beforeUpdate) ? s$9 : h$7).push(i$12); + }); + const e$14 = + null === (t$7 = this.h) || void 0 === t$7 + ? void 0 + : t$7.getPending().filter((t$8) => t$8 !== this._$Su && !this._$St.has(t$8)); + (s$9.forEach((t$8) => t$8.get()), + null === (i$11 = this._$Su) || void 0 === i$11 || i$11.get(), + e$14.forEach((t$8) => t$8.get()), + h$7.forEach((t$8) => t$8.get())); + } + _$Sv() { + this.isUpdatePending || + queueMicrotask(() => { + this.isUpdatePending || this._$Sl(); + }); + } + _$S_() { + if (void 0 !== this.h) return; + this._$Su = new Signal.Computed(() => { + (this._$So.get(), super.performUpdate()); + }); + const i$11 = (this.h = new Signal.subtle.Watcher(function () { + const t$7 = n$4.get(this); + void 0 !== t$7 && + (!1 === t$7._$Si && + (new Set(this.getPending()).has(t$7._$Su) ? t$7.requestUpdate() : t$7._$Sv()), + this.watch()); + })); + (n$4.set(i$11, this), + e$3.register(this, i$11), + i$11.watch(this._$Su), + i$11.watch(...Array.from(this._$St).map(([t$7]) => t$7))); + } + _$Sp() { + if (void 0 === this.h) return; + let i$11 = !1; + (this.h.unwatch( + ...Signal.subtle.introspectSources(this.h).filter((t$7) => { + var s$9; + const h$7 = + !0 !== + (null === (s$9 = this._$St.get(t$7)) || void 0 === s$9 + ? void 0 + : s$9.manualDispose); + return (h$7 && this._$St.delete(t$7), i$11 || (i$11 = !h$7), h$7); + }), + ), + i$11 || ((this._$Su = void 0), (this.h = void 0), this._$St.clear())); + } + updateEffect(i$11, s$9) { + var h$7; + this._$S_(); + const e$14 = new Signal.Computed(() => { + i$11(); + }); + return ( + this.h.watch(e$14), + this._$St.set(e$14, s$9), + null !== (h$7 = null == s$9 ? void 0 : s$9.beforeUpdate) && void 0 !== h$7 && h$7 + ? Signal.subtle.untrack(() => e$14.get()) + : this.updateComplete.then(() => Signal.subtle.untrack(() => e$14.get())), + () => { + (this._$St.delete(e$14), + this.h.unwatch(e$14), + !1 === this.isConnected && this._$Sp()); + } + ); + } + performUpdate() { + this.isUpdatePending && + (this._$S_(), + (this._$Si = !0), + this._$So.set(this._$So.get() + 1), + (this._$Si = !1), + this._$Sl()); + } + connectedCallback() { + (super.connectedCallback(), this.requestUpdate()); + } + disconnectedCallback() { + (super.disconnectedCallback(), + queueMicrotask(() => { + !1 === this.isConnected && this._$Sp(); + })); + } + }; } /** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s = (i$10, t$7) => { - const e$14 = i$10._$AN; - if (void 0 === e$14) return !1; - for (const i$11 of e$14) i$11._$AO?.(t$7, !1), s(i$11, t$7); - return !0; -}, o$6 = (i$10) => { - let t$7, e$14; - do { - if (void 0 === (t$7 = i$10._$AM)) break; - e$14 = t$7._$AN, e$14.delete(i$10), i$10 = t$7; - } while (0 === e$14?.size); -}, r$3 = (i$10) => { - for (let t$7; t$7 = i$10._$AM; i$10 = t$7) { - let e$14 = t$7._$AN; - if (void 0 === e$14) t$7._$AN = e$14 = new Set(); - else if (e$14.has(i$10)) break; - e$14.add(i$10), c(t$7); - } -}; + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const s = (i$10, t$7) => { + const e$14 = i$10._$AN; + if (void 0 === e$14) return !1; + for (const i$11 of e$14) (i$11._$AO?.(t$7, !1), s(i$11, t$7)); + return !0; + }, + o$6 = (i$10) => { + let t$7, e$14; + do { + if (void 0 === (t$7 = i$10._$AM)) break; + ((e$14 = t$7._$AN), e$14.delete(i$10), (i$10 = t$7)); + } while (0 === e$14?.size); + }, + r$3 = (i$10) => { + for (let t$7; (t$7 = i$10._$AM); i$10 = t$7) { + let e$14 = t$7._$AN; + if (void 0 === e$14) t$7._$AN = e$14 = new Set(); + else if (e$14.has(i$10)) break; + (e$14.add(i$10), c(t$7)); + } + }; function h$2(i$10) { - void 0 !== this._$AN ? (o$6(this), this._$AM = i$10, r$3(this)) : this._$AM = i$10; + void 0 !== this._$AN ? (o$6(this), (this._$AM = i$10), r$3(this)) : (this._$AM = i$10); } function n$3(i$10, t$7 = !1, e$14 = 0) { - const r$12 = this._$AH, h$7 = this._$AN; - if (void 0 !== h$7 && 0 !== h$7.size) if (t$7) if (Array.isArray(r$12)) for (let i$11 = e$14; i$11 < r$12.length; i$11++) s(r$12[i$11], !1), o$6(r$12[i$11]); - else null != r$12 && (s(r$12, !1), o$6(r$12)); - else s(this, i$10); + const r$12 = this._$AH, + h$7 = this._$AN; + if (void 0 !== h$7 && 0 !== h$7.size) + if (t$7) + if (Array.isArray(r$12)) + for (let i$11 = e$14; i$11 < r$12.length; i$11++) (s(r$12[i$11], !1), o$6(r$12[i$11])); + else null != r$12 && (s(r$12, !1), o$6(r$12)); + else s(this, i$10); } const c = (i$10) => { - i$10.type == t$4.CHILD && (i$10._$AP ??= n$3, i$10._$AQ ??= h$2); + i$10.type == t$4.CHILD && ((i$10._$AP ??= n$3), (i$10._$AQ ??= h$2)); }; var f = class extends i$5 { - constructor() { - super(...arguments), this._$AN = void 0; - } - _$AT(i$10, t$7, e$14) { - super._$AT(i$10, t$7, e$14), r$3(this), this.isConnected = i$10._$AU; - } - _$AO(i$10, t$7 = !0) { - i$10 !== this.isConnected && (this.isConnected = i$10, i$10 ? this.reconnected?.() : this.disconnected?.()), t$7 && (s(this, i$10), o$6(this)); - } - setValue(t$7) { - if (r$8(this._$Ct)) this._$Ct._$AI(t$7, this); - else { - const i$10 = [...this._$Ct._$AH]; - i$10[this._$Ci] = t$7, this._$Ct._$AI(i$10, this, 0); - } - } - disconnected() {} - reconnected() {} + constructor() { + (super(...arguments), (this._$AN = void 0)); + } + _$AT(i$10, t$7, e$14) { + (super._$AT(i$10, t$7, e$14), r$3(this), (this.isConnected = i$10._$AU)); + } + _$AO(i$10, t$7 = !0) { + (i$10 !== this.isConnected && + ((this.isConnected = i$10), i$10 ? this.reconnected?.() : this.disconnected?.()), + t$7 && (s(this, i$10), o$6(this))); + } + setValue(t$7) { + if (r$8(this._$Ct)) this._$Ct._$AI(t$7, this); + else { + const i$10 = [...this._$Ct._$AH]; + ((i$10[this._$Ci] = t$7), this._$Ct._$AI(i$10, this, 0)); + } + } + disconnected() {} + reconnected() {} }; /** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ let o$5 = !1; const n$2 = new Signal.subtle.Watcher(async () => { - o$5 || (o$5 = !0, queueMicrotask(() => { - o$5 = !1; - for (const i$10 of n$2.getPending()) i$10.get(); - n$2.watch(); - })); + o$5 || + ((o$5 = !0), + queueMicrotask(() => { + o$5 = !1; + for (const i$10 of n$2.getPending()) i$10.get(); + n$2.watch(); + })); }); var r$2 = class extends f { - _$S_() { - var i$10, t$7; - void 0 === this._$Sm && (this._$Sj = new Signal.Computed(() => { - var i$11; - const t$8 = null === (i$11 = this._$SW) || void 0 === i$11 ? void 0 : i$11.get(); - return this.setValue(t$8), t$8; - }), this._$Sm = null !== (t$7 = null === (i$10 = this._$Sk) || void 0 === i$10 ? void 0 : i$10.h) && void 0 !== t$7 ? t$7 : n$2, this._$Sm.watch(this._$Sj), Signal.subtle.untrack(() => { - var i$11; - return null === (i$11 = this._$Sj) || void 0 === i$11 ? void 0 : i$11.get(); - })); - } - _$Sp() { - void 0 !== this._$Sm && (this._$Sm.unwatch(this._$SW), this._$Sm = void 0); - } - render(i$10) { - return Signal.subtle.untrack(() => i$10.get()); - } - update(i$10, [t$7]) { - var o$15, n$13; - return null !== (o$15 = this._$Sk) && void 0 !== o$15 || (this._$Sk = null === (n$13 = i$10.options) || void 0 === n$13 ? void 0 : n$13.host), t$7 !== this._$SW && void 0 !== this._$SW && this._$Sp(), this._$SW = t$7, this._$S_(), Signal.subtle.untrack(() => this._$SW.get()); - } - disconnected() { - this._$Sp(); - } - reconnected() { - this._$S_(); - } + _$S_() { + var i$10, t$7; + void 0 === this._$Sm && + ((this._$Sj = new Signal.Computed(() => { + var i$11; + const t$8 = null === (i$11 = this._$SW) || void 0 === i$11 ? void 0 : i$11.get(); + return (this.setValue(t$8), t$8); + })), + (this._$Sm = + null !== (t$7 = null === (i$10 = this._$Sk) || void 0 === i$10 ? void 0 : i$10.h) && + void 0 !== t$7 + ? t$7 + : n$2), + this._$Sm.watch(this._$Sj), + Signal.subtle.untrack(() => { + var i$11; + return null === (i$11 = this._$Sj) || void 0 === i$11 ? void 0 : i$11.get(); + })); + } + _$Sp() { + void 0 !== this._$Sm && (this._$Sm.unwatch(this._$SW), (this._$Sm = void 0)); + } + render(i$10) { + return Signal.subtle.untrack(() => i$10.get()); + } + update(i$10, [t$7]) { + var o$15, n$13; + return ( + (null !== (o$15 = this._$Sk) && void 0 !== o$15) || + (this._$Sk = null === (n$13 = i$10.options) || void 0 === n$13 ? void 0 : n$13.host), + t$7 !== this._$SW && void 0 !== this._$SW && this._$Sp(), + (this._$SW = t$7), + this._$S_(), + Signal.subtle.untrack(() => this._$SW.get()) + ); + } + disconnected() { + this._$Sp(); + } + reconnected() { + this._$S_(); + } }; const h$1 = e$10(r$2); /** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const m = (o$15) => (t$7, ...m$3) => o$15(t$7, ...m$3.map((o$16) => o$16 instanceof Signal.State || o$16 instanceof Signal.Computed ? h$1(o$16) : o$16)), l$1 = m(b), r$1 = m(w); + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const m = + (o$15) => + (t$7, ...m$3) => + o$15( + t$7, + ...m$3.map((o$16) => + o$16 instanceof Signal.State || o$16 instanceof Signal.Computed ? h$1(o$16) : o$16, + ), + ), + l$1 = m(b), + r$1 = m(w); /** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const l = Signal.State, o$4 = Signal.Computed, r = (l$5, o$15) => new Signal.State(l$5, o$15), i$1 = (l$5, o$15) => new Signal.Computed(l$5, o$15); + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const l = Signal.State, + o$4 = Signal.Computed, + r = (l$5, o$15) => new Signal.State(l$5, o$15), + i$1 = (l$5, o$15) => new Signal.Computed(l$5, o$15); /** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function* o$3(o$15, f$4) { - if (void 0 !== o$15) { - let i$10 = 0; - for (const t$7 of o$15) yield f$4(t$7, i$10++); - } + if (void 0 !== o$15) { + let i$10 = 0; + for (const t$7 of o$15) yield f$4(t$7, i$10++); + } } let pending = false; let watcher = new Signal.subtle.Watcher(() => { - if (!pending) { - pending = true; - queueMicrotask(() => { - pending = false; - flushPending(); - }); - } + if (!pending) { + pending = true; + queueMicrotask(() => { + pending = false; + flushPending(); + }); + } }); function flushPending() { - for (const signal of watcher.getPending()) { - signal.get(); - } - watcher.watch(); + for (const signal of watcher.getPending()) { + signal.get(); + } + watcher.watch(); } /** -* ⚠️ WARNING: Nothing unwatches ⚠️ -* This will produce a memory leak. -*/ + * ⚠️ WARNING: Nothing unwatches ⚠️ + * This will produce a memory leak. + */ function effect(cb) { - let c$7 = new Signal.Computed(() => cb()); - watcher.watch(c$7); - c$7.get(); - return () => { - watcher.unwatch(c$7); - }; + let c$7 = new Signal.Computed(() => cb()); + watcher.watch(c$7); + c$7.get(); + return () => { + watcher.unwatch(c$7); + }; } const themeContext = n$7("A2UITheme"); @@ -4287,371 +5037,469 @@ const themeContext = n$7("A2UITheme"); const structuralStyles = r$11(structuralStyles$1); var ComponentRegistry = class { - constructor() { - this.registry = new Map(); - } - register(typeName, constructor, tagName) { - if (!/^[a-zA-Z0-9]+$/.test(typeName)) { - throw new Error(`[Registry] Invalid typeName '${typeName}'. Must be alphanumeric.`); - } - this.registry.set(typeName, constructor); - const actualTagName = tagName || `a2ui-custom-${typeName.toLowerCase()}`; - const existingName = customElements.getName(constructor); - if (existingName) { - if (existingName !== actualTagName) { - throw new Error(`Component ${typeName} is already registered as ${existingName}, but requested as ${actualTagName}.`); - } - return; - } - if (!customElements.get(actualTagName)) { - customElements.define(actualTagName, constructor); - } - } - get(typeName) { - return this.registry.get(typeName); - } + constructor() { + this.registry = new Map(); + } + register(typeName, constructor, tagName) { + if (!/^[a-zA-Z0-9]+$/.test(typeName)) { + throw new Error(`[Registry] Invalid typeName '${typeName}'. Must be alphanumeric.`); + } + this.registry.set(typeName, constructor); + const actualTagName = tagName || `a2ui-custom-${typeName.toLowerCase()}`; + const existingName = customElements.getName(constructor); + if (existingName) { + if (existingName !== actualTagName) { + throw new Error( + `Component ${typeName} is already registered as ${existingName}, but requested as ${actualTagName}.`, + ); + } + return; + } + if (!customElements.get(actualTagName)) { + customElements.define(actualTagName, constructor); + } + } + get(typeName) { + return this.registry.get(typeName); + } }; const componentRegistry = new ComponentRegistry(); -var __runInitializers$19 = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; -var __esDecorate$19 = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; +var __runInitializers$19 = + (void 0 && (void 0).__runInitializers) || + function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i$10 = 0; i$10 < initializers.length; i$10++) { + value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); + } + return useValue ? value : void 0; + }; +var __esDecorate$19 = + (void 0 && (void 0).__esDecorate) || + function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f$4) { + if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); + return f$4; + } + var kind = contextIn.kind, + key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? (contextIn["static"] ? ctor : ctor.prototype) : null; + var descriptor = + descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _$1, + done = false; + for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { + var context = {}; + for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; + for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; + context.addInitializer = function (f$4) { + if (done) throw new TypeError("Cannot add initializers after decoration has completed"); + extraInitializers.push(accept(f$4 || null)); + }; + var result = (0, decorators[i$10])( + kind === "accessor" + ? { + get: descriptor.get, + set: descriptor.set, + } + : descriptor[key], + context, + ); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if ((_$1 = accept(result.get))) descriptor.get = _$1; + if ((_$1 = accept(result.set))) descriptor.set = _$1; + if ((_$1 = accept(result.init))) initializers.unshift(_$1); + } else if ((_$1 = accept(result))) { + if (kind === "field") initializers.unshift(_$1); + else descriptor[key] = _$1; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; + }; let Root = (() => { - let _classDecorators = [t$1("a2ui-root")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = o$7(i$6); - let _instanceExtraInitializers = []; - let _surfaceId_decorators; - let _surfaceId_initializers = []; - let _surfaceId_extraInitializers = []; - let _component_decorators; - let _component_initializers = []; - let _component_extraInitializers = []; - let _theme_decorators; - let _theme_initializers = []; - let _theme_extraInitializers = []; - let _childComponents_decorators; - let _childComponents_initializers = []; - let _childComponents_extraInitializers = []; - let _processor_decorators; - let _processor_initializers = []; - let _processor_extraInitializers = []; - let _dataContextPath_decorators; - let _dataContextPath_initializers = []; - let _dataContextPath_extraInitializers = []; - let _enableCustomElements_decorators; - let _enableCustomElements_initializers = []; - let _enableCustomElements_extraInitializers = []; - let _set_weight_decorators; - var Root$1 = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _surfaceId_decorators = [n$6()]; - _component_decorators = [n$6()]; - _theme_decorators = [c$1({ context: themeContext })]; - _childComponents_decorators = [n$6({ attribute: false })]; - _processor_decorators = [n$6({ attribute: false })]; - _dataContextPath_decorators = [n$6()]; - _enableCustomElements_decorators = [n$6()]; - _set_weight_decorators = [n$6()]; - __esDecorate$19(this, null, _surfaceId_decorators, { - kind: "accessor", - name: "surfaceId", - static: false, - private: false, - access: { - has: (obj) => "surfaceId" in obj, - get: (obj) => obj.surfaceId, - set: (obj, value) => { - obj.surfaceId = value; - } - }, - metadata: _metadata - }, _surfaceId_initializers, _surfaceId_extraInitializers); - __esDecorate$19(this, null, _component_decorators, { - kind: "accessor", - name: "component", - static: false, - private: false, - access: { - has: (obj) => "component" in obj, - get: (obj) => obj.component, - set: (obj, value) => { - obj.component = value; - } - }, - metadata: _metadata - }, _component_initializers, _component_extraInitializers); - __esDecorate$19(this, null, _theme_decorators, { - kind: "accessor", - name: "theme", - static: false, - private: false, - access: { - has: (obj) => "theme" in obj, - get: (obj) => obj.theme, - set: (obj, value) => { - obj.theme = value; - } - }, - metadata: _metadata - }, _theme_initializers, _theme_extraInitializers); - __esDecorate$19(this, null, _childComponents_decorators, { - kind: "accessor", - name: "childComponents", - static: false, - private: false, - access: { - has: (obj) => "childComponents" in obj, - get: (obj) => obj.childComponents, - set: (obj, value) => { - obj.childComponents = value; - } - }, - metadata: _metadata - }, _childComponents_initializers, _childComponents_extraInitializers); - __esDecorate$19(this, null, _processor_decorators, { - kind: "accessor", - name: "processor", - static: false, - private: false, - access: { - has: (obj) => "processor" in obj, - get: (obj) => obj.processor, - set: (obj, value) => { - obj.processor = value; - } - }, - metadata: _metadata - }, _processor_initializers, _processor_extraInitializers); - __esDecorate$19(this, null, _dataContextPath_decorators, { - kind: "accessor", - name: "dataContextPath", - static: false, - private: false, - access: { - has: (obj) => "dataContextPath" in obj, - get: (obj) => obj.dataContextPath, - set: (obj, value) => { - obj.dataContextPath = value; - } - }, - metadata: _metadata - }, _dataContextPath_initializers, _dataContextPath_extraInitializers); - __esDecorate$19(this, null, _enableCustomElements_decorators, { - kind: "accessor", - name: "enableCustomElements", - static: false, - private: false, - access: { - has: (obj) => "enableCustomElements" in obj, - get: (obj) => obj.enableCustomElements, - set: (obj, value) => { - obj.enableCustomElements = value; - } - }, - metadata: _metadata - }, _enableCustomElements_initializers, _enableCustomElements_extraInitializers); - __esDecorate$19(this, null, _set_weight_decorators, { - kind: "setter", - name: "weight", - static: false, - private: false, - access: { - has: (obj) => "weight" in obj, - set: (obj, value) => { - obj.weight = value; - } - }, - metadata: _metadata - }, null, _instanceExtraInitializers); - __esDecorate$19(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Root$1 = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #surfaceId_accessor_storage = (__runInitializers$19(this, _instanceExtraInitializers), __runInitializers$19(this, _surfaceId_initializers, null)); - get surfaceId() { - return this.#surfaceId_accessor_storage; - } - set surfaceId(value) { - this.#surfaceId_accessor_storage = value; - } - #component_accessor_storage = (__runInitializers$19(this, _surfaceId_extraInitializers), __runInitializers$19(this, _component_initializers, null)); - get component() { - return this.#component_accessor_storage; - } - set component(value) { - this.#component_accessor_storage = value; - } - #theme_accessor_storage = (__runInitializers$19(this, _component_extraInitializers), __runInitializers$19(this, _theme_initializers, void 0)); - get theme() { - return this.#theme_accessor_storage; - } - set theme(value) { - this.#theme_accessor_storage = value; - } - #childComponents_accessor_storage = (__runInitializers$19(this, _theme_extraInitializers), __runInitializers$19(this, _childComponents_initializers, null)); - get childComponents() { - return this.#childComponents_accessor_storage; - } - set childComponents(value) { - this.#childComponents_accessor_storage = value; - } - #processor_accessor_storage = (__runInitializers$19(this, _childComponents_extraInitializers), __runInitializers$19(this, _processor_initializers, null)); - get processor() { - return this.#processor_accessor_storage; - } - set processor(value) { - this.#processor_accessor_storage = value; - } - #dataContextPath_accessor_storage = (__runInitializers$19(this, _processor_extraInitializers), __runInitializers$19(this, _dataContextPath_initializers, "")); - get dataContextPath() { - return this.#dataContextPath_accessor_storage; - } - set dataContextPath(value) { - this.#dataContextPath_accessor_storage = value; - } - #enableCustomElements_accessor_storage = (__runInitializers$19(this, _dataContextPath_extraInitializers), __runInitializers$19(this, _enableCustomElements_initializers, false)); - get enableCustomElements() { - return this.#enableCustomElements_accessor_storage; - } - set enableCustomElements(value) { - this.#enableCustomElements_accessor_storage = value; - } - set weight(weight) { - this.#weight = weight; - this.style.setProperty("--weight", `${weight}`); - } - get weight() { - return this.#weight; - } - #weight = (__runInitializers$19(this, _enableCustomElements_extraInitializers), 1); - static { - this.styles = [structuralStyles, i$9` + let _classDecorators = [t$1("a2ui-root")]; + let _classDescriptor; + let _classExtraInitializers = []; + let _classThis; + let _classSuper = o$7(i$6); + let _instanceExtraInitializers = []; + let _surfaceId_decorators; + let _surfaceId_initializers = []; + let _surfaceId_extraInitializers = []; + let _component_decorators; + let _component_initializers = []; + let _component_extraInitializers = []; + let _theme_decorators; + let _theme_initializers = []; + let _theme_extraInitializers = []; + let _childComponents_decorators; + let _childComponents_initializers = []; + let _childComponents_extraInitializers = []; + let _processor_decorators; + let _processor_initializers = []; + let _processor_extraInitializers = []; + let _dataContextPath_decorators; + let _dataContextPath_initializers = []; + let _dataContextPath_extraInitializers = []; + let _enableCustomElements_decorators; + let _enableCustomElements_initializers = []; + let _enableCustomElements_extraInitializers = []; + let _set_weight_decorators; + var Root$1 = class extends _classSuper { + static { + _classThis = this; + } + static { + const _metadata = + typeof Symbol === "function" && Symbol.metadata + ? Object.create(_classSuper[Symbol.metadata] ?? null) + : void 0; + _surfaceId_decorators = [n$6()]; + _component_decorators = [n$6()]; + _theme_decorators = [c$1({ context: themeContext })]; + _childComponents_decorators = [n$6({ attribute: false })]; + _processor_decorators = [n$6({ attribute: false })]; + _dataContextPath_decorators = [n$6()]; + _enableCustomElements_decorators = [n$6()]; + _set_weight_decorators = [n$6()]; + __esDecorate$19( + this, + null, + _surfaceId_decorators, + { + kind: "accessor", + name: "surfaceId", + static: false, + private: false, + access: { + has: (obj) => "surfaceId" in obj, + get: (obj) => obj.surfaceId, + set: (obj, value) => { + obj.surfaceId = value; + }, + }, + metadata: _metadata, + }, + _surfaceId_initializers, + _surfaceId_extraInitializers, + ); + __esDecorate$19( + this, + null, + _component_decorators, + { + kind: "accessor", + name: "component", + static: false, + private: false, + access: { + has: (obj) => "component" in obj, + get: (obj) => obj.component, + set: (obj, value) => { + obj.component = value; + }, + }, + metadata: _metadata, + }, + _component_initializers, + _component_extraInitializers, + ); + __esDecorate$19( + this, + null, + _theme_decorators, + { + kind: "accessor", + name: "theme", + static: false, + private: false, + access: { + has: (obj) => "theme" in obj, + get: (obj) => obj.theme, + set: (obj, value) => { + obj.theme = value; + }, + }, + metadata: _metadata, + }, + _theme_initializers, + _theme_extraInitializers, + ); + __esDecorate$19( + this, + null, + _childComponents_decorators, + { + kind: "accessor", + name: "childComponents", + static: false, + private: false, + access: { + has: (obj) => "childComponents" in obj, + get: (obj) => obj.childComponents, + set: (obj, value) => { + obj.childComponents = value; + }, + }, + metadata: _metadata, + }, + _childComponents_initializers, + _childComponents_extraInitializers, + ); + __esDecorate$19( + this, + null, + _processor_decorators, + { + kind: "accessor", + name: "processor", + static: false, + private: false, + access: { + has: (obj) => "processor" in obj, + get: (obj) => obj.processor, + set: (obj, value) => { + obj.processor = value; + }, + }, + metadata: _metadata, + }, + _processor_initializers, + _processor_extraInitializers, + ); + __esDecorate$19( + this, + null, + _dataContextPath_decorators, + { + kind: "accessor", + name: "dataContextPath", + static: false, + private: false, + access: { + has: (obj) => "dataContextPath" in obj, + get: (obj) => obj.dataContextPath, + set: (obj, value) => { + obj.dataContextPath = value; + }, + }, + metadata: _metadata, + }, + _dataContextPath_initializers, + _dataContextPath_extraInitializers, + ); + __esDecorate$19( + this, + null, + _enableCustomElements_decorators, + { + kind: "accessor", + name: "enableCustomElements", + static: false, + private: false, + access: { + has: (obj) => "enableCustomElements" in obj, + get: (obj) => obj.enableCustomElements, + set: (obj, value) => { + obj.enableCustomElements = value; + }, + }, + metadata: _metadata, + }, + _enableCustomElements_initializers, + _enableCustomElements_extraInitializers, + ); + __esDecorate$19( + this, + null, + _set_weight_decorators, + { + kind: "setter", + name: "weight", + static: false, + private: false, + access: { + has: (obj) => "weight" in obj, + set: (obj, value) => { + obj.weight = value; + }, + }, + metadata: _metadata, + }, + null, + _instanceExtraInitializers, + ); + __esDecorate$19( + null, + (_classDescriptor = { value: _classThis }), + _classDecorators, + { + kind: "class", + name: _classThis.name, + metadata: _metadata, + }, + null, + _classExtraInitializers, + ); + Root$1 = _classThis = _classDescriptor.value; + if (_metadata) + Object.defineProperty(_classThis, Symbol.metadata, { + enumerable: true, + configurable: true, + writable: true, + value: _metadata, + }); + } + #surfaceId_accessor_storage = + (__runInitializers$19(this, _instanceExtraInitializers), + __runInitializers$19(this, _surfaceId_initializers, null)); + get surfaceId() { + return this.#surfaceId_accessor_storage; + } + set surfaceId(value) { + this.#surfaceId_accessor_storage = value; + } + #component_accessor_storage = + (__runInitializers$19(this, _surfaceId_extraInitializers), + __runInitializers$19(this, _component_initializers, null)); + get component() { + return this.#component_accessor_storage; + } + set component(value) { + this.#component_accessor_storage = value; + } + #theme_accessor_storage = + (__runInitializers$19(this, _component_extraInitializers), + __runInitializers$19(this, _theme_initializers, void 0)); + get theme() { + return this.#theme_accessor_storage; + } + set theme(value) { + this.#theme_accessor_storage = value; + } + #childComponents_accessor_storage = + (__runInitializers$19(this, _theme_extraInitializers), + __runInitializers$19(this, _childComponents_initializers, null)); + get childComponents() { + return this.#childComponents_accessor_storage; + } + set childComponents(value) { + this.#childComponents_accessor_storage = value; + } + #processor_accessor_storage = + (__runInitializers$19(this, _childComponents_extraInitializers), + __runInitializers$19(this, _processor_initializers, null)); + get processor() { + return this.#processor_accessor_storage; + } + set processor(value) { + this.#processor_accessor_storage = value; + } + #dataContextPath_accessor_storage = + (__runInitializers$19(this, _processor_extraInitializers), + __runInitializers$19(this, _dataContextPath_initializers, "")); + get dataContextPath() { + return this.#dataContextPath_accessor_storage; + } + set dataContextPath(value) { + this.#dataContextPath_accessor_storage = value; + } + #enableCustomElements_accessor_storage = + (__runInitializers$19(this, _dataContextPath_extraInitializers), + __runInitializers$19(this, _enableCustomElements_initializers, false)); + get enableCustomElements() { + return this.#enableCustomElements_accessor_storage; + } + set enableCustomElements(value) { + this.#enableCustomElements_accessor_storage = value; + } + set weight(weight) { + this.#weight = weight; + this.style.setProperty("--weight", `${weight}`); + } + get weight() { + return this.#weight; + } + #weight = (__runInitializers$19(this, _enableCustomElements_extraInitializers), 1); + static { + this.styles = [ + structuralStyles, + i$9` :host { display: flex; flex-direction: column; gap: 8px; max-height: 80%; } - `]; - } - /** - * Holds the cleanup function for our effect. - * We need this to stop the effect when the component is disconnected. - */ - #lightDomEffectDisposer = null; - willUpdate(changedProperties) { - if (changedProperties.has("childComponents")) { - if (this.#lightDomEffectDisposer) { - this.#lightDomEffectDisposer(); - } - this.#lightDomEffectDisposer = effect(() => { - const allChildren = this.childComponents ?? null; - const lightDomTemplate = this.renderComponentTree(allChildren); - D(lightDomTemplate, this, { host: this }); - }); - } - } - /** - * Clean up the effect when the component is removed from the DOM. - */ - disconnectedCallback() { - super.disconnectedCallback(); - if (this.#lightDomEffectDisposer) { - this.#lightDomEffectDisposer(); - } - } - /** - * Turns the SignalMap into a renderable TemplateResult for Lit. - */ - renderComponentTree(components) { - if (!components) { - return A; - } - if (!Array.isArray(components)) { - return A; - } - return b` ${o$3(components, (component) => { - if (this.enableCustomElements) { - const registeredCtor = componentRegistry.get(component.type); - const elCtor = registeredCtor || customElements.get(component.type); - if (elCtor) { - const node = component; - const el = new elCtor(); - el.id = node.id; - if (node.slotName) { - el.slot = node.slotName; - } - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) { - el[prop] = val; - } - return b`${el}`; - } - } - switch (component.type) { - case "List": { - const node = component; - const childComponents = node.properties.children; - return b` { + const allChildren = this.childComponents ?? null; + const lightDomTemplate = this.renderComponentTree(allChildren); + D(lightDomTemplate, this, { host: this }); + }); + } + } + /** + * Clean up the effect when the component is removed from the DOM. + */ + disconnectedCallback() { + super.disconnectedCallback(); + if (this.#lightDomEffectDisposer) { + this.#lightDomEffectDisposer(); + } + } + /** + * Turns the SignalMap into a renderable TemplateResult for Lit. + */ + renderComponentTree(components) { + if (!components) { + return A; + } + if (!Array.isArray(components)) { + return A; + } + return b` ${o$3(components, (component) => { + if (this.enableCustomElements) { + const registeredCtor = componentRegistry.get(component.type); + const elCtor = registeredCtor || customElements.get(component.type); + if (elCtor) { + const node = component; + const el = new elCtor(); + el.id = node.id; + if (node.slotName) { + el.slot = node.slotName; + } + el.component = node; + el.weight = node.weight ?? "initial"; + el.processor = this.processor; + el.surfaceId = this.surfaceId; + el.dataContextPath = node.dataContextPath ?? "/"; + for (const [prop, val] of Object.entries(component.properties)) { + el[prop] = val; + } + return b`${el}`; + } + } + switch (component.type) { + case "List": { + const node = component; + const childComponents = node.properties.children; + return b` { .childComponents=${childComponents} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Card": { - const node = component; - let childComponents = node.properties.children; - if (!childComponents && node.properties.child) { - childComponents = [node.properties.child]; - } - return b` { .dataContextPath=${node.dataContextPath ?? ""} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Column": { - const node = component; - return b` { .distribution=${node.properties.distribution ?? "start"} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Row": { - const node = component; - return b` { .distribution=${node.properties.distribution ?? "start"} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Image": { - const node = component; - return b` { .fit=${node.properties.fit} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Icon": { - const node = component; - return b` { .dataContextPath=${node.dataContextPath ?? ""} .enableCustomElements=${this.enableCustomElements} >`; - } - case "AudioPlayer": { - const node = component; - return b` { .dataContextPath=${node.dataContextPath ?? ""} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Button": { - const node = component; - return b` { .childComponents=${[node.properties.child]} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Text": { - const node = component; - return b` { .usageHint=${node.properties.usageHint} .enableCustomElements=${this.enableCustomElements} >`; - } - case "CheckBox": { - const node = component; - return b` { .value=${node.properties.value} .enableCustomElements=${this.enableCustomElements} >`; - } - case "DateTimeInput": { - const node = component; - return b` { .value=${node.properties.value} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Divider": { - const node = component; - return b` { .color=${node.properties.color} .enableCustomElements=${this.enableCustomElements} >`; - } - case "MultipleChoice": { - const node = component; - return b` { .selections=${node.properties.selections} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Slider": { - const node = component; - return b` { .maxValue=${node.properties.maxValue} .enableCustomElements=${this.enableCustomElements} >`; - } - case "TextField": { - const node = component; - return b` { .validationRegexp=${node.properties.validationRegexp} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Video": { - const node = component; - return b` { .url=${node.properties.url} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Tabs": { - const node = component; - const titles = []; - const childComponents = []; - if (node.properties.tabItems) { - for (const item of node.properties.tabItems) { - titles.push(item.title); - childComponents.push(item.child); - } - } - return b` { .childComponents=${childComponents} .enableCustomElements=${this.enableCustomElements} >`; - } - case "Modal": { - const node = component; - const childComponents = [node.properties.entryPointChild, node.properties.contentChild]; - node.properties.entryPointChild.slotName = "entry"; - return b` { .childComponents=${childComponents} .enableCustomElements=${this.enableCustomElements} >`; - } - default: { - return this.renderCustomComponent(component); - } - } - })}`; - } - renderCustomComponent(component) { - if (!this.enableCustomElements) { - return; - } - const node = component; - const registeredCtor = componentRegistry.get(component.type); - const elCtor = registeredCtor || customElements.get(component.type); - if (!elCtor) { - return b`Unknown element ${component.type}`; - } - const el = new elCtor(); - el.id = node.id; - if (node.slotName) { - el.slot = node.slotName; - } - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) { - el[prop] = val; - } - return b`${el}`; - } - render() { - return b``; - } - static { - __runInitializers$19(_classThis, _classExtraInitializers); - } - }; - return Root$1 = _classThis; + } + default: { + return this.renderCustomComponent(component); + } + } + })}`; + } + renderCustomComponent(component) { + if (!this.enableCustomElements) { + return; + } + const node = component; + const registeredCtor = componentRegistry.get(component.type); + const elCtor = registeredCtor || customElements.get(component.type); + if (!elCtor) { + return b`Unknown element ${component.type}`; + } + const el = new elCtor(); + el.id = node.id; + if (node.slotName) { + el.slot = node.slotName; + } + el.component = node; + el.weight = node.weight ?? "initial"; + el.processor = this.processor; + el.surfaceId = this.surfaceId; + el.dataContextPath = node.dataContextPath ?? "/"; + for (const [prop, val] of Object.entries(component.properties)) { + el[prop] = val; + } + return b`${el}`; + } + render() { + return b``; + } + static { + __runInitializers$19(_classThis, _classExtraInitializers); + } + }; + return (Root$1 = _classThis); })(); /** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const e$2 = e$10(class extends i$5 { - constructor(t$7) { - if (super(t$7), t$7.type !== t$4.ATTRIBUTE || "class" !== t$7.name || t$7.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute."); - } - render(t$7) { - return " " + Object.keys(t$7).filter((s$9) => t$7[s$9]).join(" ") + " "; - } - update(s$9, [i$10]) { - if (void 0 === this.st) { - this.st = new Set(), void 0 !== s$9.strings && (this.nt = new Set(s$9.strings.join(" ").split(/\s/).filter((t$7) => "" !== t$7))); - for (const t$7 in i$10) i$10[t$7] && !this.nt?.has(t$7) && this.st.add(t$7); - return this.render(i$10); - } - const r$12 = s$9.element.classList; - for (const t$7 of this.st) t$7 in i$10 || (r$12.remove(t$7), this.st.delete(t$7)); - for (const t$7 in i$10) { - const s$10 = !!i$10[t$7]; - s$10 === this.st.has(t$7) || this.nt?.has(t$7) || (s$10 ? (r$12.add(t$7), this.st.add(t$7)) : (r$12.remove(t$7), this.st.delete(t$7))); - } - return E; - } -}); + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const e$2 = e$10( + class extends i$5 { + constructor(t$7) { + if ( + (super(t$7), t$7.type !== t$4.ATTRIBUTE || "class" !== t$7.name || t$7.strings?.length > 2) + ) + throw Error( + "`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.", + ); + } + render(t$7) { + return ( + " " + + Object.keys(t$7) + .filter((s$9) => t$7[s$9]) + .join(" ") + + " " + ); + } + update(s$9, [i$10]) { + if (void 0 === this.st) { + ((this.st = new Set()), + void 0 !== s$9.strings && + (this.nt = new Set( + s$9.strings + .join(" ") + .split(/\s/) + .filter((t$7) => "" !== t$7), + ))); + for (const t$7 in i$10) i$10[t$7] && !this.nt?.has(t$7) && this.st.add(t$7); + return this.render(i$10); + } + const r$12 = s$9.element.classList; + for (const t$7 of this.st) t$7 in i$10 || (r$12.remove(t$7), this.st.delete(t$7)); + for (const t$7 in i$10) { + const s$10 = !!i$10[t$7]; + s$10 === this.st.has(t$7) || + this.nt?.has(t$7) || + (s$10 ? (r$12.add(t$7), this.st.add(t$7)) : (r$12.remove(t$7), this.st.delete(t$7))); + } + return E; + } + }, +); /** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const n$1 = "important", i = " !" + n$1, o$2 = e$10(class extends i$5 { - constructor(t$7) { - if (super(t$7), t$7.type !== t$4.ATTRIBUTE || "style" !== t$7.name || t$7.strings?.length > 2) throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute."); - } - render(t$7) { - return Object.keys(t$7).reduce((e$14, r$12) => { - const s$9 = t$7[r$12]; - return null == s$9 ? e$14 : e$14 + `${r$12 = r$12.includes("-") ? r$12 : r$12.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase()}:${s$9};`; - }, ""); - } - update(e$14, [r$12]) { - const { style: s$9 } = e$14.element; - if (void 0 === this.ft) return this.ft = new Set(Object.keys(r$12)), this.render(r$12); - for (const t$7 of this.ft) null == r$12[t$7] && (this.ft.delete(t$7), t$7.includes("-") ? s$9.removeProperty(t$7) : s$9[t$7] = null); - for (const t$7 in r$12) { - const e$15 = r$12[t$7]; - if (null != e$15) { - this.ft.add(t$7); - const r$13 = "string" == typeof e$15 && e$15.endsWith(i); - t$7.includes("-") || r$13 ? s$9.setProperty(t$7, r$13 ? e$15.slice(0, -11) : e$15, r$13 ? n$1 : "") : s$9[t$7] = e$15; - } - } - return E; - } -}); + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ const n$1 = "important", + i = " !" + n$1, + o$2 = e$10( + class extends i$5 { + constructor(t$7) { + if ( + (super(t$7), + t$7.type !== t$4.ATTRIBUTE || "style" !== t$7.name || t$7.strings?.length > 2) + ) + throw Error( + "The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.", + ); + } + render(t$7) { + return Object.keys(t$7).reduce((e$14, r$12) => { + const s$9 = t$7[r$12]; + return null == s$9 + ? e$14 + : e$14 + + `${(r$12 = r$12.includes("-") ? r$12 : r$12.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase())}:${s$9};`; + }, ""); + } + update(e$14, [r$12]) { + const { style: s$9 } = e$14.element; + if (void 0 === this.ft) return ((this.ft = new Set(Object.keys(r$12))), this.render(r$12)); + for (const t$7 of this.ft) + null == r$12[t$7] && + (this.ft.delete(t$7), t$7.includes("-") ? s$9.removeProperty(t$7) : (s$9[t$7] = null)); + for (const t$7 in r$12) { + const e$15 = r$12[t$7]; + if (null != e$15) { + this.ft.add(t$7); + const r$13 = "string" == typeof e$15 && e$15.endsWith(i); + t$7.includes("-") || r$13 + ? s$9.setProperty(t$7, r$13 ? e$15.slice(0, -11) : e$15, r$13 ? n$1 : "") + : (s$9[t$7] = e$15); + } + } + return E; + } + }, + ); -var __esDecorate$18 = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers$18 = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; +var __esDecorate$18 = + (void 0 && (void 0).__esDecorate) || + function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f$4) { + if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); + return f$4; + } + var kind = contextIn.kind, + key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? (contextIn["static"] ? ctor : ctor.prototype) : null; + var descriptor = + descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _$1, + done = false; + for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { + var context = {}; + for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; + for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; + context.addInitializer = function (f$4) { + if (done) throw new TypeError("Cannot add initializers after decoration has completed"); + extraInitializers.push(accept(f$4 || null)); + }; + var result = (0, decorators[i$10])( + kind === "accessor" + ? { + get: descriptor.get, + set: descriptor.set, + } + : descriptor[key], + context, + ); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if ((_$1 = accept(result.get))) descriptor.get = _$1; + if ((_$1 = accept(result.set))) descriptor.set = _$1; + if ((_$1 = accept(result.init))) initializers.unshift(_$1); + } else if ((_$1 = accept(result))) { + if (kind === "field") initializers.unshift(_$1); + else descriptor[key] = _$1; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; + }; +var __runInitializers$18 = + (void 0 && (void 0).__runInitializers) || + function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i$10 = 0; i$10 < initializers.length; i$10++) { + value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); + } + return useValue ? value : void 0; + }; let Audio = (() => { - let _classDecorators = [t$1("a2ui-audioplayer")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = Root; - let _url_decorators; - let _url_initializers = []; - let _url_extraInitializers = []; - var Audio$1 = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _url_decorators = [n$6()]; - __esDecorate$18(this, null, _url_decorators, { - kind: "accessor", - name: "url", - static: false, - private: false, - access: { - has: (obj) => "url" in obj, - get: (obj) => obj.url, - set: (obj, value) => { - obj.url = value; - } - }, - metadata: _metadata - }, _url_initializers, _url_extraInitializers); - __esDecorate$18(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Audio$1 = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #url_accessor_storage = __runInitializers$18(this, _url_initializers, null); - get url() { - return this.#url_accessor_storage; - } - set url(value) { - this.#url_accessor_storage = value; - } - static { - this.styles = [structuralStyles, i$9` + let _classDecorators = [t$1("a2ui-audioplayer")]; + let _classDescriptor; + let _classExtraInitializers = []; + let _classThis; + let _classSuper = Root; + let _url_decorators; + let _url_initializers = []; + let _url_extraInitializers = []; + var Audio$1 = class extends _classSuper { + static { + _classThis = this; + } + static { + const _metadata = + typeof Symbol === "function" && Symbol.metadata + ? Object.create(_classSuper[Symbol.metadata] ?? null) + : void 0; + _url_decorators = [n$6()]; + __esDecorate$18( + this, + null, + _url_decorators, + { + kind: "accessor", + name: "url", + static: false, + private: false, + access: { + has: (obj) => "url" in obj, + get: (obj) => obj.url, + set: (obj, value) => { + obj.url = value; + }, + }, + metadata: _metadata, + }, + _url_initializers, + _url_extraInitializers, + ); + __esDecorate$18( + null, + (_classDescriptor = { value: _classThis }), + _classDecorators, + { + kind: "class", + name: _classThis.name, + metadata: _metadata, + }, + null, + _classExtraInitializers, + ); + Audio$1 = _classThis = _classDescriptor.value; + if (_metadata) + Object.defineProperty(_classThis, Symbol.metadata, { + enumerable: true, + configurable: true, + writable: true, + value: _metadata, + }); + } + #url_accessor_storage = __runInitializers$18(this, _url_initializers, null); + get url() { + return this.#url_accessor_storage; + } + set url(value) { + this.#url_accessor_storage = value; + } + static { + this.styles = [ + structuralStyles, + i$9` * { box-sizing: border-box; } @@ -5145,254 +6064,317 @@ let Audio = (() => { display: block; width: 100%; } - `]; - } - #renderAudio() { - if (!this.url) { - return A; - } - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) { - return b`