agent-brain-cli 10.0.5__tar.gz → 10.0.7__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 (41) hide show
  1. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/PKG-INFO +2 -2
  2. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/__init__.py +1 -1
  3. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/diagnostics.py +212 -0
  4. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/pyproject.toml +2 -2
  5. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/README.md +0 -0
  6. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/cli.py +0 -0
  7. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/client/__init__.py +0 -0
  8. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/client/api_client.py +0 -0
  9. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/__init__.py +0 -0
  10. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/cache.py +0 -0
  11. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/config.py +0 -0
  12. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/doctor.py +0 -0
  13. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/folders.py +0 -0
  14. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/index.py +0 -0
  15. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/init.py +0 -0
  16. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/inject.py +0 -0
  17. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/install_agent.py +0 -0
  18. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/jobs.py +0 -0
  19. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/list_cmd.py +0 -0
  20. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/query.py +0 -0
  21. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/reset.py +0 -0
  22. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/start.py +0 -0
  23. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/status.py +0 -0
  24. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/stop.py +0 -0
  25. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/types.py +0 -0
  26. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/commands/uninstall.py +0 -0
  27. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/config.py +0 -0
  28. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/config_migrate.py +0 -0
  29. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/config_schema.py +0 -0
  30. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/migration.py +0 -0
  31. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/__init__.py +0 -0
  32. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/claude_converter.py +0 -0
  33. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/codex_converter.py +0 -0
  34. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/converter_base.py +0 -0
  35. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/gemini_converter.py +0 -0
  36. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/opencode_converter.py +0 -0
  37. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/parser.py +0 -0
  38. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
  39. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/tool_maps.py +0 -0
  40. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/runtime/types.py +0 -0
  41. {agent_brain_cli-10.0.5 → agent_brain_cli-10.0.7}/agent_brain_cli/xdg_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: agent-brain-cli
3
- Version: 10.0.5
3
+ Version: 10.0.7
4
4
  Summary: Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval
5
5
  Home-page: https://github.com/SpillwaveSolutions/agent-brain
6
6
  License: MIT
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Dist: agent-brain-rag (>=10.0.5,<11.0.0)
18
+ Requires-Dist: agent-brain-rag (>=10.0.7,<11.0.0)
19
19
  Requires-Dist: click (>=8.1.0,<9.0.0)
20
20
  Requires-Dist: httpx (>=0.28.0,<0.29.0)
21
21
  Requires-Dist: pydantic (>=2.10.0,<3.0.0)
@@ -1,3 +1,3 @@
1
1
  """Doc-Serve CLI - Command-line interface for managing Doc-Serve server."""
2
2
 
3
- __version__ = "10.0.5"
3
+ __version__ = "10.0.7"
@@ -371,6 +371,103 @@ def _check_optional_dep(provider: str, module_name: str, extra: str) -> CheckRes
371
371
  )
372
372
 
373
373
 
