whygraph 0.1.1 → 0.1.3

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,8 @@
1
+ import type { Command } from "commander";
2
+ export interface CrossrefResult {
3
+ outputPath: string;
4
+ nodeCount: number;
5
+ decisionCount: number;
6
+ }
7
+ export declare function runCrossref(targetDir: string): CrossrefResult;
8
+ export declare function registerCrossrefCommand(program: Command): void;
@@ -0,0 +1,132 @@
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { isStructuralNode, isDecisionNode } from "../../entity/types.js";
4
+ import { parseEntity } from "../../entity/parser.js";
5
+ import { findWhygraphDir } from "./serve.js";
6
+ // ============================================================
7
+ // Core Logic
8
+ // ============================================================
9
+ export function runCrossref(targetDir) {
10
+ const projectDir = findWhygraphDir(targetDir);
11
+ if (!projectDir) {
12
+ throw new Error(`.whygraph/ not found. Run "npx whygraph init" first.`);
13
+ }
14
+ const graphDir = join(projectDir, ".whygraph", "graph");
15
+ const entities = loadEntities(graphDir);
16
+ const nodes = entities.filter(isStructuralNode);
17
+ const decisions = entities.filter(isDecisionNode);
18
+ const idToNode = new Map(nodes.map((n) => [n.id, n]));
19
+ const markdown = buildMarkdown(nodes, decisions, idToNode);
20
+ const outputPath = join(projectDir, ".whygraph", "CONTEXT.md");
21
+ writeFileSync(outputPath, markdown, "utf-8");
22
+ return { outputPath, nodeCount: nodes.length, decisionCount: decisions.length };
23
+ }
24
+ function loadEntities(graphDir) {
25
+ if (!existsSync(graphDir))
26
+ return [];
27
+ return readdirSync(graphDir)
28
+ .filter((f) => f.endsWith(".md"))
29
+ .flatMap((file) => {
30
+ const content = readFileSync(join(graphDir, file), "utf-8");
31
+ const entity = parseEntity(content);
32
+ return entity !== null ? [entity] : [];
33
+ });
34
+ }
35
+ function buildMarkdown(nodes, decisions, idToNode) {
36
+ const lines = [
37
+ "# Whygraph Context Snapshot",
38
+ "",
39
+ `_Generated: ${new Date().toISOString()}_`,
40
+ "_This file is auto-generated by `npx whygraph crossref`. Do not edit manually._",
41
+ "",
42
+ ];
43
+ // Structural graph section
44
+ lines.push("## Graph Structure", "");
45
+ const apps = nodes.filter((n) => n.label === "App");
46
+ const features = nodes.filter((n) => n.label === "Feature");
47
+ const components = nodes.filter((n) => n.label === "Component");
48
+ for (const app of apps) {
49
+ lines.push(`- **App** \`${app.id}\` — ${app.name}`);
50
+ for (const feature of features.filter((f) => f.parent === app.id)) {
51
+ lines.push(` - **Feature** \`${feature.id}\` — ${feature.name}`);
52
+ for (const comp of components.filter((c) => c.parent === feature.id)) {
53
+ lines.push(` - **Component** \`${comp.id}\` — ${comp.name}`);
54
+ }
55
+ }
56
+ }
57
+ // Orphaned features (no matching app)
58
+ for (const feature of features.filter((f) => !f.parent || !idToNode.has(f.parent))) {
59
+ lines.push(`- **Feature** \`${feature.id}\` — ${feature.name} _(unlinked)_`);
60
+ for (const comp of components.filter((c) => c.parent === feature.id)) {
61
+ lines.push(` - **Component** \`${comp.id}\` — ${comp.name}`);
62
+ }
63
+ }
64
+ lines.push("");
65
+ // Decisions section
66
+ lines.push("## Decisions", "");
67
+ if (decisions.length === 0) {
68
+ lines.push("_No decisions recorded yet._", "");
69
+ }
70
+ else {
71
+ for (const d of decisions) {
72
+ lines.push(`### [${d.id}] ${d.title}`);
73
+ lines.push("");
74
+ const affectsLabels = d.affects
75
+ .map((id) => {
76
+ const node = idToNode.get(id);
77
+ return node ? `\`${id}\` (${node.name})` : `\`${id}\``;
78
+ })
79
+ .join(", ");
80
+ lines.push(`- **Status:** ${d.status} | **Date:** ${d.date} | **Tags:** ${d.tags.join(", ")}`);
81
+ if (d.affects.length > 0) {
82
+ lines.push(`- **Affects:** ${affectsLabels}`);
83
+ }
84
+ /* v8 ignore next 3 */
85
+ if (d.supersedes) {
86
+ lines.push(`- **Supersedes:** \`${d.supersedes}\``);
87
+ }
88
+ lines.push("");
89
+ lines.push(`> **Context:** ${d.context}`);
90
+ lines.push(`> **Decision:** ${d.decision}`);
91
+ lines.push(`> **Tradeoffs:** ${d.tradeoffs}`);
92
+ lines.push(`> **Alternatives:** ${d.alternatives}`);
93
+ lines.push("");
94
+ lines.push("---");
95
+ lines.push("");
96
+ }
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ // ============================================================
101
+ // CLI Wiring
102
+ // ============================================================
103
+ export function registerCrossrefCommand(program) {
104
+ program
105
+ .command("crossref")
106
+ .description("Generate .whygraph/CONTEXT.md — a graph snapshot for use when MCP is unavailable")
107
+ .option("--json", "Output results as JSON")
108
+ .action((opts) => {
109
+ try {
110
+ const result = runCrossref(process.cwd());
111
+ if (opts.json) {
112
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
113
+ }
114
+ else {
115
+ process.stdout.write(`Context snapshot written to ${result.outputPath}\n` +
116
+ ` Nodes: ${result.nodeCount}\n` +
117
+ ` Decisions: ${result.decisionCount}\n`);
118
+ }
119
+ }
120
+ catch (err) {
121
+ /* v8 ignore next 1 */
122
+ const message = err instanceof Error ? err.message : String(err);
123
+ if (opts.json) {
124
+ process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
125
+ }
126
+ else {
127
+ process.stderr.write(`Error: ${message}\n`);
128
+ }
129
+ process.exitCode = 1;
130
+ }
131
+ });
132
+ }
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import { Command } from "commander";
4
4
  const require = createRequire(import.meta.url);
5
5
  const { version } = require("../../package.json");
6
6
  import { registerConfigCommand } from "./commands/config.js";
7
+ import { registerCrossrefCommand } from "./commands/crossref.js";
7
8
  import { registerDownCommand } from "./commands/down.js";
8
9
  import { registerInitCommand } from "./commands/init.js";
9
10
  import { registerIssuesCommand } from "./commands/issues.js";
@@ -21,6 +22,7 @@ program
21
22
  .description("The graph of why — so your agent knows before it touches anything.")
22
23
  .version(version);
23
24
  registerConfigCommand(program);
25
+ registerCrossrefCommand(program);
24
26
  registerDownCommand(program);
25
27
  registerInitCommand(program);
26
28
  registerIssuesCommand(program);
@@ -59,10 +59,23 @@ function generateInstructions(config) {
59
59
  "",
60
60
  `Allowed tags: ${tagList}`,
61
61
  "",
62
- "### MCP Server",
62
+ "### MCP Server (preferred)",
63
63
  "",
64
- `Use the \`whygraph\` MCP server for decision capture tools.`,
65
- "If the server is unreachable, write decision files directly to `.whygraph/graph/`.",
64
+ `Use the \`whygraph\` MCP server for decision capture tools when available.`,
65
+ `- \`whygraph_context\` get existing decisions and structural nodes for a file`,
66
+ `- \`whygraph_create_decision\` — capture a new decision`,
67
+ "",
68
+ "### Direct File Access (fallback)",
69
+ "",
70
+ "If the MCP server is unreachable or MCP is blocked by your environment:",
71
+ "",
72
+ "**For context:** Read `.whygraph/CONTEXT.md` if it exists — this is a pre-built",
73
+ "graph snapshot generated by `npx whygraph crossref`. It lists all structural nodes",
74
+ "and decisions with their `affects` cross-references. If the file is absent, ask",
75
+ "the user to run `npx whygraph crossref` to generate it.",
76
+ "",
77
+ "**For capture:** Write the decision file directly to `.whygraph/graph/`",
78
+ "using the format above. It will be picked up automatically on next server start.",
66
79
  "",
67
80
  "### Server Status",
68
81
  "",
@@ -89,7 +102,7 @@ function upsertMarkedSection(existing, content) {
89
102
  // ============================================================
90
103
  function registerMcpWithClaude(projectDir) {
91
104
  try {
92
- execSync("claude mcp add --scope project whygraph -- whygraph mcp", {
105
+ execSync("claude mcp add --scope project whygraph -- npx whygraph mcp", {
93
106
  cwd: projectDir,
94
107
  stdio: "ignore",
95
108
  });
@@ -106,7 +119,7 @@ function registerMcpWithClaude(projectDir) {
106
119
  }
107
120
  const servers = (mcpJson.mcpServers ?? {});
108
121
  if (!servers.whygraph) {
109
- servers.whygraph = { type: "stdio", command: "whygraph", args: ["mcp"] };
122
+ servers.whygraph = { type: "stdio", command: "npx", args: ["whygraph", "mcp"] };
110
123
  mcpJson.mcpServers = servers;
111
124
  writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf-8");
112
125
  }
@@ -129,7 +142,7 @@ function registerMcpWithCursor(projectDir) {
129
142
  }
130
143
  const servers = (mcp.mcpServers ?? {});
131
144
  if (!servers.whygraph) {
132
- servers.whygraph = { command: "whygraph", args: ["mcp"] };
145
+ servers.whygraph = { command: "npx", args: ["whygraph", "mcp"] };
133
146
  mcp.mcpServers = servers;
134
147
  writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
135
148
  }
@@ -150,7 +163,7 @@ function registerMcpWithCopilot(projectDir) {
150
163
  }
151
164
  const servers = (mcp.servers ?? {});
152
165
  if (!servers.whygraph) {
153
- servers.whygraph = { type: "stdio", command: "whygraph", args: ["mcp"] };
166
+ servers.whygraph = { type: "stdio", command: "npx", args: ["whygraph", "mcp"] };
154
167
  mcp.servers = servers;
155
168
  writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
156
169
  }
@@ -167,16 +180,16 @@ function writeMcpSetupMd(projectDir, environment) {
167
180
  const lines = ["# Whygraph MCP Setup", ""];
168
181
  switch (environment) {
169
182
  case "claude-code":
170
- lines.push("Run the following command in your project root:", "", "```bash", "claude mcp add --scope project whygraph -- whygraph mcp", "```");
183
+ lines.push("Run the following command in your project root:", "", "```bash", "claude mcp add --scope project whygraph -- npx whygraph mcp", "```");
171
184
  break;
172
185
  case "cursor":
173
- lines.push("Add the following to `.cursor/mcp.json` in your project root:", "", "```json", JSON.stringify({ mcpServers: { whygraph: { command: "whygraph", args: ["mcp"] } } }, null, 2), "```");
186
+ lines.push("Add the following to `.cursor/mcp.json` in your project root:", "", "```json", JSON.stringify({ mcpServers: { whygraph: { command: "npx", args: ["whygraph", "mcp"] } } }, null, 2), "```");
174
187
  break;
175
188
  case "copilot":
176
- lines.push("Add the following to `.vscode/mcp.json` in your project root:", "", "```json", JSON.stringify({ servers: { whygraph: { type: "stdio", command: "whygraph", args: ["mcp"] } } }, null, 2), "```");
189
+ lines.push("Add the following to `.vscode/mcp.json` in your project root:", "", "```json", JSON.stringify({ servers: { whygraph: { type: "stdio", command: "npx", args: ["whygraph", "mcp"] } } }, null, 2), "```");
177
190
  break;
178
191
  default:
179
- lines.push("Add the whygraph MCP server to your AI assistant's MCP configuration.", "", "**Command:** `whygraph mcp`", "**Transport:** stdio", "", "Refer to your AI assistant's documentation for how to register MCP servers.");
192
+ lines.push("Add the whygraph MCP server to your AI assistant's MCP configuration.", "", "**Command:** `npx whygraph mcp`", "**Transport:** stdio", "", "Refer to your AI assistant's documentation for how to register MCP servers.");
180
193
  }
181
194
  writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
182
195
  return filePath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whygraph",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "The graph of why. So your agent knows before it touches anything.",
5
5
  "author": "Geovanie Ruiz",
6
6
  "license": "MIT",