bharatcode 0.1.0__py3-none-any.whl
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.
- bharatcode/__init__.py +13 -0
- bharatcode/agent.py +2088 -0
- bharatcode/commands.py +1072 -0
- bharatcode/config.py +93 -0
- bharatcode/coordinator.py +670 -0
- bharatcode/cost.py +77 -0
- bharatcode/diff.py +113 -0
- bharatcode/hooks.py +75 -0
- bharatcode/index.py +155 -0
- bharatcode/main.py +846 -0
- bharatcode/memory.py +286 -0
- bharatcode/permissions.py +99 -0
- bharatcode/project.py +179 -0
- bharatcode/session_storage.py +108 -0
- bharatcode/skills.py +1746 -0
- bharatcode/subagent.py +363 -0
- bharatcode/tools.py +1021 -0
- bharatcode/ui.py +72 -0
- bharatcode-0.1.0.dist-info/METADATA +150 -0
- bharatcode-0.1.0.dist-info/RECORD +23 -0
- bharatcode-0.1.0.dist-info/WHEEL +5 -0
- bharatcode-0.1.0.dist-info/entry_points.txt +3 -0
- bharatcode-0.1.0.dist-info/top_level.txt +1 -0
bharatcode/commands.py
ADDED
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slash commands — like Claude Code's /clear, /compact, /review, /cost, /doctor, /git, /memory, /skill, /plan.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from .ui import console, show_info, show_success, show_warning, show_error
|
|
6
|
+
from .config import load_config, save_config, model_label
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ── Interactive dropdown helpers ───────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
def _try_questionary_select(prompt: str, choices: list) -> str | None:
|
|
12
|
+
"""
|
|
13
|
+
Show an interactive arrow-key dropdown using questionary.
|
|
14
|
+
Each item in choices is (display_label, value).
|
|
15
|
+
Returns the selected value, or None if cancelled / questionary not installed.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
import questionary
|
|
19
|
+
from questionary import Style
|
|
20
|
+
|
|
21
|
+
q_style = Style([
|
|
22
|
+
("highlighted", "fg:cyan bold"),
|
|
23
|
+
("pointer", "fg:cyan bold"),
|
|
24
|
+
("selected", "fg:green"),
|
|
25
|
+
("question", "fg:yellow bold"),
|
|
26
|
+
("instruction", "fg:gray italic"),
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
q_choices = [
|
|
30
|
+
questionary.Choice(title=label, value=val)
|
|
31
|
+
for label, val in choices
|
|
32
|
+
]
|
|
33
|
+
q_choices.append(questionary.Separator())
|
|
34
|
+
q_choices.append(questionary.Choice(title="↩ Cancel", value=None))
|
|
35
|
+
|
|
36
|
+
result = questionary.select(
|
|
37
|
+
prompt,
|
|
38
|
+
choices=q_choices,
|
|
39
|
+
style=q_style,
|
|
40
|
+
instruction=" (↑↓ move Enter select Ctrl-C cancel)",
|
|
41
|
+
).ask()
|
|
42
|
+
return result
|
|
43
|
+
except ImportError:
|
|
44
|
+
return "__FALLBACK__"
|
|
45
|
+
except (KeyboardInterrupt, EOFError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _numbered_select(title: str, choices: list) -> str | None:
|
|
50
|
+
"""
|
|
51
|
+
Fallback numbered list when questionary is not installed.
|
|
52
|
+
choices = [(label, value), ...]
|
|
53
|
+
"""
|
|
54
|
+
console.print(f"\n[bold]{title}[/bold]")
|
|
55
|
+
for i, (label, val) in enumerate(choices, 1):
|
|
56
|
+
console.print(f" [green]{i:>2}[/green] {label}")
|
|
57
|
+
console.print()
|
|
58
|
+
try:
|
|
59
|
+
raw = input(" Enter number (or name, or Enter to cancel): ").strip()
|
|
60
|
+
except (EOFError, KeyboardInterrupt):
|
|
61
|
+
return None
|
|
62
|
+
if not raw:
|
|
63
|
+
return None
|
|
64
|
+
if raw.isdigit():
|
|
65
|
+
idx = int(raw) - 1
|
|
66
|
+
if 0 <= idx < len(choices):
|
|
67
|
+
return choices[idx][1]
|
|
68
|
+
show_error(f"Invalid number: {raw}")
|
|
69
|
+
return None
|
|
70
|
+
# Try matching by value
|
|
71
|
+
for label, val in choices:
|
|
72
|
+
if raw.lower() == str(val).lower():
|
|
73
|
+
return val
|
|
74
|
+
show_error(f"Not found: {raw}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _select(prompt: str, choices: list) -> str | None:
|
|
79
|
+
"""Show questionary dropdown, fallback to numbered list."""
|
|
80
|
+
result = _try_questionary_select(prompt, choices)
|
|
81
|
+
if result == "__FALLBACK__":
|
|
82
|
+
return _numbered_select(prompt, choices)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
COMMANDS: dict[str, dict] = {}
|
|
86
|
+
|
|
87
|
+
def command(name: str, description: str):
|
|
88
|
+
def decorator(fn):
|
|
89
|
+
COMMANDS[name] = {"fn": fn, "description": description}
|
|
90
|
+
return fn
|
|
91
|
+
return decorator
|
|
92
|
+
|
|
93
|
+
def handle_slash_command(cmd: str, session: dict) -> bool:
|
|
94
|
+
parts = cmd.strip().lstrip("/").split(maxsplit=1)
|
|
95
|
+
name = parts[0].lower()
|
|
96
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
97
|
+
|
|
98
|
+
if name in COMMANDS:
|
|
99
|
+
COMMANDS[name]["fn"](args, session)
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
show_warning(f"Unknown command: /{name} — type /help")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# ── Conversation ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
@command("clear", "Clear conversation history")
|
|
108
|
+
def cmd_clear(args: str, session: dict):
|
|
109
|
+
session["messages"] = []
|
|
110
|
+
show_success("Conversation cleared.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@command("changes", "Show all files modified this session")
|
|
114
|
+
def cmd_changes(args: str, session: dict):
|
|
115
|
+
change_log = session.get("change_log", {})
|
|
116
|
+
if not change_log:
|
|
117
|
+
show_info("No files modified this session.")
|
|
118
|
+
return
|
|
119
|
+
console.print(
|
|
120
|
+
f"\n[bold]Session Changes[/bold] "
|
|
121
|
+
f"[dim]({len(change_log)} file{'s' if len(change_log) > 1 else ''} touched)[/dim]\n"
|
|
122
|
+
)
|
|
123
|
+
for path in sorted(change_log.keys()):
|
|
124
|
+
stats = change_log[path]
|
|
125
|
+
writes = stats.get("writes", 0)
|
|
126
|
+
edits = stats.get("edits", 0)
|
|
127
|
+
parts = []
|
|
128
|
+
if writes:
|
|
129
|
+
parts.append(f"[green]{writes} write{'s' if writes > 1 else ''}[/green]")
|
|
130
|
+
if edits:
|
|
131
|
+
parts.append(f"[cyan]{edits} edit{'s' if edits > 1 else ''}[/cyan]")
|
|
132
|
+
stat_str = " ".join(parts) if parts else "[dim]touched[/dim]"
|
|
133
|
+
console.print(f" [cyan]{path}[/cyan] {stat_str}")
|
|
134
|
+
console.print()
|
|
135
|
+
|
|
136
|
+
@command("compact", "Summarise old conversation history to free up context (use when session is very long)")
|
|
137
|
+
def cmd_compact(args: str, session: dict):
|
|
138
|
+
from .agent import _auto_compact, _estimate_tokens
|
|
139
|
+
from openai import OpenAI
|
|
140
|
+
from .config import get_api_key, load_config
|
|
141
|
+
|
|
142
|
+
msgs = session.get("messages", [])
|
|
143
|
+
if len(msgs) < 4:
|
|
144
|
+
show_info("Not enough history to compact.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
before = _estimate_tokens(msgs)
|
|
148
|
+
cfg = load_config()
|
|
149
|
+
client = OpenAI(api_key=get_api_key(), base_url="https://api.deepseek.com")
|
|
150
|
+
|
|
151
|
+
# Force compact regardless of threshold
|
|
152
|
+
from .agent import _COMPACT_TARGET_RATIO
|
|
153
|
+
import math
|
|
154
|
+
cutoff = max(2, math.floor(len(msgs) * _COMPACT_TARGET_RATIO))
|
|
155
|
+
# Temporarily lower threshold so _auto_compact fires
|
|
156
|
+
import bharatcode.agent as _ag
|
|
157
|
+
old_thresh = _ag._COMPACT_THRESHOLD
|
|
158
|
+
_ag._COMPACT_THRESHOLD = 0
|
|
159
|
+
compacted = _auto_compact(
|
|
160
|
+
msgs, client, cfg.get("model", "deepseek-v4-flash"),
|
|
161
|
+
file_cache=session.get("file_cache", {}), # Feature 6: pass cache for context-aware compact
|
|
162
|
+
)
|
|
163
|
+
_ag._COMPACT_THRESHOLD = old_thresh
|
|
164
|
+
|
|
165
|
+
after = _estimate_tokens(msgs)
|
|
166
|
+
if compacted:
|
|
167
|
+
show_success(f"Compacted: ~{before:,} → ~{after:,} tokens ({len(msgs)} messages remaining)")
|
|
168
|
+
else:
|
|
169
|
+
show_info("Nothing to compact.")
|
|
170
|
+
|
|
171
|
+
# ── Code Actions ──────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
@command("review", "Review current directory code")
|
|
174
|
+
def cmd_review(args: str, session: dict):
|
|
175
|
+
from .agent import run_agent
|
|
176
|
+
target = args or os.getcwd()
|
|
177
|
+
show_info(f"Reviewing: {target}")
|
|
178
|
+
run_agent(
|
|
179
|
+
f"Do a thorough code review of: {target}. Check bugs, security, Indian compliance.",
|
|
180
|
+
project_path=os.getcwd(),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@command("audit", "Run Indian compliance audit (DPDP, RBI, GST)")
|
|
184
|
+
def cmd_audit(args: str, session: dict):
|
|
185
|
+
from .agent import run_agent
|
|
186
|
+
show_info("Running Indian compliance audit...")
|
|
187
|
+
run_agent(
|
|
188
|
+
"Audit this project for DPDP Act 2023, RBI, GST, Aadhaar/PAN compliance.",
|
|
189
|
+
project_path=os.getcwd(),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@command("plan", "Toggle plan mode — agent reads only and proposes plan before any changes")
|
|
193
|
+
def cmd_plan(args: str, session: dict):
|
|
194
|
+
arg = args.strip().lower()
|
|
195
|
+
|
|
196
|
+
# Explicit on/off/approve
|
|
197
|
+
if arg in ("on",):
|
|
198
|
+
session["plan_mode"] = True
|
|
199
|
+
elif arg in ("off", "approve", "go", "execute", "yes", "y"):
|
|
200
|
+
session["plan_mode"] = False
|
|
201
|
+
else:
|
|
202
|
+
session["plan_mode"] = not session.get("plan_mode", False)
|
|
203
|
+
|
|
204
|
+
if session.get("plan_mode"):
|
|
205
|
+
console.print(
|
|
206
|
+
"\n[bold cyan]PLAN MODE ON[/bold cyan] "
|
|
207
|
+
"[dim]Agent will only read files and propose a plan — no writes, no bash.[/dim]\n"
|
|
208
|
+
"[dim]When you're happy with the plan, type [/dim][cyan]/plan off[/cyan][dim] then re-send your task to execute.[/dim]\n"
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
console.print(
|
|
212
|
+
"\n[bold green]PLAN MODE OFF[/bold green] "
|
|
213
|
+
"[dim]Agent can now write files and run commands.[/dim]\n"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# ── New Website / App ────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
@command("newsite", "Build a website: /newsite Chhelu Portfolio - developer portfolio")
|
|
219
|
+
def cmd_newsite(args: str, session: dict):
|
|
220
|
+
from .skills import ask_skill_questions, build_skill_prompt
|
|
221
|
+
from .agent import run_agent
|
|
222
|
+
import os
|
|
223
|
+
|
|
224
|
+
prefilled: dict = {}
|
|
225
|
+
if args.strip():
|
|
226
|
+
parts = args.strip().split(" - ", 1) if " - " in args else args.strip().split(",", 1)
|
|
227
|
+
if len(parts) == 2:
|
|
228
|
+
prefilled["name"] = parts[0].strip()
|
|
229
|
+
prefilled["desc"] = parts[1].strip()
|
|
230
|
+
else:
|
|
231
|
+
prefilled["name"] = args.strip()
|
|
232
|
+
|
|
233
|
+
answers = ask_skill_questions("newsite", prefilled=prefilled)
|
|
234
|
+
if answers is None:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
name = answers.get("name", "website")
|
|
238
|
+
if not name:
|
|
239
|
+
show_error("Site name is required.")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
dest = name.lower().replace(" ", "-")
|
|
243
|
+
dest_path = os.path.join(os.getcwd(), dest)
|
|
244
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
245
|
+
console.print(f"[dim]Output: {dest_path}[/dim]\n")
|
|
246
|
+
|
|
247
|
+
task = f"""{build_skill_prompt("newsite", answers)}
|
|
248
|
+
|
|
249
|
+
Output folder (absolute): {dest_path}
|
|
250
|
+
|
|
251
|
+
CRITICAL PATH RULE: Every file MUST use the full absolute path.
|
|
252
|
+
Correct: <<<FILE:{dest_path}/frontend/src/App.jsx>>>
|
|
253
|
+
Correct: <<<FILE:{dest_path}/backend/app/__init__.py>>>
|
|
254
|
+
WRONG: <<<FILE:frontend/src/App.jsx>>>"""
|
|
255
|
+
|
|
256
|
+
run_agent(task, project_path=dest_path,
|
|
257
|
+
auto_approve=session.get("auto_approve", False),
|
|
258
|
+
history=session.get("messages"),
|
|
259
|
+
system_content=session.get("system"),
|
|
260
|
+
file_cache=session.get("file_cache"))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@command("newapp", "Build an app: /newapp TaskFlow - kanban project manager")
|
|
264
|
+
def cmd_newapp(args: str, session: dict):
|
|
265
|
+
from .skills import ask_skill_questions, build_skill_prompt
|
|
266
|
+
from .agent import run_agent
|
|
267
|
+
import os
|
|
268
|
+
|
|
269
|
+
prefilled: dict = {}
|
|
270
|
+
raw = args.strip()
|
|
271
|
+
if raw:
|
|
272
|
+
parts = raw.split(" - ", 1) if " - " in raw else raw.split(",", 1)
|
|
273
|
+
if len(parts) == 2:
|
|
274
|
+
prefilled["name"] = parts[0].strip()
|
|
275
|
+
prefilled["desc"] = parts[1].strip()
|
|
276
|
+
else:
|
|
277
|
+
prefilled["name"] = raw.strip()
|
|
278
|
+
|
|
279
|
+
answers = ask_skill_questions("newapp", prefilled=prefilled)
|
|
280
|
+
if answers is None:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
name = answers.get("name", "app")
|
|
284
|
+
if not name:
|
|
285
|
+
show_error("App name is required.")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
dest = name.lower().replace(" ", "-")
|
|
289
|
+
dest_path = os.path.join(os.getcwd(), dest)
|
|
290
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
291
|
+
console.print(f"[dim]Output: {dest_path}[/dim]\n")
|
|
292
|
+
|
|
293
|
+
task = f"""{build_skill_prompt("newapp", answers)}
|
|
294
|
+
|
|
295
|
+
Output folder (absolute): {dest_path}
|
|
296
|
+
|
|
297
|
+
CRITICAL PATH RULE: Every file MUST use the full absolute path.
|
|
298
|
+
Correct: <<<FILE:{dest_path}/frontend/src/App.jsx>>>
|
|
299
|
+
Correct: <<<FILE:{dest_path}/backend/app/__init__.py>>>
|
|
300
|
+
WRONG: <<<FILE:frontend/src/App.jsx>>>"""
|
|
301
|
+
|
|
302
|
+
run_agent(task, project_path=dest_path,
|
|
303
|
+
auto_approve=session.get("auto_approve", False),
|
|
304
|
+
history=session.get("messages"),
|
|
305
|
+
system_content=session.get("system"),
|
|
306
|
+
file_cache=session.get("file_cache"))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ── Skills ────────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
_SKILL_DESCRIPTIONS = {
|
|
312
|
+
"newsite": "Full-stack site — pick frontend + backend tech, frontend/ + backend/ folders",
|
|
313
|
+
"newapp": "Full-stack app — pick frontend + backend tech, detailed per-framework rules",
|
|
314
|
+
"docker": "Dockerize everything — multi-stage build, compose, healthcheck, .dockerignore",
|
|
315
|
+
"ci-github": "GitHub Actions CI/CD — lint → test → build → deploy, caching, secrets",
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@command("skills", "Browse and run skills interactively (arrow-key dropdown)")
|
|
320
|
+
def cmd_skills(args: str, session: dict):
|
|
321
|
+
from .skills import load_skills, BUILTIN_SKILLS, ask_skill_questions, build_skill_prompt, get_skill_raw
|
|
322
|
+
from .agent import run_agent
|
|
323
|
+
|
|
324
|
+
skills = load_skills()
|
|
325
|
+
builtin = list(BUILTIN_SKILLS.keys())
|
|
326
|
+
custom = [k for k in skills if k not in builtin]
|
|
327
|
+
|
|
328
|
+
choices = []
|
|
329
|
+
for name in builtin:
|
|
330
|
+
desc = _SKILL_DESCRIPTIONS.get(name, "")
|
|
331
|
+
choices.append((f"{name:<18} [dim]{desc}[/dim]", name))
|
|
332
|
+
for name in custom:
|
|
333
|
+
preview = skills[name].split("\n")[0][:55]
|
|
334
|
+
choices.append((f"{name:<18} [cyan](custom)[/cyan] {preview}", name))
|
|
335
|
+
|
|
336
|
+
selected = _select("Select a skill to run:", choices)
|
|
337
|
+
if not selected:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
_run_skill(selected, session)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@command("skill", "Run a skill directly: /skill razorpay")
|
|
344
|
+
def cmd_skill(args: str, session: dict):
|
|
345
|
+
if not args:
|
|
346
|
+
cmd_skills("", session)
|
|
347
|
+
return
|
|
348
|
+
_run_skill(args.strip().lower(), session)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _run_skill(name: str, session: dict) -> None:
|
|
352
|
+
"""Ask Q&A for a skill, build the prompt, and run the agent."""
|
|
353
|
+
from .skills import BUILTIN_SKILLS, ask_skill_questions, build_skill_prompt, get_skill_raw
|
|
354
|
+
from .agent import run_agent
|
|
355
|
+
import os
|
|
356
|
+
|
|
357
|
+
if name in BUILTIN_SKILLS:
|
|
358
|
+
# Interactive Q&A for built-in skills
|
|
359
|
+
answers = ask_skill_questions(name)
|
|
360
|
+
if answers is None:
|
|
361
|
+
return # user cancelled
|
|
362
|
+
|
|
363
|
+
# Scaffold skills need a real output folder
|
|
364
|
+
task_prompt = build_skill_prompt(name, answers)
|
|
365
|
+
|
|
366
|
+
if name in ("newsite", "newapp"):
|
|
367
|
+
proj_name = answers.get("name", name)
|
|
368
|
+
dest = proj_name.lower().replace(" ", "-")
|
|
369
|
+
dest_path = os.path.join(os.getcwd(), dest)
|
|
370
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
371
|
+
console.print(f"[dim]Output: {dest_path}[/dim]\n")
|
|
372
|
+
task_prompt = (
|
|
373
|
+
f"{task_prompt}\n\nOutput folder (absolute): {dest_path}\n"
|
|
374
|
+
f"CRITICAL PATH RULE: Every file MUST be written inside {dest_path}."
|
|
375
|
+
)
|
|
376
|
+
run_agent(task_prompt, project_path=dest_path,
|
|
377
|
+
auto_approve=session.get("auto_approve", False),
|
|
378
|
+
history=session.get("messages"),
|
|
379
|
+
system_content=session.get("system"),
|
|
380
|
+
file_cache=session.get("file_cache"))
|
|
381
|
+
else:
|
|
382
|
+
show_info(f"Running skill: {name}")
|
|
383
|
+
run_agent(task_prompt, project_path=os.getcwd(),
|
|
384
|
+
auto_approve=session.get("auto_approve", False),
|
|
385
|
+
history=session.get("messages"),
|
|
386
|
+
system_content=session.get("system"),
|
|
387
|
+
file_cache=session.get("file_cache"))
|
|
388
|
+
else:
|
|
389
|
+
# Custom file-based skill — raw prompt, no Q&A
|
|
390
|
+
raw = get_skill_raw(name)
|
|
391
|
+
if not raw:
|
|
392
|
+
show_error(f"Skill '{name}' not found. Type /skills to browse.")
|
|
393
|
+
return
|
|
394
|
+
show_info(f"Running custom skill: {name}")
|
|
395
|
+
run_agent(raw, project_path=os.getcwd(),
|
|
396
|
+
auto_approve=session.get("auto_approve", False),
|
|
397
|
+
history=session.get("messages"),
|
|
398
|
+
system_content=session.get("system"),
|
|
399
|
+
file_cache=session.get("file_cache"))
|
|
400
|
+
|
|
401
|
+
# ── Git ───────────────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
@command("git", "Show git status and recent commits")
|
|
404
|
+
def cmd_git(args: str, session: dict):
|
|
405
|
+
import subprocess
|
|
406
|
+
try:
|
|
407
|
+
status = subprocess.run(
|
|
408
|
+
["git", "status", "--short"], capture_output=True, text=True, timeout=5
|
|
409
|
+
).stdout
|
|
410
|
+
log = subprocess.run(
|
|
411
|
+
["git", "log", "--oneline", "-8"], capture_output=True, text=True, timeout=5
|
|
412
|
+
).stdout
|
|
413
|
+
branch = subprocess.run(
|
|
414
|
+
["git", "branch", "--show-current"], capture_output=True, text=True, timeout=5
|
|
415
|
+
).stdout.strip()
|
|
416
|
+
|
|
417
|
+
console.print(f"\n[bold]Git Status[/bold] branch: [cyan]{branch}[/cyan]")
|
|
418
|
+
if status:
|
|
419
|
+
for line in status.splitlines():
|
|
420
|
+
color = "green" if line.startswith("?") else "yellow" if line.startswith("M") else "red"
|
|
421
|
+
console.print(f" [{color}]{line}[/{color}]")
|
|
422
|
+
else:
|
|
423
|
+
console.print(" [dim]Clean working tree[/dim]")
|
|
424
|
+
|
|
425
|
+
if log:
|
|
426
|
+
console.print("\n[bold]Recent Commits[/bold]")
|
|
427
|
+
for line in log.splitlines():
|
|
428
|
+
sha, *rest = line.split(" ", 1)
|
|
429
|
+
console.print(f" [dim]{sha}[/dim] {' '.join(rest)}")
|
|
430
|
+
console.print()
|
|
431
|
+
except FileNotFoundError:
|
|
432
|
+
show_error("git not found in PATH")
|
|
433
|
+
except Exception as e:
|
|
434
|
+
show_error(str(e))
|
|
435
|
+
|
|
436
|
+
@command("diff", "Show uncommitted git changes")
|
|
437
|
+
def cmd_diff(args: str, session: dict):
|
|
438
|
+
import subprocess
|
|
439
|
+
try:
|
|
440
|
+
diff = subprocess.run(
|
|
441
|
+
["git", "diff", "--stat"], capture_output=True, text=True, timeout=5
|
|
442
|
+
).stdout
|
|
443
|
+
full = subprocess.run(
|
|
444
|
+
["git", "diff"], capture_output=True, text=True, timeout=5
|
|
445
|
+
).stdout
|
|
446
|
+
if not diff and not full:
|
|
447
|
+
console.print("[dim]No uncommitted changes.[/dim]")
|
|
448
|
+
return
|
|
449
|
+
console.print(f"\n[bold]Uncommitted Changes[/bold]\n{diff}")
|
|
450
|
+
if full and len(full) < 6000:
|
|
451
|
+
from rich.syntax import Syntax
|
|
452
|
+
console.print(Syntax(full, "diff", theme="monokai"))
|
|
453
|
+
elif full:
|
|
454
|
+
console.print(f"[dim](diff too large to display — {len(full):,} chars)[/dim]")
|
|
455
|
+
except FileNotFoundError:
|
|
456
|
+
show_error("git not found")
|
|
457
|
+
except Exception as e:
|
|
458
|
+
show_error(str(e))
|
|
459
|
+
|
|
460
|
+
# ── Cost & Status ─────────────────────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
@command("cost", "Show session token usage and estimated cost")
|
|
463
|
+
def cmd_cost(args: str, session: dict):
|
|
464
|
+
from .cost import session_cost
|
|
465
|
+
session_cost.display(console)
|
|
466
|
+
|
|
467
|
+
@command("status", "Show Sylithe Code version, model, API status")
|
|
468
|
+
def cmd_status(args: str, session: dict):
|
|
469
|
+
import platform
|
|
470
|
+
from . import __version__
|
|
471
|
+
cfg = load_config()
|
|
472
|
+
key = cfg.get("api_key", "")
|
|
473
|
+
key_display = (key[:8] + "..." + key[-4:]) if key else "[red]NOT SET[/red]"
|
|
474
|
+
|
|
475
|
+
# Test API connectivity
|
|
476
|
+
api_ok = False
|
|
477
|
+
try:
|
|
478
|
+
from openai import OpenAI
|
|
479
|
+
client = OpenAI(api_key=key, base_url="https://api.deepseek.com")
|
|
480
|
+
client.models.list()
|
|
481
|
+
api_ok = True
|
|
482
|
+
except Exception:
|
|
483
|
+
pass
|
|
484
|
+
|
|
485
|
+
console.print(f"\n[bold]Sylithe Code Status[/bold]")
|
|
486
|
+
console.print(f" [dim]Version[/dim] [cyan]{__version__}[/cyan]")
|
|
487
|
+
console.print(f" [dim]Model[/dim] [cyan]{model_label(cfg.get('model', 'deepseek-v4-flash'))}[/cyan]")
|
|
488
|
+
console.print(f" [dim]API key[/dim] {key_display}")
|
|
489
|
+
console.print(f" [dim]API status[/dim] {'[green]connected[/green]' if api_ok else '[red]unreachable[/red]'}")
|
|
490
|
+
console.print(f" [dim]Python[/dim] [cyan]{platform.python_version()}[/cyan]")
|
|
491
|
+
console.print(f" [dim]Platform[/dim] [cyan]{platform.system()} {platform.release()}[/cyan]")
|
|
492
|
+
console.print(f" [dim]Workdir[/dim] [cyan]{os.getcwd()}[/cyan]")
|
|
493
|
+
console.print()
|
|
494
|
+
|
|
495
|
+
@command("doctor", "Diagnose Sylithe Code setup")
|
|
496
|
+
def cmd_doctor(args: str, session: dict):
|
|
497
|
+
import subprocess
|
|
498
|
+
checks = []
|
|
499
|
+
|
|
500
|
+
# Python version
|
|
501
|
+
import sys
|
|
502
|
+
py_ok = sys.version_info >= (3, 10)
|
|
503
|
+
checks.append(("Python >= 3.10", py_ok, f"Python {sys.version.split()[0]}"))
|
|
504
|
+
|
|
505
|
+
# API key
|
|
506
|
+
cfg = load_config()
|
|
507
|
+
key = cfg.get("api_key", "")
|
|
508
|
+
checks.append(("DeepSeek API key set", bool(key), key[:8] + "..." if key else "NOT SET"))
|
|
509
|
+
|
|
510
|
+
# API connectivity
|
|
511
|
+
api_ok = False
|
|
512
|
+
try:
|
|
513
|
+
from openai import OpenAI
|
|
514
|
+
client = OpenAI(api_key=key, base_url="https://api.deepseek.com")
|
|
515
|
+
client.models.list()
|
|
516
|
+
api_ok = True
|
|
517
|
+
except Exception as e:
|
|
518
|
+
pass
|
|
519
|
+
checks.append(("DeepSeek API reachable", api_ok, "OK" if api_ok else "Connection failed"))
|
|
520
|
+
|
|
521
|
+
# Git
|
|
522
|
+
try:
|
|
523
|
+
r = subprocess.run(["git", "--version"], capture_output=True, text=True, timeout=3)
|
|
524
|
+
git_ok = r.returncode == 0
|
|
525
|
+
git_ver = r.stdout.strip()
|
|
526
|
+
except Exception:
|
|
527
|
+
git_ok, git_ver = False, "not found"
|
|
528
|
+
checks.append(("git installed", git_ok, git_ver))
|
|
529
|
+
|
|
530
|
+
# Required packages
|
|
531
|
+
for pkg in ["rich", "click", "openai", "dotenv"]:
|
|
532
|
+
try:
|
|
533
|
+
__import__(pkg.replace("-", "_"))
|
|
534
|
+
checks.append((f"package: {pkg}", True, "installed"))
|
|
535
|
+
except ImportError:
|
|
536
|
+
checks.append((f"package: {pkg}", False, "MISSING — run: pip install " + pkg))
|
|
537
|
+
|
|
538
|
+
# BHARATCODE.md
|
|
539
|
+
has_md = (os.path.exists("BHARATCODE.md"))
|
|
540
|
+
checks.append(("BHARATCODE.md in project", has_md, "found" if has_md else "run: bharatcode init"))
|
|
541
|
+
|
|
542
|
+
console.print("\n[bold]Sylithe Code Doctor[/bold]\n")
|
|
543
|
+
for name, ok, detail in checks:
|
|
544
|
+
icon = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
545
|
+
color = "dim" if ok else "red"
|
|
546
|
+
console.print(f" {icon} {name:<30} [{color}]{detail}[/{color}]")
|
|
547
|
+
console.print()
|
|
548
|
+
all_ok = all(ok for _, ok, _ in checks)
|
|
549
|
+
if all_ok:
|
|
550
|
+
show_success("All checks passed! Sylithe Code is ready.")
|
|
551
|
+
else:
|
|
552
|
+
show_warning("Some checks failed. Fix the issues above.")
|
|
553
|
+
|
|
554
|
+
# ── Memory ────────────────────────────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
@command("memory", "Manage persistent memory: /memory [list] | /memory add <text> | /memory del <id>")
|
|
557
|
+
def cmd_memory(args: str, session: dict):
|
|
558
|
+
from .memory import add_memory, delete_memory, show_memories, MEMORY_DIR
|
|
559
|
+
parts = args.strip().split(maxsplit=1)
|
|
560
|
+
sub = parts[0].lower() if parts else "list"
|
|
561
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
562
|
+
|
|
563
|
+
if sub in ("list", "ls", ""):
|
|
564
|
+
show_memories(console)
|
|
565
|
+
console.print(f"[dim] Memory dir: {MEMORY_DIR}[/dim]")
|
|
566
|
+
elif sub == "add":
|
|
567
|
+
if not rest:
|
|
568
|
+
show_error("Usage: /memory add <text>")
|
|
569
|
+
return
|
|
570
|
+
entry = add_memory(rest)
|
|
571
|
+
show_success(f"Memory saved (id={entry['id']}): {rest[:60]}")
|
|
572
|
+
elif sub in ("del", "delete", "rm"):
|
|
573
|
+
try:
|
|
574
|
+
mid = int(rest)
|
|
575
|
+
if delete_memory(mid):
|
|
576
|
+
show_success(f"Memory {mid} deleted.")
|
|
577
|
+
else:
|
|
578
|
+
show_error(f"Memory id={mid} not found.")
|
|
579
|
+
except ValueError:
|
|
580
|
+
show_error("Usage: /memory del <id>")
|
|
581
|
+
else:
|
|
582
|
+
# Bare text — treat whole args as "add"
|
|
583
|
+
entry = add_memory(args)
|
|
584
|
+
show_success(f"Memory saved (id={entry['id']}): {args[:60]}")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@command("resume", "Resume a previous session: /resume [session_id]")
|
|
588
|
+
def cmd_resume(args: str, session: dict):
|
|
589
|
+
from . import session_storage
|
|
590
|
+
import os
|
|
591
|
+
|
|
592
|
+
cwd = os.getcwd()
|
|
593
|
+
args = args.strip()
|
|
594
|
+
recent = session_storage.list_recent(cwd, max_n=5)
|
|
595
|
+
|
|
596
|
+
if not recent:
|
|
597
|
+
show_info("No previous sessions found for this directory.")
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
# Pick which session to load
|
|
601
|
+
if args:
|
|
602
|
+
# User specified a session ID prefix
|
|
603
|
+
match = next((s for s in recent if s["session_id"].startswith(args)), None)
|
|
604
|
+
if not match:
|
|
605
|
+
show_error(f"Session '{args}' not found.")
|
|
606
|
+
for s in recent:
|
|
607
|
+
console.print(f" [dim]{s['session_id']}[/dim] {s['mtime_str']} {s['last_message']}")
|
|
608
|
+
return
|
|
609
|
+
chosen = match
|
|
610
|
+
else:
|
|
611
|
+
# Show list and let user pick
|
|
612
|
+
choices = [
|
|
613
|
+
(
|
|
614
|
+
f"{s['session_id']} {s['mtime_str']} ({s['turns']} turns) \"{s['last_message']}\"",
|
|
615
|
+
s["session_id"],
|
|
616
|
+
)
|
|
617
|
+
for s in recent
|
|
618
|
+
]
|
|
619
|
+
picked = _select("Resume which session?", choices)
|
|
620
|
+
if not picked:
|
|
621
|
+
return
|
|
622
|
+
chosen = next(s for s in recent if s["session_id"] == picked)
|
|
623
|
+
|
|
624
|
+
# Load messages
|
|
625
|
+
messages = session_storage.load_messages(chosen["path"])
|
|
626
|
+
if not messages:
|
|
627
|
+
show_error("Session file is empty.")
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
# Restore into session
|
|
631
|
+
session["messages"][:] = messages
|
|
632
|
+
session_storage.save_latest_pointer(cwd, chosen["session_id"])
|
|
633
|
+
|
|
634
|
+
show_success(
|
|
635
|
+
f"Resumed session {chosen['session_id']} — "
|
|
636
|
+
f"{len(messages)} messages, {chosen['turns']} user turns."
|
|
637
|
+
)
|
|
638
|
+
console.print(
|
|
639
|
+
f"[dim] Last message: \"{chosen['last_message']}\"[/dim]\n"
|
|
640
|
+
f"[dim] Continue where you left off — or just start typing a new task.[/dim]"
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
@command("yolo", "Toggle auto-approve mode (skip all permission prompts)")
|
|
646
|
+
def cmd_yolo(args: str, session: dict):
|
|
647
|
+
session["auto_approve"] = not session.get("auto_approve", False)
|
|
648
|
+
if session["auto_approve"]:
|
|
649
|
+
console.print("[yellow]Auto-approve ON[/yellow] — all bash commands run without prompts.")
|
|
650
|
+
else:
|
|
651
|
+
console.print("[green]Auto-approve OFF[/green] — permission prompts restored.")
|
|
652
|
+
|
|
653
|
+
@command("model", "Switch model: /model sylithe-flash | sylithe-pro")
|
|
654
|
+
def cmd_model(args: str, session: dict):
|
|
655
|
+
if not args:
|
|
656
|
+
cfg = load_config()
|
|
657
|
+
show_info(f"Current model: {model_label(cfg.get('model', 'deepseek-v4-flash'))}")
|
|
658
|
+
console.print(" Options: [cyan]Sylithe Code Flash[/cyan] [cyan]Sylithe Code Pro[/cyan]")
|
|
659
|
+
console.print(" [dim]Usage: /model sylithe-flash or /model sylithe-pro[/dim]")
|
|
660
|
+
return
|
|
661
|
+
from .config import MODEL_API_MAP
|
|
662
|
+
name = args.strip()
|
|
663
|
+
api_name = MODEL_API_MAP.get(name, name)
|
|
664
|
+
cfg = load_config()
|
|
665
|
+
cfg["model"] = api_name
|
|
666
|
+
save_config(cfg)
|
|
667
|
+
show_success(f"Model switched to: {model_label(api_name)}")
|
|
668
|
+
|
|
669
|
+
@command("pwd", "Show current working directory")
|
|
670
|
+
def cmd_pwd(args: str, session: dict):
|
|
671
|
+
show_info(os.getcwd())
|
|
672
|
+
|
|
673
|
+
@command("config", "Show current config")
|
|
674
|
+
def cmd_config(args: str, session: dict):
|
|
675
|
+
cfg = load_config()
|
|
676
|
+
console.print("\n[bold]Config[/bold]")
|
|
677
|
+
for k, v in cfg.items():
|
|
678
|
+
if k == "api_key" and v:
|
|
679
|
+
v = v[:8] + "..." + v[-4:]
|
|
680
|
+
console.print(f" [dim]{k:<20}[/dim] [cyan]{v}[/cyan]")
|
|
681
|
+
console.print()
|
|
682
|
+
|
|
683
|
+
# ── Agents ────────────────────────────────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
@command("agent", "Spawn a specialist AI agent — explore, coder, verifier, researcher: /agent <type> <task>")
|
|
686
|
+
def cmd_agent(args: str, session: dict):
|
|
687
|
+
from .subagent import run_subagent, AGENT_TYPES
|
|
688
|
+
|
|
689
|
+
parts = args.strip().split(maxsplit=1)
|
|
690
|
+
agent_type = parts[0].lower() if parts else ""
|
|
691
|
+
task = parts[1] if len(parts) > 1 else ""
|
|
692
|
+
|
|
693
|
+
if not agent_type or agent_type not in AGENT_TYPES:
|
|
694
|
+
# Show interactive selector with taglines
|
|
695
|
+
choices = [
|
|
696
|
+
(
|
|
697
|
+
f"{info['icon']} [{atype:<10}] {info['tagline']}",
|
|
698
|
+
atype,
|
|
699
|
+
)
|
|
700
|
+
for atype, info in AGENT_TYPES.items()
|
|
701
|
+
]
|
|
702
|
+
console.print(
|
|
703
|
+
"\n[bold]Sylithe Code Agents[/bold] [dim]— each specialist runs with its own isolated context[/dim]"
|
|
704
|
+
)
|
|
705
|
+
agent_type = _select("Which agent do you want to spawn?", choices)
|
|
706
|
+
if not agent_type:
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
if not task:
|
|
710
|
+
try:
|
|
711
|
+
info = AGENT_TYPES[agent_type]
|
|
712
|
+
console.print(
|
|
713
|
+
f"\n {info['icon']} [bold {info['color']}]{info['label']}[/bold {info['color']}] "
|
|
714
|
+
f"[dim]{info['tagline']}[/dim]\n"
|
|
715
|
+
)
|
|
716
|
+
console.print("[dim]What should this agent do? Be specific — it has no memory of your session.[/dim]")
|
|
717
|
+
task = input(" > ").strip()
|
|
718
|
+
except (EOFError, KeyboardInterrupt):
|
|
719
|
+
pass
|
|
720
|
+
if not task:
|
|
721
|
+
show_error("No task provided.")
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
result = run_subagent(
|
|
725
|
+
task=task,
|
|
726
|
+
agent_type=agent_type,
|
|
727
|
+
project_path=os.getcwd(),
|
|
728
|
+
parent_system=session.get("system"),
|
|
729
|
+
parent_file_cache=session.get("file_cache"),
|
|
730
|
+
)
|
|
731
|
+
if result.success:
|
|
732
|
+
if result.output:
|
|
733
|
+
from rich.panel import Panel
|
|
734
|
+
from rich.markdown import Markdown
|
|
735
|
+
console.print(Panel(
|
|
736
|
+
Markdown(result.output),
|
|
737
|
+
border_style="cyan",
|
|
738
|
+
title=f"[bold cyan]{AGENT_TYPES[agent_type]['label']} Agent[/bold cyan]",
|
|
739
|
+
padding=(0, 1),
|
|
740
|
+
))
|
|
741
|
+
show_success(f"{AGENT_TYPES[agent_type]['label']} agent completed in {result.duration:.1f}s")
|
|
742
|
+
else:
|
|
743
|
+
show_error(f"Agent failed: {result.error}")
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
# ── Coordinator Mode ──────────────────────────────────────────────────────────
|
|
747
|
+
|
|
748
|
+
@command("coordinator", "Enter coordinator mode — main agent orchestrates parallel specialist workers")
|
|
749
|
+
def cmd_coordinator(args: str, session: dict):
|
|
750
|
+
from .coordinator import WorkerPool, COORDINATOR_SYSTEM_PROMPT
|
|
751
|
+
|
|
752
|
+
if session.get("coordinator_mode"):
|
|
753
|
+
# Already in coordinator mode — show live worker status
|
|
754
|
+
pool = session.get("worker_pool")
|
|
755
|
+
if pool:
|
|
756
|
+
console.print("\n[bold cyan]Coordinator — Active Workers[/bold cyan]")
|
|
757
|
+
console.print(pool.status_table())
|
|
758
|
+
return
|
|
759
|
+
|
|
760
|
+
from rich.panel import Panel
|
|
761
|
+
console.print(Panel(
|
|
762
|
+
"[bold cyan]Coordinator Mode[/bold cyan]\n\n"
|
|
763
|
+
"Sylithe Code is now your orchestrator. It will:\n\n"
|
|
764
|
+
" 🚀 [cyan]spawn_worker[/cyan] — launch parallel specialist agents (non-blocking)\n"
|
|
765
|
+
" 📨 [cyan]send_message[/cyan] — continue a worker with new instructions\n"
|
|
766
|
+
" 🛑 [cyan]task_stop[/cyan] — kill a worker that went off track\n\n"
|
|
767
|
+
"Workers run in background threads and report back via [dim]<task-notification>[/dim] messages.\n"
|
|
768
|
+
"The coordinator synthesizes their findings and directs the next phase.\n\n"
|
|
769
|
+
"[dim]Worker types: explore / coder / verifier / researcher / general[/dim]\n"
|
|
770
|
+
"[dim]Type /workers to see active workers. /exit-coordinator to return to normal.[/dim]",
|
|
771
|
+
border_style="cyan",
|
|
772
|
+
title="[bold cyan]⚡ Coordinator Mode[/bold cyan]",
|
|
773
|
+
padding=(1, 2),
|
|
774
|
+
))
|
|
775
|
+
|
|
776
|
+
pool = WorkerPool()
|
|
777
|
+
base_system = session.get("system", "")
|
|
778
|
+
|
|
779
|
+
# Save base system so we can restore it on exit
|
|
780
|
+
session["base_system"] = base_system
|
|
781
|
+
session["system"] = base_system + COORDINATOR_SYSTEM_PROMPT
|
|
782
|
+
session["coordinator_mode"] = True
|
|
783
|
+
session["worker_pool"] = pool
|
|
784
|
+
|
|
785
|
+
show_success("Coordinator mode active. Send your task and I'll orchestrate workers to solve it.")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@command("workers", "Show status of all coordinator workers in this session")
|
|
789
|
+
def cmd_workers(args: str, session: dict):
|
|
790
|
+
if not session.get("coordinator_mode"):
|
|
791
|
+
show_warning("Not in coordinator mode. Type /coordinator to enter.")
|
|
792
|
+
return
|
|
793
|
+
pool = session.get("worker_pool")
|
|
794
|
+
if not pool:
|
|
795
|
+
show_warning("No worker pool found.")
|
|
796
|
+
return
|
|
797
|
+
console.print("\n[bold]Active Workers[/bold]")
|
|
798
|
+
console.print(pool.status_table())
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
@command("exit-coordinator", "Exit coordinator mode and return to normal agent mode")
|
|
802
|
+
def cmd_exit_coordinator(args: str, session: dict):
|
|
803
|
+
if not session.get("coordinator_mode"):
|
|
804
|
+
show_info("Already in normal mode.")
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
pool = session.get("worker_pool")
|
|
808
|
+
if pool:
|
|
809
|
+
# Show final summary before exit
|
|
810
|
+
from rich.table import Table
|
|
811
|
+
console.print("\n[bold]Final Worker Summary[/bold]")
|
|
812
|
+
console.print(pool.status_table())
|
|
813
|
+
|
|
814
|
+
session["coordinator_mode"] = False
|
|
815
|
+
session["worker_pool"] = None
|
|
816
|
+
session["system"] = session.get("base_system", session.get("system", ""))
|
|
817
|
+
show_success("Returned to normal agent mode.")
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
# ── Help ──────────────────────────────────────────────────────────────────────
|
|
821
|
+
|
|
822
|
+
@command("export", "Export session as Markdown: /export or /export session.md")
|
|
823
|
+
def cmd_export(args: str, session: dict):
|
|
824
|
+
"""Save the current conversation to a Markdown file."""
|
|
825
|
+
import datetime
|
|
826
|
+
from pathlib import Path
|
|
827
|
+
|
|
828
|
+
history = session.get("messages", [])
|
|
829
|
+
if not history:
|
|
830
|
+
show_warning("Nothing to export — conversation is empty.")
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
# Build output path
|
|
834
|
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
835
|
+
default_name = f"bharatcode_session_{ts}.md"
|
|
836
|
+
out_path = Path(args.strip() or default_name)
|
|
837
|
+
if out_path.is_dir():
|
|
838
|
+
out_path = out_path / default_name
|
|
839
|
+
|
|
840
|
+
lines: list[str] = [f"# BharatCode Session — {datetime.datetime.now():%Y-%m-%d %H:%M}\n\n"]
|
|
841
|
+
for msg in history:
|
|
842
|
+
role = msg.get("role", "")
|
|
843
|
+
if role == "user":
|
|
844
|
+
content = msg.get("content", "")
|
|
845
|
+
if isinstance(content, list):
|
|
846
|
+
# multipart: extract text blocks only
|
|
847
|
+
content = " ".join(
|
|
848
|
+
b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"
|
|
849
|
+
)
|
|
850
|
+
lines.append(f"**You:** {content}\n\n")
|
|
851
|
+
elif role == "assistant":
|
|
852
|
+
content = msg.get("content") or ""
|
|
853
|
+
if isinstance(content, list):
|
|
854
|
+
content = " ".join(
|
|
855
|
+
b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"
|
|
856
|
+
)
|
|
857
|
+
if content:
|
|
858
|
+
lines.append(f"**BharatCode:** {content}\n\n")
|
|
859
|
+
# tool calls summary
|
|
860
|
+
tool_calls = msg.get("tool_calls", [])
|
|
861
|
+
if tool_calls:
|
|
862
|
+
names = ", ".join(
|
|
863
|
+
tc.get("function", {}).get("name", "?") for tc in tool_calls
|
|
864
|
+
)
|
|
865
|
+
lines.append(f"*Tools used: {names}*\n\n")
|
|
866
|
+
elif role == "tool":
|
|
867
|
+
pass # skip raw tool results — they clutter the export
|
|
868
|
+
|
|
869
|
+
try:
|
|
870
|
+
out_path.write_text("".join(lines), encoding="utf-8")
|
|
871
|
+
show_success(f"Session exported to {out_path} ({len(history)} messages)")
|
|
872
|
+
except Exception as exc:
|
|
873
|
+
show_error(f"Export failed: {exc}")
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
_HELP_GROUPS = {
|
|
877
|
+
"Scaffold": ["newsite", "newapp"],
|
|
878
|
+
"Coordinator": ["coordinator", "workers", "exit-coordinator"],
|
|
879
|
+
"Sub-agents": ["agent"],
|
|
880
|
+
"Conversation": ["clear", "compact", "changes", "resume", "export"],
|
|
881
|
+
"Code Actions": ["review", "audit", "plan"],
|
|
882
|
+
"Skills": ["skills", "skill"],
|
|
883
|
+
"Git": ["git", "diff"],
|
|
884
|
+
"Status": ["cost", "status", "doctor"],
|
|
885
|
+
"Memory": ["memory"],
|
|
886
|
+
"Settings": ["yolo", "model", "config", "pwd"],
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
_CLI_CMDS = [
|
|
890
|
+
("bharatcode new website \"Name\"", "Build a website from scratch"),
|
|
891
|
+
("bharatcode new website \"Name\" \"desc\"", "Website with description"),
|
|
892
|
+
("bharatcode new app \"Name\" --type flask", "Flask app from scratch"),
|
|
893
|
+
("bharatcode new app \"Name\" --type react", "React app from scratch"),
|
|
894
|
+
("bharatcode new app \"Name\" --type fullstack", "Full-stack app from scratch"),
|
|
895
|
+
("bharatcode new app \"Name\" --type node", "Node.js app from scratch"),
|
|
896
|
+
("bharatcode new app \"Name\" --type nextjs", "Next.js app from scratch"),
|
|
897
|
+
("bharatcode fix \"bug\"", "Fix a bug"),
|
|
898
|
+
("bharatcode build \"feature\"", "Build a feature"),
|
|
899
|
+
("bharatcode review [path]", "Code review"),
|
|
900
|
+
("bharatcode test src/file.py", "Write & run tests"),
|
|
901
|
+
("bharatcode audit", "Indian compliance"),
|
|
902
|
+
("bharatcode ask \"question\"", "Ask about code"),
|
|
903
|
+
("bharatcode explain src/file.py", "Explain a file"),
|
|
904
|
+
("bharatcode refactor src/file.py", "Refactor code"),
|
|
905
|
+
("bharatcode init", "Create BHARATCODE.md"),
|
|
906
|
+
("bharatcode -y fix \"bug\"", "Fix, skip prompts"),
|
|
907
|
+
]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@command("help", "Show all commands (interactive dropdown)")
|
|
911
|
+
def cmd_help(args: str, session: dict):
|
|
912
|
+
# Build flat choice list from all groups
|
|
913
|
+
choices = []
|
|
914
|
+
for group, names in _HELP_GROUPS.items():
|
|
915
|
+
for name in names:
|
|
916
|
+
if name in COMMANDS:
|
|
917
|
+
desc = COMMANDS[name]["description"]
|
|
918
|
+
choices.append((
|
|
919
|
+
f"/{name:<14} [dim]{desc}[/dim] [bright_black]({group})[/bright_black]",
|
|
920
|
+
name,
|
|
921
|
+
))
|
|
922
|
+
|
|
923
|
+
selected = _select("Select a command to learn more or run:", choices)
|
|
924
|
+
|
|
925
|
+
if selected is None:
|
|
926
|
+
# User cancelled — print full help instead
|
|
927
|
+
_print_full_help()
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
# Show details for selected command
|
|
931
|
+
desc = COMMANDS[selected]["description"]
|
|
932
|
+
console.print(f"\n[bold green]/{selected}[/bold green] {desc}\n")
|
|
933
|
+
|
|
934
|
+
# Command-specific help text
|
|
935
|
+
detailed = {
|
|
936
|
+
"newsite": (
|
|
937
|
+
"Builds a fully custom website — not a generic template, a real site designed for your project.\n"
|
|
938
|
+
"Splits CSS into variables/reset/typography/layout/components/responsive files.\n"
|
|
939
|
+
"Usage: /newsite <name>\n"
|
|
940
|
+
" /newsite <name> - <description>\n"
|
|
941
|
+
"Example: /newsite Chhelu Portfolio - dark theme developer portfolio with projects and blog"
|
|
942
|
+
),
|
|
943
|
+
"newapp": (
|
|
944
|
+
"Builds a complete application with proper file separation, real models, real routes, real logic.\n"
|
|
945
|
+
"Full-stack apps get CORS configured, Vite proxy, .env files, and exact startup commands.\n"
|
|
946
|
+
"Usage: /newapp <name> [--flask|--react|--fullstack|--node|--nextjs]\n"
|
|
947
|
+
"Example: /newapp ShopIndia e-commerce with Razorpay payments --fullstack"
|
|
948
|
+
),
|
|
949
|
+
"agent": (
|
|
950
|
+
"Spawn a specialist AI agent that runs with its own isolated context and dedicated tools.\n"
|
|
951
|
+
"Types:\n"
|
|
952
|
+
" explore — reads everything, writes nothing, maps your codebase\n"
|
|
953
|
+
" coder — full access, ships complete production code\n"
|
|
954
|
+
" verifier — ruthless auditor, finds every bug and security hole\n"
|
|
955
|
+
" researcher — live web fetcher, gets real docs and real examples\n"
|
|
956
|
+
" general — all tools, no restrictions\n"
|
|
957
|
+
"Usage: /agent <type> <task>\n"
|
|
958
|
+
"Example: /agent verifier Read backend/app.py and report every security issue with line numbers"
|
|
959
|
+
),
|
|
960
|
+
"skills": (
|
|
961
|
+
"Opens an interactive dropdown with all available skills.\n"
|
|
962
|
+
"Arrow keys to navigate, Enter to select, Ctrl-C to cancel.\n"
|
|
963
|
+
"After selecting, some skills (newsite/newapp) ask for a project name."
|
|
964
|
+
),
|
|
965
|
+
"skill": (
|
|
966
|
+
"Run a skill directly without the dropdown.\n"
|
|
967
|
+
"Usage: /skill <name>\n"
|
|
968
|
+
"Example: /skill razorpay | /skill docker | /skill jwt-auth"
|
|
969
|
+
),
|
|
970
|
+
"plan": (
|
|
971
|
+
"Plan mode: agent reads files and proposes a plan — no writes, no bash.\n"
|
|
972
|
+
"Review the plan, then type /plan off to let the agent execute it.\n"
|
|
973
|
+
"/plan on — enable (read-only)\n"
|
|
974
|
+
"/plan off — disable (agent can now write and run commands)"
|
|
975
|
+
),
|
|
976
|
+
"compact": (
|
|
977
|
+
"Compresses old conversation history into a dense summary to free up context tokens.\n"
|
|
978
|
+
"Use when: the agent seems to lose track, or the session has been running for a long time.\n"
|
|
979
|
+
"Sylithe Code also auto-compacts when history exceeds ~50,000 tokens."
|
|
980
|
+
),
|
|
981
|
+
"yolo": (
|
|
982
|
+
"Toggles auto-approve mode — skips all permission prompts for bash, write, and edit.\n"
|
|
983
|
+
"Use when you trust the agent and want it to run without interruption.\n"
|
|
984
|
+
"Green = ON (no prompts). Default = OFF (prompts on bash commands)."
|
|
985
|
+
),
|
|
986
|
+
"model": (
|
|
987
|
+
"Switch the model. Sylithe Code also auto-selects based on task complexity.\n"
|
|
988
|
+
"/model sylithe-flash — Sylithe Code Flash, fast, cheap, great for most tasks (~$0.27/1M in)\n"
|
|
989
|
+
"/model sylithe-pro — Sylithe Code Pro, deeper reasoning, debugging / architecture (~$0.55/1M in)\n"
|
|
990
|
+
"Auto-select upgrades Flash → Pro for complex tasks automatically."
|
|
991
|
+
),
|
|
992
|
+
"coordinator": (
|
|
993
|
+
"Enters coordinator mode — Sylithe Code becomes an orchestrator that spawns\n"
|
|
994
|
+
"parallel specialist workers instead of doing the work itself.\n\n"
|
|
995
|
+
"Workflow:\n"
|
|
996
|
+
" 1. You send a task (e.g. 'fix the auth bug and add tests')\n"
|
|
997
|
+
" 2. Coordinator spawns parallel explore workers to map the code\n"
|
|
998
|
+
" 3. When workers finish, coordinator receives <task-notification> messages\n"
|
|
999
|
+
" 4. Coordinator synthesizes findings → spawns coder workers with precise specs\n"
|
|
1000
|
+
" 5. After implementation → spawns a fresh verifier to prove it works\n\n"
|
|
1001
|
+
"Worker types:\n"
|
|
1002
|
+
" explore — maps codebase (read-only, fast, can run 5 in parallel)\n"
|
|
1003
|
+
" researcher — fetches live docs, API specs, real examples from the web\n"
|
|
1004
|
+
" coder — implements code, runs tests, commits\n"
|
|
1005
|
+
" verifier — audits code, runs tests, finds security holes\n"
|
|
1006
|
+
" general — all tools, use when task spans multiple roles\n\n"
|
|
1007
|
+
"Type /workers to see all active workers and their status.\n"
|
|
1008
|
+
"Type /exit-coordinator to return to normal mode."
|
|
1009
|
+
),
|
|
1010
|
+
"memory": (
|
|
1011
|
+
"Persistent memory survives across sessions — the agent reads it at the start of every task.\n"
|
|
1012
|
+
"/memory list — see everything saved\n"
|
|
1013
|
+
"/memory add <text> — save a fact (paths, decisions, preferences, metrics)\n"
|
|
1014
|
+
"/memory del <id> — remove a specific memory by its ID"
|
|
1015
|
+
),
|
|
1016
|
+
}
|
|
1017
|
+
if selected in detailed:
|
|
1018
|
+
console.print(f"[dim]{detailed[selected]}[/dim]\n")
|
|
1019
|
+
|
|
1020
|
+
# Ask if they want to run it
|
|
1021
|
+
try:
|
|
1022
|
+
run_it = input(f" Run /{selected} now? [y/N] ").strip().lower()
|
|
1023
|
+
except (EOFError, KeyboardInterrupt):
|
|
1024
|
+
run_it = ""
|
|
1025
|
+
|
|
1026
|
+
if run_it != "y":
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
# Commands that still need a CLI argument before running.
|
|
1030
|
+
# newsite / newapp are NOT here — their built-in Q&A collects everything.
|
|
1031
|
+
_arg_hints = {
|
|
1032
|
+
"skill": ("Skill name", "Example: razorpay"),
|
|
1033
|
+
"review": ("Path to review (Enter for current dir)", ""),
|
|
1034
|
+
"explain": ("File to explain", "Example: src/auth.py"),
|
|
1035
|
+
"refactor":("File to refactor", "Example: src/utils.py"),
|
|
1036
|
+
"test": ("File to test", "Example: src/payment.py"),
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if selected in _arg_hints:
|
|
1040
|
+
hint, example = _arg_hints[selected]
|
|
1041
|
+
if example:
|
|
1042
|
+
console.print(f" [dim]{example}[/dim]")
|
|
1043
|
+
try:
|
|
1044
|
+
extra = input(f" /{selected} {hint}: ").strip()
|
|
1045
|
+
except (EOFError, KeyboardInterrupt):
|
|
1046
|
+
extra = ""
|
|
1047
|
+
|
|
1048
|
+
# Commands where blank is valid (review defaults to cwd)
|
|
1049
|
+
if not extra and selected not in ("review",):
|
|
1050
|
+
show_error(f"No name provided — run /{selected} <name> manually.")
|
|
1051
|
+
return
|
|
1052
|
+
|
|
1053
|
+
handle_slash_command(f"/{selected} {extra}".strip(), session)
|
|
1054
|
+
else:
|
|
1055
|
+
handle_slash_command(f"/{selected}", session)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _print_full_help():
|
|
1059
|
+
"""Print the traditional full help listing."""
|
|
1060
|
+
console.print()
|
|
1061
|
+
for group, names in _HELP_GROUPS.items():
|
|
1062
|
+
console.print(f"[bold]{group}[/bold]")
|
|
1063
|
+
for name in names:
|
|
1064
|
+
if name in COMMANDS:
|
|
1065
|
+
console.print(f" [green]/{name:<14}[/green] {COMMANDS[name]['description']}")
|
|
1066
|
+
console.print()
|
|
1067
|
+
|
|
1068
|
+
console.print("[bold]CLI Commands[/bold]")
|
|
1069
|
+
for cmd, desc in _CLI_CMDS:
|
|
1070
|
+
console.print(f" [cyan]{cmd:<44}[/cyan] [dim]{desc}[/dim]")
|
|
1071
|
+
console.print()
|
|
1072
|
+
console.print("[dim]Tip: type /help for interactive mode, or /skills to browse skills with dropdown.[/dim]\n")
|