374
+ def _graph_index_dir(state_dir: Path) -> Path:
375
+ """Return the conventional graph_index location under a state dir."""
376
+ return state_dir / "data" / "graph_index"
377
+
378
+
379
+ def _read_graphrag_block(state_dir: Path) -> dict[str, Any] | None:
380
+ """Read the ``graphrag`` block from the project's config.yaml, if any.
381
+
382
+ The CLI's Pydantic ``AgentBrainConfig`` doesn't model graphrag (it's a
383
+ server concern), so this peeks directly at the YAML. Returns None when
384
+ no graphrag block is present.
385
+ """
386
+ import yaml # local import to avoid a hard dep at module load
387
+
388
+ for name in ("config.yaml", "agent-brain.yaml", "config.yml"):
389
+ path = state_dir / name
390
+ if not path.exists():
391
+ continue
392
+ try:
393
+ data = yaml.safe_load(path.read_text()) or {}
394
+ except (OSError, yaml.YAMLError):
395
+ continue
396
+ block = data.get("graphrag")
397
+ if isinstance(block, dict):
398
+ return block
399
+ return None
400
+
401
+
402
+ def _check_graph_store_health(state_dir: Path) -> CheckResult | None:
403
+ """Verify the Kuzu DB opens cleanly when GraphRAG is enabled (Issue #166).
404
+
405
+ Returns None when GraphRAG is disabled or the store backend isn't Kuzu —
406
+ those cases have no graph health to report. Otherwise opens the Kuzu DB
407
+ in process briefly. A successful open is OK. An IndexError / RuntimeError
408
+ (the typical kill-mid-write signature) is FAIL with a fix hint pointing
409
+ at ``--fix`` or manual recovery.
410
+ """
411
+ block = _read_graphrag_block(state_dir)
412
+ if not block or not block.get("enabled"):
413
+ return None
414
+ store_type = str(block.get("store_type") or "simple").lower()
415
+ if store_type != "kuzu":
416
+ return None
417
+
418
+ graph_dir = _graph_index_dir(state_dir)
419
+ kuzu_db_path = graph_dir / "kuzu_db"
420
+ if not kuzu_db_path.exists():
421
+ # Nothing on disk yet — nothing to be corrupted.
422
+ return CheckResult(
423
+ "graph_store_health",
424
+ SEVERITY_OK,
425
+ f"No Kuzu DB on disk yet at {kuzu_db_path} (will be created on "
426
+ "first indexing job).",
427
+ details={"path": str(kuzu_db_path)},
428
+ )
429
+
430
+ try:
431
+ import kuzu
432
+ except ImportError:
433
+ # The langextract optional-dep check already covers missing extras.
434
+ return None
435
+
436
+ try:
437
+ kuzu.Database(str(kuzu_db_path))
438
+ except (IndexError, RuntimeError) as exc:
439
+ return CheckResult(
440
+ "graph_store_health",
441
+ SEVERITY_FAIL,
442
+ f"Kuzu DB at {kuzu_db_path} appears corrupted: {exc}. This "
443
+ "typically happens when the server was killed mid-indexing.",
444
+ fix=(
445
+ "Run `agent-brain doctor --fix` (server must be stopped) to "
446
+ "quarantine the corrupted file and restore from the latest "
447
+ "snapshot, or manually `rm` the file (loses all triplets)."
448
+ ),
449
+ details={"path": str(kuzu_db_path), "error": str(exc)},
450
+ )
451
+ except Exception as exc: # noqa: BLE001 — anything unexpected
452
+ return CheckResult(
453
+ "graph_store_health",
454
+ SEVERITY_WARN,
455
+ f"Could not check Kuzu DB at {kuzu_db_path}: {exc}",
456
+ )
457
+
458
+ return CheckResult(
459
+ "graph_store_health",
460
+ SEVERITY_OK,
461
+ f"Kuzu DB at {kuzu_db_path} opens cleanly.",
462
+ details={"path": str(kuzu_db_path)},
463
+ )
464
+
465
+
466
+ def _server_is_running(state_dir: Path) -> bool:
467
+ """Best-effort check for an active server lock under state_dir."""
468
+ return (state_dir / "server.lock").exists() or (state_dir / "lock").exists()
469
+
470
+
374
471
  def _check_gitignore(project_root: Path) -> CheckResult:
375
472
  gi = project_root / ".gitignore"
376
473
  if not gi.exists():
@@ -436,6 +533,11 @@ def run_doctor(server_url_override: str | None = None) -> DoctorReport:
436
533
  _check_optional_dep("graphrag (langextract)", "langextract", "graphrag")
437
534
  )
438
535
 
536
+ # Issue #166 — verify Kuzu DB opens cleanly.
537
+ graph_check = _check_graph_store_health(state_dir)
538
+ if graph_check is not None:
539
+ checks.append(graph_check)
540
+
439
541
  checks.append(_check_gitignore(project_root))
440
542
 
441
543
  checks.append(_check_server(server_url, runtime_file))
@@ -490,9 +592,119 @@ def apply_safe_fixes(report: DoctorReport) -> list[str]:
490
592
  + "\n"
491
593
  )
492
594
  actions.append(f"Created {cfg_json}.")
595
+ elif check.name == "graph_store_health" and check.status == SEVERITY_FAIL:
596
+ # Issue #166 — recover a corrupted Kuzu DB offline.
597
+ if _server_is_running(state_dir):
598
+ actions.append(
599
+ "Skipped Kuzu recovery: server appears to be running. "
600
+ "Stop it with `agent-brain stop` first, then re-run "
601
+ "`agent-brain doctor --fix`."
602
+ )
603
+ continue
604
+ graph_dir = _graph_index_dir(state_dir)
605
+ kuzu_db = graph_dir / "kuzu_db"
606
+ kuzu_wal = graph_dir / "kuzu_db.wal"
607
+ stamp = _utc_stamp()
608
+ for src in (kuzu_db, kuzu_wal):
609
+ if src.exists():
610
+ dest = src.with_name(f"{src.name}.corrupted-{stamp}")
611
+ src.rename(dest)
612
+ actions.append(
613
+ f"Quarantined {src.name} → {dest.name} "
614
+ "(forensic preservation)."
615
+ )
616
+ # Restore from latest snapshot, if one exists.
617
+ restored = _replay_latest_snapshot(graph_dir)
618
+ if restored is not None:
619
+ snapshot_name, count = restored
620
+ actions.append(
621
+ f"Restored {count} triplets from snapshot "
622
+ f"{snapshot_name} into a fresh Kuzu DB."
623
+ )
624
+ else:
625
+ actions.append(
626
+ "No snapshot available to restore; graph index will "
627
+ "start empty. Re-run `agent-brain index <folder>` to "
628
+ "rebuild."
629
+ )
493
630
  return actions
