@rubytech/create-maxy-code 0.1.125 → 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 (53) hide show
  1. package/dist/index.js +11 -1
  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/package-lock.json +318 -0
  12. package/payload/platform/package.json +3 -2
  13. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  14. package/payload/platform/plugins/docs/references/visitor-graph.md +4 -3
  15. package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
  16. package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
  17. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
  18. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
  19. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
  20. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
  21. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
  22. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
  23. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
  24. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
  25. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
  26. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
  27. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
  28. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
  29. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
  30. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
  31. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
  32. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
  33. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
  34. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
  35. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
  36. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
  37. package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
  38. package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
  39. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
  40. package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
  41. package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
  42. package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
  43. package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
  44. package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
  45. package/payload/server/public/assets/page-BOtNny_4.js +51 -0
  46. package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
  47. package/payload/server/public/data.html +3 -3
  48. package/payload/server/public/graph.html +3 -3
  49. package/payload/server/public/index.html +4 -4
  50. package/payload/server/server.js +236 -92
  51. package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
  52. package/payload/server/public/assets/page-B4oirCvn.js +0 -1
  53. package/payload/server/public/assets/page-FmJ7PIYx.js +0 -51
@@ -6,8 +6,8 @@
6
6
  "services/*"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -19,6 +19,7 @@
19
19
  "build:workflows": "tsc -p plugins/workflows/mcp/tsconfig.json",
20
20
  "build:stubs": "tsc -p plugins/scheduling/mcp/tsconfig.json",
21
21
  "build:replicate": "tsc -p plugins/replicate/mcp/tsconfig.json",
22
+ "build:graph-viewer": "tsc -p plugins/graph-viewer/mcp/tsconfig.json",
22
23
  "build:claude-session-manager": "tsc -p services/claude-session-manager/tsconfig.json"
23
24
  }
24
25
  }
@@ -44,6 +44,11 @@
44
44
  "source": "./email",
45
45
  "version": "0.1.0"
46
46
  },
