@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.
- package/dist/__tests__/claude-bin.test.js +51 -0
- package/dist/__tests__/claude-upgrade-privilege.test.js +17 -0
- package/dist/__tests__/joblogic-excluded.test.js +39 -0
- package/dist/__tests__/launchd-plist.test.js +36 -1
- package/dist/__tests__/macos-darwin-branch.test.js +18 -0
- package/dist/__tests__/neo4j-datadir.test.js +14 -0
- package/dist/claude-bin.js +78 -0
- package/dist/claude-upgrade-privilege.js +21 -0
- package/dist/index.js +262 -34
- package/dist/launchd-plist.js +25 -9
- package/dist/neo4j-datadir.js +15 -0
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/plugins/.claude-plugin/marketplace.json +0 -5
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +24 -5
- package/payload/platform/plugins/docs/references/admin-session.md +4 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +18 -1
- package/payload/platform/plugins/docs/references/joblogic.md +2 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
- package/payload/platform/plugins/work/PLUGIN.md +3 -0
- package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts +2 -0
- package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts.map +1 -0
- package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js +98 -0
- package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js.map +1 -0
- package/payload/platform/plugins/work/mcp/dist/index.js +16 -0
- package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts +53 -0
- package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts.map +1 -0
- package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js +80 -0
- package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js.map +1 -0
- package/payload/platform/plugins/work/mcp/package.json +2 -1
- package/payload/platform/scripts/smoke-boot-services.sh +35 -10
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts +11 -0
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +37 -0
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +98 -8
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pricing.d.ts +45 -0
- package/payload/platform/services/claude-session-manager/dist/pricing.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pricing.js +57 -0
- package/payload/platform/services/claude-session-manager/dist/pricing.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts +38 -0
- package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/session-metering.js +292 -0
- package/payload/platform/services/claude-session-manager/dist/session-metering.js.map +1 -0
- package/payload/platform/templates/specialists/agents/project-manager.md +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-BejIjbmU.js → AdminLoginScreens-CL27ZV86.js} +1 -1
- package/payload/server/public/assets/AdminShell-CBcSh9Yd.js +1 -0
- package/payload/server/public/assets/{Checkbox-1F1tzqca.js → Checkbox-DRIcYN9r.js} +1 -1
- package/payload/server/public/assets/{Transcript-DkGa4CQH.js → Transcript-CRc72hkG.js} +1 -1
- package/payload/server/public/assets/{admin-DqQARkjy.js → admin-iKYYnv3w.js} +1 -1
- package/payload/server/public/assets/{browser-nDtEK6RC.js → browser--8gzAe-E.js} +1 -1
- package/payload/server/public/assets/calendar-7xAT81N_.js +1 -0
- package/payload/server/public/assets/chat-B8Z9W42Z.js +1 -0
- package/payload/server/public/assets/chevron-left-DDselYau.js +1 -0
- package/payload/server/public/assets/data-Ece320dZ.js +1 -0
- package/payload/server/public/assets/{graph-DFyicKd7.js → graph-B3haz2DN.js} +2 -2
- package/payload/server/public/assets/{graph-labels-D3BQdcpD.js → graph-labels-Ba_fyw6I.js} +1 -1
- package/payload/server/public/assets/{operator-BxrjWdT9.js → operator-BYL1IFz9.js} +1 -1
- package/payload/server/public/assets/page-Bj6lB07z.js +32 -0
- package/payload/server/public/assets/page-CLKVCquw.js +1 -0
- package/payload/server/public/assets/{public-DbdqfdYq.js → public-BKJUKC76.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BkX8HODe.js → rotate-ccw-BypBbSI4.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-4qilMyPX.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CsRv6MEd.js +9 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +5 -5
- package/payload/server/public/chat.html +8 -8
- package/payload/server/public/data.html +7 -7
- package/payload/server/public/graph.html +8 -8
- package/payload/server/public/index.html +10 -10
- package/payload/server/public/operator.html +10 -10
- package/payload/server/public/public.html +8 -8
- package/payload/server/server.js +327 -255
- package/payload/platform/plugins/joblogic/.claude-plugin/plugin.json +0 -21
- package/payload/platform/plugins/joblogic/PLUGIN.md +0 -182
- package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/index.js +0 -193
- package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/package.json +0 -3
- package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts +0 -2
- package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/index.js +0 -229
- package/payload/platform/plugins/joblogic/mcp/dist/index.js.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts +0 -31
- package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js +0 -78
- package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts +0 -35
- package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js +0 -106
- package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts +0 -8
- package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js +0 -41
- package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts +0 -21
- package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js +0 -47
- package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js.map +0 -1
- package/payload/platform/plugins/joblogic/mcp/package.json +0 -10
- package/payload/platform/plugins/joblogic/skills/joblogic/SKILL.md +0 -32
- package/payload/server/public/assets/AdminShell-D2-uBSB5.js +0 -1
- package/payload/server/public/assets/calendar-CO4Bwmho.js +0 -1
- package/payload/server/public/assets/chat-DeIge_bC.js +0 -1
- package/payload/server/public/assets/chevron-left-DhVdq0aN.js +0 -1
- package/payload/server/public/assets/data-B1GIdzHk.js +0 -1
- package/payload/server/public/assets/page-ByDLq_o1.js +0 -1
- package/payload/server/public/assets/page-D-Ep4bXd.js +0 -32
- package/payload/server/public/assets/useSubAccountSwitcher-DMbRhLPv.js +0 -9
- 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(
|
|
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
|
|
1058
|
-
// root-owned by default
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
|
|
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("
|
|
1068
|
-
shellRetry("npm", ["install", "-g", ...NPM_NET_FLAGS, "--loglevel", "verbose", "@anthropic-ai/claude-code@latest"], { sudo:
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
]
|
|
1173
|
-
if (
|
|
1174
|
-
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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;
|
package/dist/launchd-plist.js
CHANGED
|
@@ -26,16 +26,32 @@ function xmlEscape(s) {
|
|
|
26
26
|
.replace(/'/g, "'");
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
+
* omits — CLAUDE_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
|
@@ -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": [
|
|
@@ -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:
|
|
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)
|
|
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
|
|
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`.
|