concierge-graph 3.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
interface/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ interface/ - External Interface of Grafo Concierge v3.8.0
3
+
4
+ Modules:
5
+ mcp_server.py -> FastMCP server with 7 tools (mine, search, commit, wakeup, resume, load, status)
6
+ action_hooks.py -> Reactive lifecycle triggers (on_planning, on_execution, on_done)
7
+ cli.py -> Terminal interface (argparse) with 9 commands
8
+ """
9
+
10
+ from interface.mcp_server import GrafoConciergeServer
11
+ from interface.action_hooks import ActionHooks
12
+
13
+ __all__ = [
14
+ "GrafoConciergeServer",
15
+ "ActionHooks",
16
+ ]
@@ -0,0 +1,261 @@
1
+ """
2
+ interface/action_hooks.py — Grafo Concierge v3.8.0 (Absolute Solidity)
3
+
4
+ Reactive Lifecycle Triggers for Operational Modules.
5
+
6
+ This module exposes decorators and functions that allow any
7
+ Operational Module (Cursor Skills, n8n Agents, Automations)
8
+ to connect to Grafo Concierge at key lifecycle moments.
9
+
10
+ Triggers:
11
+ on_planning() -> Reactivates consciousness + searches relevant drawers
12
+ on_execution() -> Focused search + lazy load of specific nodes
13
+ on_done() -> Audited commit + trajectory decay
14
+
15
+ Typical flow of an Operational Module:
16
+ 1. Module receives user task
17
+ 2. Calls hooks.on_planning() -> receives Compass + context
18
+ 3. Executes task using hooks.on_execution() to fetch code
19
+ 4. Finishes with hooks.on_done() -> writes result to graph
20
+
21
+ Integration:
22
+ - Consumes core.middleware.GrafoConcierge as sole dependency
23
+ - Consumes agents.revisor_critico.RevisorCritico for auditing
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ from typing import Any, Optional
30
+
31
+ from core.middleware import GrafoConcierge
32
+ from agents.revisor_critico import RevisorCritico
33
+
34
+ logger = logging.getLogger("grafo-concierge.hooks")
35
+
36
+
37
+ class ActionHooks:
38
+ """Reactive triggers for Operational Modules.
39
+
40
+ Centralizes contact points between Operational Modules and
41
+ Grafo Concierge. Each method corresponds to a phase of
42
+ a task's lifecycle.
43
+
44
+ Args:
45
+ concierge: Central Facade instance.
46
+ revisor: Critical Reviewer instance (optional).
47
+ If None, commits do not go through LLM auditing.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ concierge: GrafoConcierge,
53
+ revisor: Optional[RevisorCritico] = None,
54
+ ) -> None:
55
+ self._gc = concierge
56
+ self._revisor = revisor or RevisorCritico()
57
+
58
+ logger.info(
59
+ "ActionHooks initialized: reviewer=%s",
60
+ "LLM" if revisor and revisor._llm else "heuristic",
61
+ )
62
+
63
+ # ===================================================================
64
+ # ON_PLANNING — Consciousness reactivation
65
+ # ===================================================================
66
+
67
+ def on_planning(
68
+ self,
69
+ project_uuid: str,
70
+ task: str,
71
+ top_k: int = 5,
72
+ node_type: Optional[str] = None,
73
+ ) -> dict:
74
+ """Planning trigger — prepares the context for the task.
75
+
76
+ Flow:
77
+ 1. Wake-up -> loads Compass + Reference Wings + commits
78
+ 2. Hybrid Search -> finds relevant drawers for the task
79
+ 3. Returns full context package
80
+
81
+ Args:
82
+ project_uuid: UUID of the project.
83
+ task: Description of the task to be executed.
84
+ top_k: Maximum number of search results.
85
+ node_type: Optional surgical filter.
86
+
87
+ Returns:
88
+ Dict with:
89
+ {
90
+ "wake_up": dict (Compass + Wings + commits),
91
+ "relevant_nodes": list[dict] (search results),
92
+ "task": str,
93
+ }
94
+ """
95
+ logger.info("on_planning: project=%s, task='%.50s...'", project_uuid, task)
96
+
97
+ # 1. Wake-up
98
+ wake_data = self._gc.wake_up(project_uuid)
99
+
100
+ # 2. Hybrid Search
101
+ search_results = self._gc.hybrid_search(
102
+ query=task,
103
+ project_uuid=project_uuid,
104
+ top_k=top_k,
105
+ node_type=node_type,
106
+ )
107
+
108
+ result = {
109
+ "wake_up": wake_data,
110
+ "relevant_nodes": search_results,
111
+ "task": task,
112
+ }
113
+
114
+ logger.info(
115
+ "on_planning OK: wake_up with %d commits, %d relevant nodes.",
116
+ len(wake_data.get("recent_commits", [])),
117
+ len(search_results),
118
+ )
119
+ return result
120
+
121
+ # ===================================================================
122
+ # ON_EXECUTION — Focused search during execution
123
+ # ===================================================================
124
+
125
+ def on_execution(
126
+ self,
127
+ project_uuid: str,
128
+ task: str,
129
+ top_k: int = 10,
130
+ node_type: Optional[str] = None,
131
+ include_references: bool = False,
132
+ rerank: bool = True,
133
+ ) -> list[dict]:
134
+ """Execution trigger — searches and filters drawers by relevance.
135
+
136
+ Flow:
137
+ 1. Hybrid Search v4 with Strict Scoping
138
+ 2. If rerank=True, Critical Reviewer filters top-5
139
+ 3. Returns relevant drawers for the task
140
+
141
+ Args:
142
+ project_uuid: UUID of the project.
143
+ task: Description of the current task.
144
+ top_k: Maximum number of initial search results.
145
+ node_type: Surgical filter (FACT, SKILL, INSIGHT, etc.).
146
+ include_references: Include Reference Wings.
147
+ rerank: Apply Critical Reviewer Reranking.
148
+
149
+ Returns:
150
+ List of dicts with filtered results.
151
+ """
152
+ logger.info("on_execution: project=%s, task='%.50s...'", project_uuid, task)
153
+
154
+ # 1. Hybrid Search
155
+ search_results = self._gc.hybrid_search(
156
+ query=task,
157
+ project_uuid=project_uuid,
158
+ top_k=top_k,
159
+ include_references=include_references,
160
+ node_type=node_type,
161
+ )
162
+
163
+ # 2. Reranking (if active and there are results)
164
+ if rerank and search_results:
165
+ search_results = self._revisor.rerank(
166
+ candidates=search_results,
167
+ task_context=task,
168
+ max_results=5,
169
+ )
170
+
171
+ logger.info(
172
+ "on_execution OK: %d results (rerank=%s).",
173
+ len(search_results), rerank,
174
+ )
175
+ return search_results
176
+
177
+ # ===================================================================
178
+ # ON_DONE — Audited commit post-task
179
+ # ===================================================================
180
+
181
+ def on_done(
182
+ self,
183
+ project_uuid: str,
184
+ outcome: dict,
185
+ ) -> dict:
186
+ """Conclusion trigger — registers result in the graph.
187
+
188
+ Flow:
189
+ 1. Critical Reviewer audits the draft
190
+ 2. If approved, writes commit to the graph
191
+ 3. If there is an error/solution, registers episodic trajectory
192
+ 4. Returns operation result
193
+
194
+ Args:
195
+ project_uuid: UUID of the project.
196
+ outcome: Dict with task outcome:
197
+ - phase (str): Phase (planning, build, done, review)
198
+ - technical_changes (str): Technical changes
199
+ - updated_pointers (list[str]): Updated pointers
200
+ - node_ids (list[int], optional): Affected nodes
201
+ - erro_encontrado (str, optional): Error occurred
202
+ - solucao_aplicada (str, optional): Solution applied
203
+
204
+ Returns:
205
+ Dict with:
206
+ {
207
+ "commit_id": int,
208
+ "audit": AuditResult.to_dict(),
209
+ "trajectory_id": int (if there was an error),
210
+ }
211
+ """
212
+ logger.info("on_done: project=%s, phase='%s'", project_uuid, outcome.get("phase", "?"))
213
+
214
+ result: dict[str, Any] = {}
215
+
216
+ # 1. Auditoria do commit
217
+ audit = self._revisor.audit({
218
+ "phase": outcome.get("phase", "done"),
219
+ "technical_changes": outcome.get("technical_changes", ""),
220
+ "updated_pointers": outcome.get("updated_pointers", []),
221
+ "source_wing": "primary",
222
+ })
223
+ result["audit"] = audit.to_dict()
224
+
225
+ # 2. Grava commit (mesmo se partial_audit, para não perder dados)
226
+ if audit.approved:
227
+ commit_id = self._gc.commit_memory(
228
+ project_uuid=project_uuid,
229
+ phase=outcome.get("phase", "done"),
230
+ technical_changes=audit.technical_changes,
231
+ updated_pointers=audit.updated_pointers,
232
+ node_ids=outcome.get("node_ids"),
233
+ )
234
+ result["commit_id"] = commit_id
235
+ else:
236
+ result["commit_id"] = None
237
+ logger.warning("on_done: commit REJEITADO — %s", audit.reason)
238
+
239
+ # 3. Registra trajetória episódica (se houve erro)
240
+ if outcome.get("erro_encontrado"):
241
+ try:
242
+ trajectory_id = self._gc.store.create_trajectory(
243
+ project_uuid=project_uuid,
244
+ prompt_origem=outcome.get("task", ""),
245
+ tentativa_execucao=outcome.get("technical_changes", ""),
246
+ erro_encontrado=outcome.get("erro_encontrado"),
247
+ solucao_aplicada=outcome.get("solucao_aplicada"),
248
+ )
249
+ result["trajectory_id"] = trajectory_id
250
+ logger.info(
251
+ "Trajetória episódica registrada: id=%d", trajectory_id,
252
+ )
253
+ except Exception as e:
254
+ logger.error("Falha ao registrar trajetória: %s", e)
255
+ result["trajectory_id"] = None
256
+
257
+ logger.info(
258
+ "on_done OK: commit_id=%s, audit_approved=%s, partial=%s",
259
+ result.get("commit_id"), audit.approved, audit.partial_audit,
260
+ )
261
+ return result
interface/cli.py ADDED
@@ -0,0 +1,391 @@
1
+ """
2
+ interface/cli.py — Grafo Concierge v3.8.0 (Absolute Solidity)
3
+
4
+ Terminal Interface (CLI) to manage Grafo Concierge
5
+ via Bash/PowerShell.
6
+
7
+ Commands:
8
+ grafo-concierge register → Registers a new project
9
+ grafo-concierge mine → Directory ingestion
10
+ grafo-concierge search → Hybrid Search v4
11
+ grafo-concierge wakeup → Consciousness reactivation
12
+ grafo-concierge resume → Context Compass
13
+ grafo-concierge commit → Changelog registration
14
+ grafo-concierge load → Node Lazy Load
15
+ grafo-concierge status → System health status
16
+ grafo-concierge projects → Lists projects
17
+
18
+ Usage:
19
+ python -m interface.cli mine --path /projects/vortex --name vortex-pro
20
+ python -m interface.cli search --query "JWT authentication" --project abc123
21
+ python -m interface.cli wakeup --project abc123
22
+
23
+ Integration:
24
+ Consumes core.middleware.GrafoConcierge as sole dependency.
25
+ Bootstrap is automatically performed via _bootstrap_concierge().
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import logging
33
+ import os
34
+ import sys
35
+ from pathlib import Path
36
+ from typing import Optional
37
+
38
+ logger = logging.getLogger("grafo-concierge.cli")
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Bootstrap — Initializes the GrafoConcierge for CLI use
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def _bootstrap_concierge():
46
+ """Initializes the Central Facade for CLI usage.
47
+
48
+ Uses the same environment variables as main.py.
49
+ Returns a ready-to-use GrafoConcierge instance.
50
+ """
51
+ from dotenv import load_dotenv
52
+ load_dotenv()
53
+
54
+ from storage import SqliteStore, EmbeddingManager, EmbeddingTier, ChromaVectorStore
55
+ from ingestion import IngestionManager, ZoomSummarizer
56
+ from ingestion.summarizer import LLMAdapter
57
+ from core.middleware import GrafoConcierge
58
+
59
+ # Dynamic anchor: interface/ → project root
60
+ _project_root = Path(__file__).parent.parent.resolve()
61
+
62
+ def resolve_project_path(env_value: str, default_rel: str) -> str:
63
+ val = env_value or default_rel
64
+ path = Path(val)
65
+ if path.is_absolute():
66
+ return str(path)
67
+ return str((_project_root / path).resolve())
68
+
69
+ db_path = resolve_project_path(os.environ.get("GRAFO_DB_PATH", ""), "data/concierge.db")
70
+ chroma_path = resolve_project_path(os.environ.get("GRAFO_CHROMA_PATH", ""), "data/chroma")
71
+ chroma_collection = os.environ.get("GRAFO_CHROMA_COLLECTION", "grafo_concierge")
72
+ llm_model = os.environ.get("GRAFO_LLM_MODEL", "gemini-2.0-flash")
73
+ llm_api_key = os.environ.get("GRAFO_LLM_API_KEY", "")
74
+ llm_base_url = os.environ.get("GRAFO_LLM_BASE_URL", "")
75
+
76
+ os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
77
+ os.makedirs(chroma_path, exist_ok=True)
78
+
79
+ store = SqliteStore(db_path)
80
+ embedder = EmbeddingManager(tier=EmbeddingTier.FLASH)
81
+
82
+ vector_backend = os.environ.get("GRAFO_VECTOR_BACKEND", "chroma").lower()
83
+ if vector_backend == "qdrant":
84
+ qdrant_url = os.environ.get("GRAFO_QDRANT_URL", "http://localhost:6333")
85
+ qdrant_key = os.environ.get("GRAFO_QDRANT_API_KEY", "") or None
86
+ from core.vector_backend import QdrantVectorStore
87
+ vector_store = QdrantVectorStore(
88
+ url=qdrant_url,
89
+ api_key=qdrant_key,
90
+ collection_name=chroma_collection,
91
+ embedding_dimensions=embedder.dimensions,
92
+ )
93
+ else:
94
+ vector_store = ChromaVectorStore(
95
+ persist_dir=chroma_path,
96
+ collection_name=chroma_collection,
97
+ embedding_manager=embedder,
98
+ )
99
+
100
+ llm_adapter = LLMAdapter(
101
+ model_name=llm_model,
102
+ api_key=llm_api_key or None,
103
+ base_url=llm_base_url or None,
104
+ )
105
+ summarizer = ZoomSummarizer(llm_adapter=llm_adapter, sqlite_store=store)
106
+
107
+ ingestion_manager = IngestionManager(
108
+ sqlite_store=store,
109
+ vector_store=vector_store,
110
+ embedding_manager=embedder,
111
+ summarizer=summarizer,
112
+ )
113
+
114
+ gc = GrafoConcierge(
115
+ sqlite_store=store,
116
+ vector_store=vector_store,
117
+ embedding_manager=embedder,
118
+ ingestion_manager=ingestion_manager,
119
+ )
120
+
121
+ return gc, store
122
+
123
+
124
+ def _print_json(data: dict) -> None:
125
+ """Prints result formatted as JSON."""
126
+ print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # CLI Commands
131
+ # ---------------------------------------------------------------------------
132
+
133
+ def cmd_register(args, gc, store):
134
+ """Registers a new project."""
135
+ uuid = gc.register_project(
136
+ folder_name=args.name,
137
+ wing=args.wing,
138
+ privacy_level=args.privacy or "PUBLIC",
139
+ summary=args.summary,
140
+ )
141
+ print(f"Project registered: {args.name} → {uuid}")
142
+
143
+
144
+ def cmd_mine(args, gc, store):
145
+ """Executes directory ingestion."""
146
+ uuid = gc.register_project(folder_name=args.name)
147
+ result = gc.mine(uuid, args.path, auto_tag=not args.no_tag)
148
+ _print_json(result)
149
+
150
+
151
+ def cmd_search(args, gc, store):
152
+ """Hybrid Search v4."""
153
+ results = gc.hybrid_search(
154
+ query=args.query,
155
+ project_uuid=args.project,
156
+ top_k=args.top_k,
157
+ node_type=args.node_type,
158
+ include_references=args.refs,
159
+ all_wings=args.all_wings,
160
+ )
161
+ _print_json({"results_count": len(results), "results": results})
162
+
163
+
164
+ def cmd_wakeup(args, gc, store):
165
+ """Consciousness reactivation."""
166
+ result = gc.wake_up(args.project)
167
+ _print_json(result)
168
+
169
+
170
+ def cmd_resume(args, gc, store):
171
+ """Context Compass."""
172
+ resume = gc.get_resume(args.project)
173
+ print(resume)
174
+
175
+
176
+ def cmd_commit(args, gc, store):
177
+ """Commit registration."""
178
+ pointers = args.pointers.split(",") if args.pointers else []
179
+ commit_id = gc.commit_memory(
180
+ project_uuid=args.project,
181
+ phase=args.phase,
182
+ technical_changes=args.changes,
183
+ updated_pointers=pointers,
184
+ )
185
+ print(f"Commit registered: id={commit_id}")
186
+
187
+
188
+ def cmd_load(args, gc, store):
189
+ """Lazy Load of a node."""
190
+ result = gc.lazy_load(args.node_id)
191
+ _print_json(result)
192
+
193
+
194
+ def cmd_status(args, gc, store):
195
+ """System status."""
196
+ if args.project:
197
+ result = gc.status(args.project)
198
+ else:
199
+ projects = store.list_projects()
200
+ result = {
201
+ "system": "Grafo Concierge v3.8.0",
202
+ "total_projects": len(projects),
203
+ "projects": [
204
+ {"uuid": p["uuid"], "name": p.get("folder_name", ""), "wing": p.get("primary_wing", "geral")}
205
+ for p in projects
206
+ ],
207
+ }
208
+ _print_json(result)
209
+
210
+
211
+ def cmd_projects(args, gc, store):
212
+ """Lists all projects."""
213
+ projects = store.list_projects()
214
+ if not projects:
215
+ print("No projects registered.")
216
+ return
217
+
218
+ print(f"{'UUID':<38} {'Name':<25} {'Wing':<20} {'Privacy':<12}")
219
+ print("-" * 95)
220
+ for p in projects:
221
+ print(
222
+ f"{p['uuid']:<38} {p.get('folder_name', ''):<25} "
223
+ f"{p.get('primary_wing', 'general'):<20} {p.get('privacy_level', 'PUBLIC'):<12}"
224
+ )
225
+
226
+
227
+ def cmd_sync_vector(args, gc, store):
228
+ """Synchronizes and reconciles missing embeddings in the active vector store manually."""
229
+ from services.janitor import JanitorService, MaintenanceReport
230
+
231
+ # Initializes local JanitorService
232
+ janitor = JanitorService(
233
+ sqlite_store=store,
234
+ vector_store=gc._vector,
235
+ ingestion_manager=gc._ingestion,
236
+ )
237
+
238
+ projects_to_sync = []
239
+ if args.project:
240
+ projects_to_sync.append(args.project)
241
+ else:
242
+ all_projects = store.list_projects()
243
+ projects_to_sync = [p["uuid"] for p in all_projects]
244
+
245
+ if not projects_to_sync:
246
+ print("No registered projects found to synchronize.")
247
+ return
248
+
249
+ print(f"Starting manual batch vector reconciliation for {len(projects_to_sync)} projects...")
250
+ for p_uuid in projects_to_sync:
251
+ p_name = next((p["folder_name"] for p in store.list_projects() if p["uuid"] == p_uuid), p_uuid)
252
+ print(f"-> Synchronizing project: {p_name} ({p_uuid})...")
253
+ report = MaintenanceReport()
254
+ # Executes bidirectional reconciliation (deletes orphans and generates missing)
255
+ janitor._sync_vectors(p_uuid, report)
256
+ if report.errors:
257
+ print(f" [ERROR] {report.errors[0]}")
258
+ else:
259
+ print(f" [OK] Vector reconciliation completed successfully.")
260
+ print("Reconciliation and synchronization finished!")
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Argument parser
265
+ # ---------------------------------------------------------------------------
266
+
267
+ def build_parser() -> argparse.ArgumentParser:
268
+ """Builds the CLI argument parser."""
269
+
270
+ parser = argparse.ArgumentParser(
271
+ prog="grafo-concierge",
272
+ description="Grafo Concierge v3.8.0 — Sovereign Memory for AI Agents",
273
+ )
274
+ subparsers = parser.add_subparsers(dest="command", help="Available command")
275
+
276
+ # --- register ---
277
+ p_register = subparsers.add_parser("register", help="Registers a new project")
278
+ p_register.add_argument("--name", required=True, help="Project name")
279
+ p_register.add_argument("--wing", default=None, help="Primary Wing")
280
+ p_register.add_argument("--privacy", default="PUBLIC", help="Privacy level")
281
+ p_register.add_argument("--summary", default=None, help="Project description")
282
+
283
+ # --- mine ---
284
+ p_mine = subparsers.add_parser("mine", help="Directory ingestion")
285
+ p_mine.add_argument("--path", required=True, help="Directory path")
286
+ p_mine.add_argument("--name", required=True, help="Project name")
287
+ p_mine.add_argument("--no-tag", action="store_true", help="Disables auto-tag")
288
+
289
+ # --- search ---
290
+ p_search = subparsers.add_parser("search", help="Hybrid Search v4")
291
+ p_search.add_argument("--query", required=True, help="Search text")
292
+ p_search.add_argument("--project", required=True, help="Project UUID")
293
+ p_search.add_argument("--top-k", type=int, default=10, help="Maximum results")
294
+ p_search.add_argument("--node-type", default=None, help="Node type filter")
295
+ p_search.add_argument("--refs", action="store_true", help="Include Reference Wings")
296
+ p_search.add_argument("--all-wings", action="store_true", help="Search in all wings")
297
+
298
+ # --- wakeup ---
299
+ p_wakeup = subparsers.add_parser("wakeup", help="Consciousness reactivation")
300
+ p_wakeup.add_argument("--project", required=True, help="Project UUID")
301
+
302
+ # --- resume ---
303
+ p_resume = subparsers.add_parser("resume", help="Context Compass")
304
+ p_resume.add_argument("--project", required=True, help="Project UUID")
305
+
306
+ # --- commit ---
307
+ p_commit = subparsers.add_parser("commit", help="Registers a memory commit")
308
+ p_commit.add_argument("--project", required=True, help="Project UUID")
309
+ p_commit.add_argument("--phase", required=True, help="Current phase")
310
+ p_commit.add_argument("--changes", required=True, help="Technical changes")
311
+ p_commit.add_argument("--pointers", required=True, help="Pointers (comma-separated)")
312
+
313
+ # --- load ---
314
+ p_load = subparsers.add_parser("load", help="Lazy Load of a node")
315
+ p_load.add_argument("--node-id", type=int, required=True, help="Node ID")
316
+
317
+ # --- status ---
318
+ p_status = subparsers.add_parser("status", help="System status")
319
+ p_status.add_argument("--project", default=None, help="Project UUID (optional)")
320
+
321
+ # --- projects ---
322
+ subparsers.add_parser("projects", help="Lists all projects")
323
+
324
+ # --- sync-vector ---
325
+ p_sync_vector = subparsers.add_parser("sync-vector", help="Synchronizes and reconciles missing embeddings in the active vector store manually")
326
+ p_sync_vector.add_argument("--project", default=None, help="UUID of a specific project to synchronize (optional)")
327
+
328
+ return parser
329
+
330
+
331
+ # ---------------------------------------------------------------------------
332
+ # Main
333
+ # ---------------------------------------------------------------------------
334
+
335
+ COMMAND_MAP = {
336
+ "register": cmd_register,
337
+ "mine": cmd_mine,
338
+ "search": cmd_search,
339
+ "wakeup": cmd_wakeup,
340
+ "resume": cmd_resume,
341
+ "commit": cmd_commit,
342
+ "load": cmd_load,
343
+ "status": cmd_status,
344
+ "projects": cmd_projects,
345
+ "sync-vector": cmd_sync_vector,
346
+ }
347
+
348
+
349
+ def main():
350
+ """CLI entry point."""
351
+ parser = build_parser()
352
+ args = parser.parse_args()
353
+
354
+ if not args.command:
355
+ parser.print_help()
356
+ sys.exit(1)
357
+
358
+ # Setup minimal logging for CLI
359
+ logging.basicConfig(
360
+ level=logging.WARNING,
361
+ format="%(levelname)s: %(message)s",
362
+ stream=sys.stderr,
363
+ )
364
+
365
+ # Bootstrap
366
+ try:
367
+ gc, store = _bootstrap_concierge()
368
+ except Exception as e:
369
+ print(f"Error initializing Grafo Concierge: {e}", file=sys.stderr)
370
+ sys.exit(1)
371
+
372
+ # Executes command
373
+ handler = COMMAND_MAP.get(args.command)
374
+ if handler:
375
+ try:
376
+ handler(args, gc, store)
377
+ except Exception as e:
378
+ print(f"Error: {e}", file=sys.stderr)
379
+ sys.exit(1)
380
+ finally:
381
+ try:
382
+ store.close()
383
+ except Exception:
384
+ pass
385
+ else:
386
+ parser.print_help()
387
+ sys.exit(1)
388
+
389
+
390
+ if __name__ == "__main__":
391
+ main()