From b0f3f04ac6d784c0c2a802776c8353af6fa24b54 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Thu, 26 Feb 2026 16:32:59 -0600 Subject: [PATCH] feat: add OpenClaw adapter type Introduce openclaw adapter package with server execution, CLI stream formatting, and UI config fields. Register the adapter across CLI, server, and UI registries. Add adapter label in all relevant pages. Co-Authored-By: Claude Opus 4.6 --- cli/package.json | 1 + cli/src/adapters/registry.ts | 8 +- packages/adapters/openclaw/package.json | 22 +++ .../adapters/openclaw/src/cli/format-event.ts | 18 +++ packages/adapters/openclaw/src/cli/index.ts | 1 + packages/adapters/openclaw/src/index.ts | 27 ++++ .../adapters/openclaw/src/server/execute.ts | 144 ++++++++++++++++++ .../adapters/openclaw/src/server/index.ts | 3 + .../adapters/openclaw/src/server/parse.ts | 15 ++ packages/adapters/openclaw/src/server/test.ts | 122 +++++++++++++++ .../adapters/openclaw/src/ui/build-config.ts | 9 ++ packages/adapters/openclaw/src/ui/index.ts | 2 + .../adapters/openclaw/src/ui/parse-stdout.ts | 5 + packages/adapters/openclaw/tsconfig.json | 8 + packages/shared/src/constants.ts | 2 +- pnpm-lock.yaml | 22 +++ server/package.json | 1 + server/src/adapters/registry.ts | 19 ++- ui/package.json | 1 + ui/src/adapters/openclaw/config-fields.tsx | 53 +++++++ ui/src/adapters/openclaw/index.ts | 12 ++ ui/src/adapters/registry.ts | 3 +- ui/src/components/AgentProperties.tsx | 3 +- ui/src/components/agent-config-primitives.tsx | 4 +- ui/src/pages/Agents.tsx | 3 +- ui/src/pages/OrgChart.tsx | 1 + 26 files changed, 502 insertions(+), 7 deletions(-) create mode 100644 packages/adapters/openclaw/package.json create mode 100644 packages/adapters/openclaw/src/cli/format-event.ts create mode 100644 packages/adapters/openclaw/src/cli/index.ts create mode 100644 packages/adapters/openclaw/src/index.ts create mode 100644 packages/adapters/openclaw/src/server/execute.ts create mode 100644 packages/adapters/openclaw/src/server/index.ts create mode 100644 packages/adapters/openclaw/src/server/parse.ts create mode 100644 packages/adapters/openclaw/src/server/test.ts create mode 100644 packages/adapters/openclaw/src/ui/build-config.ts create mode 100644 packages/adapters/openclaw/src/ui/index.ts create mode 100644 packages/adapters/openclaw/src/ui/parse-stdout.ts create mode 100644 packages/adapters/openclaw/tsconfig.json create mode 100644 ui/src/adapters/openclaw/config-fields.tsx create mode 100644 ui/src/adapters/openclaw/index.ts diff --git a/cli/package.json b/cli/package.json index 7d9e3706..da9736c2 100644 --- a/cli/package.json +++ b/cli/package.json @@ -15,6 +15,7 @@ "@clack/prompts": "^0.10.0", "@paperclip/adapter-claude-local": "workspace:*", "@paperclip/adapter-codex-local": "workspace:*", + "@paperclip/adapter-openclaw": "workspace:*", "@paperclip/adapter-utils": "workspace:*", "@paperclip/db": "workspace:*", "@paperclip/server": "workspace:*", diff --git a/cli/src/adapters/registry.ts b/cli/src/adapters/registry.ts index 461a6e2d..5b65874e 100644 --- a/cli/src/adapters/registry.ts +++ b/cli/src/adapters/registry.ts @@ -1,6 +1,7 @@ import type { CLIAdapterModule } from "@paperclip/adapter-utils"; import { printClaudeStreamEvent } from "@paperclip/adapter-claude-local/cli"; import { printCodexStreamEvent } from "@paperclip/adapter-codex-local/cli"; +import { printOpenClawStreamEvent } from "@paperclip/adapter-openclaw/cli"; import { processCLIAdapter } from "./process/index.js"; import { httpCLIAdapter } from "./http/index.js"; @@ -14,8 +15,13 @@ const codexLocalCLIAdapter: CLIAdapterModule = { formatStdoutEvent: printCodexStreamEvent, }; +const openclawCLIAdapter: CLIAdapterModule = { + type: "openclaw", + formatStdoutEvent: printOpenClawStreamEvent, +}; + const adaptersByType = new Map( - [claudeLocalCLIAdapter, codexLocalCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]), + [claudeLocalCLIAdapter, codexLocalCLIAdapter, openclawCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]), ); export function getCLIAdapter(type: string): CLIAdapterModule { diff --git a/packages/adapters/openclaw/package.json b/packages/adapters/openclaw/package.json new file mode 100644 index 00000000..0725040c --- /dev/null +++ b/packages/adapters/openclaw/package.json @@ -0,0 +1,22 @@ +{ + "name": "@paperclip/adapter-openclaw", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./server": "./src/server/index.ts", + "./ui": "./src/ui/index.ts", + "./cli": "./src/cli/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paperclip/adapter-utils": "workspace:*", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/adapters/openclaw/src/cli/format-event.ts b/packages/adapters/openclaw/src/cli/format-event.ts new file mode 100644 index 00000000..c0c0c910 --- /dev/null +++ b/packages/adapters/openclaw/src/cli/format-event.ts @@ -0,0 +1,18 @@ +import pc from "picocolors"; + +export function printOpenClawStreamEvent(raw: string, debug: boolean): void { + const line = raw.trim(); + if (!line) return; + + if (!debug) { + console.log(line); + return; + } + + if (line.startsWith("[openclaw]")) { + console.log(pc.cyan(line)); + return; + } + + console.log(pc.gray(line)); +} diff --git a/packages/adapters/openclaw/src/cli/index.ts b/packages/adapters/openclaw/src/cli/index.ts new file mode 100644 index 00000000..107ebf8b --- /dev/null +++ b/packages/adapters/openclaw/src/cli/index.ts @@ -0,0 +1 @@ +export { printOpenClawStreamEvent } from "./format-event.js"; diff --git a/packages/adapters/openclaw/src/index.ts b/packages/adapters/openclaw/src/index.ts new file mode 100644 index 00000000..835d2ebf --- /dev/null +++ b/packages/adapters/openclaw/src/index.ts @@ -0,0 +1,27 @@ +export const type = "openclaw"; +export const label = "OpenClaw"; + +export const models: { id: string; label: string }[] = []; + +export const agentConfigurationDoc = `# openclaw agent configuration + +Adapter: openclaw + +Use when: +- You run an OpenClaw agent remotely and wake it via webhook. +- You want Paperclip heartbeat/task events delivered over HTTP. + +Don't use when: +- You need local CLI execution inside Paperclip (use claude_local/codex_local/process). +- The OpenClaw endpoint is not reachable from the Paperclip server. + +Core fields: +- url (string, required): OpenClaw webhook endpoint URL +- method (string, optional): HTTP method, default POST +- headers (object, optional): extra HTTP headers for webhook calls +- webhookAuthHeader (string, optional): Authorization header value if your endpoint requires auth +- payloadTemplate (object, optional): additional JSON payload fields merged into each wake payload + +Operational fields: +- timeoutSec (number, optional): request timeout in seconds (default 30) +`; diff --git a/packages/adapters/openclaw/src/server/execute.ts b/packages/adapters/openclaw/src/server/execute.ts new file mode 100644 index 00000000..3c675577 --- /dev/null +++ b/packages/adapters/openclaw/src/server/execute.ts @@ -0,0 +1,144 @@ +import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils"; +import { asNumber, asString, parseObject } from "@paperclip/adapter-utils/server-utils"; +import { parseOpenClawResponse } from "./parse.js"; + +function nonEmpty(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +export async function execute(ctx: AdapterExecutionContext): Promise { + const { config, runId, agent, context, onLog, onMeta } = ctx; + const url = asString(config.url, "").trim(); + if (!url) { + return { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: "OpenClaw adapter missing url", + errorCode: "openclaw_url_missing", + }; + } + + const method = asString(config.method, "POST").trim().toUpperCase() || "POST"; + const timeoutSec = Math.max(1, asNumber(config.timeoutSec, 30)); + const headersConfig = parseObject(config.headers) as Record; + const payloadTemplate = parseObject(config.payloadTemplate); + const webhookAuthHeader = nonEmpty(config.webhookAuthHeader); + + const headers: Record = { + "content-type": "application/json", + }; + for (const [key, value] of Object.entries(headersConfig)) { + if (typeof value === "string" && value.trim().length > 0) { + headers[key] = value; + } + } + if (webhookAuthHeader && !headers.authorization && !headers.Authorization) { + headers.authorization = webhookAuthHeader; + } + + const wakePayload = { + runId, + agentId: agent.id, + companyId: agent.companyId, + taskId: nonEmpty(context.taskId) ?? nonEmpty(context.issueId), + issueId: nonEmpty(context.issueId), + wakeReason: nonEmpty(context.wakeReason), + wakeCommentId: nonEmpty(context.wakeCommentId) ?? nonEmpty(context.commentId), + approvalId: nonEmpty(context.approvalId), + approvalStatus: nonEmpty(context.approvalStatus), + issueIds: Array.isArray(context.issueIds) + ? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) + : [], + }; + + const body = { + ...payloadTemplate, + paperclip: { + ...wakePayload, + context, + }, + }; + + if (onMeta) { + await onMeta({ + adapterType: "openclaw", + command: "webhook", + commandArgs: [method, url], + context, + }); + } + + await onLog("stdout", `[openclaw] invoking ${method} ${url}\n`); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutSec * 1000); + + try { + const response = await fetch(url, { + method, + headers, + body: JSON.stringify(body), + signal: controller.signal, + }); + + const responseText = await response.text(); + if (responseText.trim().length > 0) { + await onLog("stdout", `[openclaw] response (${response.status}) ${responseText.slice(0, 2000)}\n`); + } else { + await onLog("stdout", `[openclaw] response (${response.status}) \n`); + } + + if (!response.ok) { + return { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: `OpenClaw webhook failed with status ${response.status}`, + errorCode: "openclaw_http_error", + resultJson: { + status: response.status, + statusText: response.statusText, + response: parseOpenClawResponse(responseText) ?? responseText, + }, + }; + } + + return { + exitCode: 0, + signal: null, + timedOut: false, + provider: "openclaw", + model: null, + summary: `OpenClaw webhook ${method} ${url}`, + resultJson: { + status: response.status, + statusText: response.statusText, + response: parseOpenClawResponse(responseText) ?? responseText, + }, + }; + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + await onLog("stderr", `[openclaw] request timed out after ${timeoutSec}s\n`); + return { + exitCode: null, + signal: null, + timedOut: true, + errorMessage: `Timed out after ${timeoutSec}s`, + errorCode: "timeout", + }; + } + + const message = err instanceof Error ? err.message : String(err); + await onLog("stderr", `[openclaw] request failed: ${message}\n`); + return { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: message, + errorCode: "openclaw_request_failed", + }; + } finally { + clearTimeout(timeout); + } +} diff --git a/packages/adapters/openclaw/src/server/index.ts b/packages/adapters/openclaw/src/server/index.ts new file mode 100644 index 00000000..b44c258b --- /dev/null +++ b/packages/adapters/openclaw/src/server/index.ts @@ -0,0 +1,3 @@ +export { execute } from "./execute.js"; +export { testEnvironment } from "./test.js"; +export { parseOpenClawResponse, isOpenClawUnknownSessionError } from "./parse.js"; diff --git a/packages/adapters/openclaw/src/server/parse.ts b/packages/adapters/openclaw/src/server/parse.ts new file mode 100644 index 00000000..5045c202 --- /dev/null +++ b/packages/adapters/openclaw/src/server/parse.ts @@ -0,0 +1,15 @@ +export function parseOpenClawResponse(text: string): Record | null { + try { + const parsed = JSON.parse(text); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return null; + } + return parsed as Record; + } catch { + return null; + } +} + +export function isOpenClawUnknownSessionError(_text: string): boolean { + return false; +} diff --git a/packages/adapters/openclaw/src/server/test.ts b/packages/adapters/openclaw/src/server/test.ts new file mode 100644 index 00000000..905f57a7 --- /dev/null +++ b/packages/adapters/openclaw/src/server/test.ts @@ -0,0 +1,122 @@ +import type { + AdapterEnvironmentCheck, + AdapterEnvironmentTestContext, + AdapterEnvironmentTestResult, +} from "@paperclip/adapter-utils"; +import { asString, parseObject } from "@paperclip/adapter-utils/server-utils"; + +function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { + if (checks.some((check) => check.level === "error")) return "fail"; + if (checks.some((check) => check.level === "warn")) return "warn"; + return "pass"; +} + +function isLoopbackHost(hostname: string): boolean { + const value = hostname.trim().toLowerCase(); + return value === "localhost" || value === "127.0.0.1" || value === "::1"; +} + +export async function testEnvironment( + ctx: AdapterEnvironmentTestContext, +): Promise { + const checks: AdapterEnvironmentCheck[] = []; + const config = parseObject(ctx.config); + const urlValue = asString(config.url, ""); + + if (!urlValue) { + checks.push({ + code: "openclaw_url_missing", + level: "error", + message: "OpenClaw adapter requires a webhook URL.", + hint: "Set adapterConfig.url to your OpenClaw webhook endpoint.", + }); + return { + adapterType: ctx.adapterType, + status: summarizeStatus(checks), + checks, + testedAt: new Date().toISOString(), + }; + } + + let url: URL | null = null; + try { + url = new URL(urlValue); + } catch { + checks.push({ + code: "openclaw_url_invalid", + level: "error", + message: `Invalid URL: ${urlValue}`, + }); + } + + if (url && url.protocol !== "http:" && url.protocol !== "https:") { + checks.push({ + code: "openclaw_url_protocol_invalid", + level: "error", + message: `Unsupported URL protocol: ${url.protocol}`, + hint: "Use an http:// or https:// endpoint.", + }); + } + + if (url) { + checks.push({ + code: "openclaw_url_valid", + level: "info", + message: `Configured endpoint: ${url.toString()}`, + }); + + if (isLoopbackHost(url.hostname)) { + checks.push({ + code: "openclaw_loopback_endpoint", + level: "warn", + message: "Endpoint uses loopback hostname. Remote OpenClaw workers cannot reach localhost on the Paperclip host.", + hint: "Use a reachable hostname/IP (for example Tailscale/private hostname or public domain).", + }); + } + } + + const method = asString(config.method, "POST").trim().toUpperCase() || "POST"; + checks.push({ + code: "openclaw_method_configured", + level: "info", + message: `Configured method: ${method}`, + }); + + if (url && (url.protocol === "http:" || url.protocol === "https:")) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + try { + const response = await fetch(url, { method: "HEAD", signal: controller.signal }); + if (!response.ok && response.status !== 405 && response.status !== 501) { + checks.push({ + code: "openclaw_endpoint_probe_unexpected_status", + level: "warn", + message: `Endpoint probe returned HTTP ${response.status}.`, + hint: "Verify OpenClaw webhook reachability and auth/network settings.", + }); + } else { + checks.push({ + code: "openclaw_endpoint_probe_ok", + level: "info", + message: "Endpoint responded to a HEAD probe.", + }); + } + } catch (err) { + checks.push({ + code: "openclaw_endpoint_probe_failed", + level: "warn", + message: err instanceof Error ? err.message : "Endpoint probe failed", + hint: "This may be expected in restricted networks; validate from the Paperclip server host.", + }); + } finally { + clearTimeout(timeout); + } + } + + return { + adapterType: ctx.adapterType, + status: summarizeStatus(checks), + checks, + testedAt: new Date().toISOString(), + }; +} diff --git a/packages/adapters/openclaw/src/ui/build-config.ts b/packages/adapters/openclaw/src/ui/build-config.ts new file mode 100644 index 00000000..376909c5 --- /dev/null +++ b/packages/adapters/openclaw/src/ui/build-config.ts @@ -0,0 +1,9 @@ +import type { CreateConfigValues } from "@paperclip/adapter-utils"; + +export function buildOpenClawConfig(v: CreateConfigValues): Record { + const ac: Record = {}; + if (v.url) ac.url = v.url; + ac.method = "POST"; + ac.timeoutSec = 30; + return ac; +} diff --git a/packages/adapters/openclaw/src/ui/index.ts b/packages/adapters/openclaw/src/ui/index.ts new file mode 100644 index 00000000..f3f1905e --- /dev/null +++ b/packages/adapters/openclaw/src/ui/index.ts @@ -0,0 +1,2 @@ +export { parseOpenClawStdoutLine } from "./parse-stdout.js"; +export { buildOpenClawConfig } from "./build-config.js"; diff --git a/packages/adapters/openclaw/src/ui/parse-stdout.ts b/packages/adapters/openclaw/src/ui/parse-stdout.ts new file mode 100644 index 00000000..8941fa94 --- /dev/null +++ b/packages/adapters/openclaw/src/ui/parse-stdout.ts @@ -0,0 +1,5 @@ +import type { TranscriptEntry } from "@paperclip/adapter-utils"; + +export function parseOpenClawStdoutLine(line: string, ts: string): TranscriptEntry[] { + return [{ kind: "stdout", ts, text: line }]; +} diff --git a/packages/adapters/openclaw/tsconfig.json b/packages/adapters/openclaw/tsconfig.json new file mode 100644 index 00000000..2f355cfe --- /dev/null +++ b/packages/adapters/openclaw/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index f0ecfcb2..0b3490ab 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -21,7 +21,7 @@ export const AGENT_STATUSES = [ ] as const; export type AgentStatus = (typeof AGENT_STATUSES)[number]; -export const AGENT_ADAPTER_TYPES = ["process", "http", "claude_local", "codex_local"] as const; +export const AGENT_ADAPTER_TYPES = ["process", "http", "claude_local", "codex_local", "openclaw"] as const; export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number]; export const AGENT_ROLES = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89f0cc2e..1c8b2db6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@paperclip/adapter-codex-local': specifier: workspace:* version: link:../packages/adapters/codex-local + '@paperclip/adapter-openclaw': + specifier: workspace:* + version: link:../packages/adapters/openclaw '@paperclip/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils @@ -93,6 +96,19 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/adapters/openclaw: + dependencies: + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../../adapter-utils + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/db: dependencies: '@paperclip/shared': @@ -139,6 +155,9 @@ importers: '@paperclip/adapter-codex-local': specifier: workspace:* version: link:../packages/adapters/codex-local + '@paperclip/adapter-openclaw': + specifier: workspace:* + version: link:../packages/adapters/openclaw '@paperclip/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils @@ -234,6 +253,9 @@ importers: '@paperclip/adapter-codex-local': specifier: workspace:* version: link:../packages/adapters/codex-local + '@paperclip/adapter-openclaw': + specifier: workspace:* + version: link:../packages/adapters/openclaw '@paperclip/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils diff --git a/server/package.json b/server/package.json index 418763e0..8120640b 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,7 @@ "dependencies": { "@paperclip/adapter-claude-local": "workspace:*", "@paperclip/adapter-codex-local": "workspace:*", + "@paperclip/adapter-openclaw": "workspace:*", "@paperclip/adapter-utils": "workspace:*", "@paperclip/db": "workspace:*", "@paperclip/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 8ec19af2..17f8e96b 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -11,6 +11,14 @@ import { sessionCodec as codexSessionCodec, } from "@paperclip/adapter-codex-local/server"; import { agentConfigurationDoc as codexAgentConfigurationDoc, models as codexModels } from "@paperclip/adapter-codex-local"; +import { + execute as openclawExecute, + testEnvironment as openclawTestEnvironment, +} from "@paperclip/adapter-openclaw/server"; +import { + agentConfigurationDoc as openclawAgentConfigurationDoc, + models as openclawModels, +} from "@paperclip/adapter-openclaw"; import { listCodexModels } from "./codex-models.js"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; @@ -36,8 +44,17 @@ const codexLocalAdapter: ServerAdapterModule = { agentConfigurationDoc: codexAgentConfigurationDoc, }; +const openclawAdapter: ServerAdapterModule = { + type: "openclaw", + execute: openclawExecute, + testEnvironment: openclawTestEnvironment, + models: openclawModels, + supportsLocalAgentJwt: false, + agentConfigurationDoc: openclawAgentConfigurationDoc, +}; + const adaptersByType = new Map( - [claudeLocalAdapter, codexLocalAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]), + [claudeLocalAdapter, codexLocalAdapter, openclawAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]), ); export function getServerAdapter(type: string): ServerAdapterModule { diff --git a/ui/package.json b/ui/package.json index cfc70405..67125b15 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "@mdxeditor/editor": "^3.52.4", "@paperclip/adapter-claude-local": "workspace:*", "@paperclip/adapter-codex-local": "workspace:*", + "@paperclip/adapter-openclaw": "workspace:*", "@paperclip/adapter-utils": "workspace:*", "@paperclip/shared": "workspace:*", "@radix-ui/react-slot": "^1.2.4", diff --git a/ui/src/adapters/openclaw/config-fields.tsx b/ui/src/adapters/openclaw/config-fields.tsx new file mode 100644 index 00000000..abad6b12 --- /dev/null +++ b/ui/src/adapters/openclaw/config-fields.tsx @@ -0,0 +1,53 @@ +import type { AdapterConfigFieldsProps } from "../types"; +import { + Field, + DraftInput, + help, +} from "../../components/agent-config-primitives"; + +const inputClass = + "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; + +export function OpenClawConfigFields({ + isCreate, + values, + set, + config, + eff, + mark, +}: AdapterConfigFieldsProps) { + return ( + <> + + + isCreate + ? set!({ url: v }) + : mark("adapterConfig", "url", v || undefined) + } + immediate + className={inputClass} + placeholder="https://..." + /> + + {!isCreate && ( + + mark("adapterConfig", "webhookAuthHeader", v || undefined)} + immediate + className={inputClass} + placeholder="Bearer " + /> + + )} + + ); +} diff --git a/ui/src/adapters/openclaw/index.ts b/ui/src/adapters/openclaw/index.ts new file mode 100644 index 00000000..5f03ec0b --- /dev/null +++ b/ui/src/adapters/openclaw/index.ts @@ -0,0 +1,12 @@ +import type { UIAdapterModule } from "../types"; +import { parseOpenClawStdoutLine } from "@paperclip/adapter-openclaw/ui"; +import { buildOpenClawConfig } from "@paperclip/adapter-openclaw/ui"; +import { OpenClawConfigFields } from "./config-fields"; + +export const openClawUIAdapter: UIAdapterModule = { + type: "openclaw", + label: "OpenClaw", + parseStdoutLine: parseOpenClawStdoutLine, + ConfigFields: OpenClawConfigFields, + buildAdapterConfig: buildOpenClawConfig, +}; diff --git a/ui/src/adapters/registry.ts b/ui/src/adapters/registry.ts index 4ae0d0bc..8dbe3637 100644 --- a/ui/src/adapters/registry.ts +++ b/ui/src/adapters/registry.ts @@ -1,11 +1,12 @@ import type { UIAdapterModule } from "./types"; import { claudeLocalUIAdapter } from "./claude-local"; import { codexLocalUIAdapter } from "./codex-local"; +import { openClawUIAdapter } from "./openclaw"; import { processUIAdapter } from "./process"; import { httpUIAdapter } from "./http"; const adaptersByType = new Map( - [claudeLocalUIAdapter, codexLocalUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]), + [claudeLocalUIAdapter, codexLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]), ); export function getUIAdapter(type: string): UIAdapterModule { diff --git a/ui/src/components/AgentProperties.tsx b/ui/src/components/AgentProperties.tsx index bafd5955..d9005e63 100644 --- a/ui/src/components/AgentProperties.tsx +++ b/ui/src/components/AgentProperties.tsx @@ -17,6 +17,7 @@ interface AgentPropertiesProps { const adapterLabels: Record = { claude_local: "Claude (local)", codex_local: "Codex (local)", + openclaw: "OpenClaw", process: "Process", http: "HTTP", }; @@ -72,7 +73,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) { )} {runtimeState?.lastError && ( - {runtimeState.lastError} + {runtimeState.lastError} )} {agent.lastHeartbeatAt && ( diff --git a/ui/src/components/agent-config-primitives.tsx b/ui/src/components/agent-config-primitives.tsx index b809a724..572c601f 100644 --- a/ui/src/components/agent-config-primitives.tsx +++ b/ui/src/components/agent-config-primitives.tsx @@ -14,11 +14,12 @@ export const help: Record = { role: "Organizational role. Determines position and capabilities.", reportsTo: "The agent this one reports to in the org hierarchy.", capabilities: "Describes what this agent can do. Shown in the org chart and used for task routing.", - adapterType: "How this agent runs: local CLI (Claude/Codex), spawned process, or HTTP webhook.", + adapterType: "How this agent runs: local CLI (Claude/Codex), OpenClaw webhook, spawned process, or generic HTTP webhook.", cwd: "Default working directory fallback for local adapters. Use an absolute path on the machine running Paperclip.", promptTemplate: "The prompt sent to the agent on each heartbeat. Supports {{ agent.id }}, {{ agent.name }}, {{ agent.role }} variables.", model: "Override the default model used by the adapter.", thinkingEffort: "Control model reasoning depth. Supported values vary by adapter/model.", + chrome: "Enable Claude's Chrome integration by passing --chrome.", dangerouslySkipPermissions: "Run Claude without permission prompts. Required for unattended operation.", dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.", search: "Enable Codex web search capability during runs.", @@ -43,6 +44,7 @@ export const help: Record = { export const adapterLabels: Record = { claude_local: "Claude (local)", codex_local: "Codex (local)", + openclaw: "OpenClaw", process: "Process", http: "HTTP", }; diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index 410330df..ad75ab95 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -22,6 +22,7 @@ import type { Agent } from "@paperclip/shared"; const adapterLabels: Record = { claude_local: "Claude", codex_local: "Codex", + openclaw: "OpenClaw", process: "Process", http: "HTTP", }; @@ -398,7 +399,7 @@ function LiveRunIndicator({ - + Live{liveCount > 1 ? ` (${liveCount})` : ""} diff --git a/ui/src/pages/OrgChart.tsx b/ui/src/pages/OrgChart.tsx index ba42bbfc..7f5cb387 100644 --- a/ui/src/pages/OrgChart.tsx +++ b/ui/src/pages/OrgChart.tsx @@ -116,6 +116,7 @@ function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; child: L const adapterLabels: Record = { claude_local: "Claude", codex_local: "Codex", + openclaw: "OpenClaw", process: "Process", http: "HTTP", };