@rubytech/create-maxy-code 0.1.184 → 0.1.185

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 (64) hide show
  1. package/dist/__tests__/platform-port-stamp.test.js +4 -0
  2. package/dist/index.js +1 -0
  3. package/dist/port-resolution.js +1 -0
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand.json +1 -1
  6. package/payload/platform/package-lock.json +178 -0
  7. package/payload/platform/plugins/.claude-plugin/marketplace.json +15 -0
  8. package/payload/platform/plugins/admin/PLUGIN.md +0 -1
  9. package/payload/platform/plugins/browser/.claude-plugin/plugin.json +17 -0
  10. package/payload/platform/plugins/browser/PLUGIN.md +44 -0
  11. package/payload/platform/plugins/browser/mcp/dist/index.d.ts +2 -0
  12. package/payload/platform/plugins/browser/mcp/dist/index.d.ts.map +1 -0
  13. package/payload/platform/plugins/browser/mcp/dist/index.js +61 -0
  14. package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -0
  15. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts +66 -0
  16. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts.map +1 -0
  17. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js +248 -0
  18. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js.map +1 -0
  19. package/payload/platform/plugins/browser/mcp/package.json +19 -0
  20. package/payload/platform/plugins/docs/references/internals.md +1 -1
  21. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -0
  22. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -4
  23. package/payload/platform/plugins/prompt-optimiser/.claude-plugin/plugin.json +8 -0
  24. package/payload/platform/plugins/prompt-optimiser/PLUGIN.md +14 -0
  25. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +301 -0
  26. package/payload/platform/plugins/url-get/.claude-plugin/plugin.json +17 -0
  27. package/payload/platform/plugins/url-get/PLUGIN.md +86 -0
  28. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts +9 -0
  29. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts.map +1 -0
  30. package/payload/platform/plugins/url-get/mcp/dist/index.js +53 -0
  31. package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -0
  32. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts +8 -0
  33. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts.map +1 -0
  34. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js +83 -0
  35. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js.map +1 -0
  36. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts +21 -0
  37. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts.map +1 -0
  38. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js +147 -0
  39. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js.map +1 -0
  40. package/payload/platform/plugins/url-get/mcp/package.json +22 -0
  41. package/payload/platform/scripts/setup-account.sh +0 -6
  42. package/payload/platform/scripts/vnc.sh +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +8 -0
  45. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +2 -2
  47. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
  48. package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
  49. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  50. package/payload/platform/templates/specialists/agents/personal-assistant.md +3 -3
  51. package/payload/platform/templates/specialists/agents/research-assistant.md +3 -3
  52. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
  53. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +20 -6
  54. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  55. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +14 -0
  56. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  57. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +28 -34
  58. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  59. package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +43 -8
  60. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +20 -8
  61. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +51 -12
  62. package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +7 -1
  63. package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +0 -278
  64. package/payload/platform/plugins/admin/hooks/playwright-file-guard.sh +0 -214
@@ -15,10 +15,14 @@ test("PLATFORM_PORT stamped on manager unit equal to MAXY_UI_INTERNAL_PORT", ()
15
15
  brandPort: 19200,
16
16
  maxyUiInternalPort: 19201,
17
17
  accountId: "00000000-0000-0000-0000-000000000000",
18
+ cdpPort: 9222,
18
19
  });
19
20
  assert.match(unit, /^Environment=PLATFORM_PORT=19201$/m);
20
21
  assert.match(unit, /^Environment=MAXY_UI_INTERNAL_PORT=19201$/m);
21
22
  assert.match(unit, /^Environment=MAXY_BRAND_PORT=19200$/m);
23
+ // Task 492 — CDP_PORT stamped so the browser plugin's MCP server can
24
+ // attach to the per-brand VNC Chromium.
25
+ assert.match(unit, /^Environment=CDP_PORT=9222$/m);
22
26
  // Order is not asserted; coexistence with the existing brand-port and
