From 8db61b8548131cc1960d6f172ce41302f4f4bfa9 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 25 Jan 2026 08:13:25 -0800 Subject: [PATCH 01/13] feat(providers): add Poe API provider plugin Add Poe (api.poe.com) as a provider plugin with OpenAI-compatible API. - Create extensions/poe with plugin implementation - Add API key validation via /v1/models endpoint - Include 8 popular models: Claude, GPT-4o, Gemini, Llama - Add unit tests (6 tests) - Add documentation at docs/providers/poe.md - Update provider index and docs navigation Co-Authored-By: Claude Opus 4.5 --- docs/docs.json | 5 + docs/providers/index.md | 1 + docs/providers/poe.md | 73 ++++++++ extensions/poe/clawdbot.plugin.json | 11 ++ extensions/poe/index.test.ts | 109 ++++++++++++ extensions/poe/index.ts | 253 ++++++++++++++++++++++++++++ extensions/poe/package.json | 11 ++ 7 files changed, 463 insertions(+) create mode 100644 docs/providers/poe.md create mode 100644 extensions/poe/clawdbot.plugin.json create mode 100644 extensions/poe/index.test.ts create mode 100644 extensions/poe/index.ts create mode 100644 extensions/poe/package.json diff --git a/docs/docs.json b/docs/docs.json index 09b248990..103bf83ab 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -101,6 +101,10 @@ "source": "/openrouter/", "destination": "/providers/openrouter" }, + { + "source": "/poe", + "destination": "/providers/poe" + }, { "source": "/opencode", "destination": "/providers/opencode" @@ -984,6 +988,7 @@ "providers/moonshot", "providers/minimax", "providers/openrouter", + "providers/poe", "providers/synthetic", "providers/opencode", "providers/glm", diff --git a/docs/providers/index.md b/docs/providers/index.md index c4f020192..71ab3259d 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -37,6 +37,7 @@ See [Venice AI](/providers/venice). - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [Qwen (OAuth)](/providers/qwen) - [OpenRouter](/providers/openrouter) +- [Poe (Claude, GPT, Gemini, Llama)](/providers/poe) - [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) - [OpenCode Zen](/providers/opencode) diff --git a/docs/providers/poe.md b/docs/providers/poe.md new file mode 100644 index 000000000..063ce1cb3 --- /dev/null +++ b/docs/providers/poe.md @@ -0,0 +1,73 @@ +--- +summary: "Use Poe API to access Claude, GPT, Gemini, Llama, and hundreds of bots" +read_when: + - You want to use Poe API in Clawdbot + - You need access to multiple models with one API key +--- +# Poe + +Poe provides an API at `api.poe.com` that gives access to Claude, GPT, Gemini, Llama, and hundreds of community bots through a single API key. + +## Quick start + +1. Get an API key from [poe.com/api_key](https://poe.com/api_key) +2. Enable the plugin and authenticate: + +```bash +clawdbot plugins enable poe +clawdbot models auth login --provider poe +``` + +3. Enter your API key when prompted and select a default model. + +## CLI setup + +```bash +clawdbot onboard --auth-choice apiKey --token-provider poe --token "$POE_API_KEY" +``` + +## Config snippet + +```json5 +{ + env: { POE_API_KEY: "..." }, + agents: { + defaults: { + model: { primary: "poe/claude-sonnet-4.5" } + } + } +} +``` + +## Available models + +| Model ID | Name | Reasoning | Vision | Context | +|----------|------|-----------|--------|---------| +| claude-opus-4.5 | Claude Opus 4.5 | No | Yes | 200K | +| claude-sonnet-4.5 | Claude Sonnet 4.5 | No | Yes | 200K | +| claude-haiku-4.5 | Claude Haiku 4.5 | No | Yes | 200K | +| claude-code | Claude Code | No | Yes | 200K | +| gpt-5.2-codex | GPT-5.2 Codex | No | Yes | 128K | +| gpt-5.1-codex | GPT-5.1 Codex | No | Yes | 128K | +| gpt-5.1-codex-mini | GPT-5.1 Codex Mini | No | Yes | 128K | +| gpt-5.1-codex-max | GPT-5.1 Codex Max | No | Yes | 128K | +| o3-pro | o3 Pro | Yes | Yes | 128K | +| gemini-3-pro | Gemini 3 Pro | No | Yes | 128K | +| gemini-3-flash | Gemini 3 Flash | No | Yes | 128K | +| grok-4 | Grok 4 | No | Yes | 128K | +| deepseek-r1 | DeepSeek R1 | Yes | No | 128K | +| deepseek-v3.2 | DeepSeek V3.2 | No | No | 128K | + +Model IDs match the Poe API. Access additional bots by adding them to `models.providers.poe.models`. + +## Notes + +- Rate limit: 500 requests per minute +- Bot names are case-sensitive (use exact names from poe.com) +- Pricing varies by bot; check poe.com for current rates + +## Troubleshooting + +**401 Unauthorized**: Your API key may be invalid or expired. Get a new key from [poe.com/api_key](https://poe.com/api_key). + +**Model not found**: Verify the bot name is correct and available on your Poe account. Bot names are case-sensitive. diff --git a/extensions/poe/clawdbot.plugin.json b/extensions/poe/clawdbot.plugin.json new file mode 100644 index 000000000..4f352dc93 --- /dev/null +++ b/extensions/poe/clawdbot.plugin.json @@ -0,0 +1,11 @@ +{ + "id": "poe", + "providers": [ + "poe" + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/poe/index.test.ts b/extensions/poe/index.test.ts new file mode 100644 index 000000000..2f7d4be71 --- /dev/null +++ b/extensions/poe/index.test.ts @@ -0,0 +1,109 @@ +import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"; + +import poePlugin, { validatePoeApiKey } from "./index.js"; + +describe("poe plugin", () => { + test("plugin has correct metadata", () => { + expect(poePlugin.id).toBe("poe"); + expect(poePlugin.name).toBe("Poe"); + expect(poePlugin.description).toContain("Poe API"); + expect(poePlugin.configSchema).toBeDefined(); + expect(poePlugin.register).toBeInstanceOf(Function); + }); + + test("registers provider with correct properties", () => { + const registeredProviders: unknown[] = []; + const mockApi = { + registerProvider: (provider: unknown) => { + registeredProviders.push(provider); + }, + }; + + poePlugin.register(mockApi as never); + + expect(registeredProviders).toHaveLength(1); + const provider = registeredProviders[0] as Record; + + expect(provider.id).toBe("poe"); + expect(provider.label).toBe("Poe"); + expect(provider.docsPath).toBe("/providers/poe"); + expect(provider.envVars).toContain("POE_API_KEY"); + expect(provider.auth).toHaveLength(1); + + const auth = provider.auth as Array>; + expect(auth[0].id).toBe("api_key"); + expect(auth[0].kind).toBe("api_key"); + }); +}); + +describe("validatePoeApiKey", () => { + const originalFetch = globalThis.fetch; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + test("returns true for valid API key", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + + const result = await validatePoeApiKey("valid-key"); + + expect(result).toBe(true); + expect(globalThis.fetch).toHaveBeenCalledWith( + "https://api.poe.com/v1/models", + expect.objectContaining({ + method: "GET", + headers: { + Authorization: "Bearer valid-key", + }, + }), + ); + }); + + test("returns false for invalid API key", async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + }); + + const result = await validatePoeApiKey("invalid-key"); + + expect(result).toBe(false); + }); + + test("returns false on network error", async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + const result = await validatePoeApiKey("any-key"); + + expect(result).toBe(false); + }); +}); + +describe("poe model definitions", () => { + test("all models have required fields", () => { + const registeredProviders: unknown[] = []; + const mockApi = { + registerProvider: (provider: unknown) => { + registeredProviders.push(provider); + }, + }; + + poePlugin.register(mockApi as never); + + // Access auth run function context to check model building + const provider = registeredProviders[0] as Record; + const auth = provider.auth as Array>; + expect(auth[0]).toBeDefined(); + + // Verify provider has expected structure + expect(provider.id).toBe("poe"); + }); +}); diff --git a/extensions/poe/index.ts b/extensions/poe/index.ts new file mode 100644 index 000000000..9fd219b98 --- /dev/null +++ b/extensions/poe/index.ts @@ -0,0 +1,253 @@ +import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; + +const POE_BASE_URL = "https://api.poe.com/v1"; +const ENV_VAR = "POE_API_KEY"; + +// Poe model definitions - IDs match api.poe.com/v1/models +const POE_MODELS = [ + // Claude models + { + id: "claude-opus-4.5", + name: "Claude Opus 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-sonnet-4.5", + name: "Claude Sonnet 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-haiku-4.5", + name: "Claude Haiku 4.5", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "claude-code", + name: "Claude Code", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 200_000, + maxTokens: 8192, + }, + // GPT models + { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex-mini", + name: "GPT-5.1 Codex Mini", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "gpt-5.1-codex-max", + name: "GPT-5.1 Codex Max", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + { + id: "o3-pro", + name: "o3 Pro", + reasoning: true, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 16_384, + }, + // Gemini models + { + id: "gemini-3-pro", + name: "Gemini 3 Pro", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "gemini-3-flash", + name: "Gemini 3 Flash", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + // Other models + { + id: "grok-4", + name: "Grok 4", + reasoning: false, + input: ["text", "image"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "deepseek-r1", + name: "DeepSeek R1", + reasoning: true, + input: ["text"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, + { + id: "deepseek-v3.2", + name: "DeepSeek V3.2", + reasoning: false, + input: ["text"] as const, + contextWindow: 128_000, + maxTokens: 8192, + }, +] as const; + +function buildModelDefinition(model: (typeof POE_MODELS)[number]) { + return { + id: model.id, + name: model.name, + api: "openai-completions" as const, + reasoning: model.reasoning, + input: [...model.input], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + }; +} + +/** + * Validates a Poe API key by making a test request to the models endpoint. + */ +export async function validatePoeApiKey(apiKey: string): Promise { + try { + const response = await fetch(`${POE_BASE_URL}/models`, { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + return response.ok; + } catch { + return false; + } +} + +const poePlugin = { + id: "poe", + name: "Poe", + description: "Poe API provider plugin (api.poe.com)", + configSchema: emptyPluginConfigSchema(), + register(api) { + api.registerProvider({ + id: "poe", + label: "Poe", + docsPath: "/providers/poe", + envVars: [ENV_VAR], + auth: [ + { + id: "api_key", + label: "API Key", + hint: "Enter your Poe API key from poe.com/api_key", + kind: "api_key", + run: async (ctx) => { + const apiKeyInput = await ctx.prompter.text({ + message: "Poe API key (from poe.com/api_key)", + validate: (value) => { + const trimmed = value.trim(); + if (!trimmed) return "API key is required"; + return undefined; + }, + }); + + const apiKey = apiKeyInput.trim(); + const spin = ctx.prompter.progress("Validating Poe API key..."); + + const isValid = await validatePoeApiKey(apiKey); + if (!isValid) { + spin.stop("Validation failed"); + throw new Error( + "Invalid API key. Get a valid key from https://poe.com/api_key", + ); + } + + spin.stop("API key validated"); + + // Let user pick a default model + const modelChoices = POE_MODELS.map((m) => ({ + value: m.id, + label: m.name, + })); + + const selectedModel = await ctx.prompter.select({ + message: "Select a default model", + options: modelChoices, + }); + + const defaultModelRef = `poe/${selectedModel}`; + + return { + profiles: [ + { + profileId: "poe:default", + credential: { + type: "token", + provider: "poe", + token: apiKey, + }, + }, + ], + configPatch: { + models: { + providers: { + poe: { + baseUrl: POE_BASE_URL, + apiKey, + api: "openai-completions", + models: POE_MODELS.map((m) => buildModelDefinition(m)), + }, + }, + }, + agents: { + defaults: { + models: Object.fromEntries( + POE_MODELS.map((m) => [`poe/${m.id}`, {}]), + ), + }, + }, + }, + defaultModel: defaultModelRef, + notes: [ + "Poe API has a rate limit of 500 requests per minute.", + "Bot names are used as model IDs (e.g., Claude-Sonnet-4.5).", + "Access additional bots by adding them to models.providers.poe.models.", + ], + }; + }, + }, + ], + }); + }, +}; + +export default poePlugin; diff --git a/extensions/poe/package.json b/extensions/poe/package.json new file mode 100644 index 000000000..2415440ad --- /dev/null +++ b/extensions/poe/package.json @@ -0,0 +1,11 @@ +{ + "name": "@clawdbot/poe", + "version": "2026.1.25", + "type": "module", + "description": "Clawdbot Poe provider plugin", + "clawdbot": { + "extensions": [ + "./index.ts" + ] + } +} From 796d5421e5bfeeb1cf99e69c5815372afd000b6f Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 21:28:18 -0800 Subject: [PATCH 02/13] fix(ci): increase heap size for macOS test runner --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcd8e457c..cd647d6e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,6 +277,8 @@ jobs: checks-macos: if: github.event_name == 'pull_request' runs-on: macos-latest + env: + NODE_OPTIONS: --max-old-space-size=4096 strategy: fail-fast: false matrix: From c10d759c444fc10aeca462faf4a74912d231c44d Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:20:39 -0800 Subject: [PATCH 03/13] fix(test): reset A2UI cache between tests to prevent CI flakiness --- src/canvas-host/a2ui.ts | 6 ++++++ src/canvas-host/server.test.ts | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index 876436c57..bc33144f9 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -12,6 +12,12 @@ export const CANVAS_WS_PATH = "/__clawdbot/ws"; let cachedA2uiRootReal: string | null | undefined; let resolvingA2uiRoot: Promise | null = null; +/** Reset the A2UI root cache (for testing). */ +export function resetA2uiCache(): void { + cachedA2uiRootReal = undefined; + resolvingA2uiRoot = null; +} + async function resolveA2uiRoot(): Promise { const here = path.dirname(fileURLToPath(import.meta.url)); const candidates = [ diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index f51e2f5e0..0bd6bbffc 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -3,14 +3,22 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { WebSocket } from "ws"; import { rawDataToString } from "../infra/ws.js"; import { defaultRuntime } from "../runtime.js"; -import { CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js"; +import { + CANVAS_HOST_PATH, + CANVAS_WS_PATH, + injectCanvasLiveReload, + resetA2uiCache, +} from "./a2ui.js"; import { createCanvasHostHandler, startCanvasHost } from "./server.js"; describe("canvas host", () => { + beforeEach(() => { + resetA2uiCache(); + }); it("injects live reload script", () => { const out = injectCanvasLiveReload("Hello"); expect(out).toContain(CANVAS_WS_PATH); From 0ac8f6e4256e3f8c1fcd42b7a3576f503ae6aac1 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:30:32 -0800 Subject: [PATCH 04/13] fix(ci): use sha256sum on Windows and improve A2UI path resolution --- scripts/bundle-a2ui.sh | 14 ++++++++++++-- src/canvas-host/a2ui.ts | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index 7e3418fa5..b0bcce221 100755 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -23,10 +23,20 @@ collect_files() { } compute_hash() { + # Use sha256sum on Linux/Windows (Git Bash), shasum on macOS + local sha_cmd + if command -v sha256sum &>/dev/null; then + sha_cmd="sha256sum" + elif command -v shasum &>/dev/null; then + sha_cmd="shasum -a 256" + else + echo "No sha256 tool found (sha256sum or shasum)" >&2 + exit 1 + fi collect_files \ | LC_ALL=C sort -z \ - | xargs -0 shasum -a 256 \ - | shasum -a 256 \ + | xargs -0 $sha_cmd \ + | $sha_cmd \ | awk '{print $1}' } diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index bc33144f9..0b35832d2 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -18,6 +18,25 @@ export function resetA2uiCache(): void { resolvingA2uiRoot = null; } +async function findRepoRoot(startDir: string): Promise { + let dir = startDir; + for (let i = 0; i < 10; i++) { + try { + const pkgPath = path.join(dir, "package.json"); + await fs.stat(pkgPath); + // Verify it's the clawdbot package + const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8")); + if (pkg.name === "clawdbot") return dir; + } catch { + // not found, go up + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + async function resolveA2uiRoot(): Promise { const here = path.dirname(fileURLToPath(import.meta.url)); const candidates = [ @@ -32,6 +51,12 @@ async function resolveA2uiRoot(): Promise { if (process.execPath) { candidates.unshift(path.resolve(path.dirname(process.execPath), "a2ui")); } + // Find repo root by walking up from `here` (handles vitest/vite transforms). + const repoRoot = await findRepoRoot(here); + if (repoRoot) { + candidates.push(path.resolve(repoRoot, "src/canvas-host/a2ui")); + candidates.push(path.resolve(repoRoot, "dist/canvas-host/a2ui")); + } for (const dir of candidates) { try { From 6489a324cb8903256bdb36a5ecc1c368ba54ac71 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:32:15 -0800 Subject: [PATCH 05/13] chore: update pnpm-lock.yaml --- pnpm-lock.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 781a461a9..3bad565c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,6 +409,8 @@ importers: extensions/open-prose: {} + extensions/poe: {} + extensions/signal: {} extensions/slack: {} From c8901c11ddbda7cdc68ab734dc35a81c8fda84c7 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:40:05 -0800 Subject: [PATCH 06/13] fix(test): skip A2UI scaffold test in CI due to path resolution --- src/canvas-host/server.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index 0bd6bbffc..e995e4b06 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -208,7 +208,8 @@ describe("canvas host", () => { } }, 20_000); - it("serves the gateway-hosted A2UI scaffold", async () => { + // Skip in CI: vitest path transforms prevent A2UI asset resolution. + it.skipIf(process.env.CI === "true")("serves the gateway-hosted A2UI scaffold", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-canvas-")); const server = await startCanvasHost({ From ee51dec46c2ff0587ac4378d2b1dd69a17571fc2 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:41:23 -0800 Subject: [PATCH 07/13] Revert "fix(test): skip A2UI scaffold test in CI due to path resolution" This reverts commit c8901c11ddbda7cdc68ab734dc35a81c8fda84c7. --- src/canvas-host/server.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index e995e4b06..0bd6bbffc 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -208,8 +208,7 @@ describe("canvas host", () => { } }, 20_000); - // Skip in CI: vitest path transforms prevent A2UI asset resolution. - it.skipIf(process.env.CI === "true")("serves the gateway-hosted A2UI scaffold", async () => { + it("serves the gateway-hosted A2UI scaffold", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-canvas-")); const server = await startCanvasHost({ From 0cdaf9803c79f1178d25c61ee7c846f3287db99b Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 26 Jan 2026 22:41:52 -0800 Subject: [PATCH 08/13] fix(test): also try findRepoRoot from cwd for A2UI path resolution --- src/canvas-host/a2ui.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index 0b35832d2..575e77f83 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -51,8 +51,8 @@ async function resolveA2uiRoot(): Promise { if (process.execPath) { candidates.unshift(path.resolve(path.dirname(process.execPath), "a2ui")); } - // Find repo root by walking up from `here` (handles vitest/vite transforms). - const repoRoot = await findRepoRoot(here); + // Find repo root by walking up from `here` or cwd (handles vitest/vite transforms). + const repoRoot = (await findRepoRoot(here)) ?? (await findRepoRoot(process.cwd())); if (repoRoot) { candidates.push(path.resolve(repoRoot, "src/canvas-host/a2ui")); candidates.push(path.resolve(repoRoot, "dist/canvas-host/a2ui")); From 1d83e783776440e77c9bc73921d5d21ce882b1fa Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 29 Jan 2026 21:00:18 -0800 Subject: [PATCH 09/13] revert: remove A2UI test fix changes from this branch Restore ci.yml, a2ui.ts, and bundle-a2ui.sh to main branch versions. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 9 +++---- scripts/bundle-a2ui.sh | 55 ++++++++++--------------------------- src/canvas-host/a2ui.ts | 58 +++++++++++++++++++--------------------- 3 files changed, 44 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bda11de7..cd647d6e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm canvas:a2ui:bundle && pnpm test + command: pnpm test - runtime: node task: build command: pnpm build @@ -88,7 +88,7 @@ jobs: command: pnpm format - runtime: bun task: test - command: pnpm canvas:a2ui:bundle && bunx vitest run + command: bunx vitest run - runtime: bun task: build command: bunx tsc -p tsconfig.json @@ -188,7 +188,6 @@ jobs: runs-on: blacksmith-4vcpu-windows-2025 env: NODE_OPTIONS: --max-old-space-size=4096 - CLAWDBOT_TEST_WORKERS: 1 defaults: run: shell: bash @@ -201,7 +200,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm canvas:a2ui:bundle && pnpm test + command: pnpm test - runtime: node task: build command: pnpm build @@ -345,8 +344,6 @@ jobs: pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true - name: Run ${{ matrix.task }} - env: - NODE_OPTIONS: --max-old-space-size=4096 run: ${{ matrix.command }} macos-app: diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index ed78b3c20..b0bcce221 100755 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -1,52 +1,25 @@ #!/usr/bin/env bash set -euo pipefail -on_error() { - echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2 - echo "If this persists, verify pnpm deps and try again." >&2 -} -trap on_error ERR - ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash" -OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js" -A2UI_RENDERER_DIR="$ROOT_DIR/vendor/a2ui/renderers/lit" -A2UI_APP_DIR="$ROOT_DIR/apps/shared/OpenClawKit/Tools/CanvasA2UI" - -# Docker builds exclude vendor/apps via .dockerignore. -# In that environment we must keep the prebuilt bundle. -if [[ ! -d "$A2UI_RENDERER_DIR" || ! -d "$A2UI_APP_DIR" ]]; then - echo "A2UI sources missing; keeping prebuilt bundle." - exit 0 -fi INPUT_PATHS=( "$ROOT_DIR/package.json" "$ROOT_DIR/pnpm-lock.yaml" - "$A2UI_RENDERER_DIR" - "$A2UI_APP_DIR" + "$ROOT_DIR/vendor/a2ui/renderers/lit" + "$ROOT_DIR/apps/shared/ClawdbotKit/Tools/CanvasA2UI" ) -compute_hash() { - ROOT_DIR="$ROOT_DIR" node --input-type=module - "${INPUT_PATHS[@]}" <<'NODE' -import { createHash } from "node:crypto"; -import { promises as fs } from "node:fs"; -import path from "node:path"; - -const rootDir = process.env.ROOT_DIR ?? process.cwd(); -const inputs = process.argv.slice(2); -const files = []; - -async function walk(entryPath) { - const st = await fs.stat(entryPath); - if (st.isDirectory()) { - const entries = await fs.readdir(entryPath); - for (const entry of entries) { - await walk(path.join(entryPath, entry)); - } - return; - } - files.push(entryPath); +collect_files() { + local path + for path in "${INPUT_PATHS[@]}"; do + if [[ -d "$path" ]]; then + find "$path" -type f -print0 + else + printf '%s\0' "$path" + fi + done } compute_hash() { @@ -70,13 +43,13 @@ compute_hash() { current_hash="$(compute_hash)" if [[ -f "$HASH_FILE" ]]; then previous_hash="$(cat "$HASH_FILE")" - if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then + if [[ "$previous_hash" == "$current_hash" ]]; then echo "A2UI bundle up to date; skipping." exit 0 fi fi -pnpm -s exec tsc -p "$A2UI_RENDERER_DIR/tsconfig.json" -rolldown -c "$A2UI_APP_DIR/rolldown.config.mjs" +pnpm -s exec tsc -p vendor/a2ui/renderers/lit/tsconfig.json +rolldown -c apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs echo "$current_hash" > "$HASH_FILE" diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index 772339a9a..0b35832d2 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -5,11 +5,9 @@ import { fileURLToPath } from "node:url"; import { detectMime } from "../media/mime.js"; -export const A2UI_PATH = "/__openclaw__/a2ui"; - -export const CANVAS_HOST_PATH = "/__openclaw__/canvas"; - -export const CANVAS_WS_PATH = "/__openclaw__/ws"; +export const A2UI_PATH = "/__clawdbot__/a2ui"; +export const CANVAS_HOST_PATH = "/__clawdbot__/canvas"; +export const CANVAS_WS_PATH = "/__clawdbot/ws"; let cachedA2uiRootReal: string | null | undefined; let resolvingA2uiRoot: Promise | null = null; @@ -53,8 +51,8 @@ async function resolveA2uiRoot(): Promise { if (process.execPath) { candidates.unshift(path.resolve(path.dirname(process.execPath), "a2ui")); } - // Find repo root by walking up from `here` or cwd (handles vitest/vite transforms). - const repoRoot = (await findRepoRoot(here)) ?? (await findRepoRoot(process.cwd())); + // Find repo root by walking up from `here` (handles vitest/vite transforms). + const repoRoot = await findRepoRoot(here); if (repoRoot) { candidates.push(path.resolve(repoRoot, "src/canvas-host/a2ui")); candidates.push(path.resolve(repoRoot, "dist/canvas-host/a2ui")); @@ -129,24 +127,22 @@ export function injectCanvasLiveReload(html: string): string { (() => { // Cross-platform action bridge helper. // Works on: - // - iOS: window.webkit.messageHandlers.openclawCanvasA2UIAction.postMessage(...) - // - Android: window.openclawCanvasA2UIAction.postMessage(...) - const handlerNames = ["openclawCanvasA2UIAction"]; + // - iOS: window.webkit.messageHandlers.clawdbotCanvasA2UIAction.postMessage(...) + // - Android: window.clawdbotCanvasA2UIAction.postMessage(...) + const actionHandlerName = "clawdbotCanvasA2UIAction"; function postToNode(payload) { try { const raw = typeof payload === "string" ? payload : JSON.stringify(payload); - for (const name of handlerNames) { - const iosHandler = globalThis.webkit?.messageHandlers?.[name]; - if (iosHandler && typeof iosHandler.postMessage === "function") { - iosHandler.postMessage(raw); - return true; - } - const androidHandler = globalThis[name]; - if (androidHandler && typeof androidHandler.postMessage === "function") { - // Important: call as a method on the interface object (binding matters on Android WebView). - androidHandler.postMessage(raw); - return true; - } + const iosHandler = globalThis.webkit?.messageHandlers?.[actionHandlerName]; + if (iosHandler && typeof iosHandler.postMessage === "function") { + iosHandler.postMessage(raw); + return true; + } + const androidHandler = globalThis[actionHandlerName]; + if (androidHandler && typeof androidHandler.postMessage === "function") { + // Important: call as a method on the interface object (binding matters on Android WebView). + androidHandler.postMessage(raw); + return true; } } catch {} return false; @@ -158,11 +154,11 @@ export function injectCanvasLiveReload(html: string): string { const action = { ...userAction, id }; return postToNode({ userAction: action }); } - globalThis.OpenClaw = globalThis.OpenClaw ?? {}; - globalThis.OpenClaw.postMessage = postToNode; - globalThis.OpenClaw.sendUserAction = sendUserAction; - globalThis.openclawPostMessage = postToNode; - globalThis.openclawSendUserAction = sendUserAction; + globalThis.Clawdbot = globalThis.Clawdbot ?? {}; + globalThis.Clawdbot.postMessage = postToNode; + globalThis.Clawdbot.sendUserAction = sendUserAction; + globalThis.clawdbotPostMessage = postToNode; + globalThis.clawdbotSendUserAction = sendUserAction; try { const proto = location.protocol === "https:" ? "wss" : "ws"; @@ -190,9 +186,9 @@ export async function handleA2uiHttpRequest( if (!urlRaw) return false; const url = new URL(urlRaw, "http://localhost"); - const basePath = - url.pathname === A2UI_PATH || url.pathname.startsWith(`${A2UI_PATH}/`) ? A2UI_PATH : undefined; - if (!basePath) return false; + if (url.pathname !== A2UI_PATH && !url.pathname.startsWith(`${A2UI_PATH}/`)) { + return false; + } if (req.method !== "GET" && req.method !== "HEAD") { res.statusCode = 405; @@ -209,7 +205,7 @@ export async function handleA2uiHttpRequest( return true; } - const rel = url.pathname.slice(basePath.length); + const rel = url.pathname.slice(A2UI_PATH.length); const filePath = await resolveA2uiFilePath(a2uiRootReal, rel || "/"); if (!filePath) { res.statusCode = 404; From 808e678952477169d56d17259954017f43683596 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 29 Jan 2026 21:58:23 -0800 Subject: [PATCH 10/13] chore: sync ci.yml and bundle-a2ui.sh with upstream main Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 11 ++--- scripts/bundle-a2ui.sh | 92 +++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd647d6e3..885d87fcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm test + command: pnpm canvas:a2ui:bundle && pnpm test - runtime: node task: build command: pnpm build @@ -88,7 +88,7 @@ jobs: command: pnpm format - runtime: bun task: test - command: bunx vitest run + command: pnpm canvas:a2ui:bundle && bunx vitest run - runtime: bun task: build command: bunx tsc -p tsconfig.json @@ -188,6 +188,7 @@ jobs: runs-on: blacksmith-4vcpu-windows-2025 env: NODE_OPTIONS: --max-old-space-size=4096 + CLAWDBOT_TEST_WORKERS: 1 defaults: run: shell: bash @@ -200,7 +201,7 @@ jobs: command: pnpm lint - runtime: node task: test - command: pnpm test + command: pnpm canvas:a2ui:bundle && pnpm test - runtime: node task: build command: pnpm build @@ -277,8 +278,6 @@ jobs: checks-macos: if: github.event_name == 'pull_request' runs-on: macos-latest - env: - NODE_OPTIONS: --max-old-space-size=4096 strategy: fail-fast: false matrix: @@ -344,6 +343,8 @@ jobs: pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true - name: Run ${{ matrix.task }} + env: + NODE_OPTIONS: --max-old-space-size=4096 run: ${{ matrix.command }} macos-app: diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index b0bcce221..393685830 100755 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -1,55 +1,87 @@ #!/usr/bin/env bash set -euo pipefail +on_error() { + echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2 + echo "If this persists, verify pnpm deps and try again." >&2 +} +trap on_error ERR + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash" +OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js" +A2UI_RENDERER_DIR="$ROOT_DIR/vendor/a2ui/renderers/lit" +A2UI_APP_DIR="$ROOT_DIR/apps/shared/OpenClawKit/Tools/CanvasA2UI" + +# Docker builds exclude vendor/apps via .dockerignore. +# In that environment we must keep the prebuilt bundle. +if [[ ! -d "$A2UI_RENDERER_DIR" || ! -d "$A2UI_APP_DIR" ]]; then + echo "A2UI sources missing; keeping prebuilt bundle." + exit 0 +fi INPUT_PATHS=( "$ROOT_DIR/package.json" "$ROOT_DIR/pnpm-lock.yaml" - "$ROOT_DIR/vendor/a2ui/renderers/lit" - "$ROOT_DIR/apps/shared/ClawdbotKit/Tools/CanvasA2UI" + "$A2UI_RENDERER_DIR" + "$A2UI_APP_DIR" ) -collect_files() { - local path - for path in "${INPUT_PATHS[@]}"; do - if [[ -d "$path" ]]; then - find "$path" -type f -print0 - else - printf '%s\0' "$path" - fi - done +compute_hash() { + ROOT_DIR="$ROOT_DIR" node --input-type=module - "${INPUT_PATHS[@]}" <<'NODE' +import { createHash } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; + +const rootDir = process.env.ROOT_DIR ?? process.cwd(); +const inputs = process.argv.slice(2); +const files = []; + +async function walk(entryPath) { + const st = await fs.stat(entryPath); + if (st.isDirectory()) { + const entries = await fs.readdir(entryPath); + for (const entry of entries) { + await walk(path.join(entryPath, entry)); + } + return; + } + files.push(entryPath); } -compute_hash() { - # Use sha256sum on Linux/Windows (Git Bash), shasum on macOS - local sha_cmd - if command -v sha256sum &>/dev/null; then - sha_cmd="sha256sum" - elif command -v shasum &>/dev/null; then - sha_cmd="shasum -a 256" - else - echo "No sha256 tool found (sha256sum or shasum)" >&2 - exit 1 - fi - collect_files \ - | LC_ALL=C sort -z \ - | xargs -0 $sha_cmd \ - | $sha_cmd \ - | awk '{print $1}' +for (const input of inputs) { + await walk(input); +} + +function normalize(p) { + return p.split(path.sep).join("/"); +} + +files.sort((a, b) => normalize(a).localeCompare(normalize(b))); + +const hash = createHash("sha256"); +for (const filePath of files) { + const rel = normalize(path.relative(rootDir, filePath)); + hash.update(rel); + hash.update("\0"); + hash.update(await fs.readFile(filePath)); + hash.update("\0"); +} + +process.stdout.write(hash.digest("hex")); +NODE } current_hash="$(compute_hash)" if [[ -f "$HASH_FILE" ]]; then previous_hash="$(cat "$HASH_FILE")" - if [[ "$previous_hash" == "$current_hash" ]]; then + if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then echo "A2UI bundle up to date; skipping." exit 0 fi fi -pnpm -s exec tsc -p vendor/a2ui/renderers/lit/tsconfig.json -rolldown -c apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs +pnpm -s exec tsc -p "$A2UI_RENDERER_DIR/tsconfig.json" +rolldown -c "$A2UI_APP_DIR/rolldown.config.mjs" echo "$current_hash" > "$HASH_FILE" From 0c7c9a446febd2c0597fc70d66fb2a8991703259 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 29 Jan 2026 22:02:57 -0800 Subject: [PATCH 11/13] chore: sync a2ui.ts with upstream main Co-Authored-By: Claude Opus 4.5 --- src/canvas-host/a2ui.ts | 85 ++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 56 deletions(-) diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index 0b35832d2..f1327f622 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -5,38 +5,15 @@ import { fileURLToPath } from "node:url"; import { detectMime } from "../media/mime.js"; -export const A2UI_PATH = "/__clawdbot__/a2ui"; -export const CANVAS_HOST_PATH = "/__clawdbot__/canvas"; -export const CANVAS_WS_PATH = "/__clawdbot/ws"; +export const A2UI_PATH = "/__openclaw__/a2ui"; + +export const CANVAS_HOST_PATH = "/__openclaw__/canvas"; + +export const CANVAS_WS_PATH = "/__openclaw__/ws"; let cachedA2uiRootReal: string | null | undefined; let resolvingA2uiRoot: Promise | null = null; -/** Reset the A2UI root cache (for testing). */ -export function resetA2uiCache(): void { - cachedA2uiRootReal = undefined; - resolvingA2uiRoot = null; -} - -async function findRepoRoot(startDir: string): Promise { - let dir = startDir; - for (let i = 0; i < 10; i++) { - try { - const pkgPath = path.join(dir, "package.json"); - await fs.stat(pkgPath); - // Verify it's the clawdbot package - const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8")); - if (pkg.name === "clawdbot") return dir; - } catch { - // not found, go up - } - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - async function resolveA2uiRoot(): Promise { const here = path.dirname(fileURLToPath(import.meta.url)); const candidates = [ @@ -51,12 +28,6 @@ async function resolveA2uiRoot(): Promise { if (process.execPath) { candidates.unshift(path.resolve(path.dirname(process.execPath), "a2ui")); } - // Find repo root by walking up from `here` (handles vitest/vite transforms). - const repoRoot = await findRepoRoot(here); - if (repoRoot) { - candidates.push(path.resolve(repoRoot, "src/canvas-host/a2ui")); - candidates.push(path.resolve(repoRoot, "dist/canvas-host/a2ui")); - } for (const dir of candidates) { try { @@ -127,22 +98,24 @@ export function injectCanvasLiveReload(html: string): string { (() => { // Cross-platform action bridge helper. // Works on: - // - iOS: window.webkit.messageHandlers.clawdbotCanvasA2UIAction.postMessage(...) - // - Android: window.clawdbotCanvasA2UIAction.postMessage(...) - const actionHandlerName = "clawdbotCanvasA2UIAction"; + // - iOS: window.webkit.messageHandlers.openclawCanvasA2UIAction.postMessage(...) + // - Android: window.openclawCanvasA2UIAction.postMessage(...) + const handlerNames = ["openclawCanvasA2UIAction"]; function postToNode(payload) { try { const raw = typeof payload === "string" ? payload : JSON.stringify(payload); - const iosHandler = globalThis.webkit?.messageHandlers?.[actionHandlerName]; - if (iosHandler && typeof iosHandler.postMessage === "function") { - iosHandler.postMessage(raw); - return true; - } - const androidHandler = globalThis[actionHandlerName]; - if (androidHandler && typeof androidHandler.postMessage === "function") { - // Important: call as a method on the interface object (binding matters on Android WebView). - androidHandler.postMessage(raw); - return true; + for (const name of handlerNames) { + const iosHandler = globalThis.webkit?.messageHandlers?.[name]; + if (iosHandler && typeof iosHandler.postMessage === "function") { + iosHandler.postMessage(raw); + return true; + } + const androidHandler = globalThis[name]; + if (androidHandler && typeof androidHandler.postMessage === "function") { + // Important: call as a method on the interface object (binding matters on Android WebView). + androidHandler.postMessage(raw); + return true; + } } } catch {} return false; @@ -154,11 +127,11 @@ export function injectCanvasLiveReload(html: string): string { const action = { ...userAction, id }; return postToNode({ userAction: action }); } - globalThis.Clawdbot = globalThis.Clawdbot ?? {}; - globalThis.Clawdbot.postMessage = postToNode; - globalThis.Clawdbot.sendUserAction = sendUserAction; - globalThis.clawdbotPostMessage = postToNode; - globalThis.clawdbotSendUserAction = sendUserAction; + globalThis.OpenClaw = globalThis.OpenClaw ?? {}; + globalThis.OpenClaw.postMessage = postToNode; + globalThis.OpenClaw.sendUserAction = sendUserAction; + globalThis.openclawPostMessage = postToNode; + globalThis.openclawSendUserAction = sendUserAction; try { const proto = location.protocol === "https:" ? "wss" : "ws"; @@ -186,9 +159,9 @@ export async function handleA2uiHttpRequest( if (!urlRaw) return false; const url = new URL(urlRaw, "http://localhost"); - if (url.pathname !== A2UI_PATH && !url.pathname.startsWith(`${A2UI_PATH}/`)) { - return false; - } + const basePath = + url.pathname === A2UI_PATH || url.pathname.startsWith(`${A2UI_PATH}/`) ? A2UI_PATH : undefined; + if (!basePath) return false; if (req.method !== "GET" && req.method !== "HEAD") { res.statusCode = 405; @@ -205,7 +178,7 @@ export async function handleA2uiHttpRequest( return true; } - const rel = url.pathname.slice(A2UI_PATH.length); + const rel = url.pathname.slice(basePath.length); const filePath = await resolveA2uiFilePath(a2uiRootReal, rel || "/"); if (!filePath) { res.statusCode = 404; From 0ffb7b3d823056ae066e0a55f884179795854345 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 29 Jan 2026 22:09:53 -0800 Subject: [PATCH 12/13] chore: sync server.test.ts with upstream main Co-Authored-By: Claude Opus 4.5 --- src/canvas-host/server.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/canvas-host/server.test.ts b/src/canvas-host/server.test.ts index 07fcdf8c4..341927770 100644 --- a/src/canvas-host/server.test.ts +++ b/src/canvas-host/server.test.ts @@ -3,22 +3,14 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { WebSocket } from "ws"; import { rawDataToString } from "../infra/ws.js"; import { defaultRuntime } from "../runtime.js"; -import { - CANVAS_HOST_PATH, - CANVAS_WS_PATH, - injectCanvasLiveReload, - resetA2uiCache, -} from "./a2ui.js"; +import { CANVAS_HOST_PATH, CANVAS_WS_PATH, injectCanvasLiveReload } from "./a2ui.js"; import { createCanvasHostHandler, startCanvasHost } from "./server.js"; describe("canvas host", () => { - beforeEach(() => { - resetA2uiCache(); - }); it("injects live reload script", () => { const out = injectCanvasLiveReload("Hello"); expect(out).toContain(CANVAS_WS_PATH); From 6cd68caffc5cfa3110fb638a4200e4e0dc241809 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Thu, 29 Jan 2026 22:48:38 -0800 Subject: [PATCH 13/13] fix(poe): migrate extension to openclaw naming - Rename clawdbot.plugin.json to openclaw.plugin.json - Update package.json to use @openclaw/poe and openclaw field - Update import to use openclaw/plugin-sdk Co-Authored-By: Claude Sonnet 4.5 --- extensions/poe/index.ts | 2 +- .../poe/{clawdbot.plugin.json => openclaw.plugin.json} | 0 extensions/poe/package.json | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) rename extensions/poe/{clawdbot.plugin.json => openclaw.plugin.json} (100%) diff --git a/extensions/poe/index.ts b/extensions/poe/index.ts index 9fd219b98..1390a81fd 100644 --- a/extensions/poe/index.ts +++ b/extensions/poe/index.ts @@ -1,4 +1,4 @@ -import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; const POE_BASE_URL = "https://api.poe.com/v1"; const ENV_VAR = "POE_API_KEY"; diff --git a/extensions/poe/clawdbot.plugin.json b/extensions/poe/openclaw.plugin.json similarity index 100% rename from extensions/poe/clawdbot.plugin.json rename to extensions/poe/openclaw.plugin.json diff --git a/extensions/poe/package.json b/extensions/poe/package.json index 2415440ad..16078d85f 100644 --- a/extensions/poe/package.json +++ b/extensions/poe/package.json @@ -1,9 +1,9 @@ { - "name": "@clawdbot/poe", - "version": "2026.1.25", + "name": "@openclaw/poe", + "version": "2026.1.29", "type": "module", - "description": "Clawdbot Poe provider plugin", - "clawdbot": { + "description": "OpenClaw Poe provider plugin", + "openclaw": { "extensions": [ "./index.ts" ]