@rubytech/create-maxy-code 0.1.184 → 0.1.186

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 (66) 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/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +70 -0
  10. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +52 -0
  11. package/payload/platform/plugins/browser/.claude-plugin/plugin.json +17 -0
  12. package/payload/platform/plugins/browser/PLUGIN.md +44 -0
  13. package/payload/platform/plugins/browser/mcp/dist/index.d.ts +2 -0
  14. package/payload/platform/plugins/browser/mcp/dist/index.d.ts.map +1 -0
  15. package/payload/platform/plugins/browser/mcp/dist/index.js +61 -0
  16. package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -0
  17. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts +66 -0
  18. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.d.ts.map +1 -0
  19. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js +248 -0
  20. package/payload/platform/plugins/browser/mcp/dist/lib/cdp-render.js.map +1 -0
  21. package/payload/platform/plugins/browser/mcp/package.json +19 -0
  22. package/payload/platform/plugins/docs/references/internals.md +1 -1
  23. package/payload/platform/plugins/docs/references/plugins-guide.md +3 -0
  24. package/payload/platform/plugins/docs/references/troubleshooting.md +4 -4
  25. package/payload/platform/plugins/prompt-optimiser/.claude-plugin/plugin.json +8 -0
  26. package/payload/platform/plugins/prompt-optimiser/PLUGIN.md +14 -0
  27. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +316 -0
  28. package/payload/platform/plugins/url-get/.claude-plugin/plugin.json +17 -0
  29. package/payload/platform/plugins/url-get/PLUGIN.md +86 -0
  30. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts +9 -0
  31. package/payload/platform/plugins/url-get/mcp/dist/index.d.ts.map +1 -0
  32. package/payload/platform/plugins/url-get/mcp/dist/index.js +53 -0
  33. package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -0
  34. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts +8 -0
  35. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.d.ts.map +1 -0
  36. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js +83 -0
  37. package/payload/platform/plugins/url-get/mcp/dist/lib/summarise.js.map +1 -0
  38. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts +21 -0
  39. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.d.ts.map +1 -0
  40. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js +147 -0
  41. package/payload/platform/plugins/url-get/mcp/dist/tools/url-get.js.map +1 -0
  42. package/payload/platform/plugins/url-get/mcp/package.json +22 -0
  43. package/payload/platform/scripts/setup-account.sh +2 -7
  44. package/payload/platform/scripts/vnc.sh +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +8 -0
  47. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +2 -2
  49. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
  50. package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
  51. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  52. package/payload/platform/templates/specialists/agents/personal-assistant.md +3 -3
  53. package/payload/platform/templates/specialists/agents/research-assistant.md +3 -3
  54. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
  55. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +20 -6
  56. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  57. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +14 -0
  58. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  59. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +28 -34
  60. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  61. package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +43 -8
  62. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +20 -8
  63. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +51 -12
  64. package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +7 -1
  65. package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +0 -278
  66. 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.186",
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,70 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for prompt-optimiser-directive.sh.
3
+ #
4
+ # Covers:
5
+ # 1. Valid UserPromptSubmit stdin -> exit 0, stdout is valid JSON,
6
+ # hookEventName == "UserPromptSubmit", additionalContext non-empty.
7
+ # 2. Empty / garbage stdin -> exit 0, still valid JSON (the hook
8
+ # ignores the prompt text; the directive is standing).
9
+ # 3. python3 unavailable on PATH -> exit 0, empty stdout (fail-open).
10
+
11
+ set -u
12
+
13
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/prompt-optimiser-directive.sh"
14
+ if [[ ! -x "$HOOK" ]]; then
15
+ echo "FAIL: $HOOK not executable" >&2
16
+ exit 1
17
+ fi
18
+
19
+ PASS=0
20
+ FAIL=0
21
+
22
+ # Assert: hook on $1 (stdin) exits 0 and stdout is JSON with a non-empty
23
+ # additionalContext and the correct hookEventName.
24
+ assert_valid_injection() {
25
+ local name="$1" stdin="$2" out actual_exit
26
+ out=$(printf '%s' "$stdin" | bash "$HOOK" 2>/dev/null)
27
+ actual_exit=$?
28
+ if [[ "$actual_exit" -ne 0 ]]; then
29
+ echo "FAIL: $name (expected exit=0, got=$actual_exit)" >&2; FAIL=$((FAIL+1)); return
30
+ fi
31
+ if printf '%s' "$out" | python3 -c '
32
+ import json, sys
33
+ d = json.load(sys.stdin)
34
+ h = d["hookSpecificOutput"]
35
+ assert h["hookEventName"] == "UserPromptSubmit", "wrong hookEventName"
36
+ assert isinstance(h["additionalContext"], str) and len(h["additionalContext"]) > 0, "empty additionalContext"
37
+ ' 2>/dev/null; then
38
+ echo "PASS: $name"; PASS=$((PASS+1))
39
+ else
40
+ echo "FAIL: $name (stdout not valid directive JSON: $out)" >&2; FAIL=$((FAIL+1))
41
+ fi
42
+ }
43
+
44
+ # Case 1 — valid UserPromptSubmit envelope.
45
+ assert_valid_injection "valid UserPromptSubmit stdin -> directive JSON" \
46
+ '{"hook_event_name":"UserPromptSubmit","prompt":"fix the thing","session_id":"abc"}'
47
+
48
+ # Case 2 — garbage stdin still yields the standing directive.
49
+ assert_valid_injection "garbage stdin -> still directive JSON" \
50
+ 'not json at all'
51
+
52
+ # Case 3 — fail-open when python3 is absent from PATH.
53
+ FAKEBIN=$(mktemp -d)
54
+ # A minimal PATH containing the core utils the hook needs (cat, printf, env,
55
+ # command) but NOT python3. Resolve their real dir(s) and symlink them in.
56
+ for util in cat printf env bash; do
57
+ src=$(command -v "$util" 2>/dev/null) && ln -sf "$src" "$FAKEBIN/$util" 2>/dev/null
58
+ done
59
+ nopy_out=$(printf '%s' '{"prompt":"x"}' | PATH="$FAKEBIN" bash "$HOOK" 2>/dev/null)
60
+ nopy_exit=$?
61
+ if [[ "$nopy_exit" -eq 0 && -z "$nopy_out" ]]; then
62
+ echo "PASS: python3 absent -> exit 0, empty stdout (fail-open)"; PASS=$((PASS+1))
63
+ else
64
+ echo "FAIL: python3 absent (expected exit=0 + empty stdout, got exit=$nopy_exit stdout=$nopy_out)" >&2; FAIL=$((FAIL+1))
65
+ fi
66
+ rm -rf "$FAKEBIN"
67
+
68
+ echo "----"
69
+ echo "PASS=$PASS FAIL=$FAIL"
70
+ [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit hook: inject the standing prompt-optimiser directive as
3
+ # additionalContext on every turn, so the agent restates each non-trivial
4
+ # prompt via the prompt-optimiser skill before acting on it. Mirrors
5
+ # doctrine.sh: emit hookSpecificOutput.additionalContext via python3,
6
+ # fail-open with exit 0.
7
+ #
8
+ # Compliance is behavioural, not deterministic. A hook cannot force a skill
9
+ # call; the directive steers the agent, it does not gate the turn. The raw
10
+ # prompt always still reaches the model (UserPromptSubmit output is ADDED to
11
+ # the prompt, never substituted), so "the restatement governs" is an
12
+ # instruction to the agent, not a removal of the original text.
13
+ #
14
+ # Input (stdin, UserPromptSubmit contract): { "prompt": "...", ... } — drained
15
+ # and ignored; the directive is standing and prompt-independent.
16
+ # Output (stdout JSON):
17
+ # { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
18
+ # "additionalContext": "<directive>" } }
19
+ #
20
+ # Fail-open: if python3 is unavailable or JSON encoding fails, exit 0 with no
21
+ # stdout. The hook must never block or corrupt a turn.
22
+
23
+ set -uo pipefail
24
+
25
+ # Drain stdin; the directive does not depend on the prompt text.
26
+ cat >/dev/null 2>&1 || true
27
+
28
+ # Fail-open if the JSON encoder is unavailable.
29
+ command -v python3 >/dev/null 2>&1 || exit 0
30
+
31
+ DIRECTIVE=$(cat <<'DIRECTIVE_EOF'
32
+ PROMPT-OPTIMISER DIRECTIVE (standing)
33
+ Before acting on this turn's request, run the prompt-optimiser skill on the user's raw prompt in its in-session restatement mode, then treat the restated task as the authoritative statement of what was asked and proceed from it. Set aside your own first reading of the raw wording; the restatement governs. The raw prompt stays in context for reference only.
34
+ Skip the restatement when the prompt is a one-word confirmation, a slash-command invocation, or a direct continuation of the immediately preceding turn; restatement adds nothing in those cases.
35
+ DIRECTIVE_EOF
36
+ )
37
+
38
+ OUT=$(printf '%s' "$DIRECTIVE" | python3 -c '
39
+ import json, sys
40
+ ctx = sys.stdin.read()
41
+ print(json.dumps({
42
+ "hookSpecificOutput": {
43
+ "hookEventName": "UserPromptSubmit",
44
+ "additionalContext": ctx,
45
+ }
46
+ }))
47
+ ' 2>/dev/null) || exit 0
48
+
49
+ [ -n "$OUT" ] || exit 0
50
+ printf '%s\n' "$OUT"
51
+ echo "[prompt-optimiser-directive] injected len=${#DIRECTIVE}" >&2
52
+ exit 0
@@ -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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"}