tao-shen Claude Opus 4.6 commited on
Commit
d653b4e
Β·
1 Parent(s): b44dfca

feat: integrate Claude Code as coding-agent backend (z.ai/Zhipu GLM)

Browse files

Replace custom HF API tools with Claude Code CLI sub-agent. The extension
now spawns `claude -p` with ANTHROPIC_BASE_URL=api.z.ai to autonomously
clone, analyze, fix, and push code changes to HF Spaces.

- Dockerfile: install @anthropic-ai/claude-code globally
- index.ts: 3 tools (claude_code, hf_space_status, hf_restart_space)
- sync_hf.py: add ZAI_API_KEY env var passthrough
- plugin.json: add zaiApiKey config field, bump to v2.0.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Dockerfile CHANGED
@@ -21,6 +21,11 @@ RUN echo "[build] Installing system deps..." && START=$(date +%s) \
21
  && chown -R node:node /home/node \
22
  && echo "[build] System deps: $(($(date +%s) - START))s"
23
 
 
 
 
 
 
24
  # ── Copy pre-built OpenClaw (skips clone + install + build entirely) ─────────
25
  COPY --from=openclaw-prebuilt --chown=node:node /app /app/openclaw
26
 
 
21
  && chown -R node:node /home/node \
22
  && echo "[build] System deps: $(($(date +%s) - START))s"
23
 
24
+ # ── Claude Code CLI (for coding agent extension β€” uses z.ai/Zhipu GLM) ──────
25
+ RUN echo "[build] Installing Claude Code CLI..." && START=$(date +%s) \
26
+ && npm install -g @anthropic-ai/claude-code \
27
+ && echo "[build] Claude Code CLI: $(($(date +%s) - START))s"
28
+
29
  # ── Copy pre-built OpenClaw (skips clone + install + build entirely) ─────────
30
  COPY --from=openclaw-prebuilt --chown=node:node /app /app/openclaw
31
 
extensions/coding-agent/index.ts CHANGED
@@ -1,15 +1,17 @@
1
  /**
2
  * Coding Agent Extension for OpenClaw
3
  *
4
- * Registers tools that let the OpenClaw agent manage HuggingFace Spaces:
5
- * - Read/write/delete files on Space and Dataset repos
6
- * - Check Space health and restart
7
- * - Search code patterns (grep-like)
8
- * - Validate Python syntax before writing
9
- * - List files in repos
 
10
  */
11
 
12
  import { execSync } from "node:child_process";
 
13
 
14
  // ── Types ────────────────────────────────────────────────────────────────────
15
 
@@ -17,7 +19,6 @@ interface PluginApi {
17
  pluginConfig: Record<string, unknown>;
18
  logger: { info: (...a: unknown[]) => void; warn: (...a: unknown[]) => void; error: (...a: unknown[]) => void };
19
  registerTool?: (def: ToolDef) => void;
20
- resolvePath?: (p: string) => string;
21
  }
22
 
