jorgex-stack 1.9.41 → 1.9.42

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 (3) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.js +301 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -128,7 +128,9 @@ pnpm dlx jorgex-stack@1.9.7 sync --agents pi
128
128
  pnpm dlx jorgex-stack@1.9.7 uninstall --agents pi
129
129
  ```
130
130
 
131
- Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. The exact artifact and integrity values are authoritative in `src/lib/pi-runtime-pin.json`, while the lifecycle remains authoritative in `src/lib/pi-runtime.ts`. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. Pi 0.8.18 is a legacy projection: its adapter folds the Context7 text into the base system-prompt block and combines the selected browser guides in `jorgex:browser`; it does not provide a Context7 MCP server. For the file runtimes, Context7, Playwright and DevTools use independent managed sections. The global Playwright package and Chromium cache are shared by the machine; `--playwright-runtimes` controls which runtime receives the guide. The adopted Pi package implements and tests `playwright-handoff-v1` through `PI_CODING_AGENT_DIR/jorgex-pi/playwright.v1.json`. Historical package entries do not describe the current pin. Filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Package receipts reject manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state. Projection cleanup requires an exact scope-bound ownership receipt; DevTools conflicts preserve the handoff for review.
131
+ Stack downloads the frozen registry tarball, verifies its exact size plus SHA-256/SHA-512, backs up Pi's `settings.json`, and only then asks Pi to install that local file. The exact artifact and integrity values are authoritative in `src/lib/pi-runtime-pin.json`, while the lifecycle remains authoritative in `src/lib/pi-runtime.ts`. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. After the package is healthy, Stack projects the shared resources into Pi: marked `jorgex:system-prompt` and `jorgex:engram-protocol` sections in `~/.pi/agent/AGENTS.md`, canonical skills under `~/.agents/skills`, and `~/.pi/agent/prompts/lean-audit.md`. Pi 0.8.20 registers Context7 through an isolated in-memory HTTP bridge during bootstrap; `available` means configuration permits registration and does not imply an HTTP handshake. A conflict preserves the existing MCP file and blocks managed activation. No MCP configuration or credentials are written. For the file runtimes, Context7, Playwright and DevTools use independent managed sections. The global Playwright package and Chromium cache are shared by the machine; `--playwright-runtimes` controls which runtime receives the guide. The adopted Pi package implements and tests `playwright-handoff-v1` through `PI_CODING_AGENT_DIR/jorgex-pi/playwright.v1.json`. Historical package entries do not describe the current pin. Filters are applied only after this projection exists, so the package does not duplicate shared resources. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Package receipts reject manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state. Projection cleanup requires an exact scope-bound ownership receipt; DevTools conflicts preserve the handoff for review.
132
+
133
+ The static `AGENTS.md` projected by Stack does not include Context7; Pi's native bootstrap adds that section after registering the isolated bridge. Pi never writes, owns or removes MCP files. In Claude Code, Codex and OpenCode, a compatible existing Context7 entry is preserved and removed only with explicit canonical Stack ownership.
132
134
 
133
135
  Historically, the published Pi 0.8.0 direct-package snapshot added `work-audit`: the snapshot grew from **17 to 18 skill trees** (96 to 97 files), and the active runtime allowlist grew from **16 to 17 skills**. The historical Pi 0.8.5 snapshot reference records 18 skill trees and 98 files; it does not describe the adopted pin. Reference F2-A is included while the private F1 skills remain preserved. `playwright-cli` remains in the snapshot but inactive because browser automation is a separate opt-in integration.
134
136
 
@@ -190,7 +192,7 @@ Daily operation:
190
192
  - `doctor` reports the Playwright CLI state (`disabled`, `healthy`, `missing:package`, `missing:browser`, `unreadable`, `broken`, `outdated`) and, when the opt-in package and browser cache are present, verifies one local headless Chromium launch against `about:blank`; it does not open external sites or repair state. An `unreadable` browser cache includes its exact path and filesystem error code so permissions or another local cause can be investigated. If either preference file is corrupt, doctor prints the exact path and the remedy (`Corrige o borra ese archivo antes de reintentar`) before any other browser check; in that case `install`/`uninstall`/`update`/`update --check` will abort with exit 1 until the file is fixed, so the corruption cannot be reconciled destructively.
191
193
  - `update --check` only inspects Playwright CLI when its preference is `enabled` (a binary appearing in `PATH` is not consent). It compares the installed version against the approved pin `0.1.18` — it does not consult npm latest, and a Playwright CLI binary-only update does not require `sync` afterwards.
192
194
  - `uninstall` preserves the global `@playwright/cli` package and all browser data by default; `--remove-playwright` removes the package only (never the browser cache, profiles, cookies, storage state, traces, screenshots or videos). If `pnpm remove --global @playwright/cli` exits non-zero, `uninstall` reports the failure instead of a success outro. For DevTools MCP, ownership is released only after the corresponding unmerge is applied; if no unmerge action is written, the ownership marker is preserved for a later retry.
193
- - `install`/`sync` also inject (and `disable`/`uninstall` remove) managed capability sections in `AGENTS.md` (OpenCode, Codex) or `CLAUDE.md` (Claude Code). File runtimes keep `jorgex:playwright`, `jorgex:chrome-devtools` and `jorgex:context7` independent; Pi 0.8.18 adapts them to its legacy `jorgex:browser`/base projection. User content outside managed markers is preserved. `--target-dir` never reads the real preferences, so by default no section is emitted in target-dir runs — but explicit flags like `--devtools` simulate the MCP entry (and its DevTools block) inside the temp target without touching the real global state, and `install --dry-run --playwright` projects the Playwright section into the plan preview without installing anything. If reconciliation detects a partial or ambiguous system-prompt state, the CLI exits non-zero and recommends repairing it with `sync` or a repeated install; no write is attempted before marker validation. See [docs/references/browser-automation.md](docs/references/browser-automation.md) §2.7 for the full lifecycle.
195
+ - `install`/`sync` also inject (and `disable`/`uninstall` remove) managed capability sections in `AGENTS.md` (OpenCode, Codex) or `CLAUDE.md` (Claude Code). File runtimes keep `jorgex:playwright`, `jorgex:chrome-devtools` and `jorgex:context7` independent; Pi 0.8.20 projects the Context7 section after registering its isolated HTTP bridge and keeps the legacy `jorgex:browser` format for migration. User content outside managed markers is preserved. `--target-dir` never reads the real preferences, so by default no section is emitted in target-dir runs — but explicit flags like `--devtools` simulate the MCP entry (and its DevTools block) inside the temp target without touching the real global state, and `install --dry-run --playwright` projects the Playwright section into the plan preview without installing anything. If reconciliation detects a partial or ambiguous system-prompt state, the CLI exits non-zero and recommends repairing it with `sync` or a repeated install; no write is attempted before marker validation. See [docs/references/browser-automation.md](docs/references/browser-automation.md) §2.7 for the full lifecycle.
194
196
 
195
197
  ### Update: Interactive Flow
196
198
 
package/dist/cli.js CHANGED
@@ -401,6 +401,12 @@ function readTomlSection(existing, section) {
401
401
  if (found === null) return null;
402
402
  return lines.slice(found.start + 1, found.end).join("\n");
403
403
  }
404
+ function hasTomlChildSection(existing, section) {
405
+ if (existing === null) return false;
406
+ const lines = existing.split(/\r?\n/);
407
+ const mask = multilineStringMask(lines);
408
+ return lines.some((line, index) => !mask[index] && headerName(line)?.startsWith(`${section}.`) === true);
409
+ }
404
410
 
405
411
  // src/lib/writing-style.ts
406
412
  var DEFAULT_SECTION = "writing-style-default";
@@ -916,6 +922,24 @@ var gitReadCommands = [
916
922
  function yamlString(value) {
917
923
  return JSON.stringify(value);
918
924
  }
925
+ function readMcpConfig(file) {
926
+ let content;
927
+ try {
928
+ content = fs6.readFileSync(file, "utf8");
929
+ } catch (error) {
930
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
931
+ if (code === "ENOENT") return null;
932
+ throw new Error(`OpenCode: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
933
+ }
934
+ if (content.trim() !== "") {
935
+ try {
936
+ if (objectValue(JSON.parse(content)) === null) throw new Error();
937
+ } catch {
938
+ throw new Error(`OpenCode: la configuraci\xF3n MCP en ${file} debe contener un objeto JSON v\xE1lido.`);
939
+ }
940
+ }
941
+ return content;
942
+ }
919
943
  function isManagedOptionalStdioServer(server, value) {
920
944
  if (!server.optional || server.transport !== "stdio" || value === null || typeof value !== "object" || Array.isArray(value)) {
921
945
  return false;
@@ -936,6 +960,31 @@ var PRIMARY_LIMIT_PREFIX = `provider.openai.models.${PRIMARY_MODEL_ID}.limit`;
936
960
  function objectValue(value) {
937
961
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
938
962
  }
963
+ function isCompatibleContext7Server(server, value) {
964
+ const current = objectValue(value);
965
+ if (server.transport !== "http" || typeof server.url !== "string" || current === null) return false;
966
+ return current.type === "remote" && current.url === server.url && (current.enabled === void 0 || current.enabled === true);
967
+ }
968
+ function canonicalContext7Server(server) {
969
+ const headers = {};
970
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
971
+ const envRef = /^\$\{(\w+)\}$/.exec(raw);
972
+ headers[key] = envRef ? `{env:${envRef[1]}}` : raw;
973
+ }
974
+ return {
975
+ type: "remote",
976
+ url: server.url,
977
+ ...Object.keys(headers).length > 0 ? { headers } : {}
978
+ };
979
+ }
980
+ function isCanonicalContext7Server(server, value) {
981
+ return isCompatibleContext7Server(server, value) && isDeepStrictEqual(value, canonicalContext7Server(server));
982
+ }
983
+ function assertCompatibleContext7(server, value) {
984
+ if (value !== void 0 && !isCompatibleContext7Server(server, value)) {
985
+ throw new Error("OpenCode: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint, tipo o estado nativo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
986
+ }
987
+ }
939
988
  function ensureObject(parent, key, fieldPath) {
940
989
  if (parent[key] === void 0) parent[key] = {};
941
990
  const value = objectValue(parent[key]);
@@ -1118,12 +1167,19 @@ ${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
1118
1167
  planMainConfig(canonical, ctx) {
1119
1168
  const file = path9.join(ctx.configDir, "opencode.json");
1120
1169
  const { pluginsDir } = this.paths(ctx.configDir);
1121
- const original = readTextIfExists(file);
1170
+ const original = readMcpConfig(file);
1122
1171
  const contentSource = original === null || original.trim() === "" ? null : original;
1123
1172
  const isFreshConfig = contentSource === null;
1124
1173
  const mcpOwnership = [];
1125
1174
  const primaryModelOwnership = [];
1126
1175
  const content = upsertJson(contentSource, (root) => {
1176
+ const rawMcp = root["mcp"];
1177
+ if (rawMcp !== void 0 && objectValue(rawMcp) === null) {
1178
+ throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar sync.");
1179
+ }
1180
+ const existingMcp = rawMcp;
1181
+ const context7 = canonical.servers.context7;
1182
+ if (context7 !== void 0) assertCompatibleContext7(context7, existingMcp?.["context7"]);
1127
1183
  root["$schema"] ??= "https://opencode.ai/config.json";
1128
1184
  if (root[PRIMARY_MODEL_FIELD] === void 0) {
1129
1185
  root[PRIMARY_MODEL_FIELD] = PRIMARY_MODEL;
@@ -1157,6 +1213,12 @@ ${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
1157
1213
  for (const [name, server] of Object.entries(canonical.servers)) {
1158
1214
  const existing = mcp[name];
1159
1215
  const owned = ctx.ownedMcpServers?.has(name) === true;
1216
+ if (name === "context7" && existing !== void 0) {
1217
+ if (owned && !isCanonicalContext7Server(server, existing)) {
1218
+ mcpOwnership.push({ server: name, owned: false });
1219
+ }
1220
+ continue;
1221
+ }
1160
1222
  if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
1161
1223
  if (owned) {
1162
1224
  if (isManagedOptionalStdioServer(server, existing)) delete mcp[name];
@@ -1193,6 +1255,9 @@ ${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
1193
1255
  url: server.url,
1194
1256
  ...Object.keys(headers).length > 0 ? { headers } : {}
1195
1257
  };
1258
+ if (name === "context7" && existing === void 0 && !owned) {
1259
+ mcpOwnership.push({ server: name, owned: true });
1260
+ }
1196
1261
  }
1197
1262
  }
1198
1263
  if (pluginsDir !== null) {
@@ -1227,7 +1292,7 @@ ${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
1227
1292
  actions.push({ kind: "write", target: systemPromptFile, content });
1228
1293
  }
1229
1294
  const configFile = path9.join(ctx.configDir, "opencode.json");
1230
- const config = readTextIfExists(configFile);
1295
+ const config = readMcpConfig(configFile);
1231
1296
  if (config !== null) {
1232
1297
  const mcpOwnership = [];
1233
1298
  const primaryModelOwnership = [];
@@ -1264,9 +1329,22 @@ ${gitReadCommands.map((command) => `- \`${command}\``).join("\n")}
1264
1329
  for (const field of managedFields) {
1265
1330
  if (ctx.ownedPrimaryModelFields?.has(field) === true) primaryModelOwnership.push({ field, owned: false });
1266
1331
  }
1267
- const mcpBlock = root["mcp"];
1268
- if (mcpBlock) {
1332
+ const rawMcpBlock = root["mcp"];
1333
+ if (rawMcpBlock !== void 0 && objectValue(rawMcpBlock) === null) {
1334
+ throw new Error("OpenCode: la clave 'mcp' debe ser un objeto; corr\xEDgela antes de reintentar uninstall.");
1335
+ }
1336
+ const mcpBlock = rawMcpBlock;
1337
+ if (mcpBlock !== void 0) {
1269
1338
  for (const [name, server] of Object.entries(mcp.servers)) {
1339
+ if (name === "context7") {
1340
+ const canonical = isCanonicalContext7Server(server, mcpBlock[name]);
1341
+ const owned = ctx.ownedMcpServers?.has(name) === true;
1342
+ if (owned) {
1343
+ if (canonical) delete mcpBlock[name];
1344
+ mcpOwnership.push({ server: name, owned: false });
1345
+ }
1346
+ continue;
1347
+ }
1270
1348
  if (!server.optional) {
1271
1349
  delete mcpBlock[name];
1272
1350
  continue;
@@ -1341,6 +1419,47 @@ var ENGRAM_AGENT_TOOLS = [
1341
1419
  function isRecord(value) {
1342
1420
  return value !== null && typeof value === "object" && !Array.isArray(value);
1343
1421
  }
1422
+ function readMcpConfig2(file) {
1423
+ let content;
1424
+ try {
1425
+ content = fs7.readFileSync(file, "utf8");
1426
+ } catch (error) {
1427
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
1428
+ if (code === "ENOENT") return null;
1429
+ throw new Error(`Claude Code: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
1430
+ }
1431
+ if (content.trim() !== "") {
1432
+ try {
1433
+ if (!isRecord(JSON.parse(content))) throw new Error();
1434
+ } catch {
1435
+ throw new Error(`Claude Code: la configuraci\xF3n MCP en ${file} debe contener un objeto JSON v\xE1lido.`);
1436
+ }
1437
+ }
1438
+ return content;
1439
+ }
1440
+ function isCompatibleContext7Server2(server, value) {
1441
+ if (server.transport !== "http" || typeof server.url !== "string" || !isRecord(value)) return false;
1442
+ return value.type === "http" && value.url === server.url;
1443
+ }
1444
+ function canonicalContext7Server2(server) {
1445
+ const headers = {};
1446
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
1447
+ headers[key] = /^\$\{(\w+)\}$/.test(raw) ? "" : raw;
1448
+ }
1449
+ return {
1450
+ type: "http",
1451
+ url: server.url,
1452
+ ...Object.keys(headers).length > 0 ? { headers } : {}
1453
+ };
1454
+ }
1455
+ function isCanonicalContext7Server2(server, value) {
1456
+ return isCompatibleContext7Server2(server, value) && isDeepStrictEqual2(value, canonicalContext7Server2(server));
1457
+ }
1458
+ function assertCompatibleContext72(server, value) {
1459
+ if (value !== void 0 && !isCompatibleContext7Server2(server, value)) {
1460
+ throw new Error("Claude Code: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint o tipo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
1461
+ }
1462
+ }
1344
1463
  function hasClaudeManualApproval(configDir) {
1345
1464
  const content = readTextIfExists(path10.join(configDir, "settings.json"));
1346
1465
  if (content === null) return false;
@@ -1486,11 +1605,24 @@ ${agent.body}`,
1486
1605
  planMainConfig(canonical, ctx) {
1487
1606
  const file = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
1488
1607
  const mcpOwnership = [];
1489
- const content = upsertJson(readTextIfExists(file), (root) => {
1608
+ const content = upsertJson(readMcpConfig2(file), (root) => {
1609
+ const rawServers = root["mcpServers"];
1610
+ if (rawServers !== void 0 && !isRecord(rawServers)) {
1611
+ throw new Error("Claude Code: la clave 'mcpServers' debe ser un objeto; corr\xEDgela antes de reintentar sync.");
1612
+ }
1613
+ const existingServers = rawServers;
1614
+ const context7 = canonical.servers.context7;
1615
+ if (context7 !== void 0) assertCompatibleContext72(context7, existingServers?.["context7"]);
1490
1616
  const servers = root["mcpServers"] ??= {};
1491
1617
  for (const [name, server] of Object.entries(canonical.servers)) {
1492
1618
  const existing = servers[name];
1493
1619
  const owned = ctx.ownedMcpServers?.has(name) === true;
1620
+ if (name === "context7" && existing !== void 0) {
1621
+ if (owned && !isCanonicalContext7Server2(server, existing)) {
1622
+ mcpOwnership.push({ server: name, owned: false });
1623
+ }
1624
+ continue;
1625
+ }
1494
1626
  if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
1495
1627
  if (owned) {
1496
1628
  if (isManagedOptionalStdioServer2(server, existing)) delete servers[name];
@@ -1534,6 +1666,9 @@ ${agent.body}`,
1534
1666
  url: server.url,
1535
1667
  ...Object.keys(headers).length > 0 ? { headers } : {}
1536
1668
  };
1669
+ if (name === "context7" && existing === void 0 && !owned) {
1670
+ mcpOwnership.push({ server: name, owned: true });
1671
+ }
1537
1672
  }
1538
1673
  }
1539
1674
  });
@@ -1556,13 +1691,26 @@ ${agent.body}`,
1556
1691
  }
1557
1692
  }
1558
1693
  const mainFile = path10.join(path10.dirname(ctx.configDir), `${path10.basename(ctx.configDir)}.json`);
1559
- const main2 = readTextIfExists(mainFile);
1694
+ const main2 = readMcpConfig2(mainFile);
1560
1695
  if (main2 !== null) {
1561
1696
  const mcpOwnership = [];
1562
1697
  const content = upsertJson(main2, (root) => {
1563
- const servers = root["mcpServers"];
1564
- if (!servers) return;
1698
+ const rawServers = root["mcpServers"];
1699
+ if (rawServers === void 0) return;
1700
+ if (!isRecord(rawServers)) {
1701
+ throw new Error("Claude Code: la clave 'mcpServers' debe ser un objeto; corr\xEDgela antes de reintentar uninstall.");
1702
+ }
1703
+ const servers = rawServers;
1565
1704
  for (const [name, server] of Object.entries(mcp.servers)) {
1705
+ if (name === "context7") {
1706
+ const canonical = isCanonicalContext7Server2(server, servers[name]);
1707
+ const owned = ctx.ownedMcpServers?.has(name) === true;
1708
+ if (owned) {
1709
+ if (canonical) delete servers[name];
1710
+ mcpOwnership.push({ server: name, owned: false });
1711
+ }
1712
+ continue;
1713
+ }
1566
1714
  if (!server.optional) {
1567
1715
  delete servers[name];
1568
1716
  continue;
@@ -1586,6 +1734,15 @@ import fs8 from "fs";
1586
1734
  function tomlString(value) {
1587
1735
  return JSON.stringify(value);
1588
1736
  }
1737
+ function readMcpConfig3(file) {
1738
+ try {
1739
+ return fs8.readFileSync(file, "utf8");
1740
+ } catch (error) {
1741
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN";
1742
+ if (code === "ENOENT") return null;
1743
+ throw new Error(`Codex: no se pudo leer la configuraci\xF3n MCP en ${file} (${code}).`);
1744
+ }
1745
+ }
1589
1746
  var PRIMARY_MODEL2 = '"gpt-5.6-sol"';
1590
1747
  var PRIMARY_CONTEXT_WINDOW = "872000";
1591
1748
  var PRIMARY_MODEL_FIELD2 = "model";
@@ -1622,6 +1779,67 @@ var CODEX_JSON_STRING_ARRAY = String.raw`\[(?:\s*${CODEX_JSON_STRING}(?:\s*,\s*$
1622
1779
  var CODEX_KEY = String.raw`(?:[A-Za-z0-9_-]+|${CODEX_JSON_STRING})`;
1623
1780
  var CODEX_ASSIGNMENT = new RegExp(String.raw`^\s*(${CODEX_KEY})\s*=\s*(${CODEX_JSON_STRING_ARRAY}|${CODEX_JSON_VALUE})\s*(?:#.*)?$`);
1624
1781
  var CODEX_HEADER = new RegExp(String.raw`^\[\s*(${CODEX_KEY}(?:\s*\.\s*${CODEX_KEY})*)\s*\]\s*(?:#.*)?$`);
1782
+ function tomlAssignment(section, key) {
1783
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1784
+ const keyPattern = new RegExp(`^\\s*(?:${escaped}|"${escaped}"|'${escaped}')\\s*=`);
1785
+ const lines = section.split(/\r?\n/);
1786
+ const mask = multilineStringMask(lines);
1787
+ for (const [index, line] of lines.entries()) {
1788
+ if (mask[index] || !keyPattern.test(line)) continue;
1789
+ const match = CODEX_ASSIGNMENT.exec(line);
1790
+ if (match) return { present: true, raw: match[2] };
1791
+ const equals = line.indexOf("=");
1792
+ const raw = equals === -1 ? void 0 : line.slice(equals + 1).trim().replace(/\s+#.*$/, "");
1793
+ return { present: true, ...raw?.startsWith("'") && raw.endsWith("'") ? { raw } : {} };
1794
+ }
1795
+ return { present: false };
1796
+ }
1797
+ function parseTomlString(raw) {
1798
+ if (raw === void 0) return void 0;
1799
+ const value = raw.trim();
1800
+ if (value.startsWith('"')) {
1801
+ try {
1802
+ const parsed = JSON.parse(value);
1803
+ return typeof parsed === "string" ? parsed : void 0;
1804
+ } catch {
1805
+ return void 0;
1806
+ }
1807
+ }
1808
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
1809
+ return value.slice(1, -1);
1810
+ }
1811
+ return void 0;
1812
+ }
1813
+ function context7HttpSection(server) {
1814
+ const body = [`url = ${tomlString(server.url)}`];
1815
+ const envHeaders = [];
1816
+ const literalHeaders = [];
1817
+ for (const [key, raw] of Object.entries(server.headers ?? {})) {
1818
+ const envRef = /^\$\{(\w+)\}$/.exec(raw);
1819
+ if (envRef) envHeaders.push(`${tomlString(key)} = ${tomlString(envRef[1])}`);
1820
+ else literalHeaders.push(`${tomlString(key)} = ${tomlString(raw)}`);
1821
+ }
1822
+ if (literalHeaders.length > 0) body.push(`http_headers = { ${literalHeaders.join(", ")} }`);
1823
+ if (envHeaders.length > 0) body.push(`env_http_headers = { ${envHeaders.join(", ")} }`);
1824
+ return body.join("\n");
1825
+ }
1826
+ function isCompatibleContext7Server3(server, section) {
1827
+ if (server.transport !== "http" || typeof server.url !== "string" || section === null) return false;
1828
+ const url = tomlAssignment(section, "url");
1829
+ const command = tomlAssignment(section, "command");
1830
+ const type = tomlAssignment(section, "type");
1831
+ const enabled = tomlAssignment(section, "enabled");
1832
+ const declaredType = type.present ? parseTomlString(type.raw) : "http";
1833
+ return url.present && parseTomlString(url.raw) === server.url && !command.present && declaredType === "http" && (!enabled.present || enabled.raw?.trim() === "true");
1834
+ }
1835
+ function isCanonicalContext7Server3(server, section, config) {
1836
+ return isCompatibleContext7Server3(server, section) && section.trim() === context7HttpSection(server) && !hasTomlChildSection(config, "mcp_servers.context7");
1837
+ }
1838
+ function assertCompatibleContext73(server, section, config) {
1839
+ if (section === null && hasTomlChildSection(config, "mcp_servers.context7") || section !== null && !isCompatibleContext7Server3(server, section)) {
1840
+ throw new Error("Codex: MCP 'context7' entra en conflicto con una definici\xF3n existente (endpoint, tipo o estado nativo incompatible). Conserva la configuraci\xF3n y corrige el conflicto antes de reintentar.");
1841
+ }
1842
+ }
1625
1843
  var CODEX_PERMISSION_HEADERS = {
1626
1844
  base: "permissions.jorgex-read-anywhere",
1627
1845
  filesystem: "permissions.jorgex-read-anywhere.filesystem",
@@ -1861,11 +2079,15 @@ ${body}`
1861
2079
  },
1862
2080
  planMainConfig(canonical, ctx) {
1863
2081
  const file = path11.join(ctx.configDir, "config.toml");
1864
- const original = readTextIfExists(file);
2082
+ const original = readMcpConfig3(file);
1865
2083
  const contentSource = original === null || original.trim() === "" ? null : original;
1866
2084
  let content = contentSource;
1867
2085
  const mcpOwnership = [];
1868
2086
  const primaryModelOwnership = [];
2087
+ const context7 = canonical.servers.context7;
2088
+ if (context7 !== void 0) {
2089
+ assertCompatibleContext73(context7, readTomlSection(contentSource, "mcp_servers.context7"), contentSource);
2090
+ }
1869
2091
  if (contentSource === null) {
1870
2092
  const defaults = loadCanonicalDefaults(ctx.stackDir)["codex"] ?? {};
1871
2093
  for (const [key, value] of Object.entries(defaults)) {
@@ -1909,6 +2131,12 @@ ${body}`
1909
2131
  const section = `mcp_servers.${name}`;
1910
2132
  const existing = readTomlSection(content, section);
1911
2133
  const owned = ctx.ownedMcpServers?.has(name) === true;
2134
+ if (name === "context7" && existing !== null) {
2135
+ if (owned && !isCanonicalContext7Server3(server, existing, content)) {
2136
+ mcpOwnership.push({ server: name, owned: false });
2137
+ }
2138
+ continue;
2139
+ }
1912
2140
  if (!isCanonicalMcpServerEnabled(name, server, ctx.enabledMcpServers)) {
1913
2141
  if (owned) {
1914
2142
  if (isManagedOptionalStdioServer3(server, existing)) content = removeTomlSection(content, section);
@@ -1957,6 +2185,9 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1957
2185
  if (literalPairs.length > 0) body.push(`http_headers = { ${literalPairs.join(", ")} }`);
1958
2186
  if (refPairs.length > 0) body.push(`env_http_headers = { ${refPairs.join(", ")} }`);
1959
2187
  content = upsertTomlSection(content, section, body.join("\n"));
2188
+ if (name === "context7" && existing === null && !owned) {
2189
+ mcpOwnership.push({ server: name, owned: true });
2190
+ }
1960
2191
  }
1961
2192
  }
1962
2193
  if (content === null) return [];
@@ -1978,7 +2209,7 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1978
2209
  actions.push({ kind: "write", target: systemPromptFile, content });
1979
2210
  }
1980
2211
  const configFile = path11.join(ctx.configDir, "config.toml");
1981
- const config = readTextIfExists(configFile);
2212
+ const config = readMcpConfig3(configFile);
1982
2213
  if (config !== null) {
1983
2214
  let content = config;
1984
2215
  const primaryModelOwnership = [];
@@ -1993,6 +2224,18 @@ args = [${(server.args ?? []).map(tomlString).join(", ")}]`);
1993
2224
  const mcpOwnership = [];
1994
2225
  for (const [name, server] of Object.entries(mcp.servers)) {
1995
2226
  const section = `mcp_servers.${name}`;
2227
+ if (name === "context7") {
2228
+ const current = readTomlSection(content, section);
2229
+ const canonical = isCanonicalContext7Server3(server, current, content);
2230
+ const owned = ctx.ownedMcpServers?.has(name) === true;
2231
+ if (owned) {
2232
+ if (canonical) {
2233
+ content = removeTomlSection(content, section);
2234
+ }
2235
+ mcpOwnership.push({ server: name, owned: false });
2236
+ }
2237
+ continue;
2238
+ }
1996
2239
  if (!server.optional) {
1997
2240
  content = removeTomlSection(content, section);
1998
2241
  continue;
@@ -2782,7 +3025,7 @@ function enabledMcpServers(runtime, explicitDevtoolsEnabled, useBrowserPreferenc
2782
3025
  function ownedMcpServers(runtime, useBrowserPreferences = true) {
2783
3026
  if (!useBrowserPreferences) return /* @__PURE__ */ new Set();
2784
3027
  const file = devtoolsMcpPreferenceFile();
2785
- return loadDevtoolsMcpOwnership(file, runtime, DEVTOOLS_MCP_SERVER) ? /* @__PURE__ */ new Set([DEVTOOLS_MCP_SERVER]) : /* @__PURE__ */ new Set();
3028
+ return new Set([DEVTOOLS_MCP_SERVER, "context7"].filter((server) => loadDevtoolsMcpOwnership(file, runtime, server)));
2786
3029
  }
2787
3030
  function persistConfigurationOwnershipChanges(runtime, configDir, plan) {
2788
3031
  const latest = /* @__PURE__ */ new Map();
@@ -2814,17 +3057,29 @@ function makeContext(adapter, configDir, mode = DEFAULT_INSTALL_MODE_PREFERENCE,
2814
3057
  ownedPrimaryModelFields: useBrowserPreferences ? loadPrimaryModelOwnership(primaryModelOwnershipFile(), adapter.id, configDir) : /* @__PURE__ */ new Set()
2815
3058
  };
2816
3059
  }
2817
- function buildPlan(adapter, ctx) {
3060
+ function buildContentPlan(adapter, ctx) {
2818
3061
  return [
2819
3062
  ...planSystemPrompt(adapter, ctx),
2820
3063
  ...planAgents(adapter, ctx),
2821
3064
  ...planSkills(adapter, ctx),
2822
3065
  ...planCommands(adapter, ctx),
2823
3066
  ...planHooks(adapter, ctx),
2824
- ...planMcp(adapter, ctx),
2825
3067
  ...planPlugins(adapter, ctx)
2826
3068
  ];
2827
3069
  }
3070
+ function buildPlan(adapter, ctx) {
3071
+ return [...planMcp(adapter, ctx), ...buildContentPlan(adapter, ctx)];
3072
+ }
3073
+ function preflightSelectedMcpConfigs(runtimes, targetDir) {
3074
+ for (const id of runtimes) {
3075
+ const adapter = ADAPTERS[id];
3076
+ if (!adapter) continue;
3077
+ const detection = adapter.detect();
3078
+ if (targetDir === void 0 && !detection.installed) continue;
3079
+ const ctx = makeContext(adapter, targetDir ?? detection.configDir, void 0, targetDir === void 0);
3080
+ if (ctx) planMcp(adapter, ctx);
3081
+ }
3082
+ }
2828
3083
  function diffPlan(plan) {
2829
3084
  return plan.map((action) => {
2830
3085
  if (action.kind === "write") {
@@ -2883,6 +3138,13 @@ async function runInstall(opts) {
2883
3138
  p.log.error(error instanceof Error ? error.message : String(error));
2884
3139
  return 1;
2885
3140
  }
3141
+ const modelMap = opts.runtimes.length > 0 ? loadModelMap() : {};
3142
+ try {
3143
+ preflightSelectedMcpConfigs(opts.runtimes, opts.targetDir);
3144
+ } catch (error) {
3145
+ p.log.error(error instanceof Error ? error.message : String(error));
3146
+ return 1;
3147
+ }
2886
3148
  let writingStyle;
2887
3149
  let preparedStyle;
2888
3150
  try {
@@ -2912,7 +3174,6 @@ async function runInstall(opts) {
2912
3174
  const playwrightCapability = opts.dryRun || !useManifest ? void 0 : opts.playwrightCapability ?? (shouldInspectPlaywright ? inspectPlaywrightCapability() : void 0);
2913
3175
  const effectivePlaywright = playwrightCapability?.effective;
2914
3176
  const plannedPlaywright = effectivePlaywright ?? (toolPlan !== null && toolPlan.actions.length > 0 ? false : void 0);
2915
- const modelMap = hasFileRuntimes ? loadModelMap() : {};
2916
3177
  if (preparedStyle !== void 0) {
2917
3178
  try {
2918
3179
  p.log.info(`Estilo de escritura: ${preparedStyle.sourcePath}${opts.dryRun ? " (instalaci\xF3n prevista; sin escrituras)" : ""}.`);
@@ -2977,7 +3238,7 @@ async function runInstall(opts) {
2977
3238
  let diff = diffPlan(plan);
2978
3239
  let creates = diff.filter((d) => d.status === "create");
2979
3240
  let updates = diff.filter((d) => d.status === "update");
2980
- let changes = [...creates, ...updates];
3241
+ let changes = diff.filter((change) => change.status !== "unchanged");
2981
3242
  const prevManifest = useManifest ? readManifest().runtimes[id] : void 0;
2982
3243
  const orphans = canOrphan && prevManifest ? findOrphans(prevManifest.owned, current.targets) : [];
2983
3244
  p.log.step(`${adapter.name} \u2192 ${configDir}`);
@@ -3024,7 +3285,7 @@ async function runInstall(opts) {
3024
3285
  diff = diffPlan(plan);
3025
3286
  creates = diff.filter((d) => d.status === "create");
3026
3287
  updates = diff.filter((d) => d.status === "update");
3027
- changes = [...creates, ...updates];
3288
+ changes = diff.filter((change) => change.status !== "unchanged");
3028
3289
  }
3029
3290
  const backup = useManifest ? createBackup([...updates.map((c) => c.action.target), ...orphans], `install-${id}`) : null;
3030
3291
  if (backup) p.log.info(`Backup: ${backup.id} (${backup.files.length} archivos)`);
@@ -3195,14 +3456,11 @@ var piAdapter = {
3195
3456
  injectEngramProtocol() {
3196
3457
  return true;
3197
3458
  },
3198
- // Pi 0.8.18 recompone estos marcadores desde su snapshot: no conoce los nuevos.
3459
+ // La guía Context7 se habilita al adoptar su registro HTTP en Pi.
3199
3460
  adaptSystemPromptSections(sections) {
3200
- const { context7, playwright, "chrome-devtools": devtools, ...legacy } = sections;
3201
- return {
3202
- ...legacy,
3203
- "system-prompt": [legacy["system-prompt"], context7].filter(Boolean).join("\n\n"),
3204
- browser: [playwright, devtools].filter(Boolean).join("\n\n")
3205
- };
3461
+ const modular = { ...sections };
3462
+ delete modular.context7;
3463
+ return modular;
3206
3464
  }
3207
3465
  };
3208
3466
 
@@ -4285,7 +4543,7 @@ async function runUninstall(opts) {
4285
4543
  if (!detection.installed) continue;
4286
4544
  const keepCtx = makeContext(keep, detection.configDir);
4287
4545
  if (!keepCtx) continue;
4288
- for (const action of buildPlan(keep, keepCtx)) retained.add(path26.resolve(action.target));
4546
+ for (const action of buildContentPlan(keep, keepCtx)) retained.add(path26.resolve(action.target));
4289
4547
  }
4290
4548
  }
4291
4549
  for (const id of opts.runtimes) {
@@ -4303,13 +4561,20 @@ async function runUninstall(opts) {
4303
4561
  const ctx = makeContext(adapter, configDir, void 0, useBrowserPreferences);
4304
4562
  if (!ctx) continue;
4305
4563
  ctx.preserveEngram = !removeEngram;
4306
- const unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
4564
+ let unmerge;
4565
+ try {
4566
+ unmerge = adapter.planUnmerge(mcpForUnmerge, hooks, ctx);
4567
+ } catch (error) {
4568
+ p2.log.error(`${adapter.name}: no se pudo planificar la limpieza en ${configDir} \u2014 ${error instanceof Error ? error.message : String(error)}.`);
4569
+ exitCode = 1;
4570
+ continue;
4571
+ }
4307
4572
  const mergedTargets = new Set(unmerge.map((a) => path26.resolve(a.target)));
4308
4573
  const usingRealConfig = opts.targetDir === void 0;
4309
4574
  const prevOwned = usingRealConfig ? readManifest().runtimes[id]?.owned ?? [] : [];
4310
4575
  const pruneRoot = usingRealConfig ? HOME : path26.dirname(configDir);
4311
4576
  const planTargets = [
4312
- .../* @__PURE__ */ new Set([...buildPlan(adapter, ctx).map((a) => path26.resolve(a.target)), ...prevOwned.map((t) => path26.resolve(t))])
4577
+ .../* @__PURE__ */ new Set([...buildContentPlan(adapter, ctx).map((a) => path26.resolve(a.target)), ...prevOwned.map((t) => path26.resolve(t))])
4313
4578
  ].filter((t) => !mergedTargets.has(t) && fs19.existsSync(t));
4314
4579
  const deleteTargets = planTargets.filter(
4315
4580
  (t) => !retained.has(t) && !(ctx.preserveEngram && path26.basename(t) === "engram.ts") && isContainedIn(t, pruneRoot)
@@ -4579,7 +4844,7 @@ async function runDoctor(options = {}) {
4579
4844
  pending = diffPlan(buildPlan(adapter, ctx)).filter((d) => d.status !== "unchanged").length;
4580
4845
  } catch (err) {
4581
4846
  p3.log.error(
4582
- `${adapter.name}: config ilegible en ${detection.configDir} \u2014 ${err instanceof Error ? err.message : err}`
4847
+ `${adapter.name}: configuraci\xF3n incompatible o ilegible en ${detection.configDir} \u2014 ${err instanceof Error ? err.message : err}`
4583
4848
  );
4584
4849
  problems++;
4585
4850
  continue;
@@ -5866,16 +6131,16 @@ import { createHash as createHash2 } from "crypto";
5866
6131
  var pi_runtime_pin_default = {
5867
6132
  package: {
5868
6133
  name: "jorgex-pi",
5869
- version: "0.8.18",
5870
- source: "npm:jorgex-pi@0.8.18"
6134
+ version: "0.8.20",
6135
+ source: "npm:jorgex-pi@0.8.20"
5871
6136
  },
5872
6137
  provenance: {
5873
- commit: "61d676b300707d61840165616f0880d8c6588ce7"
6138
+ commit: "c4d021382cd3b752eaa396044753ac7533b6645d"
5874
6139
  },
5875
6140
  tarball: {
5876
- bytes: 89122709,
5877
- sha256: "02e5577d95d19ae28de55c170d4b6b48e682fbe741c8eec26cd79587da2f829e",
5878
- sha512: "a9fa60be18b6a84dca6703bf38a1462482dd3ea00d9eb82aead06e9a86b3d92ce09b68da2968cb760d6b3ad8d5b2b36d53889bed227e7ea6791932ce25153220"
6141
+ bytes: 89127646,
6142
+ sha256: "c7bbb0dc587b10e8e04174589cc519ad7576d2cbbceb5d2d20614d313e1ee884",
6143
+ sha512: "189480a4d812048b65466156479d1d89f7fa7044471ff6a076b710c0014b0e50eaa0261d7a2a8381a5e71a598be61fe2ed714b6b1dacd5a5f955b59a412009c0"
5879
6144
  }
5880
6145
  };
5881
6146
 
@@ -5890,6 +6155,7 @@ var PI_RUNTIME_CANDIDATE = {
5890
6155
  capabilities: [
5891
6156
  "foundation-contract-v1",
5892
6157
  "stack-snapshot-v2",
6158
+ "modular-system-prompts-v1",
5893
6159
  "runtime-agents-v1",
5894
6160
  "permission-gated-tools-v1",
5895
6161
  "structured-questions-v1",
@@ -5897,6 +6163,7 @@ var PI_RUNTIME_CANDIDATE = {
5897
6163
  "goal-continuation-v1",
5898
6164
  "mcp-adapter-v1",
5899
6165
  "engram-runtime-tools-v1",
6166
+ "context7-http-v1",
5900
6167
  "chrome-devtools-handoff-v1",
5901
6168
  "playwright-handoff-v1",
5902
6169
  "runner-json-v1",
@@ -7583,6 +7850,7 @@ Flags disponibles: jorgex-stack --help`
7583
7850
  p6.intro(`jorgex-stack ${command}${flags.dryRun ? " (dry-run)" : ""}`);
7584
7851
  try {
7585
7852
  assertSelectedPromptFiles(runtimes, flags.targetDir);
7853
+ preflightSelectedMcpConfigs(fileRuntimes, flags.targetDir);
7586
7854
  const writingStyle = prepareWritingStyle(resolveWritingStyleFile({ targetDir: flags.targetDir }), { rootDir: flags.targetDir });
7587
7855
  if (flags.targetDir === void 0) {
7588
7856
  const errors = browserPreferenceErrors();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.41",
3
+ "version": "1.9.42",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",