@rubytech/create-maxy 1.0.739 → 1.0.741

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 (46) hide show
  1. package/dist/index.js +130 -79
  2. package/package.json +1 -1
  3. package/payload/platform/lib/brand-templating/dist/index.d.ts +18 -0
  4. package/payload/platform/lib/brand-templating/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/brand-templating/dist/index.js +69 -0
  6. package/payload/platform/lib/brand-templating/dist/index.js.map +1 -0
  7. package/payload/platform/lib/brand-templating/src/index.ts +76 -0
  8. package/payload/platform/lib/brand-templating/tsconfig.json +8 -0
  9. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-write/dist/index.js +23 -1
  11. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-write/src/index.ts +27 -4
  13. package/payload/platform/neo4j/schema.cypher +5 -2
  14. package/payload/platform/package.json +2 -2
  15. package/payload/platform/plugins/admin/mcp/dist/index.js +6 -1
  16. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +7 -7
  18. package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +1 -1
  19. package/payload/platform/plugins/anthropic/skills/get-api-key/SKILL.md +2 -2
  20. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
  21. package/payload/platform/plugins/docs/references/access-control.md +10 -10
  22. package/payload/platform/plugins/docs/references/contacts-guide.md +11 -11
  23. package/payload/platform/plugins/docs/references/deployment.md +14 -13
  24. package/payload/platform/plugins/docs/references/getting-started.md +19 -19
  25. package/payload/platform/plugins/docs/references/internals.md +4 -4
  26. package/payload/platform/plugins/docs/references/memory-guide.md +21 -21
  27. package/payload/platform/plugins/docs/references/migration-guide.md +5 -5
  28. package/payload/platform/plugins/docs/references/platform.md +9 -9
  29. package/payload/platform/plugins/docs/references/plugins-guide.md +20 -12
  30. package/payload/platform/plugins/docs/references/projects-guide.md +10 -10
  31. package/payload/platform/plugins/docs/references/settings.md +13 -13
  32. package/payload/platform/plugins/docs/references/telegram-guide.md +14 -14
  33. package/payload/platform/plugins/docs/references/troubleshooting.md +23 -23
  34. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +6 -6
  35. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +2 -2
  36. package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +2 -2
  37. package/payload/platform/plugins/workflows/mcp/test-workflows.sh +5 -1
  38. package/payload/platform/scripts/dedupe-userprofile-ghosts.sh +388 -0
  39. package/payload/platform/scripts/embed-backfill.sh +8 -1
  40. package/payload/platform/scripts/migrate-import.sh +42 -1
  41. package/payload/platform/scripts/seed-neo4j.sh +8 -1
  42. package/payload/server/chunk-PQ6LDXZ4.js +2997 -0
  43. package/payload/server/chunk-W6ZUNLLS.js +9446 -0
  44. package/payload/server/client-pool-DQBHSKAF.js +28 -0
  45. package/payload/server/maxy-edge.js +2 -2
  46. package/payload/server/server.js +41 -3
package/dist/index.js CHANGED
@@ -138,14 +138,23 @@ function shell(command, args, options) {
138
138
  stdio: "inherit",
139
139
  timeout: options?.timeout ?? 300_000,
140
140
  cwd: options?.cwd,
141
+ env: options?.env,
141
142
  });
142
143
  const dur = ((Date.now() - start) / 1000).toFixed(1);
144
+ // bestEffort (Task 787): tear-down ops on units/state that may or may not
145
+ // exist (stop/disable a system service we may never have started, reset-failed
146
+ // a freshly-created unit) log the non-zero exit but do not throw. Reserved
147
+ // for the "may not exist" pattern only — never use for ops that must succeed.
143
148
  if (result.signal) {
144
149
  logFile(` KILLED (${result.signal}) after ${dur}s`);
150
+ if (options?.bestEffort)
151
+ return;
145
152
  throw new Error(`Command killed (${result.signal}) after ${dur}s: ${cmd} ${cmdArgs.join(" ")}`);
146
153
  }
