@rubytech/create-realagent 1.0.865 → 1.0.867

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 (41) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts +51 -0
  3. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-search/dist/index.js +77 -7
  5. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-search/src/__tests__/bm25-strong-bypass-threshold.test.ts +126 -0
  7. package/payload/platform/lib/graph-search/src/__tests__/vector-threshold.test.ts +170 -0
  8. package/payload/platform/lib/graph-search/src/index.ts +129 -9
  9. package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +2 -0
  10. package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +2 -0
  11. package/payload/platform/templates/agents/admin/IDENTITY.md +2 -1
  12. package/payload/platform/templates/specialists/agents/content-producer.md +17 -3
  13. package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
  14. package/payload/server/chunk-DHSBEMWW.js +11319 -0
  15. package/payload/server/chunk-FHNFKJZN.js +2143 -0
  16. package/payload/server/chunk-ND23BDBM.js +11312 -0
  17. package/payload/server/chunk-TOLLHW7W.js +1155 -0
  18. package/payload/server/chunk-UXLZ5Z3Y.js +667 -0
  19. package/payload/server/client-pool-2IUOSYDF.js +34 -0
  20. package/payload/server/cloudflare-task-tracker-OCFIVXEJ.js +20 -0
  21. package/payload/server/maxy-edge.js +5 -6
  22. package/payload/server/public/assets/{Checkbox-BySsatDO.js → Checkbox-B9hff9s8.js} +1 -1
  23. package/payload/server/public/assets/{admin-CCML_l4E.js → admin-Cpi6L_g7.js} +3 -3
  24. package/payload/server/public/assets/data-Da6iYRW1.js +1 -0
  25. package/payload/server/public/assets/graph-BHq-JYwV.js +1 -0
  26. package/payload/server/public/assets/{useAdminFetch-B3MO55eB.js → graph-labels-ChinGFwI.js} +1 -1
  27. package/payload/server/public/assets/{jsx-runtime-O5ef8xK8.css → jsx-runtime-CVA1ZrPS.css} +1 -1
  28. package/payload/server/public/assets/page-DqPf65sS.js +50 -0
  29. package/payload/server/public/assets/page-OVrxtgOZ.js +1 -0
  30. package/payload/server/public/assets/{public-DRrf63wm.js → public-CJN5KAiK.js} +1 -1
  31. package/payload/server/public/assets/{useVoiceRecorder-CR8gcELb.js → useVoiceRecorder-DyVx7e7a.js} +1 -1
  32. package/payload/server/public/data.html +5 -5
  33. package/payload/server/public/graph.html +6 -6
  34. package/payload/server/public/index.html +8 -8
  35. package/payload/server/public/public.html +5 -5
  36. package/payload/server/server.js +211 -165
  37. package/payload/server/public/assets/data-BuuqlV4L.js +0 -1
  38. package/payload/server/public/assets/graph-CtVITeok.js +0 -1
  39. package/payload/server/public/assets/page-Ddc_nKh8.js +0 -1
  40. package/payload/server/public/assets/page-IQBQoOdT.js +0 -50
  41. /package/payload/server/public/assets/{jsx-runtime-DnY0498s.js → jsx-runtime-nxP_2eNo.js} +0 -0
@@ -92,6 +92,20 @@ export interface ScoredNode {
92
92
  properties: Record<string, unknown>;
93
93
  vectorScore: number;
94
94
  bm25Score: number;
95
+ /**
96
+ * Task 967 — true when this node entered the merge map via the BM25 path
97
+ * with a raw Lucene score > 0 (literal-token match against the universal
98
+ * fulltext index), or via the keyword-subscriptions property-lookup path.
99
+ * Set BEFORE bm25 normalisation, so a single-hit set whose normalised
100
+ * value collapses to 0.0 still carries the flag.
101
+ *
102
+ * Read by the vector-threshold filter as the carve-out predicate: rows
103
+ * with weak cosine survive when `bm25Hit` is true (operator's literal
104
+ * tokens override the semantic similarity floor). Without this flag the
105
+ * naive `bm25Score > 0` check would drop every bottom-of-set BM25 row
106
+ * because min-max normalisation pins the lowest score to 0.
107
+ */
108
+ bm25Hit: boolean;
95
109
  }
