@rubytech/create-realagent 1.0.852 → 1.0.854

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/dist/__tests__/preflight-port-classifier.test.js +240 -73
  2. package/dist/index.js +59 -11
  3. package/dist/preflight-port-classifier.js +176 -41
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand-registry.json +44 -0
  6. package/payload/platform/lib/persistent-components/dist/index.d.ts +21 -0
  7. package/payload/platform/lib/persistent-components/dist/index.d.ts.map +1 -0
  8. package/payload/platform/lib/persistent-components/dist/index.js +32 -0
  9. package/payload/platform/lib/persistent-components/dist/index.js.map +1 -0
  10. package/payload/platform/lib/persistent-components/src/index.ts +28 -0
  11. package/payload/platform/lib/persistent-components/tsconfig.json +8 -0
  12. package/payload/platform/package.json +2 -2
  13. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  14. package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +278 -0
  15. package/payload/platform/plugins/admin/hooks/playwright-file-guard.sh +204 -20
  16. package/payload/platform/plugins/admin/mcp/dist/index.js +40 -1
  17. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/docs/references/deployment.md +2 -0
  19. package/payload/platform/plugins/docs/references/getting-started.md +2 -0
  20. package/payload/platform/plugins/docs/references/platform.md +1 -1
  21. package/payload/platform/plugins/docs/references/troubleshooting.md +10 -0
  22. package/payload/platform/scripts/admin-persist-audit.ts +191 -0
  23. package/payload/platform/scripts/component-knowledgedoc-backfill.ts +214 -0
  24. package/payload/platform/scripts/installer-device-verify.sh +249 -0
  25. package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
  26. package/payload/server/chunk-CFNSKDGA.js +667 -0
  27. package/payload/server/chunk-DC6DWYZJ.js +1603 -0
  28. package/payload/server/chunk-LTB5SSQW.js +10889 -0
  29. package/payload/server/chunk-MN2LGNUB.js +2143 -0
  30. package/payload/server/client-pool-AMT2W3II.js +34 -0
  31. package/payload/server/cloudflare-task-tracker-LJ4SMK2D.js +20 -0
  32. package/payload/server/maxy-edge.js +3 -3
  33. package/payload/server/public/assets/admin-DZ8Ke7t3.js +352 -0
  34. package/payload/server/public/assets/public-DApUXgoq.js +5 -0
  35. package/payload/server/public/assets/useVoiceRecorder-CI8GpxfU.js +36 -0
  36. package/payload/server/public/index.html +2 -2
  37. package/payload/server/public/public.html +2 -2
  38. package/payload/server/server.js +535 -351
  39. package/payload/server/public/assets/admin-Dyl8uNxX.js +0 -352
  40. package/payload/server/public/assets/public-B_PNZUph.js +0 -5
  41. package/payload/server/public/assets/useVoiceRecorder-fD0IWzJj.js +0 -36
@@ -1,32 +1,39 @@
1
1
  // Task 938 — pure classifier for the install-time port collision pre-flight.
2
- // Extracted from index.ts so the OWN_BRAND / PEER_BRAND / UNRELATED decision
3
- // can be unit-tested without ss(8), /proc, or kill(2). Mirrors the
4
- // peer-brand-detect.ts pattern: inputs in, classification out, no I/O.
2
+ // Task 939 extended with Xtigervnc and websockify holder anchors, closing
3
+ // the laptop + Pi miss where the brand's own VNC stack was misclassified
4
+ // UNRELATED because Xtigervnc has no `--user-data-dir=` argv to anchor on.
5
5
  //
6
6
  // The wrapper in index.ts owns the side effects: ss read, /proc/<pid>/cmdline
7
7
  // read, SIGTERM/SIGKILL escalation, and the operator-override exit. This
8
- // module owns only the classification rule.
8
+ // module owns only the classification rule — inputs in, decision out.
9
9
  //
10
- // Rule, in order of precedence:
11
- // 1. ssOutput is empty EMPTY
12
- // 2. No `pid=N` token in ssOutputUNRELATED (pid undef)
13
- // 3. getCmdline(pid) throws UNRELATED (cmdlineReadFailed)
14
- // 4. argv has --user-data-dir=PATH where PATH contains
15
- // `/<ownConfigDir>/chromium-profile` OWN_BRAND
16
- // 5. same for any `<peerConfigDir>` → PEER_BRAND
17
- // 6. otherwise UNRELATED
10
+ // Holder detection (post-pid):
11
+ // argv[0] basename chromium-set chromium
12
+ // argv[0] basename === "Xtigervnc"xtigervnc
13
+ // any argv basename === "websockify" (full scan websockify
14
+ // because Pi Bookworm invokes it as
15
+ // `python3 /usr/bin/websockify …`, so argv[0] is
16
+ // the interpreter, not websockify itself)
17
+ // else unknown
18
18
  //
