diff --git a/skills/grok-refactor/SKILL.md b/skills/grok-refactor/SKILL.md index eae9d95..1e5cf94 100644 --- a/skills/grok-refactor/SKILL.md +++ b/skills/grok-refactor/SKILL.md @@ -78,6 +78,25 @@ echo '{"api_key": "sk-or-v1-..."}' > ~/.config/grok-swarm/config.json chmod 600 ~/.config/grok-swarm/config.json ``` +## Grounding Prompt (optional) + +Grok's role and default behaviour can be customised by creating a plain-text file at `~/.config/grok-swarm/system-prompt.txt`. Its contents are prepended to every mode-specific system prompt. Leave the file absent to use the built-in default. + +```bash +cat > ~/.config/grok-swarm/system-prompt.txt <<'EOF' +You are Grok, a senior engineer focused on security and correctness above all else. +EOF +``` + +## Usage Stats + +Track token spend across sessions: + +```bash +grok-swarm stats # all-time totals +grok-swarm stats --days 7 # last 7 days +``` + ## Installation ```bash diff --git a/skills/grok-refactor/bridge/grok_bridge.py b/skills/grok-refactor/bridge/grok_bridge.py index 173b4b1..be2dcbf 100644 --- a/skills/grok-refactor/bridge/grok_bridge.py +++ b/skills/grok-refactor/bridge/grok_bridge.py @@ -25,10 +25,68 @@ print("ERROR: openai package required. Install: pip3 install openai", file=sys.stderr) sys.exit(1) +try: + from usage_tracker import record_usage as _record_usage +except ImportError: + _record_usage = None + +_shared_dir = str(Path(__file__).parent.parent / "shared") +if _shared_dir not in sys.path: + sys.path.insert(0, _shared_dir) +try: + from patterns import get_filename_pattern_string as _get_filename_pattern_string +except ImportError: + _get_filename_pattern_string = None + OPENROUTER_BASE = "https://openrouter.ai/api/v1" MODEL_ID = "x-ai/grok-4.20-multi-agent-beta" +# Agent counts per thinking level +AGENT_COUNTS = { + "low": 4, + "high": 16, +} + +# Plain-language phrases that trigger High Thinking mode automatically +HIGH_THINKING_PHRASES = [ + "16 agent swarm", + "16-agent swarm", + "high thinking", + "high thinking mode", + "thinking mode high", + "--thinking high", +] + +# Default grounding prompt prepended to every mode-specific system prompt. +# Users can override this by creating ~/.config/grok-swarm/system-prompt.txt +DEFAULT_GROUNDING_PROMPT = ( + "You are Grok, a specialized agentic coding assistant powered by xAI's multi-agent swarm. " + "Your primary role is to help software engineers write, analyze, refactor, and debug code. " + "You have access to a large context window and collaborate with multiple parallel agents to " + "produce thorough, well-reasoned results. Always produce production-quality output: " + "correct, readable, and idiomatic for the target language. " + "When modifying files, annotate every code block with its file path so changes can be " + "applied automatically." +) + + +def load_grounding_prompt(): + """ + Return the user's custom grounding prompt from + ~/.config/grok-swarm/system-prompt.txt, or DEFAULT_GROUNDING_PROMPT. + """ + custom = Path.home() / ".config" / "grok-swarm" / "system-prompt.txt" + if custom.exists(): + try: + text = custom.read_text(encoding="utf-8").strip() + if text: + return text + except OSError: + pass + return DEFAULT_GROUNDING_PROMPT + + # Mode-specific system prompts MODE_PROMPTS = { "refactor": ( @@ -151,11 +209,20 @@ 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.). + ValueError for unsafe paths (containing ``..``, etc.). + + Absolute paths are rebased under ``output_path`` using only the filename + component (e.g. ``/home/user/.sandbox/task.py`` → ``/task.py``), + and a warning is printed to stderr. """ raw = Path(file_path) if raw.is_absolute(): - raise ValueError(f"Absolute paths are not allowed: {file_path!r}") + rebased = Path(raw.name) + print( + f"WARNING: Absolute path rebased to output dir: {file_path!r} → {rebased!r}", + file=sys.stderr, + ) + raw = rebased if ".." in raw.parts: raise ValueError(f"Path traversal not allowed: {file_path!r}") dest = (output_path / raw).resolve() @@ -184,10 +251,16 @@ def parse_and_write_files(response_text, output_dir): written = [] output_path = Path(output_dir) - # Pattern for lang:path at start of block (language tag contains path) + # Pattern 1: lang:path at start of block (relative OR absolute path) lang_path_pattern = re.compile(r'^(\w+):([^\s\n]+)\n', re.MULTILINE) - # Pattern for // FILE: or # FILE: markers + # Pattern 2: // FILE: or # FILE: markers inside the block file_marker_pattern = re.compile(r'^\s*(?://|#)\s*FILE:\s*(.+?)\s*$', re.MULTILINE) + # Pattern 3: bare '# filename.ext' as first line (common Grok output) + filename_pattern = ( + re.compile(_get_filename_pattern_string(), re.MULTILINE) + if _get_filename_pattern_string is not None + else None + ) def _write_file(file_path, content): """Validate path, write content, and record result. Returns True on success.""" @@ -222,10 +295,35 @@ def _write_file(file_path, content): marker_match = file_marker_pattern.search(part) if marker_match: _write_file(marker_match.group(1).strip(), part[marker_match.end():]) - + continue + + # Pattern 3: bare '# filename.ext' as first non-empty line + if filename_pattern is not None: + # Strip the language tag line if present, then scan for first non-empty content line + content_start = part.find('\n') + content = part[content_start + 1:] if content_start >= 0 else part + lines = content.splitlines(keepends=True) + for idx, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + fn_match = filename_pattern.match(stripped) + if fn_match: + filename = fn_match.group(1) + rest = "".join(lines[idx + 1:]) + _write_file(filename, rest) + # Only consider the first non-empty line for Pattern 3 + break + return written -def call_grok(prompt, mode="reason", context="", system_override=None, tools=None, timeout=120): +def detect_high_thinking(prompt): + """Return True if the prompt contains a plain-language High Thinking trigger.""" + lower = prompt.lower() + return any(phrase in lower for phrase in HIGH_THINKING_PHRASES) + + +def call_grok(prompt, mode="reason", context="", system_override=None, tools=None, timeout=120, thinking="low"): """Make the API call to Grok 4.20 Multi-Agent Beta.""" api_key = get_api_key() if not api_key: @@ -235,12 +333,15 @@ def call_grok(prompt, mode="reason", context="", system_override=None, tools=Non # Resolve system prompt if system_override: + # orchestrate mode: user owns the full system prompt, skip grounding system_content = system_override else: - system_content = MODE_PROMPTS.get(mode) - if system_content is None: + mode_prompt = MODE_PROMPTS.get(mode) + if mode_prompt is None: print(f"ERROR: Mode '{mode}' requires --system flag", file=sys.stderr) sys.exit(1) + grounding = load_grounding_prompt() + system_content = grounding + "\n\n" + mode_prompt # Append context to system prompt if context: @@ -262,7 +363,7 @@ def call_grok(prompt, mode="reason", context="", system_override=None, tools=Non "messages": messages, "max_tokens": 16384, "temperature": 0.3, - "extra_body": {"agent_count": 4}, + "extra_body": {"agent_count": AGENT_COUNTS.get(thinking, AGENT_COUNTS["low"])}, } if tools: @@ -288,8 +389,22 @@ def call_grok(prompt, mode="reason", context="", system_override=None, tools=Non # Log usage if hasattr(response, 'usage') and response.usage: u = response.usage - print(f"USAGE: mode={mode} prompt={u.prompt_tokens} completion={u.completion_tokens} " + agent_count = AGENT_COUNTS.get(thinking, AGENT_COUNTS["low"]) + print(f"USAGE: mode={mode} thinking={thinking} agents={agent_count} " + f"prompt={u.prompt_tokens} completion={u.completion_tokens} " f"total={u.total_tokens} time={elapsed:.1f}s", file=sys.stderr) + if _record_usage is not None: + try: + _record_usage( + mode=mode, + thinking=thinking, + prompt_tokens=u.prompt_tokens, + completion_tokens=u.completion_tokens, + total_tokens=u.total_tokens, + elapsed_secs=elapsed, + ) + except Exception: + pass # Handle content filtering if choice.finish_reason == "content_filter": @@ -337,9 +452,20 @@ def main(): 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/)") + parser.add_argument("--thinking", default=None, choices=["low", "high"], + help="Thinking level: low (4 agents) or high (16 agents, High Thinking mode) (default: low)") args = parser.parse_args() + # Auto-detect High Thinking mode from plain language in prompt (only if not explicitly set) + thinking = args.thinking + if thinking is None: + if detect_high_thinking(args.prompt): + thinking = "high" + print("INFO: High Thinking mode detected from prompt — using 16-agent swarm", file=sys.stderr) + else: + thinking = "low" + # Validate orchestrate mode if args.mode == "orchestrate" and not args.system: print("ERROR: --mode orchestrate requires --system flag", file=sys.stderr) @@ -356,7 +482,9 @@ def main(): tools = load_tools(args.tools) # Call Grok - print(f"Calling {MODEL_ID} (mode={args.mode}, 4 agents, timeout={args.timeout}s)...", file=sys.stderr) + agent_count = AGENT_COUNTS.get(thinking, AGENT_COUNTS["low"]) + thinking_label = " [HIGH THINKING MODE — 16-agent swarm]" if thinking == "high" else "" + print(f"Calling {MODEL_ID} (mode={args.mode}, {agent_count} agents, timeout={args.timeout}s){thinking_label}...", file=sys.stderr) result = call_grok( prompt=args.prompt, mode=args.mode, @@ -364,6 +492,7 @@ def main(): system_override=args.system, tools=tools, timeout=args.timeout, + thinking=thinking, ) # Output @@ -380,11 +509,18 @@ def main(): print(f" {rel_path} ({byte_count:,} bytes)") print(f"Total: {total_bytes:,} bytes") else: + # Save full response as a fallback so no output is lost + fallback_dir = Path(args.output_dir) + fallback_dir.mkdir(parents=True, exist_ok=True) + fallback_path = fallback_dir / "grok-response.txt" + fallback_path.write_text(result, encoding="utf-8") print( - "No annotated files found in model response to write to disk.\n" - "Re-run without --write-files to see the full response.", + f"ERROR: No annotated files found in model response.\n" + f"Full response saved to: {fallback_path}\n" + f"Tip: ask Grok to annotate code blocks with ```lang:path/to/file or # FILE: path/to/file", file=sys.stderr, ) + sys.exit(1) elif not args.output: print(result) diff --git a/skills/grok-refactor/bridge/usage_tracker.py b/skills/grok-refactor/bridge/usage_tracker.py new file mode 100644 index 0000000..c47fe95 --- /dev/null +++ b/skills/grok-refactor/bridge/usage_tracker.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +usage_tracker.py — Persistent token/cost tracking for grok-swarm requests. + +Logs each API call to ~/.config/grok-swarm/usage.json (newline-delimited JSON). +Provides aggregation and a human-readable stats report. + +OpenRouter pricing for x-ai/grok-4.20-multi-agent-beta (as of 2026-03): + Prompt tokens: $5.00 / 1M tokens + Completion tokens: $15.00 / 1M tokens +""" + +import json +import sys +from datetime import datetime, timezone, timedelta +from pathlib import Path + +USAGE_FILE = Path.home() / ".config" / "grok-swarm" / "usage.json" + +# OpenRouter pricing (USD per 1M tokens) +PROMPT_PRICE_PER_M = 5.00 +COMPLETION_PRICE_PER_M = 15.00 + + +def record_usage(mode, thinking, prompt_tokens, completion_tokens, total_tokens, elapsed_secs): + """ + Append one usage record to the log file. + Never raises — failures are printed to stderr and silently ignored. + """ + record = { + "ts": datetime.now(timezone.utc).isoformat(), + "mode": mode, + "thinking": thinking, + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens, + "elapsed_secs": round(elapsed_secs, 2), + "cost_usd": round( + (prompt_tokens / 1_000_000) * PROMPT_PRICE_PER_M + + (completion_tokens / 1_000_000) * COMPLETION_PRICE_PER_M, + 6, + ), + } + try: + USAGE_FILE.parent.mkdir(parents=True, exist_ok=True) + with USAGE_FILE.open("a", encoding="utf-8") as f: + f.write(json.dumps(record) + "\n") + except OSError as exc: + print(f"WARNING: Could not write usage log: {exc}", file=sys.stderr) + + +def get_stats(since_days=None): + """ + Read the usage log and return aggregated stats dict. + + Args: + since_days: If set, only include records from the last N days. + + Returns dict with keys: + calls, prompt_tokens, completion_tokens, total_tokens, + cost_usd, elapsed_secs, by_mode (dict of mode -> call count) + """ + if not USAGE_FILE.exists(): + return None + + cutoff = None + if since_days is not None: + cutoff = datetime.now(timezone.utc) - timedelta(days=since_days) + + stats = { + "calls": 0, + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "cost_usd": 0.0, + "elapsed_secs": 0.0, + "by_mode": {}, + } + + try: + with USAGE_FILE.open(encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + rec = json.loads(line) + except json.JSONDecodeError: + continue + + if cutoff: + try: + rec_ts = datetime.fromisoformat(rec["ts"]) + # Normalize naive timestamps (assume UTC) so comparison is safe + if rec_ts.tzinfo is None: + rec_ts = rec_ts.replace(tzinfo=timezone.utc) + if rec_ts < cutoff: + continue + except (KeyError, ValueError, TypeError): + pass + + stats["calls"] += 1 + stats["prompt_tokens"] += rec.get("prompt_tokens", 0) + stats["completion_tokens"] += rec.get("completion_tokens", 0) + stats["total_tokens"] += rec.get("total_tokens", 0) + stats["cost_usd"] += rec.get("cost_usd", 0.0) + stats["elapsed_secs"] += rec.get("elapsed_secs", 0.0) + + mode = rec.get("mode", "unknown") + stats["by_mode"][mode] = stats["by_mode"].get(mode, 0) + 1 + + except OSError as exc: + print(f"WARNING: Could not read usage log: {exc}", file=sys.stderr) + return None + + return stats + + +def format_stats_report(stats, since_days=None): + """Return a human-readable stats report string.""" + if stats is None or stats["calls"] == 0: + return "No usage data recorded yet. Run a grok-swarm command to start tracking." + + period = f"last {since_days} day(s)" if since_days else "all time" + lines = [ + "", + "=" * 55, + f" grok-swarm usage stats ({period})", + "=" * 55, + f" Requests: {stats['calls']:>10,}", + f" Prompt tokens: {stats['prompt_tokens']:>10,}", + f" Completion tokens: {stats['completion_tokens']:>10,}", + f" Total tokens: {stats['total_tokens']:>10,}", + f" Estimated cost: ${stats['cost_usd']:>10.4f}", + f" Total time: {stats['elapsed_secs']:>9.1f}s", + ] + if stats["by_mode"]: + lines.append("") + lines.append(" By mode:") + for mode, count in sorted(stats["by_mode"].items(), key=lambda x: -x[1]): + lines.append(f" {mode:<16} {count:>6,} call(s)") + lines.append("=" * 55) + lines.append(f" Log file: {USAGE_FILE}") + lines.append("=" * 55) + return "\n".join(lines) diff --git a/skills/grok-refactor/shared/patterns.py b/skills/grok-refactor/shared/patterns.py new file mode 100644 index 0000000..d5ec915 --- /dev/null +++ b/skills/grok-refactor/shared/patterns.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +""" +patterns.py — Shared regex patterns for parsing Grok response code blocks. + +Used by grok_bridge.py and grok_agent.py to identify file annotations inside +fenced code blocks in Grok's markdown output. +""" + + +def get_filename_pattern_string(): + """ + Return a regex string matching '# filename.ext' as the first line of a + code block — a common Grok output pattern when no explicit FILE: marker + is present. + + Pattern matches lines like: + # task.py + # utils/helpers.js + # README.md + + Does NOT match: + # /absolute/path/file.py (use file_marker_pattern for those) + # some comment without extension + # FILE: path (handled by file_marker_pattern) + + Group 1 captures the filename (possibly with a relative subdirectory). + """ + return r'^#\s+([^\s/][^\s]*\.[a-zA-Z0-9]+)\s*$' diff --git a/src/bridge/cli.py b/src/bridge/cli.py index 628dbb6..eda5b74 100644 --- a/src/bridge/cli.py +++ b/src/bridge/cli.py @@ -23,6 +23,7 @@ sys.path.insert(0, str(current)) from grok_bridge import call_grok, read_files, MODE_PROMPTS +from usage_tracker import get_stats, format_stats_report def check_morph_available(): @@ -131,7 +132,23 @@ def parse_and_write(result_text, output_dir, dry_run=True): return format_summary(result, output_dir) +def _handle_stats(argv): + """Handle `grok-swarm stats [--days N]` subcommand.""" + import argparse as _ap + p = _ap.ArgumentParser(prog="grok-swarm stats", description="Show token/cost usage statistics") + p.add_argument("--days", "-d", type=int, default=None, + metavar="N", help="Limit to last N days (default: all time)") + args = p.parse_args(argv) + stats = get_stats(since_days=args.days) + print(format_stats_report(stats, since_days=args.days)) + + def main(): + # Dispatch stats subcommand before full argument parsing + if len(sys.argv) > 1 and sys.argv[1] == "stats": + _handle_stats(sys.argv[2:]) + return + parser = argparse.ArgumentParser( description="Grok Swarm — Multi-agent CLI powered by Grok 4.20", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/src/bridge/usage_tracker.py b/src/bridge/usage_tracker.py new file mode 100644 index 0000000..c47fe95 --- /dev/null +++ b/src/bridge/usage_tracker.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +usage_tracker.py — Persistent token/cost tracking for grok-swarm requests. + +Logs each API call to ~/.config/grok-swarm/usage.json (newline-delimited JSON). +Provides aggregation and a human-readable stats report. + +OpenRouter pricing for x-ai/grok-4.20-multi-agent-beta (as of 2026-03): + Prompt tokens: $5.00 / 1M tokens + Completion tokens: $15.00 / 1M tokens +""" + +import json +import sys +from datetime import datetime, timezone, timedelta +from pathlib import Path + +USAGE_FILE = Path.home() / ".config" / "grok-swarm" / "usage.json" + +# OpenRouter pricing (USD per 1M tokens) +PROMPT_PRICE_PER_M = 5.00 +COMPLETION_PRICE_PER_M = 15.00 + + +def record_usage(mode, thinking, prompt_tokens, completion_tokens, total_tokens, elapsed_secs): + """ + Append one usage record to the log file. + Never raises — failures are printed to stderr and silently ignored. + """ + record = { + "ts": datetime.now(timezone.utc).isoformat(), + "mode": mode, + "thinking": thinking, + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens, + "elapsed_secs": round(elapsed_secs, 2), + "cost_usd": round( + (prompt_tokens / 1_000_000) * PROMPT_PRICE_PER_M + + (completion_tokens / 1_000_000) * COMPLETION_PRICE_PER_M, + 6, + ), + } + try: + USAGE_FILE.parent.mkdir(parents=True, exist_ok=True) + with USAGE_FILE.open("a", encoding="utf-8") as f: + f.write(json.dumps(record) + "\n") + except OSError as exc: + print(f"WARNING: Could not write usage log: {exc}", file=sys.stderr) + + +def get_stats(since_days=None): + """ + Read the usage log and return aggregated stats dict. + + Args: + since_days: If set, only include records from the last N days. + + Returns dict with keys: + calls, prompt_tokens, completion_tokens, total_tokens, + cost_usd, elapsed_secs, by_mode (dict of mode -> call count) + """ + if not USAGE_FILE.exists(): + return None + + cutoff = None + if since_days is not None: + cutoff = datetime.now(timezone.utc) - timedelta(days=since_days) + + stats = { + "calls": 0, + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "cost_usd": 0.0, + "elapsed_secs": 0.0, + "by_mode": {}, + } + + try: + with USAGE_FILE.open(encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + rec = json.loads(line) + except json.JSONDecodeError: + continue + + if cutoff: + try: + rec_ts = datetime.fromisoformat(rec["ts"]) + # Normalize naive timestamps (assume UTC) so comparison is safe + if rec_ts.tzinfo is None: + rec_ts = rec_ts.replace(tzinfo=timezone.utc) + if rec_ts < cutoff: + continue + except (KeyError, ValueError, TypeError): + pass + + stats["calls"] += 1 + stats["prompt_tokens"] += rec.get("prompt_tokens", 0) + stats["completion_tokens"] += rec.get("completion_tokens", 0) + stats["total_tokens"] += rec.get("total_tokens", 0) + stats["cost_usd"] += rec.get("cost_usd", 0.0) + stats["elapsed_secs"] += rec.get("elapsed_secs", 0.0) + + mode = rec.get("mode", "unknown") + stats["by_mode"][mode] = stats["by_mode"].get(mode, 0) + 1 + + except OSError as exc: + print(f"WARNING: Could not read usage log: {exc}", file=sys.stderr) + return None + + return stats + + +def format_stats_report(stats, since_days=None): + """Return a human-readable stats report string.""" + if stats is None or stats["calls"] == 0: + return "No usage data recorded yet. Run a grok-swarm command to start tracking." + + period = f"last {since_days} day(s)" if since_days else "all time" + lines = [ + "", + "=" * 55, + f" grok-swarm usage stats ({period})", + "=" * 55, + f" Requests: {stats['calls']:>10,}", + f" Prompt tokens: {stats['prompt_tokens']:>10,}", + f" Completion tokens: {stats['completion_tokens']:>10,}", + f" Total tokens: {stats['total_tokens']:>10,}", + f" Estimated cost: ${stats['cost_usd']:>10.4f}", + f" Total time: {stats['elapsed_secs']:>9.1f}s", + ] + if stats["by_mode"]: + lines.append("") + lines.append(" By mode:") + for mode, count in sorted(stats["by_mode"].items(), key=lambda x: -x[1]): + lines.append(f" {mode:<16} {count:>6,} call(s)") + lines.append("=" * 55) + lines.append(f" Log file: {USAGE_FILE}") + lines.append("=" * 55) + return "\n".join(lines) diff --git a/src/plugin/grok_agent_plugin.ts b/src/plugin/grok_agent_plugin.ts index a3ec699..9be6cf4 100644 --- a/src/plugin/grok_agent_plugin.ts +++ b/src/plugin/grok_agent_plugin.ts @@ -18,6 +18,7 @@ import { Type } from "@sinclair/typebox"; const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); const DEFAULT_AGENT = join(PLUGIN_ROOT, "src", "agent", "grok_agent.py"); +const DEFAULT_PYTHON = join(PLUGIN_ROOT, "skills", "grok-refactor", ".venv", "bin", "python3"); const GrokAgentSchema = Type.Object({ task: Type.String({ description: "Natural language task instruction" }), @@ -57,6 +58,7 @@ export default function (api: any) { try { const agentScript = api.config?.agentScript || DEFAULT_AGENT; + const pythonPath = api.config?.pythonPath || DEFAULT_PYTHON; // Validate agent script exists if (!existsSync(agentScript)) { @@ -88,7 +90,7 @@ export default function (api: any) { // Spawn agent with timeout enforcement return new Promise((resolve) => { - const child = spawn("python3", args, { + const child = spawn(pythonPath, args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env }, }); diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 0656297..01c16da 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -77,7 +77,7 @@ const GrokSwarmSchema = Type.Object({ ), }); -export default function (api: any) { +export default async function (api: any) { api.registerTool( { name: "grok_swarm", @@ -214,4 +214,8 @@ export default function (api: any) { }, { optional: true }, ); + + // Register autonomous agent tool + const { default: registerAgent } = await import("./grok_agent_plugin"); + registerAgent(api); } \ No newline at end of file