code-lm 0.3.0__tar.gz → 0.3.2__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.
Files changed (30) hide show
  1. {code_lm-0.3.0/src/code_lm.egg-info → code_lm-0.3.2}/PKG-INFO +20 -8
  2. {code_lm-0.3.0 → code_lm-0.3.2}/README.md +19 -7
  3. {code_lm-0.3.0 → code_lm-0.3.2}/pyproject.toml +1 -1
  4. {code_lm-0.3.0 → code_lm-0.3.2/src/code_lm.egg-info}/PKG-INFO +20 -8
  5. {code_lm-0.3.0 → code_lm-0.3.2}/src/code_lm.egg-info/SOURCES.txt +1 -1
  6. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/main.py +240 -2
  7. code_lm-0.3.2/src/lm_code/session.py +283 -0
  8. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/utils.py +1 -3
  9. code_lm-0.3.0/src/lm_code/models/gemini.py +0 -43
  10. {code_lm-0.3.0 → code_lm-0.3.2}/MANIFEST.in +0 -0
  11. {code_lm-0.3.0 → code_lm-0.3.2}/setup.cfg +0 -0
  12. {code_lm-0.3.0 → code_lm-0.3.2}/setup.py +0 -0
  13. {code_lm-0.3.0 → code_lm-0.3.2}/src/code_lm.egg-info/dependency_links.txt +0 -0
  14. {code_lm-0.3.0 → code_lm-0.3.2}/src/code_lm.egg-info/entry_points.txt +0 -0
  15. {code_lm-0.3.0 → code_lm-0.3.2}/src/code_lm.egg-info/requires.txt +0 -0
  16. {code_lm-0.3.0 → code_lm-0.3.2}/src/code_lm.egg-info/top_level.txt +0 -0
  17. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/__init__.py +0 -0
  18. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/config.py +0 -0
  19. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/models/__init__.py +0 -0
  20. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/models/openrouter.py +0 -0
  21. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/__init__.py +0 -0
  22. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/base.py +0 -0
  23. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/directory_tools.py +0 -0
  24. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/file_tools.py +0 -0
  25. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/quality_tools.py +0 -0
  26. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/summarizer_tool.py +0 -0
  27. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/system_tools.py +0 -0
  28. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/task_complete_tool.py +0 -0
  29. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/test_runner.py +0 -0
  30. {code_lm-0.3.0 → code_lm-0.3.2}/src/lm_code/tools/tree_tool.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-lm
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: An AI coding assistant using various LLM models.
5
5
  Home-page: https://github.com/Panagiotis897/lm-code
6
6
  Author: Panagiotis897
@@ -39,6 +39,10 @@ LM Code is a powerful AI coding assistant for your terminal supporting 17 free m
39
39
  - Markdown rendering for improved readability.
40
40
  - **17 Free Models via OpenRouter**:
41
41
  - NVIDIA Nemotron, Qwen, OpenAI, Meta Llama, Mistral, and more.
42
+ - **Session Persistence**:
43
+ - Conversations are saved per project directory.
44
+ - Resume previous sessions automatically or manually with `/load`.
45
+ - Auto-saves after each exchange.
42
46
  - **Automated Tool Usage**:
43
47
  - File operations: `view`, `edit`, `grep`, `glob`.
44
48
  - Directory operations: `ls`, `tree`, `create_directory`.
@@ -132,8 +136,12 @@ lmcode list-models
132
136
 
133
137
  During an interactive session:
134
138
 
135
- - **`/exit`**: Exit the session.
139
+ - **`/exit`**: Save session and exit.
136
140
  - **`/help`**: Display help information.
141
+ - **`/sessions`**: List all sessions for the current project.
142
+ - **`/load <id>`**: Load a session by ID.
143
+ - **`/new`**: Start a new session (saves current first).
144
+ - **`/save`**: Manually save the current session.
137
145
 
138
146
  ---
139
147
 
@@ -153,10 +161,16 @@ LM Code is under active development. Contributions, feature requests, and feedba
153
161
 
154
162
  ### Changelog
155
163
 
164
+ #### v0.3.2
165
+ - Added session persistence: conversations are saved per project directory.
166
+ - Sessions auto-save after each exchange and on exit.
167
+ - New commands: `/sessions`, `/load <id>`, `/new`, `/save`.
168
+ - On startup, offers to resume previous sessions for the current project.
169
+
156
170
  #### v0.3.0
157
171
  - Updated default model to NVIDIA Nemotron 3 Super 120B.
158
172
  - Added 17 free models from OpenRouter (previously 6).
159
- - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point.
173
+ - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point after package rename.
160
174
  - Fixed `UnicodeDecodeError` on Windows (cp1253) for all subprocess commands.