23
  interface ToolDef {
@@ -30,163 +31,68 @@ interface ToolDef {
30
 
31
  interface ToolResult {
32
  content: Array<{ type: "text"; text: string }>;
33
- details?: Record<string, unknown>;
34
- }
35
-
36
- // ── HuggingFace API helpers ──────────────────────────────────────────────────
37
-
38
- const HF_API = "https://huggingface.co/api";
39
-
40
- async function hfFetch(path: string, token: string, options: RequestInit = {}): Promise<Response> {
41
- const url = path.startsWith("http") ? path : `${HF_API}${path}`;
42
- return fetch(url, {
43
- ...options,
44
- headers: {
45
- Authorization: `Bearer ${token}`,
46
- ...((options.headers as Record<string, string>) || {}),
47
- },
48
- });
49
- }
50
-
51
- async function hfReadFile(repoType: string, repoId: string, filePath: string, token: string): Promise<string> {
52
- const url = `https://huggingface.co/${repoType}s/${repoId}/resolve/main/${filePath}`;
53
- const resp = await hfFetch(url, token);
54
- if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}: ${filePath}`);
55
- return resp.text();
56
  }
57
 
58
- async function hfWriteFile(repoType: string, repoId: string, filePath: string, content: string, token: string): Promise<string> {
59
- const url = `${HF_API}/${repoType}s/${repoId}/upload/main/${filePath}`;
60
- const blob = new Blob([content], { type: "text/plain" });
61
- const form = new FormData();
62
- form.append("file", blob, filePath);
63
- const resp = await hfFetch(url, token, { method: "POST", body: form });
64
- if (!resp.ok) {
65
- const text = await resp.text().catch(() => "");
66
- throw new Error(`${resp.status}: ${text}`);
67
- }
68
- return `Wrote ${content.length} bytes to ${repoType}:${filePath}`;
69
- }
70
 
71
- async function hfDeleteFile(repoType: string, repoId: string, filePath: string, token: string): Promise<string> {
72
- // Use the HF Hub commit API to delete a file
73
- const url = `${HF_API}/${repoType}s/${repoId}/commit/main`;
74
- const body = {
75
- summary: `Delete ${filePath}`,
76
- operations: [{ op: "delete", path: filePath }],
77
- };
78
- const resp = await hfFetch(url, token, {
79
- method: "POST",
80
- headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify(body),
82
- });
83
- if (!resp.ok) {
84
- const text = await resp.text().catch(() => "");
85
- throw new Error(`${resp.status}: ${text}`);
86
- }
87
- return `Deleted ${repoType}:${filePath}`;
88
- }
89
 
90
- async function hfListFiles(repoType: string, repoId: string, token: string): Promise<string[]> {
91
- const url = `${HF_API}/${repoType}s/${repoId}`;
92
- const resp = await hfFetch(url, token);
93
- if (!resp.ok) throw new Error(`${resp.status}: listing failed`);
94
- const data = await resp.json() as Record<string, unknown>;
95
- const siblings = (data.siblings || []) as Array<{ rfilename: string }>;
96
- return siblings.map((s) => s.rfilename);
97
  }
98
 
99
- async function hfSpaceInfo(spaceId: string, token: string): Promise<Record<string, unknown>> {
100
- const resp = await hfFetch(`/spaces/${spaceId}`, token);
101
- if (!resp.ok) throw new Error(`${resp.status}: space info failed`);
102
- return resp.json() as Promise<Record<string, unknown>>;
103
  }
104
 
105
- async function hfRestartSpace(spaceId: string, token: string): Promise<string> {
106
- const resp = await hfFetch(`/spaces/${spaceId}/restart`, token, { method: "POST" });
107
- if (!resp.ok) {
108
- const text = await resp.text().catch(() => "");
109
- throw new Error(`${resp.status}: ${text}`);
110
- }
111
- return `Space ${spaceId} is restarting`;
112
- }
113
 
114
- // ── Python syntax checker ────────────────────────────────────────────────────
 
115
 
116
- function checkPythonSyntax(code: string): { valid: boolean; error?: string } {
117
- try {
118
- execSync(`python3 -c "import ast; ast.parse('''${code.replace(/'/g, "\\'")}''')"`, {
119
- timeout: 5000,
120
- stdio: ["pipe", "pipe", "pipe"],
121
- });
122
- return { valid: true };
123
- } catch (e: unknown) {
124
- // Fallback: write to temp file and check
125
  try {
126
- const tmpFile = `/tmp/_syntax_check_${Date.now()}.py`;
127
- require("node:fs").writeFileSync(tmpFile, code);
128
- execSync(`python3 -c "import ast; ast.parse(open('${tmpFile}').read())"`, {
129
- timeout: 5000,
130
- stdio: ["pipe", "pipe", "pipe"],
131
  });
132
- require("node:fs").unlinkSync(tmpFile);
133
- return { valid: true };
134
- } catch (e2: unknown) {
135
- const msg = e2 instanceof Error ? e2.message : String(e2);
136
- // Extract the actual syntax error line
137
- const match = msg.match(/SyntaxError:.*|File.*line \d+/g);
138
- return { valid: false, error: match ? match.join("\n") : msg.slice(0, 500) };
139
- }
140
- }
141
- }
142
-
143
- // ── Code search helper ───────────────────────────────────────────────────────
144
-
145
- async function searchInRepo(repoType: string, repoId: string, pattern: string, token: string, fileGlob?: string): Promise<string> {
146
- const files = await hfListFiles(repoType, repoId, token);
147
- const codeFiles = files.filter((f) => {
148
- const ext = f.split(".").pop()?.toLowerCase() || "";
149
- const isCode = ["py", "js", "ts", "json", "yaml", "yml", "md", "txt", "sh", "css", "html", "toml", "cfg", "ini"].includes(ext);
150
- if (fileGlob) {
151
- // Simple glob: *.py matches .py extension
152
- const globExt = fileGlob.replace("*.", "");
153
- return f.endsWith(`.${globExt}`);
154
- }
155
- return isCode;
156
- });
157
-
158
- const results: string[] = [];
159
- const regex = new RegExp(pattern, "gi");
160
-
161
- for (const file of codeFiles.slice(0, 30)) {
162
- try {
163
- const content = await hfReadFile(repoType, repoId, file, token);
164
- const lines = content.split("\n");
165
- for (let i = 0; i < lines.length; i++) {
166
- if (regex.test(lines[i])) {
167
- results.push(`${file}:${i + 1}: ${lines[i].trim()}`);
168
- }
169
- regex.lastIndex = 0; // Reset regex state
170
- }
171
  } catch {
172
- // Skip unreadable files (binary, too large, etc.)
 
173
  }
174
- if (results.length >= 50) break;
175
  }
176
 
177
- return results.length > 0
178
- ? `Found ${results.length} match(es):\n${results.join("\n")}`
179
- : `No matches for "${pattern}" in ${codeFiles.length} files`;
 
 
 
 
 
 
 
180
  }
