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