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/main.py
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sylithe Code CLI — main entry point.
|
|
3
|
+
Inspired by Claude Code's CLI architecture.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# Force UTF-8 on Windows before anything else loads.
|
|
9
|
+
# Without this, emoji and Devanagari in source files crash the diff printer
|
|
10
|
+
# with UnicodeEncodeError: 'charmap' codec can't encode character.
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except AttributeError:
|
|
16
|
+
pass # Python < 3.7 — best-effort
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import readline # noqa: F401 — enables arrow key history on Unix/Mac
|
|
20
|
+
except ImportError:
|
|
21
|
+
readline = None # not available on Windows — use pyreadline3 if needed
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
from rich.prompt import Prompt
|
|
26
|
+
from rich.rule import Rule
|
|
27
|
+
|
|
28
|
+
from .ui import (
|
|
29
|
+
console, show_banner, show_error, show_success,
|
|
30
|
+
show_info, show_warning, show_separator,
|
|
31
|
+
)
|
|
32
|
+
from .agent import run_agent, _build_system
|
|
33
|
+
from .config import load_config, save_config, get_api_key
|
|
34
|
+
from .commands import handle_slash_command
|
|
35
|
+
|
|
36
|
+
# ── Main Group ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
@click.group(invoke_without_command=True)
|
|
39
|
+
@click.pass_context
|
|
40
|
+
@click.argument("task", required=False, default=None)
|
|
41
|
+
@click.option("--auto-approve", "-y", is_flag=True, help="Auto-approve all bash commands (yolo mode)")
|
|
42
|
+
@click.option("--model", "-m", default=None, help="Override model for this run")
|
|
43
|
+
@click.option("--print", "-p", "print_mode", is_flag=True,
|
|
44
|
+
help="Non-interactive: run task once and print output, then exit. "
|
|
45
|
+
"Reads from stdin if piped: cat file.py | bharatcode -p 'explain this'")
|
|
46
|
+
def cli(ctx, task, auto_approve, model, print_mode):
|
|
47
|
+
"""Sylithe Code — AI Coding Agent for Indian Developers
|
|
48
|
+
|
|
49
|
+
\b
|
|
50
|
+
Run without arguments to enter interactive mode.
|
|
51
|
+
Pass a TASK with --print for non-interactive / piped use:
|
|
52
|
+
bharatcode --print "explain this file" < src/app.py
|
|
53
|
+
cat error.log | bharatcode -p "fix this"
|
|
54
|
+
Or use a subcommand: fix, build, review, audit, test, explain...
|
|
55
|
+
"""
|
|
56
|
+
ctx.ensure_object(dict)
|
|
57
|
+
ctx.obj["auto_approve"] = auto_approve
|
|
58
|
+
ctx.obj["model"] = model
|
|
59
|
+
|
|
60
|
+
if ctx.invoked_subcommand is not None:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# ── Non-interactive / pipe mode ───────────────────────────────────────────
|
|
64
|
+
# Triggered by --print flag OR when stdin is a pipe/file (not a terminal).
|
|
65
|
+
stdin_is_pipe = not sys.stdin.isatty()
|
|
66
|
+
if print_mode or stdin_is_pipe:
|
|
67
|
+
_print_mode(task=task, auto_approve=auto_approve, model=model)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# ── Interactive REPL ──────────────────────────────────────────────────────
|
|
71
|
+
interactive_mode(auto_approve=auto_approve)
|
|
72
|
+
|
|
73
|
+
# ── Config ────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
@cli.command()
|
|
76
|
+
@click.option("--key", help="Set DeepSeek API key")
|
|
77
|
+
@click.option("--show", is_flag=True, help="Show current config")
|
|
78
|
+
@click.option("--model", help="Set model (deepseek-v4-flash or deepseek-v4-pro)")
|
|
79
|
+
@click.option("--auto-approve", is_flag=True, default=None, help="Enable auto-approve by default")
|
|
80
|
+
def config(key, show, model, auto_approve):
|
|
81
|
+
"""Configure Sylithe Code settings."""
|
|
82
|
+
cfg = load_config()
|
|
83
|
+
changed = False
|
|
84
|
+
|
|
85
|
+
if key:
|
|
86
|
+
cfg["api_key"] = key
|
|
87
|
+
changed = True
|
|
88
|
+
show_success("API key saved.")
|
|
89
|
+
|
|
90
|
+
if model:
|
|
91
|
+
cfg["model"] = model
|
|
92
|
+
changed = True
|
|
93
|
+
show_success(f"Model set to {model}.")
|
|
94
|
+
|
|
95
|
+
if auto_approve is not None:
|
|
96
|
+
cfg["auto_approve"] = auto_approve
|
|
97
|
+
changed = True
|
|
98
|
+
show_success(f"Auto-approve {'enabled' if auto_approve else 'disabled'}.")
|
|
99
|
+
|
|
100
|
+
if changed:
|
|
101
|
+
save_config(cfg)
|
|
102
|
+
|
|
103
|
+
if show or not changed:
|
|
104
|
+
console.print("\n[bold]Config[/bold] [dim]~/.bharatcode/config.json[/dim]\n")
|
|
105
|
+
for k, v in cfg.items():
|
|
106
|
+
if k == "api_key" and v:
|
|
107
|
+
v = v[:8] + "..." + v[-4:]
|
|
108
|
+
console.print(f" [dim]{k}[/dim] [cyan]{v}[/cyan]")
|
|
109
|
+
console.print()
|
|
110
|
+
|
|
111
|
+
# ── Fix ───────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
@cli.command()
|
|
114
|
+
@click.argument("description")
|
|
115
|
+
@click.option("--file", "-f", help="File where bug is")
|
|
116
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
117
|
+
def fix(description, file, auto_approve):
|
|
118
|
+
"""Autonomously fix a bug.
|
|
119
|
+
|
|
120
|
+
\b
|
|
121
|
+
Examples:
|
|
122
|
+
bharatcode fix "login fails with uppercase email"
|
|
123
|
+
bharatcode fix "payment 500 error" -f src/payment.py
|
|
124
|
+
"""
|
|
125
|
+
show_banner()
|
|
126
|
+
console.print(f"\n[bold red]Bug:[/bold red] {description}\n")
|
|
127
|
+
task = f"""Fix this bug: {description}
|
|
128
|
+
{"Focus on file: " + file if file else ""}
|
|
129
|
+
|
|
130
|
+
Steps:
|
|
131
|
+
1. Search for relevant code files
|
|
132
|
+
2. Read all related files carefully
|
|
133
|
+
3. Find the root cause
|
|
134
|
+
4. Fix with minimal changes
|
|
135
|
+
5. Run tests if they exist
|
|
136
|
+
6. Give a summary of what you fixed and why"""
|
|
137
|
+
run_agent(task, os.getcwd(), auto_approve=auto_approve)
|
|
138
|
+
|
|
139
|
+
# ── Build ─────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
@cli.command()
|
|
142
|
+
@click.argument("feature")
|
|
143
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
144
|
+
def build(feature):
|
|
145
|
+
"""Autonomously build a new feature.
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
Examples:
|
|
149
|
+
bharatcode build "add Razorpay payment integration"
|
|
150
|
+
bharatcode build "add JWT authentication with refresh tokens"
|
|
151
|
+
"""
|
|
152
|
+
show_banner()
|
|
153
|
+
console.print(f"\n[bold green]Building:[/bold green] {feature}\n")
|
|
154
|
+
task = f"""Build this feature: {feature}
|
|
155
|
+
|
|
156
|
+
Steps:
|
|
157
|
+
1. Read project structure (package.json / requirements.txt / pom.xml / build.gradle)
|
|
158
|
+
2. Understand existing code patterns and architecture
|
|
159
|
+
3. Plan the implementation
|
|
160
|
+
4. Implement following existing code style
|
|
161
|
+
5. Write basic tests
|
|
162
|
+
6. Run tests and fix failures
|
|
163
|
+
7. Summarize everything created and changed"""
|
|
164
|
+
run_agent(task, os.getcwd())
|
|
165
|
+
|
|
166
|
+
# ── Review ────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
@cli.command()
|
|
169
|
+
@click.argument("target", default=".")
|
|
170
|
+
def review(target):
|
|
171
|
+
"""Review code for bugs, security, best practices.
|
|
172
|
+
|
|
173
|
+
\b
|
|
174
|
+
Examples:
|
|
175
|
+
bharatcode review
|
|
176
|
+
bharatcode review src/auth.py
|
|
177
|
+
bharatcode review src/
|
|
178
|
+
"""
|
|
179
|
+
show_banner()
|
|
180
|
+
console.print(f"\n[bold blue]Reviewing:[/bold blue] {target}\n")
|
|
181
|
+
task = f"""Do a thorough code review of: {target}
|
|
182
|
+
|
|
183
|
+
Check for:
|
|
184
|
+
1. Bugs and logic errors
|
|
185
|
+
2. Security vulnerabilities (SQL injection, XSS, auth bypass, insecure deserialization)
|
|
186
|
+
3. Indian regulatory issues (DPDP Act, RBI, GST logic errors) if applicable
|
|
187
|
+
4. Performance bottlenecks
|
|
188
|
+
5. Code quality (naming, duplication, complexity)
|
|
189
|
+
|
|
190
|
+
Output a structured report:
|
|
191
|
+
## Critical Issues
|
|
192
|
+
## Warnings
|
|
193
|
+
## Suggestions
|
|
194
|
+
## Positives"""
|
|
195
|
+
run_agent(task, os.getcwd())
|
|
196
|
+
|
|
197
|
+
# ── Test ──────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
@cli.command()
|
|
200
|
+
@click.argument("file")
|
|
201
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
202
|
+
def test(file, auto_approve):
|
|
203
|
+
"""Write and run tests for a file.
|
|
204
|
+
|
|
205
|
+
\b
|
|
206
|
+
Example:
|
|
207
|
+
bharatcode test src/auth.py
|
|
208
|
+
bharatcode test src/payment/razorpay.py
|
|
209
|
+
"""
|
|
210
|
+
show_banner()
|
|
211
|
+
console.print(f"\n[bold yellow]Writing tests for:[/bold yellow] {file}\n")
|
|
212
|
+
task = f"""Write comprehensive tests for: {file}
|
|
213
|
+
|
|
214
|
+
Steps:
|
|
215
|
+
1. Read {file} fully
|
|
216
|
+
2. Find existing test files to match the testing framework and patterns
|
|
217
|
+
3. Write tests covering: happy path, edge cases, error cases, boundary values
|
|
218
|
+
4. Save to the appropriate test file
|
|
219
|
+
5. Run the tests
|
|
220
|
+
6. Fix any failures"""
|
|
221
|
+
run_agent(task, os.getcwd(), auto_approve=auto_approve)
|
|
222
|
+
|
|
223
|
+
# ── Audit ─────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
@cli.command()
|
|
226
|
+
def audit():
|
|
227
|
+
"""Run Indian compliance audit (DPDP Act 2023, RBI, GST, Aadhaar/PAN)."""
|
|
228
|
+
show_banner()
|
|
229
|
+
console.print("\n[bold magenta]Indian Compliance Audit[/bold magenta]\n")
|
|
230
|
+
task = """Audit this project for Indian regulatory compliance.
|
|
231
|
+
|
|
232
|
+
Check:
|
|
233
|
+
1. **DPDP Act 2023**: user consent mechanism, data encryption at rest+transit,
|
|
234
|
+
right-to-deletion mechanism, privacy policy present, no unnecessary data collection
|
|
235
|
+
2. **Payments (if present)**: RBI PPI guidelines, no raw card data stored,
|
|
236
|
+
HTTPS enforced, no storing CVV
|
|
237
|
+
3. **GST (if present)**: correct CGST/SGST/IGST split logic, correct tax rates (5/12/18/28%),
|
|
238
|
+
HSN/SAC codes present
|
|
239
|
+
4. **Aadhaar/PAN (if present)**: UIDAI guidelines, Aadhaar not stored in plain text,
|
|
240
|
+
masked display (last 4 digits only)
|
|
241
|
+
5. **General security**: hardcoded secrets, SQL injection, XSS, insecure dependencies
|
|
242
|
+
|
|
243
|
+
Produce a compliance report with severity ratings:
|
|
244
|
+
## CRITICAL (immediate action required)
|
|
245
|
+
## HIGH (fix before production)
|
|
246
|
+
## MEDIUM (should fix)
|
|
247
|
+
## LOW (nice to fix)
|
|
248
|
+
## COMPLIANT (things done right)"""
|
|
249
|
+
run_agent(task, os.getcwd())
|
|
250
|
+
|
|
251
|
+
# ── Ask ───────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
@cli.command()
|
|
254
|
+
@click.argument("question")
|
|
255
|
+
@click.option("--file", "-f", help="Specific file to ask about")
|
|
256
|
+
def ask(question, file):
|
|
257
|
+
"""Ask a question about your codebase.
|
|
258
|
+
|
|
259
|
+
\b
|
|
260
|
+
Examples:
|
|
261
|
+
bharatcode ask "how does auth work?"
|
|
262
|
+
bharatcode ask "what does this do" -f src/payment.py
|
|
263
|
+
"""
|
|
264
|
+
show_banner()
|
|
265
|
+
task = f"Read '{file}' then answer: {question}" if file else question
|
|
266
|
+
run_agent(task, os.getcwd())
|
|
267
|
+
|
|
268
|
+
# ── Explain ───────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
@cli.command()
|
|
271
|
+
@click.argument("file")
|
|
272
|
+
def explain(file):
|
|
273
|
+
"""Explain what a file does in plain English.
|
|
274
|
+
|
|
275
|
+
\b
|
|
276
|
+
Example:
|
|
277
|
+
bharatcode explain src/payment/razorpay.py
|
|
278
|
+
"""
|
|
279
|
+
show_banner()
|
|
280
|
+
task = f"""Read and explain: {file}
|
|
281
|
+
|
|
282
|
+
Provide:
|
|
283
|
+
1. What this file does (1-2 sentence overview)
|
|
284
|
+
2. Key functions/classes and their purpose
|
|
285
|
+
3. How data flows through this file
|
|
286
|
+
4. How it connects to the rest of the project
|
|
287
|
+
5. Any potential issues or improvements"""
|
|
288
|
+
run_agent(task, os.getcwd())
|
|
289
|
+
|
|
290
|
+
# ── Refactor ──────────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
@cli.command()
|
|
293
|
+
@click.argument("file")
|
|
294
|
+
@click.option("--reason", "-r", help="Why to refactor")
|
|
295
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
296
|
+
def refactor(file, reason, auto_approve):
|
|
297
|
+
"""Refactor code for better quality.
|
|
298
|
+
|
|
299
|
+
\b
|
|
300
|
+
Example:
|
|
301
|
+
bharatcode refactor src/utils.py
|
|
302
|
+
bharatcode refactor src/auth.py -r "split into smaller functions"
|
|
303
|
+
"""
|
|
304
|
+
show_banner()
|
|
305
|
+
task = f"""Refactor: {file}
|
|
306
|
+
{"Reason: " + reason if reason else "General quality improvement"}
|
|
307
|
+
|
|
308
|
+
Goals:
|
|
309
|
+
- Readability and clarity
|
|
310
|
+
- Single responsibility principle
|
|
311
|
+
- DRY (Don't Repeat Yourself)
|
|
312
|
+
- Better naming
|
|
313
|
+
- Reduce complexity
|
|
314
|
+
|
|
315
|
+
IMPORTANT: Keep identical functionality. Run tests after refactoring."""
|
|
316
|
+
run_agent(task, os.getcwd(), auto_approve=auto_approve)
|
|
317
|
+
|
|
318
|
+
# ── New: website / app scaffolding ───────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
@cli.group()
|
|
321
|
+
def new():
|
|
322
|
+
"""Scaffold a new website or app from scratch.
|
|
323
|
+
|
|
324
|
+
\b
|
|
325
|
+
Examples:
|
|
326
|
+
bharatcode new website "My Portfolio"
|
|
327
|
+
bharatcode new website "Startup Landing"
|
|
328
|
+
bharatcode new app "Task Manager" --type flask
|
|
329
|
+
bharatcode new app "Dashboard" --type react
|
|
330
|
+
bharatcode new app "SaaS Platform" --type fullstack
|
|
331
|
+
"""
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@new.command("website")
|
|
336
|
+
@click.argument("name")
|
|
337
|
+
@click.argument("description", default="")
|
|
338
|
+
@click.option("--dir", "-d", "output_dir", default=None,
|
|
339
|
+
help="Output directory (default: ./<name>)")
|
|
340
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
341
|
+
def new_website(name, description, output_dir, auto_approve):
|
|
342
|
+
"""Create a complete website from scratch.
|
|
343
|
+
|
|
344
|
+
\b
|
|
345
|
+
Examples:
|
|
346
|
+
bharatcode new website "Chhelu Portfolio"
|
|
347
|
+
bharatcode new website "GreenTech India" "solar energy startup landing page"
|
|
348
|
+
bharatcode new website "Bistro Kitchen" "restaurant website with menu and reservations"
|
|
349
|
+
"""
|
|
350
|
+
from .skills import get_skill
|
|
351
|
+
show_banner()
|
|
352
|
+
|
|
353
|
+
dest_path = os.path.abspath(output_dir or name.lower().replace(" ", "-"))
|
|
354
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
355
|
+
|
|
356
|
+
console.print(f"\n[bold green]Building website:[/bold green] {name}")
|
|
357
|
+
if description:
|
|
358
|
+
console.print(f"[dim]{description}[/dim]")
|
|
359
|
+
console.print(f"[dim]Output: {dest_path}[/dim]\n")
|
|
360
|
+
|
|
361
|
+
skill_prompt = get_skill("newsite")
|
|
362
|
+
task = f"""{skill_prompt}
|
|
363
|
+
|
|
364
|
+
PROJECT DETAILS:
|
|
365
|
+
- Name: {name}
|
|
366
|
+
- Description: {description or "A modern, beautiful website"}
|
|
367
|
+
- Output folder (absolute): {dest_path}
|
|
368
|
+
|
|
369
|
+
CRITICAL PATH RULE: Every file MUST be written inside {dest_path}.
|
|
370
|
+
Use the full absolute path in every <<<FILE:>>> marker and every write_file call.
|
|
371
|
+
Correct: <<<FILE:{dest_path}/index.html>>>
|
|
372
|
+
Correct: <<<FILE:{dest_path}/css/variables.css>>>
|
|
373
|
+
WRONG: <<<FILE:index.html>>> ← relative paths go to wrong place
|
|
374
|
+
|
|
375
|
+
Start by thinking about what kind of website this is and what it needs.
|
|
376
|
+
Then design the file structure, then write every file completely."""
|
|
377
|
+
|
|
378
|
+
run_agent(task, project_path=dest_path, auto_approve=auto_approve)
|
|
379
|
+
|
|
380
|
+
# Safety net: git init + initial commit so every later change is revertable
|
|
381
|
+
from .agent import _git_checkpoint
|
|
382
|
+
_h = _git_checkpoint(dest_path, f"initial scaffold of {name}", init=True)
|
|
383
|
+
if _h:
|
|
384
|
+
console.print(f"\n[dim]📌 Initial git commit [cyan]{_h}[/cyan] — use git diff / git checkout to review or revert changes[/dim]")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@new.command("app")
|
|
388
|
+
@click.argument("name")
|
|
389
|
+
@click.argument("description", default="")
|
|
390
|
+
@click.option("--type", "-t", "app_type",
|
|
391
|
+
type=click.Choice(["flask", "react", "fullstack", "node", "nextjs"], case_sensitive=False),
|
|
392
|
+
default=None,
|
|
393
|
+
help="App type: flask | react | fullstack | node | nextjs")
|
|
394
|
+
@click.option("--dir", "-d", "output_dir", default=None,
|
|
395
|
+
help="Output directory (default: ./<name>)")
|
|
396
|
+
@click.option("--auto-approve", "-y", is_flag=True)
|
|
397
|
+
def new_app(name, description, app_type, output_dir, auto_approve):
|
|
398
|
+
"""Create a complete application from scratch.
|
|
399
|
+
|
|
400
|
+
\b
|
|
401
|
+
Examples:
|
|
402
|
+
bharatcode new app "TaskFlow" "kanban task manager with teams"
|
|
403
|
+
bharatcode new app "ShopIndia" "e-commerce with Razorpay" --type flask
|
|
404
|
+
bharatcode new app "AnalyticsPro" "data dashboard" --type react
|
|
405
|
+
bharatcode new app "StartupOS" "SaaS platform" --type fullstack
|
|
406
|
+
"""
|
|
407
|
+
from .skills import get_skill
|
|
408
|
+
show_banner()
|
|
409
|
+
|
|
410
|
+
dest_path = os.path.abspath(output_dir or name.lower().replace(" ", "-"))
|
|
411
|
+
type_display = app_type or "auto-detect from description"
|
|
412
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
413
|
+
|
|
414
|
+
console.print(f"\n[bold green]Building app:[/bold green] {name}")
|
|
415
|
+
if description:
|
|
416
|
+
console.print(f"[dim]{description}[/dim]")
|
|
417
|
+
console.print(f"[dim]Type: {type_display} | Output: {dest_path}[/dim]\n")
|
|
418
|
+
|
|
419
|
+
skill_prompt = get_skill("newapp")
|
|
420
|
+
|
|
421
|
+
type_hint = ""
|
|
422
|
+
if app_type:
|
|
423
|
+
stack_hints = {
|
|
424
|
+
"flask": "Python + Flask (REST API + Jinja2 templates, SQLAlchemy, Flask-Login)",
|
|
425
|
+
"react": "React 18 + Vite + React Router 6 (pure frontend SPA)",
|
|
426
|
+
"fullstack": "Flask REST API (port 5000) + React Vite frontend (port 5173), flask-cors configured, Vite proxy set up, .env files for both sides",
|
|
427
|
+
"node": "Node.js + Express (REST API, JWT auth, Mongoose/Prisma)",
|
|
428
|
+
"nextjs": "Next.js 14 App Router (SSR/SSG, server components, API routes)",
|
|
429
|
+
}
|
|
430
|
+
type_hint = f"\nTECH STACK: {stack_hints[app_type.lower()]}"
|
|
431
|
+
|
|
432
|
+
task = f"""{skill_prompt}
|
|
433
|
+
|
|
434
|
+
PROJECT DETAILS:
|
|
435
|
+
- Name: {name}
|
|
436
|
+
- Description: {description or "A modern web application"}
|
|
437
|
+
- Output folder (absolute): {dest_path}{type_hint}
|
|
438
|
+
|
|
439
|
+
CRITICAL PATH RULE: Every file MUST be written inside {dest_path}.
|
|
440
|
+
Use the full absolute path in every <<<FILE:>>> marker and every write_file call.
|
|
441
|
+
Correct: <<<FILE:{dest_path}/app.py>>>
|
|
442
|
+
Correct: <<<FILE:{dest_path}/frontend/src/App.jsx>>>
|
|
443
|
+
WRONG: <<<FILE:app.py>>> ← relative paths go to wrong place
|
|
444
|
+
|
|
445
|
+
{"If no stack is specified, choose the best one for this specific project based on the description." if not app_type else ""}
|
|
446
|
+
|
|
447
|
+
Start by analyzing what the app does, design the data models and architecture, then write every file completely."""
|
|
448
|
+
|
|
449
|
+
run_agent(task, project_path=dest_path, auto_approve=auto_approve)
|
|
450
|
+
|
|
451
|
+
# Safety net: git init + initial commit so every later change is revertable
|
|
452
|
+
from .agent import _git_checkpoint
|
|
453
|
+
_h = _git_checkpoint(dest_path, f"initial scaffold of {name}", init=True)
|
|
454
|
+
if _h:
|
|
455
|
+
console.print(f"\n[dim]📌 Initial git commit [cyan]{_h}[/cyan] — use git diff / git checkout to review or revert changes[/dim]")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ── Init ──────────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
@cli.command()
|
|
461
|
+
def init():
|
|
462
|
+
"""Initialize Sylithe Code in your project (creates BHARATCODE.md)."""
|
|
463
|
+
p = Path("BHARATCODE.md")
|
|
464
|
+
if p.exists():
|
|
465
|
+
show_warning("BHARATCODE.md already exists.")
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
name = Prompt.ask("[bold]Project name[/bold]")
|
|
469
|
+
stack = Prompt.ask("[bold]Tech stack[/bold]", default="Python/Flask")
|
|
470
|
+
|
|
471
|
+
p.write_text(f"""# {name} — Sylithe Code Config
|
|
472
|
+
|
|
473
|
+
## Tech Stack
|
|
474
|
+
{stack}
|
|
475
|
+
|
|
476
|
+
## Project Description
|
|
477
|
+
<!-- Describe your project here -->
|
|
478
|
+
|
|
479
|
+
## Coding Standards
|
|
480
|
+
- Follow PEP8 for Python / Google Style for Java / StandardJS for JS
|
|
481
|
+
- Write tests for all new features
|
|
482
|
+
- Use type hints (Python) or type annotations (TypeScript)
|
|
483
|
+
|
|
484
|
+
## Indian Integrations
|
|
485
|
+
<!-- List any Indian APIs: Razorpay, UIDAI, GST APIs, DigiLocker, etc. -->
|
|
486
|
+
|
|
487
|
+
## Run Commands
|
|
488
|
+
- Tests: `pytest` (or `npm test` / `mvn test`)
|
|
489
|
+
- Start: `python app.py` (or `npm start`)
|
|
490
|
+
- Lint: `flake8` (or `eslint .`)
|
|
491
|
+
|
|
492
|
+
## Notes for Sylithe Code
|
|
493
|
+
<!-- Special instructions for the AI agent -->
|
|
494
|
+
- Never commit .env files
|
|
495
|
+
- Always run tests after fixes
|
|
496
|
+
""", encoding="utf-8")
|
|
497
|
+
|
|
498
|
+
show_success(f"Created BHARATCODE.md for {name}")
|
|
499
|
+
show_info("Edit BHARATCODE.md to add project-specific instructions.")
|
|
500
|
+
|
|
501
|
+
# ── Paste-aware input ─────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
def _print_mode(task: str | None, auto_approve: bool, model: str | None):
|
|
504
|
+
"""Non-interactive mode: run once and print the final answer, then exit.
|
|
505
|
+
|
|
506
|
+
Usage:
|
|
507
|
+
bharatcode --print "explain this"
|
|
508
|
+
bharatcode -p "fix this bug" < src/app.py
|
|
509
|
+
cat error.log | bharatcode -p "what is wrong here"
|
|
510
|
+
"""
|
|
511
|
+
from .config import save_config, load_config, MODEL_API_MAP, MODEL_ALIASES
|
|
512
|
+
|
|
513
|
+
# ── Override model for this run if -m was passed ──────────────────────────
|
|
514
|
+
if model:
|
|
515
|
+
cfg = load_config()
|
|
516
|
+
api_model = MODEL_API_MAP.get(model) or MODEL_ALIASES.get(model) or model
|
|
517
|
+
cfg["model"] = api_model
|
|
518
|
+
save_config(cfg)
|
|
519
|
+
|
|
520
|
+
# ── Build the task string ─────────────────────────────────────────────────
|
|
521
|
+
stdin_content = ""
|
|
522
|
+
if not sys.stdin.isatty():
|
|
523
|
+
stdin_content = sys.stdin.read()
|
|
524
|
+
|
|
525
|
+
if not task and not stdin_content:
|
|
526
|
+
click.echo(
|
|
527
|
+
"Usage: bharatcode --print \"your task\"\n"
|
|
528
|
+
" cat file.py | bharatcode -p \"explain this\"\n"
|
|
529
|
+
" bharatcode -p \"fix the bug\" < src/app.py",
|
|
530
|
+
err=True,
|
|
531
|
+
)
|
|
532
|
+
sys.exit(1)
|
|
533
|
+
|
|
534
|
+
if stdin_content and task:
|
|
535
|
+
full_task = f"{task}\n\n--- stdin ---\n{stdin_content}"
|
|
536
|
+
elif stdin_content:
|
|
537
|
+
full_task = stdin_content
|
|
538
|
+
else:
|
|
539
|
+
full_task = task
|
|
540
|
+
|
|
541
|
+
# ── Run agent once, capture output ────────────────────────────────────────
|
|
542
|
+
cwd = os.getcwd()
|
|
543
|
+
system_content = _build_system(cwd)
|
|
544
|
+
|
|
545
|
+
output = run_agent(
|
|
546
|
+
full_task,
|
|
547
|
+
project_path=cwd,
|
|
548
|
+
auto_approve=auto_approve,
|
|
549
|
+
system_content=system_content,
|
|
550
|
+
silent=True,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if output:
|
|
554
|
+
print(output)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _read_input() -> str:
|
|
558
|
+
"""
|
|
559
|
+
Read one user turn with paste detection.
|
|
560
|
+
|
|
561
|
+
When the user pastes multi-line text the terminal buffers all lines but
|
|
562
|
+
only delivers them one-at-a-time through input(). We detect this by
|
|
563
|
+
checking whether more data is waiting in the input buffer immediately
|
|
564
|
+
after reading the first line:
|
|
565
|
+
|
|
566
|
+
Windows — msvcrt.kbhit() returns True while console input is buffered.
|
|
567
|
+
Unix — select() with timeout=0 tells us stdin has more bytes ready.
|
|
568
|
+
|
|
569
|
+
Normal typing never triggers this because the buffer is empty between
|
|
570
|
+
keystrokes. A pasted block arrives all at once, so every subsequent line
|
|
571
|
+
is found waiting and gets merged.
|
|
572
|
+
"""
|
|
573
|
+
# Print styled prompt, then read first line via plain input() so we can
|
|
574
|
+
# inspect the buffer ourselves afterwards.
|
|
575
|
+
console.print("[bold green]>[/bold green] ", end="", highlight=False)
|
|
576
|
+
try:
|
|
577
|
+
first = input()
|
|
578
|
+
except (EOFError, KeyboardInterrupt):
|
|
579
|
+
raise
|
|
580
|
+
|
|
581
|
+
lines = [first]
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
if sys.platform == "win32":
|
|
585
|
+
import msvcrt, time
|
|
586
|
+
time.sleep(0.030) # let the paste buffer fill
|
|
587
|
+
while msvcrt.kbhit():
|
|
588
|
+
try:
|
|
589
|
+
lines.append(input())
|
|
590
|
+
except EOFError:
|
|
591
|
+
break
|
|
592
|
+
time.sleep(0.010)
|
|
593
|
+
else:
|
|
594
|
+
import select, time
|
|
595
|
+
time.sleep(0.030)
|
|
596
|
+
while select.select([sys.stdin], [], [], 0)[0]:
|
|
597
|
+
try:
|
|
598
|
+
lines.append(sys.stdin.readline().rstrip("\n"))
|
|
599
|
+
except EOFError:
|
|
600
|
+
break
|
|
601
|
+
except Exception:
|
|
602
|
+
pass # any failure → return what we have
|
|
603
|
+
|
|
604
|
+
return "\n".join(lines)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ── Coordinator Notification Loop ─────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
def _coordinator_notification_loop(session, cwd, auto_approve, history):
|
|
610
|
+
"""
|
|
611
|
+
After each coordinator turn, workers may still be running in background threads.
|
|
612
|
+
This loop polls the WorkerPool's notification_queue and re-enters run_agent()
|
|
613
|
+
(with empty task = no new user message) whenever a worker completes.
|
|
614
|
+
|
|
615
|
+
Exits only when: no workers are running AND notification queue is empty.
|
|
616
|
+
Ctrl+C to interrupt early.
|
|
617
|
+
"""
|
|
618
|
+
import time
|
|
619
|
+
from .coordinator import RUNNING as W_RUNNING
|
|
620
|
+
|
|
621
|
+
pool = session.get("worker_pool")
|
|
622
|
+
if not pool:
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
console.print("[dim] ⏳ Workers running — will synthesize when they report back. Ctrl+C to interrupt.[/dim]")
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
accumulated = 0 # total notifications waiting in history
|
|
629
|
+
|
|
630
|
+
while True:
|
|
631
|
+
time.sleep(0.4)
|
|
632
|
+
|
|
633
|
+
with pool._lock:
|
|
634
|
+
running_count = sum(
|
|
635
|
+
1 for w in pool._workers.values()
|
|
636
|
+
if w.status == W_RUNNING
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Drain any new notifications — accumulate silently, don't synthesize yet
|
|
640
|
+
notifications = pool.drain_notifications()
|
|
641
|
+
if notifications:
|
|
642
|
+
for n in notifications:
|
|
643
|
+
history.append(n)
|
|
644
|
+
accumulated += len(notifications)
|
|
645
|
+
console.print(
|
|
646
|
+
f" [dim]📬 {len(notifications)} worker(s) reported — "
|
|
647
|
+
f"{running_count} still running...[/dim]"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# Still have workers running — keep accumulating
|
|
651
|
+
if running_count > 0:
|
|
652
|
+
continue
|
|
653
|
+
|
|
654
|
+
# All workers done — do one final drain to catch any last-second results
|
|
655
|
+
final = pool.drain_notifications()
|
|
656
|
+
for n in final:
|
|
657
|
+
history.append(n)
|
|
658
|
+
accumulated += 1
|
|
659
|
+
|
|
660
|
+
# No accumulated results at all — nothing to synthesize
|
|
661
|
+
if accumulated == 0:
|
|
662
|
+
break
|
|
663
|
+
|
|
664
|
+
# ── Synthesize ONCE with every worker result in history ──────────
|
|
665
|
+
console.print(
|
|
666
|
+
f"\n [bold cyan]🎯 All {accumulated} worker result{'s' if accumulated > 1 else ''} in"
|
|
667
|
+
f" — synthesizing...[/bold cyan]\n"
|
|
668
|
+
)
|
|
669
|
+
accumulated = 0 # reset before synthesis (coordinator may spawn new workers)
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
run_agent(
|
|
673
|
+
"",
|
|
674
|
+
project_path=cwd,
|
|
675
|
+
auto_approve=session.get("auto_approve", auto_approve),
|
|
676
|
+
history=history,
|
|
677
|
+
system_content=session.get("system"),
|
|
678
|
+
file_cache=session.get("file_cache"),
|
|
679
|
+
worker_pool=pool,
|
|
680
|
+
change_log=session.get("change_log"),
|
|
681
|
+
)
|
|
682
|
+
except Exception as e:
|
|
683
|
+
console.print(f"\n [dim red]⚠ Coordinator synthesis error: {e}[/dim red]")
|
|
684
|
+
|
|
685
|
+
# After synthesis the coordinator may have spawned new workers.
|
|
686
|
+
# Loop back — if new workers exist we accumulate again; otherwise we exit.
|
|
687
|
+
|
|
688
|
+
except KeyboardInterrupt:
|
|
689
|
+
with pool._lock:
|
|
690
|
+
still = sum(1 for w in pool._workers.values() if w.status == W_RUNNING)
|
|
691
|
+
console.print(f"\n[dim] Interrupted. {still} worker(s) still running in background.[/dim]")
|
|
692
|
+
console.print("[dim] Their results arrive automatically on your next message. /workers for status.[/dim]")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# ── Interactive Mode ──────────────────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
def interactive_mode(auto_approve: bool = False):
|
|
698
|
+
from . import session_storage
|
|
699
|
+
|
|
700
|
+
show_banner()
|
|
701
|
+
cwd = os.getcwd()
|
|
702
|
+
|
|
703
|
+
console.print(f"[dim]Working directory: {cwd}[/dim]")
|
|
704
|
+
|
|
705
|
+
# ── Session resume ────────────────────────────────────────────────────────
|
|
706
|
+
conversation_history: list = []
|
|
707
|
+
_sess_id = session_storage.new_session_id()
|
|
708
|
+
_sess_path = session_storage.session_path(cwd, _sess_id)
|
|
709
|
+
_last_saved = 0 # tracks how many messages have been flushed to disk
|
|
710
|
+
|
|
711
|
+
recent = session_storage.list_recent(cwd, max_n=3)
|
|
712
|
+
if recent:
|
|
713
|
+
prev = recent[0]
|
|
714
|
+
console.print(
|
|
715
|
+
f"[dim]Previous session found: {prev['turns']} turns, "
|
|
716
|
+
f"{prev['mtime_str']} — last: \"{prev['last_message']}\"[/dim]"
|
|
717
|
+
)
|
|
718
|
+
console.print("[dim] /resume to continue it, or start fresh below.[/dim]")
|
|
719
|
+
|
|
720
|
+
console.print("[dim]Type your task below. /help for commands. Ctrl+C to exit.[/dim]\n")
|
|
721
|
+
|
|
722
|
+
# Build system prompt ONCE for the whole session — not per turn
|
|
723
|
+
system_content = _build_system(cwd)
|
|
724
|
+
|
|
725
|
+
file_cache: dict = {}
|
|
726
|
+
change_log: dict = {}
|
|
727
|
+
todo_list: list = []
|
|
728
|
+
session = {
|
|
729
|
+
"messages": conversation_history,
|
|
730
|
+
"file_cache": file_cache,
|
|
731
|
+
"change_log": change_log,
|
|
732
|
+
"todo_list": todo_list,
|
|
733
|
+
"auto_approve": auto_approve,
|
|
734
|
+
"plan_mode": False,
|
|
735
|
+
"system": system_content,
|
|
736
|
+
# session storage context — used by /resume command
|
|
737
|
+
"_sess_id": _sess_id,
|
|
738
|
+
"_sess_path": _sess_path,
|
|
739
|
+
"_recent": recent,
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
# Write session pointer so /resume can find it
|
|
743
|
+
session_storage.save_latest_pointer(cwd, _sess_id)
|
|
744
|
+
|
|
745
|
+
# Enable readline history if available (Unix/Mac)
|
|
746
|
+
history_file = Path.home() / ".bharatcode" / "history"
|
|
747
|
+
try:
|
|
748
|
+
if readline is not None:
|
|
749
|
+
history_file.parent.mkdir(exist_ok=True)
|
|
750
|
+
if history_file.exists():
|
|
751
|
+
readline.read_history_file(str(history_file))
|
|
752
|
+
readline.set_history_length(500)
|
|
753
|
+
except Exception:
|
|
754
|
+
pass
|
|
755
|
+
|
|
756
|
+
while True:
|
|
757
|
+
try:
|
|
758
|
+
console.print()
|
|
759
|
+
raw = _read_input()
|
|
760
|
+
|
|
761
|
+
if not raw.strip():
|
|
762
|
+
continue
|
|
763
|
+
|
|
764
|
+
if raw.lower() in ("exit", "quit", "q", ":q"):
|
|
765
|
+
_cl = session.get("change_log", {})
|
|
766
|
+
if _cl:
|
|
767
|
+
console.print(f"\n[dim]Session changes ({len(_cl)} file{'s' if len(_cl) > 1 else ''}):[/dim]")
|
|
768
|
+
for _fp in sorted(_cl.keys()):
|
|
769
|
+
_st = _cl[_fp]
|
|
770
|
+
_total = _st.get("writes", 0) + _st.get("edits", 0)
|
|
771
|
+
_parts = []
|
|
772
|
+
if _st.get("writes"):
|
|
773
|
+
_parts.append(f"{_st['writes']}W")
|
|
774
|
+
if _st.get("edits"):
|
|
775
|
+
_parts.append(f"{_st['edits']}E")
|
|
776
|
+
console.print(
|
|
777
|
+
f" [dim cyan]{_fp}[/dim cyan] [dim]({', '.join(_parts)})[/dim]"
|
|
778
|
+
)
|
|
779
|
+
console.print("\n[dim]Goodbye! Happy coding![/dim]\n")
|
|
780
|
+
break
|
|
781
|
+
|
|
782
|
+
# Save history
|
|
783
|
+
try:
|
|
784
|
+
if readline is not None:
|
|
785
|
+
readline.write_history_file(str(history_file))
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
|
|
789
|
+
# Slash commands
|
|
790
|
+
if raw.startswith("/"):
|
|
791
|
+
handle_slash_command(raw, session)
|
|
792
|
+
continue
|
|
793
|
+
|
|
794
|
+
# Run agent — cached system prompt + persistent history + plan mode
|
|
795
|
+
console.print()
|
|
796
|
+
plan_mode = session.get("plan_mode", False)
|
|
797
|
+
if plan_mode:
|
|
798
|
+
console.print("[dim yellow] [PLAN MODE — read only][/dim yellow]\n")
|
|
799
|
+
_paths_before = set(session.get("change_log", {}).keys())
|
|
800
|
+
run_agent(
|
|
801
|
+
raw,
|
|
802
|
+
project_path=cwd,
|
|
803
|
+
auto_approve=session.get("auto_approve", auto_approve),
|
|
804
|
+
history=conversation_history,
|
|
805
|
+
system_content=session.get("system"),
|
|
806
|
+
plan_mode=plan_mode,
|
|
807
|
+
file_cache=session.get("file_cache"),
|
|
808
|
+
worker_pool=session.get("worker_pool"),
|
|
809
|
+
change_log=session.get("change_log"),
|
|
810
|
+
todo_state=session.get("todo_list"),
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# ── Flush new messages to JSONL session file ─────────────────────
|
|
814
|
+
new_msgs = conversation_history[_last_saved:]
|
|
815
|
+
if new_msgs:
|
|
816
|
+
session_storage.append_messages(_sess_path, new_msgs)
|
|
817
|
+
_last_saved = len(conversation_history)
|
|
818
|
+
|
|
819
|
+
# ── Refresh the project index when new files were created ────────
|
|
820
|
+
# (the index is baked into the system prompt at session start; a
|
|
821
|
+
# build that adds files would otherwise leave the model blind to them)
|
|
822
|
+
if set(session.get("change_log", {}).keys()) - _paths_before:
|
|
823
|
+
try:
|
|
824
|
+
session["system"] = _build_system(cwd)
|
|
825
|
+
except Exception:
|
|
826
|
+
pass
|
|
827
|
+
|
|
828
|
+
# ── Coordinator: wait for workers and re-enter when they report ──
|
|
829
|
+
# run_agent() exits as soon as the coordinator's turn ends.
|
|
830
|
+
# Workers run in background threads and push <task-notification>
|
|
831
|
+
# to the queue. We poll here and re-call run_agent() (with empty
|
|
832
|
+
# task so no new user message is added) whenever results arrive.
|
|
833
|
+
if session.get("coordinator_mode"):
|
|
834
|
+
_coordinator_notification_loop(
|
|
835
|
+
session, cwd, auto_approve, conversation_history
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
except KeyboardInterrupt:
|
|
839
|
+
console.print("\n[dim]Use 'exit' or Ctrl+D to quit.[/dim]")
|
|
840
|
+
except EOFError:
|
|
841
|
+
console.print("\n[dim]Goodbye![/dim]\n")
|
|
842
|
+
break
|
|
843
|
+
except Exception as e:
|
|
844
|
+
show_error(str(e))
|
|
845
|
+
if os.getenv("BHARATCODE_DEBUG"):
|
|
846
|
+
import traceback; traceback.print_exc()
|