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.
- graphnav-0.1.0/PKG-INFO +9 -0
- graphnav-0.1.0/README.md +259 -0
- graphnav-0.1.0/codex_graph/__init__.py +10 -0
- graphnav-0.1.0/codex_graph/cli.py +238 -0
- graphnav-0.1.0/codex_graph/config.py +127 -0
- graphnav-0.1.0/codex_graph/graph_nav.py +113 -0
- graphnav-0.1.0/codex_graph/graph_query.py +187 -0
- graphnav-0.1.0/codex_graph/multirepo.py +793 -0
- graphnav-0.1.0/codex_graph/runner.py +123 -0
- graphnav-0.1.0/graphnav.egg-info/PKG-INFO +9 -0
- graphnav-0.1.0/graphnav.egg-info/SOURCES.txt +20 -0
- graphnav-0.1.0/graphnav.egg-info/dependency_links.txt +1 -0
- graphnav-0.1.0/graphnav.egg-info/entry_points.txt +2 -0
- graphnav-0.1.0/graphnav.egg-info/requires.txt +5 -0
- graphnav-0.1.0/graphnav.egg-info/top_level.txt +1 -0
- graphnav-0.1.0/pyproject.toml +23 -0
- graphnav-0.1.0/setup.cfg +4 -0
- graphnav-0.1.0/tests/test_cli.py +201 -0
- graphnav-0.1.0/tests/test_config.py +176 -0
- graphnav-0.1.0/tests/test_graph_query.py +243 -0
- graphnav-0.1.0/tests/test_multirepo.py +1296 -0
- graphnav-0.1.0/tests/test_runner.py +126 -0
graphnav-0.1.0/PKG-INFO
ADDED
|
@@ -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"
|
graphnav-0.1.0/README.md
ADDED
|
@@ -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,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
|