161
175
  - Fixed API URL (`/chat/completions` was missing) causing HTML response errors.
162
176
  - Improved API error handling for empty/invalid responses.
@@ -168,20 +182,18 @@ LM Code is under active development. Contributions, feature requests, and feedba
168
182
  #### v0.2.5
169
183
  - Added more models to the model list.
170
184
  - Fixed crucial bugs from previous versions.
171
- - Removed Gemini models.
185
+ - Removed legacy Gemini module.
172
186
  - Updated models to latest versions.
173
187
 
174
188
  #### v0.1.0
175
- - Rebranded from Gemini to LM Code.
189
+ - Rebranded to LM Code.
176
190
  - Integrated OpenRouter as the default provider.
177
191
  - Added multi-model support.
178
- - Overhauled CLI commands (`gemini` -> `lmcode`).
192
+ - Overhauled CLI commands.
179
193
 
180
194
  ---
181
195
 
182
196
  ## Future Plans
183
-
184
- - Pricing with appropriate rate limits.
185
197
  - Non-free model support.
186
198
  - MCP Server integration.
187
199
  - Additional providers.
@@ -11,6 +11,10 @@ LM Code is a powerful AI coding assistant for your terminal supporting 17 free m
11
11
  - Markdown rendering for improved readability.
12
12
  - **17 Free Models via OpenRouter**:
13
13
  - NVIDIA Nemotron, Qwen, OpenAI, Meta Llama, Mistral, and more.
14
+ - **Session Persistence**:
15
+ - Conversations are saved per project directory.
16
+ - Resume previous sessions automatically or manually with `/load`.
17
+ - Auto-saves after each exchange.
14
18
  - **Automated Tool Usage**:
15
19
  - File operations: `view`, `edit`, `grep`, `glob`.
16
20
  - Directory operations: `ls`, `tree`, `create_directory`.
@@ -104,8 +108,12 @@ lmcode list-models
104
108
 
105
109
  During an interactive session:
106
110
 
107
- - **`/exit`**: Exit the session.
111
+ - **`/exit`**: Save session and exit.
108
112
  - **`/help`**: Display help information.
113
+ - **`/sessions`**: List all sessions for the current project.
114
+ - **`/load <id>`**: Load a session by ID.
115
+ - **`/new`**: Start a new session (saves current first).
116
+ - **`/save`**: Manually save the current session.
109
117
 
110
118
  ---
111
119
 
@@ -125,10 +133,16 @@ LM Code is under active development. Contributions, feature requests, and feedba
125
133
 
126
134
  ### Changelog
127
135
 
136
+ #### v0.3.2
137
+ - Added session persistence: conversations are saved per project directory.
138
+ - Sessions auto-save after each exchange and on exit.
139
+ - New commands: `/sessions`, `/load <id>`, `/new`, `/save`.
140
+ - On startup, offers to resume previous sessions for the current project.
141
+
128
142
  #### v0.3.0
129
143
  - Updated default model to NVIDIA Nemotron 3 Super 120B.
130
144
  - Added 17 free models from OpenRouter (previously 6).
131
- - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point.
145
+ - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point after package rename.
132
146
  - Fixed `UnicodeDecodeError` on Windows (cp1253) for all subprocess commands.
133
147
  - Fixed API URL (`/chat/completions` was missing) causing HTML response errors.
134
148
  - Improved API error handling for empty/invalid responses.
@@ -140,20 +154,18 @@ LM Code is under active development. Contributions, feature requests, and feedba
140
154
  #### v0.2.5
141
155
  - Added more models to the model list.
142
156
  - Fixed crucial bugs from previous versions.
143
- - Removed Gemini models.
157
+ - Removed legacy Gemini module.
144
158
  - Updated models to latest versions.
145
159
 
146
160
  #### v0.1.0
147
- - Rebranded from Gemini to LM Code.
161
+ - Rebranded to LM Code.
148
162
  - Integrated OpenRouter as the default provider.
149
163
  - Added multi-model support.
150
- - Overhauled CLI commands (`gemini` -> `lmcode`).
164
+ - Overhauled CLI commands.
151
165
 
152
166
  ---
153
167
 
154
168
  ## Future Plans
155
-
156
- - Pricing with appropriate rate limits.
157
169
  - Non-free model support.
158
170
  - MCP Server integration.
159
171
  - Additional providers.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-lm"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  authors = [
9
9
  { name="Panagiotis897", email="orion256business@gmail.com" }
10
10
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-lm
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: An AI coding assistant using various LLM models.
5
5
  Home-page: https://github.com/Panagiotis897/lm-code
6
6
  Author: Panagiotis897
