cli-memory-os 0.1.7__tar.gz → 0.1.9__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 (69) hide show
  1. {cli_memory_os-0.1.7/cli_memory_os.egg-info → cli_memory_os-0.1.9}/PKG-INFO +1 -1
  2. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/tui/app.py +84 -43
  3. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9/cli_memory_os.egg-info}/PKG-INFO +1 -1
  4. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/context_builder.py +2 -1
  5. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/llm.py +35 -0
  6. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/pyproject.toml +1 -1
  7. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_sync_ask_cli.py +30 -0
  8. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_tui.py +14 -14
  9. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/LICENSE +0 -0
  10. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/MANIFEST.in +0 -0
  11. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/README.md +0 -0
  12. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/__init__.py +0 -0
  13. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/__init__.py +0 -0
  14. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/ask.py +0 -0
  15. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/benchmark.py +0 -0
  16. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/config_cmd.py +0 -0
  17. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/doctor.py +0 -0
  18. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/export.py +0 -0
  19. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/import_cmd.py +0 -0
  20. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/init.py +0 -0
  21. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/logs.py +0 -0
  22. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/monitor.py +0 -0
  23. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/plugins.py +0 -0
  24. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/restart.py +0 -0
  25. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/start.py +0 -0
  26. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/status.py +0 -0
  27. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/stop.py +0 -0
  28. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/sync.py +0 -0
  29. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/version.py +0 -0
  30. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/commands/workspace.py +0 -0
  31. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/main.py +0 -0
  32. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/parser.py +0 -0
  33. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli/tui/__init__.py +0 -0
  34. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli_memory_os.egg-info/SOURCES.txt +0 -0
  35. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli_memory_os.egg-info/dependency_links.txt +0 -0
  36. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli_memory_os.egg-info/entry_points.txt +0 -0
  37. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli_memory_os.egg-info/requires.txt +0 -0
  38. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/cli_memory_os.egg-info/top_level.txt +0 -0
  39. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/__init__.py +0 -0
  40. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/base.py +0 -0
  41. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/github.py +0 -0
  42. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/gmail.py +0 -0
  43. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/notion.py +0 -0
  44. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/connectors/registry.py +0 -0
  45. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/__init__.py +0 -0
  46. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/chunker.py +0 -0
  47. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/embedder.py +0 -0
  48. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/core/vector_store.py +0 -0
  49. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/__init__.py +0 -0
  50. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/compose.py +0 -0
  51. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/config.py +0 -0
  52. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/docker.py +0 -0
  53. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/health.py +0 -0
  54. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/observability.py +0 -0
  55. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/infrastructure/workspace.py +0 -0
  56. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/models/__init__.py +0 -0
  57. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/models/memory.py +0 -0
  58. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/setup.cfg +0 -0
  59. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/storage/__init__.py +0 -0
  60. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/storage/db.py +0 -0
  61. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/storage/graph.py +0 -0
  62. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/storage/schema.sql +0 -0
  63. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/storage/tech_detector.py +0 -0
  64. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_cli_integration.py +0 -0
  65. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_commands_phase2.py +0 -0
  66. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_compose_manager.py +0 -0
  67. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_first_time_user_experience.py +0 -0
  68. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_infrastructure_stabilization.py +0 -0
  69. {cli_memory_os-0.1.7 → cli_memory_os-0.1.9}/tests/test_v1_features.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-memory-os
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: CLI-based Personal Knowledge Operating System
5
5
  Author-email: Anirudh T <anirudh200584@gmail.com>
6
6
  License-Expression: MIT
@@ -159,16 +159,16 @@ class Sidebar(Static):
159
159
  f"Gmail: {self.gmail_status}\n"
160
160
  f"Notion: {self.notion_status}\n\n"
161
161
  f"[bold cyan]⌨️ Shortcuts[/bold cyan]\n"
