@wwjd/pi-graphify 0.2.0 → 0.3.0
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.
- package/CHANGELOG.md +13 -0
- package/README.md +54 -61
- package/docs/TESTING.md +149 -0
- package/extensions/index.ts +10 -6
- package/package.json +3 -2
- package/src/backends/mock.ts +47 -0
- package/src/coordinator.ts +10 -5
- package/src/extensions/extension-load.test.ts +15 -0
- package/src/graphify.ts +2 -0
- package/src/tools/affected.ts +37 -0
- package/src/tools/build.ts +43 -0
- package/src/tools/explain.ts +38 -0
- package/src/tools/format.ts +25 -0
- package/src/tools/index.ts +84 -7
- package/src/tools/path.ts +43 -0
- package/src/tools/query.ts +37 -0
- package/src/tools/status.ts +6 -1
- package/src/tools/tools.test.ts +282 -0
- package/src/tools/types.ts +31 -0
- package/src/tools/version.ts +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0](https://github.com/WianVDM/pi-graphify/compare/pi-graphify-v0.2.0...pi-graphify-v0.3.0) (2026-07-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **tools:** add build, query, path, explain, affected, and version tools ([5b8cb07](https://github.com/WianVDM/pi-graphify/commit/5b8cb0730f7c766dd00f40149e7abb6b1fce64fa))
|
|
14
|
+
* **tools:** add shared tool types and formatting helpers ([0efd421](https://github.com/WianVDM/pi-graphify/commit/0efd4214e6b4397c126abecb864ad947be896fa6))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* replace /graphify-build references with graphify CLI command ([63f4fb2](https://github.com/WianVDM/pi-graphify/commit/63f4fb25889719448f13c4dffb485164f7774c55))
|
|
20
|
+
|
|
8
21
|
## [0.2.0](https://github.com/WianVDM/pi-graphify/compare/pi-graphify-v0.1.0...pi-graphify-v0.2.0) (2026-07-12)
|
|
9
22
|
|
|
10
23
|
|
package/README.md
CHANGED
|
@@ -1,93 +1,86 @@
|
|
|
1
1
|
# pi-graphify
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A [Pi](https://pi.dev) extension that brings [Graphify](https://github.com/Graphify-Labs/graphify) knowledge graph capabilities into your agent sessions.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
pi install @wwjd/pi-graphify
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Or run locally during development:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
# Install from a local path (during development)
|
|
13
14
|
pi install .
|
|
14
|
-
|
|
15
|
-
# Or run ephemerally without installing
|
|
16
|
-
pi -e .
|
|
17
|
-
|
|
18
|
-
# Type check
|
|
19
|
-
npm run typecheck
|
|
20
|
-
|
|
21
|
-
# Lint
|
|
22
|
-
npm run lint
|
|
23
15
|
```
|
|
24
16
|
|
|
25
|
-
## What
|
|
17
|
+
## What it does
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│ └── index.ts # Extension entry point (tools, commands, events)
|
|
31
|
-
├── src/ # Core implementation
|
|
32
|
-
├── package.json # Pi manifest + npm config
|
|
33
|
-
├── tsconfig.json # TypeScript config (type checking only)
|
|
34
|
-
├── biome.json # Linter/formatter config
|
|
35
|
-
├── .gitignore
|
|
36
|
-
├── LICENSE
|
|
37
|
-
├── CHANGELOG.md
|
|
38
|
-
├── RELEASE.md # Versioning and release standards
|
|
39
|
-
├── AGENTS.md # Agent context / development conventions
|
|
40
|
-
└── README.md
|
|
41
|
-
```
|
|
19
|
+
`pi-graphify` detects the Graphify CLI and a project's `graphify-out/graph.json` graph, then exposes Graphify operations as Pi tools and slash commands. It routes requests through a backend abstraction so the agent can use Graphify without worrying about whether it's talking to the CLI or (in the future) an MCP server.
|
|
20
|
+
|
|
21
|
+
When a graph is present, the extension also injects a lightweight hint into the system prompt so the agent knows it can ask structural codebase questions via the graph.
|
|
42
22
|
|
|
43
|
-
##
|
|
23
|
+
## Requirements
|
|
44
24
|
|
|
45
|
-
|
|
25
|
+
- [Graphify](https://github.com/Graphify-Labs/graphify) must be installed and on your `PATH`.
|
|
26
|
+
- The project must have a generated graph at `graphify-out/graph.json`.
|
|
46
27
|
|
|
47
|
-
|
|
28
|
+
## Current commands
|
|
48
29
|
|
|
49
|
-
|
|
30
|
+
| Command | Description |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `/graphify-status` | Check whether a graph exists and whether the Graphify CLI is compatible. |
|
|
50
33
|
|
|
51
|
-
|
|
34
|
+
## Current tools
|
|
52
35
|
|
|
53
|
-
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `graphify_status` | Check graph presence, path, and Graphify CLI compatibility. |
|
|
39
|
+
| `graphify_build` | Build or incrementally update the Graphify knowledge graph. |
|
|
40
|
+
| `graphify_query` | Ask a natural-language question against the graph. |
|
|
41
|
+
| `graphify_path` | Find the shortest path between two graph nodes. |
|
|
42
|
+
| `graphify_explain` | Explain a node and its connections in the graph. |
|
|
43
|
+
| `graphify_affected` | Show the blast radius of changes to one or more files. |
|
|
44
|
+
| `graphify_version` | Report the installed Graphify version and compatibility status. |
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
## What's coming
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
- npm 10+
|
|
59
|
-
- pi CLI
|
|
48
|
+
Slash commands for the tools above are planned in the next phase:
|
|
60
49
|
|
|
61
|
-
|
|
50
|
+
- `/graphify-build`
|
|
51
|
+
- `/graphify-query`
|
|
52
|
+
- `/graphify-path`
|
|
53
|
+
- `/graphify-explain`
|
|
54
|
+
- `/graphify-affected`
|
|
55
|
+
- `/graphify-version`
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|---------|-------------|
|
|
65
|
-
| `npm run typecheck` | TypeScript type checking (`tsc --noEmit`) |
|
|
66
|
-
| `npm run lint` | Check lint + formatting (`biome check`) |
|
|
67
|
-
| `npm run lint:fix` | Auto-fix lint + formatting issues |
|
|
68
|
-
| `npm run format` | Format code only |
|
|
57
|
+
Later phases will add background file watching, MCP backend support, ecosystem integrations, and release readiness work. See [plan/PLAN.md](plan/PLAN.md) for the full roadmap.
|
|
69
58
|
|
|
70
|
-
|
|
59
|
+
## How it works
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
61
|
+
1. On session start, the extension detects the installed Graphify version and looks for `graphify-out/graph.json`.
|
|
62
|
+
2. If a graph is found, a hint is added to the agent context.
|
|
63
|
+
3. Tools and commands route through a coordinator that selects the best available backend.
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
pi -ne -e . --no-session -p "List the available tools."
|
|
78
|
-
```
|
|
65
|
+
## Status
|
|
79
66
|
|
|
80
|
-
|
|
67
|
+
This extension is in early development. The public API, tools, and commands will evolve as the extension matures toward a stable 1.0 release.
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
## Documentation
|
|
83
70
|
|
|
84
|
-
|
|
71
|
+
- [Design](docs/DESIGN.md)
|
|
72
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
73
|
+
- [Ecosystem](docs/ECOSYSTEM.md)
|
|
74
|
+
- [Versioning](docs/VERSIONING.md)
|
|
75
|
+
- [Release standards](RELEASE.md)
|
|
76
|
+
- [Agent context](AGENTS.md)
|
|
85
77
|
|
|
86
|
-
|
|
78
|
+
## Development
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
```bash
|
|
81
|
+
npm run typecheck
|
|
82
|
+
npm run lint
|
|
83
|
+
```
|
|
91
84
|
|
|
92
85
|
## License
|
|
93
86
|
|
package/docs/TESTING.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Local Development and Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide covers how to install and test the local `pi-graphify` extension, plus how to install and uninstall the Graphify CLI for end-to-end verification.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [Pi](https://pi.dev) installed and on your PATH.
|
|
8
|
+
- Node.js 20+ and npm.
|
|
9
|
+
- Python 3.10+ (only needed when testing with the Graphify CLI).
|
|
10
|
+
- [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/) (recommended for Graphify).
|
|
11
|
+
|
|
12
|
+
## Install the local pi-graphify extension
|
|
13
|
+
|
|
14
|
+
From the project root directory, run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pi install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This installs the local package into Pi so the extension is loaded on the next Pi session.
|
|
21
|
+
|
|
22
|
+
To verify the install, start Pi in any project and check that `/graphify-status` autocompletes.
|
|
23
|
+
|
|
24
|
+
## Automated checks
|
|
25
|
+
|
|
26
|
+
Before running manual tests, run the static and automated checks:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run typecheck
|
|
30
|
+
npm run lint
|
|
31
|
+
npm test
|
|
32
|
+
npm audit --audit-level=moderate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All should pass.
|
|
36
|
+
|
|
37
|
+
## Install the Graphify CLI
|
|
38
|
+
|
|
39
|
+
The official PyPI package is **`graphifyy`** (double-y). The CLI command is still `graphify`.
|
|
40
|
+
|
|
41
|
+
### Recommended: uv
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install graphifyy
|
|
45
|
+
graphify --version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Alternative: pipx
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pipx install graphifyy
|
|
52
|
+
graphify --version
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Alternative: pip
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install graphifyy
|
|
59
|
+
graphify --version
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
After installing, `graphify` should be available on your PATH. If it is not, add the tool's bin directory to your PATH (for example, `~/.local/bin` for `uv tool` or `pipx`).
|
|
63
|
+
|
|
64
|
+
## Build a graph for testing
|
|
65
|
+
|
|
66
|
+
Phase 3 only adds the tools. The `/graphify-build` command is added in Phase 4, so for now build the graph directly with the Graphify CLI.
|
|
67
|
+
|
|
68
|
+
The recommended approach for testing `pi-graphify` is a code-only graph. It skips semantic extraction, so it does not require an API key or extra Python dependencies:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
graphify . --code-only
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This creates a `graphify-out/` directory with `graph.json`, `graph.html`, and `GRAPH_REPORT.md`. It has been verified to work with the default `uv tool install graphifyy` setup.
|
|
75
|
+
|
|
76
|
+
If you want semantic extraction (docs, images, etc.), you need the `kimi` backend dependencies. Install them with:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv tool install "graphifyy[kimi]" --force
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then run the full build:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
graphify .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Once Phase 4 is complete, `/graphify-build` will be available as an alternative.
|
|
89
|
+
|
|
90
|
+
## Verify the extension with Graphify installed
|
|
91
|
+
|
|
92
|
+
1. Start Pi in a project with a built graph.
|
|
93
|
+
2. You should see a notification: `Graphify graph ready: ...`.
|
|
94
|
+
3. The supported tools should be available to the LLM. You can verify by asking: "What graphify tools are available?" or by checking the system prompt tool list.
|
|
95
|
+
4. Test a tool by asking: "Use the graphify graph to explain how auth works in this codebase."
|
|
96
|
+
|
|
97
|
+
## Verify the extension without Graphify
|
|
98
|
+
|
|
99
|
+
1. Uninstall Graphify (see below).
|
|
100
|
+
2. Start Pi in a project.
|
|
101
|
+
3. You should see a warning: `Graphify CLI was not detected on PATH...`.
|
|
102
|
+
4. No `graphify_*` tools should be available to the LLM.
|
|
103
|
+
5. `/graphify-status` should still work and report that no graph was found.
|
|
104
|
+
|
|
105
|
+
## Uninstall the Graphify CLI
|
|
106
|
+
|
|
107
|
+
### uv
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv tool uninstall graphifyy
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### pipx
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pipx uninstall graphifyy
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### pip
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip uninstall graphifyy
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
After uninstalling, run `graphify --version` to confirm it is no longer on PATH.
|
|
126
|
+
|
|
127
|
+
## Uninstall the local pi-graphify extension
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pi uninstall npm:@wwjd/pi-graphify
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or remove the package manually from Pi's package directory if you installed it locally.
|
|
134
|
+
|
|
135
|
+
## Troubleshooting
|
|
136
|
+
|
|
137
|
+
| Problem | Solution |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `graphify` command not found after install | Ensure the tool bin directory is on your PATH. For `uv tool`, try `uv tool update-shell`. |
|
|
140
|
+
| Pi does not load the extension | Run `pi --version` and confirm the package is listed in `pi packages`. Try `/reload`. |
|
|
141
|
+
| Extension shows old version after changes | Run `pi install .` again, then start a fresh Pi session. |
|
|
142
|
+
| `graphify_status` returns "No Graphify graph found" | Build the graph first with `graphify . --code-only`. |
|
|
143
|
+
| Semantic extraction fails with `openai` package error | Run `graphify . --code-only` for a code-only graph, or install the `kimi` backend with `uv tool install "graphifyy[kimi]" --force`. |
|
|
144
|
+
|
|
145
|
+
## References
|
|
146
|
+
|
|
147
|
+
- [Graphify repository](https://github.com/Graphify-Labs/graphify)
|
|
148
|
+
- [graphifyy on PyPI](https://pypi.org/project/graphifyy/)
|
|
149
|
+
- [Pi extensions documentation](https://pi.dev/docs/extensions)
|
package/extensions/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Brings Graphify knowledge graph capabilities into Pi sessions:
|
|
5
5
|
* - Detects existing graphify-out/graph.json in the project
|
|
6
6
|
* - Registers LLM-callable tools (graphify_status, graphify_query, ...)
|
|
7
|
-
* - Registers slash commands (/graphify-
|
|
7
|
+
* - Registers slash commands (/graphify-status, ...)
|
|
8
8
|
* - Optionally watches source files for incremental rebuilds
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -17,10 +17,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
17
17
|
// Coordinator is created per session and shared by tools and commands.
|
|
18
18
|
let coordinator: GraphifyCoordinator | null = null;
|
|
19
19
|
const getCoordinator = () => coordinator;
|
|
20
|
+
const registeredToolNames = new Set<string>();
|
|
20
21
|
|
|
21
|
-
// ── 1. Register
|
|
22
|
-
registerGraphifyTools(pi, getCoordinator);
|
|
23
|
-
|
|
22
|
+
// ── 1. Register slash commands synchronously ─────────────────────
|
|
24
23
|
pi.registerCommand("graphify-status", {
|
|
25
24
|
description: "Show Graphify graph status for the current project",
|
|
26
25
|
handler: async (_args, ctx) => {
|
|
@@ -34,16 +33,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
34
33
|
if (status.hasGraph && status.graphPath) {
|
|
35
34
|
ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
|
|
36
35
|
} else {
|
|
37
|
-
ctx.ui.notify("No Graphify graph found.
|
|
36
|
+
ctx.ui.notify("No Graphify graph found. Build one with `graphify .`.", "warning");
|
|
38
37
|
}
|
|
39
38
|
},
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
// ── 2. Initialize coordinator per session
|
|
41
|
+
// ── 2. Initialize coordinator and register tools per session ───────
|
|
42
|
+
// Tools are registered inside session_start so we can gate them by the
|
|
43
|
+
// coordinator's detected capabilities. Registering at load time would mean
|
|
44
|
+
// the LLM sees tools that may not be supported by the installed Graphify version.
|
|
43
45
|
pi.on("session_start", async (_event, ctx) => {
|
|
44
46
|
coordinator = new GraphifyCoordinator({ cwd: ctx.cwd });
|
|
45
47
|
await coordinator.initialize();
|
|
46
48
|
|
|
49
|
+
registerGraphifyTools(pi, getCoordinator, { registeredToolNames });
|
|
50
|
+
|
|
47
51
|
const status = await coordinator.status({ cwd: ctx.cwd });
|
|
48
52
|
if (status.hasGraph && status.graphPath) {
|
|
49
53
|
ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwjd/pi-graphify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Graphify knowledge graph integration for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "
|
|
37
|
+
"test": "node --test --import tsx src/tools/tools.test.ts src/extensions/extension-load.test.ts",
|
|
38
38
|
"lint": "biome check .",
|
|
39
39
|
"lint:fix": "biome check --write .",
|
|
40
40
|
"format": "biome format --write .",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
58
58
|
"@earendil-works/pi-coding-agent": "^0.80.6",
|
|
59
59
|
"@types/node": "^20.0.0",
|
|
60
|
+
"tsx": "^4.23.0",
|
|
60
61
|
"typebox": "^1.1.24",
|
|
61
62
|
"typescript": "^5.0.0"
|
|
62
63
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GraphifyBackend, GraphifyCapabilities, GraphifyResult } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const allCapabilitiesEnabled = (): GraphifyCapabilities => ({
|
|
4
|
+
build: true,
|
|
5
|
+
query: true,
|
|
6
|
+
path: true,
|
|
7
|
+
explain: true,
|
|
8
|
+
affected: true,
|
|
9
|
+
add: true,
|
|
10
|
+
watch: true,
|
|
11
|
+
reflect: true,
|
|
12
|
+
mcp: true,
|
|
13
|
+
status: true,
|
|
14
|
+
version: true,
|
|
15
|
+
directedGraph: true,
|
|
16
|
+
incrementalUpdate: true,
|
|
17
|
+
semanticExtraction: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export interface MockBackendOptions {
|
|
21
|
+
version?: string;
|
|
22
|
+
capabilities?: GraphifyCapabilities;
|
|
23
|
+
run?: (operation: string, options: unknown) => Promise<GraphifyResult>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createMockBackend(options: MockBackendOptions = {}): GraphifyBackend {
|
|
27
|
+
const {
|
|
28
|
+
version = "2.100.0",
|
|
29
|
+
capabilities = allCapabilitiesEnabled(),
|
|
30
|
+
run = async (operation) => ({
|
|
31
|
+
stdout: `ok: ${operation}`,
|
|
32
|
+
stderr: "",
|
|
33
|
+
exitCode: 0,
|
|
34
|
+
durationMs: 1,
|
|
35
|
+
backend: "cli",
|
|
36
|
+
feature: operation,
|
|
37
|
+
}),
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
type: "cli",
|
|
42
|
+
version,
|
|
43
|
+
capabilities,
|
|
44
|
+
run,
|
|
45
|
+
close: async () => {},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/coordinator.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface GraphifyStatusResult {
|
|
|
37
37
|
export interface CoordinatorOptions {
|
|
38
38
|
cwd: string;
|
|
39
39
|
config?: GraphifyConfig;
|
|
40
|
+
/** Optional backend, primarily for tests. */
|
|
41
|
+
backend?: GraphifyBackend;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export class GraphifyCoordinator {
|
|
@@ -49,6 +51,7 @@ export class GraphifyCoordinator {
|
|
|
49
51
|
constructor(options: CoordinatorOptions) {
|
|
50
52
|
this.cwd = options.cwd;
|
|
51
53
|
this.config = options.config ?? loadConfig();
|
|
54
|
+
this.backend = options.backend ?? null;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
async initialize(): Promise<void> {
|
|
@@ -57,11 +60,13 @@ export class GraphifyCoordinator {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
if (!this.backend) {
|
|
64
|
+
// Phase 2 only supports CLI backend. MCP is a Phase 6 addition.
|
|
65
|
+
this.backend = await createCliBackend({
|
|
66
|
+
cwd: this.cwd,
|
|
67
|
+
timeoutMs: this.config.queryTimeoutMs,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
65
70
|
} catch (error) {
|
|
66
71
|
this.backend = null;
|
|
67
72
|
this._warnings.push(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smoke test that verifies the extension entry point module loads and exports a
|
|
6
|
+
* valid factory function. This catches syntax errors, import issues, or jiti
|
|
7
|
+
* problems without requiring the full Pi runtime.
|
|
8
|
+
*/
|
|
9
|
+
describe("extension entry point", () => {
|
|
10
|
+
it("exports a default factory function", async () => {
|
|
11
|
+
const module = await import("../../extensions/index.js");
|
|
12
|
+
assert.equal(typeof module.default, "function");
|
|
13
|
+
assert.equal(module.default.length, 1);
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/graphify.ts
CHANGED
|
@@ -33,6 +33,8 @@ export function buildGraphifyHint(graphPath: string): string {
|
|
|
33
33
|
"<graphify-context>",
|
|
34
34
|
`A Graphify knowledge graph is available at ${graphPath}.`,
|
|
35
35
|
"When answering questions about this codebase, prefer querying the graph via the graphify_status, graphify_query, graphify_path, and graphify_explain tools instead of grepping or reading raw files.",
|
|
36
|
+
"Use graphify_explain to explain a specific function, file, tool, or concept that is a node in the graph.",
|
|
37
|
+
"Use graphify_path to trace how two functions, files, tools, or concepts are connected in the graph.",
|
|
36
38
|
"Only fall back to raw file reads when the graph lacks the needed detail or when modifying/debugging specific lines.",
|
|
37
39
|
"</graphify-context>",
|
|
38
40
|
].join("\n");
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerAffectedTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_affected",
|
|
10
|
+
label: "Graphify Affected",
|
|
11
|
+
description: "Show the blast radius of changes to the given files or nodes.",
|
|
12
|
+
promptSnippet: "Show what files, nodes, or concepts depend on the given files or nodes",
|
|
13
|
+
promptGuidelines: [
|
|
14
|
+
"Use graphify_affected when the user asks what would break if they change a file or node.",
|
|
15
|
+
"Use graphify_affected to find downstream dependents of a file or concept before refactoring.",
|
|
16
|
+
],
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
cwd: Type.String({ description: "Working directory of the project to analyze." }),
|
|
19
|
+
files: Type.Array(Type.String({ minLength: 1 }), {
|
|
20
|
+
minItems: 1,
|
|
21
|
+
description: "The files or nodes whose downstream dependents should be reported.",
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
25
|
+
const coordinator = getCoordinator();
|
|
26
|
+
if (!coordinator) {
|
|
27
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await coordinator.affected({ ...params, cwd: ctx.cwd });
|
|
31
|
+
return formatGraphifyResult(result);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return formatGraphifyError(error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerBuildTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_build",
|
|
10
|
+
label: "Graphify Build",
|
|
11
|
+
description:
|
|
12
|
+
"Build or incrementally update the Graphify knowledge graph for the current project.",
|
|
13
|
+
promptSnippet: "Build or update the project's Graphify knowledge graph",
|
|
14
|
+
promptGuidelines: [
|
|
15
|
+
"Use graphify_build when the user wants to create or update the knowledge graph.",
|
|
16
|
+
"Use graphify_build when graphify_status reports that no graph exists.",
|
|
17
|
+
],
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
cwd: Type.String({ description: "Working directory of the project to build the graph for." }),
|
|
20
|
+
codeOnly: Type.Optional(
|
|
21
|
+
Type.Boolean({ description: "Only index code files; skip semantic extraction." }),
|
|
22
|
+
),
|
|
23
|
+
update: Type.Optional(
|
|
24
|
+
Type.Boolean({ description: "Perform an incremental update instead of a full rebuild." }),
|
|
25
|
+
),
|
|
26
|
+
directed: Type.Optional(
|
|
27
|
+
Type.Boolean({ description: "Build a directed graph instead of an undirected one." }),
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
31
|
+
const coordinator = getCoordinator();
|
|
32
|
+
if (!coordinator) {
|
|
33
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const result = await coordinator.build({ ...params, cwd: ctx.cwd });
|
|
37
|
+
return formatGraphifyResult(result);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return formatGraphifyError(error);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerExplainTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_explain",
|
|
10
|
+
label: "Graphify Explain",
|
|
11
|
+
description:
|
|
12
|
+
"Explain how a specific node (function, file, tool, or concept) is connected in the Graphify knowledge graph.",
|
|
13
|
+
promptSnippet: "Explain a function, file, tool, or concept from the project's knowledge graph",
|
|
14
|
+
promptGuidelines: [
|
|
15
|
+
"Use graphify_explain when the user asks what a specific function, file, tool, or concept does.",
|
|
16
|
+
"Use graphify_explain instead of reading source code when the subject is a node in the knowledge graph.",
|
|
17
|
+
],
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
cwd: Type.String({ description: "Working directory of the project to explain." }),
|
|
20
|
+
node: Type.String({
|
|
21
|
+
minLength: 1,
|
|
22
|
+
description: "The node or concept to explain in the graph.",
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
26
|
+
const coordinator = getCoordinator();
|
|
27
|
+
if (!coordinator) {
|
|
28
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await coordinator.explain({ ...params, cwd: ctx.cwd });
|
|
32
|
+
return formatGraphifyResult(result);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return formatGraphifyError(error);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared result formatting helpers for graphify tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GraphifyResult } from "../backends/types.js";
|
|
6
|
+
import { GraphifyError } from "../errors.js";
|
|
7
|
+
import { errorResult, successResult, type ToolResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Format a successful Graphify result into tool content. */
|
|
10
|
+
export function formatGraphifyResult(result: GraphifyResult): ToolResult {
|
|
11
|
+
return successResult(result);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Format a Graphify or unexpected error into friendly tool content. */
|
|
15
|
+
export function formatGraphifyError(error: unknown): ToolResult {
|
|
16
|
+
if (error instanceof GraphifyError) {
|
|
17
|
+
return errorResult(error.userMessage, { code: error.code, details: error.details });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return errorResult(error.message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return errorResult(String(error));
|
|
25
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,12 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool registration barrel
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { GraphifyCapabilities } from "../backends/types.js";
|
|
3
|
+
import type { GraphifyCoordinator } from "../coordinator.js";
|
|
4
|
+
import { registerAffectedTool } from "./affected.js";
|
|
5
|
+
import { registerBuildTool } from "./build.js";
|
|
6
|
+
import { registerExplainTool } from "./explain.js";
|
|
7
|
+
import { registerPathTool } from "./path.js";
|
|
8
|
+
import { registerQueryTool } from "./query.js";
|
|
6
9
|
import { registerStatusTool } from "./status.js";
|
|
10
|
+
import { registerVersionTool } from "./version.js";
|
|
11
|
+
|
|
12
|
+
export type CoordinatorProvider = () => GraphifyCoordinator | null;
|
|
13
|
+
|
|
14
|
+
export interface RegisterToolsOptions {
|
|
15
|
+
/** Tracks tool names that have already been registered to prevent duplicates. */
|
|
16
|
+
registeredToolNames: Set<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerGraphifyTools(
|
|
20
|
+
pi: ExtensionAPI,
|
|
21
|
+
getCoordinator: CoordinatorProvider,
|
|
22
|
+
options: RegisterToolsOptions,
|
|
23
|
+
): void {
|
|
24
|
+
const { registeredToolNames } = options;
|
|
25
|
+
const coordinator = getCoordinator();
|
|
26
|
+
if (!coordinator) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
7
29
|
|
|
8
|
-
|
|
30
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_status", () =>
|
|
31
|
+
registerStatusTool(pi, getCoordinator),
|
|
32
|
+
);
|
|
33
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_build", () =>
|
|
34
|
+
registerBuildTool(pi, getCoordinator),
|
|
35
|
+
);
|
|
36
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_query", () =>
|
|
37
|
+
registerQueryTool(pi, getCoordinator),
|
|
38
|
+
);
|
|
39
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_path", () =>
|
|
40
|
+
registerPathTool(pi, getCoordinator),
|
|
41
|
+
);
|
|
42
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_explain", () =>
|
|
43
|
+
registerExplainTool(pi, getCoordinator),
|
|
44
|
+
);
|
|
45
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_affected", () =>
|
|
46
|
+
registerAffectedTool(pi, getCoordinator),
|
|
47
|
+
);
|
|
48
|
+
registerToolIfSupported(coordinator, registeredToolNames, "graphify_version", () =>
|
|
49
|
+
registerVersionTool(pi, getCoordinator),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function registerToolIfSupported(
|
|
54
|
+
coordinator: GraphifyCoordinator,
|
|
55
|
+
registeredToolNames: Set<string>,
|
|
56
|
+
name: string,
|
|
57
|
+
register: () => void,
|
|
58
|
+
): void {
|
|
59
|
+
if (registeredToolNames.has(name)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const feature = nameToFeature(name);
|
|
64
|
+
if (feature && !coordinator.supports(feature)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registeredToolNames.add(name);
|
|
69
|
+
register();
|
|
70
|
+
}
|
|
9
71
|
|
|
10
|
-
|
|
11
|
-
|
|
72
|
+
function nameToFeature(name: string): keyof GraphifyCapabilities | null {
|
|
73
|
+
switch (name) {
|
|
74
|
+
case "graphify_build":
|
|
75
|
+
return "build";
|
|
76
|
+
case "graphify_query":
|
|
77
|
+
return "query";
|
|
78
|
+
case "graphify_path":
|
|
79
|
+
return "path";
|
|
80
|
+
case "graphify_explain":
|
|
81
|
+
return "explain";
|
|
82
|
+
case "graphify_affected":
|
|
83
|
+
return "affected";
|
|
84
|
+
case "graphify_version":
|
|
85
|
+
return "version";
|
|
86
|
+
default:
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
12
89
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerPathTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_path",
|
|
10
|
+
label: "Graphify Path",
|
|
11
|
+
description:
|
|
12
|
+
"Trace the shortest path between two nodes (functions, files, tools, or concepts) in the Graphify knowledge graph.",
|
|
13
|
+
promptSnippet:
|
|
14
|
+
"Trace how two functions, files, tools, or concepts are connected in the knowledge graph",
|
|
15
|
+
promptGuidelines: [
|
|
16
|
+
"Use graphify_path when the user asks how two concepts, functions, files, tools, or symbols are related.",
|
|
17
|
+
"Use graphify_path instead of manually tracing code when both source and target are known nodes in the graph.",
|
|
18
|
+
],
|
|
19
|
+
parameters: Type.Object({
|
|
20
|
+
cwd: Type.String({ description: "Working directory of the project to search." }),
|
|
21
|
+
source: Type.String({
|
|
22
|
+
minLength: 1,
|
|
23
|
+
description: "The source node or concept to start from.",
|
|
24
|
+
}),
|
|
25
|
+
target: Type.String({
|
|
26
|
+
minLength: 1,
|
|
27
|
+
description: "The target node or concept to find a path to.",
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
31
|
+
const coordinator = getCoordinator();
|
|
32
|
+
if (!coordinator) {
|
|
33
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const result = await coordinator.path({ ...params, cwd: ctx.cwd });
|
|
37
|
+
return formatGraphifyResult(result);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return formatGraphifyError(error);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerQueryTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_query",
|
|
10
|
+
label: "Graphify Query",
|
|
11
|
+
description: "Ask a natural-language question against the Graphify knowledge graph.",
|
|
12
|
+
promptSnippet: "Ask a natural-language question about the project's knowledge graph",
|
|
13
|
+
promptGuidelines: [
|
|
14
|
+
"Use graphify_query when the user asks a broad question about the codebase.",
|
|
15
|
+
"Use graphify_query instead of grepping or reading files when a graph is available.",
|
|
16
|
+
],
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
cwd: Type.String({ description: "Working directory of the project to query." }),
|
|
19
|
+
question: Type.String({
|
|
20
|
+
minLength: 1,
|
|
21
|
+
description: "The natural-language question to run against the graph.",
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
25
|
+
const coordinator = getCoordinator();
|
|
26
|
+
if (!coordinator) {
|
|
27
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await coordinator.query({ ...params, cwd: ctx.cwd });
|
|
31
|
+
return formatGraphifyResult(result);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return formatGraphifyError(error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
package/src/tools/status.ts
CHANGED
|
@@ -11,6 +11,11 @@ export function registerStatusTool(pi: ExtensionAPI, getCoordinator: Coordinator
|
|
|
11
11
|
name: "graphify_status",
|
|
12
12
|
label: "Graphify Status",
|
|
13
13
|
description: "Check whether a Graphify knowledge graph exists for the current project.",
|
|
14
|
+
promptSnippet: "Check whether a Graphify knowledge graph exists for the current project",
|
|
15
|
+
promptGuidelines: [
|
|
16
|
+
"Use graphify_status when the user asks if a Graphify graph is available.",
|
|
17
|
+
"Use graphify_status before calling other graphify tools to confirm the graph exists.",
|
|
18
|
+
],
|
|
14
19
|
parameters: Type.Object({}),
|
|
15
20
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
16
21
|
const coordinator = getCoordinator();
|
|
@@ -38,7 +43,7 @@ export function registerStatusTool(pi: ExtensionAPI, getCoordinator: Coordinator
|
|
|
38
43
|
content: [
|
|
39
44
|
{
|
|
40
45
|
type: "text",
|
|
41
|
-
text: `No Graphify graph found.
|
|
46
|
+
text: `No Graphify graph found. Build one with \`graphify .\`.${cliNote}`,
|
|
42
47
|
},
|
|
43
48
|
],
|
|
44
49
|
details,
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { allCapabilitiesEnabled, createMockBackend } from "../backends/mock.js";
|
|
5
|
+
import { GraphifyCoordinator } from "../coordinator.js";
|
|
6
|
+
import { GraphifyError } from "../errors.js";
|
|
7
|
+
import { type RegisterToolsOptions, registerGraphifyTools } from "./index.js";
|
|
8
|
+
|
|
9
|
+
interface ToolRecord {
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: unknown;
|
|
14
|
+
execute: (
|
|
15
|
+
toolCallId: string,
|
|
16
|
+
params: unknown,
|
|
17
|
+
signal: unknown,
|
|
18
|
+
onUpdate: (content: unknown) => void,
|
|
19
|
+
ctx: { cwd: string },
|
|
20
|
+
) => Promise<{ content: { type: string; text: string }[]; details: Record<string, unknown> }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MockPi {
|
|
24
|
+
tools: Record<string, ToolRecord>;
|
|
25
|
+
registerTool: (tool: ToolRecord) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createMockPi(): MockPi {
|
|
29
|
+
const tools: Record<string, ToolRecord> = {};
|
|
30
|
+
return {
|
|
31
|
+
tools,
|
|
32
|
+
registerTool: (tool: ToolRecord) => {
|
|
33
|
+
tools[tool.name] = tool;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTestCoordinator(backend = createMockBackend()): GraphifyCoordinator {
|
|
39
|
+
return new GraphifyCoordinator({ cwd: process.cwd(), backend });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createRegisterOptions(): RegisterToolsOptions {
|
|
43
|
+
return { registeredToolNames: new Set<string>() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const noopOnUpdate = (): void => {};
|
|
47
|
+
|
|
48
|
+
const expectedTools = [
|
|
49
|
+
"graphify_status",
|
|
50
|
+
"graphify_build",
|
|
51
|
+
"graphify_query",
|
|
52
|
+
"graphify_path",
|
|
53
|
+
"graphify_explain",
|
|
54
|
+
"graphify_affected",
|
|
55
|
+
"graphify_version",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
describe("registerGraphifyTools", () => {
|
|
59
|
+
it("registers all expected tools when all capabilities are enabled", () => {
|
|
60
|
+
const pi = createMockPi();
|
|
61
|
+
const coordinator = createTestCoordinator();
|
|
62
|
+
const options = createRegisterOptions();
|
|
63
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => coordinator, options);
|
|
64
|
+
|
|
65
|
+
for (const name of expectedTools) {
|
|
66
|
+
assert.ok(name in pi.tools, `expected tool ${name} to be registered`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not register tools when coordinator is null", () => {
|
|
71
|
+
const pi = createMockPi();
|
|
72
|
+
const options = createRegisterOptions();
|
|
73
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => null, options);
|
|
74
|
+
|
|
75
|
+
assert.equal(Object.keys(pi.tools).length, 0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("skips tools whose capabilities are disabled", () => {
|
|
79
|
+
const pi = createMockPi();
|
|
80
|
+
const capabilities = { ...allCapabilitiesEnabled(), build: false, query: false };
|
|
81
|
+
const backend = createMockBackend({ capabilities });
|
|
82
|
+
const coordinator = createTestCoordinator(backend);
|
|
83
|
+
const options = createRegisterOptions();
|
|
84
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => coordinator, options);
|
|
85
|
+
|
|
86
|
+
assert.ok(
|
|
87
|
+
!("graphify_build" in pi.tools),
|
|
88
|
+
"graphify_build should not be registered when build is unsupported",
|
|
89
|
+
);
|
|
90
|
+
assert.ok(
|
|
91
|
+
!("graphify_query" in pi.tools),
|
|
92
|
+
"graphify_query should not be registered when query is unsupported",
|
|
93
|
+
);
|
|
94
|
+
assert.ok(
|
|
95
|
+
"graphify_path" in pi.tools,
|
|
96
|
+
"graphify_path should be registered when path is supported",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("prevents duplicate registration when called again with the same set", () => {
|
|
101
|
+
const pi = createMockPi();
|
|
102
|
+
const coordinator = createTestCoordinator();
|
|
103
|
+
const options = createRegisterOptions();
|
|
104
|
+
|
|
105
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => coordinator, options);
|
|
106
|
+
const firstCount = Object.keys(pi.tools).length;
|
|
107
|
+
|
|
108
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => coordinator, options);
|
|
109
|
+
const secondCount = Object.keys(pi.tools).length;
|
|
110
|
+
|
|
111
|
+
assert.equal(firstCount, expectedTools.length);
|
|
112
|
+
assert.equal(secondCount, expectedTools.length);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
async function runTool(
|
|
117
|
+
pi: MockPi,
|
|
118
|
+
name: string,
|
|
119
|
+
params: Record<string, unknown>,
|
|
120
|
+
cwd: string,
|
|
121
|
+
): Promise<{ content: { type: string; text: string }[]; details: Record<string, unknown> }> {
|
|
122
|
+
return pi.tools[name].execute("call-1", params, null, noopOnUpdate, { cwd });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("graphify_build", () => {
|
|
126
|
+
it("returns formatted result on success", async () => {
|
|
127
|
+
const pi = createMockPi();
|
|
128
|
+
const coordinator = createTestCoordinator();
|
|
129
|
+
registerGraphifyTools(
|
|
130
|
+
pi as unknown as ExtensionAPI,
|
|
131
|
+
() => coordinator,
|
|
132
|
+
createRegisterOptions(),
|
|
133
|
+
);
|
|
134
|
+
const result = await runTool(pi, "graphify_build", {}, process.cwd());
|
|
135
|
+
|
|
136
|
+
assert.equal(result.content[0].text, "ok: build");
|
|
137
|
+
assert.equal(result.details.exitCode, 0);
|
|
138
|
+
assert.equal(result.details.feature, "build");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("graphify_query", () => {
|
|
143
|
+
it("passes question and cwd to the coordinator", async () => {
|
|
144
|
+
const pi = createMockPi();
|
|
145
|
+
let captured: unknown = null;
|
|
146
|
+
const backend = createMockBackend({
|
|
147
|
+
run: async (_operation, options) => {
|
|
148
|
+
captured = options;
|
|
149
|
+
return {
|
|
150
|
+
stdout: "answer",
|
|
151
|
+
stderr: "",
|
|
152
|
+
exitCode: 0,
|
|
153
|
+
durationMs: 1,
|
|
154
|
+
backend: "cli",
|
|
155
|
+
feature: "query",
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const coordinator = createTestCoordinator(backend);
|
|
160
|
+
registerGraphifyTools(
|
|
161
|
+
pi as unknown as ExtensionAPI,
|
|
162
|
+
() => coordinator,
|
|
163
|
+
createRegisterOptions(),
|
|
164
|
+
);
|
|
165
|
+
const result = await runTool(
|
|
166
|
+
pi,
|
|
167
|
+
"graphify_query",
|
|
168
|
+
{ question: "how does auth work?" },
|
|
169
|
+
process.cwd(),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
assert.equal(result.content[0].text, "answer");
|
|
173
|
+
assert.deepEqual(captured, { cwd: process.cwd(), question: "how does auth work?" });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("is not registered when the coordinator is missing", () => {
|
|
177
|
+
const pi = createMockPi();
|
|
178
|
+
registerGraphifyTools(pi as unknown as ExtensionAPI, () => null, createRegisterOptions());
|
|
179
|
+
assert.equal("graphify_query" in pi.tools, false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("returns an error when the coordinator becomes null after registration", async () => {
|
|
183
|
+
const pi = createMockPi();
|
|
184
|
+
let coordinator: GraphifyCoordinator | null = createTestCoordinator();
|
|
185
|
+
registerGraphifyTools(
|
|
186
|
+
pi as unknown as ExtensionAPI,
|
|
187
|
+
() => coordinator,
|
|
188
|
+
createRegisterOptions(),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
coordinator = null;
|
|
192
|
+
const result = await runTool(pi, "graphify_query", { question: "x" }, process.cwd());
|
|
193
|
+
|
|
194
|
+
assert.ok(result.content[0].text.includes("Graphify is not initialized"));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("graphify_path", () => {
|
|
199
|
+
it("returns formatted result on success", async () => {
|
|
200
|
+
const pi = createMockPi();
|
|
201
|
+
const coordinator = createTestCoordinator();
|
|
202
|
+
registerGraphifyTools(
|
|
203
|
+
pi as unknown as ExtensionAPI,
|
|
204
|
+
() => coordinator,
|
|
205
|
+
createRegisterOptions(),
|
|
206
|
+
);
|
|
207
|
+
const result = await runTool(pi, "graphify_path", { source: "a", target: "b" }, process.cwd());
|
|
208
|
+
|
|
209
|
+
assert.equal(result.content[0].text, "ok: path");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("graphify_explain", () => {
|
|
214
|
+
it("returns formatted result on success", async () => {
|
|
215
|
+
const pi = createMockPi();
|
|
216
|
+
const coordinator = createTestCoordinator();
|
|
217
|
+
registerGraphifyTools(
|
|
218
|
+
pi as unknown as ExtensionAPI,
|
|
219
|
+
() => coordinator,
|
|
220
|
+
createRegisterOptions(),
|
|
221
|
+
);
|
|
222
|
+
const result = await runTool(pi, "graphify_explain", { node: "auth" }, process.cwd());
|
|
223
|
+
|
|
224
|
+
assert.equal(result.content[0].text, "ok: explain");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("graphify_affected", () => {
|
|
229
|
+
it("returns formatted result on success", async () => {
|
|
230
|
+
const pi = createMockPi();
|
|
231
|
+
const coordinator = createTestCoordinator();
|
|
232
|
+
registerGraphifyTools(
|
|
233
|
+
pi as unknown as ExtensionAPI,
|
|
234
|
+
() => coordinator,
|
|
235
|
+
createRegisterOptions(),
|
|
236
|
+
);
|
|
237
|
+
const result = await runTool(
|
|
238
|
+
pi,
|
|
239
|
+
"graphify_affected",
|
|
240
|
+
{ files: ["src/auth.ts"] },
|
|
241
|
+
process.cwd(),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
assert.equal(result.content[0].text, "ok: affected");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("graphify_version", () => {
|
|
249
|
+
it("returns formatted version on success", async () => {
|
|
250
|
+
const pi = createMockPi();
|
|
251
|
+
const backend = createMockBackend({ version: "2.100.0" });
|
|
252
|
+
const coordinator = createTestCoordinator(backend);
|
|
253
|
+
registerGraphifyTools(
|
|
254
|
+
pi as unknown as ExtensionAPI,
|
|
255
|
+
() => coordinator,
|
|
256
|
+
createRegisterOptions(),
|
|
257
|
+
);
|
|
258
|
+
const result = await runTool(pi, "graphify_version", {}, process.cwd());
|
|
259
|
+
|
|
260
|
+
assert.equal(result.content[0].text, "2.100.0");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("tool error handling", () => {
|
|
265
|
+
it("returns friendly error when the backend throws GraphifyError", async () => {
|
|
266
|
+
const pi = createMockPi();
|
|
267
|
+
const backend = createMockBackend({
|
|
268
|
+
run: async () => {
|
|
269
|
+
throw new GraphifyError("QUERY", "graphify query failed");
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const coordinator = createTestCoordinator(backend);
|
|
273
|
+
registerGraphifyTools(
|
|
274
|
+
pi as unknown as ExtensionAPI,
|
|
275
|
+
() => coordinator,
|
|
276
|
+
createRegisterOptions(),
|
|
277
|
+
);
|
|
278
|
+
const result = await runTool(pi, "graphify_query", { question: "x" }, process.cwd());
|
|
279
|
+
|
|
280
|
+
assert.ok(result.content[0].text.includes("graphify query failed"));
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GraphifyResult } from "../backends/types.js";
|
|
2
|
+
|
|
3
|
+
export interface ToolContext {
|
|
4
|
+
cwd: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface TextContentItem {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolResult {
|
|
13
|
+
content: TextContentItem[];
|
|
14
|
+
details: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function successResult(result: GraphifyResult): ToolResult {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: result.stdout || "Operation completed successfully." }],
|
|
20
|
+
details: {
|
|
21
|
+
exitCode: result.exitCode,
|
|
22
|
+
durationMs: result.durationMs,
|
|
23
|
+
backend: result.backend,
|
|
24
|
+
feature: result.feature,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function errorResult(text: string, details: Record<string, unknown> = {}): ToolResult {
|
|
30
|
+
return { content: [{ type: "text", text: `Error: ${text}` }], details };
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { GraphifyError } from "../errors.js";
|
|
4
|
+
import { formatGraphifyError, formatGraphifyResult } from "./format.js";
|
|
5
|
+
import type { CoordinatorProvider } from "./index.js";
|
|
6
|
+
|
|
7
|
+
export function registerVersionTool(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
name: "graphify_version",
|
|
10
|
+
label: "Graphify Version",
|
|
11
|
+
description: "Report the installed Graphify version and compatibility status.",
|
|
12
|
+
promptSnippet: "Report the installed Graphify CLI version and compatibility status",
|
|
13
|
+
promptGuidelines: [
|
|
14
|
+
"Use graphify_version when the user asks what version of Graphify is installed.",
|
|
15
|
+
"Use graphify_version to check whether the installed Graphify version is supported.",
|
|
16
|
+
],
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
cwd: Type.Optional(
|
|
19
|
+
Type.String({ description: "Optional working directory to check Graphify in." }),
|
|
20
|
+
),
|
|
21
|
+
}),
|
|
22
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
23
|
+
const coordinator = getCoordinator();
|
|
24
|
+
if (!coordinator) {
|
|
25
|
+
return formatGraphifyError(new GraphifyError("MISSING", "Graphify is not initialized."));
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = await coordinator.getVersion({ cwd: ctx.cwd });
|
|
29
|
+
return formatGraphifyResult(result);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return formatGraphifyError(error);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|