@wwjd/pi-graphify 0.1.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 CHANGED
@@ -5,6 +5,33 @@ 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
+
21
+ ## [0.2.0](https://github.com/WianVDM/pi-graphify/compare/pi-graphify-v0.1.0...pi-graphify-v0.2.0) (2026-07-12)
22
+
23
+
24
+ ### Features
25
+
26
+ * add graphify_status tool and /graphify-status command ([99f809c](https://github.com/WianVDM/pi-graphify/commit/99f809caace328e5af97a5dcdac50af025437776))
27
+ * implement phase 2 backend abstraction ([40afa65](https://github.com/WianVDM/pi-graphify/commit/40afa65bbadf9bcb72676a613d94f6bdb677cc5d))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * correct status tool description and missing-Graphify warning ([c4225c1](https://github.com/WianVDM/pi-graphify/commit/c4225c1591124a65ca0df33565168970a6720a9d))
33
+ * normalize package author email casing ([fcfa352](https://github.com/WianVDM/pi-graphify/commit/fcfa3520463ed860be1264daa6443c608a197444))
34
+
8
35
  ## [Unreleased]
9
36
 
10
37
  ## 0.1.0 (2026-07-11)
package/README.md CHANGED
@@ -1,93 +1,86 @@
1
1
  # pi-graphify
2
2
 
3
- A production-quality [pi](https://pi.dev) extension for [Graphify](https://github.com/Graphify-Labs/graphify).
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
- This extension brings Graphify knowledge graph capabilities into your Pi sessions, making it easy to query, explore, and update codebase graphs without leaving the agent.
5
+ ## Install
6
6
 
7
- > ⚠️ **Work in progress** — this package is in early development. The public API, tools, and commands will evolve as the extension matures toward a stable 1.0 release.
7
+ ```bash
8
+ pi install @wwjd/pi-graphify
9
+ ```
8
10
 
9
- ## Quick Start
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's Included
17
+ ## What it does
26
18
 
27
- ```
28
- pi-graphify/
29
- ├── extensions/
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
- ## Package Structure
23
+ ## Requirements
44
24
 
45
- ### Extensions (`extensions/`)
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
- TypeScript modules that extend pi's behavior with custom tools, slash commands, and event handlers.
28
+ ## Current commands
48
29
 
49
- ### Source (`src/`)
30
+ | Command | Description |
31
+ |---|---|
32
+ | `/graphify-status` | Check whether a graph exists and whether the Graphify CLI is compatible. |
50
33
 
51
- Pi-agnostic implementation logic. The `extensions/` layer imports from here and registers everything with Pi.
34
+ ## Current tools
52
35
 
53
- ## Development
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
- ### Requirements
46
+ ## What's coming
56
47
 
57
- - Node.js 22+
58
- - npm 10+
59
- - pi CLI
48
+ Slash commands for the tools above are planned in the next phase:
60
49
 
61
- ### Scripts
50
+ - `/graphify-build`
51
+ - `/graphify-query`
52
+ - `/graphify-path`
53
+ - `/graphify-explain`
54
+ - `/graphify-affected`
55
+ - `/graphify-version`
62
56
 
63
- | Command | Description |
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
- ### Testing the package
59
+ ## How it works
71
60
 
72
- ```bash
73
- # Run ephemerally with no other extensions loaded
74
- pi -ne -e . --no-session
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
- # Test a specific tool in print mode
77
- pi -ne -e . --no-session -p "List the available tools."
78
- ```
65
+ ## Status
79
66
 
80
- ## Publishing
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
- This package is intended for publication to npm so it can be listed on [pi.dev/packages](https://pi.dev/packages). CI/CD workflows for release automation are included under `.github/workflows/`.
69
+ ## Documentation
83
70
 
84
- See `RELEASE.md` for versioning and release standards.
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
- Before publishing, update:
78
+ ## Development
87
79
 
88
- - `package.json` author and repository fields
89
- - `README.md` with final usage instructions
90
- - `CHANGELOG.md` with release notes
80
+ ```bash
81
+ npm run typecheck
82
+ npm run lint
83
+ ```
91
84
 
92
85
  ## License
93
86
 
@@ -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)
@@ -4,28 +4,70 @@
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-build, /graphify-status, ...)
7
+ * - Registers slash commands (/graphify-status, ...)
8
8
  * - Optionally watches source files for incremental rebuilds
9
9
  */
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import { loadConfig } from "../src/config.js";
12
+ import { GraphifyCoordinator } from "../src/coordinator.js";
13
13
  import { buildGraphifyHint, graphifyStatus } from "../src/graphify.js";
14
14
  import { registerGraphifyTools } from "../src/tools/index.js";
15
15
 
16
16
  export default function (pi: ExtensionAPI) {
17
- const config = loadConfig();
17
+ // Coordinator is created per session and shared by tools and commands.
18
+ let coordinator: GraphifyCoordinator | null = null;
19
+ const getCoordinator = () => coordinator;
20
+ const registeredToolNames = new Set<string>();
18
21
 
19
- // ── 1. Notify and inject context when a graph is present ──────────
22
+ // ── 1. Register slash commands synchronously ─────────────────────
23
+ pi.registerCommand("graphify-status", {
24
+ description: "Show Graphify graph status for the current project",
25
+ handler: async (_args, ctx) => {
26
+ const current = getCoordinator();
27
+ if (!current) {
28
+ ctx.ui.notify("Graphify is not initialized.", "warning");
29
+ return;
30
+ }
31
+
32
+ const status = await current.status({ cwd: ctx.cwd });
33
+ if (status.hasGraph && status.graphPath) {
34
+ ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
35
+ } else {
36
+ ctx.ui.notify("No Graphify graph found. Build one with `graphify .`.", "warning");
37
+ }
38
+ },
39
+ });
40
+
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.
20
45
  pi.on("session_start", async (_event, ctx) => {
21
- const status = await graphifyStatus(ctx.cwd);
46
+ coordinator = new GraphifyCoordinator({ cwd: ctx.cwd });
47
+ await coordinator.initialize();
48
+
49
+ registerGraphifyTools(pi, getCoordinator, { registeredToolNames });
50
+
51
+ const status = await coordinator.status({ cwd: ctx.cwd });
22
52
  if (status.hasGraph && status.graphPath) {
23
53
  ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
24
54
  }
55
+
56
+ if (coordinator.version === null) {
57
+ ctx.ui.notify(
58
+ "Graphify CLI was not detected on PATH. Install Graphify to use build, query, and other graph tools.",
59
+ "warning",
60
+ );
61
+ }
62
+
63
+ for (const warning of coordinator.warnings) {
64
+ ctx.ui.notify(warning, "warning");
65
+ }
25
66
  });
26
67
 
68
+ // ── 3. Inject context hint when a graph is present ────────────────
27
69
  pi.on("before_agent_start", async (event, ctx) => {
28
- const status = await graphifyStatus(ctx.cwd);
70
+ const status = graphifyStatus(ctx.cwd);
29
71
  if (!status.hasGraph || !status.graphPath) {
30
72
  return;
31
73
  }
@@ -36,19 +78,11 @@ export default function (pi: ExtensionAPI) {
36
78
  };
37
79
  });
38
80
 
39
- // ── 2. Register custom tools the LLM can call ─────────────────────
40
- registerGraphifyTools(pi, config);
41
-
42
- // ── 3. Register slash commands ────────────────────────────────────
43
- pi.registerCommand("graphify-status", {
44
- description: "Show Graphify graph status for the current project",
45
- handler: async (_args, ctx) => {
46
- const status = await graphifyStatus(ctx.cwd);
47
- if (status.hasGraph && status.graphPath) {
48
- ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
49
- } else {
50
- ctx.ui.notify("No Graphify graph found. Run /graphify-build to create one.", "warning");
51
- }
52
- },
81
+ // ── 4. Clean up coordinator on shutdown ─────────────────────────
82
+ pi.on("session_shutdown", async () => {
83
+ if (coordinator) {
84
+ await coordinator.close();
85
+ coordinator = null;
86
+ }
53
87
  });
54
88
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@wwjd/pi-graphify",
3
- "version": "0.1.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",
7
- "author": "WIAN <WIANVDM4@GMAIL.COM>",
7
+ "author": "Wian <wianvdm4@gmail.com>",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/WianVDM/pi-graphify.git"
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "scripts": {
37
- "test": "echo \"No tests yet\" && exit 0",
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,201 @@
1
+ /**
2
+ * CLI backend for Graphify.
3
+ *
4
+ * Resolves the `graphify` executable, runs commands via `execFile`, and
5
+ * normalizes the results into the backend-agnostic `GraphifyResult` shape.
6
+ */
7
+
8
+ import { execFile } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ import { buildCapabilities, detectVersion } from "../version.js";
13
+ import type { GraphifyBackend, GraphifyCapabilities, GraphifyResult } from "./types.js";
14
+ import { allCapabilitiesDisabled } from "./types.js";
15
+
16
+ const DEFAULT_TIMEOUT = 30_000;
17
+
18
+ export interface CliBackendOptions {
19
+ cwd: string;
20
+ timeoutMs?: number;
21
+ /** Explicit executable path from config or env. */
22
+ explicitPath?: string;
23
+ }
24
+
25
+ export class CliBackend implements GraphifyBackend {
26
+ readonly type = "cli" as const;
27
+ private readonly cwd: string;
28
+ private readonly timeoutMs: number;
29
+ private readonly explicitPath: string | undefined;
30
+ private _cli: string | null = null;
31
+ private _version: string | null = null;
32
+ private _capabilities: GraphifyCapabilities | null = null;
33
+
34
+ constructor(options: CliBackendOptions) {
35
+ this.cwd = options.cwd;
36
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
37
+ this.explicitPath = options.explicitPath;
38
+ }
39
+
40
+ get version(): string | null {
41
+ return this._version;
42
+ }
43
+
44
+ get capabilities(): GraphifyCapabilities {
45
+ return this._capabilities ?? allCapabilitiesDisabled();
46
+ }
47
+
48
+ async initialize(): Promise<void> {
49
+ this._cli = await this.resolveCli();
50
+ const versionInfo = await detectVersion(this._cli, this.cwd);
51
+ this._version = versionInfo.version;
52
+
53
+ this._capabilities = await buildCapabilities(this._cli, this.cwd, versionInfo, async (args) =>
54
+ this.runHelp(args),
55
+ );
56
+ }
57
+
58
+ async run(operation: string, options: unknown): Promise<GraphifyResult> {
59
+ const cli = await this.resolveCli();
60
+ const args = buildArgs(operation, options);
61
+ const start = performance.now();
62
+
63
+ return new Promise((resolve) => {
64
+ execFile(cli, args, { cwd: this.cwd, timeout: this.timeoutMs }, (error, stdout, stderr) => {
65
+ const durationMs = Math.round(performance.now() - start);
66
+ if (error) {
67
+ resolve({
68
+ stdout: stdout.trim(),
69
+ stderr: stderr.trim() || error.message,
70
+ exitCode: typeof error.code === "number" ? error.code : 1,
71
+ durationMs,
72
+ backend: "cli",
73
+ feature: operation,
74
+ });
75
+ return;
76
+ }
77
+ resolve({
78
+ stdout: stdout.trim(),
79
+ stderr: stderr.trim(),
80
+ exitCode: 0,
81
+ durationMs,
82
+ backend: "cli",
83
+ feature: operation,
84
+ });
85
+ });
86
+ });
87
+ }
88
+
89
+ async close(): Promise<void> {
90
+ // CLI backend has no persistent process to clean up.
91
+ this._cli = null;
92
+ }
93
+
94
+ private async resolveCli(): Promise<string> {
95
+ if (this._cli) return this._cli;
96
+ if (this.explicitPath) return this.explicitPath;
97
+
98
+ // Try the PATH first.
99
+ const fromPath = await findOnPath("graphify");
100
+ if (fromPath) return fromPath;
101
+
102
+ // Common install locations.
103
+ const candidates = [
104
+ join(homedir(), ".local", "bin", "graphify"),
105
+ join(homedir(), ".cargo", "bin", "graphify"),
106
+ join(homedir(), ".pipx", "venvs", "graphify", "bin", "graphify"),
107
+ ];
108
+
109
+ for (const candidate of candidates) {
110
+ if (existsSync(candidate)) return candidate;
111
+ }
112
+
113
+ // Fall back to the bare command and let execFile report the error.
114
+ return "graphify";
115
+ }
116
+
117
+ private async runHelp(args: string[]): Promise<{ stdout: string; exitCode: number }> {
118
+ const cli = await this.resolveCli();
119
+ return new Promise((resolve) => {
120
+ execFile(cli, args, { cwd: this.cwd, timeout: 10_000 }, (error, stdout, _stderr) => {
121
+ if (error) {
122
+ resolve({
123
+ stdout: stdout.trim(),
124
+ exitCode: typeof error.code === "number" ? error.code : 1,
125
+ });
126
+ return;
127
+ }
128
+ resolve({ stdout: stdout.trim(), exitCode: 0 });
129
+ });
130
+ });
131
+ }
132
+ }
133
+
134
+ /** Best-effort find an executable on the user PATH. */
135
+ function findOnPath(command: string): Promise<string | null> {
136
+ const checkCmd = process.platform === "win32" ? "where" : "which";
137
+ const shell = process.platform === "win32" ? "cmd.exe" : undefined;
138
+
139
+ return new Promise((resolve) => {
140
+ execFile(checkCmd, [command], { shell }, (error, stdout) => {
141
+ if (error || !stdout.trim()) {
142
+ resolve(null);
143
+ return;
144
+ }
145
+ resolve(stdout.split("\n")[0].trim());
146
+ });
147
+ });
148
+ }
149
+
150
+ /** Build a CLI argument array from an operation and options. */
151
+ function buildArgs(operation: string, options: unknown): string[] {
152
+ if (!options || typeof options !== "object") {
153
+ return [operation];
154
+ }
155
+
156
+ const opts = options as Record<string, unknown>;
157
+ const args = [operation];
158
+
159
+ if (opts.cwd && typeof opts.cwd === "string") {
160
+ // graphify commands operate on cwd via the spawned process working directory;
161
+ // no explicit flag is needed for most operations.
162
+ }
163
+
164
+ if (opts.codeOnly === true) args.push("--code-only");
165
+ if (opts.update === true) args.push("--update");
166
+ if (opts.directed === true) args.push("--directed");
167
+
168
+ if (opts.question && typeof opts.question === "string") {
169
+ args.push(opts.question);
170
+ }
171
+ if (opts.source && typeof opts.source === "string") {
172
+ args.push("--source", opts.source);
173
+ }
174
+ if (opts.target && typeof opts.target === "string") {
175
+ args.push("--target", opts.target);
176
+ }
177
+ if (opts.node && typeof opts.node === "string") {
178
+ args.push("--node", opts.node);
179
+ }
180
+ if (Array.isArray(opts.files)) {
181
+ for (const file of opts.files) {
182
+ if (typeof file === "string") args.push(file);
183
+ }
184
+ }
185
+ if (Array.isArray(opts.paths)) {
186
+ for (const p of opts.paths) {
187
+ if (typeof p === "string") {
188
+ args.push(resolve(p));
189
+ }
190
+ }
191
+ }
192
+
193
+ return args;
194
+ }
195
+
196
+ /** Create a CLI backend and initialize it. */
197
+ export async function createCliBackend(options: CliBackendOptions): Promise<CliBackend> {
198
+ const backend = new CliBackend(options);
199
+ await backend.initialize();
200
+ return backend;
201
+ }