@rubytech/create-maxy-code 0.1.388 → 0.1.391

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 (121) hide show
  1. package/dist/__tests__/claude-bin.test.js +51 -0
  2. package/dist/__tests__/claude-upgrade-privilege.test.js +17 -0
  3. package/dist/__tests__/joblogic-excluded.test.js +39 -0
  4. package/dist/__tests__/launchd-plist.test.js +36 -1
  5. package/dist/__tests__/macos-darwin-branch.test.js +18 -0
  6. package/dist/__tests__/neo4j-datadir.test.js +14 -0
  7. package/dist/claude-bin.js +78 -0
  8. package/dist/claude-upgrade-privilege.js +21 -0
  9. package/dist/index.js +262 -34
  10. package/dist/launchd-plist.js +25 -9
  11. package/dist/neo4j-datadir.js +15 -0
  12. package/package.json +1 -1
  13. package/payload/platform/config/brand.json +1 -1
  14. package/payload/platform/plugins/.claude-plugin/marketplace.json +0 -5
  15. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +24 -5
  16. package/payload/platform/plugins/docs/references/admin-session.md +4 -0
  17. package/payload/platform/plugins/docs/references/admin-ui.md +18 -1
  18. package/payload/platform/plugins/docs/references/joblogic.md +2 -0
  19. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
  20. package/payload/platform/plugins/work/PLUGIN.md +3 -0
  21. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts +2 -0
  22. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js +98 -0
  24. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js.map +1 -0
  25. package/payload/platform/plugins/work/mcp/dist/index.js +16 -0
  26. package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
  27. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts +53 -0
  28. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts.map +1 -0
  29. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js +80 -0
  30. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js.map +1 -0
  31. package/payload/platform/plugins/work/mcp/package.json +2 -1
  32. package/payload/platform/scripts/smoke-boot-services.sh +35 -10
  33. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  35. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts +11 -0
  37. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -0
  38. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +37 -0
  39. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -0
  40. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/http-server.js +98 -8
  42. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/pricing.d.ts +45 -0
  44. package/payload/platform/services/claude-session-manager/dist/pricing.d.ts.map +1 -0
  45. package/payload/platform/services/claude-session-manager/dist/pricing.js +57 -0
  46. package/payload/platform/services/claude-session-manager/dist/pricing.js.map +1 -0
  47. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -0
  49. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +1 -1
  51. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +5 -1
  53. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  54. package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts +38 -0
  55. package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts.map +1 -0
  56. package/payload/platform/services/claude-session-manager/dist/session-metering.js +292 -0
  57. package/payload/platform/services/claude-session-manager/dist/session-metering.js.map +1 -0
  58. package/payload/platform/templates/specialists/agents/project-manager.md +1 -1
  59. package/payload/server/public/assets/{AdminLoginScreens-BejIjbmU.js → AdminLoginScreens-CL27ZV86.js} +1 -1
  60. package/payload/server/public/assets/AdminShell-CBcSh9Yd.js +1 -0
  61. package/payload/server/public/assets/{Checkbox-1F1tzqca.js → Checkbox-DRIcYN9r.js} +1 -1
  62. package/payload/server/public/assets/{Transcript-DkGa4CQH.js → Transcript-CRc72hkG.js} +1 -1
  63. package/payload/server/public/assets/{admin-DqQARkjy.js → admin-iKYYnv3w.js} +1 -1
  64. package/payload/server/public/assets/{browser-nDtEK6RC.js → browser--8gzAe-E.js} +1 -1
  65. package/payload/server/public/assets/calendar-7xAT81N_.js +1 -0
  66. package/payload/server/public/assets/chat-B8Z9W42Z.js +1 -0
  67. package/payload/server/public/assets/chevron-left-DDselYau.js +1 -0
  68. package/payload/server/public/assets/data-Ece320dZ.js +1 -0
  69. package/payload/server/public/assets/{graph-DFyicKd7.js → graph-B3haz2DN.js} +2 -2
  70. package/payload/server/public/assets/{graph-labels-D3BQdcpD.js → graph-labels-Ba_fyw6I.js} +1 -1
  71. package/payload/server/public/assets/{operator-BxrjWdT9.js → operator-BYL1IFz9.js} +1 -1
  72. package/payload/server/public/assets/page-Bj6lB07z.js +32 -0
  73. package/payload/server/public/assets/page-CLKVCquw.js +1 -0
  74. package/payload/server/public/assets/{public-DbdqfdYq.js → public-BKJUKC76.js} +1 -1
  75. package/payload/server/public/assets/{rotate-ccw-BkX8HODe.js → rotate-ccw-BypBbSI4.js} +1 -1
  76. package/payload/server/public/assets/useSubAccountSwitcher-4qilMyPX.css +1 -0
  77. package/payload/server/public/assets/useSubAccountSwitcher-CsRv6MEd.js +9 -0
  78. package/payload/server/public/browser.html +4 -4
  79. package/payload/server/public/calendar.html +5 -5
  80. package/payload/server/public/chat.html +8 -8
  81. package/payload/server/public/data.html +7 -7
  82. package/payload/server/public/graph.html +8 -8
  83. package/payload/server/public/index.html +10 -10
  84. package/payload/server/public/operator.html +10 -10
  85. package/payload/server/public/public.html +8 -8
  86. package/payload/server/server.js +327 -255
  87. package/payload/platform/plugins/joblogic/.claude-plugin/plugin.json +0 -21
  88. package/payload/platform/plugins/joblogic/PLUGIN.md +0 -182
  89. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/index.js +0 -193
  90. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/package.json +0 -3
  91. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts +0 -2
  92. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts.map +0 -1
  93. package/payload/platform/plugins/joblogic/mcp/dist/index.js +0 -229
  94. package/payload/platform/plugins/joblogic/mcp/dist/index.js.map +0 -1
  95. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts +0 -31
  96. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts.map +0 -1
  97. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js +0 -78
  98. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js.map +0 -1
  99. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts +0 -35
  100. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts.map +0 -1
  101. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js +0 -106
  102. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js.map +0 -1
  103. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts +0 -8
  104. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts.map +0 -1
  105. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js +0 -41
  106. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js.map +0 -1
  107. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts +0 -21
  108. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts.map +0 -1
  109. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js +0 -47
  110. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js.map +0 -1
  111. package/payload/platform/plugins/joblogic/mcp/package.json +0 -10
  112. package/payload/platform/plugins/joblogic/skills/joblogic/SKILL.md +0 -32
  113. package/payload/server/public/assets/AdminShell-D2-uBSB5.js +0 -1
  114. package/payload/server/public/assets/calendar-CO4Bwmho.js +0 -1
  115. package/payload/server/public/assets/chat-DeIge_bC.js +0 -1
  116. package/payload/server/public/assets/chevron-left-DhVdq0aN.js +0 -1
  117. package/payload/server/public/assets/data-B1GIdzHk.js +0 -1
  118. package/payload/server/public/assets/page-ByDLq_o1.js +0 -1
  119. package/payload/server/public/assets/page-D-Ep4bXd.js +0 -32
  120. package/payload/server/public/assets/useSubAccountSwitcher-DMbRhLPv.js +0 -9
  121. package/payload/server/public/assets/useSubAccountSwitcher-DS0iqSkP.css +0 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn, spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, statSync, realpathSync } from "node:fs";
