@khaentertainment/grok-swarm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +294 -0
- package/dist/VERSION +1 -0
- package/dist/apply.py +191 -0
- package/dist/cli.py +270 -0
- package/dist/grok_bridge.py +393 -0
- package/dist/index.js +205 -0
- package/dist/package.json +73 -0
- package/package.json +73 -0
- package/src/bridge/apply.py +191 -0
- package/src/bridge/cli.py +270 -0
- package/src/bridge/grok_bridge.py +393 -0
- package/src/bridge/index.js +205 -0
- package/src/plugin/index.ts +198 -0
- package/src/plugin/openclaw.plugin.json +28 -0
- package/src/plugin/package.json +7 -0
package/dist/apply.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
apply.py - Parse code blocks from Grok responses and write them to files.
|
|
4
|
+
|
|
5
|
+
Handles markdown code blocks with optional language hints and file paths
|
|
6
|
+
from fenced code block attributes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_code_blocks(markdown_text):
|
|
16
|
+
blocks = []
|
|
17
|
+
lines = markdown_text.split('\n')
|
|
18
|
+
i = 0
|
|
19
|
+
|
|
20
|
+
while i < len(lines):
|
|
21
|
+
line = lines[i]
|
|
22
|
+
fence_match = re.match(r'^(`{3,})(.*)$', line)
|
|
23
|
+
if fence_match:
|
|
24
|
+
fence = fence_match.group(1)
|
|
25
|
+
header = fence_match.group(2).strip()
|
|
26
|
+
parts = header.split(None, 1)
|
|
27
|
+
lang = parts[0] if parts else ""
|
|
28
|
+
path_hint = parts[1] if len(parts) > 1 else ""
|
|
29
|
+
|
|
30
|
+
code_lines = []
|
|
31
|
+
i += 1
|
|
32
|
+
while i < len(lines):
|
|
33
|
+
if re.match(r'^' + re.escape(fence) + r'$', lines[i]):
|
|
34
|
+
i += 1 # Skip past closing fence
|
|
35
|
+
break
|
|
36
|
+
code_lines.append(lines[i])
|
|
37
|
+
i += 1
|
|
38
|
+
|
|
39
|
+
code = '\n'.join(code_lines).strip()
|
|
40
|
+
|
|
41
|
+
if code:
|
|
42
|
+
blocks.append({
|
|
43
|
+
"language": lang,
|
|
44
|
+
"code": code,
|
|
45
|
+
"path_hint": path_hint
|
|
46
|
+
})
|
|
47
|
+
else:
|
|
48
|
+
i += 1
|
|
49
|
+
|
|
50
|
+
return blocks
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def infer_filename(block, base_dir):
|
|
54
|
+
if block.get("path_hint"):
|
|
55
|
+
return block["path_hint"]
|
|
56
|
+
|
|
57
|
+
code = block["code"]
|
|
58
|
+
lang = block.get("language", "").lower()
|
|
59
|
+
|
|
60
|
+
shebang_match = re.match(r'#!\S+/(\S+)', code.split('\n')[0])
|
|
61
|
+
if shebang_match:
|
|
62
|
+
name = shebang_match.group(1)
|
|
63
|
+
return f"script.{name}"
|
|
64
|
+
|
|
65
|
+
lang_map = {
|
|
66
|
+
"python": "output.py", "py": "output.py",
|
|
67
|
+
"javascript": "output.js", "js": "output.js",
|
|
68
|
+
"typescript": "output.ts", "ts": "output.ts",
|
|
69
|
+
"rust": "output.rs", "go": "output.go",
|
|
70
|
+
"java": "Output.java", "c": "output.c",
|
|
71
|
+
"cpp": "output.cpp", "c++": "output.cpp",
|
|
72
|
+
"ruby": "output.rb", "rb": "output.rb",
|
|
73
|
+
"php": "output.php", "shell": "output.sh",
|
|
74
|
+
"bash": "output.sh", "sh": "output.sh",
|
|
75
|
+
"yaml": "output.yaml", "yml": "output.yml",
|
|
76
|
+
"json": "output.json", "toml": "output.toml",
|
|
77
|
+
"html": "output.html", "css": "output.css",
|
|
78
|
+
"sql": "output.sql", "markdown": "output.md",
|
|
79
|
+
"md": "output.md",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if lang in lang_map:
|
|
83
|
+
return lang_map[lang]
|
|
84
|
+
|
|
85
|
+
return "output.txt"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def apply_blocks(blocks, base_dir, dry_run=True):
|
|
89
|
+
base = Path(base_dir).resolve()
|
|
90
|
+
if not dry_run:
|
|
91
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
files_written = 0
|
|
94
|
+
files_skipped = 0
|
|
95
|
+
changes = []
|
|
96
|
+
|
|
97
|
+
for block in blocks:
|
|
98
|
+
filename = infer_filename(block, base_dir)
|
|
99
|
+
filepath = (base / filename).resolve()
|
|
100
|
+
|
|
101
|
+
# Validate containment using try/except instead of is_relative_to (Python 3.8 compat)
|
|
102
|
+
try:
|
|
103
|
+
rel = filepath.relative_to(base)
|
|
104
|
+
rel_path = str(rel)
|
|
105
|
+
except ValueError:
|
|
106
|
+
# Path is outside base_dir
|
|
107
|
+
files_skipped += 1
|
|
108
|
+
changes.append({
|
|
109
|
+
"path": str(filepath),
|
|
110
|
+
"action": "skipped",
|
|
111
|
+
"reason": "path outside base_dir"
|
|
112
|
+
})
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if dry_run:
|
|
116
|
+
files_skipped += 1
|
|
117
|
+
action = "would write"
|
|
118
|
+
else:
|
|
119
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
filepath.write_text(block["code"])
|
|
121
|
+
files_written += 1
|
|
122
|
+
action = "written"
|
|
123
|
+
|
|
124
|
+
changes.append({
|
|
125
|
+
"path": rel_path,
|
|
126
|
+
"action": action,
|
|
127
|
+
"language": block.get("language", ""),
|
|
128
|
+
"size": len(block["code"])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"files_written": files_written,
|
|
133
|
+
"files_skipped": files_skipped,
|
|
134
|
+
"changes": changes
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def format_summary(result, base_dir):
|
|
139
|
+
lines = []
|
|
140
|
+
lines.append(f"\n{'='*60}")
|
|
141
|
+
lines.append(f"Applied code blocks to: {base_dir}/")
|
|
142
|
+
lines.append(f"{'='*60}")
|
|
143
|
+
|
|
144
|
+
for change in result["changes"]:
|
|
145
|
+
path = change["path"]
|
|
146
|
+
action = change["action"]
|
|
147
|
+
lang = change.get("language", "")
|
|
148
|
+
size = change.get("size", 0)
|
|
149
|
+
|
|
150
|
+
if action == "skipped":
|
|
151
|
+
lines.append(f" SKIPPED: {path} ({change.get('reason', 'n/a')})")
|
|
152
|
+
else:
|
|
153
|
+
lines.append(f" {path} ({lang}, {size:,} chars) - {action}")
|
|
154
|
+
|
|
155
|
+
lines.append(f"\nSummary: {result['files_written']} written, {result['files_skipped']} skipped")
|
|
156
|
+
return "\n".join(lines)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main():
|
|
160
|
+
parser = argparse.ArgumentParser(description="Parse code blocks from Grok responses and write to files")
|
|
161
|
+
parser.add_argument("input", help="Input markdown file, or - for stdin")
|
|
162
|
+
parser.add_argument("--base-dir", "-d", default="./grok-output", help="Base directory for output files")
|
|
163
|
+
parser.add_argument("--yes", "-y", action="store_true", help="Actually write files (default is dry-run)")
|
|
164
|
+
parser.add_argument("--json", action="store_true", help="Output JSON summary")
|
|
165
|
+
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
|
|
168
|
+
if args.input == "-":
|
|
169
|
+
markdown_text = sys.stdin.read()
|
|
170
|
+
else:
|
|
171
|
+
markdown_text = Path(args.input).read_text()
|
|
172
|
+
|
|
173
|
+
blocks = parse_code_blocks(markdown_text)
|
|
174
|
+
|
|
175
|
+
if not blocks:
|
|
176
|
+
print("No code blocks found in input.", file=sys.stderr)
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
print(f"Found {len(blocks)} code block(s)", file=sys.stderr)
|
|
180
|
+
|
|
181
|
+
result = apply_blocks(blocks, args.base_dir, dry_run=not args.yes)
|
|
182
|
+
|
|
183
|
+
if args.json:
|
|
184
|
+
import json
|
|
185
|
+
print(json.dumps(result, indent=2))
|
|
186
|
+
else:
|
|
187
|
+
print(format_summary(result, args.base_dir))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
package/dist/cli.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unified Grok Swarm CLI entrypoint.
|
|
4
|
+
Dispatches to refactor/analyze/code/reason modes using grok_bridge.py logic.
|
|
5
|
+
|
|
6
|
+
Supports file writing when Grok generates code:
|
|
7
|
+
--output-dir <path> Directory to write files to
|
|
8
|
+
--apply Actually write files (dry-run by default)
|
|
9
|
+
--execute <cmd> Run a command after generation
|
|
10
|
+
--use-morph Use Morph LLM MCP for file edits if available
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Support both source and installed package layouts
|
|
21
|
+
current = Path(__file__).parent
|
|
22
|
+
if str(current) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(current))
|
|
24
|
+
|
|
25
|
+
from grok_bridge import call_grok, read_files, MODE_PROMPTS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_morph_available():
|
|
29
|
+
"""Check if Morph LLM MCP is installed."""
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["claude", "mcp", "list"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
timeout=10
|
|
36
|
+
)
|
|
37
|
+
return "morph" in result.stdout.lower()
|
|
38
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_with_morph(blocks, base_dir):
|
|
43
|
+
"""
|
|
44
|
+
Apply code blocks using Morph LLM MCP edit_file tool.
|
|
45
|
+
Returns summary dict.
|
|
46
|
+
"""
|
|
47
|
+
import uuid
|
|
48
|
+
|
|
49
|
+
applied = 0
|
|
50
|
+
errors = []
|
|
51
|
+
base_resolved = Path(base_dir).resolve()
|
|
52
|
+
|
|
53
|
+
for block in blocks:
|
|
54
|
+
# For Morph, we prefer partial edits. Use path_hint as target.
|
|
55
|
+
path_hint = block.get("path_hint", "")
|
|
56
|
+
if not path_hint:
|
|
57
|
+
# Try to infer from code
|
|
58
|
+
inferred_path = block.get("inferred_path", "")
|
|
59
|
+
if not inferred_path:
|
|
60
|
+
# No valid path - skip this block
|
|
61
|
+
errors.append("No path_hint or inferred_path provided - skipping partial edit")
|
|
62
|
+
continue
|
|
63
|
+
path_hint = inferred_path
|
|
64
|
+
|
|
65
|
+
# Sanitize and validate path_hint to prevent path traversal
|
|
66
|
+
# Check if path_hint is absolute
|
|
67
|
+
path_obj = Path(path_hint)
|
|
68
|
+
if path_obj.is_absolute():
|
|
69
|
+
errors.append(f"{path_hint}: absolute paths not allowed")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# Resolve the full path and ensure it's within base_dir
|
|
73
|
+
try:
|
|
74
|
+
target_path = (base_resolved / path_hint).resolve()
|
|
75
|
+
# Validate containment
|
|
76
|
+
target_path.relative_to(base_resolved)
|
|
77
|
+
validated_path = str(target_path)
|
|
78
|
+
except ValueError:
|
|
79
|
+
errors.append(f"{path_hint}: path traversal detected - outside base_dir")
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Execute via claude mcp
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
["claude", "mcp", "call", "morphllm", "edit_file",
|
|
86
|
+
"--file", validated_path,
|
|
87
|
+
"--code", block["code"],
|
|
88
|
+
"--language", block.get("language", "")],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=30
|
|
92
|
+
)
|
|
93
|
+
if result.returncode == 0:
|
|
94
|
+
applied += 1
|
|
95
|
+
else:
|
|
96
|
+
errors.append(f"{path_hint}: {result.stderr}")
|
|
97
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
98
|
+
errors.append(f"{path_hint}: {e}")
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"applied": applied,
|
|
102
|
+
"total": len(blocks),
|
|
103
|
+
"errors": errors
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_and_write(result_text, output_dir, dry_run=True):
|
|
108
|
+
"""
|
|
109
|
+
Parse code blocks from Grok response and write to files.
|
|
110
|
+
Returns summary string.
|
|
111
|
+
"""
|
|
112
|
+
from apply import parse_code_blocks, apply_blocks, format_summary
|
|
113
|
+
|
|
114
|
+
blocks = parse_code_blocks(result_text)
|
|
115
|
+
|
|
116
|
+
if not blocks:
|
|
117
|
+
return "No code blocks found in response."
|
|
118
|
+
|
|
119
|
+
if dry_run:
|
|
120
|
+
# Just show what would happen
|
|
121
|
+
base = Path(output_dir)
|
|
122
|
+
summaries = []
|
|
123
|
+
for block in blocks:
|
|
124
|
+
from apply import infer_filename
|
|
125
|
+
filename = infer_filename(block, output_dir)
|
|
126
|
+
summaries.append(f" • {filename} ({block.get('language', 'text')}, {len(block['code']):,} chars)")
|
|
127
|
+
return f"\nFound {len(blocks)} code block(s) — dry-run:\n" + "\n".join(summaries)
|
|
128
|
+
|
|
129
|
+
# Actually write
|
|
130
|
+
result = apply_blocks(blocks, output_dir, dry_run=False)
|
|
131
|
+
return format_summary(result, output_dir)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
parser = argparse.ArgumentParser(
|
|
136
|
+
description="Grok Swarm — Multi-agent CLI powered by Grok 4.20",
|
|
137
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"mode",
|
|
141
|
+
nargs="?",
|
|
142
|
+
choices=list(MODE_PROMPTS.keys()),
|
|
143
|
+
default="reason",
|
|
144
|
+
help="Task mode (default: reason)",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument("--prompt", "-p", required=True, help="Task instruction or question")
|
|
147
|
+
parser.add_argument("--files", "-f", nargs="*", default=[], help="Files to include as context")
|
|
148
|
+
parser.add_argument("--system", "-s", help="Override system prompt")
|
|
149
|
+
parser.add_argument("--tools", "-t", help="Path to OpenAI-format tools JSON")
|
|
150
|
+
parser.add_argument("--timeout", type=int, default=120, help="Timeout in seconds")
|
|
151
|
+
parser.add_argument("--output", help="Write raw output to file")
|
|
152
|
+
parser.add_argument("--output-dir", "--od", "-d",
|
|
153
|
+
help="Directory to write generated files (used with --apply)")
|
|
154
|
+
parser.add_argument("--apply", "-a", action="store_true",
|
|
155
|
+
help="Actually write files from code blocks (dry-run by default)")
|
|
156
|
+
parser.add_argument("--execute", "-e", metavar="CMD",
|
|
157
|
+
help="Execute command after generation (shell string)")
|
|
158
|
+
parser.add_argument("--use-morph", action="store_true",
|
|
159
|
+
help="Use Morph LLM MCP for file edits if available")
|
|
160
|
+
|
|
161
|
+
args = parser.parse_args()
|
|
162
|
+
|
|
163
|
+
if args.mode == "orchestrate" and not args.system:
|
|
164
|
+
print("ERROR: orchestrate mode requires --system", file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
# Check Morph availability if --use-morph is set
|
|
168
|
+
use_morph = False
|
|
169
|
+
if args.use_morph:
|
|
170
|
+
if check_morph_available():
|
|
171
|
+
use_morph = True
|
|
172
|
+
print("Morph LLM MCP detected — will use for file edits", file=sys.stderr)
|
|
173
|
+
else:
|
|
174
|
+
print("WARNING: --use-morph set but Morph LLM MCP not found", file=sys.stderr)
|
|
175
|
+
print(" Install with: claude mcp add morphllm", file=sys.stderr)
|
|
176
|
+
|
|
177
|
+
context = read_files(args.files) if args.files else ""
|
|
178
|
+
|
|
179
|
+
if args.files:
|
|
180
|
+
print(f"Read {len(args.files)} file(s) — {len(context):,} chars", file=sys.stderr)
|
|
181
|
+
|
|
182
|
+
# Parse tools if provided
|
|
183
|
+
tools = None
|
|
184
|
+
if args.tools:
|
|
185
|
+
with open(args.tools, 'r', encoding='utf-8') as f:
|
|
186
|
+
tools = json.load(f)
|
|
187
|
+
|
|
188
|
+
print(f"Calling Grok 4.20 (mode={args.mode}, 4 agents)...", file=sys.stderr)
|
|
189
|
+
|
|
190
|
+
start = time.time()
|
|
191
|
+
result = call_grok(
|
|
192
|
+
prompt=args.prompt,
|
|
193
|
+
mode=args.mode,
|
|
194
|
+
context=context,
|
|
195
|
+
system_override=args.system,
|
|
196
|
+
tools=tools,
|
|
197
|
+
timeout=args.timeout,
|
|
198
|
+
)
|
|
199
|
+
elapsed = time.time() - start
|
|
200
|
+
|
|
201
|
+
# Preserve raw result for output
|
|
202
|
+
raw_result = result
|
|
203
|
+
|
|
204
|
+
# Handle JSON tool call responses
|
|
205
|
+
response_data = None
|
|
206
|
+
normalized_result = result
|
|
207
|
+
if result.startswith("{") and '"tool_calls"' in result:
|
|
208
|
+
try:
|
|
209
|
+
response_data = json.loads(result)
|
|
210
|
+
normalized_result = response_data.get("content", result)
|
|
211
|
+
except json.JSONDecodeError:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# File writing logic
|
|
215
|
+
if args.apply or args.output_dir:
|
|
216
|
+
output_dir = args.output_dir or "./grok-output"
|
|
217
|
+
|
|
218
|
+
if use_morph and args.apply:
|
|
219
|
+
# Use Morph LLM for edits
|
|
220
|
+
from apply import parse_code_blocks
|
|
221
|
+
blocks = parse_code_blocks(normalized_result)
|
|
222
|
+
|
|
223
|
+
if not blocks:
|
|
224
|
+
print("\nNo code blocks found to apply via Morph.", file=sys.stderr)
|
|
225
|
+
else:
|
|
226
|
+
morph_result = apply_with_morph(blocks, output_dir)
|
|
227
|
+
print(f"\nApplied {morph_result['applied']}/{morph_result['total']} edits via Morph LLM",
|
|
228
|
+
file=sys.stderr)
|
|
229
|
+
if morph_result['errors']:
|
|
230
|
+
for err in morph_result['errors']:
|
|
231
|
+
print(f" Error: {err}", file=sys.stderr)
|
|
232
|
+
else:
|
|
233
|
+
# Use direct file writing
|
|
234
|
+
summary = parse_and_write(normalized_result, output_dir, dry_run=not args.apply)
|
|
235
|
+
print(summary, file=sys.stderr)
|
|
236
|
+
|
|
237
|
+
# Write raw output if requested
|
|
238
|
+
if args.output:
|
|
239
|
+
output_path = Path(args.output)
|
|
240
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
output_path.write_text(raw_result, encoding='utf-8')
|
|
242
|
+
print(f"Output written to {args.output}", file=sys.stderr)
|
|
243
|
+
|
|
244
|
+
# Execute command if requested
|
|
245
|
+
if args.execute:
|
|
246
|
+
print(f"\nExecuting: {args.execute}", file=sys.stderr)
|
|
247
|
+
exec_result = subprocess.run(
|
|
248
|
+
args.execute,
|
|
249
|
+
shell=True,
|
|
250
|
+
capture_output=True,
|
|
251
|
+
text=True,
|
|
252
|
+
timeout=300
|
|
253
|
+
)
|
|
254
|
+
if exec_result.stdout:
|
|
255
|
+
print(exec_result.stdout)
|
|
256
|
+
if exec_result.stderr:
|
|
257
|
+
print(exec_result.stderr, file=sys.stderr)
|
|
258
|
+
if exec_result.returncode != 0:
|
|
259
|
+
print(f"Command exited with code {exec_result.returncode}", file=sys.stderr)
|
|
260
|
+
sys.exit(exec_result.returncode)
|
|
261
|
+
|
|
262
|
+
# Output the normalized response to stdout (unless we wrote files and nothing else)
|
|
263
|
+
if not args.output and not args.execute:
|
|
264
|
+
print(normalized_result)
|
|
265
|
+
|
|
266
|
+
print(f"\nCompleted in {elapsed:.1f}s", file=sys.stderr)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|