@rubytech/create-maxy 1.0.875 → 1.0.876

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 (27) hide show
  1. package/dist/index.js +51 -37
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-search/src/__tests__/brochure-threshold.test.ts +136 -0
  4. package/payload/platform/plugins/admin/mcp/dist/index.js +19 -2
  5. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  6. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -1
  7. package/payload/platform/plugins/docs/references/deployment.md +1 -1
  8. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
  9. package/payload/server/public/assets/{Checkbox-CiL1E1ss.js → Checkbox-BsqexMy3.js} +1 -1
  10. package/payload/server/public/assets/{admin-DJodbJ_C.js → admin-pIeHRytz.js} +6 -6
  11. package/payload/server/public/assets/data-rhAG7W2b.js +1 -0
  12. package/payload/server/public/assets/graph-DVAWZmkb.js +1 -0
  13. package/payload/server/public/assets/{graph-labels-B3yLO-UT.js → graph-labels-t_04n4zX.js} +1 -1
  14. package/payload/server/public/assets/{jsx-runtime-CstxPbUG.css → jsx-runtime-CGCRFPeX.css} +1 -1
  15. package/payload/server/public/assets/{page-C_FMEZcW.js → page-BM9O7QN8.js} +1 -1
  16. package/payload/server/public/assets/page-qI0NJSs6.js +50 -0
  17. package/payload/server/public/assets/{public-nLRQYsaG.js → public-oNo_2gt0.js} +1 -1
  18. package/payload/server/public/assets/{useVoiceRecorder-PQASU6Eq.js → useVoiceRecorder-DVVSQc-9.js} +1 -1
  19. package/payload/server/public/data.html +5 -5
  20. package/payload/server/public/graph.html +6 -6
  21. package/payload/server/public/index.html +8 -8
  22. package/payload/server/public/public.html +5 -5
  23. package/payload/server/server.js +1 -1
  24. package/payload/server/public/assets/data-CXF4YQ99.js +0 -1
  25. package/payload/server/public/assets/graph-CtSKqC3V.js +0 -1
  26. package/payload/server/public/assets/page-XR25fnQN.js +0 -50
  27. /package/payload/server/public/assets/{jsx-runtime-BRxv9PO7.js → jsx-runtime-B8sGPXtT.js} +0 -0