19
- // Why argv parsing instead of substring matching: `cmdline.includes('/.maxy/
20
- // chromium-profile')` would false-match `--load-extension=/tmp/.maxy/chromium-
21
- // profile` (or any other flag whose value happens to contain the profile path
22
- // component). Anchoring on `--user-data-dir=` is the only signal that this
23
- // process actually owns that profile directory.
19
+ // Per-holder anchor:
20
+ // chromium — `--user-data-dir=PATH`; PATH contains `/<configDir>/chromium-profile`
21
+ // OWN_BRAND if configDir matches own; PEER_BRAND if matches peer.
22
+ // False-match guard: argv-anchor on the flag, never substring,
23
+ // so `--load-extension=…/<configDir>/…` cannot poison.
24
+ // xtigervnc — first non-flag argv of the form `:N` (display number);
25
+ // vnc.sh invokes `Xtigervnc "${VNC_DISPLAY}" -rfbport …` so
26
+ // argv[1] is literally `:99`, `:100`, etc. Match N against
27
+ // own.vncDisplay / peer.vncDisplay.
28
+ // websockify — collect every port number embedded in argv (covers both
29
+ // `[::]:6080` bind form and `localhost:5900` upstream form).
30
+ // Per Task 924, brand websockifyPort/rfbPort sets are
31
+ // disjoint, so at most one brand's websockifyPort can appear.
24
32
  //
25
33
  // Why "last pid=" instead of "first pid=": ss with `-tlnpH sport = :PORT`
26
- // emits the LISTEN row last; if a future ss locale or release prepends a
27
- // header line that contains `pid=` text, the first-match heuristic would
28
- // lift the wrong pid. The last `pid=` is always inside the LISTEN row's
29
- // `users:((…))` segment.
34
+ // emits the LISTEN row last; if a future ss locale prepends a header line
35
+ // containing `pid=` text, the first-match heuristic would lift the wrong
36
+ // pid. The last `pid=` is always inside the LISTEN row's `users:((…))`.
30
37
  /**
31
38
  * `cmdline` should be the raw `/proc/<pid>/cmdline` contents — NUL-separated
32
39
  * argv as the kernel emits it. Do NOT replace NUL with space before passing:
@@ -34,11 +41,9 @@
34
41
  * different flag whose value happens to contain `--user-data-dir=`.
35
42
  */
36
43
  export function classifyPortHolder(args) {
37
- const { ssOutput, ownConfigDir, peerConfigDirs, getCmdline } = args;
44
+ const { ssOutput, ownBrand, peerBrands, getCmdline } = args;
38
45
  if (ssOutput.trim() === "")
39
46
  return { kind: "EMPTY" };
40
- // Take the LAST pid= match. ss -tlnpH puts the LISTEN row (which contains
41
- // `users:((…,pid=N,fd=…))`) last, so the last pid= is always the listener.
42
47
  const pidMatches = [...ssOutput.matchAll(/pid=(\d+)/g)];
43
48
  if (pidMatches.length === 0)
44
49
  return { kind: "UNRELATED" };
@@ -50,38 +55,168 @@ export function classifyPortHolder(args) {
50
55
  catch {
51
56
  return { kind: "UNRELATED", pid, cmdlineReadFailed: true };
52
57
  }
53
- // Pretty cmdline (NULs → spaces) for log output. The argv-aware matching
54
- // operates on the raw NUL-separated form.
55
58
  const prettyCmdline = cmdline.replace(/\0/g, " ").trim();
56
59
  const argv = cmdline.split("\0").filter(s => s.length > 0);
57
- const userDataDir = findUserDataDir(argv);
60
+ const holderType = detectHolderType(argv);
61
+ switch (holderType) {
62
+ case "chromium":
63
+ return classifyChromium(argv, prettyCmdline, pid, ownBrand, peerBrands);
64
+ case "xtigervnc":
65
+ return classifyXtigervnc(argv, prettyCmdline, pid, ownBrand, peerBrands);
66
+ case "websockify":
67
+ return classifyWebsockify(argv, prettyCmdline, pid, ownBrand, peerBrands);
68
+ case "unknown":
69
+ return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType };
70
+ }
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Holder detection
74
+ // ---------------------------------------------------------------------------
75
+ const CHROMIUM_BASENAMES = new Set([
76
+ "chrome",
77
+ "chromium",
78
+ "chromium-browser",
79
+ "google-chrome",
80
+ "google-chrome-stable",
81
+ ]);
82
+ function basename(p) {
83
+ const i = p.lastIndexOf("/");
84
+ return i === -1 ? p : p.slice(i + 1);
85
+ }
86
+ function detectHolderType(argv) {
87
+ if (argv.length === 0)
88
+ return "unknown";
89
+ const head = basename(argv[0]);
90
+ if (CHROMIUM_BASENAMES.has(head))
91
+ return "chromium";
92
+ if (head === "Xtigervnc")
93
+ return "xtigervnc";
94
+ // websockify can be invoked directly (shebang) or via python; scan argv.
95
+ for (const a of argv) {
96
+ if (basename(a) === "websockify")
97
+ return "websockify";
98
+ }
99
+ return "unknown";
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Per-holder anchors
103
+ // ---------------------------------------------------------------------------
104
+ function classifyChromium(argv, prettyCmdline, pid, ownBrand, peerBrands) {
105
+ const userDataDir = findFlagValue(argv, "--user-data-dir");
58
106
  if (userDataDir === null) {
59
- return { kind: "UNRELATED", pid, cmdline: prettyCmdline };
107
+ // Kernel threads, GPU/zygote/utility chrome processes inherit profile
108
+ // via fork without re-stating --user-data-dir. They wouldn't be
109
+ // listening anyway, but classify UNRELATED if one ever shows up here
110
+ // — never OWN_BRAND on a substring fluke.
111
+ return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "chromium" };
60
112
  }
61
- const ownSuffix = `/${ownConfigDir}/chromium-profile`;
113
+ const ownSuffix = `/${ownBrand.configDir}/chromium-profile`;
62
114
  if (userDataDir.includes(ownSuffix)) {
63
- return { kind: "OWN_BRAND", pid, cmdline: prettyCmdline, profilePath: userDataDir };
115
+ return {
116
+ kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
117
+ profilePath: userDataDir, holderType: "chromium",
118
+ };
64
119
  }
65
- for (const peerCD of peerConfigDirs) {
66
- const peerSuffix = `/${peerCD}/chromium-profile`;
120
+ for (const peer of peerBrands) {
121
+ const peerSuffix = `/${peer.configDir}/chromium-profile`;
67
122
  if (userDataDir.includes(peerSuffix)) {
68
- return { kind: "PEER_BRAND", pid, cmdline: prettyCmdline, profilePath: userDataDir };
123
+ return {
124
+ kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
125
+ profilePath: userDataDir, holderType: "chromium",
126
+ };
127
+ }
128
+ }
129
+ return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "chromium" };
130
+ }
131
+ function classifyXtigervnc(argv, prettyCmdline, pid, ownBrand, peerBrands) {
132
+ const display = parseDisplayArg(argv);
133
+ if (display === null) {
134
+ return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "xtigervnc" };
135
+ }
136
+ if (display === ownBrand.vncDisplay) {
137
+ return {
138
+ kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
139
+ vncDisplay: display, holderType: "xtigervnc",
140
+ };
141
+ }
142
+ for (const peer of peerBrands) {
143
+ if (display === peer.vncDisplay) {
144
+ return {
145
+ kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
146
+ vncDisplay: display, holderType: "xtigervnc",
147
+ };
69
148
  }
70
149
  }
71
- return { kind: "UNRELATED", pid, cmdline: prettyCmdline };
150
+ return {
151
+ kind: "UNRELATED", pid, cmdline: prettyCmdline,
152
+ vncDisplay: display, holderType: "xtigervnc",
153
+ };
72
154
  }
73
- // Find the value of `--user-data-dir`, supporting both `--user-data-dir=PATH`
74
- // (single argv) and `--user-data-dir PATH` (split across two argvs). Returns
75
- // null if the flag is absent.
76
- function findUserDataDir(argv) {
77
- const FLAG = "--user-data-dir";
78
- const PREFIX = `${FLAG}=`;
155
+ function classifyWebsockify(argv, prettyCmdline, pid, ownBrand, peerBrands) {
156
+ const ports = collectPorts(argv);
157
+ if (ports.has(ownBrand.websockifyPort)) {
158
+ return {
159
+ kind: "OWN_BRAND", pid, cmdline: prettyCmdline,
160
+ websockifyPort: ownBrand.websockifyPort, holderType: "websockify",
161
+ };
162
+ }
163
+ for (const peer of peerBrands) {
164
+ if (ports.has(peer.websockifyPort)) {
165
+ return {
166
+ kind: "PEER_BRAND", pid, cmdline: prettyCmdline,
167
+ websockifyPort: peer.websockifyPort, holderType: "websockify",
168
+ };
169
+ }
170
+ }
171
+ return { kind: "UNRELATED", pid, cmdline: prettyCmdline, holderType: "websockify" };
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ // argv parsing helpers
175
+ // ---------------------------------------------------------------------------
176
+ // Find --flag=VALUE (single argv) or --flag VALUE (split across two argvs).
177
+ function findFlagValue(argv, flag) {
178
+ const PREFIX = `${flag}=`;
79
179
  for (let i = 0; i < argv.length; i++) {
80
180
  const a = argv[i];
81
181
  if (a.startsWith(PREFIX))
82
182
  return a.slice(PREFIX.length);
83
- if (a === FLAG && i + 1 < argv.length)
183
+ if (a === flag && i + 1 < argv.length)
84
184
  return argv[i + 1];
85
185
  }
86
186
  return null;
87
187
  }
188
+ // First non-flag argv of the form `:N` after argv[0]. vnc.sh's invocation
189
+ // puts the display in argv[1], but the function tolerates additional
190
+ // pre-positional flags by scanning past any `-`-prefixed token.
191
+ function parseDisplayArg(argv) {
192
+ for (let i = 1; i < argv.length; i++) {
193
+ const a = argv[i];
194
+ if (a.startsWith("-"))
195
+ continue;
196
+ const m = a.match(/^:(\d+)$/);
197
+ if (m)
198
+ return Number(m[1]);
199
+ }
200
+ return null;
201
+ }
202
+ // Collect every port number embedded in argv. Matches:
203
+ // "[::]:6080" → 6080 (websockify bind, IPv6 wildcard)
204
+ // "0.0.0.0:8080" → 8080 (bind, IPv4 wildcard)
205
+ // "localhost:5900" → 5900 (websockify upstream)
206
+ // "6080" → 6080 (bare positional)
207
+ // Skips flag tokens beginning with "-".
208
+ function collectPorts(argv) {
209
+ const ports = new Set();
210
+ const portRe = /(?::|^)(\d{1,5})$/;
211
+ for (const a of argv) {
212
+ if (a.startsWith("-"))
213
+ continue;
214
+ const m = a.match(portRe);
215
+ if (m === null)
216
+ continue;
217
+ const n = Number(m[1]);
218
+ if (n >= 1 && n <= 65535)
219
+ ports.add(n);
220
+ }
221
+ return ports;
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.852",
3
+ "version": "1.0.854",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -0,0 +1,44 @@
1
+ {
2
+ "brands": [
3
+ {
4
+ "hostname": "maxy",
5
+ "configDir": ".maxy",
6
+ "vncDisplay": 99,
7
+ "rfbPort": 5900,
8
+ "websockifyPort": 6080,
9
+ "cdpPort": 9222
10
+ },
11
+ {
12
+ "hostname": "maxy-2",
13
+ "configDir": ".maxy-2",
14
+ "vncDisplay": 101,
15
+ "rfbPort": 5902,
16
+ "websockifyPort": 6082,
17
+ "cdpPort": 9224
18
+ },
19
+ {
20
+ "hostname": "maxy-3",
21
+ "configDir": ".maxy-3",
22
+ "vncDisplay": 102,
23
+ "rfbPort": 5903,
24
+ "websockifyPort": 6083,
25
+ "cdpPort": 9225
26
+ },
27
+ {
28
+ "hostname": "maxy-4",
29
+ "configDir": ".maxy-4",
30
+ "vncDisplay": 103,
31
+ "rfbPort": 5904,
32
+ "websockifyPort": 6084,
33
+ "cdpPort": 9226
34
+ },
35
+ {
36
+ "hostname": "realagent",
37
+ "configDir": ".realagent",
38
+ "vncDisplay": 100,
39
+ "rfbPort": 5901,
40
+ "websockifyPort": 6081,
41
+ "cdpPort": 9223
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The component names that the platform UI treats as long-lived editor
3
+ * surfaces (not single-shot prompts). They survive multiple `onSubmit`
4
+ * fires because the operator may auto-save mid-edit, and they are also
5
+ * the **server-side commitment surface** for Task 942: when the admin
6
+ * agent emits one of these names via `render-component`, the live
7
+ * stream-parser materialises the inline content as a sibling
8
+ * `:KnowledgeDocument` artefact so the row appears in the artefacts
9
+ * panel and survives session compaction.
10
+ *
11
+ * Single source of truth — the platform UI (`app/admin-types.ts`) and
12
+ * the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
13
+ * from this module. Any drift here is an account-isolation /
14
+ * persistence-doctrine bug; keep the constant in one place.
15
+ */
16
+ export declare const PERSISTENT_COMPONENTS: Set<string>;
17
+ /**
18
+ * Cheap, allocation-free guard for hot-path checks.
19
+ */
20
+ export declare function isPersistentComponent(name: string | undefined | null): boolean;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,aAKhC,CAAC;AAEH;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAE9E"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PERSISTENT_COMPONENTS = void 0;
4
+ exports.isPersistentComponent = isPersistentComponent;
5
+ /**
6
+ * The component names that the platform UI treats as long-lived editor
7
+ * surfaces (not single-shot prompts). They survive multiple `onSubmit`
8
+ * fires because the operator may auto-save mid-edit, and they are also
9
+ * the **server-side commitment surface** for Task 942: when the admin
10
+ * agent emits one of these names via `render-component`, the live
11
+ * stream-parser materialises the inline content as a sibling
12
+ * `:KnowledgeDocument` artefact so the row appears in the artefacts
13
+ * panel and survives session compaction.
14
+ *
15
+ * Single source of truth — the platform UI (`app/admin-types.ts`) and
16
+ * the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
17
+ * from this module. Any drift here is an account-isolation /
18
+ * persistence-doctrine bug; keep the constant in one place.
19
+ */
20
+ exports.PERSISTENT_COMPONENTS = new Set([
21
+ 'action-list',
22
+ 'document-editor',
23
+ 'rich-content-editor',
24
+ 'grid-editor',
25
+ ]);
26
+ /**
27
+ * Cheap, allocation-free guard for hot-path checks.
28
+ */
29
+ function isPersistentComponent(name) {
30
+ return typeof name === 'string' && exports.PERSISTENT_COMPONENTS.has(name);
31
+ }
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyBA,sDAEC;AA3BD;;;;;;;;;;;;;;GAcG;AACU,QAAA,qBAAqB,GAAG,IAAI,GAAG,CAAS;IACnD,aAAa;IACb,iBAAiB;IACjB,qBAAqB;IACrB,aAAa;CACd,CAAC,CAAC;AAEH;;GAEG;AACH,SAAgB,qBAAqB,CAAC,IAA+B;IACnE,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,6BAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The component names that the platform UI treats as long-lived editor
3
+ * surfaces (not single-shot prompts). They survive multiple `onSubmit`
4
+ * fires because the operator may auto-save mid-edit, and they are also
5
+ * the **server-side commitment surface** for Task 942: when the admin
6
+ * agent emits one of these names via `render-component`, the live
7
+ * stream-parser materialises the inline content as a sibling
8
+ * `:KnowledgeDocument` artefact so the row appears in the artefacts
9
+ * panel and survives session compaction.
10
+ *
11
+ * Single source of truth — the platform UI (`app/admin-types.ts`) and
12
+ * the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
13
+ * from this module. Any drift here is an account-isolation /
14
+ * persistence-doctrine bug; keep the constant in one place.
15
+ */
16
+ export const PERSISTENT_COMPONENTS = new Set<string>([
17
+ 'action-list',
18
+ 'document-editor',
19
+ 'rich-content-editor',
20
+ 'grid-editor',
21
+ ]);
22
+
23
+ /**
24
+ * Cheap, allocation-free guard for hot-path checks.
25
+ */
26
+ export function isPersistentComponent(name: string | undefined | null): boolean {
27
+ return typeof name === 'string' && PERSISTENT_COMPONENTS.has(name);
28
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -71,5 +71,5 @@ Tools are available via the `admin` MCP server.
71
71
  ## Hooks
72
72
 
73
73
  - `hooks/pre-tool-use.sh` — enforces admin agent write boundaries
74
- - `hooks/playwright-file-guard.sh` — intercepts file:// URLs with actionable guidance
74
+ - `hooks/playwright-file-guard.sh` — rewrites file:// URLs to a backgrounded loopback http.server before Playwright sees them
75
75
  - `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.