@@ -39,6 +39,10 @@ LM Code is a powerful AI coding assistant for your terminal supporting 17 free m
39
39
  - Markdown rendering for improved readability.
40
40
  - **17 Free Models via OpenRouter**:
41
41
  - NVIDIA Nemotron, Qwen, OpenAI, Meta Llama, Mistral, and more.
42
+ - **Session Persistence**:
43
+ - Conversations are saved per project directory.
44
+ - Resume previous sessions automatically or manually with `/load`.
45
+ - Auto-saves after each exchange.
42
46
  - **Automated Tool Usage**:
43
47
  - File operations: `view`, `edit`, `grep`, `glob`.
44
48
  - Directory operations: `ls`, `tree`, `create_directory`.
@@ -132,8 +136,12 @@ lmcode list-models
132
136
 
133
137
  During an interactive session:
134
138
 
135
- - **`/exit`**: Exit the session.
139
+ - **`/exit`**: Save session and exit.
136
140
  - **`/help`**: Display help information.
141
+ - **`/sessions`**: List all sessions for the current project.
142
+ - **`/load <id>`**: Load a session by ID.
143
+ - **`/new`**: Start a new session (saves current first).
144
+ - **`/save`**: Manually save the current session.
137
145
 
138
146
  ---
139
147
 
@@ -153,10 +161,16 @@ LM Code is under active development. Contributions, feature requests, and feedba
153
161
 
154
162
  ### Changelog
155
163
 
164
+ #### v0.3.2
165
+ - Added session persistence: conversations are saved per project directory.
166
+ - Sessions auto-save after each exchange and on exit.
167
+ - New commands: `/sessions`, `/load <id>`, `/new`, `/save`.
168
+ - On startup, offers to resume previous sessions for the current project.
169
+
156
170
  #### v0.3.0
157
171
  - Updated default model to NVIDIA Nemotron 3 Super 120B.
158
172
  - Added 17 free models from OpenRouter (previously 6).
159
- - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point.
173
+ - Fixed `ModuleNotFoundError: No module named 'gemini_cli'` from stale entry point after package rename.
160
174
  - Fixed `UnicodeDecodeError` on Windows (cp1253) for all subprocess commands.
161
175
  - Fixed API URL (`/chat/completions` was missing) causing HTML response errors.
162
176
  - Improved API error handling for empty/invalid responses.
@@ -168,20 +182,18 @@ LM Code is under active development. Contributions, feature requests, and feedba
168
182
  #### v0.2.5
169
183
  - Added more models to the model list.
170
184
  - Fixed crucial bugs from previous versions.
171
- - Removed Gemini models.
185
+ - Removed legacy Gemini module.
172
186
  - Updated models to latest versions.
173
187
 
174
188
  #### v0.1.0
175
- - Rebranded from Gemini to LM Code.
189
+ - Rebranded to LM Code.
176
190
  - Integrated OpenRouter as the default provider.
177
191
  - Added multi-model support.
178
- - Overhauled CLI commands (`gemini` -> `lmcode`).
192
+ - Overhauled CLI commands.
179
193
 
180
194
  ---
181
195
 
182
196
  ## Future Plans
183
-
184
- - Pricing with appropriate rate limits.
185
197
  - Non-free model support.
186
198
  - MCP Server integration.
187
199
  - Additional providers.
@@ -11,9 +11,9 @@ src/code_lm.egg-info/top_level.txt
11
11
  src/lm_code/__init__.py
12
12
  src/lm_code/config.py
13
13
  src/lm_code/main.py
14
+ src/lm_code/session.py
14
15
  src/lm_code/utils.py
15
16
  src/lm_code/models/__init__.py
16
- src/lm_code/models/gemini.py
17
17
  src/lm_code/models/openrouter.py
18
18
  src/lm_code/tools/__init__.py
19
19
  src/lm_code/tools/base.py
@@ -10,14 +10,17 @@ from rich.console import Console
10
10
  from rich.markdown import Markdown
11
11
  from rich.panel import Panel
12
12
  from pathlib import Path
13
+ from typing import Optional, Dict, Any
13
14
  import yaml
14
15
  import logging
15
16
  import time
17
+ import questionary
16
18
 
17
19
  from .models.openrouter import OpenRouterModel, list_available_models
18
20
  from .config import Config
19
21
  from .utils import count_tokens
20
22
  from .tools import AVAILABLE_TOOLS
23
+ from .session import SessionManager
21
24
 
22
25
  # Setup console and config
23
26
  console = Console(
@@ -259,6 +262,13 @@ def start_interactive_session(model_name: str, console: Console):
259
262
  )
260
263
  return
261
264
 