96
110
 
97
111
  export type SearchMode = "hybrid" | "bm25";
@@ -124,6 +138,23 @@ export interface HybridParams extends Bm25OnlyParams {
124
138
  * false so embed failure surfaces loudly.
125
139
  */
126
140
  degradeOnEmbedFailure?: boolean;
141
+ /**
142
+ * Task 967 — minimum raw vector cosine in [0,1] required for a row to
143
+ * pass the merge step. Rows below the threshold are dropped UNLESS they
144
+ * also entered via the BM25 path (`bm25Hit === true`) — that carve-out
145
+ * preserves literal-token matches whose embedding happened to score
146
+ * mediocre. Counts surface in HybridResponse so callers can render an
147
+ * "N suppressed — show all" affordance.
148
+ *
149
+ * Undefined ⇒ no threshold (legacy behaviour; memory MCP keeps this).
150
+ * Zero ⇒ explicit "show all" override (admin route's /data and /graph
151
+ * surfaces use this when the operator clicks "show all").
152
+ *
153
+ * Calibration: see `DEFAULT_VECTOR_THRESHOLD` in
154
+ * `platform/ui/server/routes/admin/graph-search.ts` for the route's
155
+ * default; the lib itself takes no opinion on the value.
156
+ */
157
+ vectorThreshold?: number;
127
158
  }
128
159
 
129
160
  export interface HybridResponse {
@@ -139,6 +170,26 @@ export interface HybridResponse {
139
170
  * `expandHops === 0` or no merged results.
140
171
  */
141
172
  expandMs: number;
173
+ /**
174
+ * Task 967 — operator-facing threshold accounting.
175
+ *
176
+ * `rawMerged` count of nodes in the merge map BEFORE the threshold
177
+ * filter ran (vector + BM25 + keyword-subscriptions
178
+ * union, deduped by nodeId).
179
+ * `suppressed` count of nodes the threshold filter dropped.
180
+ * Always 0 when `vectorThreshold` is undefined.
181
+ * `bm25Bypass` count of nodes that would have been dropped by the
182
+ * vector-cosine floor BUT survived because `bm25Hit`
183
+ * was true. Subset of the rendered set.
184
+ * `threshold` the actual threshold applied (echoes the param);
185
+ * null when no filter ran. Drives the "show all"
186
+ * affordance: if null, no banner; if non-null and
187
+ * `suppressed > 0`, banner with count.
188
+ */
189
+ rawMerged: number;
190
+ suppressed: number;
191
+ bm25Bypass: number;
192
+ threshold: number | null;
142
193
  }
143
194
 
144
195
  export type EmbedFn = (text: string) => Promise<number[]>;
@@ -332,7 +383,21 @@ export async function hybrid(
332
383
  bm25Score: normalised[i] ?? 0,
333
384
  related: [],
334
385
  }));
335
- return { mode: "bm25", results, embedError: msg, expandMs: 0 };
386
+ // Task 967 threshold has no meaning in the degraded path: no embed
387
+ // happened, so there is no vector cosine to compare against. Surface
388
+ // counts in the response (rawMerged = rendered, suppressed = 0,
389
+ // bm25Bypass = 0, threshold = null) so the caller's render path is
390
+ // uniform across hybrid + degraded modes.
391
+ return {
392
+ mode: "bm25",
393
+ results,
394
+ embedError: msg,
395
+ expandMs: 0,
396
+ rawMerged: results.length,
397
+ suppressed: 0,
398
+ bm25Bypass: 0,
399
+ threshold: null,
400
+ };
336
401
  }
337
402
 
338
403
  const labelToIndex = await discoverIndexes(session);