3
+ import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, statSync, realpathSync, accessSync, constants as fsConstants } from "node:fs";
4
4
  import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specialist-registration.js";
5
5
  import { seedBypassPermissionsSettings, assertBypassPermissionsSeed } from "./permissions-seed.js";
6
6
  import { resolve, join, dirname } from "node:path";
@@ -13,8 +13,11 @@ import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
13
13
  import { resolveNeo4jPidfileScrub } from "./neo4j-pidfile-scrub.js";
14
14
  import { runInitLogging } from "./init-logging.js";
15
15
  import { requireSupportedPlatform, detectPlatform } from "./platform-detect.js";
16
+ import { decideClaudeUpgradePrivilege } from "./claude-upgrade-privilege.js";
17
+ import { resolveNeo4jDataDir } from "./neo4j-datadir.js";
16
18
  import { protectCommands, unprotectCommands, lsattrShowsImmutable } from "./install-immutability.js";
17
- import { renderPlist } from "./launchd-plist.js";
19
+ import { renderPlist, buildSessionManagerWrapper } from "./launchd-plist.js";
20
+ import { enumerateClaudeCandidates, pickCanonicalClaude } from "./claude-bin.js";
18
21
  import { installAllBrewPackages } from "./brew-install.js";
19
22
  import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
20
23
  import { decideChromiumAction, isSnapConfinedPath } from "./snap-chromium.js";
@@ -278,6 +281,29 @@ function canSudo() {
278
281
  const result = spawnSync("sudo", ["-n", "true"], { stdio: "pipe", timeout: 5_000 });
279
282
  return result.status === 0;
280
283
  }
284
+ /** True when the npm global prefix's node_modules dir is writable by the current
285
+ * user (nvm/Homebrew installs) — meaning `npm install -g` needs no sudo. */
286
+ function npmGlobalPrefixWritable() {
287
+ try {
288
+ const prefix = execFileSync("npm", ["prefix", "-g"], { encoding: "utf-8", timeout: 10_000 }).trim();
289
+ accessSync(join(prefix, "lib", "node_modules"), fsConstants.W_OK);
290
+ return true;
291
+ }
292
+ catch {
293
+ return false;
294
+ }
295
+ }
296
+ /** Resolve the Homebrew prefix (`/usr/local` on Intel, `/opt/homebrew` on Apple
297
+ * Silicon). Falls back to the arch default if `brew --prefix` is unavailable. */
298
+ function resolveBrewPrefix() {
299
+ try {
300
+ const out = execFileSync("brew", ["--prefix"], { encoding: "utf-8", timeout: 10_000 }).trim();
301
+ if (out)
302
+ return out;
303
+ }
304
+ catch { /* fall through to arch default */ }
305
+ return isArm64() ? "/opt/homebrew" : "/usr/local";
306
+ }
281
307
  // verified-not-asserted apt-dep reconciliation.
282
308
  // resolve package-name aliases before probing dpkg, so the
283
309
  // post-install check no longer false-negatives when apt resolves a virtual
@@ -1023,13 +1049,44 @@ function installNodejs() {
1023
1049
  console.log(" [privileged] apt-get install");
1024
1050
  shell("apt-get", ["install", "-y", "nodejs"], { sudo: true });
1025
1051
  }
