Skip to content

Commit 195dbf2

Browse files
committed
Add command to grokcode.
1 parent b4afcfc commit 195dbf2

17 files changed

Lines changed: 976 additions & 4 deletions

File tree

grokcode/agent/multi_agent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async def run_multi_agent(
4545
from grokcode.agent.grok_client import GrokClient
4646
from grokcode.agent.tool_registry import ToolRegistry
4747
from grokcode.tools.bash import BashTool
48-
from grokcode.tools.fs import FS_TOOL_SCHEMAS, edit_file, read_directory, read_file, write_file
48+
from grokcode.tools.fs import FS_TOOL_SCHEMAS, edit_file, glob_files, grep_files, read_directory, read_file, write_file
4949
from grokcode.utils.ui import console
5050

5151
async with GrokClient(api_key=api_key, model=config.model, max_tokens=config.max_tokens) as client:
@@ -91,6 +91,8 @@ async def run_subtask(subtask: SubtaskPlan) -> list[str]:
9191
registry.register("read_directory", lambda path, recursive=False: read_directory(path, recursive), FS_TOOL_SCHEMAS[1])
9292
registry.register("write_file", lambda path, content: locked_write_file(path, content), FS_TOOL_SCHEMAS[2])
9393
registry.register("edit_file", lambda path, old_str, new_str: locked_edit_file(path, old_str, new_str), FS_TOOL_SCHEMAS[3])
94+
registry.register("glob_files", lambda pattern, directory=".": glob_files(pattern, directory), FS_TOOL_SCHEMAS[5])
95+
registry.register("grep_files", lambda pattern, directory=".", file_glob="**/*": grep_files(pattern, directory, file_glob), FS_TOOL_SCHEMAS[6])
9496

9597
async with GrokClient(api_key=api_key, model=config.model, max_tokens=config.max_tokens) as sub_client:
9698
agent = Agent(config=config, tool_registry=registry, grok_client=sub_client)

grokcode/cli/repl.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ def _xai_logo() -> Text:
9393
("/mcp remove <name>", "Remove an MCP server and its token"),
9494
("/mcp test <name>", "Test connection and list available tools"),
9595
],
96+
"Onboarding": [
97+
("/onboard", "Generate an onboarding audio guide for this codebase"),
98+
("/onboard --no-audio", "Generate onboarding script only (no audio)"),
99+
("/onboard --voice <v>", "Choose voice for audio (default: alloy)"),
100+
],
96101
"General": [
97102
("/init", "Create a GROKCODE.md with instructions for Grok"),
98103
("/help", "Show this help"),
@@ -810,6 +815,13 @@ def run_repl(config: AppConfig, api_key: str) -> None:
810815
except KeyboardInterrupt:
811816
console.print("\n [dim]Interrupted.[/dim]")
812817

818+
elif cmd == "/onboard" or cmd.startswith("/onboard "):
819+
args = user_input[len("/onboard"):].split()
820+
try:
821+
asyncio.run(_handle_onboard(args, config, api_key))
822+
except KeyboardInterrupt:
823+
console.print("\n [dim]Interrupted.[/dim]")
824+
813825
else:
814826
try:
815827
asyncio.run(_execute_task(user_input, api_key, config))
@@ -842,3 +854,10 @@ async def _execute_task(
842854
dry_run=dry_run,
843855
session_id=session_id,
844856
)
857+
858+
859+
async def _handle_onboard(args: list[str], config: AppConfig, api_key: str) -> None:
860+
"""Delegate to the onboard command handler."""
861+
from grokcode.repl.commands.onboard import handle_onboard
862+
863+
await handle_onboard(args, config, api_key)

grokcode/cli/workspace.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def workspace_init(
5353

5454

5555
async def _workspace_init(name: str, team_id: str) -> None:
56+
import subprocess
57+
5658
from grokcode.workspace.collections_client import CollectionsClient
5759
from grokcode.workspace.workspace import init_workspace
5860

@@ -64,14 +66,31 @@ async def _workspace_init(name: str, team_id: str) -> None:
6466
print_error(f"Failed to create workspace: {e}")
6567
raise typer.Exit(1)
6668

69+
# Automatically stage and commit the workspace config file
70+
with console.status("[cyan]Committing workspace config...[/cyan]"):
71+
try:
72+
subprocess.run(
73+
["git", "add", "grokcode.workspace.json"],
74+
check=True,
75+
capture_output=True,
76+
)
77+
subprocess.run(
78+
["git", "commit", "-m", "Add workspace config"],
79+
check=True,
80+
capture_output=True,
81+
)
82+
commit_note = " [dim]✓ grokcode.workspace.json committed to git[/dim]\n\n"
83+
except subprocess.CalledProcessError:
84+
commit_note = " [yellow]⚠ Could not auto-commit (not a git repo or nothing to commit)[/yellow]\n\n"
85+
6786
console.print(
6887
Panel(
6988
f"[bold green]Workspace created: {name}[/bold green]\n\n"
7089
f"[dim]Collection ID:[/dim] {workspace_data['collection_id']}\n"
7190
f"[dim]Team ID:[/dim] {team_id}\n\n"
72-
"Next steps:\n"
73-
" 1. [cyan]git add grokcode.workspace.json && git commit -m 'Add workspace config'[/cyan]\n"
74-
" 2. [cyan]grokcode workspace index ./docs ./README.md[/cyan]",
91+
+ commit_note +
92+
"Next step:\n"
93+
" [cyan]grokcode workspace index ./docs ./README.md[/cyan]",
7594
border_style="green",
7695
title="Workspace Initialized",
7796
)

grokcode/onboarding/__init__.py

Whitespace-only changes.

grokcode/onboarding/analyser.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
INCLUDE_EXTENSIONS = {
6+
".py", ".ts", ".tsx", ".js", ".jsx", ".java", ".go", ".rs", ".rb",
7+
".cs", ".cpp", ".c", ".h", ".md", ".json", ".yaml", ".yml", ".toml",
8+
}
9+
INCLUDE_FILENAMES = {".env.example", "Dockerfile", "docker-compose.yml"}
10+
SKIP_DIRS = {
11+
".git", "node_modules", "__pycache__", ".venv", "venv", "env",
12+
"dist", "build", ".next", ".nuxt", "target", "vendor", ".grokcode",
13+
}
14+
MAX_FILE_SIZE = 50 * 1024 # 50 KB
15+
KEY_FILES = {
16+
"README.md", "pyproject.toml", "package.json", "go.mod", "Cargo.toml",
17+
"pom.xml", "Dockerfile", "docker-compose.yml",
18+
}
19+
KEY_FILE_LINES = 100
20+
OTHER_FILE_LINES = 30
21+
OTHER_FILES_CAP = 60 # avoid enormous summaries
22+
23+
24+
def collect_files(path: Path) -> list[Path]:
25+
"""Recursively collect relevant source files, skipping ignored dirs and large files."""
26+
result: list[Path] = []
27+
for entry in path.rglob("*"):
28+
if any(part in SKIP_DIRS for part in entry.parts):
29+
continue
30+
if not entry.is_file():
31+
continue
32+
if entry.suffix.lower() not in INCLUDE_EXTENSIONS and entry.name not in INCLUDE_FILENAMES:
33+
continue
34+
try:
35+
if entry.stat().st_size > MAX_FILE_SIZE:
36+
continue
37+
except OSError:
38+
continue
39+
result.append(entry)
40+
return sorted(result)
41+
42+
43+
def _build_tree(path: Path, max_depth: int = 2) -> str:
44+
"""Build a directory tree string up to max_depth levels."""
45+
lines: list[str] = [str(path) + "/"]
46+
47+
def _walk(p: Path, depth: int) -> None:
48+
if depth > max_depth:
49+
return
50+
try:
51+
entries = sorted(p.iterdir(), key=lambda x: (x.is_file(), x.name))
52+
except PermissionError:
53+
return
54+
for entry in entries:
55+
if entry.name in SKIP_DIRS:
56+
continue
57+
indent = " " * (depth - 1)
58+
suffix = "/" if entry.is_dir() else ""
59+
lines.append(f"{indent}├── {entry.name}{suffix}")
60+
if entry.is_dir() and depth < max_depth:
61+
_walk(entry, depth + 1)
62+
63+
_walk(path, 1)
64+
return "\n".join(lines)
65+
66+
67+
def build_summary(path: Path, files: list[Path]) -> str:
68+
"""Build a structured codebase summary string to send to Grok."""
69+
sections: list[str] = []
70+
71+
sections.append("## Directory Structure\n" + _build_tree(path))
72+
73+
key_found: list[Path] = []
74+
other_files: list[Path] = []
75+
test_files: list[Path] = []
76+
ci_files: list[Path] = []
77+
78+
for f in files:
79+
try:
80+
rel = f.relative_to(path)
81+
except ValueError:
82+
rel = Path(f.name)
83+
84+
if f.name in KEY_FILES:
85+
key_found.append(f)
86+
elif "test" in f.name.lower() or any("test" in part.lower() for part in rel.parts):
87+
test_files.append(rel)
88+
elif ".github" in rel.parts and "workflows" in rel.parts:
89+
ci_files.append(rel)
90+
else:
91+
other_files.append(f)
92+
93+
if key_found:
94+
sections.append("## Key Files")
95+
for f in key_found:
96+
try:
97+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()[:KEY_FILE_LINES]
98+
sections.append(f"### {f.name}\n```\n" + "\n".join(lines) + "\n```")
99+
except OSError:
100+
pass
101+
102+
if other_files:
103+
sections.append("## Source Files (first 30 lines each)")
104+
for f in other_files[:OTHER_FILES_CAP]:
105+
try:
106+
rel = f.relative_to(path)
107+
except ValueError:
108+
rel = Path(f.name)
109+
try:
110+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()[:OTHER_FILE_LINES]
111+
sections.append(f"### {rel}\n```\n" + "\n".join(lines) + "\n```")
112+
except OSError:
113+
pass
114+
115+
if test_files:
116+
sections.append("## Test Files\n" + "\n".join(f"- {p}" for p in test_files))
117+
118+
if ci_files:
119+
sections.append("## CI/CD Config\n" + "\n".join(f"- {p}" for p in ci_files))
120+
121+
return "\n\n".join(sections)

grokcode/onboarding/audio.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import httpx
6+
7+
from grokcode.utils.ui import print_warning
8+
9+
XAI_TTS_URL = "https://api.x.ai/v1/tts"
10+
11+
# Available xAI voices: eve, ara, rex, sal, leo
12+
DEFAULT_VOICE = "eve"
13+
14+
15+
def generate_audio(script: str, api_key: str, voice: str = DEFAULT_VOICE) -> bytes | None:
16+
"""Call the xAI TTS API. Returns raw MP3 bytes, or None on failure (non-fatal)."""
17+
try:
18+
resp = httpx.post(
19+
XAI_TTS_URL,
20+
json={
21+
"text": script,
22+
"voice_id": voice,
23+
"language": "en",
24+
"output_format": {
25+
"codec": "mp3",
26+
"sample_rate": 24000,
27+
"bit_rate": 128000,
28+
},
29+
},
30+
headers={
31+
"Authorization": f"Bearer {api_key}",
32+
"Content-Type": "application/json",
33+
},
34+
timeout=60.0,
35+
)
36+
if resp.status_code in (403, 404):
37+
print_warning(
38+
"Audio API not available — check that your xAI API key has audio access. "
39+
"The onboarding.md has been saved. You can generate audio manually later."
40+
)
41+
return None
42+
resp.raise_for_status()
43+
return resp.content
44+
except httpx.HTTPStatusError as e:
45+
print_warning(
46+
f"Audio API error {e.response.status_code} — "
47+
"The onboarding.md has been saved. You can generate audio manually later."
48+
)
49+
return None
50+
except Exception as e:
51+
print_warning(f"Audio generation failed: {e}")
52+
return None
53+
54+
55+
def save_audio(audio_bytes: bytes, output_path: Path) -> None:
56+
"""Write raw audio bytes to file."""
57+
output_path.write_bytes(audio_bytes)

grokcode/onboarding/player.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import platform
4+
import subprocess
5+
6+
from grokcode.utils.ui import console
7+
8+
9+
def play_audio(path: str) -> bool:
10+
"""Attempt cross-platform MP3 playback. Returns True if a player succeeded."""
11+
system = platform.system()
12+
13+
if system == "Darwin":
14+
candidates = [["afplay", path]]
15+
elif system == "Linux":
16+
candidates = [
17+
["mpg123", path],
18+
["ffplay", "-nodisp", "-autoexit", path],
19+
]
20+
else:
21+
# Windows: SoundPlayer only supports WAV — skip
22+
console.print(
23+
f" [dim]Auto-play not supported on Windows. Open {path} manually to listen.[/dim]"
24+
)
25+
return False
26+
27+
for cmd in candidates:
28+
result = subprocess.run(cmd, capture_output=True)
29+
if result.returncode == 0:
30+
return True
31+
32+
console.print(
33+
f" [dim]Could not auto-play audio. Open {path} manually to listen.[/dim]"
34+
)
35+
return False

0 commit comments

Comments
 (0)