devin-graph 0.1.0__tar.gz → 0.1.2__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 (33) hide show
  1. {devin_graph-0.1.0/src/devin_graph.egg-info → devin_graph-0.1.2}/PKG-INFO +66 -24
  2. {devin_graph-0.1.0 → devin_graph-0.1.2}/README.md +62 -23
  3. {devin_graph-0.1.0 → devin_graph-0.1.2}/pyproject.toml +4 -2
  4. devin_graph-0.1.2/src/devin_graph/__init__.py +1 -0
  5. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/cli.py +81 -6
  6. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/extract.py +3 -2
  7. devin_graph-0.1.2/src/devin_graph/mcp_server.py +134 -0
  8. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/query.py +100 -0
  9. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/store.py +35 -11
  10. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/view.py +0 -2
  11. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/vscdb.py +1 -1
  12. {devin_graph-0.1.0 → devin_graph-0.1.2/src/devin_graph.egg-info}/PKG-INFO +66 -24
  13. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph.egg-info/SOURCES.txt +4 -0
  14. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph.egg-info/entry_points.txt +1 -0
  15. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph.egg-info/requires.txt +4 -0
  16. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_cli.py +7 -6
  17. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_extract.py +3 -6
  18. devin_graph-0.1.2/tests/test_mcp.py +198 -0
  19. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_offline_core.py +4 -2
  20. devin_graph-0.1.2/tests/test_orphans_blame.py +160 -0
  21. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_query.py +2 -4
  22. devin_graph-0.1.2/tests/test_skill.py +47 -0
  23. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_store.py +6 -5
  24. devin_graph-0.1.0/src/devin_graph/__init__.py +0 -1
  25. {devin_graph-0.1.0 → devin_graph-0.1.2}/LICENSE +0 -0
  26. {devin_graph-0.1.0 → devin_graph-0.1.2}/setup.cfg +0 -0
  27. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/identity.py +0 -0
  28. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph/paths.py +0 -0
  29. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph.egg-info/dependency_links.txt +0 -0
  30. {devin_graph-0.1.0 → devin_graph-0.1.2}/src/devin_graph.egg-info/top_level.txt +0 -0
  31. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_paths.py +0 -0
  32. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_smoke.py +0 -0
  33. {devin_graph-0.1.0 → devin_graph-0.1.2}/tests/test_vscdb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devin-graph
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Knowledge graph over Devin sessions — projects, files, tools and decisions as queryable nodes.
5
5
  Author: Icaro0310
6
6
  License: MIT
@@ -10,32 +10,34 @@ License-File: LICENSE
10
10
  Requires-Dist: devin-internals-spec<0.4.0,>=0.3.0
11
11
  Provides-Extra: dev
12
12
  Requires-Dist: pytest>=8; extra == "dev"
13
+ Requires-Dist: mcp>=1.2; extra == "dev"
14
+ Provides-Extra: mcp
15
+ Requires-Dist: mcp>=1.2; extra == "mcp"
13
16
  Dynamic: license-file
14
17
 
15
18
  <div align="center">
16
19
 
17
20
  <img src="assets/banner.svg" alt="devin-graph" width="100%"/>
18
21
 
19
- <a href="https://github.com/Icaro0310/devin-graph/actions/workflows/ci.yml"><img src="https://github.com/Icaro0310/devin-graph/actions/workflows/ci.yml/badge.svg" alt="ci"/></a>
22
+ <a href="https://github.com/Icaro0310/devin-explore/actions/workflows/test-graph.yml"><img src="https://github.com/Icaro0310/devin-explore/actions/workflows/test-graph.yml/badge.svg" alt="ci"/></a>
20
23
 
21
-
22
- <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-graph"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-graph/badge" alt="OpenSSF Scorecard"/></a>
24
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-explore"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-explore/badge" alt="OpenSSF Scorecard"/></a>
23
25
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
24
26
  <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