1052
+ // Task 1395 gap 4 — the single canonical `claude` path resolved for this
1053
+ // install, pinned into `.env` as CLAUDE_BIN by installServiceDarwin. Null on
1054
+ // Linux (and darwin when unresolved) — callers fall back to bare `claude`.
1055
+ let RESOLVED_CLAUDE_BIN = null;
1056
+ /** Resolve the highest-version `claude` reachable from the runtime PATH the
1057
+ * launchd wrapper will use (`/opt/homebrew/bin:/usr/local/bin:$PATH`). Darwin
1058
+ * only — a Mac can carry two claude binaries at different versions; the
1059
+ * logged-in one must win, not whichever the bare PATH hits first. */
1060
+ function resolveCanonicalClaudeBin() {
1061
+ if (process.platform !== "darwin")
1062
+ return null;
1063
+ const runtimePath = `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH ?? ""}`;
1064
+ const candidates = enumerateClaudeCandidates(runtimePath, {
1065
+ isExecutable: (p) => { try {
1066
+ accessSync(p, fsConstants.X_OK);
1067
+ return true;
1068
+ }
1069
+ catch {
1070
+ return false;
1071
+ } },
1072
+ realpath: (p) => realpathSync(p),
1073
+ runVersion: (p) => {
1074
+ const r = spawnSync(p, ["--version"], { encoding: "utf-8", timeout: 5_000 });
1075
+ return r.status === 0 ? (r.stdout ?? "").toString() : null;
1076
+ },
1077
+ });
1078
+ return pickCanonicalClaude(candidates);
1079
+ }
1026
1080
  function installClaudeCode() {
1027
1081
  let needsInstall = true;
1082
+ // Darwin: pin the canonical claude before the version check so the check (and
1083
+ // any upgrade decision) targets the logged-in binary, not the stale one.
1084
+ const claudeForCheck = (RESOLVED_CLAUDE_BIN = resolveCanonicalClaudeBin()) ?? "claude";
1028
1085
  if (commandExists("claude")) {
1029
1086
  try {
1030
1087
  // `claude --version` prints "2.1.114 (Claude Code)" — extract the semver so
1031
1088
  // the equality check against `npm view` (which returns bare "2.1.114") works.
1032
- const rawVersion = execFileSync("claude", ["--version"], { encoding: "utf-8", timeout: 10_000 }).trim();
1089
+ const rawVersion = execFileSync(claudeForCheck, ["--version"], { encoding: "utf-8", timeout: 10_000 }).trim();
1033
1090
  const installed = rawVersion.match(/^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/)?.[1] ?? rawVersion;
1034
1091
  let latest = null;
1035
1092
  try {
@@ -1054,21 +1111,38 @@ function installClaudeCode() {
1054
1111
  log("3", TOTAL, "Installing Claude Code...");
1055
1112
  }
1056
1113
  if (needsInstall) {
1057
- // `npm install -g` needs write access to the global prefix, which on Linux is
1058
- // root-owned by default so we run it under sudo. When sudo requires a password
1059
- // and the installer is running non-interactively (e.g. systemd-run --scope on
1060
- // upgrade), sudo fails instantly. Skip the upgrade in that case; the running
1061
- // installation is assumed adequate. Matches the apt-get skip in step 1.
1062
- if (isLinux() && !canSudo()) {
1114
+ // `npm install -g` needs write access to the global prefix. On Linux that
1115
+ // prefix is root-owned by default, so we run under sudo and skip when sudo
1116
+ // is non-interactive (e.g. systemd-run --scope on upgrade), keeping the
1117
+ // running version. On darwin the nvm/Homebrew prefix is user-owned, so the
1118
+ // upgrade runs unprivileged; sudo there would force a password prompt that
1119
+ // hard-fails non-interactively.
1120
+ const platform = requireSupportedPlatform(process.platform);
1121
+ const decision = decideClaudeUpgradePrivilege({
1122
+ platform,
1123
+ prefixWritable: platform === "darwin" ? npmGlobalPrefixWritable() : false,
1124
+ canSudo: canSudo(),
1125
+ });
1126
+ if (decision === "skip") {
1127
+ logFile(`[create-maxy] claude-upgrade skipped reason=no-noninteractive-sudo platform=${platform}`);
1063
1128
  console.log(" Skipping Claude Code upgrade (sudo unavailable non-interactively — keeping installed version)");
1064
1129
  }
1065
1130
  else {
1131
+ const useSudo = decision === "run-sudo";
1132
+ logFile(`[create-maxy] claude-upgrade platform=${platform} sudo=${useSudo}`);
1066
1133
  console.log(" This may take 15–30 minutes on Raspberry Pi...");
1067
- console.log(" [privileged] npm install -g @anthropic-ai/claude-code@latest");
1068
- shellRetry("npm", ["install", "-g", ...NPM_NET_FLAGS, "--loglevel", "verbose", "@anthropic-ai/claude-code@latest"], { sudo: true, timeout: 2_400_000 }, // 40 min — Pi downloads can take 25+ min
1134
+ console.log(` ${useSudo ? "[privileged] " : ""}npm install -g @anthropic-ai/claude-code@latest`);
1135
+ shellRetry("npm", ["install", "-g", ...NPM_NET_FLAGS, "--loglevel", "verbose", "@anthropic-ai/claude-code@latest"], { sudo: useSudo, timeout: 2_400_000 }, // 40 min — Pi downloads can take 25+ min
1069
1136
  3, 30);
1070
1137
  }
1071
1138
  }
1139
+ // Task 1395 gap 4 — a darwin upgrade may have just installed a newer
1140
+ // npm-global claude; re-resolve so the pinned CLAUDE_BIN reflects post-upgrade
1141
+ // state. Keep the pre-upgrade pick if re-resolution finds nothing.
1142
+ RESOLVED_CLAUDE_BIN = resolveCanonicalClaudeBin() ?? RESOLVED_CLAUDE_BIN;
1143
+ if (process.platform === "darwin") {
1144
+ logFile(` [create-maxy] claude-bin=${RESOLVED_CLAUDE_BIN ?? "unresolved"}`);
1145
+ }
1072
1146
  console.log(" Registering Claude plugin marketplaces...");
1073
1147
  // pre-create the per-brand CLAUDE_CONFIG_DIR so the first
1074
1148
  // `claude plugin marketplace add` has a directory to write into.
@@ -1124,6 +1198,27 @@ function installClaudeCode() {
1124
1198
  console.log(" Removing Playwright plugin (replaced by programmatic CDP server)...");
1125
1199
  spawnSync("claude", ["plugin", "uninstall", "playwright"], { stdio: "inherit", env: claudePluginEnv() });
1126
1200
  }
1201
+ // Task 1386: suspend the joblogic connector on installs that already registered
1202
+ // it. joblogic is now in every brand's plugins.excluded, so fresh payloads never
1203
+ // ship it and it never registers — but an install that ran an older payload still
1204
+ // carries it in `claude plugin list`, where its "jobs"-triggered skill hijacks
1205
+ // generic jobs queries. Guarded on the snapshot above (joblogic's presence is
1206
+ // independent of the playwright uninstall): uninstall only if present, mirroring
1207
+ // the playwright removal. `[joblogic-suspend] present=<bool> uninstalled=<bool>`
1208
+ // is the standing signal — present=false on fresh installs (guard skipped),
1209
+ // present=true uninstalled=true on installs that had it.
1210
+ const joblogicPresent = pluginList.stdout?.includes("joblogic") ?? false;
1211
+ let joblogicUninstalled = false;
1212
+ if (joblogicPresent) {
1213
+ console.log(" Removing suspended joblogic connector plugin...");
1214
+ const removal = spawnSync("claude", ["plugin", "uninstall", "joblogic"], { stdio: "pipe", encoding: "utf-8", env: claudePluginEnv() });
1215
+ joblogicUninstalled = removal.status === 0;
1216
+ if (!joblogicUninstalled) {
1217
+ const stderrShort = (removal.stderr ?? "").split("\n")[0]?.slice(0, 200) ?? "";
1218
+ logFile(`[joblogic-suspend] ERROR uninstall exit=${removal.status} stderr=${JSON.stringify(stderrShort)}`);
1219
+ }
1220
+ }
1221
+ logFile(`[joblogic-suspend] present=${joblogicPresent} uninstalled=${joblogicUninstalled}`);
1127
1222
  // Ensure @playwright/mcp and all its dependencies (including playwright-core)
1128
1223
  // are cached. Wipe any stale npx cache for it first — killed installs on Pi
1129
1224
  // leave corrupt temp dirs that block subsequent npm operations.
@@ -1155,34 +1250,54 @@ function installClaudeCode() {
1155
1250
  console.log(` Warning: Playwright MCP cache may be incomplete (browser automation may not work).${npxResult.stderr ? ` npx stderr: ${npxResult.stderr.slice(0, 200)}` : ""}`);
1156
1251
  }
1157
1252
  }
1158
- function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j") {
1253
+ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", platform = "linux") {
1159
1254
  const password = randomBytes(24).toString("base64url");
1160
1255
  const dedicated = port !== DEFAULT_NEO4J_PORT;
1161
- const serviceName = dedicated ? `neo4j-${BRAND.hostname}` : "neo4j";
1162
- console.log(` Resetting Neo4j auth with fresh password (${serviceName})...`);
1163
- spawnSync("sudo", ["systemctl", "stop", serviceName], { stdio: "inherit" });
1164
- // Clear the system database (stores auth/roles) and dbms auth config.
1165
- // The neo4j user database (graph data) is preserved.
1166
- // set-initial-password only works before the system DB's first start,
1167
- // so we must delete it to make Neo4j treat the next start as initial.
1168
- spawnSync("sudo", ["rm", "-rf",
1256
+ // Clear the system database (stores auth/roles) and dbms auth config so the
1257
+ // next start is treated as initial and set-initial-password takes. The neo4j
1258
+ // user database (graph data) is preserved.
1259
+ const clearPaths = [
1169
1260
  `${dataDir}/data/dbms`,
1170
1261
  `${dataDir}/data/databases/system`,
1171
1262
  `${dataDir}/data/transactions/system`,
1172
- ], { stdio: "inherit" });
1173
- if (dedicated) {
1174
- const confDir = `/etc/neo4j-${BRAND.hostname}`;
1175
- // sudo env VAR=val passes the variable through sudo's env_reset
1176
- spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
1177
- stdio: "inherit",
1178
- });
1263
+ ];
1264
+ if (platform === "darwin") {
1265
+ // Homebrew Neo4j runs under a user launchd agent (brew services) and its
1266
+ // datadir is user-owned no systemctl, no sudo. This branch handles the
1267
+ // shared (default-brand) instance. Dedicated brands (realagent-code etc.)
1268
+ // set NEO4J_DEDICATED, but their dedicated provisioning (setupDedicatedNeo4j)
1269
+ // is Linux-only and aborts a dedicated-brand darwin install upstream, so a
1270
+ // non-default port never reaches here — see Task 1396 for darwin dedicated
1271
+ // support.
1272
+ console.log(" Resetting Neo4j auth with fresh password (brew services)...");
1273
+ spawnSync("brew", ["services", "stop", "neo4j"], { stdio: "inherit" });
1274
+ spawnSync("rm", ["-rf", ...clearPaths], { stdio: "inherit" });
1275
+ shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { redact: true });
1276
+ const brewStart = spawnSync("brew", ["services", "start", "neo4j"], { stdio: "inherit" });
1277
+ if (brewStart.status !== 0) {
1278
+ throw new Error(`brew services start neo4j failed (exit ${brewStart.status}) during auth reset — Neo4j must be running before the brand server starts`);
1279
+ }
1280
+ logFile(` [neo4j-darwin-auth-reset] datadir=${dataDir} service=brew-services sudo=false`);
1179
1281
  }
1180
1282
  else {
1181
- console.log(" [privileged] neo4j-admin dbms");
1182
- shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true, redact: true });
1283
+ const serviceName = dedicated ? `neo4j-${BRAND.hostname}` : "neo4j";
1284
+ console.log(` Resetting Neo4j auth with fresh password (${serviceName})...`);
1285
+ spawnSync("sudo", ["systemctl", "stop", serviceName], { stdio: "inherit" });
1286
+ spawnSync("sudo", ["rm", "-rf", ...clearPaths], { stdio: "inherit" });
1287
+ if (dedicated) {
1288
+ const confDir = `/etc/neo4j-${BRAND.hostname}`;
1289
+ // sudo env VAR=val passes the variable through sudo's env_reset
1290
+ spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
1291
+ stdio: "inherit",
1292
+ });
1293
+ }
1294
+ else {
1295
+ console.log(" [privileged] neo4j-admin dbms");
1296
+ shell("neo4j-admin", ["dbms", "set-initial-password", "--", password], { sudo: true, redact: true });
1297
+ }
1298
+ console.log(" [privileged] systemctl start");
1299
+ shell("systemctl", ["start", serviceName], { sudo: true });
1183
1300
  }
