@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,16 @@
1
+ /**
2
+ * Shared test helpers for command tests.
3
+ */
4
+
5
+ import { createMockBackend } from "../backends/mock.js";
6
+ import { GraphifyCoordinator } from "../coordinator.js";
7
+
8
+ export function createTestCoordinator(backend = createMockBackend()): GraphifyCoordinator {
9
+ return new GraphifyCoordinator({ cwd: process.cwd(), backend });
10
+ }
11
+
12
+ export function getCoordinator(
13
+ coordinator: GraphifyCoordinator | null,
14
+ ): () => GraphifyCoordinator | null {
15
+ return () => coordinator;
16
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared types for Graphify slash commands.
3
+ *
4
+ * Command executors receive a narrow subset of ExtensionCommandContext so they
5
+ * are easy to test with mock contexts.
6
+ */
7
+
8
+ import type { ExtensionCommandContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
9
+ import type { GraphifyCapabilities } from "../backends/types.js";
10
+ import type { CoordinatorProvider } from "../coordinator.js";
11
+
12
+ export type ExtensionMode = "tui" | "rpc" | "json" | "print";
13
+
14
+ /** UI surface needed by command executors and the unified menu. */
15
+ export interface CommandUIContext
16
+ extends Pick<ExtensionUIContext, "notify" | "input" | "select" | "custom"> {}
17
+
18
+ /** Context passed to every command executor. */
19
+ export interface CommandContext {
20
+ cwd: string;
21
+ ui: CommandUIContext;
22
+ hasUI: boolean;
23
+ mode: ExtensionMode;
24
+ isProjectTrusted(): boolean;
25
+ }
26
+
27
+ /** Function signature for a command executor. */
28
+ export type CommandExecutor = (
29
+ ctx: CommandContext,
30
+ getCoordinator: CoordinatorProvider,
31
+ args: string,
32
+ ) => Promise<void>;
33
+
34
+ /** How a command collects arguments from the unified menu. */
35
+ export type CommandArgMode = "none" | "input" | "flags";
36
+
37
+ /** Static metadata and runtime executor for one slash command. */
38
+ export interface CommandDefinition {
39
+ /** Slash command name, e.g. "graphify-build". */
40
+ name: string;
41
+
42
+ /** Menu label, e.g. "Build graph". */
43
+ label: string;
44
+
45
+ /** Description shown in the menu and in Pi's command list. */
46
+ description: string;
47
+
48
+ /** Required Graphify capability. */
49
+ feature: keyof GraphifyCapabilities;
50
+
51
+ /** Usage string shown when arguments are invalid. */
52
+ usage: string;
53
+
54
+ /** How the menu collects arguments. */
55
+ argMode: CommandArgMode;
56
+
57
+ /** Title for ctx.ui.input when argMode is "input" or "flags". */
58
+ prompt?: string;
59
+
60
+ /** Placeholder for ctx.ui.input. */
61
+ placeholder?: string;
62
+
63
+ /** Execute the command with the given raw argument string. */
64
+ execute: CommandExecutor;
65
+ }
66
+
67
+ /** Narrow an ExtensionCommandContext to the CommandContext shape. */
68
+ export function toCommandContext(ctx: ExtensionCommandContext): CommandContext {
69
+ return {
70
+ cwd: ctx.cwd,
71
+ ui: ctx.ui,
72
+ hasUI: ctx.hasUI,
73
+ mode: ctx.mode as ExtensionMode,
74
+ isProjectTrusted: ctx.isProjectTrusted,
75
+ };
76
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * /graphify-version — show the installed Graphify CLI version.
3
+ */
4
+
5
+ import type { GraphifyCapabilities } from "../backends/types.js";
6
+ import { formatErrorForCommand, formatResultForCommand } from "./format.js";
7
+ import { requireCoordinatorWithCapability } from "./helpers.js";
8
+ import type { CommandDefinition, CommandExecutor } from "./types.js";
9
+
10
+ const FEATURE: keyof GraphifyCapabilities = "version";
11
+
12
+ const executeVersion: CommandExecutor = async (ctx, getCoordinator) => {
13
+ if (!ctx.hasUI) return;
14
+
15
+ const coordinator = requireCoordinatorWithCapability(
16
+ ctx,
17
+ getCoordinator,
18
+ FEATURE,
19
+ "The installed Graphify version does not support version reporting.",
20
+ );
21
+ if (!coordinator) return;
22
+
23
+ try {
24
+ const result = await coordinator.getVersion();
25
+ ctx.ui.notify(formatResultForCommand(result), "info");
26
+ } catch (error) {
27
+ const { message, severity } = formatErrorForCommand(error);
28
+ ctx.ui.notify(message, severity);
29
+ }
30
+ };
31
+
32
+ export const versionCommand: CommandDefinition = {
33
+ name: "graphify-version",
34
+ label: "Graphify version",
35
+ description: "Show the installed Graphify CLI version",
36
+ feature: FEATURE,
37
+ usage: "",
38
+ argMode: "none",
39
+ execute: executeVersion,
40
+ };
@@ -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
- // Phase 2 only supports CLI backend. MCP is a Phase 6 addition.
61
- this.backend = await createCliBackend({
62
- cwd: this.cwd,
63
- timeoutMs: this.config.queryTimeoutMs,
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(
@@ -176,6 +181,8 @@ export class GraphifyCoordinator {
176
181
  }
177
182
  }
178
183
 
184
+ export type CoordinatorProvider = () => GraphifyCoordinator | null;
185
+
179
186
  /** Convenience factory. */
180
187
  export async function createGraphifyCoordinator(
181
188
  cwd: string,
@@ -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
+ }
@@ -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 { CoordinatorProvider, 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 } from "../coordinator.js";
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
- export type CoordinatorProvider = () => import("../coordinator.js").GraphifyCoordinator | null;
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
- export function registerGraphifyTools(pi: ExtensionAPI, getCoordinator: CoordinatorProvider): void {
11
- registerStatusTool(pi, getCoordinator);
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
+ }
@@ -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. Run /graphify-build to build one.${cliNote}`,
46
+ text: `No Graphify graph found. Build one with \`graphify .\`.${cliNote}`,
42
47
  },
43
48
  ],
44
49
  details,