147
154
  if (result.status !== 0) {
148
- logFile(` FAILED (exit ${result.status}) after ${dur}s`);
155
+ logFile(` ${options?.bestEffort ? "best-effort non-zero" : "FAILED"} (exit ${result.status}) after ${dur}s`);
156
+ if (options?.bestEffort)
157
+ return;
149
158
  throw new Error(`Command failed (exit ${result.status}) after ${dur}s: ${cmd} ${cmdArgs.join(" ")}`);
150
159
  }
151
160
  logFile(` OK in ${dur}s`);
@@ -835,8 +844,11 @@ function installNeo4j() {
835
844
  /**
836
845
  * Create a dedicated Neo4j instance for this brand when NEO4J_DEDICATED is true.
837
846
  * Produces: separate config dir, data dir, log dir, systemd service, and password.
838
- * On upgrade (config already exists), skips setupensureNeo4jPassword() handles
839
- * password verification for existing instances.
847
+ * On upgrade (config already exists), skips conf creation but always runs the
848
+ * Task 787 state-remediation block (stop/disable system unit, reset-failed
849
+ * dedicated, start, verify) so a half-installed Pi recovers in-place without
850
+ * manual systemctl. ensureNeo4jPassword() handles password verification on the
851
+ * recovery path.
840
852
  */
841
853
  function setupDedicatedNeo4j() {
842
854
  if (!NEO4J_DEDICATED)
@@ -847,49 +859,52 @@ function setupDedicatedNeo4j() {
847
859
  const logDir = `/var/log/neo4j-${brandSuffix}`;
848
860
  const serviceName = `neo4j-${brandSuffix}`;
849
861
  const httpPort = NEO4J_PORT - 213; // Preserve standard 7687/7474 offset
850
- // Upgrade detection: if dedicated config already exists, skip setup entirely.
851
- // ensureNeo4jPassword() will verify the password is correct for this instance.
852
- const confCheck = spawnSync("test", ["-f", `${confDir}/neo4j.conf`], { stdio: "pipe" });
853
- if (confCheck.status === 0) {
862
+ // Conf creation is gated on first install; everything below the if/else
863
+ // (state remediation + start + verify) runs on every install Task 787
864
+ // closes the recovery gap where a half-installed Pi (failed prior install
865
+ // hit start-limit-hit, dedicated unit in failed state) returned here without
866
+ // the fix being applied.
867
+ const confExists = spawnSync("test", ["-f", `${confDir}/neo4j.conf`], { stdio: "pipe" }).status === 0;
868
+ if (confExists) {
854
869
  console.log(` Dedicated Neo4j instance for ${BRAND.productName} already configured at ${confDir}`);
855
- logFile(` Neo4j dedicated: existing config at ${confDir}, skipping setup`);
856
- return;
857
- }
858
- console.log(` Setting up dedicated Neo4j instance for ${BRAND.productName} on bolt://localhost:${NEO4J_PORT}...`);
859
- // Pre-check: neo4j user must exist (created by the apt package)
860
- const neo4jUserCheck = spawnSync("id", ["neo4j"], { stdio: "pipe" });
861
- if (neo4jUserCheck.status !== 0) {
862
- throw new Error("Neo4j system user 'neo4j' not found. Is Neo4j installed via apt?");
863
- }
864
- // Pre-check: source config must exist
865
- if (!existsSync("/etc/neo4j/neo4j.conf")) {
866
- throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
870
+ logFile(` Neo4j dedicated: existing config at ${confDir}, skipping conf creation`);
867
871
  }
868
- // 1. Copy base config
869
- console.log(" [privileged] cp -r");
870
- shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
871
- // 2. Modify config for this instance: bolt port, HTTP port, data/log directories
872
- console.log(" [privileged] sed -i");
873
- shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
874
- console.log(" [privileged] sed -i");
875
- shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
876
- console.log(" [privileged] sed -i");
877
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
878
- console.log(" [privileged] sed -i");
879
- shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
880
- // Verify config was updated — sed silently no-ops if the key format changed
881
- const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
882
- if (confContent.status !== 0) {
883
- console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
884
- logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
885
- }
886
- // 3. Create data and log directories
887
- console.log(" [privileged] mkdir -p");
888
- shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
889
- console.log(" [privileged] chown -R");
890
- shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
891
- // 4. Create systemd service
892
- const serviceContent = `[Unit]
872
+ else {
873
+ console.log(` Setting up dedicated Neo4j instance for ${BRAND.productName} on bolt://localhost:${NEO4J_PORT}...`);
874
+ // Pre-check: neo4j user must exist (created by the apt package)
875
+ const neo4jUserCheck = spawnSync("id", ["neo4j"], { stdio: "pipe" });
876
+ if (neo4jUserCheck.status !== 0) {
877
+ throw new Error("Neo4j system user 'neo4j' not found. Is Neo4j installed via apt?");
878
+ }
879
+ // Pre-check: source config must exist
880
+ if (!existsSync("/etc/neo4j/neo4j.conf")) {
881
+ throw new Error("/etc/neo4j/neo4j.conf not found. Cannot create dedicated instance without base config.");
882
+ }
883
+ // 1. Copy base config
884
+ console.log(" [privileged] cp -r");
885
+ shell("cp", ["-r", "/etc/neo4j", confDir], { sudo: true });
886
+ // 2. Modify config for this instance: bolt port, HTTP port, data/log directories
887
+ console.log(" [privileged] sed -i");
888
+ shell("sed", ["-i", `s/^#\\?server\\.bolt\\.listen_address=.*/server.bolt.listen_address=:${NEO4J_PORT}/`, `${confDir}/neo4j.conf`], { sudo: true });
889
+ console.log(" [privileged] sed -i");
890
+ shell("sed", ["-i", `s/^#\\?server\\.http\\.listen_address=.*/server.http.listen_address=:${httpPort}/`, `${confDir}/neo4j.conf`], { sudo: true });
891
+ console.log(" [privileged] sed -i");
892
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.data=.*|server.directories.data=${dataDir}/data|`, `${confDir}/neo4j.conf`], { sudo: true });
893
+ console.log(" [privileged] sed -i");
894
+ shell("sed", ["-i", `s|^#\\?server\\.directories\\.logs=.*|server.directories.logs=${logDir}|`, `${confDir}/neo4j.conf`], { sudo: true });
895
+ // Verify config was updated — sed silently no-ops if the key format changed
896
+ const confContent = spawnSync("grep", [`server.bolt.listen_address=:${NEO4J_PORT}`, `${confDir}/neo4j.conf`], { stdio: "pipe" });
897
+ if (confContent.status !== 0) {
898
+ console.error(` WARNING: neo4j.conf may not have been updated correctly — bolt port ${NEO4J_PORT} not found in config`);
899
+ logFile(` WARNING: sed verification failed — bolt port ${NEO4J_PORT} not found in ${confDir}/neo4j.conf`);
900
+ }
901
+ // 3. Create data and log directories
902
+ console.log(" [privileged] mkdir -p");
903
+ shell("mkdir", ["-p", `${dataDir}/data`, logDir], { sudo: true });
904
+ console.log(" [privileged] chown -R");
905
+ shell("chown", ["-R", "neo4j:neo4j", dataDir, logDir, confDir], { sudo: true });
906
+ // 4. Create systemd service
907
+ const serviceContent = `[Unit]
893
908
  Description=Neo4j Graph Database (${BRAND.productName})
894
909
  After=network-online.target
895
910
  Wants=network-online.target
@@ -905,53 +920,73 @@ LimitNOFILE=60000
905
920
  [Install]
906
921
  WantedBy=multi-user.target
907
922
  `;
908
- const tmpServicePath = `/tmp/${serviceName}.service`;
909
- writeFileSync(tmpServicePath, serviceContent);
910
- console.log(" [privileged] cp");
911
- shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
912
- spawnSync("rm", ["-f", tmpServicePath]);
913
- // 5. Set initial password before first start
914
- const password = randomBytes(24).toString("base64url");
915
- const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
916
- mkdirSync(persistDir, { recursive: true });
917
- writeFileSync(join(persistDir, ".neo4j-password"), password, { mode: 0o600 });
918
- const configDir = resolve(INSTALL_DIR, "platform/config");
919
- mkdirSync(configDir, { recursive: true });
920
- writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
921
- // sudo env VAR=val passes NEO4J_CONF through sudo's env_reset policy
922
- spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
923
- stdio: "inherit",
924
- });
925
- // 6. Enable and start the dedicated service
923
+ const tmpServicePath = `/tmp/${serviceName}.service`;
924
+ writeFileSync(tmpServicePath, serviceContent);
925
+ console.log(" [privileged] cp");
926
+ shell("cp", [tmpServicePath, `/etc/systemd/system/${serviceName}.service`], { sudo: true });
927
+ spawnSync("rm", ["-f", tmpServicePath]);
928
+ // 5. Set initial password before first start
929
+ const password = randomBytes(24).toString("base64url");
930
+ const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
931
+ mkdirSync(persistDir, { recursive: true });
932
+ writeFileSync(join(persistDir, ".neo4j-password"), password, { mode: 0o600 });
933
+ const configDir = resolve(INSTALL_DIR, "platform/config");
934
+ mkdirSync(configDir, { recursive: true });
935
+ writeFileSync(join(configDir, ".neo4j-password"), password, { mode: 0o600 });
936
+ // sudo env VAR=val passes NEO4J_CONF through sudo's env_reset policy
937
+ spawnSync("sudo", ["env", `NEO4J_CONF=${confDir}`, "neo4j-admin", "dbms", "set-initial-password", "--", password], {
938
+ stdio: "inherit",
939
+ });
940
+ }
941
+ // ============================================================================
942
+ // Task 787 — unified state remediation + start + verify.
943
+ //
944
+ // Runs on both fresh and recovery paths. The dedicated unit and the apt
945
+ // package's system unit both exec /usr/bin/neo4j console without overriding
946
+ // NEO4J_HOME, so server.directories.run resolves to /var/lib/neo4j/run for
947
+ // both — the launcher refuses with "Neo4j is already running (pid:N)" if
948
+ // the system unit holds the PID file. Stopping the system unit first frees
949
+ // the run-state; disabling prevents it returning at next boot. reset-failed
950
+ // clears any prior start-limit-hit from a half-installed Pi.
951
+ // ============================================================================
926
952
  spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
927
953
  console.log(" [privileged] systemctl enable");
928
954
  shell("systemctl", ["enable", serviceName], { sudo: true });
955
+ console.log(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
956
+ logFile(` [neo4j] disabling system unit (brand-dedicated active on port ${NEO4J_PORT})`);
957
+ shell("systemctl", ["stop", "neo4j"], { sudo: true, bestEffort: true });
958
+ shell("systemctl", ["disable", "neo4j"], { sudo: true, bestEffort: true });
959
+ console.log(` [neo4j] reset-failed ${serviceName} before start`);
960
+ logFile(` [neo4j] reset-failed ${serviceName} before start`);
961
+ shell("systemctl", ["reset-failed", serviceName], { sudo: true, bestEffort: true });
929
962
  console.log(" [privileged] systemctl start");
930
963
  shell("systemctl", ["start", serviceName], { sudo: true });
931
- // 7. Verify connectivity poll until cypher-shell can connect
964
+ // Verify the dedicated unit bound its port. Password verification is
965
+ // ensureNeo4jPassword()'s job (called next in the install pipeline) — that
966
+ // function tests the stored password against this port and resets auth if
967
+ // the password no longer matches the running instance.
932
968
  console.log(` Waiting for dedicated Neo4j instance on port ${NEO4J_PORT}...`);
933
- let connected = false;
969
+ let listening = false;
970
+ const portMatch = new RegExp(`:${NEO4J_PORT}\\b`);
934
971
  for (let i = 0; i < 15; i++) {
935
- const check = spawnSync("cypher-shell", [
936
- "-u", "neo4j", "-p", password,
937
- "-a", `bolt://localhost:${NEO4J_PORT}`,
938
- "RETURN 1",
939
- ], { stdio: "pipe", timeout: 5000 });
940
- if (check.status === 0) {
941
- connected = true;
972
+ const portCheck = spawnSync("ss", ["-tln"], { stdio: "pipe", timeout: 5000 });
973
+ if (portMatch.test(portCheck.stdout?.toString() ?? "")) {
974
+ listening = true;
942
975
  break;
943
976
  }
944
977
  spawnSync("sleep", ["2"]);
945
978
  }
946
- if (!connected) {
947
- // Diagnostic: check what's on this port
979
+ if (!listening) {
980
+ // Loud failure no silent fallback to the system instance (Task 787).
948
981
  const portCheck = spawnSync("ss", ["-tlnp"], { stdio: "pipe", timeout: 5000 });
949
- const portLines = portCheck.stdout?.toString().split("\n").filter((l) => l.includes(String(NEO4J_PORT))) || [];
982
+ const portLines = (portCheck.stdout?.toString() ?? "").split("\n").filter((l) => l.includes(String(NEO4J_PORT)));
950
983
  const diagnostic = portLines.length > 0 ? portLines.join("; ") : "nothing listening on port";
951
- console.error(` WARNING: Dedicated Neo4j instance not responding on bolt://localhost:${NEO4J_PORT}`);
952
- console.error(` Port ${NEO4J_PORT} status: ${diagnostic}`);
953
- logFile(` Neo4j dedicated: connectivity check FAILED on port ${NEO4J_PORT} — ${diagnostic}`);
954
- // Don't throw ensureNeo4jPassword() will retry
984
+ const journal = spawnSync("journalctl", ["-u", serviceName, "--since", "5 min ago"], { stdio: "pipe", timeout: 5000 });
985
+ const journalTail = (journal.stdout?.toString() ?? "").split("\n").slice(-20).join("\n");
986
+ logFile(` Neo4j dedicated: failed to bind port ${NEO4J_PORT} — ${diagnostic}`);
987
+ throw new Error(`Dedicated Neo4j instance ${serviceName} did not bind bolt://localhost:${NEO4J_PORT} within 30s.\n` +
988
+ `Port ${NEO4J_PORT}: ${diagnostic}\n` +
989
+ `journalctl -u ${serviceName} --since "5 min ago" | tail -20:\n${journalTail}`);
955
990
  }
956
991
  logFile(` Neo4j dedicated: config=${confDir} data=${dataDir} service=${serviceName} bolt=:${NEO4J_PORT} http=:${httpPort}`);
957
992
  console.log(` Dedicated Neo4j instance ready on bolt://localhost:${NEO4J_PORT}`);
@@ -1449,7 +1484,23 @@ function setupAccount() {
1449
1484
  log("10", TOTAL, "Setting up...");
1450
1485
  const seedScript = join(INSTALL_DIR, "platform/scripts/seed-neo4j.sh");
1451
1486
  if (existsSync(seedScript)) {
1452
- shell("bash", [seedScript], { cwd: INSTALL_DIR });
1487
+ // Task 787 explicit env to seed. The script no longer falls back to
1488
+ // bolt://localhost:7687, so the installer is responsible for naming the
1489
+ // brand-correct URI and providing the password. Missing password file is
1490
+ // a hard error: ensureNeo4jPassword() ran upstream and would have thrown
1491
+ // already if it couldn't reach the brand's Neo4j.
1492
+ const passwordFile = join(INSTALL_DIR, "platform/config/.neo4j-password");
1493
+ if (!existsSync(passwordFile)) {
1494
+ throw new Error(`Neo4j password file missing at ${passwordFile} — required by seed step.`);
1495
+ }
1496
+ const password = readFileSync(passwordFile, "utf-8").trim();
1497
+ const neo4jUri = `bolt://localhost:${NEO4J_PORT}`;
1498
+ console.log(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
1499
+ logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
1500
+ shell("bash", [seedScript], {
1501
+ cwd: INSTALL_DIR,
1502
+ env: { ...process.env, NEO4J_URI: neo4jUri, NEO4J_PASSWORD: password },
1503
+ });
1453
1504
  }
1454
1505
  // Task 748 — universal embedding coverage backfill. Run after seed so the
1455
1506
  // entity_search index is in place and any pre-Task-748 nodes (e.g. the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.739",
3
+ "version": "1.0.741",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Brand templating — single source of truth for the operator-visible brand name.
3
+ *
4
+ * Skills, agent templates, plugin manifests, and reference content reference
5
+ * the brand only via the literal `{{productName}}` placeholder. This module
6
+ * substitutes at read time from `brand.json`. Same shadow-source defect class
7
+ * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
+ * hard-fails so the wrong brand can never reach the system prompt or the
9
+ * operator UI silently.
10
+ *
11
+ * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
+ * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
13
+ * pass the absolute file path as `sourcePath` so the diagnostic line names
14
+ * exactly which file was substituted.
15
+ */
16
+ export declare function getBrandProductName(): string;
17
+ export declare function substituteBrandPlaceholders(content: string, sourcePath: string): string;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,wBAAgB,mBAAmB,IAAI,MAAM,CAsB5C;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBvF"}
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Brand templating — single source of truth for the operator-visible brand name.
4
+ *
5
+ * Skills, agent templates, plugin manifests, and reference content reference
6
+ * the brand only via the literal `{{productName}}` placeholder. This module
7
+ * substitutes at read time from `brand.json`. Same shadow-source defect class
8
+ * as Tasks 787/788: there is no fallback; missing or empty `productName`
9
+ * hard-fails so the wrong brand can never reach the system prompt or the
10
+ * operator UI silently.
11
+ *
12
+ * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
13
+ * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
14
+ * pass the absolute file path as `sourcePath` so the diagnostic line names
15
+ * exactly which file was substituted.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.getBrandProductName = getBrandProductName;
19
+ exports.substituteBrandPlaceholders = substituteBrandPlaceholders;
20
+ const node_path_1 = require("node:path");
21
+ const node_fs_1 = require("node:fs");
22
+ const PLACEHOLDER = "{{productName}}";
23
+ let cachedProductName = null;
24
+ function brandJsonPath() {
25
+ const platformRoot = process.env.MAXY_PLATFORM_ROOT;
26
+ if (!platformRoot) {
27
+ throw new Error("[skill-loader] MAXY_PLATFORM_ROOT not set — cannot resolve brand.json");
28
+ }
29
+ return (0, node_path_1.join)(platformRoot, "config", "brand.json");
30
+ }
31
+ function getBrandProductName() {
32
+ if (cachedProductName !== null)
33
+ return cachedProductName;
34
+ const path = brandJsonPath();
35
+ if (!(0, node_fs_1.existsSync)(path)) {
36
+ throw new Error(`[skill-loader] brand.json missing at ${path}`);
37
+ }
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
41
+ }
42
+ catch (err) {
43
+ throw new Error(`[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`);
44
+ }
45
+ const value = parsed.productName;
46
+ if (typeof value !== "string" || value.trim() === "") {
47
+ throw new Error(`[skill-loader] brand.json at ${path} has missing or empty productName`);
48
+ }
49
+ cachedProductName = value;
50
+ return value;
51
+ }
52
+ function substituteBrandPlaceholders(content, sourcePath) {
53
+ if (!content.includes(PLACEHOLDER))
54
+ return content;
55
+ let productName;
56
+ try {
57
+ productName = getBrandProductName();
58
+ }
59
+ catch (err) {
60
+ console.error(`[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`);
61
+ throw err;
62
+ }
63
+ // split-length counts non-overlapping literal matches without regex-escape risk.
64
+ const occurrences = content.split(PLACEHOLDER).length - 1;
65
+ const substituted = content.split(PLACEHOLDER).join(productName);
66
+ console.log(`[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`);
67
+ return substituted;
68
+ }
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAmBH,kDAsBC;AAED,kEAkBC;AA3DD,yCAAiC;AACjC,qCAAmD;AAEnD,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,IAAI,iBAAiB,GAAkB,IAAI,CAAC;AAE5C,SAAS,aAAa;IACpB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACpD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,gBAAI,EAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,mBAAmB;IACjC,IAAI,iBAAiB,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC;IACzD,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,MAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACvG,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,mCAAmC,CACxE,CAAC;IACJ,CAAC;IACD,iBAAiB,GAAG,KAAK,CAAC;IAC1B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,2BAA2B,CAAC,OAAe,EAAE,UAAkB;IAC7E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,OAAO,CAAC;IACnD,IAAI,WAAmB,CAAC;IACxB,IAAI,CAAC;QACH,WAAW,GAAG,mBAAmB,EAAE,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,mFAAmF,UAAU,EAAE,CAChG,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,iFAAiF;IACjF,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CACT,gDAAgD,WAAW,UAAU,UAAU,gBAAgB,WAAW,EAAE,CAC7G,CAAC;IACF,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Brand templating — single source of truth for the operator-visible brand name.
3
+ *
4
+ * Skills, agent templates, plugin manifests, and reference content reference
5
+ * the brand only via the literal `{{productName}}` placeholder. This module
6
+ * substitutes at read time from `brand.json`. Same shadow-source defect class
7
+ * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
+ * hard-fails so the wrong brand can never reach the system prompt or the
9
+ * operator UI silently.
10
+ *
11
+ * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
+ * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
13
+ * pass the absolute file path as `sourcePath` so the diagnostic line names
14
+ * exactly which file was substituted.
15
+ */
16
+
17
+ import { join } from "node:path";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+
20
+ const PLACEHOLDER = "{{productName}}";
21
+
22
+ let cachedProductName: string | null = null;
23
+
24
+ function brandJsonPath(): string {
25
+ const platformRoot = process.env.MAXY_PLATFORM_ROOT;
26
+ if (!platformRoot) {
27
+ throw new Error(
28
+ "[skill-loader] MAXY_PLATFORM_ROOT not set — cannot resolve brand.json",
29
+ );
30
+ }
31
+ return join(platformRoot, "config", "brand.json");
32
+ }
33
+
34
+ export function getBrandProductName(): string {
35
+ if (cachedProductName !== null) return cachedProductName;
36
+ const path = brandJsonPath();
37
+ if (!existsSync(path)) {
38
+ throw new Error(`[skill-loader] brand.json missing at ${path}`);
39
+ }
40
+ let parsed: { productName?: unknown };
41
+ try {
42
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
43
+ } catch (err) {
44
+ throw new Error(
45
+ `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`,
46
+ );
47
+ }
48
+ const value = parsed.productName;
49
+ if (typeof value !== "string" || value.trim() === "") {
50
+ throw new Error(
51
+ `[skill-loader] brand.json at ${path} has missing or empty productName`,
52
+ );
53
+ }
54
+ cachedProductName = value;
55
+ return value;
56
+ }
57
+
58
+ export function substituteBrandPlaceholders(content: string, sourcePath: string): string {
59
+ if (!content.includes(PLACEHOLDER)) return content;
60
+ let productName: string;
61
+ try {
62
+ productName = getBrandProductName();
63
+ } catch (err) {
64
+ console.error(
65
+ `[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`,
66
+ );
67
+ throw err;
68
+ }
69
+ // split-length counts non-overlapping literal matches without regex-escape risk.
70
+ const occurrences = content.split(PLACEHOLDER).length - 1;
71
+ const substituted = content.split(PLACEHOLDER).join(productName);
72
+ console.log(
73
+ `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`,
74
+ );
75
+ return substituted;
76
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,UAAU,GAAG,UAAU,CAAC;IACnC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,aAAa,EAAE,iBAAiB,EAAE,CAAC;IACnC,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,SAAS,GACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAuF1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,UAAU,GAAG,UAAU,CAAC;IACnC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,aAAa,EAAE,iBAAiB,EAAE,CAAC;IACnC,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,SAAS,GACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQzB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,eAAe,CAAC,CA8G1B"}
@@ -59,7 +59,29 @@ async function writeNodeWithEdges(params) {
59
59
  process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}\n`);
60
60
  throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
61
61
  }
62
- const nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
62
+ let nodeRes;
63
+ try {
64
+ nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
65
+ }
66
+ catch (err) {
67
+ // Task 792: surface UserProfile uniqueness violations as a structured
68
+ // reject line. Neo4j's ConstraintValidationFailed message names the
69
+ // violated label + properties (e.g. "Node(N) already exists with label
70
+ // `UserProfile` and properties accountId=… userId=…") but NOT the
71
+ // constraint name, so we key on the helper's input `labels` parameter
72
+ // rather than parsing the message. Other constraint violations
73
+ // propagate without specialised logging — they are unexpected and the
74
+ // raw error is the right signal.
75
+ const code = err?.code ?? "";
76
+ if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
77
+ const accountIdProp = nodeProps.accountId;
78
+ const userIdProp = nodeProps.userId;
79
+ const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
80
+ const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
81
+ process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}\n`);
82
+ }
83
+ throw err;
84
+ }
63
85
  const nodeId = nodeRes.records[0].get("nodeId");
64
86
  const nodeLabels = nodeRes.records[0].get("nodeLabels");
65
87
  // Neo4j Community's default isolation is read-committed (not snapshot)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -112,10 +112,33 @@ export async function writeNodeWithEdges(
112
112
  );
113
113
  }
114
114
 
115
- const nodeRes = await tx.run(
116
- `CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`,
117
- { props: nodeProps }
118
- );
115
+ let nodeRes;
116
+ try {
117
+ nodeRes = await tx.run(
118
+ `CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`,
119
+ { props: nodeProps }
120
+ );
121
+ } catch (err) {
122
+ // Task 792: surface UserProfile uniqueness violations as a structured
123
+ // reject line. Neo4j's ConstraintValidationFailed message names the
124
+ // violated label + properties (e.g. "Node(N) already exists with label
125
+ // `UserProfile` and properties accountId=… userId=…") but NOT the
126
+ // constraint name, so we key on the helper's input `labels` parameter
127
+ // rather than parsing the message. Other constraint violations
128
+ // propagate without specialised logging — they are unexpected and the
129
+ // raw error is the right signal.
130
+ const code = (err as { code?: string } | null)?.code ?? "";
131
+ if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
132
+ const accountIdProp = (nodeProps as Record<string, unknown>).accountId;
133
+ const userIdProp = (nodeProps as Record<string, unknown>).userId;
134
+ const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
135
+ const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
136
+ process.stderr.write(
137
+ `[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}\n`
138
+ );
139
+ }
140
+ throw err;
141
+ }
119
142
  const nodeId = nodeRes.records[0].get("nodeId") as string;