25
- <a href="https://github.com/Icaro0310/devin-graph"><img src="https://img.shields.io/github/stars/Icaro0310/devin-graph" alt="GitHub stars"/></a>
26
- <a href="https://github.com/Icaro0310/devin-graph/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-graph" alt="Last commit"/></a>
27
+ <a href="https://github.com/Icaro0310/devin-explore"><img src="https://img.shields.io/github/stars/Icaro0310/devin-explore" alt="GitHub stars"/></a>
28
+ <a href="https://github.com/Icaro0310/devin-explore/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-explore" alt="Last commit"/></a>
27
29
  <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
28
- <a href="https://github.com/Icaro0310/devin-graph/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
30
+ <a href="https://github.com/Icaro0310/devin-explore/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
29
31
  </div>
30
32
 
31
33
  <!-- DEVIN-ECO:BEGIN -->
32
34
  > **Part of the [DEVIN ecosystem](https://github.com/Icaro0310/awesome-devin)**
33
35
  > Track: Understand · Nature: product
34
- > For: Developers, AI engineers
35
- > Interface: CLI / Python library
36
+ > For: Developers, AI engineers, Data Scientists
37
+ > Interface: CLI / Python library
38
+ > Path: Data Scientists · step 1/4 — before `devin-search`
36
39
  <!-- DEVIN-ECO:END -->
37
40
 
38
-
39
41
  # devin-graph
40
42
 
41
43
  > **Unofficial community project.** Not affiliated with, endorsed by, or
@@ -79,17 +81,6 @@ build. Schema drift is gated by `devin-internals-spec`'s version detector.
79
81
 
80
82
  Requires Python ≥ 3.10 and `pipx` or `uv`. Per-OS setup lives in the platform guides: [Linux](README.linux.md) · [Personal Windows](README.windows.md) · [Corporate Windows](README.corporate-windows.md).
81
83
 
82
- <!-- DIST-STATUS:BEGIN — generated from devin-powerups/registry.json -->
83
- > **Source-only distribution.** This tool is not yet published to PyPI.
84
- > Install from source:
85
- >
86
- > ```bash
87
- > pipx install git+https://github.com/Icaro0310/devin-graph.git
88
- > # or
89
- > uv tool install git+https://github.com/Icaro0310/devin-graph.git
90
- > ```
91
- <!-- DIST-STATUS:END -->
92
-
93
84
  ## Usage
94
85
 
95
86
  ```bash
@@ -106,6 +97,10 @@ devin-graph query projects-graph --graph graph.db --json
106
97
 
107
98
  # D3-friendly dump: {"meta", "nodes", "edges"}
108
99
  devin-graph export --format json --graph graph.db --out graph.json
100
+
101
+ # diagnostics: edge-less nodes (report-only) and per-session blame
102
+ devin-graph orphans --graph graph.db
103
+ devin-graph blame-session <id-or-prefix> --graph graph.db
109
104
  ```
110
105
 
111
106
  Matching is forgiving (`src/app.py` finds `/repo/alpha/src/app.py`);
@@ -114,6 +109,15 @@ by two or more distinct projects, with the project and session lists —
114
109
  exportable as JSON like the other queries. The source DB is opened
115
110
  `mode=ro` and never written — tests assert its hash is unchanged.
116
111
 
112
+ `orphans` lists nodes with no incident edges (id, kind, owner, a
113
+ `prunable` flag marking the subset the next `build` would drop —
114
+ shared, ownerless nodes; owned orphans persist with their owner). It is
115
+ report-only and never deletes. `blame-session <id>` shows what a
116
+ session produced and touched: its owned nodes and edges plus the
117
+ resolved files, tools, projects and commits reachable through them,
118
+ with the extraction-ledger timestamp. `<id>` accepts a unique prefix;
119
+ a missing or ambiguous match exits 2 and lists candidates.
120
+
117
121
  ## Works with Devin alone (Devin-only mode)
118
122
 
119
123
  devin-graph builds `graph.db` locally from Devin's session stores — the whole
@@ -129,13 +133,10 @@ Windows and `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
129
133
  `~/.local/share/devin/cli/sessions.db`). A legacy `~/.config/devin` location
130
134
  is also checked. Pass `--sessions-db` to override.
131
135
 
132
-
133
136
  `commit` nodes + `produced`/`referenced` edges attribute sessions to git SHAs seen in tool calls (exact when the SHA appears in a `git commit`/`git push` call, `seen` otherwise). `devin-graph sql "SELECT ..."` runs read-only SQL over graph.db (SELECT/WITH only, `query_only` pragma).
134
137
 
135
-
136
138
  `--vscdb` (auto-detected on build) adds GUI coverage: `gui_session` nodes keyed by the generated session slug, `gui_workspace` edges to their workspace project, enriched with `lastAccessed` when a `resourceToSpace` editor URI links a space to the slug. Best-effort: only the observed `windsurfSpace.*` keys are read; unknown/malformed keys are skipped. Pass `--vscdb none` to disable.
137
139
 
138
-
139
140
  `devin-graph view --out file.html` renders the graph as a self-contained HTML page (embedded JSON + vanilla-JS force layout — zero CDN, works fully offline). `--limit` caps nodes (highest-degree kept, flagged TRUNCATED in the header).
140
141
 
141
142
  ## Limitations
@@ -165,6 +166,47 @@ DDL, synthetic rows) — no binary fixtures are committed. See
165
166
  [docs/SPEC.md](docs/SPEC.md) for the graph model and
166
167
  [STATUS.md](STATUS.md) for the roadmap.
167
168
 
169
+ ## Adapters (MCP server, Devin skill, plugin)
170
+
171
+ `devin_graph.mcp_server` exposes one read-only tool via the
172
+ `devin-graph-mcp` entry point (`pip install 'devin-graph[mcp]'`):
173
+ `graph_query` (same JSON as `devin-graph query <what> --json`).
174
+
175
+ `adapters/` is a self-contained Devin plugin root
176
+ (`adapters/.devin-plugin/plugin.json` +
177
+ `adapters/skills/devin-graph/SKILL.md`), installed with
178
+ `devin plugins install
179
+ Icaro0310/devin-explore#packages/graph/adapters` — it ships in the
180
+ repo, not inside the wheel. The manifest launches the server through
181
+ `uvx --from 'devin-graph[mcp]' devin-graph-mcp`, which resolves the
182
+ published PyPI release — no local install needed. To run the server
183
+ from a source checkout instead, install it editable
184
+ (`pip install -e '.[mcp]'`) and call `devin-graph-mcp` directly, or
185
+ point a local manifest copy at the checkout
186
+ (`uvx --from './packages/graph[mcp]' devin-graph-mcp`).
187
+
188
+ ### MCP host configs
189
+
190
+ Same stdio server in other hosts — server name `devin-graph`:
191
+
192
+ **Zed** — `context_servers` in `~/.config/zed/settings.json`:
193
+
194
+ ```jsonc
195
+ { "context_servers": { "devin-graph": { "command": { "path": "devin-graph-mcp", "args": [] } } } }
196
+ ```
197
+
198
+ **JetBrains AI Assistant** — Settings → Tools → AI Assistant → Model Context Protocol → Add:
199
+
200
+ ```json
201
+ { "mcpServers": { "devin-graph": { "command": "devin-graph-mcp" } } }
202
+ ```
203
+
204
+ **Antigravity** — `~/.gemini/config/mcp_config.json`:
205
+
206
+ ```json
207
+ { "mcpServers": { "devin-graph": { "command": "devin-graph-mcp" } } }
208
+ ```
209
+
168
210
  ## When to use this
169
211
 
170
212
  - You need cross-session questions: which sessions touched file X, which
@@ -2,26 +2,25 @@
2
2
 
3
3
  <img src="assets/banner.svg" alt="devin-graph" width="100%"/>
4
4
 
5
- <a href="https://github.com/Icaro0310/devin-graph/actions/workflows/ci.yml"><img src="https://github.com/Icaro0310/devin-graph/actions/workflows/ci.yml/badge.svg" alt="ci"/></a>
5
+ <a href="https://github.com/Icaro0310/devin-explore/actions/workflows/test-graph.yml"><img src="https://github.com/Icaro0310/devin-explore/actions/workflows/test-graph.yml/badge.svg" alt="ci"/></a>
6
6
 
7
-
8
- <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-graph"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-graph/badge" alt="OpenSSF Scorecard"/></a>
7
+ <a href="https://scorecard.dev/viewer/?uri=github.com/Icaro0310/devin-explore"><img src="https://api.scorecard.dev/projects/github.com/Icaro0310/devin-explore/badge" alt="OpenSSF Scorecard"/></a>
9
8
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT"/></a>
10
9
  <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+"/></a>
11
- <a href="https://github.com/Icaro0310/devin-graph"><img src="https://img.shields.io/github/stars/Icaro0310/devin-graph" alt="GitHub stars"/></a>
12
- <a href="https://github.com/Icaro0310/devin-graph/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-graph" alt="Last commit"/></a>
10
+ <a href="https://github.com/Icaro0310/devin-explore"><img src="https://img.shields.io/github/stars/Icaro0310/devin-explore" alt="GitHub stars"/></a>
11
+ <a href="https://github.com/Icaro0310/devin-explore/commits/main"><img src="https://img.shields.io/github/last-commit/Icaro0310/devin-explore" alt="Last commit"/></a>
13
12
  <a href="https://github.com/Icaro0310/awesome-devin"><img src="https://img.shields.io/badge/part%20of-devin--*-ecosystem-7c3aed" alt="devin-* ecosystem"/></a>
14
- <a href="https://github.com/Icaro0310/devin-graph/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
13
+ <a href="https://github.com/Icaro0310/devin-explore/issues"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"/></a>
15
14
  </div>
16
15
 
17
16
  <!-- DEVIN-ECO:BEGIN -->
18
17
  > **Part of the [DEVIN ecosystem](https://github.com/Icaro0310/awesome-devin)**
19
18
  > Track: Understand · Nature: product
20
- > For: Developers, AI engineers
21
- > Interface: CLI / Python library
19
+ > For: Developers, AI engineers, Data Scientists
20
+ > Interface: CLI / Python library
21
+ > Path: Data Scientists · step 1/4 — before `devin-search`
22
22
  <!-- DEVIN-ECO:END -->
23
23
 
24
-
25
24
  # devin-graph
26
25
 
27
26
  > **Unofficial community project.** Not affiliated with, endorsed by, or
@@ -65,17 +64,6 @@ build. Schema drift is gated by `devin-internals-spec`'s version detector.
65
64
 
66
65
  Requires Python ≥ 3.10 and `pipx` or `uv`. Per-OS setup lives in the platform guides: [Linux](README.linux.md) · [Personal Windows](README.windows.md) · [Corporate Windows](README.corporate-windows.md).
67
66
 
68
- <!-- DIST-STATUS:BEGIN — generated from devin-powerups/registry.json -->
69
- > **Source-only distribution.** This tool is not yet published to PyPI.
70
- > Install from source:
71
- >
72
- > ```bash
73
- > pipx install git+https://github.com/Icaro0310/devin-graph.git
74
- > # or
75
- > uv tool install git+https://github.com/Icaro0310/devin-graph.git
76
- > ```
77
- <!-- DIST-STATUS:END -->
78
-
79
67
  ## Usage
80
68
 
81
69
  ```bash
@@ -92,6 +80,10 @@ devin-graph query projects-graph --graph graph.db --json
92
80
 
93
81
  # D3-friendly dump: {"meta", "nodes", "edges"}
94
82
  devin-graph export --format json --graph graph.db --out graph.json
83
+
84
+ # diagnostics: edge-less nodes (report-only) and per-session blame
85
+ devin-graph orphans --graph graph.db
86
+ devin-graph blame-session <id-or-prefix> --graph graph.db
95
87
  ```
96
88
 
97
89
  Matching is forgiving (`src/app.py` finds `/repo/alpha/src/app.py`);
@@ -100,6 +92,15 @@ by two or more distinct projects, with the project and session lists —
100
92
  exportable as JSON like the other queries. The source DB is opened
101
93
  `mode=ro` and never written — tests assert its hash is unchanged.
102
94
 
95
+ `orphans` lists nodes with no incident edges (id, kind, owner, a
96
+ `prunable` flag marking the subset the next `build` would drop —
97
+ shared, ownerless nodes; owned orphans persist with their owner). It is
98
+ report-only and never deletes. `blame-session <id>` shows what a
99
+ session produced and touched: its owned nodes and edges plus the
100
+ resolved files, tools, projects and commits reachable through them,
101
+ with the extraction-ledger timestamp. `<id>` accepts a unique prefix;
102
+ a missing or ambiguous match exits 2 and lists candidates.
103
+
103
104
  ## Works with Devin alone (Devin-only mode)
104
105
 
105
106
  devin-graph builds `graph.db` locally from Devin's session stores — the whole
@@ -115,13 +116,10 @@ Windows and `$XDG_DATA_HOME/devin/cli/sessions.db` on Linux (default
115
116
  `~/.local/share/devin/cli/sessions.db`). A legacy `~/.config/devin` location
116
117
  is also checked. Pass `--sessions-db` to override.
117
118
 
118
-
119
119
  `commit` nodes + `produced`/`referenced` edges attribute sessions to git SHAs seen in tool calls (exact when the SHA appears in a `git commit`/`git push` call, `seen` otherwise). `devin-graph sql "SELECT ..."` runs read-only SQL over graph.db (SELECT/WITH only, `query_only` pragma).
120
120
 
121
-
122
121
  `--vscdb` (auto-detected on build) adds GUI coverage: `gui_session` nodes keyed by the generated session slug, `gui_workspace` edges to their workspace project, enriched with `lastAccessed` when a `resourceToSpace` editor URI links a space to the slug. Best-effort: only the observed `windsurfSpace.*` keys are read; unknown/malformed keys are skipped. Pass `--vscdb none` to disable.
123
122
 
124
-
125
123
  `devin-graph view --out file.html` renders the graph as a self-contained HTML page (embedded JSON + vanilla-JS force layout — zero CDN, works fully offline). `--limit` caps nodes (highest-degree kept, flagged TRUNCATED in the header).
126
124
 
127
125
  ## Limitations
@@ -151,6 +149,47 @@ DDL, synthetic rows) — no binary fixtures are committed. See
151
149
  [docs/SPEC.md](docs/SPEC.md) for the graph model and
152
150
  [STATUS.md](STATUS.md) for the roadmap.
153
151
 
152
+ ## Adapters (MCP server, Devin skill, plugin)
153
+
154
+ `devin_graph.mcp_server` exposes one read-only tool via the
155
+ `devin-graph-mcp` entry point (`pip install 'devin-graph[mcp]'`):
156
+ `graph_query` (same JSON as `devin-graph query <what> --json`).
157
+
158
+ `adapters/` is a self-contained Devin plugin root
159
+ (`adapters/.devin-plugin/plugin.json` +
160
+ `adapters/skills/devin-graph/SKILL.md`), installed with
161
+ `devin plugins install
162
+ Icaro0310/devin-explore#packages/graph/adapters` — it ships in the
163
+ repo, not inside the wheel. The manifest launches the server through
164
+ `uvx --from 'devin-graph[mcp]' devin-graph-mcp`, which resolves the
165
+ published PyPI release — no local install needed. To run the server
166
+ from a source checkout instead, install it editable
167
+ (`pip install -e '.[mcp]'`) and call `devin-graph-mcp` directly, or
168
+ point a local manifest copy at the checkout
169
+ (`uvx --from './packages/graph[mcp]' devin-graph-mcp`).
170
+
171
+ ### MCP host configs
172
+
173
+ Same stdio server in other hosts — server name `devin-graph`:
174
+
175
+ **Zed** — `context_servers` in `~/.config/zed/settings.json`:
176
+
177
+ ```jsonc
178
+ { "context_servers": { "devin-graph": { "command": { "path": "devin-graph-mcp", "args": [] } } } }
179
+ ```
180
+
181
+ **JetBrains AI Assistant** — Settings → Tools → AI Assistant → Model Context Protocol → Add:
182
+
183
+ ```json
184
+ { "mcpServers": { "devin-graph": { "command": "devin-graph-mcp" } } }
185
+ ```
186
+
187
+ **Antigravity** — `~/.gemini/config/mcp_config.json`:
188
+
189
+ ```json
190
+ { "mcpServers": { "devin-graph": { "command": "devin-graph-mcp" } } }
191
+ ```
192
+
154
193
  ## When to use this
155
194
 
156
195
  - You need cross-session questions: which sessions touched file X, which
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devin-graph"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Knowledge graph over Devin sessions — projects, files, tools and decisions as queryable nodes."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -15,10 +15,12 @@ dependencies = [
15
15
  ]
16
16
 
17
17
  [project.optional-dependencies]
18
- dev = ["pytest>=8"]
18
+ dev = ["pytest>=8", "mcp>=1.2"]
19
+ mcp = ["mcp>=1.2"]
19
20
 
20
21
  [project.scripts]
21
22
  devin-graph = "devin_graph.cli:main"
23
+ devin-graph-mcp = "devin_graph.mcp_server:main"
22
24
 
23
25
  [tool.setuptools.packages.find]
24
26
  where = ["src"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -10,20 +10,19 @@ from pathlib import Path
10
10
  from devin_internals import SchemaError
11
11
  from devin_internals.parsers import SessionsStore
12
12
 
13
- from devin_graph.paths import default_state_vscdb
14
- from devin_graph.vscdb import extract_vscdb
15
-
16
13
  from devin_graph import __version__
17
- from devin_graph.paths import default_sessions_db
14
+ from devin_graph.paths import default_sessions_db, default_state_vscdb
18
15
  from devin_graph.query import (
16
+ blame_session,
17
+ orphaned_nodes,
19
18
  project_detail,
20
19
  projects_graph,
21
20
  sessions_for_file,
22
21
  sessions_for_tool,
23
22
  shared_files,
24
- tools_for_project,
25
23
  )
26
24
  from devin_graph.store import GraphStore
25
+ from devin_graph.vscdb import extract_vscdb
27
26
 
28
27
  DEFAULT_GRAPH = "graph.db"
29
28
 
@@ -56,7 +55,7 @@ def _open_graph(graph_arg: str | None) -> GraphStore:
56
55
  file=sys.stderr,
57
56
  )
58
57
  raise SystemExit(2)
59
- return GraphStore(path)
58
+ return GraphStore(path, readonly=True)
60
59
 
61
60
 
62
61
  def _add_graph(sub: argparse.ArgumentParser) -> None:
@@ -113,6 +112,24 @@ def build_parser() -> argparse.ArgumentParser:
113
112
  sq.add_argument("--json", action="store_true")
114
113
  sq.set_defaults(func=_cmd_query)
115
114
 
115
+ o = sub.add_parser(
116
+ "orphans", help="nodes with no incident edges — report-only, "
117
+ "never deletes (prunable flags what the next build would drop)")
118
+ _add_graph(o)
119
+ o.add_argument("--json", action="store_true")
120
+ o.set_defaults(func=_cmd_orphans)
121
+
122
+ bl = sub.add_parser(
123
+ "blame-session",
124
+ help="what a session produced/touched in the graph — owned "
125
+ "nodes and edges plus the files, tools, projects and commits "
126
+ "they reach")
127
+ bl.add_argument(
128
+ "session_id", help="session id or unique prefix")
129
+ _add_graph(bl)
130
+ bl.add_argument("--json", action="store_true")
131
+ bl.set_defaults(func=_cmd_blame)
132
+
116
133
  e = sub.add_parser("export", help="dump nodes+edges (D3-friendly)")
117
134
  e.add_argument("--format", choices=["json"], default="json")
118
135
  _add_graph(e)
@@ -229,6 +246,64 @@ def _print_query(what: str, data: dict) -> None:
229
246
  print(f"call: {t['id']} → {t['file']}")
230
247
 
231
248
 
249
+ def _cmd_orphans(args: argparse.Namespace) -> int:
250
+ with _open_graph(args.graph) as gs:
251
+ data = orphaned_nodes(gs)
252
+ if args.json:
253
+ print(json.dumps(data, ensure_ascii=False, indent=2))
254
+ return 0
255
+ if not data["nodes"]:
256
+ print("no orphaned nodes")
257
+ return 0
258
+ for n in data["nodes"]:
259
+ label = n.get("name") or ""
260
+ keep = "" if n["prunable"] else " (owned — kept by prune)"
261
+ print(f"{n['id']} {label}{keep}".rstrip())
262
+ print(f"{data['count']} orphan(s)")
263
+ return 0
264
+
265
+
266
+ def _cmd_blame(args: argparse.Namespace) -> int:
267
+ with _open_graph(args.graph) as gs:
268
+ data = blame_session(gs, args.session_id)
269
+ if data["session"] is None:
270
+ cand = data.get("candidates") or []
271
+ hint = f" — candidates: {', '.join(cand[:10])}" if cand else ""
272
+ print(
273
+ f"devin-graph: no session matches {args.session_id!r}{hint}",
274
+ file=sys.stderr,
275
+ )
276
+ return 2
277
+ if args.json:
278
+ print(json.dumps(data, ensure_ascii=False, indent=2))
279
+ return 0
280
+ s = data["session"]
281
+ print(f"session: {data['session_id']} {s.get('title') or 'Untitled'}")
282
+ if data["projects"]:
283
+ print("project: " + ", ".join(data["projects"]))
284
+ c = data["counts"]
285
+ print(
286
+ f"produced: {c['nodes']} node(s), {c['edges']} edge(s) · "
287
+ f"touched: {c['files']} file(s), {c['tools']} tool(s), "
288
+ f"{c['commits']} commit(s)"
289
+ )
290
+ if data["nodes_owned"]:
291
+ owned = [n for n in data["nodes_owned"] if n["kind"] == "tool_call"]
292
+ if owned:
293
+ print(f"tool calls ({len(owned)}):")
294
+ for n in owned:
295
+ print(f" {n['key']}")
296
+ if data["files"]:
297
+ print(f"files ({len(data['files'])}):")
298
+ for f in data["files"]:
299
+ print(f" {f}")
300
+ if data["tools"]:
301
+ print("tools: " + ", ".join(data["tools"]))
302
+ for cm in data["commits"]:
303
+ print(f"commit {cm['sha'][:7]} ({cm['relation']})")
304
+ return 0
305
+
306
+
232
307
  def _cmd_export(args: argparse.Namespace) -> int:
233
308
  with _open_graph(args.graph) as gs:
234
309
  data = gs.export()
@@ -30,10 +30,11 @@ from __future__ import annotations
30
30
  import json
31
31
  import posixpath
32
32
  import re
33
+ from collections.abc import Iterable, Iterator
33
34
  from dataclasses import dataclass, field
35
+ from typing import Any
34
36
 
35
37
  from devin_internals.commits import commit_references
36
- from typing import Any, Iterable, Iterator
37
38
 
38
39
  NODE_KINDS = ("session", "project", "file", "tool", "tool_call", "commit",
39
40
  "gui_session")
@@ -87,7 +88,7 @@ class Extraction:
87
88
  nodes: list[Node] = field(default_factory=list)
88
89
  edges: list[Edge] = field(default_factory=list)
89
90
 
90
- def merged(self) -> "Extraction":
91
+ def merged(self) -> Extraction:
91
92
  seen_n: dict[tuple[str, str], Node] = {}
92
93
  seen_e: set[tuple] = set()
93
94
  out = Extraction()
@@ -0,0 +1,134 @@
1
+ """devin-graph as an MCP server: the ``query`` subcommands as one tool.
2
+
3
+ One tool, ``graph_query`` — the read half of ``devin-graph query`` —
4
+ returns the same JSON payloads as ``devin-graph query <what> --json``.
5
+ Read-only by construction: ``build``, ``export``, ``sql`` and ``view``
6
+ stay CLI-only; opening the graph never writes to it, and the source
7
+ stores are never touched.
8
+
9
+ The logic lives in :func:`do_query`, unit-testable without a running
10
+ server or the ``mcp`` package. ``build_server()`` wraps it — needs the
11
+ ``mcp`` extra: ``pip install 'devin-graph[mcp]'``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from devin_graph.query import (
20
+ project_detail,
21
+ projects_graph,
22
+ sessions_for_file,
23
+ sessions_for_tool,
24
+ shared_files,
25
+ )
26
+ from devin_graph.store import GraphStore
27
+
28
+ # Same default as the CLI (`devin-graph query ... --graph graph.db`).
29
+ DEFAULT_GRAPH = "graph.db"
30
+
31
+ QUERY_TYPES = ("file", "tool", "project", "projects-graph", "shared-files")
32
+
33
+
34
+ def do_query(query_type: str, target: str = "", graph: str = "") -> dict:
35
+ """Run one canned graph query; return its ``--json`` payload as a dict.
36
+
37
+ ``graph.db`` must already exist — the read-write
38
+ :class:`GraphStore` creates the file on open, so the missing-file
39
+ case (the CLI's exit-2) is checked *before* opening and maps to
40
+ ``{"error": "no_graph"}``. The store is then opened read-only
41
+ (``mode=ro`` + ``query_only``): no schema DDL, no repair of an
42
+ incomplete schema.
43
+ """
44
+ path = Path(graph or DEFAULT_GRAPH).expanduser()
45
+ if not path.exists():
46
+ return {
47
+ "error": "no_graph",
48
+ "detail": f"{path}: no such graph file — "
49
+ "run `devin-graph build` first",
50
+ }
51
+ with GraphStore(path, readonly=True) as gs:
52
+ if query_type == "file":
53
+ return sessions_for_file(gs, target)
54
+ if query_type == "tool":
55
+ return sessions_for_tool(gs, target)
56
+ if query_type == "project":
57
+ return project_detail(gs, target)
58
+ if query_type == "projects-graph":
59
+ return projects_graph(gs)
60
+ if query_type == "shared-files":
61
+ return shared_files(gs)
62
+ return {
63
+ "error": "bad_query",
64
+ "detail": f"unknown query_type {query_type!r} — "
65
+ f"expected one of {', '.join(QUERY_TYPES)}",
66
+ }
67
+
68
+
69
+ def _err(error: Exception) -> dict:
70
+ return {"error": type(error).__name__, "detail": str(error)[:500]}
71
+
72
+
73
+ def _make_app(name: str):
74
+ """Return an MCP server app across SDK versions.
75
+
76
+ mcp 2.x renamed FastMCP -> MCPServer; both expose the same .tool()
77
+ decorator and .run(transport='stdio'). Support whichever is installed.
78
+ """
79
+ try: # mcp 2.x
80
+ from mcp.server.mcpserver import MCPServer
81
+ return MCPServer(name)
82
+ except ImportError:
83
+ pass
84
+ try: # mcp 1.x
85
+ from mcp.server.fastmcp import FastMCP
86
+ return FastMCP(name)
87
+ except ImportError as e: # pragma: no cover
88
+ raise ImportError(
89
+ "The MCP server needs the 'mcp' extra: "
90
+ "pip install 'devin-graph[mcp]'"
91
+ ) from e
92
+
93
+
94
+ def build_server():
95
+ """Build the MCP server app with every devin-graph tool registered."""
96
+ server = _make_app("devin-graph")
97
+
98
+ @server.tool()
99
+ def graph_query(
100
+ query_type: str,
101
+ target: str = "",
102
+ graph: str = "",
103
+ ) -> dict:
104
+ """Query the session knowledge graph — the same JSON as
105
+ ``devin-graph query <what> --json``. Read-only: never modifies
106
+ ``graph.db`` or the source stores. ``query_type`` is one of
107
+ ``file`` (sessions that touched a path), ``tool``
108
+ (sessions/projects that used a tool), ``project`` (a project's
109
+ sessions, tools and files), ``projects-graph`` (project adjacency)
110
+ or ``shared-files`` (files touched by ≥2 projects). ``target`` is
111
+ the path/name for the first three; ``graph`` overrides the
112
+ default ``./graph.db`` location."""
113
+ try:
114
+ return do_query(query_type, target=target, graph=graph)
115
+ except Exception as error: # noqa: BLE001 — tool boundary must not raise
116
+ return _err(error)
117
+
118
+ return server
119
+
120
+
121
+ def main() -> None:
122
+ try:
123
+ server = build_server()
124
+ except ImportError as exc:
125
+ # Missing 'mcp' extra — install hint lives in the exception text.
126
+ print(f"error: {exc}", file=sys.stderr)
127
+ raise SystemExit(1)
128
+ # Startup failures at .run() are NOT the missing-extra case: let them
129
+ # keep their traceback so the real failure stays debuggable.
130
+ server.run(transport="stdio")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()