@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.
- package/dist/index.js +130 -79
- package/package.json +1 -1
- package/payload/platform/lib/brand-templating/dist/index.d.ts +18 -0
- package/payload/platform/lib/brand-templating/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/brand-templating/dist/index.js +69 -0
- package/payload/platform/lib/brand-templating/dist/index.js.map +1 -0
- package/payload/platform/lib/brand-templating/src/index.ts +76 -0
- package/payload/platform/lib/brand-templating/tsconfig.json +8 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +23 -1
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +27 -4
- package/payload/platform/neo4j/schema.cypher +5 -2
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +6 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +7 -7
- package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +1 -1
- package/payload/platform/plugins/anthropic/skills/get-api-key/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/access-control.md +10 -10
- package/payload/platform/plugins/docs/references/contacts-guide.md +11 -11
- package/payload/platform/plugins/docs/references/deployment.md +14 -13
- package/payload/platform/plugins/docs/references/getting-started.md +19 -19
- package/payload/platform/plugins/docs/references/internals.md +4 -4
- package/payload/platform/plugins/docs/references/memory-guide.md +21 -21
- package/payload/platform/plugins/docs/references/migration-guide.md +5 -5
- package/payload/platform/plugins/docs/references/platform.md +9 -9
- package/payload/platform/plugins/docs/references/plugins-guide.md +20 -12
- package/payload/platform/plugins/docs/references/projects-guide.md +10 -10
- package/payload/platform/plugins/docs/references/settings.md +13 -13
- package/payload/platform/plugins/docs/references/telegram-guide.md +14 -14
- package/payload/platform/plugins/docs/references/troubleshooting.md +23 -23
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +6 -6
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +2 -2
- package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +2 -2
- package/payload/platform/plugins/workflows/mcp/test-workflows.sh +5 -1
- package/payload/platform/scripts/dedupe-userprofile-ghosts.sh +388 -0
- package/payload/platform/scripts/embed-backfill.sh +8 -1
- package/payload/platform/scripts/migrate-import.sh +42 -1
- package/payload/platform/scripts/seed-neo4j.sh +8 -1
- package/payload/server/chunk-PQ6LDXZ4.js +2997 -0
- package/payload/server/chunk-W6ZUNLLS.js +9446 -0
- package/payload/server/client-pool-DQBHSKAF.js +28 -0
- package/payload/server/maxy-edge.js +2 -2
- 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
|
|
839
|
-
*
|
|
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
|
-
//
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
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
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
console.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
//
|
|
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
|
|
969
|
+
let listening = false;
|
|
970
|
+
const portMatch = new RegExp(`:${NEO4J_PORT}\\b`);
|
|
934
971
|
for (let i = 0; i < 15; i++) {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
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 (!
|
|
947
|
-
//
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
logFile(` Neo4j dedicated:
|
|
954
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
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
|
-
|
|
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":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;AAqCH,wCAWC;AAQD,
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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") {
|