@rubytech/create-maxy-code 0.1.123 → 0.1.126

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.
Files changed (65) hide show
  1. package/dist/index.js +49 -4
  2. package/package.json +2 -2
  3. package/payload/platform/lib/graph-style/dist/index.d.ts +78 -0
  4. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/graph-style/dist/index.js +296 -0
  6. package/payload/platform/lib/graph-style/dist/index.js.map +1 -0
  7. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +114 -0
  8. package/payload/platform/lib/graph-style/src/index.ts +307 -0
  9. package/payload/platform/lib/graph-style/tsconfig.json +9 -0
  10. package/payload/platform/lib/graph-style/vitest.config.ts +9 -0
  11. package/payload/platform/neo4j/schema.cypher +50 -0
  12. package/payload/platform/package-lock.json +318 -0
  13. package/payload/platform/package.json +3 -2
  14. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  15. package/payload/platform/plugins/docs/references/visitor-graph.md +5 -4
  16. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +60 -0
  17. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +6 -2
  18. package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
  19. package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
  20. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
  21. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
  22. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
  23. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
  24. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
  25. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
  26. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
  27. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
  28. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
  29. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
  30. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
  31. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
  32. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
  33. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
  34. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
  35. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
  36. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
  37. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
  38. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
  39. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
  40. package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
  41. package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
  42. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +17 -0
  44. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +11 -2
  47. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  48. package/payload/platform/plugins/memory/references/schema-base.md +12 -4
  49. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +2 -0
  50. package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
  51. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +2 -0
  52. package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
  53. package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
  54. package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
  55. package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
  56. package/payload/server/public/assets/page-BOtNny_4.js +51 -0
  57. package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
  58. package/payload/server/public/data.html +3 -3
  59. package/payload/server/public/graph.html +3 -3
  60. package/payload/server/public/index.html +4 -4
  61. package/payload/server/public/privacy.html +2 -2
  62. package/payload/server/server.js +236 -92
  63. package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
  64. package/payload/server/public/assets/page-B4oirCvn.js +0 -1
  65. package/payload/server/public/assets/page-FmJ7PIYx.js +0 -51
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MCP server entrypoint for the graph-viewer plugin. Registers one tool,
3
+ * `graph-render`, on the standard stdio transport. Account-scoping is
4
+ * enforced inside the tool handler against the caller-supplied accountId.
5
+ */
6
+ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
7
+ initStderrTee("graph-viewer");
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
11
+ import { z } from "zod";
12
+ import { graphRender } from "./tools/graph-render.js";
13
+ import { closeDriver } from "./lib/neo4j.js";
14
+ const server = new McpServer({
15
+ name: "maxy-graph-viewer",
16
+ version: "0.1.0",
17
+ });
18
+ eagerTool(server, "graph-render", "Render an inline PNG of an account-scoped Neo4j subgraph rooted at a given nodeId, walking up to hopDepth hops (0-3). Returns the image as an MCP image content block, using the same colour palette and short-label strings as the live /graph page. Node positions differ from /graph (the renderer is d3-force, not vis-network). Subgraphs above 200 nodes are capped and return a truncated={rendered,total} envelope. Trashed nodes are excluded. Use when the operator asks to see what a subgraph looks like and you want to avoid making them switch to the admin UI on a different device.", {
19
+ accountId: z.string().min(1).describe("Operator account id. The root nodeId must belong to this account or the tool refuses with reason=scope."),
20
+ nodeId: z.string().min(1).describe("elementId of the root node, as returned by memory-search or other graph tools."),
21
+ hopDepth: z.number().int().min(0).max(3).describe("Number of hops to walk from the root (0 = just the root). Max 3."),
22
+ }, async (args) => {
23
+ const result = await graphRender(args);
24
+ if (!result.ok) {
25
+ return {
26
+ isError: true,
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: JSON.stringify({ ok: false, reason: result.reason, message: result.message }, null, 2),
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ const envelope = {
36
+ ok: true,
37
+ width: result.width,
38
+ height: result.height,
39
+ nodeCount: result.nodeCount,
40
+ edgeCount: result.edgeCount,
41
+ };
42
+ if (result.truncated)
43
+ envelope.truncated = result.truncated;
44
+ return {
45
+ content: [
46
+ {
47
+ type: "image",
48
+ data: result.pngBase64,
49
+ mimeType: "image/png",
50
+ },
51
+ {
52
+ type: "text",
53
+ text: JSON.stringify(envelope, null, 2),
54
+ },
55
+ ],
56
+ };
57
+ });
58
+ async function main() {
59
+ const transport = new StdioServerTransport();
60
+ await server.connect(transport);
61
+ }
62
+ main().catch((err) => {
63
+ console.error("[graph-viewer] fatal:", err);
64
+ process.exit(1);
65
+ });
66
+ process.on("SIGINT", async () => {
67
+ await closeDriver();
68
+ process.exit(0);
69
+ });
70
+ process.on("SIGTERM", async () => {
71
+ await closeDriver();
72
+ process.exit(0);
73
+ });
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { type Driver, type Session } from "neo4j-driver";
2
+ export declare function getDriver(): Driver;
3
+ export declare function getSession(): Session;
4
+ export declare function closeDriver(): Promise<void>;
5
+ //# sourceMappingURL=neo4j.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neo4j.d.ts","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAwBhE,wBAAgB,SAAS,IAAI,MAAM,CAgBlC;AAED,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjD"}
@@ -0,0 +1,43 @@
1
+ import neo4j from "neo4j-driver";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ let driver = null;
5
+ function readPassword() {
6
+ if (process.env.NEO4J_PASSWORD)
7
+ return process.env.NEO4J_PASSWORD;
8
+ // Five segments up from the compiled file
9
+ // (`platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js`) reaches
10
+ // `platform/`, where the installer writes `config/.neo4j-password`.
11
+ const passwordFile = resolve(process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../../.."), "config/.neo4j-password");
12
+ try {
13
+ return readFileSync(passwordFile, "utf-8").trim();
14
+ }
15
+ catch {
16
+ throw new Error(`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`);
17
+ }
18
+ }
19
+ export function getDriver() {
20
+ if (!driver) {
21
+ // Refuse to default the URI — silent localhost:7687 has historically
22
+ // crossed brand boundaries on shared devices.
23
+ const uri = process.env.NEO4J_URI;
24
+ if (!uri) {
25
+ throw new Error("[graph-viewer] NEO4J_URI unset — refusing to default to bolt://localhost:7687");
26
+ }
27
+ const user = process.env.NEO4J_USER ?? "neo4j";
28
+ const password = readPassword();
29
+ console.error(`[graph-viewer] resolved neo4j_uri=${uri}`);
30
+ driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
31
+ }
32
+ return driver;
33
+ }
34
+ export function getSession() {
35
+ return getDriver().session();
36
+ }
37
+ export async function closeDriver() {
38
+ if (driver) {
39
+ await driver.close();
40
+ driver = null;
41
+ }
42
+ }
43
+ //# sourceMappingURL=neo4j.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neo4j.js","sourceRoot":"","sources":["../../src/lib/neo4j.ts"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,SAAS,YAAY;IACnB,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAClE,0CAA0C;IAC1C,kEAAkE;IAClE,oEAAoE;IACpE,MAAM,YAAY,GAAG,OAAO,CAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAC3E,wBAAwB,CACzB,CAAC;IACF,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yCAAyC,YAAY,gCAAgC,CACtF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,qEAAqE;QACrE,8CAA8C;QAC9C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,+EAA+E,CAChF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC;QAC/C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,qCAAqC,GAAG,EAAE,CAAC,CAAC;QAC1D,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Canvas drawing for the graph-render tool. Takes a laid-out graph and
3
+ * produces a PNG buffer.
4
+ *
5
+ * Visual contract:
6
+ * - Background matches the /graph page's surface colour (#FAFAF8).
7
+ * - Node fill comes from platform/lib/graph-style's resolveNodeColour —
8
+ * identical hue to the live page for the same labels.
9
+ * - Node label is pickShortLabel from the same shared lib, rendered
10
+ * below the node circle in a sans-serif fallback. Exact webfont match
11
+ * is filed as a follow-up.
12
+ * - Edges are thin grey lines with a small arrowhead. Edge type labels
13
+ * are intentionally omitted — they crowd the canvas at high node
14
+ * counts and the operator can drill into the live page if they need
15
+ * them.
16
+ */
17
+ import type { LayoutResult, LayoutNodeOut } from "./layout.js";
18
+ export interface DrawNode extends LayoutNodeOut {
19
+ properties: Record<string, unknown>;
20
+ }
21
+ export interface DrawInput {
22
+ layout: LayoutResult;
23
+ nodeProperties: Record<string, Record<string, unknown>>;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export declare function drawGraph(input: DrawInput): Buffer;
28
+ //# sourceMappingURL=draw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw.d.ts","sourceRoot":"","sources":["../../src/render/draw.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAW/D,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,YAAY,CAAC;IAGrB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAiDlD"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Canvas drawing for the graph-render tool. Takes a laid-out graph and
3
+ * produces a PNG buffer.
4
+ *
5
+ * Visual contract:
6
+ * - Background matches the /graph page's surface colour (#FAFAF8).
7
+ * - Node fill comes from platform/lib/graph-style's resolveNodeColour —
8
+ * identical hue to the live page for the same labels.
9
+ * - Node label is pickShortLabel from the same shared lib, rendered
10
+ * below the node circle in a sans-serif fallback. Exact webfont match
11
+ * is filed as a follow-up.
12
+ * - Edges are thin grey lines with a small arrowhead. Edge type labels
13
+ * are intentionally omitted — they crowd the canvas at high node
14
+ * counts and the operator can drill into the live page if they need
15
+ * them.
16
+ */
17
+ import { createCanvas } from "@napi-rs/canvas";
18
+ import { resolveNodeColour, pickShortLabel, } from "../../../../../lib/graph-style/dist/index.js";
19
+ const BACKGROUND = "#FAFAF8";
20
+ const EDGE_COLOUR = "#C9C7C2";
21
+ const NODE_RADIUS = 12;
22
+ const NODE_STROKE = "#FFFFFF";
23
+ const NODE_STROKE_WIDTH = 2;
24
+ const LABEL_COLOUR = "#1A1A1A";
25
+ const LABEL_FONT = "12px sans-serif";
26
+ const LABEL_PADDING_Y = 4;
27
+ export function drawGraph(input) {
28
+ const { layout, nodeProperties, width, height } = input;
29
+ const canvas = createCanvas(width, height);
30
+ const ctx = canvas.getContext("2d");
31
+ ctx.fillStyle = BACKGROUND;
32
+ ctx.fillRect(0, 0, width, height);
33
+ const nodeIndex = new Map();
34
+ for (const n of layout.nodes)
35
+ nodeIndex.set(n.id, n);
36
+ // Edges first, so node circles cover the endpoints.
37
+ ctx.strokeStyle = EDGE_COLOUR;
38
+ ctx.lineWidth = 1;
39
+ for (const e of layout.edges) {
40
+ const from = nodeIndex.get(e.from);
41
+ const to = nodeIndex.get(e.to);
42
+ if (!from || !to)
43
+ continue;
44
+ ctx.beginPath();
45
+ ctx.moveTo(from.x, from.y);
46
+ ctx.lineTo(to.x, to.y);
47
+ ctx.stroke();
48
+ }
49
+ // Nodes: filled circle, white stroke, label below.
50
+ ctx.font = LABEL_FONT;
51
+ ctx.textAlign = "center";
52
+ ctx.textBaseline = "top";
53
+ for (const n of layout.nodes) {
54
+ const { colour } = resolveNodeColour(n.labels);
55
+ ctx.fillStyle = colour;
56
+ ctx.beginPath();
57
+ ctx.arc(n.x, n.y, NODE_RADIUS, 0, Math.PI * 2);
58
+ ctx.fill();
59
+ ctx.lineWidth = NODE_STROKE_WIDTH;
60
+ ctx.strokeStyle = NODE_STROKE;
61
+ ctx.stroke();
62
+ const props = nodeProperties[n.id] ?? {};
63
+ const labelNode = {
64
+ id: n.id,
65
+ labels: n.labels,
66
+ properties: props,
67
+ };
68
+ ctx.fillStyle = LABEL_COLOUR;
69
+ ctx.fillText(pickShortLabel(labelNode), n.x, n.y + NODE_RADIUS + LABEL_PADDING_Y);
70
+ }
71
+ return canvas.toBuffer("image/png");
72
+ }
73
+ //# sourceMappingURL=draw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw.js","sourceRoot":"","sources":["../../src/render/draw.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Deterministic force-directed layout for the graph-render tool.
3
+ *
4
+ * Uses d3-force with a seeded LCG so the same input graph always produces
5
+ * the same node positions. Layout fidelity to the live /graph page (which
6
+ * runs vis-network's Barnes-Hut port in the browser) is NOT a goal —
7
+ * d3-force's many-body repulsion plus link spring forces give a coherent
8
+ * force-directed layout, which is what the operator needs to read the
9
+ * subgraph. Pixel-equivalence with /graph is filed as a follow-up task.
10
+ *
11
+ * Convergence: a fixed iteration count rather than a kinetic-energy
12
+ * threshold. The kinetic-energy approach is sensitive to graph topology
13
+ * (sparse graphs converge in 50 iterations, dense ones never stabilise
14
+ * cleanly), so 300 iterations is the simple, deterministic choice. The
15
+ * caller logs whether iteration completed successfully.
16
+ */
17
+ export interface LayoutNodeIn {
18
+ id: string;
19
+ labels: string[];
20
+ }
21
+ export interface LayoutEdgeIn {
22
+ from: string;
23
+ to: string;
24
+ type: string;
25
+ }
26
+ export interface LayoutNodeOut extends LayoutNodeIn {
27
+ x: number;
28
+ y: number;
29
+ }
30
+ export interface LayoutResult {
31
+ nodes: LayoutNodeOut[];
32
+ edges: LayoutEdgeIn[];
33
+ iterations: number;
34
+ }
35
+ export declare function layoutGraph(nodes: LayoutNodeIn[], edges: LayoutEdgeIn[], opts: {
36
+ width: number;
37
+ height: number;
38
+ seed?: number;
39
+ }): LayoutResult;
40
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAcH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AA4BD,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EAAE,EACrB,KAAK,EAAE,YAAY,EAAE,EACrB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,YAAY,CAmDd"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Deterministic force-directed layout for the graph-render tool.
3
+ *
4
+ * Uses d3-force with a seeded LCG so the same input graph always produces
5
+ * the same node positions. Layout fidelity to the live /graph page (which
6
+ * runs vis-network's Barnes-Hut port in the browser) is NOT a goal —
7
+ * d3-force's many-body repulsion plus link spring forces give a coherent
8
+ * force-directed layout, which is what the operator needs to read the
9
+ * subgraph. Pixel-equivalence with /graph is filed as a follow-up task.
10
+ *
11
+ * Convergence: a fixed iteration count rather than a kinetic-energy
12
+ * threshold. The kinetic-energy approach is sensitive to graph topology
13
+ * (sparse graphs converge in 50 iterations, dense ones never stabilise
14
+ * cleanly), so 300 iterations is the simple, deterministic choice. The
15
+ * caller logs whether iteration completed successfully.
16
+ */
17
+ import { forceSimulation, forceLink, forceManyBody, forceCenter, forceX, forceY, } from "d3-force";
18
+ /**
19
+ * Seeded LCG (linear-congruential generator). Used as d3-force's
20
+ * randomSource so layout is deterministic across runs. Values are the
21
+ * Numerical Recipes constants — fast, adequate for jittering initial
22
+ * positions, not cryptographically interesting.
23
+ */
24
+ function seededRandom(seed) {
25
+ let state = seed >>> 0;
26
+ return () => {
27
+ state = (state * 1664525 + 1013904223) >>> 0;
28
+ return state / 0x100000000;
29
+ };
30
+ }
31
+ const ITERATIONS = 300;
32
+ const LINK_DISTANCE = 90;
33
+ const CHARGE_STRENGTH = -240;
34
+ const CENTER_STRENGTH = 0.05;
35
+ export function layoutGraph(nodes, edges, opts) {
36
+ const seed = opts.seed ?? 1;
37
+ const rand = seededRandom(seed);
38
+ // Drop edges whose endpoints aren't in the node set — d3-force will
39
+ // throw on a missing endpoint, and the upstream Cypher might return
40
+ // edges whose far side was capped off by the truncation cut.
41
+ const nodeIds = new Set(nodes.map((n) => n.id));
42
+ const validEdges = edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
43
+ // Initial positions: jittered around the centre. d3-force will
44
+ // displace them over the iteration count.
45
+ const cx = opts.width / 2;
46
+ const cy = opts.height / 2;
47
+ const simNodes = nodes.map((n) => ({
48
+ id: n.id,
49
+ labels: n.labels,
50
+ x: cx + (rand() - 0.5) * 40,
51
+ y: cy + (rand() - 0.5) * 40,
52
+ }));
53
+ const simLinks = validEdges.map((e) => ({
54
+ source: e.from,
55
+ target: e.to,
56
+ type: e.type,
57
+ }));
58
+ const simulation = forceSimulation(simNodes)
59
+ .randomSource(rand)
60
+ .force("link", forceLink(simLinks)
61
+ .id((d) => d.id)
62
+ .distance(LINK_DISTANCE))
63
+ .force("charge", forceManyBody().strength(CHARGE_STRENGTH))
64
+ .force("center", forceCenter(cx, cy))
65
+ .force("x", forceX(cx).strength(CENTER_STRENGTH))
66
+ .force("y", forceY(cy).strength(CENTER_STRENGTH))
67
+ .stop();
68
+ simulation.tick(ITERATIONS);
69
+ const outNodes = simNodes.map((n) => ({
70
+ id: n.id,
71
+ labels: n.labels,
72
+ x: n.x ?? cx,
73
+ y: n.y ?? cy,
74
+ }));
75
+ return { nodes: outNodes, edges: validEdges, iterations: ITERATIONS };
76
+ }
77
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * graph-render — produce an inline PNG of an account-scoped Neo4j
3
+ * subgraph rooted at a given nodeId, walking up to `hopDepth` hops.
4
+ *
5
+ * Surface contract:
6
+ * - The root node must exist and belong to the caller's account.
7
+ * A missing or foreign-account root returns a structured refusal,
8
+ * never throws.
9
+ * - The expansion is bounded by both `hopDepth` (max 3 to keep the
10
+ * image legible) and a hard 200-node cap. Subgraphs that exceed
11
+ * the cap render the first 200 nodes and carry a `truncated`
12
+ * envelope so the agent can tell the operator.
13
+ * - `:Trashed` nodes are excluded everywhere — matches memory-search.
14
+ * - One observability line per invocation, prefixed `[graph-render]`.
15
+ *
16
+ * The Cypher hopDepth is INLINED into the query string after a strict
17
+ * validate (`Number.isInteger && 0 <= n <= 3`) because Neo4j does not
18
+ * parameterise variable-length path bounds. Anything outside that range
19
+ * fails fast at the param-validation layer, well before string assembly.
20
+ */
21
+ export interface GraphRenderParams {
22
+ accountId: string;
23
+ nodeId: string;
24
+ hopDepth: number;
25
+ }
26
+ export interface GraphRenderRefusal {
27
+ ok: false;
28
+ reason: "scope" | "empty" | "cap-exceeded" | "draw-error" | "invalid-input";
29
+ message: string;
30
+ }
31
+ export interface GraphRenderSuccess {
32
+ ok: true;
33
+ pngBase64: string;
34
+ width: number;
35
+ height: number;
36
+ nodeCount: number;
37
+ edgeCount: number;
38
+ truncated?: {
39
+ rendered: number;
40
+ total: number;
41
+ };
42
+ }
43
+ export type GraphRenderResult = GraphRenderSuccess | GraphRenderRefusal;
44
+ export declare function graphRender(params: GraphRenderParams): Promise<GraphRenderResult>;
45
+ //# sourceMappingURL=graph-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-render.d.ts","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAWH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,cAAc,GAAG,YAAY,GAAG,eAAe,CAAC;IAC5E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAMxE,wBAAsB,WAAW,CAC/B,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAkK5B"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * graph-render — produce an inline PNG of an account-scoped Neo4j
3
+ * subgraph rooted at a given nodeId, walking up to `hopDepth` hops.
4
+ *
5
+ * Surface contract:
6
+ * - The root node must exist and belong to the caller's account.
7
+ * A missing or foreign-account root returns a structured refusal,
8
+ * never throws.
9
+ * - The expansion is bounded by both `hopDepth` (max 3 to keep the
10
+ * image legible) and a hard 200-node cap. Subgraphs that exceed
11
+ * the cap render the first 200 nodes and carry a `truncated`
12
+ * envelope so the agent can tell the operator.
13
+ * - `:Trashed` nodes are excluded everywhere — matches memory-search.
14
+ * - One observability line per invocation, prefixed `[graph-render]`.
15
+ *
16
+ * The Cypher hopDepth is INLINED into the query string after a strict
17
+ * validate (`Number.isInteger && 0 <= n <= 3`) because Neo4j does not
18
+ * parameterise variable-length path bounds. Anything outside that range
19
+ * fails fast at the param-validation layer, well before string assembly.
20
+ */
21
+ import { getSession } from "../lib/neo4j.js";
22
+ import { layoutGraph } from "../render/layout.js";
23
+ import { drawGraph } from "../render/draw.js";
24
+ const NODE_CAP = 200;
25
+ const MAX_HOP_DEPTH = 3;
26
+ const IMAGE_WIDTH = 1200;
27
+ const IMAGE_HEIGHT = 900;
28
+ function logLine(line) {
29
+ process.stderr.write(`${line}\n`);
30
+ }
31
+ export async function graphRender(params) {
32
+ const { accountId, nodeId } = params;
33
+ const hopDepth = params.hopDepth;
34
+ if (!Number.isInteger(hopDepth) || hopDepth < 0 || hopDepth > MAX_HOP_DEPTH) {
35
+ return {
36
+ ok: false,
37
+ reason: "invalid-input",
38
+ message: `hopDepth must be an integer between 0 and ${MAX_HOP_DEPTH} (got ${String(hopDepth)})`,
39
+ };
40
+ }
41
+ const t0 = Date.now();
42
+ const session = getSession();
43
+ try {
44
+ // Step 1 — confirm root exists and belongs to this account. The
45
+ // explicit two-step check (rather than relying on the expand to
46
+ // return zero rows) is what lets us return reason="scope" vs
47
+ // reason="empty" with a meaningful message.
48
+ const rootCheck = await session.run(`MATCH (root)
49
+ WHERE elementId(root) = $nodeId
50
+ RETURN root.accountId AS rootAccountId, labels(root) AS rootLabels`, { nodeId });
51
+ if (rootCheck.records.length === 0) {
52
+ logLine(`[graph-render] reject account=${accountId} reason=scope nodeId=${nodeId} cause=not-found`);
53
+ return {
54
+ ok: false,
55
+ reason: "scope",
56
+ message: `No node with id ${nodeId} is visible to this account.`,
57
+ };
58
+ }
59
+ const rootAccountId = rootCheck.records[0].get("rootAccountId");
60
+ if (rootAccountId !== accountId) {
61
+ logLine(`[graph-render] reject account=${accountId} reason=scope nodeId=${nodeId} cause=foreign-account`);
62
+ return {
63
+ ok: false,
64
+ reason: "scope",
65
+ message: `Node ${nodeId} is not visible to this account.`,
66
+ };
67
+ }
68
+ logLine(`[graph-render] start account=${accountId} nodeId=${nodeId} hopDepth=${hopDepth}`);
69
+ // Step 2 — expand. Variable-length path bound is inlined after the
70
+ // hopDepth validation above. Account filter + trash filter applied to
71
+ // every node on every path. RELATIONSHIPS only between matched nodes.
72
+ const expandQuery = `
73
+ MATCH (root) WHERE elementId(root) = $nodeId
74
+ MATCH path = (root)-[*0..${hopDepth}]-(n)
75
+ WHERE n.accountId = $accountId AND NOT n:Trashed
76
+ WITH collect(DISTINCT n) AS nodes
77
+ WITH nodes[0..$cap] AS capped, size(nodes) AS totalCount
78
+ UNWIND capped AS node
79
+ WITH collect({ id: elementId(node), labels: labels(node), properties: properties(node) }) AS nodeRows,
80
+ [n IN capped | elementId(n)] AS capIds, totalCount
81
+ OPTIONAL MATCH (a)-[r]->(b)
82
+ WHERE elementId(a) IN capIds AND elementId(b) IN capIds
83
+ RETURN nodeRows,
84
+ collect(DISTINCT { from: elementId(startNode(r)), to: elementId(endNode(r)), type: type(r) }) AS edgeRows,
85
+ totalCount
86
+ `;
87
+ const result = await session.run(expandQuery, {
88
+ nodeId,
89
+ accountId,
90
+ cap: NODE_CAP,
91
+ });
92
+ if (result.records.length === 0) {
93
+ logLine(`[graph-render] reject account=${accountId} reason=empty nodeId=${nodeId}`);
94
+ return {
95
+ ok: false,
96
+ reason: "empty",
97
+ message: "Root node found but its neighbourhood is empty after trash filter.",
98
+ };
99
+ }
100
+ const record = result.records[0];
101
+ const nodeRows = record.get("nodeRows");
102
+ const edgeRowsRaw = record.get("edgeRows");
103
+ const totalCountRaw = record.get("totalCount");
104
+ const totalCount = typeof totalCountRaw === "number"
105
+ ? totalCountRaw
106
+ : (totalCountRaw?.toNumber?.() ?? nodeRows.length);
107
+ // OPTIONAL MATCH on no rels produces one {from:null,to:null,type:null}
108
+ // row from collect; strip those.
109
+ const edges = edgeRowsRaw
110
+ .filter((e) => e.from && e.to && e.type)
111
+ .map((e) => ({ from: e.from, to: e.to, type: e.type }));
112
+ if (nodeRows.length === 0) {
113
+ logLine(`[graph-render] reject account=${accountId} reason=empty nodeId=${nodeId}`);
114
+ return {
115
+ ok: false,
116
+ reason: "empty",
117
+ message: "Subgraph contained no nodes after trash filter.",
118
+ };
119
+ }
120
+ const layoutNodes = nodeRows.map((n) => ({
121
+ id: n.id,
122
+ labels: n.labels,
123
+ }));
124
+ const nodeProperties = {};
125
+ for (const n of nodeRows)
126
+ nodeProperties[n.id] = n.properties;
127
+ const layout = layoutGraph(layoutNodes, edges, {
128
+ width: IMAGE_WIDTH,
129
+ height: IMAGE_HEIGHT,
130
+ });
131
+ let pngBuffer;
132
+ try {
133
+ pngBuffer = drawGraph({
134
+ layout,
135
+ nodeProperties,
136
+ width: IMAGE_WIDTH,
137
+ height: IMAGE_HEIGHT,
138
+ });
139
+ }
140
+ catch (err) {
141
+ const message = err instanceof Error ? err.message : String(err);
142
+ logLine(`[graph-render] reject account=${accountId} reason=draw-error nodeId=${nodeId} cause=${message}`);
143
+ return {
144
+ ok: false,
145
+ reason: "draw-error",
146
+ message: `Canvas draw failed: ${message}`,
147
+ };
148
+ }
149
+ const truncated = totalCount > nodeRows.length
150
+ ? { rendered: nodeRows.length, total: totalCount }
151
+ : undefined;
152
+ const ms = Date.now() - t0;
153
+ logLine(`[graph-render] ok account=${accountId} ms=${ms} bytes=${pngBuffer.length} ` +
154
+ `nodeCount=${nodeRows.length} edgeCount=${edges.length} ` +
155
+ `truncated=${truncated ? "true" : "false"} iterations=${layout.iterations}`);
156
+ return {
157
+ ok: true,
158
+ pngBase64: pngBuffer.toString("base64"),
159
+ width: IMAGE_WIDTH,
160
+ height: IMAGE_HEIGHT,
161
+ nodeCount: nodeRows.length,
162
+ edgeCount: edges.length,
163
+ ...(truncated ? { truncated } : {}),
164
+ };
165
+ }
166
+ finally {
167
+ await session.close();
168
+ }
169
+ }
170
+ //# sourceMappingURL=graph-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-render.js","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@maxy/graph-viewer",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "test": "vitest run"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.12.1",
14
+ "@napi-rs/canvas": "^0.1.56",
15
+ "d3-force": "^3.0.0",
16
+ "neo4j-driver": "^5.28.1",
17
+ "zod": "^3.23.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/d3-force": "^3.0.10",
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.7.0",
23
+ "vitest": "^4.1.2"
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ environment: "node",
7
+ },
8
+ });