cli-memory-os 0.1.2__tar.gz → 0.1.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.
Files changed (66) hide show
  1. {cli_memory_os-0.1.2/cli_memory_os.egg-info → cli_memory_os-0.1.4}/PKG-INFO +1 -1
  2. cli_memory_os-0.1.4/cli/commands/ask.py +67 -0
  3. cli_memory_os-0.1.4/cli/commands/sync.py +70 -0
  4. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/main.py +14 -29
  5. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4/cli_memory_os.egg-info}/PKG-INFO +1 -1
  6. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli_memory_os.egg-info/SOURCES.txt +3 -0
  7. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli_memory_os.egg-info/top_level.txt +1 -0
  8. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/github.py +28 -15
  9. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/gmail.py +48 -11
  10. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/vector_store.py +2 -2
  11. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/pyproject.toml +1 -1
  12. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/tests/test_first_time_user_experience.py +1 -0
  13. cli_memory_os-0.1.4/tests/test_sync_ask_cli.py +111 -0
  14. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/LICENSE +0 -0
  15. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/MANIFEST.in +0 -0
  16. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/README.md +0 -0
  17. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/__init__.py +0 -0
  18. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/__init__.py +0 -0
  19. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/benchmark.py +0 -0
  20. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/config_cmd.py +0 -0
  21. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/doctor.py +0 -0
  22. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/export.py +0 -0
  23. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/import_cmd.py +0 -0
  24. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/init.py +0 -0
  25. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/logs.py +0 -0
  26. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/monitor.py +0 -0
  27. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/plugins.py +0 -0
  28. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/restart.py +0 -0
  29. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/start.py +0 -0
  30. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/status.py +0 -0
  31. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/stop.py +0 -0
  32. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/version.py +0 -0
  33. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/commands/workspace.py +0 -0
  34. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli/parser.py +0 -0
  35. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli_memory_os.egg-info/dependency_links.txt +0 -0
  36. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli_memory_os.egg-info/entry_points.txt +0 -0
  37. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/cli_memory_os.egg-info/requires.txt +0 -0
  38. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/__init__.py +0 -0
  39. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/base.py +0 -0
  40. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/notion.py +0 -0
  41. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/connectors/registry.py +0 -0
  42. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/__init__.py +0 -0
  43. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/chunker.py +0 -0
  44. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/context_builder.py +0 -0
  45. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/embedder.py +0 -0
  46. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/core/llm.py +0 -0
  47. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/__init__.py +0 -0
  48. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/compose.py +0 -0
  49. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/config.py +0 -0
  50. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/docker.py +0 -0
  51. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/health.py +0 -0
  52. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/observability.py +0 -0
  53. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/infrastructure/workspace.py +0 -0
  54. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/models/__init__.py +0 -0
  55. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/models/memory.py +0 -0
  56. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/setup.cfg +0 -0
  57. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/storage/__init__.py +0 -0
  58. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/storage/db.py +0 -0
  59. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/storage/graph.py +0 -0
  60. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/storage/schema.sql +0 -0
  61. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/storage/tech_detector.py +0 -0
  62. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/tests/test_cli_integration.py +0 -0
  63. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/tests/test_commands_phase2.py +0 -0
  64. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/tests/test_compose_manager.py +0 -0
  65. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/tests/test_infrastructure_stabilization.py +0 -0
  66. {cli_memory_os-0.1.2 → cli_memory_os-0.1.4}/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.2
3
+ Version: 0.1.4
4
4
  Summary: CLI-based Personal Knowledge Operating System
5
5
  Author-email: Anirudh T <anirudh200584@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,67 @@
