graphnav 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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphnav
3
+ Version: 0.1.0
4
+ Summary: Knowledge-graph context injection for AI coding agents in monorepos
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: graphifyy>=0.8
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=8; extra == "dev"
9
+ Requires-Dist: anthropic>=0.40; extra == "dev"
@@ -0,0 +1,259 @@
1
+ # codex-graph
2
+
3
+ **Token-cheap AI coding for monorepos.** Builds a graphify knowledge graph of your codebase, then gives every AI coding agent (GitHub Copilot, Claude Code, OpenAI Codex) a minimal, targeted context pack instead of the whole repo.
4
+
5
+ ---
6
+
7
+ ## The problem
8
+
9
+ AI coding agents default to exploring the filesystem with `find`/`ls`/`cat`, reading entire files, and burning tokens on irrelevant code. In a monorepo this compounds: every request pulls context from every service.
10
+
11
+ codex-graph solves this by:
12
+
13
+ 1. Extracting a knowledge graph (symbols, call edges, cross-service links) once, up front
14
+ 2. Giving agents a **one-command retrieval path** that returns only the files and `file:line` locations relevant to the current task
15
+ 3. Writing instruction files that explicitly direct agents to use retrieval first — and ban raw filesystem exploration
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install git+https://github.com/Amogh887/leveraging-graphify.git
23
+ ```
24
+
25
+ Requires Python ≥ 3.11. Pulls `graphifyy` (the `graphify` binary) automatically.
26
+
27
+ **API key:** Place a `.env` file anywhere up the directory tree from your project (or inside any service subfolder). codex-graph walks up and down to find it:
28
+
29
+ ```
30
+ ANTHROPIC_KEY=sk-ant-...
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quickstart
36
+
37
+ ```bash
38
+ # In your monorepo root — detects services, builds graphs, writes agent instructions
39
+ codex-graph map
40
+
41
+ # Get a context pack for a task (free, no LLM, ~instant)
42
+ codex-graph context "add a critique scoring function to the coach"
43
+
44
+ # Keep graphs live as you edit
45
+ codex-graph watch
46
+ ```
47
+
48
+ After `map`, every AI agent in the repo has access to:
49
+
50
+ - **`CLAUDE.md`** — picked up by Claude Code
51
+ - **`AGENTS.md`** — picked up by OpenAI Codex CLI
52
+ - **`.github/copilot-instructions.md`** — picked up by GitHub Copilot
53
+ - **`<service>/graphify-out/SYMBOLS.md`** — symbol→`file:line` index per service
54
+ - **`<service>/graphify-out/BRIDGES.md`** — exact cross-service call sites with line numbers
55
+ - **`graphify-out/MONOREPO_MAP.md`** — overview of all services and their connections
56
+
57
+ ---
58
+
59
+ ## Commands
60
+
61
+ ### `codex-graph map`
62
+
63
+ Builds the knowledge graph and generates all agent instruction files.
64
+
65
+ ```
66
+ codex-graph map [--root PATH] [--backend BACKEND] [--dry-run]
67
+ ```
68
+
69
+ | Flag | Default | Description |
70
+ |---|---|---|
71
+ | `--root` | `.` | Monorepo root directory |
72
+ | `--backend` | `claude` | LLM backend for extraction: `claude`, `openai`, `gemini`, `deepseek`, `ollama` |
73
+ | `--dry-run` | off | Detect services and print the plan without calling graphify |
74
+
75
+ What it does:
76
+ 1. Auto-detects service boundaries (by marker files **and** by source code presence)
77
+ 2. Extracts a single overarching knowledge graph of the whole repo via graphify
78
+ 3. Partitions it into per-service local graphs
79
+ 4. Analyzes cross-service edges and writes `BRIDGES.md` per service
80
+ 5. Writes `SYMBOLS.md`, `MONOREPO_MAP.md`, and the coding playbook to `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`
81
+
82
+ ---
83
+
84
+ ### `codex-graph context`
85
+
86
+ Prints a token-budgeted context pack for a coding task. **No LLM call — free and instant.**
87
+
88
+ ```
89
+ codex-graph context "<task>" [--root PATH] [--budget N] [--files N]
90
+ ```
91
+
92
+ | Flag | Default | Description |
93
+ |---|---|---|
94
+ | `--budget` | `2000` | Approximate token budget for the output |
95
+ | `--files` | `8` | Max number of files to include |
96
+ | `--root` | `.` | Repo root |
97
+
98
+ Example output:
99
+
100
+ ```
101
+ # Context for: add a critique scoring function to the coach
102
+
103
+ ## Open only these files
104
+ - backend/coach.py — generate_response() L145, practice_critique() L326
105
+ - eval/run_eval.py — run_prompts_on_dataset() L78, judge_responses() L119
106
+
107
+ ## Cross-service impact
108
+ - eval/run_eval.py:run_prompts_on_dataset() L78 --calls--> backend/coach.py:generate_response() L145
109
+
110
+ ## Next
111
+ Read only the file:line regions above. Before changing a symbol under
112
+ Cross-service impact, run `graphify affected "<symbol>"`. Then run the tests.
113
+ ```
114
+
115
+ Works on single-service repos too (the Cross-service section is omitted).
116
+
117
+ ---
118
+
119
+ ### `codex-graph watch`
120
+
121
+ Long-running daemon. Watches the repo for file changes and keeps all graphs, symbol maps, bridge notes, and agent instructions up to date.
122
+
123
+ ```
124
+ codex-graph watch [--root PATH] [--backend BACKEND]
125
+ ```
126
+
127
+ Press `Ctrl-C` to stop cleanly.
128
+
129
+ ---
130
+
131
+ ### `codex-graph` (no subcommand)
132
+
133
+ If run with no arguments in a monorepo root, auto-detects services and runs `map` automatically. If a prompt is given, falls through to the context-injection path for the Codex CLI.
134
+
135
+ ---
136
+
137
+ ## Service detection
138
+
139
+ codex-graph detects a subdirectory as a service if it contains:
140
+
141
+ - A marker file: `package.json`, `pyproject.toml`, `requirements.txt`, `go.mod`, `Cargo.toml`, `tsconfig.json`, `Gemfile`, and more, **or**
142
+ - Any source code files (`.py`, `.ts`, `.tsx`, `.js`, `.go`, `.rs`, `.java`, etc.)
143
+
144
+ Skipped automatically: `node_modules`, `dist`, `build`, `graphify-out`, `__pycache__`, `.git`, dotdirs, and other non-source directories.
145
+
146
+ ---
147
+
148
+ ## Generated files
149
+
150
+ ### `CLAUDE.md` / `AGENTS.md` / `.github/copilot-instructions.md`
151
+
152
+ All three contain the same managed block — the coding playbook. Content is written between `<!-- codex-graph:start -->` / `<!-- codex-graph:end -->` markers so re-running `map` updates only the block and preserves any hand-written content outside it.
153
+
154
+ The playbook instructs agents to:
155
+
156
+ 1. Read `MONOREPO_MAP.md` first for any non-trivial task
157
+ 2. Run `codex-graph context "<task>"` instead of exploring with `find`/`ls`/`cat`
158
+ 3. Open only the returned `file:line` regions
159
+ 4. Check `graphify affected` before changing cross-service symbols
160
+ 5. Skip all of the above for single-line edits
161
+
162
+ ### `<service>/graphify-out/SYMBOLS.md`
163
+
164
+ Compact symbol index for the service. Example:
165
+
166
+ ```
167
+ # Symbols: backend
168
+
169
+ ## coach.py
170
+ - generate_response() — L145
171
+ - practice_critique() — L326
172
+ - _parse_json_response() — L79
173
+ ```
174
+
175
+ Much smaller than the raw `graph.json` (tens of bytes per symbol vs. kilobytes per node).
176
+
177
+ ### `<service>/graphify-out/BRIDGES.md`
178
+
179
+ Cross-service call sites with exact line numbers on both sides.
180
+
181
+ ```
182
+ | Local File | Symbol | Loc | Relation | → Service | Remote File | Remote Symbol | Loc |
183
+ |---|---|---|---|---|---|---|---|
184
+ | run_eval.py | run_prompts_on_dataset() | L78 | calls | backend | backend/coach.py | generate_response() | L145 |
185
+ ```
186
+
187
+ Includes a note to run `graphify affected "<symbol>"` before editing any listed symbol.
188
+
189
+ ### `graphify-out/MONOREPO_MAP.md`
190
+
191
+ Overview of all services and which services each connects to.
192
+
193
+ ```
194
+ | Service | Graph | Bridges To |
195
+ |---|---|---|
196
+ | api | api/graphify-out/graph.json | _none_ |
197
+ | backend | backend/graphify-out/graph.json | api |
198
+ | eval | eval/graphify-out/graph.json | backend |
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Configuration
204
+
205
+ Place a `config.toml` in the project root (or pass `--config PATH`):
206
+
207
+ ```toml
208
+ [mono]
209
+ graphify_backend = "claude" # LLM backend for extraction
210
+ watch_poll_interval = 3.0 # seconds between mtime checks in watch mode
211
+ context_budget_tokens = 2000 # token budget for codex-graph context output
212
+ context_top_files = 8 # max files returned by context command
213
+
214
+ [graph]
215
+ skip_patterns = ["node_modules", ".git", "graphify-out", "playwright-report"]
216
+ ```
217
+
218
+ ---
219
+
220
+ ## How cross-service bridges work
221
+
222
+ codex-graph extracts **one overarching graph** of the whole repo (not one per service). This means graphify's AST and semantic extraction can find call edges that cross service boundaries — something a per-service extraction followed by a union merge can never do.
223
+
224
+ The overarching graph is then partitioned into per-service local graphs for navigation. Bridges are derived from the overarching graph where an edge's endpoints belong to different services.
225
+
226
+ **Note:** Bridges only appear for direct code references (imports, function calls). Services that communicate over HTTP (e.g. a React frontend calling a Python backend via `fetch`) will correctly show zero bridges — the connection exists at the protocol level, not the code level.
227
+
228
+ ---
229
+
230
+ ## Team setup
231
+
232
+ Every team member runs one command after cloning:
233
+
234
+ ```bash
235
+ pip install git+https://github.com/Amogh887/leveraging-graphify.git
236
+ ```
237
+
238
+ Drop a `.env` with your API key anywhere in or above the repo:
239
+
240
+ ```
241
+ ANTHROPIC_KEY=sk-ant-...
242
+ ```
243
+
244
+ Then:
245
+
246
+ ```bash
247
+ codex-graph map # one-time setup, or re-run after large refactors
248
+ codex-graph watch # optional: keep graphs live during active development
249
+ ```
250
+
251
+ The generated `CLAUDE.md`, `AGENTS.md`, and `.github/copilot-instructions.md` can be committed to the repo so teammates get the agent instructions without needing to re-run `map`.
252
+
253
+ ---
254
+
255
+ ## Requirements
256
+
257
+ - Python ≥ 3.11
258
+ - `graphifyy` ≥ 0.8 (installed automatically)
259
+ - An API key for your chosen LLM backend (only needed for `map` / `watch`; `context` is free)
@@ -0,0 +1,10 @@
1
+ class GraphNotFoundError(Exception):
2
+ pass
3
+
4
+
5
+ class CodexNotFoundError(Exception):
6
+ pass
7
+
8
+
9
+ class CodexTimeoutError(Exception):
10
+ pass
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from codex_graph import CodexNotFoundError, CodexTimeoutError, GraphNotFoundError
8
+ from codex_graph.config import load_config
9
+ from codex_graph.graph_query import load_index, query_files
10
+ from codex_graph import runner
11
+
12
+
13
+ def _run_mono_command(cmd: str, argv: list[str]) -> None:
14
+ from codex_graph import multirepo
15
+
16
+ parser = argparse.ArgumentParser(
17
+ prog=f"codex-graph {cmd}",
18
+ description={
19
+ "map": "Build per-service graphs and cross-service bridge notes for a monorepo",
20
+ "watch": "Watch for file changes and keep per-service graphs and bridge notes up-to-date",
21
+ }[cmd],
22
+ )
23
+ parser.add_argument("--root", default=".", metavar="PATH", help="Monorepo root directory (default: current directory)")
24
+ parser.add_argument("--backend", default=None, metavar="BACKEND", help="graphify LLM backend (claude|openai|gemini|deepseek|ollama)")
25
+ parser.add_argument("--config", default=None, metavar="PATH", help="Path to config.toml")
26
+ if cmd == "map":
27
+ parser.add_argument("--dry-run", action="store_true", help="Detect services and print the plan without invoking graphify")
28
+
29
+ args = parser.parse_args(argv)
30
+ cfg = load_config(args.config)
31
+
32
+ if cmd == "map":
33
+ rc = multirepo.run_map(
34
+ root=args.root,
35
+ mono_cfg=cfg.mono,
36
+ backend_override=args.backend,
37
+ dry_run=args.dry_run,
38
+ )
39
+ else:
40
+ rc = multirepo.run_watch(
41
+ root=args.root,
42
+ mono_cfg=cfg.mono,
43
+ backend_override=args.backend,
44
+ )
45
+ sys.exit(rc)
46
+
47
+
48
+ def _auto_map_if_needed(cfg_path: str | None) -> None:
49
+ from codex_graph import multirepo
50
+ from codex_graph.config import load_config
51
+
52
+ cfg = load_config(cfg_path)
53
+ root = os.path.abspath(".")
54
+ services = multirepo.detect_services(root, cfg.mono.marker_files)
55
+ if not services:
56
+ return
57
+
58
+ names = ", ".join(s.name for s in services)
59
+ print(f"[codex-graph] Detected {len(services)} service(s): {names}")
60
+ print(f"[codex-graph] Running 'codex-graph map' to build knowledge graphs ...", file=sys.stderr)
61
+ rc = multirepo.run_map(root=root, mono_cfg=cfg.mono)
62
+ sys.exit(rc)
63
+
64
+
65
+ def _run_context_command(argv: list[str]) -> None:
66
+ from codex_graph import multirepo
67
+
68
+ parser = argparse.ArgumentParser(
69
+ prog="codex-graph context",
70
+ description="Print a token-budgeted context pack (files + symbol locations + cross-service impact) for a coding task",
71
+ )
72
+ parser.add_argument("task", nargs="?", help="The coding task, in natural language")
73
+ parser.add_argument("--root", default=".", metavar="PATH", help="Repo root (default: current directory)")
74
+ parser.add_argument("--budget", type=int, default=None, metavar="N", help="Approx token budget for the pack")
75
+ parser.add_argument("--files", type=int, default=None, metavar="N", help="Max number of files to include")
76
+ parser.add_argument("--config", default=None, metavar="PATH", help="Path to config.toml")
77
+ args = parser.parse_args(argv)
78
+
79
+ task = args.task
80
+ if not task and not sys.stdin.isatty():
81
+ task = sys.stdin.read().strip()
82
+ if not task:
83
+ parser.print_help()
84
+ sys.exit(1)
85
+
86
+ cfg = load_config(args.config)
87
+ pack = multirepo.build_context_pack(
88
+ root=args.root,
89
+ task=task,
90
+ top_files=args.files if args.files is not None else cfg.mono.context_top_files,
91
+ budget_tokens=args.budget if args.budget is not None else cfg.mono.context_budget_tokens,
92
+ skip_patterns=cfg.graph.skip_patterns,
93
+ )
94
+ print(pack)
95
+ sys.exit(0)
96
+
97
+
98
+ def _run_graph_query_command(kind: str, argv: list[str]) -> None:
99
+ from codex_graph import multirepo
100
+ from codex_graph.graph_nav import GraphNav
101
+
102
+ parser = argparse.ArgumentParser(prog=f"codex-graph {kind}")
103
+ parser.add_argument("term", nargs="?", help="query (find) or symbol (neighbors)")
104
+ parser.add_argument("--root", default=".", metavar="PATH")
105
+ parser.add_argument("--config", default=None, metavar="PATH")
106
+ args = parser.parse_args(argv)
107
+ if not args.term:
108
+ parser.print_help()
109
+ sys.exit(1)
110
+
111
+ cfg = load_config(args.config)
112
+ graph_path = multirepo._overarching_graph_path(os.path.abspath(args.root))
113
+ if not os.path.exists(graph_path):
114
+ print(f"Error: no knowledge graph at {graph_path}. Run `codex-graph map` first.", file=sys.stderr)
115
+ sys.exit(2)
116
+ nav = GraphNav(graph_path, cfg.graph.skip_patterns)
117
+
118
+ if kind == "find":
119
+ hits = nav.find_symbols(args.term, k=10)
120
+ if not hits:
121
+ print("(no matches)")
122
+ for h in hits:
123
+ print(f"{h['symbol']} — {h['file']}:{h['loc']}")
124
+ else:
125
+ r = nav.neighbors(args.term)
126
+ if not r.get("found", True):
127
+ print("(symbol not found)")
128
+ sys.exit(0)
129
+ print(f"{r['symbol']} defined at {r['defined_at']}")
130
+ if r.get("callers"):
131
+ print("callers:")
132
+ for c in r["callers"]:
133
+ print(" " + c)
134
+ if r.get("callees"):
135
+ print("calls:")
136
+ for c in r["callees"]:
137
+ print(" " + c)
138
+ sys.exit(0)
139
+
140
+
141
+ def main() -> None:
142
+ if len(sys.argv) > 1 and sys.argv[1] in ("map", "watch"):
143
+ _run_mono_command(sys.argv[1], sys.argv[2:])
144
+ return
145
+ if len(sys.argv) > 1 and sys.argv[1] == "context":
146
+ _run_context_command(sys.argv[2:])
147
+ return
148
+ if len(sys.argv) > 1 and sys.argv[1] in ("find", "neighbors"):
149
+ _run_graph_query_command(sys.argv[1], sys.argv[2:])
150
+ return
151
+
152
+ parser = argparse.ArgumentParser(
153
+ prog="codex-graph",
154
+ description=(
155
+ "Codex CLI with knowledge-graph context injection for monorepos.\n\n"
156
+ "First-run (after pip install): just run 'codex-graph' or 'codex-graph map'\n"
157
+ "in your monorepo root — services are auto-detected and graphs are built.\n\n"
158
+ "Subcommands:\n"
159
+ " map Build per-service graphs and cross-service bridge notes\n"
160
+ " watch Keep graphs and bridge notes up-to-date as files change"
161
+ ),
162
+ formatter_class=argparse.RawDescriptionHelpFormatter,
163
+ )
164
+ parser.add_argument("prompt", nargs="?", help="Natural language task prompt")
165
+ parser.add_argument("--config", default=None, metavar="PATH", help="Path to config.toml")
166
+ parser.add_argument("--top-k", type=int, default=None, metavar="N", help="Number of files to inject as context")
167
+ parser.add_argument("--graph", default=None, metavar="PATH", help="Path to graph.json")
168
+ parser.add_argument("--dry-run", action="store_true", help="Print enriched prompt without calling codex")
169
+ parser.add_argument("--list-files", action="store_true", help="Print ranked files and scores, then exit")
170
+ parser.add_argument("--no-context", action="store_true", help="Pass prompt to codex with no graph context")
171
+
172
+ args = parser.parse_args()
173
+
174
+ prompt = args.prompt
175
+ if not prompt:
176
+ if sys.stdin.isatty():
177
+ _auto_map_if_needed(args.config)
178
+ parser.print_help()
179
+ sys.exit(1)
180
+ prompt = sys.stdin.read().strip()
181
+ if not prompt:
182
+ parser.print_help()
183
+ sys.exit(1)
184
+
185
+ cfg = load_config(args.config)
186
+
187
+ if args.top_k is not None:
188
+ cfg.query.top_k = args.top_k
189
+ if args.graph is not None:
190
+ cfg.graph.path = args.graph
191
+
192
+ project_root = os.path.abspath(cfg.graph.project_root)
193
+ graph_path = (
194
+ cfg.graph.path
195
+ if os.path.isabs(cfg.graph.path)
196
+ else os.path.join(os.getcwd(), cfg.graph.path)
197
+ )
198
+
199
+ if args.no_context:
200
+ ranked = []
201
+ else:
202
+ try:
203
+ index = load_index(graph_path, cfg.graph.skip_patterns)
204
+ except GraphNotFoundError as e:
205
+ print(f"Error: {e}", file=sys.stderr)
206
+ sys.exit(2)
207
+
208
+ ranked = query_files(
209
+ prompt,
210
+ index,
211
+ cfg.query.top_k,
212
+ cfg.query.community_boost_weight,
213
+ cfg.query.bm25_k1,
214
+ cfg.query.bm25_b,
215
+ )
216
+
217
+ if args.list_files:
218
+ for rf in ranked:
219
+ print(f"{rf.score:.3f} {rf.source_file}")
220
+ sys.exit(0)
221
+
222
+ if args.dry_run:
223
+ print(runner.build_prompt(prompt, ranked, cfg, project_root))
224
+ sys.exit(0)
225
+
226
+ try:
227
+ exit_code = runner.run(prompt, ranked, cfg, project_root)
228
+ sys.exit(exit_code)
229
+ except CodexNotFoundError as e:
230
+ print(f"Error: {e}", file=sys.stderr)
231
+ sys.exit(127)
232
+ except CodexTimeoutError as e:
233
+ print(f"Error: {e}", file=sys.stderr)
234
+ sys.exit(124)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class GraphConfig:
10
+ path: str = "graphify-out/graph.json"
11
+ project_root: str = "."
12
+ skip_patterns: list[str] = field(default_factory=lambda: ["playwright-report", "node_modules", ".git"])
13
+
14
+
15
+ @dataclass
16
+ class QueryConfig:
17
+ top_k: int = 5
18
+ community_boost_weight: float = 2.0
19
+ bm25_k1: float = 1.5
20
+ bm25_b: float = 0.75
21
+
22
+
23
+ @dataclass
24
+ class ContextConfig:
25
+ max_file_chars: int = 8000
26
+ show_scores: bool = False
27
+
28
+
29
+ @dataclass
30
+ class CodexConfig:
31
+ command: str = "codex"
32
+ subcommand: str = "exec"
33
+ extra_args: list[str] = field(default_factory=list)
34
+ inject_via: str = "stdin"
35
+ timeout_seconds: int = 300
36
+
37
+
38
+ @dataclass
39
+ class MonoConfig:
40
+ marker_files: list[str] = field(default_factory=lambda: [
41
+ "package.json", "pyproject.toml", "go.mod", "Cargo.toml",
42
+ "pom.xml", "build.gradle", "setup.py", "setup.cfg",
43
+ "requirements.txt", "Gemfile", "composer.json", "tsconfig.json",
44
+ ])
45
+ graphify_backend: str = "claude"
46
+ watch_poll_interval: float = 3.0
47
+ context_budget_tokens: int = 2000
48
+ context_top_files: int = 8
49
+
50
+
51
+ @dataclass
52
+ class Config:
53
+ graph: GraphConfig = field(default_factory=GraphConfig)
54
+ query: QueryConfig = field(default_factory=QueryConfig)
55
+ context: ContextConfig = field(default_factory=ContextConfig)
56
+ codex: CodexConfig = field(default_factory=CodexConfig)
57
+ mono: MonoConfig = field(default_factory=MonoConfig)
58
+
59
+
60
+ def _apply_toml(cfg: Config, data: dict) -> Config:
61
+ if "graph" in data:
62
+ g = data["graph"]
63
+ cfg.graph = GraphConfig(
64
+ path=g.get("path", cfg.graph.path),
65
+ project_root=g.get("project_root", cfg.graph.project_root),
66
+ skip_patterns=g.get("skip_patterns", cfg.graph.skip_patterns),
67
+ )
68
+ if "query" in data:
69
+ q = data["query"]
70
+ cfg.query = QueryConfig(
71
+ top_k=q.get("top_k", cfg.query.top_k),
72
+ community_boost_weight=q.get("community_boost_weight", cfg.query.community_boost_weight),
73
+ bm25_k1=q.get("bm25_k1", cfg.query.bm25_k1),
74
+ bm25_b=q.get("bm25_b", cfg.query.bm25_b),
75
+ )
76
+ if "context" in data:
77
+ c = data["context"]
78
+ cfg.context = ContextConfig(
79
+ max_file_chars=c.get("max_file_chars", cfg.context.max_file_chars),
80
+ show_scores=c.get("show_scores", cfg.context.show_scores),
81
+ )
82
+ if "codex" in data:
83
+ cx = data["codex"]
84
+ cfg.codex = CodexConfig(
85
+ command=cx.get("command", cfg.codex.command),
86
+ subcommand=cx.get("subcommand", cfg.codex.subcommand),
87
+ extra_args=cx.get("extra_args", cfg.codex.extra_args),
88
+ inject_via=cx.get("inject_via", cfg.codex.inject_via),
89
+ timeout_seconds=cx.get("timeout_seconds", cfg.codex.timeout_seconds),
90
+ )
91
+ if "mono" in data:
92
+ m = data["mono"]
93
+ cfg.mono = MonoConfig(
94
+ marker_files=m.get("marker_files", cfg.mono.marker_files),
95
+ graphify_backend=m.get("graphify_backend", cfg.mono.graphify_backend),
96
+ watch_poll_interval=m.get("watch_poll_interval", cfg.mono.watch_poll_interval),
97
+ context_budget_tokens=m.get("context_budget_tokens", cfg.mono.context_budget_tokens),
98
+ context_top_files=m.get("context_top_files", cfg.mono.context_top_files),
99
+ )
100
+ return cfg
101
+
102
+
103
+ def load_config(explicit_path: str | None = None) -> Config:
104
+ cfg = Config()
105
+
106
+ candidates: list[str] = []
107
+ if explicit_path:
108
+ candidates = [explicit_path]
109
+ else:
110
+ env_path = os.environ.get("CODEX_GRAPH_CONFIG")
111
+ if env_path:
112
+ candidates.append(env_path)
113
+ candidates.append(os.path.join(os.getcwd(), "config.toml"))
114
+ candidates.append(os.path.expanduser("~/.codex-graph/config.toml"))
115
+
116
+ for path in candidates:
117
+ if os.path.exists(path):
118
+ with open(path, "rb") as f:
119
+ data = tomllib.load(f)
120
+ cfg = _apply_toml(cfg, data)
121
+ break
122
+ else:
123
+ if explicit_path:
124
+ import sys
125
+ print(f"Warning: config file not found: {explicit_path}", file=sys.stderr)
126
+
127
+ return cfg