47
+ {
48
+ "name": "graph-viewer",
49
+ "source": "./graph-viewer",
50
+ "version": "0.1.0"
51
+ },
47
52
  {
48
53
  "name": "linkedin-extension",
49
54
  "source": "./linkedin-extension",
@@ -50,10 +50,11 @@ All under the `real-agent-buyers` plugin, admin-side only:
50
50
  | `visitor-recent-by-person` | Recent sessions for a known `:Person` (morning round, 1:1 prep). |
51
51
  | `visitor-recent-by-page` | Recent visitors of a given listing slug or URL. |
52
52
  | `visitor-engagement-score` | Engagement-ranked `:Person` list (nurture queue). |
53
- | `visitor-recommendation-ctr` | Graph-backed CTR over a window. Replaces the Task 336 log-scan after the 14-day parity cutover. |
53
+ | `visitor-recommendation-ctr` | Graph-backed CTR over a window, joined from `:Recommendation` and `:Click` nodes. |
54
54
  | `visitor-session-detail` | Full event timeline for one `:Session`. |
55
55
  | `visitor-event-ingest` | Admin companion to `POST /v/event` for test harness work. |
56
- | `visitor-backfill-from-logs` | One-shot importer: parses `[property-recommended]` and `[property-card-click]` log lines, computes a parity check, populates the graph. |
56
+ | `visitor-backfill-from-logs` | One-shot importer: parses `[property-recommended]` and `[property-card-click]` log lines into the graph; used to recover late-arriving sessions and for ad-hoc forensics. |
57
+ | `mint-visitor-token` | Mints a signed token bound to a `:Person` for outbound URLs in `morning-round`, `lead-nurturing`, `vendor-updates`. Returns `{ token, expiryMs }`; the agent appends `&v=<token>` to `/listings/<slug>/click?session=<sk>`. Same secret file as the UI server; both processes share it via the wx-create pattern. (Task 362) |
57
58
 
58
59
  ## Privacy
59
60
 
@@ -70,7 +71,7 @@ Quick checks the operator can run after deployment:
70
71
  1. Load a published listing page; grep `[visitor-event] type=pageview` in `server.log` within 1s.
71
72
  2. Scroll past 50%; grep `[visitor-event] type=scroll depth=50`.
72
73
  3. Click an element marked `data-track="floorplan"`; grep `[visitor-event] type=click label=floorplan`.
73
- 4. Run `visitor-backfill-from-logs` over the existing Task 336 log window; the response includes `parityCheck=ok` when graph CTR matches log-scan CTR exactly.
74
+ 4. Run `visitor-backfill-from-logs` over a log window where live writes were lost (process restart, etc.); the response reports `recWritten` and `clickWritten` counts. Subsequent runs over the same window are idempotent for `:Recommendation` and append-only for `:Click`.
74
75
 
75
76
  ## Failure signals
76
77
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "graph-viewer",
3
+ "description": "Inline Neo4j subgraph renderer. Provides graph-render — a Node-side PNG renderer that walks N hops from a starting node, applies the same palette and display strings the live /graph page uses, and returns the image as an MCP image content block. Removes the device-switch otherwise required for the agent to show the operator what a subgraph looks like. Account-scoped: the tool refuses to render nodes outside the caller's accountId. Layout is d3-force with a deterministic seed; drawing is @napi-rs/canvas with a system-font fallback for labels. The fidelity contract is palette and display-string parity — node positions will differ from the live page's vis-network layout. Ships one skill: render-graph.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ }
8
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: graph-viewer
3
+ mcp:
4
+ command: node
5
+ args:
6
+ - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
7
+ - ${PLATFORM_ROOT}/plugins/graph-viewer/mcp/dist/index.js
8
+ env:
9
+ MCP_SPAWN_TEE_NAME: graph-viewer
10
+ LOG_DIR: ${LOG_DIR}
11
+ PLATFORM_ROOT: ${PLATFORM_ROOT}
12
+ ACCOUNT_ID: ${ACCOUNT_ID}
13
+ SESSION_ID: ${SESSION_ID}
14
+ mcp-manifest: skip
15
+ description: "Inline Neo4j subgraph renderer. Provides graph-render — a Node-side PNG renderer that walks N hops from a starting node, applies the same palette and display strings the live /graph page uses, and returns the image as an MCP image content block. Removes the device-switch otherwise required for the agent to show the operator what a subgraph looks like. Account-scoped: the tool refuses to render nodes outside the caller's accountId. Layout is d3-force with a deterministic seed; drawing is @napi-rs/canvas with a system-font fallback for labels. The fidelity contract is palette and display-string parity — node positions will differ from the live page's vis-network layout. Ships one skill: render-graph."
16
+ tools:
17
+ - name: graph-render
18
+ publicAllowlist: false
19
+ adminAllowlist: true
20
+ ---
21
+
22
+ # graph-viewer
23
+
24
+ ## What it does
25
+
26
+ A single MCP tool, `graph-render`, takes a Neo4j node ID and a hop depth and
27
+ returns an inline PNG of the subgraph rooted at that node. The agent
28
+ attaches the PNG to its reply so the operator can see the graph without
29
+ opening the admin UI on a different device.
30
+
31
+ ## Why it exists
32
+
33
+ Before this plugin, the agent had no way to ship the operator a picture
34
+ of a subgraph from inside chat. Mermaid and ASCII substitutes were
35
+ illegible and bore no resemblance to the live `/graph` page. Operators
36
+ otherwise had to leave the chat surface, open the admin UI on whatever
37
+ device hosts it, and re-find the same subgraph by hand.
38
+
39
+ ## Surface boundaries
40
+
41
+ - `graph-render` only accepts a `nodeId + hopDepth` query shape. Arbitrary
42
+ Cypher is not supported and is filed as a follow-up task.
43
+ - Pixel-equivalence with `/graph` is not a goal. The layout engine is
44
+ d3-force, not the live page's vis-network port; node positions will
45
+ differ. The colour palette, label precedence, and short-label strings
46
+ are shared with `/graph` via `platform/lib/graph-style/`.
47
+ - Subgraphs above 200 nodes return a truncation envelope alongside the
48
+ capped render.
49
+ - `:Trashed` nodes are filtered out, matching `memory-search`'s contract.
50
+
51
+ ## Enabling
52
+
53
+ The plugin is auto-discovered from `platform/plugins/`. Each operator
54
+ account must list `graph-viewer` in its `enabledPlugins` (use the admin
55
+ `plugin-toggle-enabled` MCP tool) before `graph-render` appears in the
56
+ agent's tool surface.
@@ -0,0 +1,7 @@
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
+ export {};
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -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"}