162
- f"[cyan]C[/cyan]: Chat\n"
163
- f"[cyan]S[/cyan]: Sync\n"
164
- f"[cyan]D[/cyan]: Doctor\n"
165
- f"[cyan]G[/cyan]: Graph\n"
166
- f"[cyan]F[/cyan]: Find / Search\n"
167
- f"[cyan]M[/cyan]: Monitor\n"
168
- f"[cyan]E[/cyan]: Settings\n"
169
- f"[cyan]Q[/cyan]: Quit"
162
+ f"[cyan]F1[/cyan] : Chat\n"
163
+ f"[cyan]F2[/cyan] : Sync\n"
164
+ f"[cyan]F3[/cyan] : Doctor\n"
165
+ f"[cyan]F4[/cyan] : Graph\n"
166
+ f"[cyan]F5[/cyan] : Search\n"
167
+ f"[cyan]F6[/cyan] : Monitor\n"
168
+ f"[cyan]F7[/cyan] : Settings\n"
169
+ f"[cyan]F10[/cyan]: Quit"
170
170
  )
171
- self.update(Panel(content, title="Memory-OS", border_style="cyan"))
171
+ self.update(Panel(content, title="Memory-OS", border_style="#1F2937"))
172
172
 
173
173
 
174
174
  class ChatPanel(Static):
@@ -177,8 +177,21 @@ class ChatPanel(Static):
177
177
  def compose(self) -> ComposeResult:
178
178
  with Vertical(id="chat_container"):
179
179
  with VerticalScroll(id="chat_history_scroll"):
180
+ welcome_md = (
181
+ "```\n"
182
+ " __ __ ____ ____\n"
183
+ "| \\/ | ___ _ __ ___ ___ _ __ _ _ / ___| / ___|\n"
184
+ "| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| | | | \\___ \\| |\n"
185
+ "| | | | __/ | | | | | (_) | | | |_| | ___) | |___\n"
186
+ "|_| |_|\\___|_| |_| |_|\\___/|_| \\__, | |____/ \\____|\n"
187
+ " |___/\n"
188
+ "```\n"
189
+ "**Grounded Personal Knowledge Operating System**\n\n"
190
+ "Ask questions grounded against your connected workspace: repositories, documentation files, and synced emails.\n\n"
191
+ "💡 *Tip: Try asking: 'tell me about my projects' or 'what repositories use Python?'*"
192
+ )
180
193
  yield Static(
181
- RichMarkdown("# Memory-OS RAG Assistant\nAsk any question grounded against your indexed sources! Try asking about your repositories, emails, or technologies used."),
194
+ RichMarkdown(welcome_md),
182
195
  id="welcome_message"
183
196
  )
184
197
  with Horizontal(id="input_bar"):
@@ -212,14 +225,15 @@ class ChatPanel(Static):
212
225
  except Exception:
213
226
  pass
214
227
 
215
- # Append User Message Bubble
228
+ # Append User Message
216
229
  history_scroll = self.query_one("#chat_history_scroll")
230
+ user_markup = f"[bold green]● You[/bold green]\n\n{user_text}"
217
231
  history_scroll.mount(
218
- Static(Panel(user_text, title="[bold green]You[/bold green]", border_style="green"), classes="chat_msg_user")
232
+ Static(user_markup, classes="chat_msg_user")
219
233
  )
220
234
 
221
- # Append Assistant Loading/Streaming Message Bubble
222
- agent_msg = Static(Panel("Thinking...", title="[bold cyan]Memory-OS[/bold cyan]", border_style="cyan"), classes="chat_msg_agent")
235
+ # Append Assistant Loading/Streaming Message
236
+ agent_msg = Static("[bold cyan]Memory-OS[/bold cyan]\n\n[italic dim]Thinking...[/italic dim]", classes="chat_msg_agent")
223
237
  history_scroll.mount(agent_msg)
224
238
  self.scroll_to_bottom()
225
239
 
@@ -240,7 +254,7 @@ class ChatPanel(Static):
240
254
  try:
241
255
  gen = await loop.run_in_executor(executor, get_generator)
242
256
  except Exception as e:
243
- response_widget.update(Panel(f"Error: {e}", title="[bold red]Error[/bold red]", border_style="red"))
257
+ response_widget.update(f"[bold red]Error[/bold red]\n\n{e}")
244
258
  self.enable_input()
245
259
  return
246
260
 
@@ -255,14 +269,14 @@ class ChatPanel(Static):
255
269
  except StopIteration:
256
270
  break
257
271
  except Exception as e:
258
- response_widget.update(Panel(f"Error: {e}", title="[bold red]Error[/bold red]", border_style="red"))
272
+ response_widget.update(f"[bold red]Error[/bold red]\n\n{e}")
259
273
  break
260
274
 
261
275
  if item["type"] == "diagnostics":
262
276
  query_class = item["data"].get("query_class", "Unknown")
263
277
  elif item["type"] == "token":
264
278
  answer_text += item["content"]
265
- response_widget.update(Panel(answer_text, title=f"[bold cyan]Memory-OS ({query_class})[/bold cyan]", border_style="cyan"))
279
+ response_widget.update(f"[bold cyan]Memory-OS ({query_class})[/bold cyan]\n\n{answer_text}")
266
280
  self.scroll_to_bottom()
267
281
  elif item["type"] == "done":
268
282
  sources = item["sources"]
@@ -280,7 +294,7 @@ class ChatPanel(Static):
280
294
  metadata.append(f"**Confidence**: {confidence:.1f}")
281
295
  final_md += "\n\n---\n" + "\n".join(metadata)
282
296
 
283
- response_widget.update(Panel(RichMarkdown(final_md), title=f"[bold cyan]Memory-OS ({query_class})[/bold cyan]", border_style="cyan"))
297
+ response_widget.update(RichMarkdown(f"### Memory-OS ({query_class})\n\n{final_md}"))
284
298
  self.scroll_to_bottom()
285
299
 
286
300
  self.enable_input()
@@ -354,7 +368,7 @@ class DoctorPanel(Static):
354
368
  report.append("=" * 45)
355
369
  report.append(f"Summary: [bold]{healthy_count}/{len(results)}[/bold] services are functional.")
356
370
 
357
- results_static.update(Panel("\n".join(report), border_style="cyan"))
371
+ results_static.update(Panel("\n".join(report), border_style="#1F2937"))
358
372
  btn.disabled = False
359
373
 
360
374
 
@@ -453,7 +467,7 @@ class SyncPanel(Static):
453
467
  return mystdout.getvalue()
454
468
 
455
469
  output = await loop.run_in_executor(executor, run_sync_redirected)
456
- log_widget.update(Panel(output, title="Sync Log Output", border_style="cyan"))
470
+ log_widget.update(Panel(output, title="Sync Log Output", border_style="#1F2937"))
457
471
 
458
472
  btn_all.disabled = False
459
473
  btn_rebuild.disabled = False
@@ -530,7 +544,7 @@ class GraphPanel(Static):
530
544
  for r_name, lang in repos:
531
545
  stats_text += f" - [white]{r_name}[/white] : {lang or 'Unknown'}\n"
532
546
 
533
- self.query_one("#graph_stats", Static).update(Panel(stats_text, border_style="cyan"))
547
+ self.query_one("#graph_stats", Static).update(Panel(stats_text, border_style="#1F2937"))
534
548
 
535
549
 
536
550
  class SearchPanel(Static):
@@ -640,7 +654,7 @@ class MonitorPanel(Static):
640
654
  if l_times:
641
655
  stats_text += f" - Last Groq response: {l_times[-1]:.2f}s\n"
642
656
 
643
- self.query_one("#monitor_stats", Static).update(Panel(stats_text, border_style="cyan"))
657
+ self.query_one("#monitor_stats", Static).update(Panel(stats_text, border_style="#1F2937"))
644
658
 
645
659
 
646
660
  class SettingsPanel(Static):
@@ -668,7 +682,7 @@ class SettingsPanel(Static):
668
682
  f"Groq Model: {cfg.get('groq', {}).get('model', 'llama-3.3-70b-versatile')}\n"
669
683
  f"Embedding Model: {cfg.get('embeddings', {}).get('model', 'all-MiniLM-L6-v2')}\n"
670
684
  )
671
- self.query_one("#settings_details", Static).update(Panel(details, border_style="cyan"))
685
+ self.query_one("#settings_details", Static).update(Panel(details, border_style="#1F2937"))
672
686
 
673
687
 
674
688
  class MemoryOSTUIApp(App):
@@ -677,7 +691,7 @@ class MemoryOSTUIApp(App):
677
691
  TITLE = "Memory-OS Terminal UI"