181
 
182
- // ── Helpers ──────────────────────────────────────────────────────────────────
 
 
 
 
183
 
184
- function text(t: string): ToolResult {
185
- return { content: [{ type: "text", text: t }] };
186
- }
187
 
188
- function asStr(v: unknown, fallback = ""): string {
189
- return typeof v === "string" ? v : fallback;
 
 
 
 
 
190
  }
191
 
192
  // ── Plugin ──────────────��────────────────────────────────────────────────────
@@ -194,153 +100,96 @@ function asStr(v: unknown, fallback = ""): string {
194
  const plugin = {
195
  id: "coding-agent",
196
  name: "Coding Agent",
197
- description: "HuggingFace Space coding tools with syntax validation",
198
 
199
  register(api: PluginApi) {
200
- const cfg = api.pluginConfig as Record<string, unknown> || {};
201
  const targetSpace = asStr(cfg.targetSpace) || process.env.CODING_AGENT_TARGET_SPACE || "";
202
- const targetDataset = asStr(cfg.targetDataset) || process.env.CODING_AGENT_TARGET_DATASET || "";
203
  const hfToken = asStr(cfg.hfToken) || process.env.HF_TOKEN || "";
 
204
 
205
- if (!hfToken) {
206
- api.logger.warn("coding-agent: No HF token configured β€” tools will fail");
207
- }
208
- api.logger.info(`coding-agent: targetSpace=${targetSpace}, targetDataset=${targetDataset}`);
209
 
210
  if (!api.registerTool) {
211
  api.logger.warn("coding-agent: registerTool unavailable β€” no tools registered");
212
  return;
213
  }
214
 
215
- // ── Tool: hf_read_file ──────────────────────────────────────────────────
216
  api.registerTool({
217
- name: "hf_read_file",
218
- label: "Read File from HF Repo",
219
  description:
220
- "Read a file from the target HuggingFace Space or Dataset. " +
221
- "Use repo='space' for code files, repo='dataset' for data/memory files.",
 
222
  parameters: {
223
  type: "object",
224
- required: ["repo", "path"],
225
  properties: {
226
- repo: { type: "string", enum: ["space", "dataset"], description: "Which repo: 'space' (code) or 'dataset' (data)" },
227
- path: { type: "string", description: "File path within the repo (e.g. app.py, scripts/entrypoint.sh)" },
 
 
 
 
 
 
228
  },
229
  },
230
  async execute(_id, params) {
231
- const repo = asStr(params.repo);
232
- const path = asStr(params.path);
233
- const repoId = repo === "dataset" ? targetDataset : targetSpace;
234
- if (!repoId) return text(`Error: no target ${repo} configured`);
235
- try {
236
- const content = await hfReadFile(repo === "dataset" ? "dataset" : "space", repoId, path, hfToken);
237
- return text(`=== ${repo}:${path} ===\n${content}`);
238
- } catch (e: unknown) {
239
- return text(`Error reading ${repo}:${path}: ${e instanceof Error ? e.message : e}`);
240
- }
241
- },
242
- });
243
 
244
- // ── Tool: hf_write_file ─────────────────────────────────────────────────
245
- api.registerTool({
246
- name: "hf_write_file",
247
- label: "Write File to HF Repo",
248
- description:
249
- "Write/update a file in the target HuggingFace Space or Dataset. " +
250
- "For .py files, syntax is automatically validated before writing. " +
251
- "Writing to the Space triggers a rebuild. Use repo='space' for code, repo='dataset' for data.",
252
- parameters: {
253
- type: "object",
254
- required: ["repo", "path", "content"],
255
- properties: {
256
- repo: { type: "string", enum: ["space", "dataset"], description: "Which repo: 'space' or 'dataset'" },
257
- path: { type: "string", description: "File path (e.g. app.py)" },
258
- content: { type: "string", description: "Full file content to write" },
259
- skip_syntax_check: { type: "boolean", description: "Skip Python syntax validation (default: false)" },
260
- },
261
- },
262
- async execute(_id, params) {
263
- const repo = asStr(params.repo);
264
- const path = asStr(params.path);
265
- const content = asStr(params.content);
266
- const skipCheck = params.skip_syntax_check === true;
267
- const repoType = repo === "dataset" ? "dataset" : "space";
268
- const repoId = repo === "dataset" ? targetDataset : targetSpace;
269
- if (!repoId) return text(`Error: no target ${repo} configured`);
270
-
271
- // Auto syntax check for Python files
272
- if (path.endsWith(".py") && !skipCheck) {
273
- const check = checkPythonSyntax(content);
274
- if (!check.valid) {
275
- return text(
276
- `SYNTAX ERROR β€” file NOT written.\n` +
277
- `File: ${path}\n` +
278
- `Error: ${check.error}\n\n` +
279
- `Fix the syntax error and try again. Use skip_syntax_check=true to force write.`
280
- );
281
- }
282
- }
283
 
284
  try {
285
- const result = await hfWriteFile(repoType, repoId, path, content, hfToken);
286
- const note = repo === "space" ? " (triggers Space rebuild)" : "";
287
- return text(`${result}${note}`);
288
- } catch (e: unknown) {
289
- return text(`Error writing ${repo}:${path}: ${e instanceof Error ? e.message : e}`);
290
- }
291
- },
292
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- // ── Tool: hf_delete_file ────────────────────────────────────────────────
295
- api.registerTool({
296
- name: "hf_delete_file",
297
- label: "Delete File from HF Repo",
298
- description: "Delete a file from the target Space or Dataset repo.",
299
- parameters: {
300
- type: "object",
301
- required: ["repo", "path"],
302
- properties: {
303
- repo: { type: "string", enum: ["space", "dataset"], description: "Which repo" },
304
- path: { type: "string", description: "File path to delete" },
305
- },
306
- },
307
- async execute(_id, params) {
308
- const repo = asStr(params.repo);
309
- const path = asStr(params.path);
310
- const repoType = repo === "dataset" ? "dataset" : "space";
311
- const repoId = repo === "dataset" ? targetDataset : targetSpace;
312
- if (!repoId) return text(`Error: no target ${repo} configured`);
313
- try {
314
- const result = await hfDeleteFile(repoType, repoId, path, hfToken);
315
- return text(result);
316
- } catch (e: unknown) {
317
- return text(`Error deleting ${repo}:${path}: ${e instanceof Error ? e.message : e}`);
318
- }
319
- },
320
- });
321
 
322
- // ── Tool: hf_list_files ─────────────────────────────────────────────────
323
- api.registerTool({
324
- name: "hf_list_files",
325
- label: "List Files in HF Repo",
326
- description: "List all files in the target Space or Dataset repo.",
327
- parameters: {
328
- type: "object",
329
- required: ["repo"],
330
- properties: {
331
- repo: { type: "string", enum: ["space", "dataset"], description: "Which repo to list" },
332
- },
333
- },
334
- async execute(_id, params) {
335
- const repo = asStr(params.repo);
336
- const repoType = repo === "dataset" ? "dataset" : "space";
337
- const repoId = repo === "dataset" ? targetDataset : targetSpace;
338
- if (!repoId) return text(`Error: no target ${repo} configured`);
339
- try {
340
- const files = await hfListFiles(repoType, repoId, hfToken);
341
- return text(`Files in ${repoId} (${files.length}):\n${files.map((f) => ` ${f}`).join("\n")}`);
342
  } catch (e: unknown) {
343
- return text(`Error listing ${repo}: ${e instanceof Error ? e.message : e}`);
 
344
  }
345
  },
346
  });
@@ -351,7 +200,7 @@ const plugin = {
351
  label: "Check Space Health",
352
  description:
353
  "Check the current status of the target HuggingFace Space. " +
354
- "Returns: stage (BUILDING, APP_STARTING, RUNNING, RUNTIME_ERROR, BUILD_ERROR, etc.)",
355
  parameters: {
356
  type: "object",
357
  properties: {},
@@ -359,26 +208,27 @@ const plugin = {
359
  async execute() {
360
  if (!targetSpace) return text("Error: no target space configured");
361
  try {
362
- const info = await hfSpaceInfo(targetSpace, hfToken);
363
- const runtime = info.runtime as Record<string, unknown> | undefined;
364
- const stage = runtime?.stage || "unknown";
365
- const hardware = runtime?.hardware || "unknown";
366
-
367
- // Also try hitting the API endpoint
 
 
 
 
368
  let apiStatus = "not checked";
369
  try {
370
  const spaceUrl = `https://${targetSpace.replace("/", "-").toLowerCase()}.hf.space`;
371
- const resp = await fetch(`${spaceUrl}/api/state`, { signal: AbortSignal.timeout(8000) });
372
- apiStatus = resp.ok ? `OK (${resp.status})` : `error (${resp.status})`;
373
  } catch {
374
  apiStatus = "unreachable";
375
  }
376
 
377
  return text(
378
- `Space: ${targetSpace}\n` +
379
- `Stage: ${stage}\n` +
380
- `Hardware: ${hardware}\n` +
381
- `API: ${apiStatus}`
382
  );
383
  } catch (e: unknown) {
384
  return text(`Error checking space: ${e instanceof Error ? e.message : e}`);
@@ -398,71 +248,22 @@ const plugin = {
398
  async execute() {
399
  if (!targetSpace) return text("Error: no target space configured");
400
  try {
401
- const result = await hfRestartSpace(targetSpace, hfToken);
402
- return text(result);
 
 
 
 
 
 
 
403
  } catch (e: unknown) {
404
  return text(`Error restarting space: ${e instanceof Error ? e.message : e}`);
405
  }
406
  },
407
  });
408
 
409
- // ── Tool: hf_search_code ────────────────────────────────────────────────
410
- api.registerTool({
411
- name: "hf_search_code",
412
- label: "Search Code in Repo",
413
- description:
414
- "Search for a pattern (regex) across all code files in the Space or Dataset. " +
415
- "Like grep β€” returns matching lines with file:line references.",
416
- parameters: {
417
- type: "object",
418
- required: ["repo", "pattern"],
419
- properties: {
420
- repo: { type: "string", enum: ["space", "dataset"], description: "Which repo to search" },
421
- pattern: { type: "string", description: "Regex pattern to search for (e.g. 'import gradio', 'port.*7860')" },
422
- file_glob: { type: "string", description: "Optional file filter (e.g. '*.py' to search only Python files)" },
423
- },
424
- },
425
- async execute(_id, params) {
426
- const repo = asStr(params.repo);
427
- const pattern = asStr(params.pattern);
428
- const fileGlob = asStr(params.file_glob);
429
- const repoType = repo === "dataset" ? "dataset" : "space";
430
- const repoId = repo === "dataset" ? targetDataset : targetSpace;
431
- if (!repoId) return text(`Error: no target ${repo} configured`);
432
- try {
433
- const result = await searchInRepo(repoType, repoId, pattern, hfToken, fileGlob || undefined);
434
- return text(result);
435
- } catch (e: unknown) {
436
- return text(`Error searching ${repo}: ${e instanceof Error ? e.message : e}`);
437
- }
438
- },
439
- });
440
-
441
- // ── Tool: python_syntax_check ───────────────────────────────────────────
442
- api.registerTool({
443
- name: "python_syntax_check",
444
- label: "Check Python Syntax",
445
- description:
446
- "Validate Python code syntax without writing it. " +
447
- "Use this to verify code before committing changes. Returns OK or the specific syntax error.",
448
- parameters: {
449
- type: "object",
450
- required: ["code"],
451
- properties: {
452
- code: { type: "string", description: "Python code to validate" },
453
- },
454
- },
455
- async execute(_id, params) {
456
- const code = asStr(params.code);
457
- const result = checkPythonSyntax(code);
458
- if (result.valid) {
459
- return text("Syntax OK β€” code is valid Python");
460
- }
461
- return text(`SYNTAX ERROR:\n${result.error}`);
462
- },
463
- });
464
-
465
- api.logger.info(`coding-agent: Registered 8 tools for ${targetSpace || "(no target space)"}`);
466
  },
467
  };
468
 
 
1
  /**
2
  * Coding Agent Extension for OpenClaw
3
  *
4
+ * Integrates Claude Code (backed by Zhipu GLM via z.ai) as a sub-agent
5
+ * for autonomous coding on HuggingFace Spaces.
6
+ *
7
+ * Tools:
8
+ * - claude_code: Spawn Claude Code CLI to autonomously complete coding tasks
9
+ * - hf_space_status: Check Space health/stage
10
+ * - hf_restart_space: Restart a Space
11
  */
12
 
13
  import { execSync } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
 
16
  // ── Types ────────────────────────────────────────────────────────────────────
17
 
 
19
  pluginConfig: Record<string, unknown>;
20
  logger: { info: (...a: unknown[]) => void; warn: (...a: unknown[]) => void; error: (...a: unknown[]) => void };
21
  registerTool?: (def: ToolDef) => void;
 
22
  }
23
 
24
  interface ToolDef {
 
31
 
32
  interface ToolResult {
33
  content: Array<{ type: "text"; text: string }>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
+ // ── Helpers ──────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ const WORK_DIR = "/tmp/claude-workspace";
39
+ const CLAUDE_TIMEOUT = 300_000; // 5 minutes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ function text(t: string): ToolResult {
42
+ return { content: [{ type: "text", text: t }] };
 
 
 
 
 
43
  }
44
 
45
+ function asStr(v: unknown, fallback = ""): string {
46
+ return typeof v === "string" ? v : fallback;
 
 
47
  }
48
 
49
+ // ── Git helpers ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
50
 
51
+ function ensureRepo(targetSpace: string, hfToken: string): void {
52
+ const repoUrl = `https://user:${hfToken}@huggingface.co/spaces/${targetSpace}`;
53
 
54
+ if (existsSync(`${WORK_DIR}/.git`)) {
55
+ // Reset to latest remote state (clean slate for each task)
 
 
 
 
 
 
 
56
  try {
57
+ execSync("git fetch origin && git reset --hard origin/main", {
58
+ cwd: WORK_DIR,
59
+ timeout: 30_000,
60
+ stdio: "pipe",
 
61
  });
62
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  } catch {
64
+ // If fetch/reset fails, re-clone
65
+ execSync(`rm -rf ${WORK_DIR}`, { stdio: "pipe" });
66
  }
 
67
  }
68
 
69
+ // Fresh clone
70
+ if (existsSync(WORK_DIR)) {
71
+ execSync(`rm -rf ${WORK_DIR}`, { stdio: "pipe" });
72
+ }
73
+ execSync(`git clone --depth 20 ${repoUrl} ${WORK_DIR}`, {
74
+ timeout: 60_000,
75
+ stdio: "pipe",
76
+ });
77
+ execSync('git config user.name "Claude Code"', { cwd: WORK_DIR, stdio: "pipe" });
78
+ execSync('git config user.email "claude-code@huggingclaw"', { cwd: WORK_DIR, stdio: "pipe" });
79
  }
80
 
81
+ function pushChanges(summary: string): string {
82
+ const status = execSync("git status --porcelain", {
83
+ cwd: WORK_DIR,
84
+ encoding: "utf-8",
85
+ }).trim();
86
 
87
+ if (!status) return "No files changed.";
 
 
88
 
89
+ execSync("git add -A", { cwd: WORK_DIR, stdio: "pipe" });
90
+ // Use a safe commit message
91
+ const msg = summary.slice(0, 72).replace(/"/g, '\\"');
92
+ execSync(`git commit -m "Claude Code: ${msg}"`, { cwd: WORK_DIR, stdio: "pipe" });
93
+ execSync("git push", { cwd: WORK_DIR, timeout: 60_000, stdio: "pipe" });
94
+
95
+ return `Pushed changes:\n${status}`;
96
  }
97
 
98
  // ── Plugin ──────────────��────────────────────────────────────────────────────
 
100
  const plugin = {
101
  id: "coding-agent",
102
  name: "Coding Agent",
103
+ description: "Claude Code sub-agent for autonomous coding on HF Spaces (Zhipu GLM backend via z.ai)",
104
 
105
  register(api: PluginApi) {
106
+ const cfg = (api.pluginConfig as Record<string, unknown>) || {};
107
  const targetSpace = asStr(cfg.targetSpace) || process.env.CODING_AGENT_TARGET_SPACE || "";
 
108
  const hfToken = asStr(cfg.hfToken) || process.env.HF_TOKEN || "";
109
+ const zaiApiKey = asStr(cfg.zaiApiKey) || process.env.ZAI_API_KEY || process.env.ZHIPU_API_KEY || "";
110
 
111
+ api.logger.info(`coding-agent: targetSpace=${targetSpace}, zaiKey=${zaiApiKey ? "set" : "missing"}`);
 
 
 
112
 
113
  if (!api.registerTool) {
114
  api.logger.warn("coding-agent: registerTool unavailable β€” no tools registered");
115
  return;
116
  }
117
 
118
+ // ── Tool: claude_code ───────────────────────────────────────────────────
119
  api.registerTool({
120
+ name: "claude_code",
121
+ label: "Run Claude Code",
122
  description:
123
+ "Run Claude Code to autonomously complete a coding task on the target HF Space. " +
124
+ "Claude Code clones the Space repo, analyzes code, makes changes, and pushes them back. " +
125
+ "Powered by Zhipu GLM via z.ai. Use for: debugging, fixing errors, adding features, refactoring.",
126
  parameters: {
127
  type: "object",
128
+ required: ["task"],
129
  properties: {
130
+ task: {
131
+ type: "string",
132
+ description: "Detailed coding task description. Be specific about what to fix/change and why.",
133
+ },
134
+ auto_push: {
135
+ type: "boolean",
136
+ description: "Automatically push changes after Claude Code finishes (default: true)",
137
+ },
138
  },
139
  },
140
  async execute(_id, params) {
141
+ const task = asStr(params.task);
142
+ const autoPush = params.auto_push !== false;
 
 
 
 
 
 
 
 
 
 
143
 
144
+ if (!targetSpace) return text("Error: no targetSpace configured");
145
+ if (!hfToken) return text("Error: no HF token configured");
146
+ if (!zaiApiKey) return text("Error: no ZAI_API_KEY or ZHIPU_API_KEY configured for Claude Code backend");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  try {
149
+ // 1. Clone / reset to latest
150
+ api.logger.info(`coding-agent: Syncing repo ${targetSpace}...`);
151
+ ensureRepo(targetSpace, hfToken);
152
+
153
+ // 2. Run Claude Code with z.ai backend
154
+ api.logger.info(`coding-agent: Running Claude Code: ${task.slice(0, 100)}...`);
155
+ const claudeEnv: Record<string, string> = {
156
+ ...(process.env as Record<string, string>),
157
+ ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
158
+ ANTHROPIC_AUTH_TOKEN: zaiApiKey,
159
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "GLM-4.7",
160
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "GLM-4.7",
161
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "GLM-4.5-Air",
162
+ // Avoid interactive prompts
163
+ CI: "true",
164
+ };
165
+
166
+ const output = execSync(
167
+ `claude -p ${JSON.stringify(task)} --output-format text`,
168
+ {
169
+ cwd: WORK_DIR,
170
+ env: claudeEnv,
171
+ timeout: CLAUDE_TIMEOUT,
172
+ encoding: "utf-8",
173
+ maxBuffer: 10 * 1024 * 1024, // 10MB
174
+ },
175
+ );
176
 
177
+ // 3. Push changes if requested
178
+ let pushResult = "Auto-push disabled.";
179
+ if (autoPush) {
180
+ try {
181
+ pushResult = pushChanges(task);
182
+ } catch (e: unknown) {
183
+ pushResult = `Push failed: ${e instanceof Error ? e.message : e}`;
184
+ }
185
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ return text(
188
+ `=== Claude Code Output ===\n${output}\n\n=== Changes ===\n${pushResult}`,
189
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  } catch (e: unknown) {
191
+ const msg = e instanceof Error ? e.message : String(e);
192
+ return text(`Claude Code failed:\n${msg.slice(0, 3000)}`);
193
  }
194
  },
195
  });
 
200
  label: "Check Space Health",
201
  description:
202
  "Check the current status of the target HuggingFace Space. " +
203
+ "Returns: stage (BUILDING, APP_STARTING, RUNNING, RUNTIME_ERROR, BUILD_ERROR, NO_APP_FILE).",
204
  parameters: {
205
  type: "object",
206
  properties: {},
 
208
  async execute() {
209
  if (!targetSpace) return text("Error: no target space configured");
210
  try {
211
+ const resp = await fetch(`https://huggingface.co/api/spaces/${targetSpace}`, {
212
+ headers: { Authorization: `Bearer ${hfToken}` },
213
+ });
214
+ if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
215
+ const data = (await resp.json()) as Record<string, unknown>;
216
+ const runtime = (data.runtime as Record<string, unknown>) || {};
217
+ const stage = runtime.stage || "unknown";
218
+ const hardware = runtime.hardware || "unknown";
219
+
220
+ // Try hitting the Space URL
221
  let apiStatus = "not checked";
222
  try {
223
  const spaceUrl = `https://${targetSpace.replace("/", "-").toLowerCase()}.hf.space`;
224
+ const probe = await fetch(spaceUrl, { signal: AbortSignal.timeout(8000) });
225
+ apiStatus = probe.ok ? `reachable (${probe.status})` : `error (${probe.status})`;
226
  } catch {
227
  apiStatus = "unreachable";
228
  }
229
 
230
  return text(
231
+ `Space: ${targetSpace}\nStage: ${stage}\nHardware: ${hardware}\nAPI: ${apiStatus}`,
 
 
 
232
  );
233
  } catch (e: unknown) {
234
  return text(`Error checking space: ${e instanceof Error ? e.message : e}`);
 
248
  async execute() {
249
  if (!targetSpace) return text("Error: no target space configured");
250
  try {
251
+ const resp = await fetch(`https://huggingface.co/api/spaces/${targetSpace}/restart`, {
252
+ method: "POST",
253
+ headers: { Authorization: `Bearer ${hfToken}` },
254
+ });
255
+ if (!resp.ok) {
256
+ const body = await resp.text().catch(() => "");
257
+ throw new Error(`${resp.status}: ${body}`);
258
+ }
259
+ return text(`Space ${targetSpace} is restarting`);
260
  } catch (e: unknown) {
261
  return text(`Error restarting space: ${e instanceof Error ? e.message : e}`);
262
  }
263
  },
264
  });
265
 
266
+ api.logger.info("coding-agent: Registered 3 tools (claude_code, hf_space_status, hf_restart_space)");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  },
268
  };
269
 
extensions/coding-agent/openclaw.plugin.json CHANGED
@@ -1,12 +1,13 @@
1
  {
2
  "id": "coding-agent",
3
  "name": "Coding Agent",
4
- "description": "HuggingFace Space coding tools β€” read, write, edit, search, and manage code on HF Spaces with syntax validation",
5
- "version": "1.0.0",
6
  "defaultConfig": {
7
  "targetSpace": "",
8
  "targetDataset": "",
9
- "hfToken": ""
 
10
  },
11
  "configSchema": {
12
  "type": "object",
@@ -23,10 +24,15 @@
23
  "hfToken": {
24
  "type": "string",
25
  "description": "HuggingFace API token (or use HF_TOKEN env var)"
 
 
 
 
26
  }
27
  }
28
  },
29
  "uiHints": {
30
- "hfToken": { "sensitive": true }
 
31
  }
32
  }
 
1
  {
2
  "id": "coding-agent",
3
  "name": "Coding Agent",
4
+ "description": "Claude Code sub-agent for autonomous coding on HF Spaces β€” powered by Zhipu GLM via z.ai",
5
+ "version": "2.0.0",
6
  "defaultConfig": {
7
  "targetSpace": "",
8
  "targetDataset": "",
9
+ "hfToken": "",
10
+ "zaiApiKey": ""
11
  },
12
  "configSchema": {
13
  "type": "object",
 
24
  "hfToken": {
25
  "type": "string",
26
  "description": "HuggingFace API token (or use HF_TOKEN env var)"
27
+ },
28
+ "zaiApiKey": {
29
+ "type": "string",
30
+ "description": "Z.AI API key for Claude Code backend (or use ZAI_API_KEY / ZHIPU_API_KEY env var)"
31
  }
32
  }
33
  },
34
  "uiHints": {
35
+ "hfToken": { "sensitive": true },
36
+ "zaiApiKey": { "sensitive": true }
37
  }
38
  }
scripts/sync_hf.py CHANGED
@@ -76,6 +76,9 @@ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
76
  # Zhipu AI (z.ai) API key (optional; GLM-4 series, Anthropic-compatible endpoint)
77
  ZHIPU_API_KEY = os.environ.get("ZHIPU_API_KEY", "")
78
 
 
 
 
79
  # Gateway token (default: huggingclaw; override via GATEWAY_TOKEN env var)
80
  GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw")
81
 
@@ -522,9 +525,10 @@ class OpenClawFullSync:
522
  "targetSpace": CODING_TARGET_SPACE,
523
  "targetDataset": CODING_TARGET_DATASET,
524
  "hfToken": HF_TOKEN or "",
 
525
  }
526
  }
527
- print(f"[SYNC] Coding agent configured: space={CODING_TARGET_SPACE}, dataset={CODING_TARGET_DATASET}")
528
  if "telegram" not in data["plugins"]["entries"]:
529
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
530
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
@@ -674,6 +678,8 @@ class OpenClawFullSync:
674
  env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
675
  if ZHIPU_API_KEY:
676
  env["ZHIPU_API_KEY"] = ZHIPU_API_KEY
 
 
677
  if not OPENAI_API_KEY and not OPENROUTER_API_KEY and not ZHIPU_API_KEY:
678
  print(f"[SYNC] WARNING: No API key set (OPENAI/OPENROUTER/ZHIPU), LLM features may not work")
679
 
 
76
  # Zhipu AI (z.ai) API key (optional; GLM-4 series, Anthropic-compatible endpoint)
77
  ZHIPU_API_KEY = os.environ.get("ZHIPU_API_KEY", "")
78
 
79
+ # Z.AI API key (optional; used by Claude Code backend via api.z.ai)
80
+ ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "")
81
+
82
  # Gateway token (default: huggingclaw; override via GATEWAY_TOKEN env var)
83
  GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw")
84
 
 
525
  "targetSpace": CODING_TARGET_SPACE,
526
  "targetDataset": CODING_TARGET_DATASET,
527
  "hfToken": HF_TOKEN or "",
528
+ "zaiApiKey": ZAI_API_KEY or ZHIPU_API_KEY or "",
529
  }
530
  }
531
+ print(f"[SYNC] Coding agent configured: space={CODING_TARGET_SPACE}, dataset={CODING_TARGET_DATASET}, zaiKey={'set' if (ZAI_API_KEY or ZHIPU_API_KEY) else 'missing'}")
532
  if "telegram" not in data["plugins"]["entries"]:
533
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
534
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
 
678
  env["OPENROUTER_API_KEY"] = OPENROUTER_API_KEY
679
  if ZHIPU_API_KEY:
680
  env["ZHIPU_API_KEY"] = ZHIPU_API_KEY
681
+ if ZAI_API_KEY:
682
+ env["ZAI_API_KEY"] = ZAI_API_KEY
683
  if not OPENAI_API_KEY and not OPENROUTER_API_KEY and not ZHIPU_API_KEY:
684
  print(f"[SYNC] WARNING: No API key set (OPENAI/OPENROUTER/ZHIPU), LLM features may not work")
685