23
27
  // internal-port lines is what producers and consumers contract on.
24
28
  });
package/dist/index.js CHANGED
@@ -3048,6 +3048,7 @@ function installService() {
3048
3048
  brandPort: PORT,
3049
3049
  maxyUiInternalPort: MAXY_UI_INTERNAL_PORT,
3050
3050
  accountId: installAccountId,
3051
+ cdpPort: CDP_PORT_BRAND,
3051
3052
  });
3052
3053
  writeFileSync(join(serviceDir, claudeSessionManagerUnitName), claudeSessionManagerUnit);
3053
3054
  logFile(` ${claudeSessionManagerUnitName}: CLAUDE_SESSION_MANAGER_PORT=${CLAUDE_SESSION_MANAGER_PORT_BRAND} MAXY_UI_INTERNAL_PORT=${MAXY_UI_INTERNAL_PORT}`);
@@ -148,6 +148,7 @@ Environment=PLATFORM_ROOT=${o.installDir}/platform
148
148
  Environment=MAXY_BRAND_PORT=${o.brandPort}
149
149
  Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
150
150
  Environment=PLATFORM_PORT=${o.maxyUiInternalPort}
151
+ Environment=CDP_PORT=${o.cdpPort}
151
152
  EnvironmentFile=-${o.persistDir}/.env
152
153
  StandardOutput=append:${o.persistDir}/logs/server.log
153
154
  StandardError=append:${o.persistDir}/logs/server.log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.184",
3
+ "version": "0.1.185",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -48,7 +48,7 @@
48
48
  },
49
49
 
50
50
  "plugins": {
51
- "core": ["admin", "memory", "docs", "cloudflare", "anthropic", "workflows", "work", "scheduling", "email", "contacts", "projects"],
51
+ "core": ["admin", "memory", "docs", "cloudflare", "anthropic", "workflows", "work", "scheduling", "email", "contacts", "projects", "prompt-optimiser", "browser"],
52
52
  "defaultEnabled": ["business-assistant", "sales"],
53
53
  "available": ["telegram", "deep-research", "whatsapp", "replicate", "linkedin-import", "notion-import", "obsidian-import", "x-import"],
54
54
  "excluded": []
@@ -81,6 +81,10 @@
81
81
  "resolved": "plugins/aeo/mcp",
82
82
  "link": true
83
83
  },
84
+ "node_modules/@maxy/browser": {
85
+ "resolved": "plugins/browser/mcp",
86
+ "link": true
87
+ },
84
88
  "node_modules/@maxy/claude-session-manager": {
85
89
  "resolved": "services/claude-session-manager",
86
90
  "link": true
@@ -121,6 +125,10 @@
121
125
  "resolved": "plugins/scheduling/mcp",
122
126
  "link": true
123
127
  },
128
+ "node_modules/@maxy/url-get": {
129
+ "resolved": "plugins/url-get/mcp",
130
+ "link": true
131
+ },
124
132
  "node_modules/@maxy/whatsapp": {
125
133
  "resolved": "plugins/whatsapp/mcp",
126
134
  "link": true
@@ -1098,6 +1106,12 @@
1098
1106
  "url": "https://opencollective.com/express"
1099
1107
  }
1100
1108
  },
1109
+ "node_modules/boolbase": {
1110
+ "version": "1.0.0",
1111
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
1112
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
1113
+ "license": "ISC"
1114
+ },
1101
1115
  "node_modules/buffer": {
1102
1116
  "version": "6.0.3",
1103
1117
  "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -1298,6 +1312,34 @@
1298
1312
  "node": ">= 8"
1299
1313
  }
1300
1314
  },