@@ -350,7 +415,15 @@ export async function hybrid(
350
415
  .map((l) => labelToIndex.get(l))
351
416
  .filter((idx): idx is string => idx !== undefined);
352
417
  if (indexesToQuery.length === 0) {
353
- return { mode: "hybrid", results: [], expandMs: 0 };
418
+ return {
419
+ mode: "hybrid",
420
+ results: [],
421
+ expandMs: 0,
422
+ rawMerged: 0,
423
+ suppressed: 0,
424
+ bm25Bypass: 0,
425
+ threshold: params.vectorThreshold ?? null,
426
+ };
354
427
  }
355
428
  } else {
356
429
  indexesToQuery = [...new Set(labelToIndex.values())];
@@ -402,6 +475,7 @@ export async function hybrid(
402
475
  properties: plainProperties(node.properties),
403
476
  vectorScore: score,
404
477
  bm25Score: 0,
478
+ bm25Hit: false,
405
479
  });
406
480
  }
407
481
  }
@@ -458,6 +532,7 @@ export async function hybrid(
458
532
  const existing = scoreMap.get(nodeId);
459
533
  if (existing) {
460
534
  existing.bm25Score = Math.max(existing.bm25Score, 1.0);
535
+ existing.bm25Hit = true;
461
536
  } else {
462
537
  const node = record.get("node") as { properties: Record<string, unknown> };
463
538
  scoreMap.set(nodeId, {
@@ -466,17 +541,48 @@ export async function hybrid(
466
541
  properties: plainProperties(node.properties),
467
542
  vectorScore: 0,
468
543
  bm25Score: 1.0,
544
+ bm25Hit: true,
469
545
  });
470
546
  }
471
547
  }
472
548
  }
473
549
 
474
- // --- Merge & rank ---
475
- const merged = [...scoreMap.values()]
476
- .map((node) => ({
477
- ...node,
478
- combinedScore: VECTOR_WEIGHT * node.vectorScore + BM25_WEIGHT * node.bm25Score,
479
- }))
550
+ // --- Merge, threshold, rank ---
551
+ //
552
+ // Task 967 — vectorThreshold filters BEFORE sort+slice. Order matters:
553
+ // slicing first would consume the slot budget with rows the threshold
554
+ // would have suppressed, under-filling the rendered set. Sorting before
555
+ // filtering would waste cycles ordering rows we are about to drop. So:
556
+ // filter -> sort -> slice. Counts (rawMerged / suppressed / bm25Bypass)
557
+ // are computed against the pre-filter and post-filter populations so the
558
+ // route can render "N suppressed — show all" without a second query.
559
+ //
560
+ // Carve-out: a row with vector cosine below the threshold survives iff
561
+ // bm25Hit is true. The flag (not bm25Score) is the gate — min-max
562
+ // normalisation pins the lowest BM25 row's score to 0.0, so checking
563
+ // bm25Score > 0 would silently drop a literal-token match that just
564
+ // happens to be the worst-scored BM25 hit in the set.
565
+ const allMerged = [...scoreMap.values()].map((node) => ({
566
+ ...node,
567
+ combinedScore: VECTOR_WEIGHT * node.vectorScore + BM25_WEIGHT * node.bm25Score,
568
+ }));
569
+ const rawMerged = allMerged.length;
570
+ const threshold = params.vectorThreshold;
571
+ let kept = allMerged;
572
+ let bm25Bypass = 0;
573
+ if (threshold !== undefined) {
574
+ kept = [];
575
+ for (const node of allMerged) {
576
+ if (node.vectorScore >= threshold) {
577
+ kept.push(node);
578
+ } else if (node.bm25Hit) {
579
+ kept.push(node);
580
+ bm25Bypass++;
581
+ }
582
+ }
583
+ }
584
+ const suppressed = rawMerged - kept.length;
585
+ const merged = kept
480
586
  .sort((a, b) => b.combinedScore - a.combinedScore)
481
587
  .slice(0, limit);
482
588
 
@@ -561,7 +667,15 @@ export async function hybrid(
561
667
  }
562
668
  }
563
669
 
