@wwjd/pi-graphify 0.2.0 → 0.4.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.
@@ -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
+ }