1315
+ "node_modules/css-select": {
1316
+ "version": "5.2.2",
1317
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
1318
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
1319
+ "license": "BSD-2-Clause",
1320
+ "dependencies": {
1321
+ "boolbase": "^1.0.0",
1322
+ "css-what": "^6.1.0",
1323
+ "domhandler": "^5.0.2",
1324
+ "domutils": "^3.0.1",
1325
+ "nth-check": "^2.0.1"
1326
+ },
1327
+ "funding": {
1328
+ "url": "https://github.com/sponsors/fb55"
1329
+ }
1330
+ },
1331
+ "node_modules/css-what": {
1332
+ "version": "6.2.2",
1333
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
1334
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
1335
+ "license": "BSD-2-Clause",
1336
+ "engines": {
1337
+ "node": ">= 6"
1338
+ },
1339
+ "funding": {
1340
+ "url": "https://github.com/sponsors/fb55"
1341
+ }
1342
+ },
1301
1343
  "node_modules/d3-dispatch": {
1302
1344
  "version": "3.0.1",
1303
1345
  "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
@@ -1390,6 +1432,61 @@
1390
1432
  "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
1391
1433
  "license": "MIT"
1392
1434
  },
1435
+ "node_modules/dom-serializer": {
1436
+ "version": "2.0.0",
1437
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
1438
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
1439
+ "license": "MIT",
1440
+ "dependencies": {
1441
+ "domelementtype": "^2.3.0",
1442
+ "domhandler": "^5.0.2",
1443
+ "entities": "^4.2.0"
1444
+ },
1445
+ "funding": {
1446
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
1447
+ }
1448
+ },
1449
+ "node_modules/domelementtype": {
1450
+ "version": "2.3.0",
1451
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
1452
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
1453
+ "funding": [
1454
+ {
1455
+ "type": "github",
1456
+ "url": "https://github.com/sponsors/fb55"
1457
+ }
1458
+ ],
1459
+ "license": "BSD-2-Clause"
1460
+ },
1461
+ "node_modules/domhandler": {
1462
+ "version": "5.0.3",
1463
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
1464
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
1465
+ "license": "BSD-2-Clause",
1466
+ "dependencies": {
1467
+ "domelementtype": "^2.3.0"
1468
+ },
1469
+ "engines": {
1470
+ "node": ">= 4"
1471
+ },
1472
+ "funding": {
1473
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
1474
+ }
1475
+ },
1476
+ "node_modules/domutils": {
1477
+ "version": "3.2.2",
1478
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
1479
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
1480
+ "license": "BSD-2-Clause",
1481
+ "dependencies": {
1482
+ "dom-serializer": "^2.0.0",
1483
+ "domelementtype": "^2.3.0",
1484
+ "domhandler": "^5.0.3"
1485
+ },
1486
+ "funding": {
1487
+ "url": "https://github.com/fb55/domutils?sponsor=1"
1488
+ }
1489
+ },
1393
1490
  "node_modules/dunder-proto": {
1394
1491
  "version": "1.0.1",
1395
1492
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1434,6 +1531,18 @@
1434
1531
  "node": ">=8.10.0"
1435
1532
  }
1436
1533
  },
1534
+ "node_modules/entities": {
1535
+ "version": "4.5.0",
1536
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
1537
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
1538
+ "license": "BSD-2-Clause",
1539
+ "engines": {
1540
+ "node": ">=0.12"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://github.com/fb55/entities?sponsor=1"
1544
+ }
1545
+ },
1437
1546
  "node_modules/es-define-property": {
1438
1547
  "version": "1.0.1",
1439
1548
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1806,6 +1915,15 @@
1806
1915
  "node": ">= 0.4"
1807
1916
  }
1808
1917
  },
1918
+ "node_modules/he": {
1919
+ "version": "1.2.0",
1920
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
1921
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
1922
+ "license": "MIT",
1923
+ "bin": {
1924
+ "he": "bin/he"
1925
+ }
1926
+ },
1809
1927
  "node_modules/hono": {
1810
1928
  "version": "4.12.18",
1811
1929
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
@@ -2401,6 +2519,28 @@
2401
2519
  "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
2402
2520
  "license": "MIT"
2403
2521
  },