564
- return { mode: "hybrid", results, expandMs };
670
+ return {
671
+ mode: "hybrid",
672
+ results,
673
+ expandMs,
674
+ rawMerged,
675
+ suppressed,
676
+ bm25Bypass,
677
+ threshold: threshold ?? null,
678
+ };
565
679
  }
566
680
 
567
681
  function mergeBm25Hit(
@@ -572,6 +686,11 @@ function mergeBm25Hit(
572
686
  const existing = map.get(hit.nodeId);
573
687
  if (existing) {
574
688
  existing.bm25Score = Math.max(existing.bm25Score, normalisedScore);
689
+ // Task 967 — set bm25Hit irrespective of normalised value. The hit
690
+ // came from db.index.fulltext.queryNodes (raw Lucene score > 0 by
691
+ // construction); min-max normalisation pinning the lowest row to
692
+ // 0.0 is a presentation artefact, not a "no match" signal.
693
+ existing.bm25Hit = true;
575
694
  } else {
576
695
  map.set(hit.nodeId, {
577
696
  nodeId: hit.nodeId,
@@ -579,6 +698,7 @@ function mergeBm25Hit(
579
698
  properties: hit.properties,
580
699
  vectorScore: 0,
581
700
  bm25Score: normalisedScore,
701
+ bm25Hit: true,
582
702
  });
583
703
  }
584
704
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  Move an already-extracted static-site tree into the per-account static-publish surface (`<accountDir>/sites/<slug>/`) and emit exactly one canonical path slug for the operator to share. The operator pastes the slug after their own public-host root — the public hostname is whatever their tunnel terminates at, and is not knowable from inside this skill.
4
4
 
5
+ **Invoked from `specialists:content-producer`** when the brief carries a host-website / publish-site / put-online intent (Task 966). Admin's IDENTITY.md routes those intents to that specialist on turn 1; running this skill inline on the main admin runner exhausts the 10-turn budget on per-turn ToolSearch + plugin-read discovery before publish-site executes.
6
+
5
7
  ## When to Use
6
8
 
7
9
  Activate when **both** are true:
@@ -2,6 +2,8 @@
2
2
 
3
3
  Safely extract a zip archive the admin has uploaded, inventory its contents, and propose one concrete follow-up per entry class — refusing archives that attempt path traversal, symlink escape, or decompression-bomb pathologies.
4
4
 
5
+ **Invoked from `specialists:content-producer`** when the archive is a static-site zip and the brief carries a host-website / publish-site / put-online intent (Task 966) — the specialist chains directly into `publish-site` on the extracted tree. For graph-bound archives (LinkedIn export, conversation transcripts, PDFs etc.), invocation routes via `specialists:database-operator` instead.
6
+
5
7
  ## When to Use
6
8
 
7
9
  Activate when the `[ATTACHMENTS:]` block of the current turn contains an entry whose MIME is `application/zip` or `application/x-zip-compressed`. Every such attachment line carries an `ID: <uuid> Path: <storagePath>` pair — the `storagePath` is the absolute path to the `.zip` on disk and is the input to this flow.
@@ -166,7 +166,8 @@ When the user asks what you can do, answer from the specialist domains, admin-ow
166
166
  - Never state a future commitment ("I'll flag", "I'll check", "I'll remind") without immediately creating the mechanism to fulfil it — a scheduled event, a task, or a workflow trigger. A commitment without a backing mechanism is a broken promise.
167
167
  - Store everything you learn about the business in the graph — not in files.
168
168
  - For document ingestion of any kind — PDFs, text, transcripts, web pages, audio, video, single files, archives — delegate to the `specialists:database-operator` specialist. Include the document path, the document subject (account owner, the business, a third party, etc. — ask if not obvious from context), and the scope (admin/shared/public — ask if not obvious). **Not** `specialists:content-producer`. content-producer produces documents from the populated graph; it does not ingest. The two are opposite movements through the graph and must never be conflated.
169
- - For ad-hoc graph operations pruning orphan nodes, deduplicating entities, adding edges, normalising labels, tidying schema drift — delegate to the `specialists:database-operator` specialist. Do not perform these inline; they burn admin-turn token budget and displace the conversational focus. **Not** `specialists:personal-assistant` — PA has no graph-write surface; misdelegation fails at the tool-gate after wasting a turn.
169
+ - For any raw cypher against the memory graph read or write — delegate to the `specialists:database-operator` specialist. Your read surface is the wrapped tools (`memory-search`, `memory-rank`, `conversation-search`, `profile-read`); your write surface is the schema-aware wrappers (`memory-write`, `memory-update`, `memory-find-candidates`, `memory-delete`). Anything that needs a `MATCH ... RETURN`, a property the wrappers do not expose, or a multi-statement transaction is the operator's surface (e.g. property-name reads against `:CloudflareHostname` / `:Task` / `:Person`, edge-shape audits, dedupe merges, label normalisation, prune passes). Do not perform these inline; admin's `# SCHEMA` block lists labels and edges only — it does not carry the property dictionary, so inline cypher routinely targets non-existent properties and the resulting `01N52` warning is not surfaced. **Not** `specialists:personal-assistant` — PA has no graph surface; misdelegation fails at the tool-gate after wasting a turn.
170
+ - For host-website / publish-site / put-online intents — typically a `.zip` attachment with HTML + assets and a request like "host this website" or "put this online" — delegate immediately to the `specialists:content-producer` specialist on turn 1. Do not `ToolSearch` publish-site, do not `plugin-read` the skill inline, do not pre-scan the zip. The specialist owns the `unzip-attachment` → `publish-site` chain and runs the deterministic Bash flow on its own 10-turn budget; running it inline here exhausts the main-runner budget on per-turn discovery (Task 966 evidence: a 2026-05-10 brochure session hit `error_max_turns total_tool_calls=24` before publish-site executed). **Not** `specialists:database-operator` — static-site zips are extracted to disk for publication, never written to the graph.
170
171
 
171
172
  ## Proactive Commitment Detection
172
173
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: content-producer
3
- description: "Visual production — reads from the populated graph to produce visual artifacts: image generation, PDF rendering, and component delivery. Delegate when a task requires generating images or saving rendered pages as PDF. **Not** document ingestion — ingestion of any kind routes to `specialists:database-operator`."
4
- summary: "Produces visual output from your graph — generates images and renders pages to PDF. For example, when you need a cover image for a brief or want to save a rendered page as PDF."
3
+ description: "Visual production and static-site hosting — reads from the populated graph to produce visual artifacts (image generation, PDF rendering, component delivery) and hosts already-prepared static sites by extracting attached archives via unzip-attachment then placing the tree under <accountDir>/sites/<slug>/ via publish-site. Delegate for: generating images, saving rendered pages as PDF, or any 'host this website' / 'publish this site' / 'put this online' intent carrying an HTML+assets archive. **Not** document ingestion — graph ingestion of any kind routes to `specialists:database-operator`. Static-site zips are extracted to disk for publication, never written to the graph."
4
+ summary: "Produces visual output from your graph — generates images, renders pages to PDF, and hosts static websites you upload as a zip. For example, when you need a cover image for a brief, want to save a rendered page as PDF, or upload a brochure zip and ask to put it online."
5
5
  model: claude-sonnet-4-6
6
- tools: mcp__memory__memory-search, mcp__replicate__image-generate, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_pdf_save, mcp__admin__render-component, mcp__admin__file-attach
6
+ tools: Bash, mcp__memory__memory-search, mcp__replicate__image-generate, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_pdf_save, mcp__admin__render-component, mcp__admin__file-attach, mcp__admin__plugin-read
7
7
  ---
8
8
 
9
9
  # Content Producer
@@ -68,6 +68,20 @@ Generate PDFs from rendered HTML pages using the browser tools.
68
68
 
69
69
  **Local files:** `file://` URLs are rewritten transparently by the `playwright-file-guard` PreToolUse hook — pass them directly to `browser_navigate` and the hook will spawn a loopback `python3 -m http.server` on a free port and rewrite the URL before Playwright sees it. No agent-side server management needed.
70
70
 
71
+ ## Hosting websites
72
+
73
+ When a brief carries a "host this website" / "publish this site" / "put this online" intent — typically with a `.zip` attachment whose extracted contents are HTML + assets — execute the deterministic two-skill chain. This is **not** ingestion (no graph write); it is filesystem extraction followed by a placement move into the per-account static-publish surface served by the existing `/sites/*` route. The `## Out of scope: ingestion of any kind` rule above is preserved by the SKILL gate at [publish-site SKILL.md](../../../plugins/admin/skills/publish-site/SKILL.md): only HTML + assets directory trees activate this path; PDFs, slide decks, single-image attachments, or zips whose extracted output is not a static-site tree route to their own skills (`a4-print-documents`, `deck-pages`) or — for graph-bound content — back to `specialists:database-operator`.
74
+
75
+ The chain (≤4 turns, total):
76
+
77
+ 1. **Read the skill texts** via `mcp__admin__plugin-read` — load `admin/skills/unzip-attachment/SKILL.md` and `admin/skills/publish-site/SKILL.md` into context. Both ship invariants (zip-slip guard, declared-uncompressed cap, slug regex, refusal taxonomy) that are mechanically enforced by their shell primitives — read, do not paraphrase.
78
+ 2. **Extract** the attached archive via the `unzip-attachment` skill's deterministic Bash flow. Output lands at `<accountDir>/extracted/<attachmentId>/`. Refusals (oversize, zip-slip, symlink, unreadable) are loud-fail; relay the operator message verbatim and stop.
79
+ 3. **Confirm the slug** with the operator if not already explicit. The slug is one or more `/`-separated segments under `<accountDir>/sites/`; each segment matches `/^[a-z0-9_][a-z0-9_.-]{0,99}$/i` and no segment starts with `.` or equals `..`. Never invent a slug.
80
+ 4. **Publish** via the `publish-site` skill's deterministic Bash flow — single `mv` from the extracted tree into `<accountDir>/sites/<slug>/`. Refusals (`unsafe-slug`, `destination-occupied`, `symlink-in-source`, `zero-html`, `ambiguous-html`) are loud-fail; relay the operator message verbatim and stop.
81
+ 5. **Emit** the canonical path slug (`/sites/<slug>/` when an `index.html` is present, otherwise `/sites/<slug>/<file>.html`). One line, framed as "paste this after your public-host root" — no scheme, no host. The `/sites/*` route at [server/routes/sites.ts](../../../ui/server/routes/sites.ts) takes care of the directory-form trailing-slash redirect.
82
+
83
+ **No fallback servers, no Playwright probes, no service restarts.** If the move or URL form is wrong, refuse — never reach for `python -m http.server`, `npx http-server`, browser-automation probes, or platform restarts. That is the exact failure pattern publish-site exists to close.
84
+
71
85
  ## File delivery
72
86
 
73
87
  Use `file-attach` to make generated files available for download in the chat. Use `render-component` with component name `file-attachment` for download delivery.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: database-operator
3
- description: "Document and archive ingestion and ad-hoc graph operations running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
3
+ description: "Owner of the memory graph any raw cypher (read or write) against Neo4j routes here, plus all document and archive ingestion (running the universal `document-ingest` skill for unstructured documents PDF, text, transcript, web page, audio, video and per-source archive-import skills LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels), plus one-off raw reads (property-name lookups, edge-shape audits, multi-statement queries) when admin's wrapped read tools — `memory-search`, `memory-rank`, `conversation-search`, `profile-read` — do not expose the property or relationship being asked about. Delegate when the operator uploads any document, drops an archive directory into chat, asks for any graph operation that is not a routine per-turn wrapped write, or asks a factual question whose answer requires a property or relationship admin's wrapped read tools cannot reach."
4
4
  summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
5
5
  model: claude-sonnet-4-6
6
6
  tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__tasks__task-create, mcp__admin__file-attach, mcp__admin__plugin-read