1184
- console.log(" [privileged] systemctl start");
1185
- shell("systemctl", ["start", serviceName], { sudo: true });
1186
1301
  console.log(" Waiting for Neo4j to start...");
1187
1302
  for (let i = 0; i < 15; i++) {
1188
1303
  const check = spawnSync("cypher-shell", [
@@ -1228,7 +1343,15 @@ function ensureNeo4jPassword() {
1228
1343
  const persistentPasswordFile = join(persistDir, ".neo4j-password");
1229
1344
  // Dedicated instances have their own auth database — password checks and resets
1230
1345
  // must target the dedicated port and data directory, not the shared instance.
1231
- const dataDir = NEO4J_DEDICATED ? `/var/lib/neo4j-${BRAND.hostname}` : "/var/lib/neo4j";
1346
+ // On darwin the datadir lives under the Homebrew prefix and is user-owned.
1347
+ const platform = requireSupportedPlatform(process.platform);
1348
+ const dataDir = resolveNeo4jDataDir({
1349
+ platform,
1350
+ brewPrefix: platform === "darwin" ? resolveBrewPrefix() : "",
1351
+ dedicated: NEO4J_DEDICATED,
1352
+ hostname: BRAND.hostname,
1353
+ });
1354
+ logFile(`[create-maxy] neo4j-datadir=${dataDir} platform=${platform}`);
1232
1355
  // 1. Same-brand check: if our own password file exists and works, done (upgrade path).
1233
1356
  if (existsSync(passwordFile)) {
1234
1357
  const existingPassword = readFileSync(passwordFile, "utf-8").trim();
@@ -1255,7 +1378,7 @@ function ensureNeo4jPassword() {
1255
1378
  console.log(" Neo4j has existing data. Resetting auth only (data preserved)...");
1256
1379
  logFile(` Neo4j auth reset — clearing dbms auth in ${dataDir}, preserving databases + transactions`);
1257
1380
  }
1258
- const password = resetNeo4jAuth(NEO4J_PORT, dataDir);
1381
+ const password = resetNeo4jAuth(NEO4J_PORT, dataDir, platform);
1259
1382
  writeFileSync(passwordFile, password, { mode: 0o600 });
1260
1383
  mkdirSync(persistDir, { recursive: true });
1261
1384
  writeFileSync(persistentPasswordFile, password, { mode: 0o600 });
@@ -2521,6 +2644,39 @@ function buildPlatform() {
2521
2644
  }
2522
2645
  console.log(` Installing claude-session-manager dependencies (${csmDir})...`);
2523
2646
  shellRetry("npm", ["install", "--omit=dev", ...NPM_NET_FLAGS], { cwd: csmDir }, 3, 15);
2647
+ // Task 1395 gap 3 — on darwin node-pty ships uncompiled. Its install script
2648
+ // is `node scripts/prebuild.js || node-gyp rebuild`; on this platform
2649
+ // prebuild.js exits 0 without placing a binary, so `node-gyp rebuild` never
2650
+ // runs and every PTY spawn fails `posix_spawnp failed`. `npm rebuild` just
2651
+ // re-runs the same broken script — proven inert. The fix is to invoke
2652
+ // `node-gyp rebuild` DIRECTLY in node-pty's resolved directory, bypassing
2653
+ // prebuild.js. node-pty hoists to the platform workspace root, so resolve
2654
+ // its real location rather than assuming csmDir. Xcode Command Line Tools
2655
+ // are the prerequisite. Blocker, not warning: no addon means no session
2656
+ // ever spawns.
2657
+ if (process.platform === "darwin") {
2658
+ const nodePtyCandidates = [
2659
+ join(INSTALL_DIR, "platform", "node_modules", "node-pty"),
2660
+ join(csmDir, "node_modules", "node-pty"),
2661
+ ];
2662
+ const nodePtyDir = nodePtyCandidates.find((d) => existsSync(d));
2663
+ if (!nodePtyDir) {
2664
+ throw new Error(`node-pty not found after install (looked in ${nodePtyCandidates.join(", ")}). ` +
2665
+ `The platform / claude-session-manager npm install did not place it.`);
2666
+ }
2667
+ console.log(` Rebuilding node-pty native addon (darwin, ${nodePtyDir})...`);
2668
+ const rebuilt = spawnSync("npx", ["--yes", "node-gyp", "rebuild"], { cwd: nodePtyDir, stdio: "inherit", timeout: 300_000 });
2669
+ const nodePtyRelease = join(nodePtyDir, "build", "Release");
2670
+ const ptyNode = join(nodePtyRelease, "pty.node");
2671
+ const spawnHelper = join(nodePtyRelease, "spawn-helper");
2672
+ const built = existsSync(ptyNode) && existsSync(spawnHelper);
2673
+ logFile(` [create-maxy] node-pty build=${built ? "rebuilt" : "failed"} exit=${rebuilt.status ?? "null"} dir=${nodePtyDir} pty.node=${existsSync(ptyNode)} spawn-helper=${existsSync(spawnHelper)}`);
2674
+ if (!built) {
2675
+ throw new Error(`node-pty native addon missing after node-gyp rebuild in ${nodePtyDir} ` +
2676
+ `(pty.node=${existsSync(ptyNode)} spawn-helper=${existsSync(spawnHelper)}). ` +
2677
+ `Install Xcode Command Line Tools (xcode-select --install) and re-run.`);
2678
+ }
2679
+ }
2524
2680
  }
2525
2681
  // (maxy-code, Task 677 P2) — whatsapp-channel service has its own
2526
2682
  // package.json declaring @modelcontextprotocol/sdk. It ships dist/ +
@@ -3182,8 +3338,20 @@ function installServiceDarwin() {
3182
3338
  envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `${key}=${value}\n`;
3183
3339
  }
3184
3340
  }
3341
+ // Task 1395 gap 4 — pin the canonical claude path (resolved in phase 3) so
3342
+ // every runtime spawn under launchd uses the logged-in binary, not whichever
3343
+ // the bare launchd PATH resolves first. Written only when resolved; an unset
3344
+ // CLAUDE_BIN makes runtime callers fall back to bare `claude` on PATH.
3345
+ const canonicalClaude = RESOLVED_CLAUDE_BIN ?? resolveCanonicalClaudeBin();
3346
+ if (canonicalClaude) {
3347
+ const re = /^CLAUDE_BIN=.*$/m;
3348
+ if (re.test(envContent))
3349
+ envContent = envContent.replace(re, `CLAUDE_BIN=${canonicalClaude}`);
3350
+ else
3351
+ envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `CLAUDE_BIN=${canonicalClaude}\n`;
3352
+ }
3185
3353
  writeFileSync(envPath, envContent);
3186
- logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude`);
3354
+ logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude, CLAUDE_BIN=${canonicalClaude ?? "unresolved(PATH)"}`);
3187
3355
  }
3188
3356
  catch (err) {
3189
3357
  console.error(` WARNING: failed to write .env to ${envPath}: ${err instanceof Error ? err.message : String(err)}`);
@@ -3245,6 +3413,66 @@ function installServiceDarwin() {
3245
3413
  logFile(` [create-maxy] launchd-plist=${path} loaded=false exit=${bootstrap.status}`);
3246
3414
  throw new Error(`launchctl bootstrap ${gui()} ${path} failed (exit ${bootstrap.status}): ${stderr}`);
3247
3415
  }
3416
+ // Task 1395 gap 2 — the claude-session-manager. On Linux it is a separate
3417
+ // systemd user unit (buildClaudeSessionManagerUnitFile) the brand unit
3418
+ // Requires=; darwin authored no such unit, so nothing listened on the manager
3419
+ // port and every session spawn returned `manager-unreachable`. Author a second
3420
+ // LaunchAgent mirroring the Linux unit: a wrapper sources .env + the
3421
+ // manager-only vars, then execs `node dist/index.js`.
3422
+ const smLabel = `${launchdLabel()}-claude-session-manager`;
3423
+ const smPlistPath = join(launchAgentsDir(), `${smLabel}.plist`);
3424
+ const smWrapperPath = join(persistDir, "claude-session-manager-wrapper.sh");
3425
+ writeFileSync(smWrapperPath, buildSessionManagerWrapper({
3426
+ envPath,
3427
+ installDir: INSTALL_DIR,
3428
+ nodeBin,
3429
+ persistDir,
3430
+ brandPort: PORT,
3431
+ }));
3432
+ chmodSync(smWrapperPath, 0o755);
3433
+ const smPlist = renderPlist({
3434
+ label: smLabel,
3435
+ programArguments: ["/bin/bash", smWrapperPath],
3436
+ stdoutPath: join(logsDir, "server.log"),
3437
+ stderrPath: join(logsDir, "server.log"),
3438
+ keepAlive: true,
3439
+ runAtLoad: true,
3440
+ workingDirectory: `${INSTALL_DIR}/platform/services/claude-session-manager`,
3441
+ });
3442
+ writeFileSync(smPlistPath, smPlist);
3443
+ logFile(` ${smPlistPath} written (${smPlist.length} bytes)`);
3444
+ spawnSync("launchctl", ["bootout", `${gui()}/${smLabel}`], { stdio: "pipe" });
3445
+ const smBootstrap = spawnSync("launchctl", ["bootstrap", gui(), smPlistPath], {
3446
+ stdio: "pipe",
3447
+ encoding: "utf-8",
3448
+ timeout: 15_000,
3449
+ });
3450
+ if (smBootstrap.status === 0) {
3451
+ console.log(` [launchd] bootstrap ${gui()}/${smLabel} ok`);
3452
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true`);
3453
+ }
3454
+ else {
3455
+ const smStderr = (smBootstrap.stderr ?? "").trim();
3456
+ console.error(` [launchd] bootstrap ${smLabel} returned ${smBootstrap.status}: ${smStderr}`);
3457
+ logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBootstrap.status}`);
3458
+ throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBootstrap.status}): ${smStderr}`);
3459
+ }
3460
+ // Health-probe the manager (/healthz) so a silent boot failure surfaces at
3461
+ // install time, not at first session spawn as `manager-unreachable`.
3462
+ console.log(" Waiting for session manager...");
3463
+ let smUp = false;
3464
+ for (let i = 0; i < 15; i++) {
3465
+ const probe = spawnSync("curl", ["-sf", `http://127.0.0.1:${BRAND.claudeSessionManagerPort}/healthz`, "-o", "/dev/null"], { timeout: 3000 });
3466
+ if (probe.status === 0) {
3467
+ smUp = true;
3468
+ break;
3469
+ }
3470
+ spawnSync("sleep", ["2"]);
3471
+ }
3472
+ logFile(` [create-maxy] session-manager-health=${smUp ? "ok" : "timeout"} port=${BRAND.claudeSessionManagerPort}`);
3473
+ if (!smUp) {
3474
+ console.log(` Session manager not yet healthy on ${BRAND.claudeSessionManagerPort} — check ${join(logsDir, "server.log")}.`);
3475
+ }
3248
3476
  // Wait for the server to come up.