265
+ # --- Session Persistence Setup ---
266
+ session_manager = SessionManager()
267
+ project_dir = os.getcwd()
268
+ current_session_id = None
269
+ current_model_name = model_name
270
+ # ---
271
+
262
272
  try:
263
273
  console.print(f"\nInitializing model [bold]{model_name}[/bold]...")
264
274
  # Pass the console object to OpenRouterModel constructor
@@ -275,6 +285,63 @@ def start_interactive_session(model_name: str, console: Console):
275
285
  )
276
286
  return
277
287
 
288
+ # --- Check for existing sessions and offer to resume ---
289
+ existing_sessions = session_manager.list_sessions(project_dir)
290
+ if existing_sessions:
291
+ console.print(
292
+ f"\n[yellow]Found {len(existing_sessions)} previous session(s) for this project:[/yellow]"
293
+ )
294
+ for i, session in enumerate(existing_sessions[:5], 1):
295
+ updated = session.get("updated_at", "")[:19].replace("T", " ")
296
+ messages = session.get("message_count", 0)
297
+ model = session.get("model_name", "unknown")
298
+ console.print(f" {i}. [{updated}] {messages} messages (model: {model})")
299
+
300
+ resume_choice = questionary.select(
301
+ "Resume a previous session?",
302
+ choices=["Start new session", "Resume latest", "Choose session"],
303
+ default="Start new session",
304
+ ).ask()
305
+
306
+ if resume_choice == "Resume latest":
307
+ latest_session = session_manager.get_latest_session(project_dir)
308
+ if latest_session:
309
+ model.chat_history = latest_session["chat_history"]
310
+ current_session_id = latest_session["id"]
311
+ current_model_name = latest_session.get("model_name", model_name)
312
+ console.print(
313
+ f"[green]Resumed session {current_session_id} ({len(model.chat_history)} messages)[/green]\n"
314
+ )
315
+ else:
316
+ console.print("[yellow]No session to resume. Starting new.[/yellow]\n")
317
+ elif resume_choice == "Choose session":
318
+ session_choices = [
319
+ f"{s['id']} ({s['message_count']} msgs, {s['updated_at'][:16]})"
320
+ for s in existing_sessions[:10]
321
+ ]
322
+ session_choices.append("Cancel - start new session")
323
+ chosen = questionary.select(
324
+ "Select a session to resume:",
325
+ choices=session_choices,
326
+ ).ask()
327
+ if chosen and chosen != "Cancel - start new session":
328
+ chosen_id = chosen.split(" (")[0]
329
+ loaded_session = session_manager.load_session(project_dir, chosen_id)
330
+ if loaded_session:
331
+ model.chat_history = loaded_session["chat_history"]
332
+ current_session_id = loaded_session["id"]
333
+ current_model_name = loaded_session.get("model_name", model_name)
334
+ console.print(
335
+ f"[green]Resumed session {current_session_id} ({len(model.chat_history)} messages)[/green]\n"
336
+ )
337
+ else:
338
+ console.print("[red]Failed to load session. Starting new.[/red]\n")
339
+ else:
340
+ console.print("[yellow]Starting new session.[/yellow]\n")
341
+ else:
342
+ console.print("[yellow]Starting new session.[/yellow]\n")
343
+ # --- End Session Resume Check ---
344
+
278
345
  # --- Session Start Message ---
279
346
  console.print("Type '/help' for commands, '/exit' or Ctrl+C to quit.")
280
347
 
@@ -283,10 +350,64 @@ def start_interactive_session(model_name: str, console: Console):
283
350
  user_input = console.input("[bold green]You:[/bold green] ")
284
351
 
285
352
  if user_input.lower() == "/exit":
353
+ # Save session before exiting
354
+ _save_current_session(
355
+ session_manager,
356
+ project_dir,
357
+ model,
358
+ current_model_name,
359
+ current_session_id,
360
+ console,
361
+ )
286
362
  break
287
363
  elif user_input.lower() == "/help":
288
364
  show_help()
289
365
  continue
366
+ elif user_input.lower() == "/sessions":
367
+ _show_sessions(session_manager, project_dir, console)
368
+ continue
369
+ elif user_input.lower().startswith("/load "):
370
+ session_id = user_input[6:].strip()
371
+ loaded = _load_session_by_id(
372
+ session_manager, project_dir, session_id, model, console
373
+ )
374
+ if loaded:
375
+ current_session_id = loaded["id"]
376
+ current_model_name = loaded.get("model_name", model_name)
377
+ continue
378
+ elif user_input.lower() == "/new":
379
+ # Save current session first
380
+ _save_current_session(
381
+ session_manager,
382
+ project_dir,
383
+ model,
384
+ current_model_name,
385
+ current_session_id,
386
+ console,
387
+ )
388
+ # Reset chat history
389
+ model.chat_history = [
390
+ {"role": "system", "content": model.system_instruction},
391
+ {
392
+ "role": "assistant",
393
+ "content": "Okay, I'm ready. Provide the directory context and your request.",
394
+ },
395
+ ]
396
+ current_session_id = None
397
+ console.print("[green]Started new session.[/green]\n")
398
+ continue
399
+ elif user_input.lower() == "/save":
400
+ session_id = _save_current_session(
401
+ session_manager,
402
+ project_dir,
403
+ model,
404
+ current_model_name,
405
+ current_session_id,
406
+ console,
407
+ )
408
+ if session_id:
409
+ current_session_id = session_id
410
+ continue
290
411
 