2522
+ "node_modules/node-html-markdown": {
2523
+ "version": "1.3.0",
2524
+ "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-1.3.0.tgz",
2525
+ "integrity": "sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==",
2526
+ "license": "MIT",
2527
+ "dependencies": {
2528
+ "node-html-parser": "^6.1.1"
2529
+ },
2530
+ "engines": {
2531
+ "node": ">=10.0.0"
2532
+ }
2533
+ },
2534
+ "node_modules/node-html-parser": {
2535
+ "version": "6.1.13",
2536
+ "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
2537
+ "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
2538
+ "license": "MIT",
2539
+ "dependencies": {
2540
+ "css-select": "^5.1.0",
2541
+ "he": "1.2.0"
2542
+ }
2543
+ },
2404
2544
  "node_modules/node-pty": {
2405
2545
  "version": "1.1.0",
2406
2546
  "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
@@ -2420,6 +2560,18 @@
2420
2560
  "node": ">=6.0.0"
2421
2561
  }
2422
2562
  },
2563
+ "node_modules/nth-check": {
2564
+ "version": "2.1.1",
2565
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
2566
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
2567
+ "license": "BSD-2-Clause",
2568
+ "dependencies": {
2569
+ "boolbase": "^1.0.0"
2570
+ },
2571
+ "funding": {
2572
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
2573
+ }
2574
+ },
2423
2575
  "node_modules/object-assign": {
2424
2576
  "version": "4.1.1",
2425
2577
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3673,6 +3825,18 @@
3673
3825
  "vitest": "^4.1.2"
3674
3826
  }
3675
3827
  },
3828
+ "plugins/browser/mcp": {
3829
+ "name": "@maxy/browser",
3830
+ "version": "0.1.0",
3831
+ "dependencies": {
3832
+ "@modelcontextprotocol/sdk": "^1.12.1",
3833
+ "zod": "^3.24.0"
3834
+ },
3835
+ "devDependencies": {
3836
+ "@types/node": "^22.0.0",
3837
+ "typescript": "^5.7.0"
3838
+ }
3839
+ },
3676
3840
  "plugins/cloudflare/mcp": {
3677
3841
  "name": "@maxy/cloudflare",
3678
3842
  "version": "0.1.0",
@@ -3809,6 +3973,20 @@
3809
3973
  "typescript": "^5.7.0"
3810
3974
  }
3811
3975
  },
3976
+ "plugins/url-get/mcp": {
3977
+ "name": "@maxy/url-get",
3978
+ "version": "0.1.0",
3979
+ "dependencies": {
3980
+ "@modelcontextprotocol/sdk": "^1.12.1",
3981
+ "node-html-markdown": "^1.3.0",
3982
+ "zod": "^3.23.0"
3983
+ },
3984
+ "devDependencies": {
3985
+ "@types/node": "^22.0.0",
3986
+ "typescript": "^5.7.0",
3987
+ "vitest": "^4.1.2"
3988
+ }
3989
+ },
3812
3990
  "plugins/waitlist/mcp": {
3813
3991
  "name": "@maxy/waitlist",
3814
3992
  "version": "0.1.0",
@@ -14,6 +14,11 @@
14
14
  "source": "./aeo",
15
15
  "version": "0.1.0"
16
16
  },
17
+ {
18
+ "name": "browser",
19
+ "source": "./browser",
20
+ "version": "0.1.0"
21
+ },
17
22
  {
18
23
  "name": "business-assistant",
19
24
  "source": "./business-assistant",
@@ -84,6 +89,11 @@
84
89
  "source": "./projects",
85
90
  "version": "0.1.0"
86
91
  },
92
+ {
93
+ "name": "prompt-optimiser",
94
+ "source": "./prompt-optimiser",
95
+ "version": "0.1.0"
96
+ },
87
97
  {
88
98
  "name": "replicate",
89
99
  "source": "./replicate",
@@ -109,6 +119,11 @@
109
119
  "source": "./telegram",
110
120
  "version": "0.1.0"
111
121
  },