3249
3477
  console.log(" Waiting for web server...");
3250
3478
  let webServerUp = false;
@@ -26,16 +26,32 @@ function xmlEscape(s) {
26
26
  .replace(/'/g, "&apos;");
27
27
  }
28
28
  /**
29
- * Render a launchd LaunchAgent plist as XML. Pure: same input → same output
30
- * forever. The caller writes this to `~/Library/LaunchAgents/<label>.plist`
31
- * with mode 0644 and then runs `launchctl bootstrap gui/$UID <path>`.
32
- *
33
- * The output is the canonical plist 1.0 form launchd accepts XML preamble,
34
- * PLIST PUBLIC DOCTYPE, <plist version="1.0"> wrapping a single <dict>. Key
35
- * order inside the dict mirrors Apple's LaunchAgent examples for grep-ability:
36
- * Label, ProgramArguments, StandardOutPath, StandardErrorPath, [WorkingDirectory,]
37
- * KeepAlive, RunAtLoad.
29
+ * Bash wrapper for the darwin claude-session-manager LaunchAgent (Task 1395
30
+ * gap 2). launchd has no systemd `EnvironmentFile=`/`Environment=` analogue, so
31
+ * the wrapper sources the brand `.env` and then exports the manager-only vars
32
+ * the Linux systemd unit (buildClaudeSessionManagerUnitFile) sets that `.env`
33
+ * omitsCLAUDE_SESSION_MANAGER_PERSIST_DIR, PLATFORM_ROOT, MAXY_BRAND_PORT,
34
+ * PLATFORM_PORT. NEO4J_PASSWORD self-resolves from `<persistDir>/.neo4j-password`
35
+ * (as on Linux); the Pi-only CDP/VNC display vars are deliberately excluded —
36
+ * darwin runs no display stack.
38
37
  */
38
+ export function buildSessionManagerWrapper(o) {
39
+ const managerDir = `${o.installDir}/platform/services/claude-session-manager`;
40
+ return [
41
+ "#!/bin/bash",
42
+ "# generated by create-maxy installServiceDarwin() — Task 1395 gap 2.",
43
+ "# Sources .env then exports the manager-only vars the Linux systemd unit",
44
+ "# sets that .env omits, and execs the claude-session-manager.",
45
+ `set -a; [ -f "${o.envPath}" ] && . "${o.envPath}"; set +a`,
46
+ `export CLAUDE_SESSION_MANAGER_PERSIST_DIR="${o.persistDir}"`,
47
+ `export PLATFORM_ROOT="${o.installDir}/platform"`,
48
+ `export MAXY_BRAND_PORT="${o.brandPort}"`,
49
+ `export PLATFORM_PORT="${o.brandPort}"`,
50
+ `cd "${managerDir}"`,
51
+ `exec ${o.nodeBin} dist/index.js`,
52
+ "",
53
+ ].join("\n");
54
+ }
39
55
  export function renderPlist(spec) {
40
56
  const lines = [];
41
57
  lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
@@ -0,0 +1,15 @@
1
+ // Pure resolution of the Neo4j data directory the installer resets auth against.
2
+ // No spawnSync, no fs — the caller injects the brew prefix (from `brew --prefix`)
3
+ // on darwin. Peer of the other install decision helpers.
4
+ //
5
+ // darwin: Homebrew installs Neo4j's datadir at `<brew prefix>/var/neo4j`
6
+ // (/usr/local on Intel, /opt/homebrew on Apple Silicon), user-owned. Only the
7
+ // shared instance exists on darwin — dedicated (systemd) instances are Linux
8
+ // only — so `dedicated` is not consulted here.
9
+ // linux: /var/lib/neo4j (shared) or /var/lib/neo4j-<hostname> (dedicated).
10
+ export function resolveNeo4jDataDir(input) {
11
+ if (input.platform === "darwin") {
12
+ return `${input.brewPrefix}/var/neo4j`;
13
+ }
14
+ return input.dedicated ? `/var/lib/neo4j-${input.hostname}` : "/var/lib/neo4j";
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.388",
3
+ "version": "0.1.391",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -56,7 +56,7 @@
56
56
  "core": ["admin", "memory", "docs", "cloudflare", "anthropic", "workflows", "work", "scheduling", "email", "contacts", "projects", "prompt-optimiser", "browser"],
57
57
  "defaultEnabled": ["business-assistant", "sales"],
58
58
  "available": ["connector", "telegram", "deep-research", "whatsapp", "replicate", "linkedin-import", "notion-import", "obsidian-import", "x-import", "slides"],
59
- "excluded": []
59
+ "excluded": ["joblogic"]
60
60
  },
61
61
 
62
62
  "externalPlugins": [
@@ -64,11 +64,6 @@
64
64
  "source": "./graph-viewer",
65
65
  "version": "0.1.0"
66
66
  },
67
- {
68
- "name": "joblogic",
69
- "source": "./joblogic",
70
- "version": "0.1.0"
71
- },
72
67
  {
73
68
  "name": "linkedin-extension",
74
69
  "source": "./linkedin-extension",
@@ -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:ca771c68e3f7a68883043ba838fc2dd07004d77ac08f3326f2f60a9b7bc62be0
4
+ content-hash: sha256:11f2a3458fb6227490d33fcf54f859595c215184dbb78051484df353b6fbf227
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -539,7 +539,7 @@ Source: https://docs.getmaxy.com/install/macos.md
539
539
 
540
540
  # Installing Maxy Code on macOS
541
541
 
542
- End-to-end install for a fresh macOS account on Apple Silicon (M-series). Every command is copy-pasteable and uses auto-yes flags so nothing prompts interactively.
542
+ End-to-end install for a fresh macOS account on Apple Silicon (M-series) or Intel (x86_64). Every command is copy-pasteable and uses auto-yes flags so nothing prompts interactively.
543
543
 
544
544
  The doc is brand-aware. Examples use the default brand `maxy-code`; substitute `realagent-code` (or any other brand under `maxy-code/brands/`) wherever you want a parallel install. Each brand is fully isolated — its own persist directory, its own LaunchAgent, its own admin UI port, its own `CLAUDE_CONFIG_DIR`.
545
545
 
@@ -549,7 +549,7 @@ The doc is brand-aware. Examples use the default brand `maxy-code`; substitute `
549
549
  ## Requirements
550
550
 
551
551
  - macOS 14 (Sonoma) or newer. The installer refuses to run on 13 and below; you will see `[create-maxy] platform=darwin macos=… — refusing: macOS 14+ required`.
552
- - Apple Silicon (M1/M2/M3/M4). Intel Macs are not part of the supported matrix the installer pins `node@22` from Homebrew's Apple Silicon cellar (`/opt/homebrew`) and other paths assume that prefix.
552
+ - Apple Silicon (M1/M2/M3/M4) or Intel (x86_64). The installer resolves the Node binary and Homebrew prefix at install time and stamps a runtime `PATH` of `/opt/homebrew/bin:/usr/local/bin:$PATH`, so both the Apple Silicon cellar (`/opt/homebrew`) and the Intel cellar (`/usr/local`) work.
553
553
  - Admin (sudo) account. The installer asks for your password once when it sets the system hostname via `scutil`; everything else runs unprivileged.
554
554
  - A working internet connection — Homebrew, npm, and Cloudflare endpoints are all reached during install.
555
555
 
@@ -567,7 +567,7 @@ brew link --overwrite --force node@22
567
567
  node --version
568
568
  ```
569
569
 
570
- Node from the system PATH must resolve to `/opt/homebrew/opt/node@22/bin/node`. If `which node` points anywhere else, fix the PATH before continuing — the installer reads node from `PATH` and a 20.x binary will trip the engines check.
570
+ Node from the system PATH must resolve to Homebrew's `node@22` (`/opt/homebrew/opt/node@22/bin/node` on Apple Silicon, `/usr/local/opt/node@22/bin/node` on Intel). If `which node` points anywhere else, fix the PATH before continuing — the installer reads node from `PATH` and a 20.x binary will trip the engines check.
571
571
 
572
572
  ## 2. Run the installer
573
573
 
@@ -2012,6 +2012,8 @@ Source: https://docs.getmaxy.com/joblogic.md
2012
2012
 
2013
2013
  # JobLogic
2014
2014
 
2015
+ > **Status: Suspended.** This connector is not shipped on any brand. Its skill triggers on the generic word "jobs", which collides with SiteDesk's `Job` graph node and mis-routes every generic jobs question to a connector that ships inert. It is excluded from every brand payload (`plugins.excluded`), and the installer de-registers it on any install that still carries it. The source stays in-tree; re-introduction reverts the exclusion and reworks the trigger surface under a future task. Everything below describes the connector as designed, for when it is re-introduced.
2016
+
2015
2017
  Connect JobLogic, the field-service management system, so the agent can read and update your jobs end to end — customers, sites, jobs, engineers, visits, quotes, invoices, assets, and timesheets — over JobLogic's REST API.
2016
2018
 
2017
2019
  This connector is built for one team that already runs JobLogic as its system of record. Your field staff send a voice note or message to Maxy from the field; Maxy works out what they mean and makes the JobLogic update for them. Your staff never touch the JobLogic API themselves, so only Maxy's own connection talks to JobLogic.
@@ -2590,7 +2592,7 @@ either is a regression.
2590
2592
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2591
2593
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
2592
2594
 
2593
- **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. **For admin sessions, permission mode is fixed (Bypass) and not operator-chosen, while model and effort ARE operator-chosen at launch** via the composer's `+` new-session popover — a radio model list (Opus / Sonnet / Haiku) plus a 5-stop Faster→Smarter effort slider (Low / Medium / High / Extra / Max, where `max` applies via the `--effort` argv rather than `settings.json`); the popover description lives in `.docs/admin-webchat-native-channel.md`. Absent a pick (and on every resume), the daemon-pinned defaults govern: the `/chat` composer *footer* carries no live re-seat pickers — `rc-daemon.ts` pins settings.json `model`/`effortLevel`/`permissions.defaultMode` to `claude-opus-4-8[1m]` / `medium` / `bypassPermissions` regardless of `account.json` (read by the rc-spawn webchat path), and `pty-spawner.ts` forces the claude.ai/code PTY `--permission-mode` to `bypassPermissions` for `role=admin`. Public sessions are fixed to `claude-sonnet-5` (the `/public-spawn` chokepoint + `buildPublicWebchatSpawnRequest`), keeping `dontAsk`. The composer footer instead renders a **read-only** params row (mode · model · effort labels) plus a context-window usage figure (`pct% · ~used/~window`), all sourced from the session pointer; the full path is in `.docs/admin-webchat-native-channel.md`. The composer cannot re-seat a session, so the "no transcript" 400 on a brand-new chat is unreachable; the `/api/admin/session-reseat` + `/api/webchat/settings` routes and the dashboard re-seat form stay as off-composer surfaces. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2595
+ **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. **For admin sessions, permission mode is fixed (Bypass) and not operator-chosen, while model and effort ARE operator-chosen at launch** via the composer's `+` new-session popover — a radio model list (Opus / Sonnet / Haiku) plus a 5-stop Faster→Smarter effort slider (Low / Medium / High / Extra / Max, where `max` applies via the `--effort` argv rather than `settings.json`); the popover description lives in `.docs/admin-webchat-native-channel.md`. Absent a pick (and on every resume), the daemon-pinned defaults govern: the `/chat` composer *footer* carries no live re-seat pickers — `rc-daemon.ts` pins settings.json `model`/`effortLevel`/`permissions.defaultMode` to `claude-opus-4-8[1m]` / `medium` / `bypassPermissions` regardless of `account.json` (read by the rc-spawn webchat path), and `pty-spawner.ts` forces the claude.ai/code PTY `--permission-mode` to `bypassPermissions` for `role=admin`. Public sessions are fixed to `claude-sonnet-5` (the `/public-spawn` chokepoint + `buildPublicWebchatSpawnRequest`), keeping `dontAsk`. **WhatsApp admin sessions override the daemon default:** the `/rc-spawn` handler pins `claude-sonnet-5` / `--effort max` / `bypassPermissions` for a `role=admin` WhatsApp spawn (constants `WHATSAPP_ADMIN_MODEL`/`WHATSAPP_ADMIN_EFFORT` in `http-server.ts`), pushing `--model`/`--effort` on both the fresh spawn and every resume so the params hold for the session's whole life. The override is scoped to sessions *born* under it: a fresh WhatsApp admin spawn stamps its classification sidecar `model=claude-sonnet-5`, and a resume re-asserts Sonnet/max only when that stamp is present — so a WhatsApp session created before this change (sidecar `model=null`) finishes its life on the Opus/medium default. Webchat and Telegram admin sessions are unaffected; public WhatsApp inbound never spawns. The composer footer instead renders a **read-only** params row (mode · model · effort labels) plus a context-window usage figure (`pct% · ~used/~window`), all sourced from the session pointer; the full path is in `.docs/admin-webchat-native-channel.md`. The composer cannot re-seat a session, so the "no transcript" 400 on a brand-new chat is unreachable; the `/api/admin/session-reseat` + `/api/webchat/settings` routes and the dashboard re-seat form stay as off-composer surfaces. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2594
2596
 
2595
2597
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
2596
2598
 
@@ -2647,6 +2649,23 @@ it is account-scoped by a graph-ownership check (the attachmentId must map to a
2647
2649
  partition. Resolution emits `[admin/sidebar-artefacts] download-resolved via=…`;
2648
2650
  a web/transient doc with no persisted file is `not-downloadable reason=no-persisted-file`.
2649
2651
 
2652
+ **Outbound file-delivery account scope (WhatsApp / Telegram).** When the agent
2653
+ sends a file to a channel — WhatsApp `reply-document` or `SendUserFile`, Telegram
2654
+ `SendUserFile` — the delivery core validates the file path against a single
2655
+ account directory (`<DATA_ROOT>/accounts/<maxyAccountId>/`) and rejects anything
2656
+ outside it. That validation scope is the sender's **effective session account**:
2657
+ the account the session runs in, which for a WhatsApp account-manager routed to a
2658
+ client sub-account is the gate-resolved sub-account (where the session's files
2659
+ live), and for every other sender is the house account. It is **not** the
2660
+ house/channel-routing account from `resolvePlatformAccountId()` (that helper
2661
+ stays the inbound-persistence scope only). WhatsApp threads the effective account
2662
+ from the inbound gate through the per-sender doc context (reply-document) and the
2663
+ native file follower (`SendUserFile`); Telegram has no account-manager routing,
2664
+ so the session's own spawn account is the scope. A rejected delivery logs
2665
+ `[whatsapp:outbound] document REJECTED … reason=outside_account_directory
2666
+ maxyAccountId=<scope>` (and the Telegram twin), so a wrong-account mismatch is
2667
+ greppable from the log alone.
2668
+
2650
2669
  **`/data` File panel — refresh and reload-survival.** The panel listing is a
2651
2670
  snapshot from its last fetch, so a file an agent writes (or an upload/delete
2652
2671
  elsewhere) leaves stale rows and timestamps until something re-fetches. A
@@ -102,6 +102,10 @@ The metadata pane surfaces two distinct identifier values. The three-id model wa
102
102
  | `sessionId` | Claude's session. Two phases: bridge suffix (`session_xxx`, the URL segment in `claude.ai/code/<session_xxx>`, set when claude prints the `/remote-control` URL) and JSONL basename UUID (claude's intrinsic id on disk, bound when the first turn flushes the JSONL). Both phases coexist on a live row after URL capture; the manager wire emits the bridge form when set, falling back to the JSONL basename in the pre-URL-capture window. The resolver routes either phase to the same row, so callers never need to choose. | `sessionId` (collapsed from the earlier three-field surface `sessionId` + `claudeSessionId` + `bridgeSessionId`) | `sessionId=` |
103
103
  | `accountId` | Account / workspace UUID. For the admin channel, the value the manager carries as `senderId` IS the accountId; for non-admin channels the same wire field carries a phone / email / chat-id (the channel-level sender), so its log key on those paths is still `senderId=`. | `senderId` (channel-agnostic) | `accountId=` in admin-flow emit sites; `senderId=` in channel-bridge sites |
104
104
 
105
+ ## Per-session usage & cost metering
106
+
107
+ Each session row carries a usage icon (Lucide `Gauge`) that opens a modal with a per-day table: active time (thinking / messaging / tool use), token classes, and estimated GBP cost for Opus 4.8 and Sonnet 4.6, plus a TOTAL row. The modal fetches `GET /api/admin/session-usage?sessionId=<uuid>`, which proxies the manager's `GET /:sessionId/metering`. The admin agent reaches the identical table through the work-plugin MCP tool `session-metering` (loopback to the same endpoint) — one deterministic core, two adapters, byte-identical data. The full model, pricing provenance, and caveats (span ≠ active; cache-read dominates cost; tokens are turn-level with thinking redacted; Sonnet is a tokenizer-adjusted estimate; `cache_creation` priced at the 5-minute rate) are in [`session-metering.md`](../../../.docs/session-metering.md).
108
+
105
109
  ## Memory MCP write-path outcome lines
106
110
 
107
111
  Every write-path tool in the memory plugin MCP emits one structured line per invocation to `server.log` via the loopback `/api/admin/log-ingest` route. The operator can answer "did this write succeed?" from a single greppable surface — no need to cross-reference the per-process `mcp-memory-stderr-<date>.log`.