grapheinstein 0.1.0__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 (42) hide show
  1. grapheinstein-0.1.0/LICENSE +21 -0
  2. grapheinstein-0.1.0/PKG-INFO +148 -0
  3. grapheinstein-0.1.0/README.md +96 -0
  4. grapheinstein-0.1.0/pyproject.toml +115 -0
  5. grapheinstein-0.1.0/setup.cfg +4 -0
  6. grapheinstein-0.1.0/src/grapheinstein/__init__.py +3 -0
  7. grapheinstein-0.1.0/src/grapheinstein/__main__.py +6 -0
  8. grapheinstein-0.1.0/src/grapheinstein/api.py +195 -0
  9. grapheinstein-0.1.0/src/grapheinstein/cli.py +938 -0
  10. grapheinstein-0.1.0/src/grapheinstein/core/__init__.py +1 -0
  11. grapheinstein-0.1.0/src/grapheinstein/core/cache.py +287 -0
  12. grapheinstein-0.1.0/src/grapheinstein/core/explain.py +344 -0
  13. grapheinstein-0.1.0/src/grapheinstein/core/graph.py +1008 -0
  14. grapheinstein-0.1.0/src/grapheinstein/core/index.py +440 -0
  15. grapheinstein-0.1.0/src/grapheinstein/core/match.py +192 -0
  16. grapheinstein-0.1.0/src/grapheinstein/core/merge.py +204 -0
  17. grapheinstein-0.1.0/src/grapheinstein/core/parsers/__init__.py +25 -0
  18. grapheinstein-0.1.0/src/grapheinstein/core/parsers/docs.py +198 -0
  19. grapheinstein-0.1.0/src/grapheinstein/core/parsers/extract.py +298 -0
  20. grapheinstein-0.1.0/src/grapheinstein/core/parsers/llm_enrich.py +387 -0
  21. grapheinstein-0.1.0/src/grapheinstein/core/parsers/llm_ollama.py +322 -0
  22. grapheinstein-0.1.0/src/grapheinstein/core/parsers/media_av.py +163 -0
  23. grapheinstein-0.1.0/src/grapheinstein/core/parsers/media_link.py +173 -0
  24. grapheinstein-0.1.0/src/grapheinstein/core/parsers/media_ocr.py +106 -0
  25. grapheinstein-0.1.0/src/grapheinstein/core/parsers/pdf.py +103 -0
  26. grapheinstein-0.1.0/src/grapheinstein/core/parsers/queries/__init__.py +141 -0
  27. grapheinstein-0.1.0/src/grapheinstein/core/parsers/registry.py +156 -0
  28. grapheinstein-0.1.0/src/grapheinstein/core/parsers/resolve.py +310 -0
  29. grapheinstein-0.1.0/src/grapheinstein/core/parsers/resolve_docs.py +145 -0
  30. grapheinstein-0.1.0/src/grapheinstein/core/path.py +593 -0
  31. grapheinstein-0.1.0/src/grapheinstein/core/query.py +706 -0
  32. grapheinstein-0.1.0/src/grapheinstein/core/references.py +85 -0
  33. grapheinstein-0.1.0/src/grapheinstein/core/visualize.py +107 -0
  34. grapheinstein-0.1.0/src/grapheinstein/serve/__init__.py +50 -0
  35. grapheinstein-0.1.0/src/grapheinstein/serve/app.py +161 -0
  36. grapheinstein-0.1.0/src/grapheinstein/utils.py +624 -0
  37. grapheinstein-0.1.0/src/grapheinstein.egg-info/PKG-INFO +148 -0
  38. grapheinstein-0.1.0/src/grapheinstein.egg-info/SOURCES.txt +40 -0
  39. grapheinstein-0.1.0/src/grapheinstein.egg-info/dependency_links.txt +1 -0
  40. grapheinstein-0.1.0/src/grapheinstein.egg-info/entry_points.txt +2 -0
  41. grapheinstein-0.1.0/src/grapheinstein.egg-info/requires.txt +30 -0
  42. grapheinstein-0.1.0/src/grapheinstein.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joe Zanini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: grapheinstein