1
+ """
2
+ Command: memory-os ask
3
+
4
+ Queries the RAG pipeline with a question.
5
+ """
6
+
7
+ import sys
8
+ import sqlite3
9
+ from core.llm import run_hybrid_rag
10
+ from storage.db import init_db
11
+
12
+
13
+ def run_and_print_ask(query: str):
14
+ """Run RAG query pipeline and print the structured result blocks."""
15
+ rag_res = run_hybrid_rag(query)
16
+
17
+ print("========================================")
18
+ print("ANSWER")
19
+ print("========================================")
20
+ print(rag_res["answer"])
21
+ print("\n========================================")
22
+ print("SOURCES")
23
+ print("========================================")
24
+ if rag_res.get("sources"):
25
+ for s in rag_res["sources"]:
26
+ print(f"- {s}")
27
+ else:
28
+ print("None")
29
+ print("\n========================================")
30
+ print("REPOSITORIES USED")
31
+ print("========================================")
32
+ if rag_res.get("repositories"):
33
+ for r in rag_res["repositories"]:
34
+ print(f"- {r}")
35
+ else:
36
+ print("None")
37
+ print("\n========================================")
38
+ confidence = rag_res.get("confidence", 0.0)
39
+ print(f"Confidence: {confidence:.2f}")
40
+ print("========================================")
41
+
42
+
43
+ def execute(args):
44
+ """Run the ask command parser handler."""
45
+ # Ensure database is initialized
46
+ try:
47
+ init_db()
48
+ except sqlite3.OperationalError as e:
49
+ print("----------------------------------")
50
+ print("Workspace has not been initialized.")
51
+ print("")
52
+ print("Run:")
53
+ print("memory-os init")
54
+ print("----------------------------------")
55
+ sys.exit(1)
56
+
57
+ if not args.question:
58
+ print("Usage: memory-os ask <question>")
59
+ sys.exit(1)
60
+
61
+ query = " ".join(args.question)
62
+
63
+ try:
64
+ run_and_print_ask(query)
65
+ except Exception as e:
66
+ print(f"❌ Error executing query: {e}")
67
+ sys.exit(1)
@@ -0,0 +1,70 @@
1
+ """
2
+ Command: memory-os sync
3
+
4
+ Synchronizes data from connected sources (GitHub, Gmail, Notion),
5
+ chunks documents, generates embeddings, and syncs databases.
6
+ """
7
+
8
+ import sys
9
+ import time
10
+ import logging
11
+ import sqlite3
12
+ from storage.db import init_db, clear_all
13
+ from core.vector_store import run_reindexing, close_qdrant_client
14
+ from storage.graph import GraphStore
15
+ from connectors.github import sync_github
16
+ from connectors.gmail import sync_gmail
17
+ from connectors.notion import sync_notion
18
+
19
+
20
+ def execute(args):
21
+ """Run the synchronization pipeline."""
22
+ # Ensure database is initialized
23
+ try:
24
+ init_db()
25
+ except sqlite3.OperationalError as e:
26
+ print("----------------------------------")
27
+ print("Workspace has not been initialized.")
28
+ print("")
29
+ print("Run:")
30
+ print("memory-os init")
31
+ print("----------------------------------")
32
+ sys.exit(1)
33
+
34
+ start_time = time.perf_counter()
35
+
36
+ rebuild = getattr(args, "rebuild", False)
37
+ source = getattr(args, "source", None)
38
+
39
+ if rebuild:
40
+ print("Performing full rebuild reset...")
41
+ clear_all()
42
+ close_qdrant_client()
43
+ run_reindexing()
44
+ GraphStore().clear_graph()
45
+
46
+ # Sync based on source selection
47
+ if source == "github":
48
+ print("Syncing GitHub source...")
49
+ sync_github()
50
+ elif source == "gmail":
51
+ print("Syncing Gmail source...")
52
+ sync_gmail()
53
+ elif source == "notion":
54
+ print("Syncing Notion source...")
55
+ sync_notion()
56
+ else:
57
+ print("Syncing all sources (GitHub, Gmail, Notion)...")
58
+ sync_github()
59
+ sync_gmail()
60
+ sync_notion()
61
+
62
+ print("Running document chunking and vector reindexing...")
63
+ run_reindexing()
64
+
65
+ print("Extracting and syncing knowledge graph relationships...")
66
+ GraphStore().extract_and_sync_graph()
67
+
68
+ duration = time.perf_counter() - start_time
69
+ print(f"\nSync complete. Total Duration: {duration:.2f}s")
70
+ logging.getLogger("main").info(f"Sync completed in {duration:.2f}s")
@@ -115,7 +115,6 @@ def run_interactive():
115
115
  close_qdrant_client,
116
116
  )
117
117
  from core.embedder import Embedder
118
- from core.llm import run_hybrid_rag
119
118
  from storage.graph import GraphStore
120
119
  from connectors.github import sync_github
121
120
  from connectors.gmail import sync_gmail
@@ -283,30 +282,11 @@ def run_interactive():
283
282
  if not arg:
284
283
  print("Usage: ask <question>")
285
284
  else:
286
- rag_res = run_hybrid_rag(arg)
287
- print("========================================")
288
- print("ANSWER")
289
- print("========================================")
290
- print(rag_res["answer"])
291
- print("\n========================================")
292
- print("SOURCES")
293
- print("========================================")
294
- if rag_res["sources"]:
295
- for s in rag_res["sources"]:
296
- print(f"- {s}")
297
- else:
298
- print("None")
299
- print("\n========================================")
300
- print("REPOSITORIES USED")
301
- print("========================================")
302
- if rag_res["repositories"]:
303
- for r in rag_res["repositories"]:
304
- print(f"- {r}")
305
- else:
306
- print("None")
307
- print("\n========================================")
308
- print(f"Confidence: {rag_res['confidence']:.2f}")
309
- print("========================================")
285
+ from cli.commands.ask import run_and_print_ask
286
+ try:
287
+ run_and_print_ask(arg)
288
+ except Exception as e:
289
+ print(f"❌ Error executing query: {e}")
310
290
 
311
291
  elif cmd == "repo-info":
312
292
  if not arg:
@@ -441,11 +421,16 @@ def route_command(args):
441
421
  elif command == "logs":
442
422
  from cli.commands.logs import execute
443
423
  execute(args)
424
+ elif command == "sync":
425
+ from cli.commands.sync import execute
426
+ execute(args)
427
+ elif command == "ask":
428
+ from cli.commands.ask import execute
429
+ execute(args)
444
430
  else:
445
- # For commands not yet implemented, show a helpful message
446
- print(f"Command '{command}' is not yet implemented.")
447
- print("Run 'memory-os --help' for available commands.")
448
- print("Run 'memory-os' (no arguments) for the interactive REPL.")
431
+ from cli.parser import build_parser
432
+ build_parser().print_help()
433
+ sys.exit(1)
449
434
 
450
435
 
451
436
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-memory-os
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI-based Personal Knowledge Operating System
5
5
  Author-email: Anirudh T <anirudh200584@gmail.com>
6
6
  License-Expression: MIT
@@ -6,6 +6,7 @@ cli/__init__.py
6
6
  cli/main.py
7
7
  cli/parser.py
8
8
  cli/commands/__init__.py
9
+ cli/commands/ask.py
9
10
  cli/commands/benchmark.py
10
11
  cli/commands/config_cmd.py
11
12
  cli/commands/doctor.py
@@ -19,6 +20,7 @@ cli/commands/restart.py
19
20
  cli/commands/start.py
20
21
  cli/commands/status.py
21
22
  cli/commands/stop.py
23
+ cli/commands/sync.py
22
24
  cli/commands/version.py
23
25
  cli/commands/workspace.py
24
26
  cli_memory_os.egg-info/PKG-INFO
@@ -58,4 +60,5 @@ tests/test_commands_phase2.py
58
60
  tests/test_compose_manager.py
59
61
  tests/test_first_time_user_experience.py
60
62
  tests/test_infrastructure_stabilization.py
63
+ tests/test_sync_ask_cli.py
61
64
  tests/test_v1_features.py
@@ -5,4 +5,5 @@ dist
5
5
  infrastructure
6
6
  logs
7
7
  models
8
+ scratch
8
9
  storage
@@ -67,23 +67,36 @@ def sync_github():
67
67
 
68
68
  print("Syncing GitHub...\n")
69
69
 
70
- # Fetch repositories
71
- resp = s.execute(tool_slug="github_list_repositories_for_the_authenticated_user", arguments={})
72
- if resp.error or not resp.data:
73
- print("No repositories found.")
74
- return
75
-
76
- resp_data = resp.data
77
- if isinstance(resp_data, dict) and "response_data" in resp_data:
78
- resp_data = resp_data["response_data"]
79
-
70
+ # Fetch repositories with pagination
80
71
  repos = []
81
- if isinstance(resp_data, list):
82
- repos = resp_data
83
- elif isinstance(resp_data, dict):
84
- repos = resp_data.get("repositories") or resp_data.get("items") or []
72
+ page = 1
73
+ while True:
74
+ resp = s.execute(
75
+ tool_slug="github_list_repositories_for_the_authenticated_user",
76
+ arguments={"per_page": 100, "page": page}
77
+ )
78
+ if resp.error or not resp.data:
79
+ break
80
+
81
+ resp_data = resp.data
82
+ if isinstance(resp_data, dict) and "response_data" in resp_data:
83
+ resp_data = resp_data["response_data"]
84
+
85
+ page_repos = []
86
+ if isinstance(resp_data, list):
87
+ page_repos = resp_data
88
+ elif isinstance(resp_data, dict):
89
+ page_repos = resp_data.get("repositories") or resp_data.get("items") or []
90
+
91
+ if not isinstance(page_repos, list) or not page_repos:
92
+ break
93
+
94
+ repos.extend(page_repos)
95
+ if len(page_repos) < 100:
96
+ break
97
+ page += 1
85
98
 