package/dist/index.js CHANGED
@@ -1231,15 +1231,16 @@ function setupDedicatedNeo4j() {
1231
1231
  const logDir = `/var/log/neo4j-${brandSuffix}`;
1232
1232
  const serviceName = `neo4j-${brandSuffix}`;
1233
1233
  const httpPort = NEO4J_PORT - 213; // Preserve standard 7687/7474 offset
1234
- // Conf creation is gated on first install; everything below the if/else
1235
- // (state remediation + start + verify) runs on every install — Task 787
1236
- // closes the recovery gap where a half-installed Pi (failed prior install
1237
- // hit start-limit-hit, dedicated unit in failed state) returned here without
1238
- // the fix being applied.
1234
+ // Per-brand state (sed, mkdir, chown, unit-write) is idempotent and runs on
1235
+ // every install. Only the base-config copy and initial-password rotation are
1236
+ // gated to first install Task 787 established that state remediation must
1237
+ // run every install; Task 979 extended the same principle to the conf and
1238
+ // unit emission so a half-installed host (broken unit missing NEO4J_HOME)
1239
+ // recovers on retry without an out-of-band manual reset.
1239
1240
  const confExists = spawnSync("test", ["-f", `${confDir}/neo4j.conf`], { stdio: "pipe" }).status === 0;
1240
1241
  if (confExists) {
1241
- console.log(` Dedicated Neo4j instance for ${BRAND.productName} already configured at ${confDir}`);
1242
- logFile(` Neo4j dedicated: existing config at ${confDir}, skipping conf creation`);
1242
+ console.log(` Dedicated Neo4j instance for ${BRAND.productName} already configured at ${confDir} — re-applying per-brand state`);
1243
+ logFile(` Neo4j dedicated: existing config at ${confDir}, re-applying sed/mkdir/chown/unit on every install`);
1243
1244
  }
1244
1245
  else {
1245
1246
  console.log(` Setting up dedicated Neo4j instance for ${BRAND.productName} on bolt://localhost:${NEO4J_PORT}...`);
@@ -1252,31 +1253,41 @@ function setupDedicatedNeo4j() {
1252
1253
  if (!existsSync("/etc/neo4j/neo4j.conf")) {
1253
1254
  throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
1254
1255
  }
1255
- // 1. Copy base config
1256
+ // Copy base config (first install only — sed runs unconditionally below)
1256
1257
  console.log(" [privileged] cp -r");
1257
1258
  shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
1258
- // 2. Modify config for this instance: bolt port, HTTP port, data/log directories
1259
- console.log(" [privileged] sed -i");
1260
- shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
1261
- console.log(" [privileged] sed -i");
1262
- shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
1263
- console.log(" [privileged] sed -i");
1264
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
1265
- console.log(" [privileged] sed -i");
1266
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
1267
- // Verify config was updated — sed silently no-ops if the key format changed
1268
- const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
1269
- if (confContent.status !== 0) {
1270
- console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
1271
- logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
1272
- }
1273
- // 3. Create data and log directories
1274
- console.log(" [privileged] mkdir -p");
1275
- shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
1276
- console.log(" [privileged] chown -R");
1277
- shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
1278
- // 4. Create systemd service
1279
- const serviceContent = `[Unit]
1259
+ }
1260
+ // Idempotent per-brand state — runs on every install (Task 979).
1261
+ // sed `s|^#?key=.*|key=value|` no-ops when the file already has the target
1262
+ // value; mkdir -p, chown -R, and unit-write are idempotent by definition.
1263
+ // NEO4J_HOME=${dataDir} makes server.directories.run resolve per-brand to
1264
+ // ${dataDir}/run, so the launcher's pre-flight does not collide with the
1265
+ // system unit's /var/lib/neo4j/run/neo4j.pid (Task 979 root cause). Plugins
1266
+ // and import are sed-overridden because the apt base conf pins them to
1267
+ // absolute /var/lib/neo4j/plugins and /var/lib/neo4j/import (shared).
1268
+ console.log(" [privileged] sed -i");
1269
+ shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
1270
+ console.log(" [privileged] sed -i");
1271
+ shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
1272
+ console.log(" [privileged] sed -i");
1273
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
1274
+ console.log(" [privileged] sed -i");
1275
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
1276
+ console.log(" [privileged] sed -i");
1277
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.plugins=.*|server.directories.plugins=${dataDir}/plugins|`, `${confDir}/neo4j.conf`], { sudo: true });
1278
+ console.log(" [privileged] sed -i");
1279
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.import=.*|server.directories.import=${dataDir}/import|`, `${confDir}/neo4j.conf`], { sudo: true });
1280
+ // Verify config was updated — sed silently no-ops if the key format changed
1281
+ const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
1282
+ if (confContent.status !== 0) {
1283
+ console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
1284
+ logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
1285
+ }
1286
+ console.log(" [privileged] mkdir -p");
1287
+ shell("mkdir", ["-p", `${dataDir}/data`, `${dataDir}/plugins`, `${dataDir}/import`, logDir], { sudo: true });
1288
+ console.log(" [privileged] chown -R");
1289
+ shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
1290
+ const serviceContent = `[Unit]
1280
1291
  Description=Neo4j Graph Database (${BRAND.productName})
1281
1292
  After=network-online.target
1282
1293
  Wants=network-online.target
@@ -1286,18 +1297,21 @@ ExecStart=/usr/bin/neo4j console
1286
1297
  Restart=on-failure
1287
1298
  User=neo4j
1288
1299
  Group=neo4j
1289
- Environment=NEO4J_CONF=${confDir}
1300
+ Environment="NEO4J_CONF=${confDir}" "NEO4J_HOME=${dataDir}"
1290
1301
  LimitNOFILE=60000
1291
1302
 
1292
1303
  [Install]
1293
1304
  WantedBy=multi-user.target
1294
1305
  `;
1295
- const tmpServicePath = `/tmp/${serviceName}.service`;
1296
- writeFileSync(tmpServicePath, serviceContent);
1297
- console.log(" [privileged] cp");
1298
- shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
1299
- spawnSync("rm", ["-f", tmpServicePath]);
1300
- // 5. Set initial password before first start
1306
+ const tmpServicePath = `/tmp/${serviceName}.service`;
1307
+ writeFileSync(tmpServicePath, serviceContent);
1308
+ console.log(" [privileged] cp");
1309
+ shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
1310
+ spawnSync("rm", ["-f", tmpServicePath]);
1311
+ logFile(` [neo4j] dedicated unit env: NEO4J_CONF=${confDir} NEO4J_HOME=${dataDir}`);
1312
+ if (!confExists) {
1313
+ // Set initial password before first start (first install only — rotation
1314
+ // on retry would brick an existing DB whose password is already stored).
1301
1315
  const password = randomBytes(24).toString("base64url");
1302
1316
  const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
1303
1317
  mkdirSync(persistDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.875",
3
+ "version": "1.0.876",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { hybrid, clearIndexCache } from "../index.js";
3
+
4
+ /**
5
+ * Task 978 — brochure-class calibration fixture.
6
+ *
7
+ * Pre-Task-978 the route's `DEFAULT_VECTOR_THRESHOLD` was 0.72, calibrated
8
+ * against the `savage` query (every score < 0.70, only the relevant row
9
+ * passes the BM25 carve-out). `brochure` is a different shape: keepers
10
+ * cluster in [0.8473, 0.8623], noise in [0.7159, 0.8143]. At 0.72 the
11
+ * floor caught only the lone outlier below 0.72; all 11 in-cluster rows
12
+ * rendered. Task 978 raised the default to 0.82, the midpoint of the
13
+ * 0.033-wide gap between brochure-keepers and brochure-noise.
14
+ *
15
+ * This fixture pins both ends of that calibration so a future drop back
16
+ * toward 0.72 (or a push past 0.85 that would clip the lowest keeper)
17
+ * fails the test instead of silently regressing /graph search quality.
18
+ */
19
+
20
+ interface StubRun {
21
+ match: (query: string) => boolean;
22
+ records: Array<Record<string, unknown>>;
23
+ }
24
+
25
+ function record(fields: Record<string, unknown>) {
26
+ return { get: (k: string) => fields[k] };
27
+ }
28
+
29
+ function makeStubSession(scripted: StubRun[]) {
30
+ return {
31
+ run(query: string) {
32
+ const hit = scripted.find((s) => s.match(query));
33
+ if (!hit) return Promise.resolve({ records: [] });
34
+ return Promise.resolve({ records: hit.records.map(record) });
35
+ },
36
+ } as unknown as import("neo4j-driver").Session;
37
+ }
38
+
39
+ // Real Agent laptop 2026-05-12 snapshot, `brochure` against
40
+ // project_embedding for account b3b638d9. BM25 = 0 on every row.
41
+ const BROCHURE_VECTOR_ROWS = [
42
+ { nodeId: "k1", name: "Brochure Production — Alex Pelosi Buchanan", score: 0.8623 },
43
+ { nodeId: "k2", name: "Brochure Production — Ross Margetts", score: 0.8523 },
44
+ { nodeId: "k3", name: "Brochure Production — Donna Vincent", score: 0.8521 },
45
+ { nodeId: "k4", name: "Brochure Production — Paul Leslie", score: 0.8509 },
46
+ { nodeId: "k5", name: "Brochure Production — John Savage", score: 0.8474 },
47
+ { nodeId: "k6", name: "Brochure Production — Muvin", score: 0.8473 },
48
+ { nodeId: "n1", name: "Real Agent Product Development", score: 0.8143 },
49
+ { nodeId: "n2", name: "Dan McLeod Partnership", score: 0.7498 },
50
+ { nodeId: "n3", name: "Real Agent Lettings — New Vertical", score: 0.7498 },
51
+ { nodeId: "n4", name: "Real Agent Lettings", score: 0.7487 },
52
+ { nodeId: "n5", name: "Fundraising & Finance", score: 0.7323 },
53
+ { nodeId: "n6", name: "Muvin Pilot", score: 0.7240 },
54
+ { nodeId: "n7", name: "Go-to-Market & Events", score: 0.7159 },
55
+ ];
56
+
57
+ function makeBrochureSession() {
58
+ return makeStubSession([
59
+ {
60
+ match: (q) => q.includes("SHOW INDEXES"),
61
+ records: [{ name: "project_embedding", labelsOrTypes: ["Project"] }],
62
+ },
63
+ {
64
+ match: (q) => q.includes("db.index.vector.queryNodes"),
65
+ records: BROCHURE_VECTOR_ROWS.map((r) => ({
66
+ nodeId: r.nodeId,
67
+ nodeLabels: ["Project"],
68
+ node: { properties: { name: r.name } },
69
+ score: r.score,
70
+ })),
71
+ },
72
+ // BM25 returns nothing — no `brochure` token literal in any name.
73
+ { match: (q) => q.includes("db.index.fulltext.queryNodes"), records: [] },
74
+ ]);
75
+ }
76
+
77
+ beforeEach(() => {
78
+ clearIndexCache();
79
+ });
80
+
81
+ describe("brochure-class threshold calibration (Task 978)", () => {
82
+ it("at threshold 0.72 (pre-fix) renders 12 rows — the bug shape", async () => {
83
+ const res = await hybrid(makeBrochureSession(), async () => [0.1], {
84
+ query: "brochure",
85
+ accountId: "acc-1",
86
+ limit: 100,
87
+ labels: ["Project"],
88
+ expandHops: 0,
89
+ vectorThreshold: 0.72,
90
+ });
91
+ // 13 rows merged, lowest (0.7159) below 0.72, 12 rendered.
92
+ expect(res.rawMerged).toBe(13);
93
+ expect(res.results).toHaveLength(12);
94
+ expect(res.suppressed).toBe(1);
95
+ expect(res.bm25Bypass).toBe(0);
96
+ });
97
+
98
+ it("at threshold 0.82 (post-fix) renders the 6 keepers exactly", async () => {
99
+ const res = await hybrid(makeBrochureSession(), async () => [0.1], {
100
+ query: "brochure",
101
+ accountId: "acc-1",
102
+ limit: 100,
103
+ labels: ["Project"],
104
+ expandHops: 0,
105
+ vectorThreshold: 0.82,
106
+ });
107
+ expect(res.rawMerged).toBe(13);
108
+ expect(res.results).toHaveLength(6);
109
+ expect(res.suppressed).toBe(7);
110
+ expect(res.bm25Bypass).toBe(0);
111
+ expect(res.results.map((r) => r.properties.name as string).sort()).toEqual([
112
+ "Brochure Production — Alex Pelosi Buchanan",
113
+ "Brochure Production — Donna Vincent",
114
+ "Brochure Production — John Savage",
115
+ "Brochure Production — Muvin",
116
+ "Brochure Production — Paul Leslie",
117
+ "Brochure Production — Ross Margetts",
118
+ ]);
119
+ });
120
+
121
+ it("at threshold 0.85 (too tight) starts clipping keepers — regression guard", async () => {
122
+ const res = await hybrid(makeBrochureSession(), async () => [0.1], {
123
+ query: "brochure",
124
+ accountId: "acc-1",
125
+ limit: 100,
126
+ labels: ["Project"],
127
+ expandHops: 0,
128
+ vectorThreshold: 0.85,
129
+ });
130
+ // Two keepers (0.8473, 0.8474) fall below 0.85 — proves 0.82 sits in
131
+ // the gap on purpose, not by accident. Don't move the default this
132
+ // high without rechecking.
133
+ expect(res.results).toHaveLength(4);
134
+ expect(res.suppressed).toBe(9);
135
+ });
136
+ });
@@ -536,6 +536,13 @@ server.tool("onboarding-plugin-options", "Return the fully-assembled multi-selec
536
536
  const meta = PLUGIN_DISPLAY[name] ?? { value: name, label: name, description: "" };
537
537
  options.push({ ...meta, group: "Maxy" });
538
538
  }
539
+ // Task 976: signal that the onboarding skill will append three Anthropic
540
+ // vertical entries from the claude-for-financial-services and
541
+ // knowledge-work-plugins marketplaces. The count is fixed because the
542
+ // skill prose always appends the same three (kyc-screener,
543
+ // meeting-prep-agent, pdf-viewer). Absence of this line post-install
544
+ // = the group was not surfaced (release blocker per task spec).
545
+ console.error(`[plugin-onboarding] group=anthropic-verticals presented=3`);
539
546
  return { content: [{ type: "text", text: JSON.stringify(options, null, 2) }] };
540
547
  }
541
548
  catch (err) {
@@ -2237,11 +2244,21 @@ server.tool("onboarding-get", "Read the current onboarding state from the graph.
2237
2244
  });
2238
2245
  server.tool("onboarding-complete-step", "Mark an onboarding step as completed. Sets a completion timestamp on the step " +
2239
2246
  "and updates currentStep to the highest completed step. Idempotent — completing " +
2240
- "an already-completed step preserves its original timestamp. Step must be 1–9.", {
2247
+ "an already-completed step preserves its original timestamp. Step must be 1–9. " +
2248
+ "When step is 1, optionally pass acceptedAnthropicVerticals + declinedAnthropicVerticals " +
2249
+ "(both required together) to emit the paired counter for the [plugin-onboarding] " +
2250
+ "group=anthropic-verticals prefix; the skill derives both arrays from the closed set " +
2251
+ "{kyc-screener, meeting-prep-agent, pdf-viewer} and the user's submission.", {
2241
2252
  step: z.number().int().min(1).max(9).describe("The onboarding step number (1–9) to mark as completed"),
2242
- }, async ({ step }) => {
2253
+ acceptedAnthropicVerticals: z.array(z.string()).optional().describe("Step 1 only: plugin slugs the user accepted from the Anthropic verticals group. Pass together with declinedAnthropicVerticals."),
2254
+ declinedAnthropicVerticals: z.array(z.string()).optional().describe("Step 1 only: plugin slugs the user declined from the Anthropic verticals group. Pass together with acceptedAnthropicVerticals."),
2255
+ }, async ({ step, acceptedAnthropicVerticals, declinedAnthropicVerticals }) => {
2243
2256
  try {
2244
2257
  const state = await completeOnboardingStep(getSession, ACCOUNT_ID, step);
2258
+ // Task 977: closed set lives in skill prose, not server validation — a fourth vertical updates the skill.
2259
+ if (step === 1 && acceptedAnthropicVerticals !== undefined && declinedAnthropicVerticals !== undefined) {
2260
+ console.error(`[plugin-onboarding] group=anthropic-verticals accepted=${acceptedAnthropicVerticals.length} declined=${declinedAnthropicVerticals.length}`);
2261
+ }
2245
2262
  return {
2246
2263
  content: [{
2247
2264
  type: "text",