494
631
 
495
632
 
633
+ def _utc_stamp() -> str:
634
+ """Filesystem-safe UTC timestamp suffix shared with the server's
635
+ quarantine logic (graph_store._corrupted_sibling)."""
636
+ from datetime import datetime, timezone
637
+
638
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
639
+
640
+
641
+ def _replay_latest_snapshot(graph_dir: Path) -> tuple[str, int] | None:
642
+ """Replay triplets from the newest valid snapshot into a fresh Kuzu DB.
643
+
644
+ Returns ``(snapshot_filename, triplet_count)`` on success, or ``None`` if
645
+ there's no usable snapshot. Best-effort: any failure becomes a None
646
+ return (the caller will report it). The CLI doesn't import the server
647
+ code path because the CLI may be installed without the server package;
648
+ instead it reads snapshot JSON directly using a minimal schema.
649
+ """
650
+ snap_dir = graph_dir / "snapshots"
651
+ if not snap_dir.is_dir():
652
+ return None
653
+ try:
654
+ import kuzu
655
+ from llama_index.core.graph_stores.types import EntityNode, Relation
656
+ from llama_index.graph_stores.kuzu import KuzuPropertyGraphStore
657
+ except ImportError:
658
+ return None
659
+
660
+ candidates = sorted(
661
+ (p for p in snap_dir.iterdir() if p.is_file() and p.suffix == ".json"),
662
+ key=lambda p: (p.stat().st_mtime, p.name),
663
+ reverse=True,
664
+ )
665
+ for snap in candidates:
666
+ try:
667
+ payload = json.loads(snap.read_text())
668
+ if payload.get("schema_version") != 1:
669
+ continue
670
+ triplets = payload.get("triplets") or []
671
+ except (OSError, json.JSONDecodeError):
672
+ continue
673
+
674
+ try:
675
+ db = kuzu.Database(str(graph_dir / "kuzu_db"))
676
+ store = KuzuPropertyGraphStore(db, use_vector_index=False)
677
+ for t in triplets:
678
+ subj = EntityNode(
679
+ name=t["subject"],
680
+ label=t.get("subject_type") or "Entity",
681
+ )
682
+ obj = EntityNode(
683
+ name=t["object"],
684
+ label=t.get("object_type") or "Entity",
685
+ )
686
+ store.upsert_nodes([subj, obj])
687
+ store.upsert_relations(
688
+ [
689
+ Relation(
690
+ label=t["predicate"],
691
+ source_id=subj.id,
692
+ target_id=obj.id,
693
+ properties=(
694
+ {"source_chunk_id": t["source_chunk_id"]}
695
+ if t.get("source_chunk_id")
696
+ else {}
697
+ ),
698
+ )
699
+ ]
700
+ )
701
+ return snap.name, len(triplets)
702
+ except Exception: # noqa: BLE001
703
+ # If replay against this snapshot failed, try the next-older.
704
+ continue
705
+ return None
706
+
707
+
496
708
  def doctor_hint_message(project_root: Path | None = None) -> str:
497
709
  """Suggest the doctor command — and call out the most likely setup issue.
498
710
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "agent-brain-cli"
3
- version = "10.0.5"
3
+ version = "10.0.7"
4
4
  description = "Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval"
5
5
  authors = ["Spillwave Solutions"]
6
6
  readme = "README.md"
@@ -27,7 +27,7 @@ httpx = "^0.28.0"
27
27
  rich = "^13.9.0"
28
28
  pyyaml = "^6.0.0"
29
29
  pydantic = "^2.10.0"
30
- agent-brain-rag = "^10.0.5"
30
+ agent-brain-rag = "^10.0.7"
31
31
 
32
32
  [tool.poetry.group.dev.dependencies]
33
33
  pytest = "^8.3.0"