120
143
  const nodeLabels = nodeRes.records[0].get("nodeLabels") as string[];
121
144
 
@@ -577,8 +577,11 @@ FOR (sr:StepResult) ON (sr.runId);
577
577
  // ----------------------------------------------------------
578
578
 
579
579
  // Composite unique constraint: one profile per admin per account.
580
- // Replaces the single-key accountId constraint (Task 249).
581
- CREATE CONSTRAINT user_profile_account_user_unique IF NOT EXISTS
580
+ // Replaces the single-key accountId constraint (Task 249) and the
581
+ // transitional name `user_profile_account_user_unique` (Task 792 — renamed
582
+ // for grep parity with the doctrine name in .docs/neo4j.md and the
583
+ // per-bucket dedupe script). Old name dropped in seed-neo4j.sh MIGRATE_EOF.
584
+ CREATE CONSTRAINT user_profile_unique_per_user IF NOT EXISTS
582
585
  FOR (up:UserProfile) REQUIRE (up.accountId, up.userId) IS UNIQUE;
583
586
 
584
587
  // ----------------------------------------------------------
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -9,6 +9,7 @@ import { execFileSync } from "node:child_process";
9
9
  import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
10
10
  import { writeKey, validateKey, hasKey, keyFilePath, deleteKey } from "../../../../lib/anthropic-key/dist/index.js";
11
11
  import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
12
+ import { substituteBrandPlaceholders } from "../../../../lib/brand-templating/dist/index.js";
12
13
  import { createHash, randomInt, randomUUID } from "node:crypto";
13
14
  import { createConnection } from "node:net";
14
15
  import { homedir, hostname as osHostname } from "node:os";
@@ -1226,7 +1227,11 @@ server.tool("plugin-read", "Read a plugin definition (PLUGIN.md) or one of its r
1226
1227
  isError: true,
1227
1228
  };
1228
1229
  }
1229
- const content = await readFile(pluginPath, "utf-8");
1230
+ const rawContent = await readFile(pluginPath, "utf-8");
1231
+ // Brand-substitute every plugin file before it reaches the agent —
1232
+ // PLUGIN.md, skills/<name>/SKILL.md, references/*.md all flow through
1233
+ // here. Missing brand.json hard-throws (Tasks 787/788 doctrine).
1234
+ const content = substituteBrandPlaceholders(rawContent, pluginPath);
1230
1235
  // Append file inventory on default calls (PLUGIN.md) so agents can discover
1231
1236
  // skill and reference paths without guessing
1232
1237
  if (resolvedFile === "PLUGIN.md") {