@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/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()