@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.
- package/dist/index.js +49 -4
- package/package.json +2 -2
- package/payload/platform/lib/graph-style/dist/index.d.ts +78 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-style/dist/index.js +296 -0
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +114 -0
- package/payload/platform/lib/graph-style/src/index.ts +307 -0
- package/payload/platform/lib/graph-style/tsconfig.json +9 -0
- package/payload/platform/lib/graph-style/vitest.config.ts +9 -0
- package/payload/platform/neo4j/schema.cypher +50 -0
- package/payload/platform/package-lock.json +318 -0
- package/payload/platform/package.json +3 -2
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/docs/references/visitor-graph.md +5 -4
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +60 -0
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +6 -2
- package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
- package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
- package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +17 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +11 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +12 -4
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +2 -0
- package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +2 -0
- package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
- package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
- package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
- package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
- package/payload/server/public/assets/page-BOtNny_4.js +51 -0
- package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/privacy.html +2 -2
- package/payload/server/server.js +236 -92
- package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
- package/payload/server/public/assets/page-B4oirCvn.js +0 -1
- 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 @@
|
|
|
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":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,eAAe,EACf,SAAS,EACT,aAAa,EACb,WAAW,EACX,MAAM,EACN,MAAM,GAIP,MAAM,UAAU,CAAC;AAwBlB;;;;;GAKG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,KAAK,GAAG,IAAI,KAAK,CAAC,CAAC;IACvB,OAAO,GAAG,EAAE;QACV,KAAK,GAAG,CAAC,KAAK,GAAG,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,KAAK,GAAG,WAAW,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,MAAM,eAAe,GAAG,CAAC,GAAG,CAAC;AAC7B,MAAM,eAAe,GAAG,IAAI,CAAC;AAS7B,MAAM,UAAU,WAAW,CACzB,KAAqB,EACrB,KAAqB,EACrB,IAAsD;IAEtD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEhC,oEAAoE;IACpE,oEAAoE;IACpE,6DAA6D;IAC7D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEjF,+DAA+D;IAC/D,0CAA0C;IAC1C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3B,MAAM,QAAQ,GAAc,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5C,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,GAAG,EAAE;QAC3B,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,GAAG,EAAE;KAC5B,CAAC,CAAC,CAAC;IAEJ,MAAM,QAAQ,GAAc,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACjD,MAAM,EAAE,CAAC,CAAC,IAAI;QACd,MAAM,EAAE,CAAC,CAAC,EAAE;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC,CAAC;IAEJ,MAAM,UAAU,GAAiC,eAAe,CAAC,QAAQ,CAAC;SACvE,YAAY,CAAC,IAAI,CAAC;SAClB,KAAK,CACJ,MAAM,EACN,SAAS,CAAmB,QAAQ,CAAC;SAClC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACf,QAAQ,CAAC,aAAa,CAAC,CAC3B;SACA,KAAK,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;SAC1D,KAAK,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;SACpC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;SAChD,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;SAChD,IAAI,EAAE,CAAC;IAEV,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAE5B,MAAM,QAAQ,GAAoB,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;QACZ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE;KACb,CAAC,CAAC,CAAC;IAEJ,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACxE,CAAC"}
|
|
@@ -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
|
+
}
|