678
692
  CSS = """
679
693
  Screen {
680
- background: #0f1419;
694
+ background: #0B0F19;
681
695
  }
682
696
 
683
697
  #main_layout {
@@ -687,8 +701,8 @@ class MemoryOSTUIApp(App):
687
701
  Sidebar {
688
702
  width: 35;
689
703
  height: 100%;
690
- background: #141923;
691
- border-right: solid cyan;
704
+ background: #111622;
705
+ border-right: solid #1F2937;
692
706
  }
693
707
 
694
708
  ContentSwitcher {
@@ -703,19 +717,24 @@ class MemoryOSTUIApp(App):
703
717
 
704
718
  #chat_history_scroll {
705
719
  height: 1fr;
706
- border: solid cyan;
720
+ border: solid #1F2937;
707
721
  margin-bottom: 1;
708
722
  padding: 1 2;
723
+ background: #0B0F19;
709
724
  }
710
725
 
711
726
  .chat_msg_user {
712
727
  margin: 1 0;
713
- align: right middle;
728
+ background: #161B22;
729
+ border-left: solid #10B981;
730
+ padding: 1 2;
714
731
  }
715
732
 
716
733
  .chat_msg_agent {
717
734
  margin: 1 0;
718
- align: left middle;
735
+ background: #111622;
736
+ border-left: solid #3B82F6;
737
+ padding: 1 2;
719
738
  }
720
739
 
721
740
  #input_bar {
@@ -725,14 +744,21 @@ class MemoryOSTUIApp(App):
725
744
 
726
745
  #chat_input {
727
746
  width: 1fr;
728
- background: #1c2333;
729
- border: solid cyan;
747
+ background: #161B22;
748
+ border: solid #1F2937;
730
749
  color: white;
731
750
  }
732
751
 
733
752
  #chat_send_btn {
734
753
  width: 12;
735
754
  margin-left: 1;
755
+ background: #1F2937;
756
+ color: white;
757
+ border: none;
758
+ }
759
+
760
+ #chat_send_btn:hover {
761
+ background: #3B82F6;
736
762
  }
737
763
 
738
764
  #search_bar {
@@ -742,25 +768,40 @@ class MemoryOSTUIApp(App):
742
768
 
743
769
  #search_query_input {
744
770
  width: 1fr;
745
- background: #1c2333;
746
- border: solid cyan;
771
+ background: #161B22;
772
+ border: solid #1F2937;
747
773
  color: white;
748
774
  }
749
775
 
750
776
  #search_run_btn {
751
777
  width: 12;
752
778
  margin-left: 1;
779
+ background: #1F2937;
780
+ color: white;
781
+ border: none;
782
+ }
783
+
784
+ #search_run_btn:hover {
785
+ background: #3B82F6;
753
786
  }
754
787
 
755
788
  #doctor_results_scroll, #sync_results_scroll, #search_results_scroll {
756
789
  height: 1fr;
757
- border: solid cyan;
790
+ border: solid #1F2937;
758
791
  margin-top: 1;
759
792
  padding: 1 2;
793
+ background: #0B0F19;
760
794
  }
761
795
 
762
796
  Button {
763
797
  min-width: 15;
798
+ background: #1F2937;
799
+ color: white;
800
+ border: none;
801
+ }
802
+
803
+ Button:hover {
804
+ background: #3B82F6;
764
805
  }
765
806
 
766
807
  Label {
@@ -769,15 +810,15 @@ class MemoryOSTUIApp(App):
769
810
  """
770
811
 
771
812
  BINDINGS = [
772
- Binding("ctrl+b", "toggle_sidebar", "Toggle Sidebar"),
773
- Binding("c", "switch_view('chat')", "Chat Screen"),
774
- Binding("s", "switch_view('sync')", "Sync Screen"),
775
- Binding("d", "switch_view('doctor')", "Doctor Screen"),
776
- Binding("g", "switch_view('graph')", "Graph Screen"),
777
- Binding("f", "switch_view('search')", "Search Screen"),
778
- Binding("m", "switch_view('monitor')", "Monitor Screen"),
779
- Binding("e", "switch_view('settings')", "Settings Screen"),
780
- Binding("q", "quit", "Quit"),
813
+ Binding("ctrl+b", "toggle_sidebar", "Sidebar"),
814
+ Binding("f1", "switch_view('chat')", "Chat"),
815
+ Binding("f2", "switch_view('sync')", "Sync"),
816
+ Binding("f3", "switch_view('doctor')", "Doctor"),
817
+ Binding("f4", "switch_view('graph')", "Graph"),
818
+ Binding("f5", "switch_view('search')", "Search"),
819
+ Binding("f6", "switch_view('monitor')", "Monitor"),
820
+ Binding("f7", "switch_view('settings')", "Settings"),
821
+ Binding("f10", "quit", "Quit"),
781
822
  ]