3
+ Version: 0.1.0
4
+ Summary: Local-first project knowledge graph CLI
5
+ Author-email: Joe Zanini <jozanini@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Joezanini/grapheinstein
8
+ Project-URL: Repository, https://github.com/Joezanini/grapheinstein
9
+ Project-URL: Issues, https://github.com/Joezanini/grapheinstein/issues
10
+ Keywords: cli,knowledge-graph,code-analysis,local-first,offline,tree-sitter
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: typer>=0.12
25
+ Requires-Dist: networkx>=3.2
26
+ Requires-Dist: pathspec>=0.12
27
+ Requires-Dist: rich>=13.7
28
+ Requires-Dist: loguru>=0.7
29
+ Requires-Dist: pyyaml>=6.0
30
+ Requires-Dist: tree-sitter>=0.23
31
+ Requires-Dist: tree-sitter-python>=0.23
32
+ Requires-Dist: tree-sitter-javascript>=0.23
33
+ Requires-Dist: tree-sitter-typescript>=0.23
34
+ Requires-Dist: tree-sitter-java>=0.23
35
+ Requires-Dist: tree-sitter-go>=0.23
36
+ Requires-Dist: tree-sitter-rust>=0.23
37
+ Requires-Dist: tree-sitter-cpp>=0.23
38
+ Requires-Dist: tree-sitter-sql>=0.3
39
+ Requires-Dist: pymupdf>=1.24
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=8.0; extra == "dev"
42
+ Requires-Dist: httpx>=0.27; extra == "dev"
43
+ Requires-Dist: ruff>=0.8; extra == "dev"
44
+ Provides-Extra: media
45
+ Requires-Dist: pytesseract>=0.3.10; extra == "media"
46
+ Requires-Dist: Pillow>=10.0; extra == "media"
47
+ Requires-Dist: faster-whisper>=1.0; extra == "media"
48
+ Provides-Extra: serve
49
+ Requires-Dist: fastapi>=0.115; extra == "serve"
50
+ Requires-Dist: uvicorn[standard]>=0.30; extra == "serve"
51
+ Dynamic: license-file
52
+
53
+ # Grapheinstein
54
+
55
+ Local-first CLI that maps a project folder into a portable `graph.json` knowledge graph for AI agents.
56
+
57
+ Offline by design — indexing, explain, path, and query use the local filesystem and optional local models (e.g. Ollama). No cloud APIs required.
58
+
59
+ Schema version: **6.0.0**.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install grapheinstein
65
+ # Optional local HTTP API:
66
+ # pip install 'grapheinstein[serve]'
67
+ # Optional OCR / audio extras:
68
+ # pip install 'grapheinstein[media]'
69
+ ```
70
+
71
+ ### From source
72
+
73
+ ```bash
74
+ python -m venv .venv
75
+ source .venv/bin/activate
76
+ pip install -e ".[dev]"
77
+ # Optional local HTTP API:
78
+ # pip install -e ".[dev,serve]"
79
+ ```
80
+
81
+ ## Quick start
82
+
83
+ ```bash
84
+ # Create a commented starter config (~/.grapheinstein/config.yaml by default)
85
+ grapheinstein init
86
+ # or: grapheinstein init --output ./grapheinstein.yaml
87
+
88
+ # Index a project (default command)
89
+ grapheinstein /path/to/project --output graph.json
90
+
91
+ # Explicit index with project config
92
+ grapheinstein index /path/to/project -o graph.json --config ./grapheinstein.yaml
93
+
94
+ # Explain / path / query over an existing graph
95
+ grapheinstein explain "authentication" -i graph.json -o explain.json --no-summary
96
+ grapheinstein path "login" "database" -i graph.json --no-llm-explain
97
+ grapheinstein query "How does auth work?" -i graph.json -o sub.json --no-answer
98
+
99
+ # Console summary / status
100
+ grapheinstein visualize --input graph.json
101
+ grapheinstein status --output graph.json
102
+
103
+ # Optional local HTTP (requires: pip install 'grapheinstein[serve]')
104
+ grapheinstein serve --port 8000
105
+ ```
106
+
107
+ ## Agent integration
108
+
109
+ Agents and Cursor slash-commands should call the Python API (same semantics as the CLI):
110
+
111
+ ```python
112
+ from grapheinstein.api import index, query
113
+ ```
114
+
115
+ Optional HTTP: `POST /index` and `POST /query` via `grapheinstein serve` (loopback by default).
116
+
117
+ See [docs/agent-integration.md](docs/agent-integration.md) for copy-paste examples and CLI ↔ Python ↔ HTTP parity.
118
+
119
+ ## Configuration
120
+
121
+ Config precedence: CLI flags > `--config` file > `~/.grapheinstein/config.yaml` > built-ins.
122
+
123
+ Key settings (see `grapheinstein init` for a full commented template):
124
+
125
+ | Key | Purpose |
126
+ |-----|---------|
127
+ | `ignored_patterns` | Extra gitignore-style paths beyond `.gitignore` |
128
+ | `embedding_model` | Local model for embeddings |
129
+ | `llm_model` | Local model for enrichment / answers |
130
+ | `max_file_size` | Skip parsing files larger than this (bytes; default 10 MiB) |
131
+ | `cache_dir` | Local cache for ASTs / chunks / embeddings (default `~/.grapheinstein/cache`) |
132
+
133
+ Re-indexing an unchanged project reuses the cache under `cache_dir` for faster runs.
134
+
135
+ ## Validation
136
+
137
+ ```bash
138
+ pytest
139
+ ruff check src tests
140
+ ```
141
+
142
+ Feature quickstarts live under `specs/*/quickstart.md` (e.g. `specs/011-config-cache-init/quickstart.md`).
143
+
144
+ ## Contributing
145
+
146
+ Community contributions are welcome — bug fixes, docs, tests, and features that fit the project’s local-first CLI goals.
147
+
148
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, validation, and how to propose a change.
@@ -0,0 +1,96 @@
1
+ # Grapheinstein
2
+
3
+ Local-first CLI that maps a project folder into a portable `graph.json` knowledge graph for AI agents.
4
+
5
+ Offline by design — indexing, explain, path, and query use the local filesystem and optional local models (e.g. Ollama). No cloud APIs required.
6
+
7
+ Schema version: **6.0.0**.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install grapheinstein
13
+ # Optional local HTTP API:
14
+ # pip install 'grapheinstein[serve]'
15
+ # Optional OCR / audio extras:
16
+ # pip install 'grapheinstein[media]'
17
+ ```
18
+
19
+ ### From source
20
+
21
+ ```bash
22
+ python -m venv .venv
23
+ source .venv/bin/activate
24
+ pip install -e ".[dev]"
25
+ # Optional local HTTP API:
26
+ # pip install -e ".[dev,serve]"
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```bash
32
+ # Create a commented starter config (~/.grapheinstein/config.yaml by default)
33
+ grapheinstein init
34
+ # or: grapheinstein init --output ./grapheinstein.yaml
35
+
36
+ # Index a project (default command)
37
+ grapheinstein /path/to/project --output graph.json
38
+
39
+ # Explicit index with project config
40
+ grapheinstein index /path/to/project -o graph.json --config ./grapheinstein.yaml
41
+
42
+ # Explain / path / query over an existing graph
43
+ grapheinstein explain "authentication" -i graph.json -o explain.json --no-summary
44
+ grapheinstein path "login" "database" -i graph.json --no-llm-explain
45
+ grapheinstein query "How does auth work?" -i graph.json -o sub.json --no-answer
46
+
47
+ # Console summary / status
48
+ grapheinstein visualize --input graph.json
49
+ grapheinstein status --output graph.json
50
+
51
+ # Optional local HTTP (requires: pip install 'grapheinstein[serve]')
52
+ grapheinstein serve --port 8000
53
+ ```
54
+
55
+ ## Agent integration
56
+
57
+ Agents and Cursor slash-commands should call the Python API (same semantics as the CLI):
58
+
59
+ ```python
60
+ from grapheinstein.api import index, query
61
+ ```
62
+
63
+ Optional HTTP: `POST /index` and `POST /query` via `grapheinstein serve` (loopback by default).
64
+
65
+ See [docs/agent-integration.md](docs/agent-integration.md) for copy-paste examples and CLI ↔ Python ↔ HTTP parity.
66
+
67
+ ## Configuration
68
+
69
+ Config precedence: CLI flags > `--config` file > `~/.grapheinstein/config.yaml` > built-ins.
70
+
71
+ Key settings (see `grapheinstein init` for a full commented template):
72
+
73
+ | Key | Purpose |
74
+ |-----|---------|
75
+ | `ignored_patterns` | Extra gitignore-style paths beyond `.gitignore` |
76
+ | `embedding_model` | Local model for embeddings |
77
+ | `llm_model` | Local model for enrichment / answers |
78
+ | `max_file_size` | Skip parsing files larger than this (bytes; default 10 MiB) |
79
+ | `cache_dir` | Local cache for ASTs / chunks / embeddings (default `~/.grapheinstein/cache`) |
80
+
81
+ Re-indexing an unchanged project reuses the cache under `cache_dir` for faster runs.
82
+
83
+ ## Validation
84
+
85
+ ```bash
86
+ pytest
87
+ ruff check src tests
88
+ ```
89
+
90
+ Feature quickstarts live under `specs/*/quickstart.md` (e.g. `specs/011-config-cache-init/quickstart.md`).
91
+
92
+ ## Contributing
93
+
94
+ Community contributions are welcome — bug fixes, docs, tests, and features that fit the project’s local-first CLI goals.
95
+
96
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, validation, and how to propose a change.
@@ -0,0 +1,115 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "grapheinstein"
7
+ version = "0.1.0"
8
+ description = "Local-first project knowledge graph CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Joe Zanini", email = "jozanini@users.noreply.github.com" },
15
+ ]
16
+ keywords = [
17
+ "cli",
18
+ "knowledge-graph",
19
+ "code-analysis",
20
+ "local-first",
21
+ "offline",
22
+ "tree-sitter",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Topic :: Software Development",
34
+ "Topic :: Utilities",
35
+ ]
36
+ dependencies = [
37
+ "typer>=0.12",
38
+ "networkx>=3.2",
39
+ "pathspec>=0.12",
40
+ "rich>=13.7",
41
+ "loguru>=0.7",
42
+ "pyyaml>=6.0",
43
+ "tree-sitter>=0.23",
44
+ "tree-sitter-python>=0.23",
45
+ "tree-sitter-javascript>=0.23",
46
+ "tree-sitter-typescript>=0.23",
47
+ "tree-sitter-java>=0.23",
48
+ "tree-sitter-go>=0.23",
49
+ "tree-sitter-rust>=0.23",
50
+ "tree-sitter-cpp>=0.23",
51
+ "tree-sitter-sql>=0.3",
52
+ "pymupdf>=1.24",
53
+ ]
54
+
55
+ [project.optional-dependencies]
56
+ dev = ["pytest>=8.0", "httpx>=0.27", "ruff>=0.8"]
57
+ media = [
58
+ "pytesseract>=0.3.10",
59
+ "Pillow>=10.0",
60
+ "faster-whisper>=1.0",
61
+ ]
62
+ serve = [
63
+ "fastapi>=0.115",
64
+ "uvicorn[standard]>=0.30",
65
+ ]
66
+
67
+ [project.scripts]
68
+ grapheinstein = "grapheinstein.cli:app"
69
+
70
+ [project.urls]
71
+ Homepage = "https://github.com/Joezanini/grapheinstein"
72
+ Repository = "https://github.com/Joezanini/grapheinstein"
73
+ Issues = "https://github.com/Joezanini/grapheinstein/issues"
74
+
75
+ [tool.setuptools.packages.find]
76
+ where = ["src"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ pythonpath = ["src"]
81
+
82
+ [tool.ruff]
83
+ target-version = "py311"
84
+ line-length = 100
85
+ src = ["src", "tests"]
86
+ exclude = [
87
+ ".venv",
88
+ "venv",
89
+ "build",
90
+ "dist",
91
+ "tests/fixtures",
92
+ ]
93
+
94
+ [tool.ruff.lint]
95
+ select = [
96
+ "E", # pycodestyle errors
97
+ "W", # pycodestyle warnings
98
+ "F", # Pyflakes
99
+ "I", # isort
100
+ "UP", # pyupgrade
101
+ "B", # flake8-bugbear
102
+ ]
103
+ ignore = [
104
+ "E501", # line length: keep readable long strings/CLI help without noisy failures
105
+ "B008", # function calls in defaults (Typer / FastAPI patterns)
106
+ ]
107
+
108
+ [tool.ruff.lint.per-file-ignores]
109
+ "tests/**" = [
110
+ "B017", # pytest.raises(Exception) used intentionally in a few contracts
111
+ ]
112
+
113
+ [tool.ruff.format]
114
+ quote-style = "double"
115
+ indent-style = "space"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Grapheinstein: local-first project knowledge graph CLI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow `python -m grapheinstein`."""
2
+
3
+ from grapheinstein.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app() # argv-rewriting entry (supports bare project path)
@@ -0,0 +1,195 @@
1
+ """Public Python API for agent / slash-command integration.
2
+
3
+ Importing this module MUST NOT require FastAPI or Uvicorn.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Sequence
9
+ from dataclasses import asdict, dataclass, is_dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from grapheinstein.core.cache import CacheStore
14
+ from grapheinstein.core.graph import GraphError, GraphStats, load_artifact
15
+ from grapheinstein.core.index import MediaExtrasError, index_project
16
+ from grapheinstein.core.parsers import LanguageError, parse_languages_csv
17
+ from grapheinstein.core.query import (
18
+ EmptyCorpusError,
19
+ NoEvidenceError,
20
+ QueryError,
21
+ run_query,
22
+ )
23
+ from grapheinstein.utils import ConfigError, load_config, setup_logging
24
+
25
+ __all__ = [
26
+ "IndexResult",
27
+ "index",
28
+ "query",
29
+ "stats_to_dict",
30
+ "ConfigError",
31
+ "GraphError",
32
+ "MediaExtrasError",
33
+ "QueryError",
34
+ "EmptyCorpusError",
35
+ "NoEvidenceError",
36
+ ]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class IndexResult:
41
+ """Result of a successful API index call."""
42
+
43
+ output_path: Path
44
+ stats: GraphStats | dict[str, Any]
45
+ artifact: dict[str, Any] | None = None
46
+
47
+
48
+ def _stats_as_dict(stats: GraphStats | dict[str, Any]) -> dict[str, Any]:
49
+ if isinstance(stats, dict):
50
+ return dict(stats)
51
+ if is_dataclass(stats):
52
+ return asdict(stats)
53
+ return {
54
+ "total_nodes": getattr(stats, "total_nodes", 0),
55
+ "graph_path": str(getattr(stats, "graph_path", "")),
56
+ }
57
+
58
+
59
+ def _normalize_languages(
60
+ languages: str | Sequence[str] | None,
61
+ ) -> list[str] | None:
62
+ if languages is None:
63
+ return None
64
+ if isinstance(languages, str):
65
+ try:
66
+ return list(parse_languages_csv(languages))
67
+ except LanguageError as exc:
68
+ raise ConfigError(str(exc)) from exc
69
+ try:
70
+ return list(languages)
71
+ except TypeError as exc:
72
+ raise ConfigError("languages must be a string or sequence of strings") from exc
73
+
74
+
75
+ def index(
76
+ project_path: str | Path,
77
+ *,
78
+ output: str | Path | None = None,
79
+ config: str | Path | None = None,
80
+ languages: str | Sequence[str] | None = None,
81
+ include_docs: bool = False,
82
+ include_pdfs: bool = False,
83
+ transcribe_media: bool = False,
84
+ enrich_llm: bool = False,
85
+ llm_model: str | None = None,
86
+ llm_base_url: str | None = None,
87
+ embedding_model: str | None = None,
88
+ compress: bool = False,
89
+ versioned: bool = False,
90
+ include_artifact: bool = False,
91
+ show_progress: bool = False,
92
+ ) -> IndexResult:
93
+ """
94
+ Index a project folder into a portable graph (CLI `index` semantics).
95
+
96
+ Raises FileNotFoundError, NotADirectoryError, OSError, ConfigError,
97
+ GraphError, MediaExtrasError on hard failures — never returns an empty success.
98
+ """
99
+ languages_override = _normalize_languages(languages)
100
+ cfg = load_config(
101
+ config_path=Path(config).expanduser() if config is not None else None,
102
+ output_override=Path(output).expanduser() if output is not None else None,
103
+ languages_override=languages_override,
104
+ llm_model_override=llm_model,
105
+ llm_base_url_override=llm_base_url,
106
+ embedding_model_override=embedding_model,
107
+ compress_override=True if compress else None,
108
+ versioned_override=True if versioned else None,
109
+ )
110
+ setup_logging(cfg.log_level)
111
+ output_path = Path(cfg.output)
112
+
113
+ written, stats = index_project(
114
+ Path(project_path),
115
+ output_path,
116
+ languages=list(cfg.languages),
117
+ include_docs=include_docs,
118
+ include_pdfs=include_pdfs,
119
+ transcribe_media=transcribe_media,
120
+ enrich_llm=enrich_llm,
121
+ llm_model=cfg.llm_model,
122
+ llm_base_url=cfg.llm_base_url,
123
+ llm_confidence_threshold=cfg.llm_confidence_threshold,
124
+ compress=cfg.compress,
125
+ versioned=cfg.versioned,
126
+ ignored_patterns=list(cfg.ignored_patterns),
127
+ max_file_size=cfg.max_file_size,
128
+ cache_dir=cfg.cache_dir,
129
+ embedding_model=cfg.embedding_model,
130
+ show_progress=show_progress,
131
+ )
132
+
133
+ artifact: dict[str, Any] | None = None
134
+ if include_artifact:
135
+ artifact = load_artifact(written)
136
+
137
+ return IndexResult(output_path=written, stats=stats, artifact=artifact)
138
+
139
+
140
+ def query(
141
+ question: str,
142
+ *,
143
+ input: str | Path,
144
+ output: str | Path | None = None,
145
+ config: str | Path | None = None,
146
+ k: int | None = None,
147
+ hops: int | None = None,
148
+ match_threshold: float | None = None,
149
+ no_answer: bool = False,
150
+ llm_model: str | None = None,
151
+ llm_base_url: str | None = None,
152
+ embedding_model: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ """
155
+ Answer a natural-language question over a graph (CLI `query` semantics).
156
+
157
+ Returns the query-answer JSON envelope (schema_version 1.0.0).
158
+ """
159
+ input_path = Path(input).expanduser()
160
+ if output is not None:
161
+ output_path = Path(output).expanduser()
162
+ else:
163
+ output_path = input_path.parent / "subgraph.json"
164
+
165
+ cfg = load_config(
166
+ config_path=Path(config).expanduser() if config is not None else None,
167
+ llm_model_override=llm_model,
168
+ llm_base_url_override=llm_base_url,
169
+ embedding_model_override=embedding_model,
170
+ query_k_override=k,
171
+ query_hops_override=hops,
172
+ query_match_threshold_override=match_threshold,
173
+ )
174
+ setup_logging(cfg.log_level)
175
+
176
+ result = run_query(
177
+ question,
178
+ input_path,
179
+ output_path,
180
+ k=cfg.query_k,
181
+ hops=cfg.query_hops,
182
+ match_threshold=cfg.query_match_threshold,
183
+ node_cap=cfg.query_node_cap,
184
+ want_answer=not no_answer,
185
+ llm_model=cfg.llm_model,
186
+ llm_base_url=cfg.llm_base_url,
187
+ embedding_model=cfg.embedding_model,
188
+ cache=CacheStore(cfg.cache_dir),
189
+ )
190
+ return dict(result.answer_envelope)
191
+
192
+
193
+ def stats_to_dict(stats: GraphStats | dict[str, Any]) -> dict[str, Any]:
194
+ """Serialize index stats for HTTP / agents."""
195
+ return _stats_as_dict(stats)