291
412
  # Display initial "thinking" status - generate handles intermediate ones
292
413
  response_text = model.generate(user_input)
@@ -302,7 +423,26 @@ def start_interactive_session(model_name: str, console: Console):
302
423
  console.print("[bold green]Assistant:[/bold green]")
303
424
  console.print(Markdown(response_text), highlight=True)
304
425
 
426
+ # Auto-save after each exchange
427
+ current_session_id = _auto_save_session(
428
+ session_manager,
429
+ project_dir,
430
+ model,
431
+ current_model_name,
432
+ current_session_id,
433
+ console,
434
+ )
435
+
305
436
  except KeyboardInterrupt:
437
+ # Save session before exiting
438
+ _save_current_session(
439
+ session_manager,
440
+ project_dir,
441
+ model,
442
+ current_model_name,
443
+ current_session_id,
444
+ console,
445
+ )
306
446
  console.print("\n[yellow]Session interrupted. Exiting.[/yellow]")
307
447
  break
308
448
  except Exception as e:
@@ -312,6 +452,96 @@ def start_interactive_session(model_name: str, console: Console):
312
452
  log.error("Error during interactive loop", exc_info=True)
313
453
 
314
454
 
455
+ def _save_current_session(
456
+ session_manager: SessionManager,
457
+ project_dir: str,
458
+ model: OpenRouterModel,
459
+ model_name: str,
460
+ session_id: Optional[str],
461
+ console: Console,
462
+ ) -> Optional[str]:
463
+ """Save the current session. Returns the session ID if saved."""
464
+ # Don't save if there are no user messages
465
+ user_messages = [msg for msg in model.chat_history if msg.get("role") == "user"]
466
+ if not user_messages:
467
+ return None
468
+
469
+ try:
470
+ saved_id = session_manager.save_session(
471
+ project_dir=project_dir,
472
+ chat_history=model.chat_history,
473
+ model_name=model_name,
474
+ session_id=session_id,
475
+ )
476
+ console.print(f"[dim]Session saved: {saved_id}[/dim]")
477
+ return saved_id
478
+ except Exception as e:
479
+ log.error(f"Failed to save session: {e}")
480
+ console.print(f"[red]Warning: Failed to save session: {e}[/red]")
481
+ return None
482
+
483
+
484
+ def _auto_save_session(
485
+ session_manager: SessionManager,
486
+ project_dir: str,
487
+ model: OpenRouterModel,
488
+ model_name: str,
489
+ session_id: Optional[str],
490
+ console: Console,
491
+ ) -> Optional[str]:
492
+ """Auto-save session silently. Returns the session ID."""
493
+ try:
494
+ saved_id = session_manager.save_session(
495
+ project_dir=project_dir,
496
+ chat_history=model.chat_history,
497
+ model_name=model_name,
498
+ session_id=session_id,
499
+ )
500
+ return saved_id
501
+ except Exception as e:
502
+ log.error(f"Auto-save failed: {e}")
503
+ return session_id
504
+
505
+
506
+ def _show_sessions(session_manager: SessionManager, project_dir: str, console: Console):
507
+ """Display sessions for the current project."""
508
+ sessions = session_manager.list_sessions(project_dir)
509
+ if not sessions:
510
+ console.print("[yellow]No sessions found for this project.[/yellow]")
511
+ return
512
+
513
+ console.print(f"\n[bold cyan]Sessions for current project:[/bold cyan]")
514
+ for i, session in enumerate(sessions, 1):
515
+ updated = session.get("updated_at", "")[:19].replace("T", " ")
516
+ messages = session.get("message_count", 0)
517
+ model = session.get("model_name", "unknown")
518
+ console.print(
519
+ f" {i}. ID: [green]{session['id']}[/green] | {updated} | {messages} msgs | {model}"
520
+ )
521
+ console.print("\nUse '/load <session_id>' to resume a session.\n")
522
+
523
+
524
+ def _load_session_by_id(
525
+ session_manager: SessionManager,
526
+ project_dir: str,
527
+ session_id: str,
528
+ model: OpenRouterModel,
529
+ console: Console,
530
+ ) -> Optional[Dict[str, Any]]:
531
+ """Load a session by ID. Returns session data or None."""
532
+ loaded_session = session_manager.load_session(project_dir, session_id)
533
+ if loaded_session:
534
+ model.chat_history = loaded_session["chat_history"]
535
+ console.print(
536
+ f"[green]Loaded session {session_id} ({len(model.chat_history)} messages)[/green]\n"
537
+ )
538
+ return loaded_session
539
+ else:
540
+ console.print(f"[red]Session '{session_id}' not found.[/red]")
541
+ console.print("[yellow]Use '/sessions' to list available sessions.[/yellow]\n")
542
+ return None
543
+
544
+
315
545
  def show_help():