122
+ {
123
+ "name": "url-get",
124
+ "source": "./url-get",
125
+ "version": "0.1.0"
126
+ },
112
127
  {
113
128
  "name": "whatsapp",
114
129
  "source": "./whatsapp",
@@ -136,7 +136,6 @@ Tools are available via the `admin` MCP server.
136
136
  ## Hooks
137
137
 
138
138
  - `hooks/pre-tool-use.sh` — enforces admin agent write boundaries
139
- - `hooks/playwright-file-guard.sh` — rewrites file:// URLs to a backgrounded loopback http.server before Playwright sees them
140
139
  - `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
141
140
  - `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
142
141
  - `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "browser",
3
+ "description": "Headless browser rendering for JavaScript-heavy pages. Provides browser-render, which drives the device's existing Chromium over the Chrome DevTools Protocol and returns a page's fully rendered DOM (HTML + visible text). This is the JS-rendering leg of the three-tool retrieval story: WebFetch (lossy summary) / url-get (verbatim, server-rendered) / browser-render (JS-rendered). Use it for client-rendered SPAs and pages that WebFetch or url-get return empty or as a shell.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "browser": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: browser
3
+ description: "Headless browser rendering for JavaScript-heavy pages. Provides browser-render, which drives the device's existing Chromium over the Chrome DevTools Protocol and returns a page's fully rendered DOM (HTML + visible text). This is the JS-rendering leg of the three-tool retrieval story: WebFetch (lossy summary) / url-get (verbatim, server-rendered) / browser-render (JS-rendered). Use it for client-rendered SPAs and pages that WebFetch or url-get return empty or as a shell."
4
+ tools:
5
+ - name: browser-render
6
+ publicAllowlist: false
7
+ adminAllowlist: true
8
+ always: true
9
+ embed: false
10
+ metadata: {"platform":{}}
11
+ mcp:
12
+ command: node
13
+ args:
14
+ - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
15
+ - ${PLATFORM_ROOT}/plugins/browser/mcp/dist/index.js
16
+ env:
17
+ MCP_SPAWN_TEE_NAME: browser
18
+ LOG_DIR: ${LOG_DIR}
19
+ PLATFORM_ROOT: ${PLATFORM_ROOT}
20
+ CDP_PORT: ${CDP_PORT}
21
+ mcp-manifest: auto
22
+ ---
23
+
24
+ # Browser
25
+
26
+ Renders JavaScript-heavy web pages and returns the rendered DOM, so the agent can read content that only exists after the page's scripts run.
27
+
28
+ ## Why it exists
29
+
30
+ `WebFetch` summarises and refuses verbatim output; `url-get` returns the verbatim source but does not execute JavaScript. Client-rendered pages (SPAs, content injected by in-page scripts) come back from both as an empty shell. `browser-render` is the only retrieval path that runs the page's JavaScript and reads the resulting DOM.
31
+
32
+ ## How it works
33
+
34
+ The device already runs a per-brand Chromium for the VNC surface, launched by `platform/scripts/vnc.sh` with `--remote-debugging-port=${CDP_PORT}`. `browser-render` attaches to that running browser over the Chrome DevTools Protocol — it never launches its own browser, so nothing extra ships on device and there is no on-demand download.
35
+
36
+ For each call it opens a fresh background target, navigates to the URL, waits for the page load event, reads `document.documentElement.outerHTML` and the visible `body.innerText`, then closes the target. The brand's CDP port reaches this server through the `CDP_PORT` spawn placeholder.
37
+
38
+ ## Tool
39
+
40
+ - `browser-render` — input: `url` (absolute http(s)), optional `loadTimeoutMs`. Returns the rendered HTML and visible text, or a structured failure (`cdp-unreachable`, `navigate-failed`, `load-timeout`, `evaluate-failed`, `websocket-unavailable`). Admin-allowlisted; not exposed to public chat.
41
+
42
+ ## Observability
43
+
44
+ A successful render emits `[browser-render] url=… rendered=true domBytes=…` on the server's stderr (captured by the mcp-spawn-tee log). Failures emit `rendered=false outcome=… detail=…`. On a device with no running Chromium (e.g. a laptop native-display install with no VNC browser) the tool returns `cdp-unreachable` rather than hanging.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
2
+ initStderrTee("browser");
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
6
+ import { z } from "zod";
7
+ import { renderPage } from "./lib/cdp-render.js";
8
+ // Cast to `any` to avoid TS2589 the same way the other plugin servers do.
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const server = new McpServer({ name: "maxy-browser", version: "0.1.0" });
11
+ // Module-init must never throw — plugin-system spawn enumerates tools/list
12
+ // with empty env (Task 202). The CDP port is read per-call, not at boot.
13
+ const cdpPortRaw = process.env.CDP_PORT ?? "";
14
+ const cdpPort = Number.parseInt(cdpPortRaw, 10);
15
+ process.stderr.write(`[browser] boot cdpPort=${cdpPortRaw || "-"}\n`);
16
+ const log = (line) => process.stderr.write(`${line}\n`);
17
+ // Cap what we hand back to the model so a large SPA cannot blow the agent's
18
+ // context window. domBytes in the header always reports the true size.
19
+ const MAX_TEXT_CHARS = 200_000;
20
+ const MAX_HTML_CHARS = 800_000;
21
+ function cap(s, max) {
22
+ if (s.length <= max)
23
+ return { body: s, truncated: false };
24
+ return { body: s.slice(0, max), truncated: true };
25
+ }
26
+ eagerTool(server, "browser-render", "Render a JavaScript-heavy web page in the device's real Chromium and return its fully rendered DOM. Use this for pages that WebFetch and url-get return empty or as a shell because they require JS execution (client-rendered SPAs, content gated behind in-page scripts). Returns the rendered HTML and the page's visible text. For static, server-rendered pages prefer url-get (verbatim) or WebFetch (summary) — this tool is the heavier JS-rendering path.", {
27
+ url: z.string().url().describe("Absolute http(s) URL to render"),
28
+ loadTimeoutMs: z.number().int().positive().optional().describe("Max wait for the page load event (default 30000)"),
29
+ }, async (params) => {
30
+ const result = await renderPage(params.url, { loadTimeoutMs: params.loadTimeoutMs }, {
31
+ fetchImpl: fetch,
32
+ WebSocketImpl: typeof WebSocket !== "undefined" ? WebSocket : undefined,
33
+ cdpHost: "127.0.0.1",
34
+ cdpPort,
35
+ log,
36
+ });
37
+ if (!result.rendered) {
38
+ return {
39
+ content: [{
40
+ type: "text",
41
+ text: `browser-render failed: ${result.outcome}${result.detail ? ` — ${result.detail}` : ""} (url: ${result.url})`,
42
+ }],
43
+ isError: true,
44
+ };
45
+ }
46
+ const text = cap(result.text, MAX_TEXT_CHARS);
47
+ const html = cap(result.html, MAX_HTML_CHARS);
48
+ const note = text.truncated || html.truncated
49
+ ? ` Output truncated for length (full DOM was ${result.domBytes} bytes); re-fetch a specific section if you need more.`
50
+ : "";
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `Rendered ${result.url} (${result.domBytes} bytes of DOM).${note}\n\n--- VISIBLE TEXT${text.truncated ? " (truncated)" : ""} ---\n${text.body}\n\n--- RENDERED HTML${html.truncated ? " (truncated)" : ""} ---\n${html.body}`,
55
+ }],
56
+ };
57
+ });
58
+ process.on("SIGINT", () => process.exit(0));
59
+ const transport = new StdioServerTransport();
60
+ await server.connect(transport);
61
+ //# 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,66 @@
1
+ /**
2
+ * Render a JS-heavy page in the device's existing Chromium and return its DOM.
3
+ *
4
+ * The brand's VNC Chromium is launched by platform/scripts/vnc.sh with
5
+ * `--remote-debugging-port=${CDP_PORT}`, so CDP is already listening on
6
+ * 127.0.0.1:<port>. This module attaches to that running browser — it never
7
+ * launches its own — which is why no external browser package ships on device.
8
+ *
9
+ * Lifecycle (HTTP for target create/close, WebSocket for page control):
10
+ *
11
+ * PUT /json/new create a blank target, get its WS URL
12
+ * │
13
+ * ▼
14
+ * ws://…/devtools/page/<id> Page.enable → Page.navigate(url)
15
+ * │ await Page.loadEventFired (load-timeout)
16
+ * ▼
17
+ * Runtime.evaluate outerHTML + body.innerText (returnByValue)
18
+ * │
19
+ * ▼
20
+ * PUT /json/close/<id> always, even on the error paths above
21
+ *
22
+ * `fetch` and `WebSocket` are injected so the unit tests drive every branch
23
+ * without a real browser; production passes the Node 22 globals.
24
+ */
25
+ export type RenderOutcome = "ok" | "cdp-unreachable" | "websocket-unavailable" | "navigate-failed" | "load-timeout" | "evaluate-failed" | "error";
26
+ export interface RenderResult {
27
+ url: string;
28
+ rendered: boolean;
29
+ outcome: RenderOutcome;
30
+ domBytes: number;
31
+ text: string;
32
+ html: string;
33
+ detail?: string;
34
+ }
35
+ /** Minimal structural shape of the browser-style WebSocket this module uses.
36
+ * Both the Node 22 global and the test fake satisfy it. */
37
+ export interface MinimalWebSocket {
38
+ send(data: string): void;
39
+ close(): void;
40
+ addEventListener(type: "open", listener: () => void): void;
41
+ addEventListener(type: "message", listener: (ev: {
42
+ data: unknown;
43
+ }) => void): void;
44
+ addEventListener(type: "error", listener: (ev: unknown) => void): void;
45
+ addEventListener(type: "close", listener: () => void): void;
46
+ }
47
+ export type WebSocketCtor = new (url: string) => MinimalWebSocket;
48
+ export interface RenderDeps {
49
+ fetchImpl: typeof fetch;
50
+ /** The Node 22 global `WebSocket`, or undefined on a runtime that lacks it. */
51
+ WebSocketImpl: WebSocketCtor | undefined;
52
+ cdpHost: string;
53
+ cdpPort: number;
54
+ log: (line: string) => void;
55
+ }
56
+ export interface RenderOptions {
57
+ /** Cap on the whole navigate→load wait. */
58
+ loadTimeoutMs?: number;
59
+ /** Cap on each CDP command round-trip (enable, navigate, evaluate). */
60
+ commandTimeoutMs?: number;
61
+ }
62
+ /** Render `url` in the device Chromium and return its rendered DOM text.
63
+ * Always returns a structured RenderResult — never throws, never returns a
64
+ * silent empty page. */
65
+ export declare function renderPage(url: string, opts: RenderOptions, deps: RenderDeps): Promise<RenderResult>;
66
+ //# sourceMappingURL=cdp-render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp-render.d.ts","sourceRoot":"","sources":["../../src/lib/cdp-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,aAAa,GACrB,IAAI,GACJ,iBAAiB,GACjB,uBAAuB,GACvB,iBAAiB,GACjB,cAAc,GACd,iBAAiB,GACjB,OAAO,CAAC;AAEZ,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;4DAC4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC3D,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IACnF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IACvE,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAC7D;AAED,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,EAAE,MAAM,KAAK,gBAAgB,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,+EAA+E;IAC/E,aAAa,EAAE,aAAa,GAAG,SAAS,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAgKD;;yBAEyB;AACzB,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,YAAY,CAAC,CAqEvB"}