782
823
 
783
824
  sidebar_visible = reactive(True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-memory-os
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: CLI-based Personal Knowledge Operating System
5
5
  Author-email: Anirudh T <anirudh200584@gmail.com>
6
6
  License-Expression: MIT
@@ -50,7 +50,8 @@ class ContextBuilder:
50
50
  return "Documentation Question"
51
51
 
52
52
  # 5. Cross Repository Question
53
- if "cross repository" in query_lower or "multiple repositories" in query_lower or "across all projects" in query_lower:
53
+ cross_keywords = ["projects", "repositories", "repos", "cross repository", "multiple repositories", "across all", "all my"]
54
+ if any(k in query_lower for k in cross_keywords) and not repo_filter:
54
55
  return "Cross Repository Question"
55
56
 
56
57
  # Default
@@ -86,6 +86,39 @@ def query_llm_with_retry(messages: list, model: str = None, max_retries: int = 3
86
86
  time.sleep(0.5)
87
87
  return ""
88
88
 
89
+ def inject_repository_summary_chunk(formatted_chunks: list, query_class: str) -> list:
90
+ """If the query is a cross-repository query, pre-append a summary chunk listing all repos."""
91
+ if query_class != "Cross Repository Question":
92
+ return formatted_chunks
93
+
94
+ try:
95
+ from storage.db import get_connection
96
+ conn = get_connection()
97
+ cursor = conn.cursor()
98
+ cursor.execute("SELECT repo_name, description, language FROM repositories")
99
+ all_repos = cursor.fetchall()
100
+ conn.close()
101
+
102
+ if all_repos:
103
+ repo_list_lines = ["All Connected Repositories:"]
104
+ for r in all_repos:
105
+ desc_str = f" - Description: {r[1]}" if r[1] else ""
106
+ lang_str = f" (Language: {r[2]})" if r[2] else ""
107
+ repo_list_lines.append(f"- {r[0]}{lang_str}{desc_str}")
108
+ repo_list_str = "\n".join(repo_list_lines)
109
+
110
+ # Pre-append to formatted_chunks with a high score to prevent trimming
111
+ formatted_chunks.insert(0, {
112
+ "text": repo_list_str,
113
+ "score": 999.0,
114
+ "type": "repository_summary"
115
+ })
116
+ except Exception as e:
117
+ import logging
118
+ logging.getLogger("llm").error(f"Failed to generate repository summary context chunk: {e}")
119
+
120
+ return formatted_chunks
121
+
89
122
  def run_hybrid_rag(question: str) -> dict:
90
123
  import time
91
124
  import logging
@@ -155,6 +188,7 @@ def run_hybrid_rag(question: str) -> dict:
155
188
  formatted_chunks, sources, repos, num_vector, num_keyword, num_graph, after_dedup = builder.build_context(
156
189
  question, vector_results, keyword_results, graph_results, repo_filter=repo_filter, query_class=query_class
157
190
  )
191
+ formatted_chunks = inject_repository_summary_chunk(formatted_chunks, query_class)
158
192
 
159
193
  system_prompt = (
160
194
  "You are an assistant answering ONLY from the supplied retrieved context.\n\n"
@@ -408,6 +442,7 @@ def run_hybrid_rag_stream(question: str):
408
442
  formatted_chunks, sources, repos, num_vector, num_keyword, num_graph, after_dedup = builder.build_context(
409
443
  question, vector_results, keyword_results, graph_results, repo_filter=repo_filter, query_class=query_class
410
444
  )
445
+ formatted_chunks = inject_repository_summary_chunk(formatted_chunks, query_class)
411
446
 
412
447
  system_prompt = (
413
448
  "You are an assistant answering ONLY from the supplied retrieved context.\n\n"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cli-memory-os"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "CLI-based Personal Knowledge Operating System"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -106,6 +106,36 @@ class TestSyncAskCLI(unittest.TestCase):
106
106
  # Verify that natural language query gets routed to ask and calls run_and_print_ask
107
107
  mock_run_and_print_ask.assert_called_once_with("What projects use Python?")
108
108
 
109
+ @patch("storage.db.get_connection")
110
+ def test_inject_repository_summary_chunk(self, mock_get_connection):
111
+ """Test repository summary chunk is injected for cross-repository questions."""
112
+ from core.llm import inject_repository_summary_chunk
113
+
114
+ # Mock database cursor to return some repositories
115
+ mock_conn = MagicMock()
116
+ mock_cursor = MagicMock()
117
+ mock_cursor.fetchall.return_value = [
118
+ ("Bug-Tracker", "Bug tracking system", "JavaScript"),
119
+ ("Memory-OS", "PKOS", "Python")
120
+ ]
121
+ mock_conn.cursor.return_value = mock_cursor
122
+ mock_get_connection.return_value = mock_conn
123
+
124
+ formatted_chunks = [
125
+ {"text": "Some chunk text", "score": 0.5, "type": "document"}
126
+ ]
127
+
128
+ # Calling with non-cross-repository query class should do nothing
129
+ res1 = inject_repository_summary_chunk(list(formatted_chunks), "General Knowledge Question")
130
+ self.assertEqual(len(res1), 1)
131
+
132
+ # Calling with Cross Repository Question should inject summary chunk
133
+ res2 = inject_repository_summary_chunk(list(formatted_chunks), "Cross Repository Question")
134
+ self.assertEqual(len(res2), 2)
135
+ self.assertEqual(res2[0]["type"], "repository_summary")
136
+ self.assertIn("Bug-Tracker", res2[0]["text"])
137
+ self.assertIn("Memory-OS", res2[0]["text"])
138
+
109
139
 
110
140
  if __name__ == "__main__":
111
141
  unittest.main()
@@ -47,39 +47,39 @@ async def test_tui_view_switching():
47
47
  # Unfocus input before pressing hotkeys
48
48
  app.set_focus(None)
49
49
 
50
- # Press 's' to go to Sync
50
+ # Press 'f2' to go to Sync
51
51
  app.set_focus(None)
52
- await pilot.press("s")
52
+ await pilot.press("f2")
53
53
  assert switcher.current == "sync"
54
54
 
55
- # Press 'd' to go to Doctor
55
+ # Press 'f3' to go to Doctor
56
56
  app.set_focus(None)
57
- await pilot.press("d")
57
+ await pilot.press("f3")
58
58
  assert switcher.current == "doctor"
59
59
 
60
- # Press 'g' to go to Graph
60
+ # Press 'f4' to go to Graph
61
61
  app.set_focus(None)
62
- await pilot.press("g")
62
+ await pilot.press("f4")
63
63
  assert switcher.current == "graph"
64
64
 
65
- # Press 'f' to go to Search
65
+ # Press 'f5' to go to Search
66
66
  app.set_focus(None)
67
- await pilot.press("f")
67
+ await pilot.press("f5")
68
68
  assert switcher.current == "search"
69
69
 
70
- # Press 'm' to go to Monitor
70
+ # Press 'f6' to go to Monitor
71
71
  app.set_focus(None)
72
- await pilot.press("m")
72
+ await pilot.press("f6")
73
73
  assert switcher.current == "monitor"
74
74
 
75
- # Press 'e' to go to Settings
75
+ # Press 'f7' to go to Settings
76
76
  app.set_focus(None)
77
- await pilot.press("e")
77
+ await pilot.press("f7")
78
78
  assert switcher.current == "settings"
79
79
 
80
- # Press 'c' to return to Chat
80
+ # Press 'f1' to return to Chat
81
81
  app.set_focus(None)
82
- await pilot.press("c")
82
+ await pilot.press("f1")
83
83
  assert switcher.current == "chat"
84
84
 
85
85
 
File without changes
File without changes
File without changes
File without changes
File without changes