316
546
  """Show help information for interactive mode."""
317
547
  tool_list_formatted = ""
@@ -327,8 +557,12 @@ def show_help():
327
557
  help_text = f""" [bold]Help[/bold]
328
558
 
329
559
  [cyan]Interactive Commands:[/cyan]
330
- /exit
331
- /help
560
+ /exit - Save session and exit
561
+ /help - Show this help message
562
+ /sessions - List sessions for current project
563
+ /load <id> - Load a session by ID
564
+ /new - Start a new session (saves current first)
565
+ /save - Manually save current session
332
566
 
333
567
  [cyan]CLI Commands:[/cyan]
334
568
  lmcode setup KEY
@@ -338,6 +572,10 @@ def show_help():
338
572
 
339
573
  [cyan]Workflow Hint:[/cyan] Analyze -> Plan -> Execute -> Verify -> Summarize
340
574
 
575
+ [cyan]Session Persistence:[/cyan]
576
+ Sessions are automatically saved per project directory.
577
+ Resume previous sessions with /load or on startup.
578
+
341
579
  [cyan]Available Tools:[/cyan]
342
580
  {tool_list_formatted}
343
581
  """
@@ -0,0 +1,283 @@
1
+ """
2
+ Session persistence for LM Code CLI.
3
+ Saves and loads conversation history per project directory.
4
+ """
5
+
6
+ import json
7
+ import hashlib
8
+ import os
9
+ import logging
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import List, Dict, Optional, Any
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class SessionManager:
18
+ """Manages session persistence for conversation history."""
19
+
20
+ def __init__(self, sessions_dir: Optional[str] = None):
21
+ self.sessions_dir = (
22
+ Path(sessions_dir)
23
+ if sessions_dir
24
+ else Path.home() / ".config" / "lm-code" / "sessions"
25
+ )
26
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
27
+ self.current_session_id = None
28
+ self.current_project_dir = None
29
+
30
+ def _get_project_hash(self, project_dir: str) -> str:
31
+ """Generate a unique hash for a project directory path."""
32
+ normalized = os.path.normpath(os.path.abspath(project_dir))
33
+ return hashlib.sha256(normalized.encode()).hexdigest()[:16]
34
+
35
+ def _get_project_sessions_dir(self, project_dir: str) -> Path:
36
+ """Get the sessions directory for a specific project."""
37
+ project_hash = self._get_project_hash(project_dir)
38
+ project_sessions_dir = self.sessions_dir / project_hash
39
+ project_sessions_dir.mkdir(parents=True, exist_ok=True)
40
+ return project_sessions_dir
41
+
42
+ def save_session(
43
+ self,
44
+ project_dir: str,
45
+ chat_history: List[Dict[str, Any]],
46
+ model_name: str,
47
+ session_id: Optional[str] = None,
48
+ ) -> str:
49
+ """
50
+ Save a session to disk.
51
+
52
+ Args:
53
+ project_dir: The project directory path
54
+ chat_history: The chat history list from OpenRouterModel
55
+ model_name: The model being used
56
+ session_id: Optional existing session ID to overwrite
57
+
58
+ Returns:
59
+ The session ID of the saved session
60
+ """
61
+ if session_id is None:
62
+ session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
63
+
64
+ project_sessions_dir = self._get_project_sessions_dir(project_dir)
65
+
66
+ session_data = {
67
+ "id": session_id,
68
+ "project_dir": os.path.abspath(project_dir),
69
+ "model_name": model_name,
70
+ "created_at": datetime.now().isoformat(),
71
+ "updated_at": datetime.now().isoformat(),
72
+ "chat_history": chat_history,
73
+ "message_count": len(
74
+ [
75
+ msg
76
+ for msg in chat_history
77
+ if msg.get("role") in ("user", "assistant")
78
+ ]
79
+ ),
80
+ }
81
+
82
+ session_file = project_sessions_dir / f"{session_id}.json"
83
+
84
+ try:
85
+ with open(session_file, "w", encoding="utf-8") as f:
86
+ json.dump(session_data, f, indent=2, ensure_ascii=False)
87
+ log.info(f"Session saved: {session_id} for project {project_dir}")
88
+ self.current_session_id = session_id
89
+ self.current_project_dir = project_dir
90
+ return session_id
91
+ except Exception as e:
92
+ log.error(f"Failed to save session: {e}", exc_info=True)
93
+ raise
94
+
95
+ def load_session(
96
+ self, project_dir: str, session_id: str
97
+ ) -> Optional[Dict[str, Any]]:
98
+ """
99
+ Load a session from disk.
100
+
101
+ Args:
102
+ project_dir: The project directory path
103
+ session_id: The session ID to load
104
+
105
+ Returns:
106
+ Session data dict or None if not found
107
+ """
108
+ project_sessions_dir = self._get_project_sessions_dir(project_dir)
109
+ session_file = project_sessions_dir / f"{session_id}.json"
110
+
111
+ if not session_file.exists():
112
+ log.warning(f"Session file not found: {session_file}")
113
+ return None
114
+
115
+ try:
116
+ with open(session_file, "r", encoding="utf-8") as f:
117
+ session_data = json.load(f)
118
+ log.info(f"Session loaded: {session_id}")
119
+ self.current_session_id = session_id
120
+ self.current_project_dir = project_dir
121
+ return session_data
122
+ except Exception as e:
123
+ log.error(f"Failed to load session: {e}", exc_info=True)
124
+ return None
125
+
126
+ def list_sessions(self, project_dir: str) -> List[Dict[str, Any]]:
127
+ """
128
+ List all sessions for a project directory.
129
+
130
+ Args:
131
+ project_dir: The project directory path
132
+
133
+ Returns:
134
+ List of session metadata dicts, sorted by most recent first
135
+ """
136
+ project_sessions_dir = self._get_project_sessions_dir(project_dir)
137
+ sessions = []
138
+
139
+ for session_file in project_sessions_dir.glob("*.json"):
140
+ try:
141
+ with open(session_file, "r", encoding="utf-8") as f:
142
+ session_data = json.load(f)
143
+ sessions.append(
144
+ {
145
+ "id": session_data.get("id", session_file.stem),
146
+ "project_dir": session_data.get("project_dir", project_dir),
147
+ "model_name": session_data.get("model_name", "unknown"),
148
+ "created_at": session_data.get("created_at", ""),
149
+ "updated_at": session_data.get("updated_at", ""),
150
+ "message_count": session_data.get("message_count", 0),
151
+ }
152
+ )
153
+ except Exception as e:
154
+ log.warning(f"Failed to read session file {session_file}: {e}")
155
+ continue
156
+
157
+ # Sort by most recent first
158
+ sessions.sort(key=lambda s: s.get("updated_at", ""), reverse=True)
159
+ return sessions
160
+
161
+ def list_all_sessions(self) -> List[Dict[str, Any]]:
162
+ """
163
+ List all sessions across all projects.
164
+
165
+ Returns:
166
+ List of session metadata dicts, sorted by most recent first
167
+ """
168
+ all_sessions = []
169
+
170
+ for project_hash_dir in self.sessions_dir.iterdir():
171
+ if not project_hash_dir.is_dir():
172
+ continue
173
+
174
+ for session_file in project_hash_dir.glob("*.json"):
175
+ try:
176
+ with open(session_file, "r", encoding="utf-8") as f:
177
+ session_data = json.load(f)
178
+ all_sessions.append(
179
+ {
180
+ "id": session_data.get("id", session_file.stem),
181
+ "project_dir": session_data.get("project_dir", "unknown"),
182
+ "model_name": session_data.get("model_name", "unknown"),
183
+ "created_at": session_data.get("created_at", ""),
184
+ "updated_at": session_data.get("updated_at", ""),
185
+ "message_count": session_data.get("message_count", 0),
186
+ }
187
+ )
188
+ except Exception as e:
189
+ log.warning(f"Failed to read session file {session_file}: {e}")
190
+ continue
191
+
192
+ # Sort by most recent first
193
+ all_sessions.sort(key=lambda s: s.get("updated_at", ""), reverse=True)
194
+ return all_sessions
195
+
196
+ def get_latest_session(self, project_dir: str) -> Optional[Dict[str, Any]]:
197
+ """
198
+ Get the most recent session for a project directory.
199
+
200
+ Args:
201
+ project_dir: The project directory path
202
+
203
+ Returns:
204
+ Session data dict or None if no sessions exist
205
+ """
206
+ sessions = self.list_sessions(project_dir)
207
+ if not sessions:
208
+ return None
209
+
210
+ latest_id = sessions[0]["id"]
211
+ return self.load_session(project_dir, latest_id)
212
+
213
+ def delete_session(self, project_dir: str, session_id: str) -> bool:
214
+ """
215
+ Delete a session.
216
+
217
+ Args:
218
+ project_dir: The project directory path
219
+ session_id: The session ID to delete
220
+
221
+ Returns:
222
+ True if deleted successfully, False otherwise
223
+ """
224
+ project_sessions_dir = self._get_project_sessions_dir(project_dir)
225
+ session_file = project_sessions_dir / f"{session_id}.json"
226
+
227
+ if not session_file.exists():
228
+ log.warning(f"Session file not found for deletion: {session_file}")
229
+ return False
230
+
231
+ try:
232
+ session_file.unlink()
233
+ log.info(f"Session deleted: {session_id}")
234
+ if self.current_session_id == session_id:
235
+ self.current_session_id = None
236
+ return True
237
+ except Exception as e:
238
+ log.error(f"Failed to delete session: {e}", exc_info=True)
239
+ return False
240
+
241
+ def update_session(
242
+ self, project_dir: str, session_id: str, chat_history: List[Dict[str, Any]]
243
+ ) -> bool:
244
+ """
245
+ Update an existing session with new chat history.
246
+
247
+ Args:
248
+ project_dir: The project directory path
249
+ session_id: The session ID to update
250
+ chat_history: The updated chat history
251
+
252
+ Returns:
253
+ True if updated successfully, False otherwise
254
+ """
255
+ project_sessions_dir = self._get_project_sessions_dir(project_dir)
256
+ session_file = project_sessions_dir / f"{session_id}.json"
257
+
258
+ if not session_file.exists():
259
+ log.warning(f"Session file not found for update: {session_file}")
260
+ return False
261
+
262
+ try:
263
+ with open(session_file, "r", encoding="utf-8") as f:
264
+ session_data = json.load(f)
265
+
266
+ session_data["chat_history"] = chat_history
267
+ session_data["updated_at"] = datetime.now().isoformat()
268
+ session_data["message_count"] = len(
269
+ [
270
+ msg
271
+ for msg in chat_history
272
+ if msg.get("role") in ("user", "assistant")
273
+ ]
274
+ )
275
+
276
+ with open(session_file, "w", encoding="utf-8") as f:
277
+ json.dump(session_data, f, indent=2, ensure_ascii=False)
278
+
279
+ log.info(f"Session updated: {session_id}")
280
+ return True
281
+ except Exception as e:
282
+ log.error(f"Failed to update session: {e}", exc_info=True)
283
+ return False
@@ -9,9 +9,7 @@ import json
9
9
  def count_tokens(text):