86
- if not isinstance(repos, list) or not repos:
99
+ if not repos:
87
100
  print("No repositories found.")
88
101
  return
89
102
 
@@ -23,19 +23,56 @@ def sync_gmail():
23
23
  print("Gmail connection not active.")
24
24
  return
25
25
 
26
- # Fetch emails
27
- resp = s.execute(tool_slug="gmail_fetch_emails", arguments={"max_results": 50, "include_payload": True})
28
- if not resp or resp.error or not resp.data:
29
- print("No emails found.")
30
- return
26
+ # Determine last fetched date from stored emails to optimize retrieval query
27
+ query = ""
28
+ try:
29
+ conn = get_connection()
30
+ cursor = conn.cursor()
31
+ cursor.execute("SELECT MAX(received_at) FROM emails WHERE received_at IS NOT NULL AND received_at != ''")
32
+ row = cursor.fetchone()
33
+ conn.close()
34
+ if row and row[0]:
35
+ import re
36
+ m = re.match(r"(\d{4})[-/](\d{2})[-/](\d{2})", row[0])
37
+ if m:
38
+ query = f"after:{m.group(1)}/{m.group(2)}/{m.group(3)}"
39
+ except Exception as e:
40
+ import logging
41
+ logging.getLogger("gmail").warning(f"Failed to query last fetched date from SQLite: {e}")
42
+
43
+ # Fetch emails with pagination
44
+ emails = []
45
+ page_token = None
46
+ while True:
47
+ args = {"max_results": 100, "include_payload": True}
48
+ if query:
49
+ args["q"] = query
50
+ if page_token:
51
+ args["page_token"] = page_token
52
+
53
+ resp = s.execute(tool_slug="gmail_fetch_emails", arguments=args)
54
+ if not resp or resp.error or not resp.data:
55
+ break
56
+
57
+ page_emails = resp.data.get("messages", [])
58
+ if not page_emails:
59
+ page_emails = resp.data.get("response_data", {}).get("messages", [])
60
+
61
+ if not page_emails:
62
+ break
63
+
64
+ emails.extend(page_emails)
65
+ page_token = resp.data.get("nextPageToken")
66
+ if not page_token:
67
+ break
68
+
69
+ # Safety check: avoid infinite loops if user has thousands of new emails
70
+ if len(emails) >= 500:
71
+ break
31
72
 
32
- emails = resp.data.get("messages", [])
33
73
  if not emails:
34
- # Check response_data structure just in case
35
- emails = resp.data.get("response_data", {}).get("messages", [])
36
- if not emails:
37
- print("No emails found.")
38
- return
74
+ print("No emails found.")
75
+ return
39
76
 
40
77
  print(f"Found {len(emails)} emails")
41
78
 
@@ -19,10 +19,10 @@ def get_qdrant_client() -> QdrantClient:
19
19
  if _qdrant_client is None:
20
20
  url = os.getenv("QDRANT_URL")
21
21
  if url:
22
- _qdrant_client = QdrantClient(url=url)
22
+ _qdrant_client = QdrantClient(url=url, timeout=60.0)
23
23
  else:
24
24
  path = os.getenv("MEMORY_OS_QDRANT_PATH", "qdrant_storage")
25
- _qdrant_client = QdrantClient(path=path)
25
+ _qdrant_client = QdrantClient(path=path, timeout=60.0)
26
26
  return _qdrant_client
27
27
 
28
28
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cli-memory-os"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "CLI-based Personal Knowledge Operating System"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -76,6 +76,7 @@ def test_init_missing_docker(capsys):
76
76
  """Test memory-os init when Docker Desktop is not running."""
77
77
  with patch("infrastructure.docker.check_docker_installed", return_value=(True, "Docker version 24.0.7")), \
78
78
  patch("infrastructure.docker.check_docker_compose_installed", return_value=(True, "Docker Compose version 2.23.3")), \
79
+ patch("infrastructure.docker.check_docker_daemon", return_value=(False, "Docker Desktop is not running.")), \
79
80
  patch("infrastructure.docker.check_docker_running", return_value=False):
80
81
 
81
82
  with pytest.raises(SystemExit) as excinfo:
@@ -0,0 +1,111 @@
1
+ """
2
+ Unit and integration tests for sync, ask commands, and the interactive REPL.
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import unittest
8
+ from unittest.mock import patch, MagicMock
9
+
10
+ # Ensure project root is in path
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+
14
+ class DummyArgs:
15
+ def __init__(self, **kwargs):
16
+ for k, v in kwargs.items():
17
+ setattr(self, k, v)
18
+
19
+
20
+ class TestSyncAskCLI(unittest.TestCase):
21
+ """Test suite verifying sync and ask CLI command routing and logic."""
22
+
23
+ @patch("cli.commands.sync.init_db")
24
+ @patch("cli.commands.sync.sync_github")
25
+ @patch("cli.commands.sync.sync_gmail")
26
+ @patch("cli.commands.sync.sync_notion")
27
+ @patch("cli.commands.sync.run_reindexing")
28
+ @patch("cli.commands.sync.GraphStore")
29
+ def test_sync_command_all_sources(
30
+ self, mock_graph_store_class, mock_run_reindexing, mock_sync_notion, mock_sync_gmail, mock_sync_github, mock_init_db
31
+ ):
32
+ """Test memory-os sync command runs sync for all sources."""
33
+ mock_graph_inst = MagicMock()
34
+ mock_graph_store_class.return_value = mock_graph_inst
35
+
36
+ from cli.commands.sync import execute as execute_sync
37
+
38
+ args = DummyArgs(rebuild=False, source=None)
39
+ execute_sync(args)
40
+
41
+ mock_init_db.assert_called_once()
42
+ mock_sync_github.assert_called_once()
43
+ mock_sync_gmail.assert_called_once()
44
+ mock_sync_notion.assert_called_once()
45
+ mock_run_reindexing.assert_called_once()
46
+ mock_graph_inst.extract_and_sync_graph.assert_called_once()
47
+
48
+ @patch("cli.commands.sync.init_db")
49
+ @patch("cli.commands.sync.sync_github")
50
+ @patch("cli.commands.sync.run_reindexing")
51
+ @patch("cli.commands.sync.GraphStore")
52
+ def test_sync_command_single_source(
53
+ self, mock_graph_store_class, mock_run_reindexing, mock_sync_github, mock_init_db
54
+ ):
55
+ """Test memory-os sync command can sync only a single source."""
56
+ mock_graph_inst = MagicMock()
57
+ mock_graph_store_class.return_value = mock_graph_inst
58
+
59
+ from cli.commands.sync import execute as execute_sync
60
+
61
+ args = DummyArgs(rebuild=False, source="github")
62
+ execute_sync(args)
63
+
64
+ mock_init_db.assert_called_once()
65
+ mock_sync_github.assert_called_once()
66
+ mock_run_reindexing.assert_called_once()
67
+ mock_graph_inst.extract_and_sync_graph.assert_called_once()
68
+
69
+ @patch("cli.commands.ask.init_db")
70
+ @patch("cli.commands.ask.run_hybrid_rag")
71
+ def test_ask_command(self, mock_run_hybrid_rag, mock_init_db):
72
+ """Test memory-os ask command query execution."""
73
+ mock_run_hybrid_rag.return_value = {
74
+ "answer": "Python is used in core services.",
75
+ "sources": ["source_1.txt"],
76
+ "repositories": ["repo-a"],
77
+ "confidence": 0.95
78
+ }
79
+
80
+ from cli.commands.ask import execute as execute_ask
81
+
82
+ args = DummyArgs(question=["What", "projects", "use", "Python?"])
83
+ execute_ask(args)
84
+
85
+ mock_init_db.assert_called_once()
86
+ mock_run_hybrid_rag.assert_called_once_with("What projects use Python?")
87
+
88
+ @patch("storage.db.init_db")
89
+ @patch("core.embedder.Embedder")
90
+ @patch("cli.main.input")
91
+ @patch("cli.commands.ask.run_and_print_ask")
92
+ def test_interactive_repl_ask(self, mock_run_and_print_ask, mock_input, mock_embedder_class, mock_init_db):
93
+ """Test interactive REPL triggers the ask command routing for natural language query."""
94
+ # Mock REPL sequence: 1. A question, 2. 'exit' to terminate loop
95
+ mock_input.side_effect = ["What projects use Python?", "exit"]
96
+
97
+ # Mock embedder instantiation
98
+ mock_embedder_inst = MagicMock()
99
+ mock_embedder_class.return_value = mock_embedder_inst
100
+
101
+ from cli.main import run_interactive
102
+
103
+ with patch("core.vector_store.close_qdrant_client"), patch("storage.graph.GraphStore"):
104
+ run_interactive()
105
+
106
+ # Verify that natural language query gets routed to ask and calls run_and_print_ask
107
+ mock_run_and_print_ask.assert_called_once_with("What projects use Python?")
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes