@rubytech/create-maxy-code 0.1.411 → 0.1.412

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 (147) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/account-scope/dist/index.d.ts +27 -0
  3. package/payload/platform/lib/account-scope/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/account-scope/dist/index.js +33 -0
  5. package/payload/platform/lib/account-scope/dist/index.js.map +1 -0
  6. package/payload/platform/lib/account-scope/src/index.ts +43 -0
  7. package/payload/platform/lib/account-scope/tsconfig.json +5 -0
  8. package/payload/platform/package.json +2 -2
  9. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  10. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  11. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +35 -1
  12. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
  13. package/payload/platform/plugins/docs/references/cross-account-authority.md +23 -0
  14. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  15. package/payload/platform/plugins/filesystem/.claude-plugin/plugin.json +21 -0
  16. package/payload/platform/plugins/filesystem/PLUGIN.md +63 -0
  17. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +193 -0
  18. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/package.json +3 -0
  19. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts +2 -0
  20. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts.map +1 -0
  21. package/payload/platform/plugins/filesystem/mcp/dist/index.js +91 -0
  22. package/payload/platform/plugins/filesystem/mcp/dist/index.js.map +1 -0
  23. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts +2 -0
  24. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js +89 -0
  26. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js.map +1 -0
  27. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts +12 -0
  28. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts.map +1 -0
  29. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js +6 -0
  30. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js.map +1 -0
  31. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +45 -0
  32. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -0
  33. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +86 -0
  34. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -0
  35. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts +11 -0
  36. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts.map +1 -0
  37. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js +64 -0
  38. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js.map +1 -0
  39. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts +2 -0
  40. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts.map +1 -0
  41. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js +94 -0
  42. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js.map +1 -0
  43. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts +8 -0
  44. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts.map +1 -0
  45. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js +40 -0
  46. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js.map +1 -0
  47. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts +6 -0
  48. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts.map +1 -0
  49. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js +13 -0
  50. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js.map +1 -0
  51. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts +7 -0
  52. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts.map +1 -0
  53. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js +71 -0
  54. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js.map +1 -0
  55. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts +6 -0
  56. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts.map +1 -0
  57. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js +26 -0
  58. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js.map +1 -0
  59. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts +6 -0
  60. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts.map +1 -0
  61. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js +28 -0
  62. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js.map +1 -0
  63. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts +7 -0
  64. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts.map +1 -0
  65. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js +25 -0
  66. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js.map +1 -0
  67. package/payload/platform/plugins/filesystem/mcp/package.json +21 -0
  68. package/payload/platform/plugins/filesystem/mcp/vitest.config.ts +8 -0
  69. package/payload/platform/plugins/memory/PLUGIN.md +6 -0
  70. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts +34 -0
  71. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts.map +1 -0
  72. package/payload/platform/plugins/memory/mcp/dist/index-testable.js +60 -0
  73. package/payload/platform/plugins/memory/mcp/dist/index-testable.js.map +1 -0
  74. package/payload/platform/plugins/memory/mcp/dist/index.js +275 -133
  75. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts +2 -0
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js +65 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js.map +1 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts +2 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js +54 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js.map +1 -0
  84. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  85. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +7 -0
  86. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -1
  88. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +6 -1
  89. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts +7 -1
  91. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js +7 -2
  93. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js.map +1 -1
  94. package/payload/platform/services/claude-session-manager/dist/config.d.ts +37 -0
  95. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  96. package/payload/platform/services/claude-session-manager/dist/config.js +34 -1
  97. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  98. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts +34 -0
  99. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts.map +1 -0
  100. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js +98 -0
  101. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js.map +1 -0
  102. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  103. package/payload/platform/services/claude-session-manager/dist/http-server.js +10 -0
  104. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  105. package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
  106. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  107. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  108. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +25 -0
  109. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  110. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  111. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  112. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
  113. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  114. package/payload/server/{chunk-ZYS5V5QG.js → chunk-3Z5WE53X.js} +4 -0
  115. package/payload/server/maxy-edge.js +1 -1
  116. package/payload/server/public/assets/{AdminLoginScreens-BRQgI2lW.js → AdminLoginScreens-CYPTMmTk.js} +1 -1
  117. package/payload/server/public/assets/{AdminShell-By9n5icG.js → AdminShell-CZH_EZIy.js} +1 -1
  118. package/payload/server/public/assets/{Checkbox-DjEWgYu1.js → Checkbox-B0pbYJDQ.js} +1 -1
  119. package/payload/server/public/assets/{Transcript-BejWvOaO.js → Transcript-CYaAPBFX.js} +1 -1
  120. package/payload/server/public/assets/{admin-BFEMryQm.js → admin-BKDMSRto.js} +1 -1
  121. package/payload/server/public/assets/{browser-CMJMEUz6.js → browser-CxeMDDZA.js} +1 -1
  122. package/payload/server/public/assets/{calendar-C6ZGlHEP.js → calendar-DiIiTY9-.js} +1 -1
  123. package/payload/server/public/assets/chat-C56Oqvt1.js +1 -0
  124. package/payload/server/public/assets/chevron-left-Da-iZGi_.js +1 -0
  125. package/payload/server/public/assets/data-ZUdT44Ba.js +1 -0
  126. package/payload/server/public/assets/{graph-DIrXtPLv.js → graph-DC3dwnMZ.js} +1 -1
  127. package/payload/server/public/assets/{graph-labels-BtPf1Jom.js → graph-labels-Csnovm_c.js} +1 -1
  128. package/payload/server/public/assets/{maximize-2-DeJ4K8kL.js → maximize-2-WRUJsg5n.js} +1 -1
  129. package/payload/server/public/assets/{operator-B8QhqzLI.js → operator-BpzeHTAw.js} +1 -1
  130. package/payload/server/public/assets/{page-iFOy0dFx.js → page-8jJOxx6y.js} +1 -1
  131. package/payload/server/public/assets/{page-Df6KYkf4.js → page-IQDLdebG.js} +1 -1
  132. package/payload/server/public/assets/{public-BrUIRQZd.js → public-Dqg7jwjq.js} +1 -1
  133. package/payload/server/public/assets/{rotate-ccw-CYfntpwu.js → rotate-ccw-BayMWwfc.js} +1 -1
  134. package/payload/server/public/assets/{useSubAccountSwitcher-Cvnc_fwQ.css → useSubAccountSwitcher-CRZfyJXR.css} +1 -1
  135. package/payload/server/public/browser.html +4 -4
  136. package/payload/server/public/calendar.html +5 -5
  137. package/payload/server/public/chat.html +9 -9
  138. package/payload/server/public/data.html +8 -8
  139. package/payload/server/public/graph.html +9 -9
  140. package/payload/server/public/index.html +11 -11
  141. package/payload/server/public/operator.html +11 -11
  142. package/payload/server/public/public.html +9 -9
  143. package/payload/server/server.js +36 -11
  144. package/payload/server/public/assets/chat-B7s62Nnp.js +0 -1
  145. package/payload/server/public/assets/chevron-left-Dfjcaiyr.js +0 -1
  146. package/payload/server/public/assets/data-741ZbBwR.js +0 -1
  147. /package/payload/server/public/assets/{useSubAccountSwitcher-BK_KfOLC.js → useSubAccountSwitcher-C9aLg8yI.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.411",
3
+ "version": "0.1.412",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,27 @@
1
+ export type EffectiveAccount = {
2
+ accountId: string;
3
+ } | {
4
+ reject: "cross-account-denied" | "cross-account-invalid-target";
5
+ };
6
+ /**
7
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
8
+ * may set `targetAccountId`, and only to a valid account. There is no house
9
+ * fallback — an unscoped session that names a target is denied, never silently
10
+ * downgraded to its own account.
11
+ *
12
+ * `isValidAccountId` is injected (required, no default): the UI passes its
13
+ * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
14
+ * `enumerateValidAccountIds`. Keeping validity injected is what lets this
15
+ * module stay pure.
16
+ */
17
+ export declare function resolveEffectiveAccountCore(input: {
18
+ ownAccountId: string;
19
+ houseScope: string | null;
20
+ targetAccountId?: string | null;
21
+ }, isValidAccountId: (id: string) => boolean): EffectiveAccount;
22
+ /**
23
+ * Boot-reader that consumers import to learn their house scope from the spawn
24
+ * env. An empty value reads as null — a non-house session carries no scope.
25
+ */
26
+ export declare function readHouseAdminScope(env?: NodeJS.ProcessEnv): string | null;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GACxB;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,sBAAsB,GAAG,8BAA8B,CAAA;CAAE,CAAC;AAExE;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC3F,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACxC,gBAAgB,CAMlB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,IAAI,CAEvF"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEffectiveAccountCore = resolveEffectiveAccountCore;
4
+ exports.readHouseAdminScope = readHouseAdminScope;
5
+ /**
6
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
7
+ * may set `targetAccountId`, and only to a valid account. There is no house
8
+ * fallback — an unscoped session that names a target is denied, never silently
9
+ * downgraded to its own account.
10
+ *
11
+ * `isValidAccountId` is injected (required, no default): the UI passes its
12
+ * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
13
+ * `enumerateValidAccountIds`. Keeping validity injected is what lets this
14
+ * module stay pure.
15
+ */
16
+ function resolveEffectiveAccountCore(input, isValidAccountId) {
17
+ const { ownAccountId, houseScope, targetAccountId } = input;
18
+ if (!targetAccountId)
19
+ return { accountId: ownAccountId };
20
+ if (!houseScope)
21
+ return { reject: "cross-account-denied" };
22
+ if (!isValidAccountId(targetAccountId))
23
+ return { reject: "cross-account-invalid-target" };
24
+ return { accountId: targetAccountId };
25
+ }
26
+ /**
27
+ * Boot-reader that consumers import to learn their house scope from the spawn
28
+ * env. An empty value reads as null — a non-house session carries no scope.
29
+ */
30
+ function readHouseAdminScope(env = process.env) {
31
+ return env.HOUSE_ADMIN_SCOPE ? env.HOUSE_ADMIN_SCOPE : null;
32
+ }
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAyBA,kEASC;AAMD,kDAEC;AA5BD;;;;;;;;;;GAUG;AACH,SAAgB,2BAA2B,CACzC,KAA2F,EAC3F,gBAAyC;IAEzC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAC5D,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IACzD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC3D,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;IAC1F,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,OAAO,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,43 @@
1
+ // account-scope — the single fail-closed cross-account resolver.
2
+ //
3
+ // Task 1440 landed this logic inline in
4
+ // platform/ui/app/lib/claude-agent/account.ts. Task 1458 relocates the two
5
+ // pure primitives here so BOTH the UI lib (account.ts, which re-exports them)
6
+ // and MCP plugins (the `filesystem` plugin) import ONE definition — "one
7
+ // resolver, no drift", the same privilege-boundary discipline as the 1383/1385
8
+ // account-manager escalation. This module is pure: no filesystem access, no
9
+ // throw at module load, so it is safe to import into an MCP server that boots
10
+ // with empty env during plugin-system `tools/list` enumeration.
11
+ export type EffectiveAccount =
12
+ | { accountId: string }
13
+ | { reject: "cross-account-denied" | "cross-account-invalid-target" };
14
+
15
+ /**
16
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
17
+ * may set `targetAccountId`, and only to a valid account. There is no house
18
+ * fallback — an unscoped session that names a target is denied, never silently
19
+ * downgraded to its own account.
20
+ *
21
+ * `isValidAccountId` is injected (required, no default): the UI passes its
22
+ * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
23
+ * `enumerateValidAccountIds`. Keeping validity injected is what lets this
24
+ * module stay pure.
25
+ */
26
+ export function resolveEffectiveAccountCore(
27
+ input: { ownAccountId: string; houseScope: string | null; targetAccountId?: string | null },
28
+ isValidAccountId: (id: string) => boolean,
29
+ ): EffectiveAccount {
30
+ const { ownAccountId, houseScope, targetAccountId } = input;
31
+ if (!targetAccountId) return { accountId: ownAccountId };
32
+ if (!houseScope) return { reject: "cross-account-denied" };
33
+ if (!isValidAccountId(targetAccountId)) return { reject: "cross-account-invalid-target" };
34
+ return { accountId: targetAccountId };
35
+ }
36
+
37
+ /**
38
+ * Boot-reader that consumers import to learn their house scope from the spawn
39
+ * env. An empty value reads as null — a non-house session carries no scope.
40
+ */
41
+ export function readHouseAdminScope(env: NodeJS.ProcessEnv = process.env): string | null {
42
+ return env.HOUSE_ADMIN_SCOPE ? env.HOUSE_ADMIN_SCOPE : null;
43
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src" },
4
+ "include": ["src"]
5
+ }
@@ -6,8 +6,8 @@
6
6
  "services/*"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/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/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/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/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/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/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/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/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/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/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/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/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
11
11
  "gen:canonical-tools": "node scripts/generate-canonical-tool-names.mjs",
12
12
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
13
13
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
@@ -54,6 +54,11 @@
54
54
  "source": "./email",
55
55
  "version": "0.1.0"
56
56
  },
57
+ {
58
+ "name": "filesystem",
59
+ "source": "./filesystem",
60
+ "version": "0.1.0"
61
+ },
57
62
  {
58
63
  "name": "graph",
59
64
  "source": "./graph",
@@ -4,6 +4,7 @@ browser
4
4
  connector
5
5
  contacts
6
6
  email
7
+ filesystem
7
8
  graph
8
9
  graph-viewer
9
10
  joblogic
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:69d2ec47598e75fac6f763994a077fd39ccda77767ddb90404dac2d556f5791c
4
+ content-hash: sha256:3e8c5b6a28d0ca8c9bcbbb802165d8b2fffcb1af2531353bda360e8b7eb964f6
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -516,6 +516,34 @@ A third layer closes the same gap from the platform side: when `claude-agent.ts`
516
516
 
517
517
  **Boot-smoke as publish-time gate.** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
518
518
 
519
+ ---
520
+ # House-Admin Cross-Account Authority
521
+ Source: https://docs.getmaxy.com/cross-account-authority.md
522
+
523
+ # House-admin cross-account authority
524
+
525
+ The house account is the operator-owned account that runs the admin assistant. Sub-accounts are isolated: each one's tools read and write only that account's data, which is what keeps tenants separate.
526
+
527
+ By default the admin assistant is scoped the same way — it acts on the house account. Cross-account authority lets a house-scoped admin session act on a named sub-account by passing that account as a target, while every other session (public visitors, a session already scoped to a sub-account, background specialists) is refused. The refusal is fail-closed: a session that is not house-scoped and names a target is denied, never quietly redirected to its own account.
528
+
529
+ The operator wants this for two surfaces: the account graph and the account filesystem. Each is a separate adoption; a coverage ledger tracks them and a background check confirms neither silently regresses.
530
+
531
+ ## Memory (graph)
532
+
533
+ The `memory` plugin gives the admin session access to a sub-account's knowledge graph. Its tools whose read or write is scoped solely by the account — search, lookup, write, update, edit, delete, edges, attachments, reports, conversation history, ingest, and graph maintenance — take an optional target. Omit it and the tool acts on the session's own account, exactly as before; pass a sub-account and a house-scoped admin session reads or writes that account's subgraph instead; any other session that names a target is refused.
534
+
535
+ A few memory tools do **not** take a target. `profile-read`/`update`/`delete` and `session-compact` are keyed to the caller's own identity (the operator's user, and the live session) rather than only the account, so re-pointing the account alone would act on the wrong record; cross-account for these is a separate, later piece of work. `graph-prune-denylist-*` and `memory-ingest-extract` are not account-scoped at all (a shared deny-list and a temporary staging step), so a target would mean nothing. These stay on the session's own account.
536
+
537
+ ## Filesystem
538
+
539
+ The `filesystem` plugin gives the admin session direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, arbitrary files. It exists because the native file tools (Read, Write, Edit, and the shell) are locked to the session's own account and cannot take a target. Its six tools are `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep`.
540
+
541
+ Omit the target and each tool acts on the session's own account, exactly like the native tools. Pass a sub-account as the target and a house-scoped admin session reads or writes that account's files instead; any other session that names a target is refused. Every path is confined to the resolved account directory — a path that climbs out with `../`, an absolute path outside the account root, or a symlink pointing away is rejected before anything is read or written, so the tools can never reach another account or the platform's own files.
542
+
543
+ ## Signals
544
+
545
+ A standing background check reconciles live sessions against the account roles and raises an alarm if any session still holds house authority for an account that is no longer the house account, so a stale grant cannot sit unnoticed. Each cross-account tool call also records its decision — who called, which account it targeted, whether it was allowed, and why — before it touches the graph or disk, and a rejected filesystem path escape is recorded on its own line. In normal operation neither an allowed cross-account call from a non-house session nor a path escape ever appears.
546
+
519
547
  ---
520
548
  # Install Overview
521
549
  Source: https://docs.getmaxy.com/install.md
@@ -1582,6 +1610,12 @@ If you want to write the public summary yourself — for instance, because the a
1582
1610
 
1583
1611
  People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
1584
1612
 
1613
+ ## Working across accounts
1614
+
1615
+ If you run Maxy as the house account — the operator account that manages sub-accounts — you can point a memory action at one of your sub-accounts instead of your own. Name the account when you ask, for example "search Anneke's account for the roofing quote" or "add this note to the Jones account." Most memory actions honour this: search, read, write, edit, delete, reports, and conversation history. Profile actions ("what do you know about me") stay on your own account for now.
1616
+
1617
+ Leave the account out and nothing changes — the action runs on your own account exactly as before. Only the house account can do this. A sub-account session that tries to reach another account is refused, and naming an account that does not exist is refused too. Sub-accounts stay fully isolated from each other.
1618
+
1585
1619
  ## Privacy
1586
1620
 
1587
1621
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. Maxy does not sync memory to any cloud service or third party.
@@ -439,6 +439,8 @@ shasum -a 256 "${ACCOUNT_DIR}/e-sign/<DOC_REF>/base.pdf" | awk '{print $1}' \
439
439
 
440
440
  Before persisting, **read every rendered page** of `base.pdf` — full-bleed pages bleed correctly, no footer split across pages, no near-blank trailing page (§ 2 pagination; A4 dimensions are not proof of fit). If the deployed page is behind Cloudflare Access, do this base render in a session that can authenticate to Access — it is a one-time, operator-driven step, unlike dispatch (§ 6), which must never fetch the page at all. A new `DOC_REF` triggers a fresh render; an existing one keeps its persisted base.
441
441
 
442
+ **The deployed signing-page URL is an explicit hand-back.** This deploy produces one value every downstream caller needs: the signing page's absolute `https://` URL — the canonical host (§ 7: `https://<project>.pages.dev`, or a custom domain mapped to it) joined to the document's slug (§ 1, e.g. `https://<project>.pages.dev/quote-lakeside-7f3a9c.html`). Return it to whatever flow invoked e-sign. **Invariant:** any external document that embeds a "Sign Online" / acceptance link — a quote, a proposal, a cover letter — must carry this exact value in that link, and **must not be finalised, exported, or sent before this deploy has produced it**. A Sign-Online link cannot point at a page that does not yet exist: deploy first, hand back the URL, then the caller injects it and renders. The quotation client-quote path is one such caller and gates on this — `sitedesk-job/skills/quotation/references/quote-generation.md` refuses to export a client quote whose Sign-Online anchor is not the live `https://` URL handed back here.
443
+
442
444
  ---
443
445
 
444
446
  ## 6. Sweep + dispatch
@@ -0,0 +1,23 @@
1
+ # House-admin cross-account authority
2
+
3
+ The house account is the operator-owned account that runs the admin assistant. Sub-accounts are isolated: each one's tools read and write only that account's data, which is what keeps tenants separate.
4
+
5
+ By default the admin assistant is scoped the same way — it acts on the house account. Cross-account authority lets a house-scoped admin session act on a named sub-account by passing that account as a target, while every other session (public visitors, a session already scoped to a sub-account, background specialists) is refused. The refusal is fail-closed: a session that is not house-scoped and names a target is denied, never quietly redirected to its own account.
6
+
7
+ The operator wants this for two surfaces: the account graph and the account filesystem. Each is a separate adoption; a coverage ledger tracks them and a background check confirms neither silently regresses.
8
+
9
+ ## Memory (graph)
10
+
11
+ The `memory` plugin gives the admin session access to a sub-account's knowledge graph. Its tools whose read or write is scoped solely by the account — search, lookup, write, update, edit, delete, edges, attachments, reports, conversation history, ingest, and graph maintenance — take an optional target. Omit it and the tool acts on the session's own account, exactly as before; pass a sub-account and a house-scoped admin session reads or writes that account's subgraph instead; any other session that names a target is refused.
12
+
13
+ A few memory tools do **not** take a target. `profile-read`/`update`/`delete` and `session-compact` are keyed to the caller's own identity (the operator's user, and the live session) rather than only the account, so re-pointing the account alone would act on the wrong record; cross-account for these is a separate, later piece of work. `graph-prune-denylist-*` and `memory-ingest-extract` are not account-scoped at all (a shared deny-list and a temporary staging step), so a target would mean nothing. These stay on the session's own account.
14
+
15
+ ## Filesystem
16
+
17
+ The `filesystem` plugin gives the admin session direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, arbitrary files. It exists because the native file tools (Read, Write, Edit, and the shell) are locked to the session's own account and cannot take a target. Its six tools are `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep`.
18
+
19
+ Omit the target and each tool acts on the session's own account, exactly like the native tools. Pass a sub-account as the target and a house-scoped admin session reads or writes that account's files instead; any other session that names a target is refused. Every path is confined to the resolved account directory — a path that climbs out with `../`, an absolute path outside the account root, or a symlink pointing away is rejected before anything is read or written, so the tools can never reach another account or the platform's own files.
20
+
21
+ ## Signals
22
+
23
+ A standing background check reconciles live sessions against the account roles and raises an alarm if any session still holds house authority for an account that is no longer the house account, so a stale grant cannot sit unnoticed. Each cross-account tool call also records its decision — who called, which account it targeted, whether it was allowed, and why — before it touches the graph or disk, and a rejected filesystem path escape is recorded on its own line. In normal operation neither an allowed cross-account call from a non-house session nor a path escape ever appears.
@@ -156,6 +156,12 @@ If you want to write the public summary yourself — for instance, because the a
156
156
 
157
157
  People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
158
158
 
159
+ ## Working across accounts
160
+
161
+ If you run {{productName}} as the house account — the operator account that manages sub-accounts — you can point a memory action at one of your sub-accounts instead of your own. Name the account when you ask, for example "search Anneke's account for the roofing quote" or "add this note to the Jones account." Most memory actions honour this: search, read, write, edit, delete, reports, and conversation history. Profile actions ("what do you know about me") stay on your own account for now.
162
+
163
+ Leave the account out and nothing changes — the action runs on your own account exactly as before. Only the house account can do this. A sub-account session that tries to reach another account is refused, and naming an account that does not exist is refused too. Sub-accounts stay fully isolated from each other.
164
+
159
165
  ## Privacy
160
166
 
161
167
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "filesystem",
3
+ "description": "Account-scoped file access. Read, write, edit, list, glob, and grep files under the session's own account directory; the house admin may reach any sub-account's files by naming targetAccountId, gated fail-closed and confined to that account's directory.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "filesystem": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/lib/mcp-spawn-tee/index.js",
14
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
15
+ ],
16
+ "env": {
17
+ "MCP_SPAWN_TEE_NAME": "filesystem"
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: filesystem
3
+ description: "Account-scoped file access. Read, write, edit, list, glob, and grep files under the session's own account directory; the house admin may reach any sub-account's files by naming targetAccountId, gated fail-closed and confined to that account's directory."
4
+ tools:
5
+ - name: file-read
6
+ publicAllowlist: false
7
+ adminAllowlist: true
8
+ - name: file-write
9
+ publicAllowlist: false
10
+ adminAllowlist: true
11
+ - name: file-edit
12
+ publicAllowlist: false
13
+ adminAllowlist: true
14
+ - name: file-list
15
+ publicAllowlist: false
16
+ adminAllowlist: true
17
+ - name: file-glob
18
+ publicAllowlist: false
19
+ adminAllowlist: true
20
+ - name: file-grep
21
+ publicAllowlist: false
22
+ adminAllowlist: true
23
+ metadata: {"platform":{"always":false,"embed":[],"pluginKey":"filesystem"}}
24
+ mcp:
25
+ command: node
26
+ args:
27
+ - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
28
+ - ${PLATFORM_ROOT}/plugins/filesystem/mcp/dist/index.js
29
+ env:
30
+ MCP_SPAWN_TEE_NAME: filesystem
31
+ LOG_DIR: ${LOG_DIR}
32
+ PLATFORM_ROOT: ${PLATFORM_ROOT}
33
+ ACCOUNT_ID: ${ACCOUNT_ID}
34
+ HOUSE_ADMIN_SCOPE: ${HOUSE_ADMIN_SCOPE}
35
+ SESSION_ID: ${SESSION_ID}
36
+ AGENT_SLUG: ${AGENT_SLUG}
37
+ mcp-manifest: auto
38
+ ---
39
+
40
+ # Filesystem
41
+
42
+ Direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, and arbitrary files. These tools complement the native Read/Write/Edit tools, which are locked to your own account; only these tools can reach another account's files, and only for the house admin.
43
+
44
+ ## Own account
45
+
46
+ Omit `targetAccountId`. Every tool takes a `path` relative to your account directory. `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep` behave like their native counterparts but always resolve inside your account root.
47
+
48
+ ## Cross-account (house admin only)
49
+
50
+ Pass `targetAccountId` to operate on a sub-account's directory. This is honoured only for a house-scoped admin session. A sub-account, specialist, or public session that passes `targetAccountId` is refused with `cross-account-denied`; an unknown account id is refused with `cross-account-invalid-target`. Use this to inspect or repair a client account's files from the house session without switching accounts.
51
+
52
+ ## Boundaries
53
+
54
+ Every path is confined to the resolved account directory. A path that escapes it — `../`, an absolute path outside the account root, or a symlink pointing out — is rejected before any filesystem operation. Reads and greps skip files over 5 MB; grep skips binary files.
55
+
56
+ ## Tools
57
+
58
+ - `file-read` — Read a UTF-8 file. Args: `path`, optional `targetAccountId`.
59
+ - `file-write` — Create or overwrite a UTF-8 file, making parent directories. Args: `path`, `content`, optional `targetAccountId`.
60
+ - `file-edit` — Replace one unique occurrence of `oldString` with `newString`. Fails if `oldString` is missing or not unique. Args: `path`, `oldString`, `newString`, optional `targetAccountId`.
61
+ - `file-list` — List a directory's entries with their type. Args: optional `path` (defaults to the account root), optional `targetAccountId`.
62
+ - `file-glob` — List files matching a glob (`**`, `*`, `?`). Args: `pattern`, optional `targetAccountId`.
63
+ - `file-grep` — Search file contents for a JavaScript regular expression, returning `file:line:preview`. Args: `pattern`, optional `path`, optional `targetAccountId`.
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * MCP spawn-tee — in-process stderr capture + lifecycle observability.
5
+ *
6
+ * Claude Code spawns each MCP server itself; the platform never holds a
7
+ * ChildProcess handle. This shim sits between Claude Code and the real MCP
8
+ * server: Claude Code runs `node <this> <real-entry>`, and the shim runs the
9
+ * real entry IN ITS OWN PROCESS via dynamic import() — one node runtime, not
10
+ * two. Before importing, it replaces process.stderr.write with a tee so every
11
+ * stderr byte the server writes is mirrored to the log sinks; stdin and stdout
12
+ * (the JSON-RPC channel) are never touched.
13
+ *
14
+ * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
+ * stdin/stdout : untouched (JSON-RPC channel)
16
+ * stderr : process.stderr.write teed → per-date + per-session + passthrough
17
+ *
18
+ * Destinations (Task 706) — unchanged:
19
+ * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` — per-date raw (back-compat;
20
+ * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
+ * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
+ * spawn) — then the per-date file is the fallback.
24
+ * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
+ * the [mcp-helper] lifecycle lines.
26
+ *
27
+ * Lifecycle lines, correlation key `session=<id8> server=<name>` — unchanged:
28
+ * [mcp-helper] op=spawn ... pid= entry=
29
+ * [mcp-helper] op=boot ... head=<first stderr bytes>
30
+ * [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail=
31
+ *
32
+ * Process model (Task 989): the shim IS the server's process. op=exit fires
33
+ * from process.on("exit"), so it covers a normal exit, a non-zero exit, an
34
+ * uncaught throw (code 1), and a catchable-signal exit (SIGTERM/SIGINT/SIGHUP,
35
+ * whose handlers record the signal name). An external SIGKILL is uncatchable
36
+ * and leaves no op=exit line — the per-session stderr tail already on disk and
37
+ * Claude Code's own transport-drop are the evidence for that death mode.
38
+ *
39
+ * The shim never writes to fd 1 (stdout) — that is the JSON-RPC channel.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ const node_fs_1 = require("node:fs");
43
+ const node_path_1 = require("node:path");
44
+ const node_url_1 = require("node:url");
45
+ const SERVER_NAME = process.env.MCP_SPAWN_TEE_NAME ?? "unknown";
46
+ const LOG_DIR = process.env.LOG_DIR;
47
+ const SESSION_ID = process.env.SESSION_ID;
48
+ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
+ const ENTRY = process.argv[2];
50
+ const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
+ const spawnStamp = Date.now();
52
+ // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
+ // spawn) → undefined, and the per-date file is the fallback.
54
+ const perSessionPath = LOG_DIR && SESSION_ID
55
+ ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
+ : undefined;
57
+ const perDatePath = LOG_DIR
58
+ ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
59
+ : undefined;
60
+ // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
+ // grow the buffer without bound.
62
+ const TAIL_CAP = 2048;
63
+ let stderrTail = "";
64
+ let bootEmitted = false;
65
+ let exitEmitted = false;
66
+ let exitSignal;
67
+ // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
+ // and stderr passthrough both go through this so they are never re-teed into
69
+ // the per-date sink nor recursed back into the patched writer.
70
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
+ // LOG_DIR is created once, lazily, on the first successful append — not per
72
+ // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
73
+ // mkdirSync would be two syscalls per log line for nothing.
74
+ let logDirReady = false;
75
+ function appendSafe(path, data) {
76
+ if (!path || !LOG_DIR)
77
+ return;
78
+ try {
79
+ if (!logDirReady) {
80
+ (0, node_fs_1.mkdirSync)(LOG_DIR, { recursive: true });
81
+ logDirReady = true;
82
+ }
83
+ (0, node_fs_1.appendFileSync)(path, data);
84
+ }
85
+ catch {
86
+ /* unwritable destination — never mask primary output */
87
+ }
88
+ }
89
+ // Best-effort mirror of a lifecycle line to server.log via the loopback
90
+ // log-ingest route. Fire-and-forget: the per-session file is authoritative, so
91
+ // a dropped POST loses nothing. On the "exit" event the loop is stopping, so
92
+ // the op=exit mirror may not flush — the per-session line, written sync, holds.
93
+ function postToServerLog(suffix, level) {
94
+ if (!PLATFORM_PORT)
95
+ return;
96
+ try {
97
+ const ctrl = new AbortController();
98
+ const t = setTimeout(() => ctrl.abort(), 500);
99
+ t.unref?.(); // never let the abort timer keep the shim's event loop alive
100
+ void fetch(`http://127.0.0.1:${PLATFORM_PORT}/api/admin/log-ingest`, {
101
+ method: "POST",
102
+ headers: { "content-type": "application/json" },
103
+ body: JSON.stringify({ tag: "mcp-helper", level, line: suffix }),
104
+ signal: ctrl.signal,
105
+ }).then(() => clearTimeout(t)).catch(() => clearTimeout(t));
106
+ }
107
+ catch {
108
+ /* fetch threw synchronously — best-effort only */
109
+ }
110
+ }
111
+ // Emit one [mcp-helper] lifecycle line: authoritative per-session file (sync,
112
+ // survives process.exit), the shim's own stderr via the RAW writer (journald /
113
+ // Claude Code visibility, never re-teed), and a best-effort server.log mirror.
114
+ // `suffix` is the line body after the tag and carries no newline (the
115
+ // log-ingest route rejects newlines).
116
+ function emitLifecycle(suffix, level) {
117
+ const line = `[mcp-helper] ${suffix}\n`;
118
+ appendSafe(perSessionPath, line);
119
+ try {
120
+ rawStderrWrite(line);
121
+ }
122
+ catch { /* stderr closed */ }
123
+ postToServerLog(suffix, level);
124
+ }
125
+ if (!ENTRY) {
126
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason="no entry given (argv[2] missing)"`, "error");
127
+ process.exit(2);
128
+ }
129
+ // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
+ // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // op=boot on the first bytes, then pass the write through to the real stderr.
132
+ const teeWrite = ((...args) => {
133
+ const chunk = args[0];
134
+ appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
+ appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
136
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
+ stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
+ if (!bootEmitted) {
139
+ bootEmitted = true;
140
+ const head = text.split("\n")[0].slice(0, 200);
141
+ emitLifecycle(`op=boot session=${SESSION_ID8} server=${SERVER_NAME} head=${JSON.stringify(head)}`, "info");
142
+ }
143
+ return rawStderrWrite(...args); // passthrough
144
+ });
145
+ process.stderr.write = teeWrite;
146
+ // Catchable-signal handling. The shim drives the exit ONLY when it is the sole
147
+ // listener for the signal — i.e. the imported entry installed no handler of its
148
+ // own. In that case it records the signal (so op=exit carries signal=<sig>) and
149
+ // exits with 128+signum, mirroring the prior wrapper's exit-status convention.
150
+ //
151
+ // When the entry DID install a handler, the shim defers entirely and records
152
+ // nothing: the entry's handler decides the exit code, and op=exit reflects that
153
+ // exit verbatim. This matches the prior two-process model, where a child that
154
+ // caught the signal and exited cleanly was observed as code=0 signal=— (a clean
155
+ // self-exit), not as a signal death. Setting exitSignal here unconditionally
156
+ // would mislabel every graceful signal-driven shutdown as a signal kill.
157
+ function onSignal(sig, code) {
158
+ return () => {
159
+ if (process.listenerCount(sig) === 1) {
160
+ exitSignal = sig;
161
+ process.exit(code);
162
+ }
163
+ };
164
+ }
165
+ process.on("SIGTERM", onSignal("SIGTERM", 143));
166
+ process.on("SIGINT", onSignal("SIGINT", 130));
167
+ process.on("SIGHUP", onSignal("SIGHUP", 129));
168
+ // op=exit, emitted once from the process "exit" event. Sync-only — the loop is
169
+ // stopping. When a catchable signal caused the exit, report code=— signal=<sig>
170
+ // to match the prior wrapper's format; otherwise code=<exit code> signal=—.
171
+ process.on("exit", (code) => {
172
+ if (exitEmitted)
173
+ return;
174
+ exitEmitted = true;
175
+ const lifetimeMs = Date.now() - spawnStamp;
176
+ const tail = stderrTail.slice(-200);
177
+ const codeField = exitSignal ? "—" : String(code);
178
+ const level = exitSignal || code !== 0 ? "error" : "info";
179
+ emitLifecycle(`op=exit session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} ` +
180
+ `code=${codeField} signal=${exitSignal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level);
181
+ });
182
+ emitLifecycle(`op=spawn session=${SESSION_ID8} server=${SERVER_NAME} pid=${process.pid} entry=${ENTRY}`, "info");
183
+ // Run the real MCP server in THIS process. Dynamic import() (not top-level
184
+ // await — this file compiles to CommonJS) loads the ESM entry; a load-time
185
+ // failure mirrors the old child spawn-error path (op=error, exit 127). The
186
+ // op=exit handler is suppressed on this path so op=error stays the sole record.
187
+ import((0, node_url_1.pathToFileURL)(ENTRY).href).catch((err) => {
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ exitEmitted = true;
190
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`import error: ${msg}`)}`, "error");
191
+ process.exit(127);
192
+ });
193
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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":""}