From 8bfb0a71c09ee35d92de0c4e1e4c1557edee4310 Mon Sep 17 00:00:00 2001 From: OpenClaw Service User Date: Thu, 19 Mar 2026 13:15:53 -0700 Subject: [PATCH 1/5] Implement file writing for OpenClaw plugin per Traycer's recommendation --- README.md | 129 +++++++------------------------- src/bridge/grok_bridge.py | 67 ++++++++++++++++- src/bridge/index.js | 18 +++++ src/plugin/index.ts | 23 +++++- src/plugin/openclaw.plugin.json | 4 + 5 files changed, 139 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index c5a7a9d..7f85c33 100644 --- a/README.md +++ b/README.md @@ -11,127 +11,54 @@ Give your OpenClaw agents access to a 4-agent swarm with ~2M token context for c ## Features -<<<<<<< HEAD - **4-Agent Swarm** — Grok 4.20 coordinates multiple agents for deeper analysis - **Massive Context** — ~2M token window, handles entire codebases - **5 Modes** — Analyze, Refactor, Code, Reason, Orchestrate - **Tool Passthrough** — Pass OpenAI-format tool schemas for function calling +- **File Writing** — Write annotated code blocks directly to disk - **Timeout Safety** — Process-level timeout enforcement prevents hangs -======= -You've been building with AI coding agents for a while now. They're great — they can write features, refactor modules, analyze codebases. But there's always been this ceiling. The models they run on are designed for single-turn conversations. They can collaborate with themselves behind the scenes to think through complex problems. - -**Enter Grok 4.20 Multi-Agent Beta.** - -It's different. Instead of one model responding, it's *four agents coordinating* in real-time. An orchestrator, specialists, critics — all working together to break down your request and reason through it from multiple angles. It can hold ~2M tokens of context — that's entire codebases in a single request. - -**The Problem:** - -Grok 4.20 is groundbreaking, but it doesn't play nicely with current coding tools. Claude Code doesn't have a Grok integration. OpenClaw's tooling system doesn't support multi-agent swarms. If you wanted to use Grok, you'd have to hack together custom scripts or modify your platform's core components. Not ideal. - -**The Solution:** - -This plugin bridges that gap. It makes Grok 4.20 available as a tool that any agent in Claude Code or OpenClaw can call. No core modifications, no hacking — just install and go. - -Now when your agent needs deep codebase analysis, large-scale refactoring, or complex reasoning, it can delegate to Grok's swarm and get back the kind of coordinated, multi-perspective thinking that single models can't deliver. --- -## File Writing Capabilities +## File Writing -Grok Swarm can now **write files directly** from code blocks in its responses. This solves the context flooding problem — Grok writes files to disk, and only a brief summary returns to your agent. +When `write_files=true`, Grok parses code blocks for filename annotations and writes them directly to disk, returning only a compact summary instead of the full response. -### How It Works +### Supported Patterns -Grok responses with code blocks like: -```` -```python src/auth.py -import jwt -... +**Fenced code blocks with path in the language tag:** +```markdown +```typescript:src/auth/login.ts +export function login() { ... } ``` -```` - -Are automatically parsed and written to the output directory. - -### Usage - -| Command | Behavior | -|---------|----------| -| `--output-dir ./src` | Preview files (dry-run) | -| `--apply --output-dir ./src` | Write files to `./src` | -| `--apply --execute "make test"` | Write files, then run tests | - -### Example Workflow - -```bash -# Ask Grok to generate a module and write it -python -m src.bridge.cli code \ - --prompt "Write a FastAPI auth module with JWT" \ - --output-dir ./src \ - --apply - -# Then run tests -python -m src.bridge.cli code \ - --prompt "Refactor auth to use async" \ - --apply --execute "pytest tests/" \ - --output-dir ./src ``` -### Morph LLM Integration - -For **partial file edits** (not full replacement), use the `--use-morph` flag: - -```bash -python -m src.bridge.cli refactor \ - --prompt "Convert this function to async" \ - --use-morph --apply +**Fenced code blocks with `// FILE:` marker:** +```markdown +```typescript +// FILE: src/auth/login.ts +export function login() { ... } ``` - -This requires Morph LLM MCP installed: - -```bash -claude mcp add morphllm ``` ---- - -## Known Limitations +### Example -> **Note:** When using `--apply`, Grok Swarm parses code blocks and writes files. For targeted edits within existing files, use `--use-morph` (requires Morph LLM MCP). - -### Why This Matters - -Grok can hold ~1.5M tokens of context and generate ~350K token responses. If that entire response floods back through your orchestrator's context window, you've just wasted precious tokens and slowed down your agent. - -```text -Current Flow: -Files (1.5M tokens) → Grok → Full response (376K) → Orchestrator (flooded!) - -Ideal Flow: -Files (1.5M tokens) → Grok → Writes files + brief summary → Orchestrator (clean) +```javascript +const result = await tools.grok_swarm({ + prompt: "Write a FastAPI auth module with JWT", + mode: "code", + write_files: true, + output_dir: "./src" +}); +// Returns: "Wrote 3 files to ./src +// src/auth.py (1,234 bytes) +// src/jwt_utils.py (567 bytes) +// src/middleware.py (890 bytes)" ``` -### Current Use Cases - -Grok Swarm now supports **direct file writing**: -- ✅ **Code generation with file output** — Grok writes files directly -- ✅ **Codebase analysis** — Security audits, architecture reviews -- ✅ **Refactoring with partial edits** — Use `--use-morph` for targeted changes -- ✅ **Complex reasoning** — Research synthesis, decision making - -The `--apply` flag makes Grok write files to disk. Combined with `--execute`, you can build generate → test workflows. - ---- - -## What It Does +### Why This Matters -| Feature | Why It Matters | -|----------|------------------| -| **4-Agent Coordination** | Multiple perspectives on every request | -| **2M Token Context** | Holds entire codebases without truncation | -| **5 Task Modes** | Analyze, Refactor, Generate, Reason, Orchestrate | -| **Dual Platform** | Works in both Claude Code and OpenClaw | -| **Zero Core Changes** | Drop-in tool, no platform modifications | ->>>>>>> 574902f (Add file writing and Morph LLM integration) +Grok can generate ~350K token responses. Without file writing, that floods your orchestrator's context window. With file writing, you get a brief summary and the files on disk. --- @@ -250,6 +177,8 @@ const result = await tools.grok_swarm({ | `files` | string[] | No | — | Files for context | | `system` | string | No | — | Custom system prompt | | `timeout` | number | No | 120 | Timeout in seconds | +| `write_files` | boolean | No | false | Write annotated code blocks to disk | +| `output_dir` | string | No | ./grok-output/ | Directory for file writes | --- diff --git a/src/bridge/grok_bridge.py b/src/bridge/grok_bridge.py index 3ad28b5..c89f6c3 100755 --- a/src/bridge/grok_bridge.py +++ b/src/bridge/grok_bridge.py @@ -14,6 +14,7 @@ import argparse import json import os +import re import sys import time from pathlib import Path @@ -131,6 +132,54 @@ def load_tools(tools_path): return tools +def parse_and_write_files(response_text, output_dir): + """ + Scan response for fenced code blocks with filename annotations and write to disk. + + Supports patterns: + ```lang:path/to/file ... ``` + ```lang + // FILE: path/to/file + ... + ``` + + Returns list of (relative_path, byte_count) tuples written. + """ + written = [] + output_path = Path(output_dir) + + # Pattern for lang:path at start of block (language tag contains path) + lang_path_pattern = re.compile(r'^(\w+):([^\s\n]+)\n', re.MULTILINE) + # Pattern for // FILE: or # FILE: markers + file_marker_pattern = re.compile(r'^\s*(?://|#)\s*FILE:\s*(.+?)\s*$', re.MULTILINE) + + # Split into code blocks by ``` fences + parts = re.split(r'```', response_text) + + for part in parts: + # Check for lang:path at start (language tag contains the path) + lang_match = lang_path_pattern.match(part) + if lang_match: + file_path = lang_match.group(2) + content = part[lang_match.end():] + dest = output_path / file_path + dest.parent.mkdir(parents=True, exist_ok=True) + byte_count = dest.write_text(content.strip(), errors='replace') + written.append((file_path, byte_count)) + continue + + # Check for // FILE: or # FILE: marker within the block + marker_match = file_marker_pattern.search(part) + if marker_match: + file_path = marker_match.group(1).strip() + content = part[marker_match.end():] + dest = output_path / file_path + dest.parent.mkdir(parents=True, exist_ok=True) + byte_count = dest.write_text(content.strip(), errors='replace') + written.append((file_path, byte_count)) + + return written + def call_grok(prompt, mode="reason", context="", system_override=None, tools=None, timeout=120): """Make the API call to Grok 4.20 Multi-Agent Beta.""" api_key = get_api_key() @@ -239,6 +288,10 @@ def main(): parser.add_argument("--tools", help="Path to JSON file with OpenAI-format tool definitions") parser.add_argument("--timeout", type=int, default=120, help="Timeout in seconds (default: 120)") parser.add_argument("--output", help="Output file path (default: stdout)") + parser.add_argument("--write-files", action="store_true", + help="Parse response for annotated code blocks and write to --output-dir") + parser.add_argument("--output-dir", default="./grok-output/", + help="Directory for file writes (default: ./grok-output/)") args = parser.parse_args() @@ -273,7 +326,19 @@ def main(): Path(args.output).write_text(result) print(f"Written to: {args.output}", file=sys.stderr) else: - print(result) + if args.write_files: + written = parse_and_write_files(result, args.output_dir) + if written: + total_bytes = sum(b for _, b in written) + print(f"Wrote {len(written)} files to {args.output_dir}") + for rel_path, byte_count in written: + print(f" {rel_path} ({byte_count:,} bytes)") + print(f"Total: {total_bytes:,} bytes", file=sys.stderr) + else: + print("No annotated files found in response", file=sys.stderr) + print(result) + else: + print(result) if __name__ == "__main__": diff --git a/src/bridge/index.js b/src/bridge/index.js index f3637aa..18f5866 100755 --- a/src/bridge/index.js +++ b/src/bridge/index.js @@ -28,6 +28,8 @@ function parseArgs() { tools: null, timeout: 120, output: null, + writeFiles: false, + outputDir: './grok-output/', }; for (let i = 0; i < args.length; i++) { @@ -55,6 +57,12 @@ function parseArgs() { case '--output': parsed.output = args[++i]; break; + case '--write-files': + parsed.writeFiles = true; + break; + case '--output-dir': + parsed.outputDir = args[++i]; + break; case '--help': console.log(` grok_swarm — Bridge to xAI Grok 4.20 Multi-Agent Beta (4-agent swarm) @@ -70,6 +78,8 @@ Options: --tools JSON file with OpenAI-format tool definitions --timeout Timeout in seconds (default: 120) --output Output file (default: stdout) + --write-files Parse response for annotated code blocks and write files + --output-dir Directory for file writes (default: ./grok-output/) --help Show this help Modes: @@ -128,6 +138,14 @@ function run() { pyArgs.push('--output', opts.output); } + if (opts.writeFiles) { + pyArgs.push('--write-files'); + } + + if (opts.outputDir && opts.outputDir !== './grok-output/') { + pyArgs.push('--output-dir', opts.outputDir); + } + // Spawn Python process const child = spawn(PYTHON, pyArgs, { stdio: ['inherit', 'pipe', 'pipe'], diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 83bacd6..42d66dc 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -51,6 +51,16 @@ const GrokSwarmSchema = Type.Object({ timeout: Type.Optional( Type.Number({ description: "Timeout in seconds (default: 120)" }), ), + write_files: Type.Optional( + Type.Boolean({ + description: "Write generated files directly to disk; orchestrator receives a brief summary only", + }), + ), + output_dir: Type.Optional( + Type.String({ + description: "Directory to write files into (default: ./grok-output/)", + }), + ), }); export default function (api: any) { @@ -62,7 +72,8 @@ export default function (api: any) { "Delegate tasks to xAI Grok 4.20 Multi-Agent Beta (4-agent swarm with 2M context). " + "Use for codebase analysis, refactoring, code generation, or complex reasoning. " + "Modes: refactor, analyze, code, reason, orchestrate. " + - "Orchestrate mode requires a custom system prompt.", + "With write_files=true, annotated code blocks are written directly to disk and a " + + "compact summary is returned instead of the full response.", parameters: GrokSwarmSchema, async execute(_toolCallId: string, params: any) { const json = (payload: unknown) => ({ @@ -108,6 +119,16 @@ export default function (api: any) { args.push("--system", params.system); } + if (params.write_files) { + args.push("--write-files"); + } + + if (params.output_dir) { + args.push("--output-dir", params.output_dir); + } else if (api.config?.defaultOutputDir) { + args.push("--output-dir", api.config.defaultOutputDir); + } + // Spawn Python bridge with timeout enforcement return new Promise((resolve) => { const child = spawn(pythonPath, args, { diff --git a/src/plugin/openclaw.plugin.json b/src/plugin/openclaw.plugin.json index d1e4aa4..2210adf 100644 --- a/src/plugin/openclaw.plugin.json +++ b/src/plugin/openclaw.plugin.json @@ -18,6 +18,10 @@ "bridgeScript": { "type": "string", "description": "Path to grok_bridge.py script" + }, + "defaultOutputDir": { + "type": "string", + "description": "Default output directory for file writes (default: ./grok-output/)" } } } From 6fa37677a3252883e12852c21b02169ea3fe6bbf Mon Sep 17 00:00:00 2001 From: OpenClaw Service User Date: Thu, 19 Mar 2026 13:26:46 -0700 Subject: [PATCH 2/5] docs: Document API key resolution precedence in README Explains the priority order: env vars > local config > OpenClaw profiles. Adds warning that env vars override config files. Includes example of how to unset env var to use config file. --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7f85c33..55592d1 100644 --- a/README.md +++ b/README.md @@ -189,19 +189,24 @@ const result = await tools.grok_swarm({ ls ~/.openclaw/skills/grok-refactor/grok_bridge.py ``` -### "No OpenRouter API key" -Configure your key in OpenClaw auth profiles: +### OpenRouter API Key + +Grok Swarm resolves your API key in this order (highest to lowest priority): + +1. **Environment variables** — `OPENROUTER_API_KEY` or `XAI_API_KEY` +2. **Local config file** — `~/.config/grok-swarm/config.json` with `{"api_key": "..."}` +3. **OpenClaw auth profiles** — `~/.openclaw/agents/coder/agent/auth-profiles.json` + ```bash -# Add to ~/.openclaw/agents/coder/agent/auth-profiles.json -{ - "profiles": { - "openrouter:default": { - "key": "your-key-here" - } - } -} +# If you set an env var, it takes precedence over config files: +export OPENROUTER_API_KEY="sk-or-v1-xxx" # This overrides ~/.config/grok-swarm/config.json! + +# To use the local config file instead, unset the env var: +unset OPENROUTER_API_KEY ``` +**Get a key at:** https://openrouter.ai/keys + ### Timeout errors Increase timeout for large codebases: ```bash From 3e1179278cbd5c98f38154fe7a0097f846880823 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:01:05 +0000 Subject: [PATCH 3/5] Initial plan From f1f51e154c08a0088d3ff63462d49190ba64925a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:05:55 +0000 Subject: [PATCH 4/5] Address all 7 PR review comments: security, correctness, and docs fixes Co-authored-by: KHAEntertainment <43256680+KHAEntertainment@users.noreply.github.com> --- README.md | 36 +++++++++--------- src/bridge/grok_bridge.py | 79 ++++++++++++++++++++++++++------------- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 55592d1..81c4d71 100644 --- a/README.md +++ b/README.md @@ -27,19 +27,17 @@ When `write_files=true`, Grok parses code blocks for filename annotations and wr ### Supported Patterns **Fenced code blocks with path in the language tag:** -```markdown -```typescript:src/auth/login.ts -export function login() { ... } -``` -``` + + ```typescript:src/auth/login.ts + export function login() { ... } + ``` **Fenced code blocks with `// FILE:` marker:** -```markdown -```typescript -// FILE: src/auth/login.ts -export function login() { ... } -``` -``` + + ```typescript + // FILE: src/auth/login.ts + export function login() { ... } + ``` ### Example @@ -193,16 +191,16 @@ ls ~/.openclaw/skills/grok-refactor/grok_bridge.py Grok Swarm resolves your API key in this order (highest to lowest priority): -1. **Environment variables** — `OPENROUTER_API_KEY` or `XAI_API_KEY` -2. **Local config file** — `~/.config/grok-swarm/config.json` with `{"api_key": "..."}` -3. **OpenClaw auth profiles** — `~/.openclaw/agents/coder/agent/auth-profiles.json` +1. **Environment variables** — `OPENROUTER_API_KEY` or `OPENCLAW_OPENROUTER_DEFAULT_KEY` +2. **OpenClaw auth profiles** — searched in order: + - `~/.openclaw/agents/coder/agent/auth-profiles.json` + - `~/.openclaw/agents/main/agent/auth-profiles.json` + - `~/.openclaw/auth-profiles.json` + - `~/.config/openclaw/auth-profiles.json` ```bash -# If you set an env var, it takes precedence over config files: -export OPENROUTER_API_KEY="sk-or-v1-xxx" # This overrides ~/.config/grok-swarm/config.json! - -# To use the local config file instead, unset the env var: -unset OPENROUTER_API_KEY +# Set via environment variable (highest priority): +export OPENROUTER_API_KEY="sk-or-v1-xxx" ``` **Get a key at:** https://openrouter.ai/keys diff --git a/src/bridge/grok_bridge.py b/src/bridge/grok_bridge.py index c89f6c3..8101b81 100755 --- a/src/bridge/grok_bridge.py +++ b/src/bridge/grok_bridge.py @@ -132,6 +132,26 @@ def load_tools(tools_path): return tools +def _safe_dest(output_path, file_path): + """ + Resolve ``file_path`` relative to ``output_path`` and verify the result + stays inside ``output_path``. Returns the resolved Path or raises + ValueError for unsafe paths (absolute, containing ``..``, etc.). + """ + raw = Path(file_path) + if raw.is_absolute(): + raise ValueError(f"Absolute paths are not allowed: {file_path!r}") + if ".." in raw.parts: + raise ValueError(f"Path traversal not allowed: {file_path!r}") + dest = (output_path / raw).resolve() + resolved_root = output_path.resolve() + try: + dest.relative_to(resolved_root) + except ValueError: + raise ValueError(f"Path escapes output directory: {file_path!r}") + return dest + + def parse_and_write_files(response_text, output_dir): """ Scan response for fenced code blocks with filename annotations and write to disk. @@ -143,7 +163,8 @@ def parse_and_write_files(response_text, output_dir): ... ``` - Returns list of (relative_path, byte_count) tuples written. + Returns list of (relative_path, byte_count) tuples written, where + byte_count is the number of UTF-8 bytes written. """ written = [] output_path = Path(output_dir) @@ -153,6 +174,19 @@ def parse_and_write_files(response_text, output_dir): # Pattern for // FILE: or # FILE: markers file_marker_pattern = re.compile(r'^\s*(?://|#)\s*FILE:\s*(.+?)\s*$', re.MULTILINE) + def _write_file(file_path, content): + """Validate path, write content, and record result. Returns True on success.""" + try: + dest = _safe_dest(output_path, file_path) + except ValueError as exc: + print(f"WARNING: Skipping unsafe path — {exc}", file=sys.stderr) + return False + dest.parent.mkdir(parents=True, exist_ok=True) + encoded = content.strip().encode("utf-8", errors="replace") + dest.write_bytes(encoded) + written.append((file_path, len(encoded))) + return True + # Split into code blocks by ``` fences parts = re.split(r'```', response_text) @@ -160,23 +194,13 @@ def parse_and_write_files(response_text, output_dir): # Check for lang:path at start (language tag contains the path) lang_match = lang_path_pattern.match(part) if lang_match: - file_path = lang_match.group(2) - content = part[lang_match.end():] - dest = output_path / file_path - dest.parent.mkdir(parents=True, exist_ok=True) - byte_count = dest.write_text(content.strip(), errors='replace') - written.append((file_path, byte_count)) + _write_file(lang_match.group(2), part[lang_match.end():]) continue # Check for // FILE: or # FILE: marker within the block marker_match = file_marker_pattern.search(part) if marker_match: - file_path = marker_match.group(1).strip() - content = part[marker_match.end():] - dest = output_path / file_path - dest.parent.mkdir(parents=True, exist_ok=True) - byte_count = dest.write_text(content.strip(), errors='replace') - written.append((file_path, byte_count)) + _write_file(marker_match.group(1).strip(), part[marker_match.end():]) return written @@ -325,20 +349,23 @@ def main(): if args.output: Path(args.output).write_text(result) print(f"Written to: {args.output}", file=sys.stderr) - else: - if args.write_files: - written = parse_and_write_files(result, args.output_dir) - if written: - total_bytes = sum(b for _, b in written) - print(f"Wrote {len(written)} files to {args.output_dir}") - for rel_path, byte_count in written: - print(f" {rel_path} ({byte_count:,} bytes)") - print(f"Total: {total_bytes:,} bytes", file=sys.stderr) - else: - print("No annotated files found in response", file=sys.stderr) - print(result) + + if args.write_files: + written = parse_and_write_files(result, args.output_dir) + if written: + total_bytes = sum(b for _, b in written) + print(f"Wrote {len(written)} files to {args.output_dir}") + for rel_path, byte_count in written: + print(f" {rel_path} ({byte_count:,} bytes)") + print(f"Total: {total_bytes:,} bytes") else: - print(result) + print( + "No annotated files found in model response to write to disk.\n" + "Re-run without --write-files to see the full response.", + file=sys.stderr, + ) + elif not args.output: + print(result) if __name__ == "__main__": From ed15cc10b4e31a5033cbe0b79ebedb630e359cb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:21:44 +0000 Subject: [PATCH 5/5] Initial plan