Claude Code
Claude Code: Create a WebSocket endpoint in using Gradio's queuing mechanism
19f8a70
/**
* Coding Agent Extension for OpenClaw
*
* Integrates Claude Code (backed by Zhipu GLM via z.ai) as a sub-agent
* for autonomous coding on HuggingFace Spaces.
*
* Tools:
* - claude_code: Spawn Claude Code CLI to autonomously complete coding tasks
* - hf_space_status: Check Space health/stage
* - hf_restart_space: Restart a Space
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { WebSocket } from "ws";
// ── Types ────────────────────────────────────────────────────────────────────
interface PluginApi {
pluginConfig: Record<string, unknown>;
logger: { info: (...a: unknown[]) => void; warn: (...a: unknown[]) => void; error: (...a: unknown[]) => void };
registerTool?: (def: ToolDef) => void;
}
interface ToolDef {
name: string;
description: string;
label?: string;
parameters: Record<string, unknown>;
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
}
interface ToolResult {
content: Array<{ type: "text"; text: string }>;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
const WORK_DIR = "/tmp/claude-workspace";
const CLAUDE_TIMEOUT = 300_000; // 5 minutes
function text(t: string): ToolResult {
return { content: [{ type: "text", text: t }] };
}
function asStr(v: unknown, fallback = ""): string {
return typeof v === "string" ? v : fallback;
}
// ── WebSocket Client for Agent Status ─────────────────────────────────────────
interface AgentStatus {
current_state: string;
last_updated: string;
agent: string;
error?: string;
}
let wsClient: WebSocket | null = null;
let statusListeners: Array<(status: AgentStatus) => void> = [];
let apiLogger: PluginApi["logger"] | null = null;
function connectAgentStatusWebSocket(apiBaseUrl: string = "http://127.0.0.1:7860"): void {
// Determine WebSocket URL from API base URL
const wsUrl = apiBaseUrl.replace("http://", "ws://").replace("https://", "wss://") + "/ws/agent-status";
if (wsClient?.readyState === WebSocket.OPEN) {
apiLogger?.info("coding-agent: WebSocket already connected");
return;
}
try {
wsClient = new WebSocket(wsUrl);
wsClient.on("open", () => {
apiLogger?.info("coding-agent: Agent Status Connected");
});
wsClient.on("message", (data: Buffer) => {
try {
const status: AgentStatus = JSON.parse(data.toString());
// Notify all listeners
statusListeners.forEach(listener => {
try {
listener(status);
} catch (e) {
apiLogger?.error(`coding-agent: Listener error: ${e}`);
}
});
} catch (e) {
apiLogger?.error(`coding-agent: Failed to parse status: ${e}`);
}
});
wsClient.on("error", (error) => {
apiLogger?.error(`coding-agent: WebSocket error: ${error}`);
});
wsClient.on("close", () => {
apiLogger?.warn("coding-agent: WebSocket disconnected, reconnecting in 5s");
wsClient = null;
setTimeout(() => connectAgentStatusWebSocket(apiBaseUrl), 5000);
});
} catch (e) {
apiLogger?.error(`coding-agent: Failed to create WebSocket: ${e}`);
}
}
function disconnectAgentStatusWebSocket(): void {
if (wsClient) {
wsClient.close();
wsClient = null;
}
statusListeners = [];
}
// ── Git helpers ──────────────────────────────────────────────────────────────
function ensureRepo(targetSpace: string, hfToken: string): void {
const repoUrl = `https://user:${hfToken}@huggingface.co/spaces/${targetSpace}`;
if (existsSync(`${WORK_DIR}/.git`)) {
// Reset to latest remote state (clean slate for each task)
try {
execSync("git fetch origin && git reset --hard origin/main", {
cwd: WORK_DIR,
timeout: 30_000,
stdio: "pipe",
});
return;
} catch {
// If fetch/reset fails, re-clone
execSync(`rm -rf ${WORK_DIR}`, { stdio: "pipe" });
}
}
// Fresh clone
if (existsSync(WORK_DIR)) {
execSync(`rm -rf ${WORK_DIR}`, { stdio: "pipe" });
}
execSync(`git clone --depth 20 ${repoUrl} ${WORK_DIR}`, {
timeout: 60_000,
stdio: "pipe",
});
execSync('git config user.name "Claude Code"', { cwd: WORK_DIR, stdio: "pipe" });
execSync('git config user.email "claude-code@huggingclaw"', { cwd: WORK_DIR, stdio: "pipe" });
}
function pushChanges(summary: string): string {
const status = execSync("git status --porcelain", {
cwd: WORK_DIR,
encoding: "utf-8",
}).trim();
if (!status) return "No files changed.";
execSync("git add -A", { cwd: WORK_DIR, stdio: "pipe" });
// Use a safe commit message
const msg = summary.slice(0, 72).replace(/"/g, '\\"');
execSync(`git commit -m "Claude Code: ${msg}"`, { cwd: WORK_DIR, stdio: "pipe" });
execSync("git push", { cwd: WORK_DIR, timeout: 60_000, stdio: "pipe" });
return `Pushed changes:\n${status}`;
}
// ── Plugin ───────────────────────────────────────────────────────────────────
const plugin = {
id: "coding-agent",
name: "Coding Agent",
description: "Claude Code sub-agent for autonomous coding on HF Spaces (Zhipu GLM backend via z.ai)",
register(api: PluginApi) {
// Store logger reference for WebSocket client
apiLogger = api.logger;
const cfg = (api.pluginConfig as Record<string, unknown>) || {};
const targetSpace = asStr(cfg.targetSpace) || process.env.CODING_AGENT_TARGET_SPACE || "";
const hfToken = asStr(cfg.hfToken) || process.env.HF_TOKEN || "";
const zaiApiKey = asStr(cfg.zaiApiKey) || process.env.ZAI_API_KEY || process.env.ZHIPU_API_KEY || "";
const apiBaseUrl = asStr(cfg.apiBaseUrl) || process.env.CAIN_API_URL || "http://127.0.0.1:7860";
api.logger.info(`coding-agent: targetSpace=${targetSpace}, zaiKey=${zaiApiKey ? "set" : "missing"}`);
// Auto-connect to agent status WebSocket
try {
connectAgentStatusWebSocket(apiBaseUrl);
} catch (e) {
api.logger.warn(`coding-agent: Could not connect to WebSocket: ${e}`);
}
if (!api.registerTool) {
api.logger.warn("coding-agent: registerTool unavailable β€” no tools registered");
return;
}
// ── Tool: claude_code ───────────────────────────────────────────────────
api.registerTool({
name: "claude_code",
label: "Run Claude Code",
description:
"Run Claude Code to autonomously complete a coding task on the target HF Space. " +
"Claude Code clones the Space repo, analyzes code, makes changes, and pushes them back. " +
"Powered by Zhipu GLM via z.ai. Use for: debugging, fixing errors, adding features, refactoring.",
parameters: {
type: "object",
required: ["task"],
properties: {
task: {
type: "string",
description: "Detailed coding task description. Be specific about what to fix/change and why.",
},
auto_push: {
type: "boolean",
description: "Automatically push changes after Claude Code finishes (default: true)",
},
},
},
async execute(_id, params) {
const task = asStr(params.task);
const autoPush = params.auto_push !== false;
if (!targetSpace) return text("Error: no targetSpace configured");
if (!hfToken) return text("Error: no HF token configured");
if (!zaiApiKey) return text("Error: no ZAI_API_KEY or ZHIPU_API_KEY configured for Claude Code backend");
try {
// 1. Clone / reset to latest
api.logger.info(`coding-agent: Syncing repo ${targetSpace}...`);
ensureRepo(targetSpace, hfToken);
// 2. Run Claude Code with z.ai backend
api.logger.info(`coding-agent: Running Claude Code: ${task.slice(0, 100)}...`);
const claudeEnv: Record<string, string> = {
...(process.env as Record<string, string>),
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
ANTHROPIC_AUTH_TOKEN: zaiApiKey,
ANTHROPIC_DEFAULT_OPUS_MODEL: "GLM-4.7",
ANTHROPIC_DEFAULT_SONNET_MODEL: "GLM-4.7",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "GLM-4.5-Air",
// Avoid interactive prompts
CI: "true",
};
const output = execSync(
`claude -p ${JSON.stringify(task)} --output-format text`,
{
cwd: WORK_DIR,
env: claudeEnv,
timeout: CLAUDE_TIMEOUT,
encoding: "utf-8",
maxBuffer: 10 * 1024 * 1024, // 10MB
},
);
// 3. Push changes if requested
let pushResult = "Auto-push disabled.";
if (autoPush) {
try {
pushResult = pushChanges(task);
} catch (e: unknown) {
pushResult = `Push failed: ${e instanceof Error ? e.message : e}`;
}
}
return text(
`=== Claude Code Output ===\n${output}\n\n=== Changes ===\n${pushResult}`,
);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return text(`Claude Code failed:\n${msg.slice(0, 3000)}`);
}
},
});
// ── Tool: hf_space_status ───────────────────────────────────────────────
api.registerTool({
name: "hf_space_status",
label: "Check Space Health",
description:
"Check the current status of the target HuggingFace Space. " +
"Returns: stage (BUILDING, APP_STARTING, RUNNING, RUNTIME_ERROR, BUILD_ERROR, NO_APP_FILE).",
parameters: {
type: "object",
properties: {},
},
async execute() {
if (!targetSpace) return text("Error: no target space configured");
try {
const resp = await fetch(`https://huggingface.co/api/spaces/${targetSpace}`, {
headers: { Authorization: `Bearer ${hfToken}` },
});
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
const data = (await resp.json()) as Record<string, unknown>;
const runtime = (data.runtime as Record<string, unknown>) || {};
const stage = runtime.stage || "unknown";
const hardware = runtime.hardware || "unknown";
// Try hitting the Space URL
let apiStatus = "not checked";
try {
const spaceUrl = `https://${targetSpace.replace("/", "-").toLowerCase()}.hf.space`;
const probe = await fetch(spaceUrl, { signal: AbortSignal.timeout(8000) });
apiStatus = probe.ok ? `reachable (${probe.status})` : `error (${probe.status})`;
} catch {
apiStatus = "unreachable";
}
return text(
`Space: ${targetSpace}\nStage: ${stage}\nHardware: ${hardware}\nAPI: ${apiStatus}`,
);
} catch (e: unknown) {
return text(`Error checking space: ${e instanceof Error ? e.message : e}`);
}
},
});
// ── Tool: hf_restart_space ──────────────────────────────────────────────
api.registerTool({
name: "hf_restart_space",
label: "Restart Space",
description: "Restart the target HuggingFace Space. Use when the Space is stuck or after deploying fixes.",
parameters: {
type: "object",
properties: {},
},
async execute() {
if (!targetSpace) return text("Error: no target space configured");
try {
const resp = await fetch(`https://huggingface.co/api/spaces/${targetSpace}/restart`, {
method: "POST",
headers: { Authorization: `Bearer ${hfToken}` },
});
if (!resp.ok) {
const body = await resp.text().catch(() => "");
throw new Error(`${resp.status}: ${body}`);
}
return text(`Space ${targetSpace} is restarting`);
} catch (e: unknown) {
return text(`Error restarting space: ${e instanceof Error ? e.message : e}`);
}
},
});
// ── Tool: agent_status_ws ───────────────────────────────────────────────────
api.registerTool({
name: "agent_status_ws",
label: "Agent Status WebSocket",
description:
"Get the current agent status from the WebSocket connection. " +
"Returns the latest status received from the agent status WebSocket including state, last updated time, and any errors.",
parameters: {
type: "object",
properties: {},
},
async execute() {
// Check if WebSocket is connected
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
return text("WebSocket not connected. Ensure the API server is running and WebSocket is available.");
}
// Return current connection status
const status = `Agent Status WebSocket:\n- Connected: ${wsClient.readyState === WebSocket.OPEN}\n- URL: ${wsClient.url}\n- Active listeners: ${statusListeners.length}`;
// Note: The latest status is received asynchronously via the WebSocket
// Users can check the logs for real-time status updates
return text(`${status}\n\nLatest status updates are logged in real-time.`);
},
});
api.logger.info("coding-agent: Registered 4 tools (claude_code, hf_space_status, hf_restart_space, agent_status_ws)");
},
};
export default plugin;