10
10
  """
11
11
  Count the number of tokens in a text string.
12
-
13
- This is a rough estimate for Gemini 2.5 Pro, using GPT-4 tokenizer as a proxy.
14
- For production, you'd want to use model-specific token counting.
12
+ Uses GPT-4 tokenizer as a proxy.
15
13
  """
16
14
  try:
17
15
  encoding = tiktoken.encoding_for_model("gpt-4")
@@ -1,43 +0,0 @@
1
- """
2
- Gemini model integration for the CLI tool.
3
- """
4
-
5
- import logging
6
- import time
7
- from rich.console import Console
8
- from rich.panel import Panel
9
- import questionary
10
-
11
- from ..utils import count_tokens
12
- from ..tools import get_tool, AVAILABLE_TOOLS
13
-
14
- # Setup logging (basic config, consider moving to main.py)
15
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s')
16
- log = logging.getLogger(__name__)
17
-
18
- MAX_AGENT_ITERATIONS = 10
19
- CONTEXT_TRUNCATION_THRESHOLD_TOKENS = 800000 # Example token limit
20
-
21
-
22
- class GeminiModel:
23
- """Interface for Gemini models using native function calling agentic loop."""
24
-
25
- def __init__(self, console: Console):
26
- """Initialize the Gemini model interface."""
27
- self.console = console
28
-
29
- # --- Tool Definition ---
30
- self.function_declarations = None # Tools have been removed
31
- # ---
32
-
33
- # --- System Prompt (Native Functions & Planning) ---
34
- self.system_instruction = "Initialize system prompt."
35
- # ---
36
-
37
- # --- Initialize Persistent History ---
38
- self.chat_history = [
39
- {'role': 'user', 'parts': [self.system_instruction]},
40
- {'role': 'model', 'parts': ["Okay, I'm ready. Provide the directory context and your request."]}
41
- ]
42
- log.info("Initialized persistent chat history.")
43
- # ---
File without changes
File without changes
File without changes
File without changes
File without changes