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", };