bhai-cli 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/PKG-INFO +1 -1
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/pyproject.toml +1 -1
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/cli.py +67 -6
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/orchestrator.py +121 -9
- bhai_cli-0.2.4/src/bhai_cli/session_manager.py +94 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/tools/__init__.py +2 -2
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/LICENSE +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/README.md +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/__init__.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/audio_engine.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/config_manager.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/tools/bash.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/tools/codebase.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/tools/file_ops.py +0 -0
- {bhai_cli-0.2.2 → bhai_cli-0.2.4}/src/bhai_cli/tools/search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bhai-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "BHAI — The Dual-Brain AI Coding Agent with Punjabi Swagger. Indic-native voice + universal LLM tool-calling."
|
|
5
5
|
authors = ["Madhav Kapila <smartatk04@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -30,6 +30,7 @@ from bhai_cli.config_manager import (
|
|
|
30
30
|
CONFIG_DIR,
|
|
31
31
|
_mask_key,
|
|
32
32
|
)
|
|
33
|
+
from bhai_cli.session_manager import get_all_sessions, delete_session
|
|
33
34
|
|
|
34
35
|
console = Console()
|
|
35
36
|
|
|
@@ -99,22 +100,75 @@ def interactive_menu():
|
|
|
99
100
|
console.print(" [2] 🎙️ Listen (Voice mode)")
|
|
100
101
|
console.print(" [3] ⚙️ Setup Wizard")
|
|
101
102
|
console.print(" [4] 📋 View Config")
|
|
102
|
-
console.print(" [5]
|
|
103
|
+
console.print(" [5] 🗂️ Sessions")
|
|
104
|
+
console.print(" [6] ❌ Exit\n")
|
|
103
105
|
|
|
104
|
-
choice = Prompt.ask("[bold green]Choose an option[/]", choices=["1", "2", "3", "4", "5"])
|
|
106
|
+
choice = Prompt.ask("[bold green]Choose an option[/]", choices=["1", "2", "3", "4", "5", "6"])
|
|
105
107
|
|
|
106
108
|
if choice == "1":
|
|
107
|
-
text(prompt="", vibe_model=None, coder_model=None, interactive=False)
|
|
109
|
+
text(prompt="", vibe_model=None, coder_model=None, session_id=None, interactive=False)
|
|
108
110
|
elif choice == "2":
|
|
109
|
-
listen(vibe_model=None, coder_model=None, file=None)
|
|
111
|
+
listen(vibe_model=None, coder_model=None, file=None, session_id=None)
|
|
110
112
|
elif choice == "3":
|
|
111
113
|
setup()
|
|
112
114
|
elif choice == "4":
|
|
113
115
|
config()
|
|
114
116
|
elif choice == "5":
|
|
117
|
+
sessions_menu()
|
|
118
|
+
elif choice == "6":
|
|
115
119
|
console.print("\n[bold yellow]BHAI:[/] Accha bhai, phir milenge! 🤙")
|
|
116
120
|
sys.exit(0)
|
|
117
121
|
|
|
122
|
+
def sessions_menu():
|
|
123
|
+
"""Interactive menu to manage sessions."""
|
|
124
|
+
while True:
|
|
125
|
+
sessions = get_all_sessions()
|
|
126
|
+
console.print("\n[bold cyan]🗂️ Session Management[/]")
|
|
127
|
+
|
|
128
|
+
if not sessions:
|
|
129
|
+
console.print("[dim]No past sessions found.[/]")
|
|
130
|
+
else:
|
|
131
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
132
|
+
table.add_column("No.", style="bold white")
|
|
133
|
+
table.add_column("Title", style="yellow")
|
|
134
|
+
table.add_column("Updated At", style="dim")
|
|
135
|
+
table.add_column("ID", style="dim")
|
|
136
|
+
|
|
137
|
+
for idx, s in enumerate(sessions):
|
|
138
|
+
table.add_row(str(idx + 1), s["title"], s["updated_at"][:16], s["id"])
|
|
139
|
+
console.print(table)
|
|
140
|
+
|
|
141
|
+
console.print("\nOptions: [bold green]N[/] (New Session) | [bold yellow]D <no>[/] (Delete) | [bold cyan]<no>[/] (Resume) | [bold red]B[/] (Back)")
|
|
142
|
+
choice = Prompt.ask("Select").strip().lower()
|
|
143
|
+
|
|
144
|
+
if choice == "b" or choice == "back":
|
|
145
|
+
break
|
|
146
|
+
elif choice == "n" or choice == "new":
|
|
147
|
+
console.print("[bold green]Starting new session...[/]")
|
|
148
|
+
text(prompt="", vibe_model=None, coder_model=None, session_id=None, interactive=True)
|
|
149
|
+
break
|
|
150
|
+
elif choice.startswith("d "):
|
|
151
|
+
try:
|
|
152
|
+
idx = int(choice.split(" ")[1]) - 1
|
|
153
|
+
if 0 <= idx < len(sessions):
|
|
154
|
+
s_id = sessions[idx]["id"]
|
|
155
|
+
if delete_session(s_id):
|
|
156
|
+
console.print(f"[green]Deleted session: {s_id}[/]")
|
|
157
|
+
else:
|
|
158
|
+
console.print("[red]Failed to delete session.[/]")
|
|
159
|
+
except ValueError:
|
|
160
|
+
console.print("[red]Invalid selection.[/]")
|
|
161
|
+
else:
|
|
162
|
+
try:
|
|
163
|
+
idx = int(choice) - 1
|
|
164
|
+
if 0 <= idx < len(sessions):
|
|
165
|
+
s_id = sessions[idx]["id"]
|
|
166
|
+
console.print(f"[bold green]Resuming session: {sessions[idx]['title']}[/]")
|
|
167
|
+
text(prompt="", vibe_model=None, coder_model=None, session_id=s_id, interactive=True)
|
|
168
|
+
break
|
|
169
|
+
except ValueError:
|
|
170
|
+
console.print("[red]Invalid selection.[/]")
|
|
171
|
+
|
|
118
172
|
|
|
119
173
|
# ---------------------------------------------------------------------------
|
|
120
174
|
# bhai-code setup
|
|
@@ -204,18 +258,21 @@ def text(
|
|
|
204
258
|
prompt: str = typer.Argument("", help="Your message to BHAI. If empty, starts interactive mode."),
|
|
205
259
|
vibe_model: Optional[str] = typer.Option(None, "--vibe-model", "-vm", help="Override VibeLLM model."),
|
|
206
260
|
coder_model: Optional[str] = typer.Option(None, "--coder-model", "-cm", help="Override CoderAgent model."),
|
|
261
|
+
session_id: Optional[str] = typer.Option(None, "--session-id", "-s", help="Resume a specific session ID."),
|
|
207
262
|
interactive: bool = typer.Option(False, "--interactive", "-i", help="Enter interactive chat mode (always active if prompt is empty)."),
|
|
208
263
|
) -> None:
|
|
209
264
|
"""[bold magenta]💬 text[/] — Send a text prompt to BHAI or start interactive session."""
|
|
210
265
|
if not prompt:
|
|
211
266
|
print_banner()
|
|
267
|
+
if session_id:
|
|
268
|
+
console.print(f"[bold cyan]Resuming Session: {session_id}[/]")
|
|
212
269
|
console.print("[bold cyan]Welcome to BHAI Interactive Mode.[/]\n[dim]Type 'exit', 'quit', or 'nikal' to leave.[/]\n")
|
|
213
270
|
interactive = True
|
|
214
271
|
|
|
215
272
|
from bhai_cli.orchestrator import BhaiOrchestrator
|
|
216
273
|
|
|
217
274
|
cfg = load_config(vibe_model_override=vibe_model, coder_model_override=coder_model)
|
|
218
|
-
orchestrator = BhaiOrchestrator(cfg)
|
|
275
|
+
orchestrator = BhaiOrchestrator(cfg, session_id=session_id)
|
|
219
276
|
|
|
220
277
|
if prompt:
|
|
221
278
|
asyncio.run(orchestrator.process(prompt))
|
|
@@ -229,6 +286,9 @@ def text(
|
|
|
229
286
|
if user_input.strip().lower() in ("exit", "quit", "bye", "chal hatja", "nikal"):
|
|
230
287
|
console.print("\n[bold yellow]BHAI:[/] Accha bhai, phir milenge! 🤙")
|
|
231
288
|
break
|
|
289
|
+
if user_input.strip().lower() == "/compress":
|
|
290
|
+
asyncio.run(orchestrator.compress_context(manual=True))
|
|
291
|
+
continue
|
|
232
292
|
asyncio.run(orchestrator.process(user_input))
|
|
233
293
|
except (KeyboardInterrupt, EOFError):
|
|
234
294
|
console.print("\n[bold yellow]BHAI:[/] Theek hai boss, baad mein baat karte hain! 👋")
|
|
@@ -242,6 +302,7 @@ def text(
|
|
|
242
302
|
def listen(
|
|
243
303
|
vibe_model: Optional[str] = typer.Option(None, "--vibe-model", "-vm", help="Override VibeLLM model."),
|
|
244
304
|
coder_model: Optional[str] = typer.Option(None, "--coder-model", "-cm", help="Override CoderAgent model."),
|
|
305
|
+
session_id: Optional[str] = typer.Option(None, "--session-id", "-s", help="Resume a specific session ID."),
|
|
245
306
|
file: Optional[str] = typer.Option(None, "--file", "-f", help="Path to an audio file instead of live mic."),
|
|
246
307
|
) -> None:
|
|
247
308
|
"""[bold red]🎙️ listen[/] — Voice input — speak to BHAI in any Indian language."""
|
|
@@ -264,7 +325,7 @@ def listen(
|
|
|
264
325
|
)
|
|
265
326
|
raise typer.Exit(1)
|
|
266
327
|
|
|
267
|
-
orchestrator = BhaiOrchestrator(cfg)
|
|
328
|
+
orchestrator = BhaiOrchestrator(cfg, session_id=session_id)
|
|
268
329
|
audio_bytes = None
|
|
269
330
|
audio_file = file or cfg.audio_fallback_file
|
|
270
331
|
|
|
@@ -24,6 +24,7 @@ from rich.markdown import Markdown
|
|
|
24
24
|
|
|
25
25
|
from bhai_cli.config_manager import BhaiConfig
|
|
26
26
|
from bhai_cli.tools import TOOL_SCHEMAS, dispatch_tool
|
|
27
|
+
from bhai_cli.session_manager import load_session, save_session, create_session_id
|
|
27
28
|
|
|
28
29
|
console = Console()
|
|
29
30
|
|
|
@@ -39,10 +40,10 @@ VIBE_SYSTEM_PROMPT = (
|
|
|
39
40
|
"Your job:\n"
|
|
40
41
|
"1. Parse the user's intent (any Indian language, Hinglish, or English) into a clear "
|
|
41
42
|
"English task description.\n"
|
|
42
|
-
"2. IMPORTANT: You control a CoderAgent that has tools to read/edit files, list directories,
|
|
43
|
-
"If the user asks to explain a file, run code, or do ANY system task, DO NOT ask them to
|
|
44
|
-
"Instead, output a JSON block to delegate it: {\"task\": \"Read HelloWorld.py and explain it
|
|
45
|
-
"3. If strictly chatting with no system/code actions needed, respond conversationally with swagger.\n"
|
|
43
|
+
"2. IMPORTANT: You control a CoderAgent that has tools to read/edit files, list directories, run bash commands, and perform web searches! "
|
|
44
|
+
"If the user asks to explain a file, run code, get real-time info from the internet, or do ANY system task, DO NOT ask them to do it themselves or upload it. "
|
|
45
|
+
"Instead, output a JSON block to delegate it: {\"task\": \"Read HelloWorld.py and explain it\"} or {\"task\": \"Search the web for IPL 2026 points table\"}.\n"
|
|
46
|
+
"3. If strictly chatting with no system/code/search actions needed, respond conversationally with swagger.\n"
|
|
46
47
|
"4. After CoderAgent results, review them and decide:\n"
|
|
47
48
|
' - If the task is COMPLETE, say "BHAI_DONE" somewhere in your response.\n'
|
|
48
49
|
' - If MORE WORK is needed, output another {"task": "next step description"}.\n\n'
|
|
@@ -61,7 +62,8 @@ CODER_SYSTEM_PROMPT = (
|
|
|
61
62
|
"5. Use read_file to see exact file contents before edit_file.\n"
|
|
62
63
|
"6. Use execute_bash for running tests, git, docker, and system commands.\n"
|
|
63
64
|
"7. Verify your changes work by running relevant commands.\n"
|
|
64
|
-
"8. Report results factually — what you did, what worked, what failed.\n
|
|
65
|
+
"8. Report results factually — what you did, what worked, what failed.\n"
|
|
66
|
+
"9. IMPORTANT: When you are done with a task or providing an explanation, simply output plain text. DO NOT format your explanation as a JSON tool call if no tools are needed.\n\n"
|
|
65
67
|
"## Tools Available:\n"
|
|
66
68
|
"- list_directory: Explore project structure\n"
|
|
67
69
|
"- codebase_context: Get function/class signatures (AST-based)\n"
|
|
@@ -81,11 +83,39 @@ CODER_SYSTEM_PROMPT = (
|
|
|
81
83
|
class BhaiOrchestrator:
|
|
82
84
|
"""Dual-brain agentic orchestrator for BHAI-CLI."""
|
|
83
85
|
|
|
84
|
-
def __init__(self, config: BhaiConfig):
|
|
86
|
+
def __init__(self, config: BhaiConfig, session_id: Optional[str] = None):
|
|
85
87
|
self.config = config
|
|
86
|
-
self.
|
|
88
|
+
self.session_id = session_id or create_session_id()
|
|
89
|
+
self.session_title, self.conversation_history = load_session(self.session_id)
|
|
87
90
|
self._setup_litellm()
|
|
88
91
|
|
|
92
|
+
async def _generate_session_title(self, prompt: str) -> None:
|
|
93
|
+
"""Generate a short title using VibeLLM."""
|
|
94
|
+
if self.session_title:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
messages = [
|
|
98
|
+
{"role": "system", "content": "Generate a very short title (max 5 words) for this chat based on the user's first prompt. Respond ONLY with the title string."},
|
|
99
|
+
{"role": "user", "content": prompt}
|
|
100
|
+
]
|
|
101
|
+
try:
|
|
102
|
+
kwargs = {
|
|
103
|
+
"model": self.config.vibe.model,
|
|
104
|
+
"messages": messages,
|
|
105
|
+
"temperature": 0.5,
|
|
106
|
+
"max_tokens": 20,
|
|
107
|
+
}
|
|
108
|
+
if self.config.vibe.api_base:
|
|
109
|
+
kwargs["api_base"] = self.config.vibe.api_base
|
|
110
|
+
if self.config.vibe.api_key:
|
|
111
|
+
kwargs["api_key"] = self.config.vibe.api_key
|
|
112
|
+
response = await litellm.acompletion(**kwargs)
|
|
113
|
+
title = response.choices[0].message.content.strip().strip('"\'')
|
|
114
|
+
if title:
|
|
115
|
+
self.session_title = title
|
|
116
|
+
except Exception:
|
|
117
|
+
self.session_title = prompt[:30] + "..."
|
|
118
|
+
|
|
89
119
|
def _setup_litellm(self) -> None:
|
|
90
120
|
import os
|
|
91
121
|
if self.config.vibe.api_key:
|
|
@@ -136,14 +166,24 @@ class BhaiOrchestrator:
|
|
|
136
166
|
}
|
|
137
167
|
if self.config.vibe.api_base:
|
|
138
168
|
kwargs["api_base"] = self.config.vibe.api_base
|
|
169
|
+
if self.config.vibe.api_key:
|
|
170
|
+
kwargs["api_key"] = self.config.vibe.api_key
|
|
139
171
|
|
|
140
172
|
try:
|
|
141
173
|
response = await litellm.acompletion(**kwargs)
|
|
142
174
|
content = response.choices[0].message.content or ""
|
|
143
175
|
self.conversation_history.append({"role": "user", "content": user_input})
|
|
144
176
|
self.conversation_history.append({"role": "assistant", "content": content})
|
|
145
|
-
|
|
146
|
-
|
|
177
|
+
|
|
178
|
+
# Auto-compress if history is getting too long (e.g. 30 messages)
|
|
179
|
+
if len(self.conversation_history) > 30:
|
|
180
|
+
await self.compress_context()
|
|
181
|
+
|
|
182
|
+
if not self.session_title:
|
|
183
|
+
await self._generate_session_title(user_input)
|
|
184
|
+
|
|
185
|
+
save_session(self.session_id, self.session_title, self.conversation_history)
|
|
186
|
+
|
|
147
187
|
task = self._extract_task(content)
|
|
148
188
|
return task, content
|
|
149
189
|
except Exception as e:
|
|
@@ -197,6 +237,8 @@ class BhaiOrchestrator:
|
|
|
197
237
|
}
|
|
198
238
|
if self.config.coder.api_base:
|
|
199
239
|
kwargs["api_base"] = self.config.coder.api_base
|
|
240
|
+
if self.config.coder.api_key:
|
|
241
|
+
kwargs["api_key"] = self.config.coder.api_key
|
|
200
242
|
|
|
201
243
|
try:
|
|
202
244
|
response = await litellm.acompletion(**kwargs)
|
|
@@ -234,12 +276,82 @@ class BhaiOrchestrator:
|
|
|
234
276
|
})
|
|
235
277
|
|
|
236
278
|
except Exception as e:
|
|
279
|
+
error_str = str(e)
|
|
280
|
+
if "failed_generation" in error_str:
|
|
281
|
+
try:
|
|
282
|
+
json_start = error_str.find("{")
|
|
283
|
+
if json_start != -1:
|
|
284
|
+
error_json = json.loads(error_str[json_start:])
|
|
285
|
+
failed_gen = error_json.get("error", {}).get("failed_generation", "")
|
|
286
|
+
if failed_gen:
|
|
287
|
+
return failed_gen
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
237
291
|
error = f"CoderAgent error ({type(e).__name__}): {e}"
|
|
238
292
|
console.print(f"[bold red]⚠ {error}[/]")
|
|
239
293
|
return error
|
|
240
294
|
|
|
241
295
|
return "Max iterations reached.\n" + "\n".join(all_results)
|
|
242
296
|
|
|
297
|
+
async def compress_context(self, manual: bool = False) -> None:
|
|
298
|
+
"""Summarize older messages to free up context window."""
|
|
299
|
+
if len(self.conversation_history) <= 4 and not manual:
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Keep the latest 10 messages, compress the rest. If manual, compress all but the last 2.
|
|
303
|
+
keep_count = 2 if manual else 10
|
|
304
|
+
if len(self.conversation_history) <= keep_count:
|
|
305
|
+
if manual:
|
|
306
|
+
console.print("[dim]Not enough history to compress.[/]")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
messages_to_compress = self.conversation_history[:-keep_count]
|
|
310
|
+
retained_messages = self.conversation_history[-keep_count:]
|
|
311
|
+
|
|
312
|
+
history_text = ""
|
|
313
|
+
for msg in messages_to_compress:
|
|
314
|
+
role = msg.get("role", "unknown")
|
|
315
|
+
content = msg.get("content", "")
|
|
316
|
+
history_text += f"{role.upper()}: {content}\n\n"
|
|
317
|
+
|
|
318
|
+
prompt = (
|
|
319
|
+
"Summarize the following conversation history densely. "
|
|
320
|
+
"Retain all key technical facts, decisions, and context. "
|
|
321
|
+
"Ignore conversational filler. Be concise.\n\n"
|
|
322
|
+
f"{history_text}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
with console.status("[bold cyan]BHAI is compressing memory... 🗜️[/]"):
|
|
326
|
+
try:
|
|
327
|
+
kwargs = {
|
|
328
|
+
"model": self.config.vibe.model,
|
|
329
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
330
|
+
"temperature": 0.3,
|
|
331
|
+
"max_tokens": 500,
|
|
332
|
+
}
|
|
333
|
+
if self.config.vibe.api_base:
|
|
334
|
+
kwargs["api_base"] = self.config.vibe.api_base
|
|
335
|
+
if self.config.vibe.api_key:
|
|
336
|
+
kwargs["api_key"] = self.config.vibe.api_key
|
|
337
|
+
|
|
338
|
+
response = await litellm.acompletion(**kwargs)
|
|
339
|
+
summary = (response.choices[0].message.content or "Conversation compressed.").strip()
|
|
340
|
+
|
|
341
|
+
self.conversation_history = [
|
|
342
|
+
{"role": "system", "content": f"Previous Conversation Summary:\n{summary}"}
|
|
343
|
+
] + retained_messages
|
|
344
|
+
|
|
345
|
+
save_session(self.session_id, self.session_title, self.conversation_history)
|
|
346
|
+
|
|
347
|
+
if manual:
|
|
348
|
+
console.print(Panel(
|
|
349
|
+
f"[bold green]✅ Context compressed successfully.[/]\n\n[dim]Summary:\n{summary}[/]",
|
|
350
|
+
title="[bold cyan]Memory Compression[/]", border_style="cyan", padding=(1, 2)
|
|
351
|
+
))
|
|
352
|
+
except Exception as e:
|
|
353
|
+
console.print(f"[bold red]⚠ Compression failed:[/] {e}")
|
|
354
|
+
|
|
243
355
|
# ── Agentic Pipeline ──
|
|
244
356
|
|
|
245
357
|
async def process(self, user_input: str) -> str:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Manager for BHAI-CLI.
|
|
3
|
+
|
|
4
|
+
Handles persistent conversation history across multiple sessions.
|
|
5
|
+
Stores sessions as JSON files in ~/.config/bhai/sessions/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from bhai_cli.config_manager import CONFIG_DIR
|
|
16
|
+
|
|
17
|
+
SESSIONS_DIR = CONFIG_DIR / "sessions"
|
|
18
|
+
|
|
19
|
+
def _get_sessions_dir() -> Path:
|
|
20
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
return SESSIONS_DIR
|
|
22
|
+
|
|
23
|
+
def create_session_id() -> str:
|
|
24
|
+
"""Generate a new short hash-based session ID."""
|
|
25
|
+
return uuid.uuid4().hex[:8]
|
|
26
|
+
|
|
27
|
+
def get_all_sessions() -> list[dict]:
|
|
28
|
+
"""
|
|
29
|
+
Retrieve metadata for all saved sessions.
|
|
30
|
+
Returns a list of dicts: {"id": str, "title": str, "updated_at": str}
|
|
31
|
+
sorted by updated_at descending.
|
|
32
|
+
"""
|
|
33
|
+
sessions_dir = _get_sessions_dir()
|
|
34
|
+
sessions = []
|
|
35
|
+
|
|
36
|
+
for file_path in sessions_dir.glob("*.json"):
|
|
37
|
+
try:
|
|
38
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
sessions.append({
|
|
41
|
+
"id": data.get("id", file_path.stem),
|
|
42
|
+
"title": data.get("title", "Untitled Session"),
|
|
43
|
+
"updated_at": data.get("updated_at", ""),
|
|
44
|
+
})
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
sessions.sort(key=lambda x: x["updated_at"], reverse=True)
|
|
49
|
+
return sessions
|
|
50
|
+
|
|
51
|
+
def load_session(session_id: str) -> tuple[str, list[dict]]:
|
|
52
|
+
"""
|
|
53
|
+
Load a session from disk.
|
|
54
|
+
Returns (title, history).
|
|
55
|
+
If session_id doesn't exist, returns ("", []).
|
|
56
|
+
"""
|
|
57
|
+
file_path = _get_sessions_dir() / f"{session_id}.json"
|
|
58
|
+
if not file_path.exists():
|
|
59
|
+
return "", []
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
return data.get("title", ""), data.get("history", [])
|
|
65
|
+
except Exception:
|
|
66
|
+
return "", []
|
|
67
|
+
|
|
68
|
+
def save_session(session_id: str, title: str, history: list[dict]) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Save the session history and title to disk.
|
|
71
|
+
"""
|
|
72
|
+
file_path = _get_sessions_dir() / f"{session_id}.json"
|
|
73
|
+
now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
74
|
+
|
|
75
|
+
data = {
|
|
76
|
+
"id": session_id,
|
|
77
|
+
"title": title or "Untitled Session",
|
|
78
|
+
"updated_at": now_iso,
|
|
79
|
+
"history": history,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
83
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
84
|
+
|
|
85
|
+
def delete_session(session_id: str) -> bool:
|
|
86
|
+
"""Delete a session file. Returns True if deleted, False otherwise."""
|
|
87
|
+
file_path = _get_sessions_dir() / f"{session_id}.json"
|
|
88
|
+
if file_path.exists():
|
|
89
|
+
try:
|
|
90
|
+
file_path.unlink()
|
|
91
|
+
return True
|
|
92
|
+
except Exception:
|
|
93
|
+
return False
|
|
94
|
+
return False
|
|
@@ -86,7 +86,7 @@ TOOL_SCHEMAS: list[dict] = [
|
|
|
86
86
|
},
|
|
87
87
|
"include_content": {
|
|
88
88
|
"type": "boolean",
|
|
89
|
-
"description": "If true, include full file content
|
|
89
|
+
"description": "If true, include full file content. Default: false. MUST BE A NATIVE JSON BOOLEAN (true/false), NOT A STRING.",
|
|
90
90
|
},
|
|
91
91
|
"max_depth": {
|
|
92
92
|
"type": "integer",
|
|
@@ -195,7 +195,7 @@ TOOL_SCHEMAS: list[dict] = [
|
|
|
195
195
|
},
|
|
196
196
|
"recursive": {
|
|
197
197
|
"type": "boolean",
|
|
198
|
-
"description": "If true, list recursively. Default: false.",
|
|
198
|
+
"description": "If true, list recursively. Default: false. MUST BE A NATIVE JSON BOOLEAN (true/false), NOT A STRING.",
|
|
199
199
|
},
|
|
200
200
|
"max_depth": {
|
|
201
201
|
"type": "integer",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|