@topolo/cli 0.13.7 → 0.13.9

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 CHANGED
@@ -14,7 +14,7 @@ function registerAgentPacksCommand(program2) {
14
14
  throw new Error("Exact closed smoke operation, role/case and UUID required.");
15
15
  const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
16
16
  if (options.harness === "claude") {
17
- const { operateClaudeSmoke } = await import("./claude-pack-backend-3YJNKMYD.js");
17
+ const { operateClaudeSmoke } = await import("./claude-pack-backend-4F4VHMI5.js");
18
18
  console.log(JSON.stringify(await operateClaudeSmoke({
19
19
  store,
20
20
  kind: "smoke-" + operation,
@@ -44,7 +44,7 @@ function registerAgentPacksCommand(program2) {
44
44
  });
45
45
  packs.command("configure").description("Qualify and configure an adopted Claude roster through its live native backend").argument("<pack>").option("--harness <name>", "Host adapter", "claude").requiredOption("--policy <path>", "Reviewed execution policy JSON").requiredOption("--endpoint <url>", "Explicit loopback main-process inspector endpoint, enabled separately with authorization").requiredOption("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply native groups, pins, models, effort and permission modes; otherwise preflight only").action(async (pack, options) => {
46
46
  if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
47
- const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
47
+ const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
48
48
  const result = await configureClaudePack({
49
49
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
50
50
  pack,
@@ -96,7 +96,7 @@ function registerAgentPacksCommand(program2) {
96
96
  throw new Error("Only a one-request once/deny response is supported.");
97
97
  decision = response.decision;
98
98
  }
99
- const { operateClaudeApprovals } = await import("./claude-pack-backend-3YJNKMYD.js");
99
+ const { operateClaudeApprovals } = await import("./claude-pack-backend-4F4VHMI5.js");
100
100
  console.log(JSON.stringify(await operateClaudeApprovals({
101
101
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
102
102
  kind: args[0],
@@ -111,7 +111,7 @@ function registerAgentPacksCommand(program2) {
111
111
  if (options.policy || options.endpoint || options.pid || options.apply) {
112
112
  if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
113
113
  throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
114
- const { configureClaudePack } = await import("./claude-pack-backend-3YJNKMYD.js");
114
+ const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
115
115
  const result2 = await configureClaudePack({
116
116
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
117
117
  pack: args[0],
@@ -158,7 +158,7 @@ import { platform as platform2 } from "os";
158
158
  import { createInterface } from "readline/promises";
159
159
  import { stdin as input } from "process";
160
160
 
161
- // node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/index.js
161
+ // node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/index.js
162
162
  function record(value) {
163
163
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
164
164
  }
@@ -1741,7 +1741,7 @@ var TopoloClient = class _TopoloClient {
1741
1741
  } catch (error) {
1742
1742
  const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
1743
1743
  if (!transportFailure || attempt === maxAttempts) throw error;
1744
- await new Promise((resolve4) => setTimeout(resolve4, 250));
1744
+ await new Promise((resolve5) => setTimeout(resolve5, 250));
1745
1745
  }
1746
1746
  }
1747
1747
  throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
@@ -3740,7 +3740,7 @@ function createTopolo(options) {
3740
3740
  async function pollUntilApproved(params) {
3741
3741
  let interval = Math.max(params.intervalSeconds ?? 5, 1);
3742
3742
  while (Date.now() < params.expiresAtMs) {
3743
- await new Promise((resolve4) => setTimeout(resolve4, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
3743
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
3744
3744
  if (Date.now() >= params.expiresAtMs) break;
3745
3745
  try {
3746
3746
  return await params.oauth.pollDeviceToken({
@@ -3768,10 +3768,11 @@ async function pollUntilApproved(params) {
3768
3768
  throw new Error("Device code expired. Run `topolo auth login` again.");
3769
3769
  }
3770
3770
 
3771
- // node_modules/.pnpm/@topolo+sdk@0.23.19/node_modules/@topolo/sdk/dist/node/credential-store.js
3772
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
3771
+ // node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/node/credential-store.js
3772
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
3773
3773
  import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
3774
3774
  import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
3775
+ import { constants, fstatSync, readSync } from "fs";
3775
3776
  import { homedir as homedir2, platform } from "os";
3776
3777
  import { dirname as dirname2, join as join3 } from "path";
3777
3778
  import { createHash, randomUUID } from "crypto";
@@ -4637,6 +4638,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
4637
4638
  var REFRESH_SKEW_SECONDS = 60;
4638
4639
  var CONFIG_LOCK_RETRY_MS = 25;
4639
4640
  var CONFIG_LOCK_TIMEOUT_MS = 5e3;
4641
+ var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
4640
4642
  var ConfigOwnershipError = class extends TopoloSdkError2 {
4641
4643
  constructor() {
4642
4644
  super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
@@ -4651,32 +4653,145 @@ function lockEntryExists(path) {
4651
4653
  throw new ConfigOwnershipError();
4652
4654
  }
4653
4655
  }
4656
+ function lockIdentity(entry) {
4657
+ return [entry.dev, entry.ino, entry.uid, entry.mode, entry.nlink, entry.size, entry.mtimeMs, entry.ctimeMs];
4658
+ }
4659
+ function privateLockDirectory(path) {
4660
+ const entry = lstatSync(path);
4661
+ if (typeof process.getuid !== "function" || !entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== process.getuid() || (entry.mode & 511) !== 448) throw new ConfigOwnershipError();
4662
+ return entry;
4663
+ }
4664
+ function readPrivateLockMarker(path) {
4665
+ const before = lstatSync(path);
4666
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 512 || before.uid !== process.getuid?.() || (before.mode & 511) !== 384) throw new ConfigOwnershipError();
4667
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
4668
+ try {
4669
+ if (JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
4670
+ const bytes = Buffer.alloc(513);
4671
+ const count = readSync(fd, bytes, 0, bytes.length, 0);
4672
+ if (count !== before.size || count > 512 || JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before)) || JSON.stringify(lockIdentity(lstatSync(path))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
4673
+ return bytes.subarray(0, count).toString("utf8");
4674
+ } finally {
4675
+ closeSync(fd);
4676
+ }
4677
+ }
4678
+ var OWNER_MARKER_NAME = /^owner-[a-f0-9-]{36}\.json$/;
4679
+ function readOwnerMarker(lockPath, name) {
4680
+ const bytes = readPrivateLockMarker(join3(lockPath, name));
4681
+ const owner = JSON.parse(bytes);
4682
+ if (owner.version !== 1 || Object.keys(owner).sort().join(",") !== "identity,pid,version" || name !== "owner-" + owner.identity + ".json" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new ConfigOwnershipError();
4683
+ return { bytes, pid: owner.pid };
4684
+ }
4685
+ function writePrivateLockRecord(path, bytes) {
4686
+ const fd = openSync(path, "wx", 384);
4687
+ try {
4688
+ writeFileSync(fd, bytes);
4689
+ fsyncSync(fd);
4690
+ } finally {
4691
+ closeSync(fd);
4692
+ }
4693
+ }
4694
+ function deadOwnerMarker(lockPath, ignoreOwnFence = false) {
4695
+ try {
4696
+ if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) return null;
4697
+ const directory = privateLockDirectory(lockPath);
4698
+ const entries = readdirSync(lockPath);
4699
+ if (entries.length !== 1 || !OWNER_MARKER_NAME.test(entries[0])) return null;
4700
+ const name = entries[0];
4701
+ const { bytes, pid } = readOwnerMarker(lockPath, name);
4702
+ if (Date.now() - lstatSync(join3(lockPath, name)).mtimeMs < CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS) return null;
4703
+ try {
4704
+ process.kill(pid, 0);
4705
+ return null;
4706
+ } catch (error) {
4707
+ if (error.code !== "ESRCH") return null;
4708
+ }
4709
+ return { directory, name, bytes, pid };
4710
+ } catch {
4711
+ return null;
4712
+ }
4713
+ }
4714
+ function reclaimDeadOwnerLock(lockPath, markerName, marker) {
4715
+ const observed = deadOwnerMarker(lockPath);
4716
+ if (!observed) return false;
4717
+ const fence = lockPath + ".reconcile";
4718
+ try {
4719
+ mkdirSync(fence, { mode: 448 });
4720
+ } catch {
4721
+ return false;
4722
+ }
4723
+ const fenceMarker = join3(fence, markerName);
4724
+ const quarantine = join3(dirname2(lockPath), ".credential-reclaim-" + randomUUID2());
4725
+ let captured;
4726
+ let reclaimed = false;
4727
+ try {
4728
+ writePrivateLockRecord(fenceMarker, marker);
4729
+ const current = deadOwnerMarker(lockPath, true);
4730
+ if (!current || current.name !== observed.name || current.bytes !== observed.bytes || current.directory.dev !== observed.directory.dev || current.directory.ino !== observed.directory.ino) return false;
4731
+ mkdirSync(quarantine, { mode: 448 });
4732
+ writePrivateLockRecord(join3(quarantine, "receipt.json"), JSON.stringify({
4733
+ schemaVersion: 1,
4734
+ disposition: "DEAD_OWNER_RECLAIMED",
4735
+ lockPath,
4736
+ deadOwnerMarker: current.name,
4737
+ deadOwnerPid: current.pid,
4738
+ deadOwnerEvidence: "ESRCH",
4739
+ reclaimedByPid: process.pid,
4740
+ reclaimedAt: (/* @__PURE__ */ new Date()).toISOString(),
4741
+ configCredentialOrOAuthEffectsByThisReclaim: "none"
4742
+ }, null, 2));
4743
+ captured = join3(quarantine, current.name);
4744
+ renameSync(join3(lockPath, current.name), captured);
4745
+ const directory = privateLockDirectory(lockPath);
4746
+ if (directory.dev !== observed.directory.dev || directory.ino !== observed.directory.ino || readdirSync(lockPath).length) throw new ConfigOwnershipError();
4747
+ writePrivateLockRecord(join3(lockPath, markerName), marker);
4748
+ reclaimed = true;
4749
+ } catch {
4750
+ if (captured && !reclaimed) {
4751
+ try {
4752
+ linkSync(captured, join3(lockPath, observed.name));
4753
+ } catch {
4754
+ }
4755
+ }
4756
+ } finally {
4757
+ try {
4758
+ if (readPrivateLockMarker(fenceMarker) !== marker || readdirSync(fence).length !== 1) throw new ConfigOwnershipError();
4759
+ unlinkSync(fenceMarker);
4760
+ rmdirSync(fence);
4761
+ } catch {
4762
+ reclaimed = false;
4763
+ }
4764
+ }
4765
+ return reclaimed;
4766
+ }
4767
+ var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
4654
4768
  async function withConfigWriteLock(operation) {
4655
4769
  const lockPath = `${configPath()}.lock`;
4656
4770
  await mkdir2(dirname2(lockPath), { recursive: true });
4657
4771
  const startedAt = performance.now();
4772
+ const identity = randomUUID2();
4773
+ const markerName = "owner-" + identity + ".json";
4774
+ const markerPath = join3(lockPath, markerName);
4775
+ const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4658
4776
  while (true) {
4659
4777
  try {
4660
4778
  if (lockEntryExists(lockPath + ".reconcile")) throw Object.assign(new Error(), { code: "EEXIST" });
4661
- await mkdir2(lockPath, { mode: 448 });
4779
+ mkdirSync(lockPath, { mode: 448 });
4662
4780
  if (lockEntryExists(lockPath + ".reconcile")) {
4663
4781
  throw new ConfigOwnershipError();
4664
4782
  }
4783
+ writeFileSync(markerPath, marker, { flag: "wx", mode: 384 });
4665
4784
  break;
4666
4785
  } catch (error) {
4667
4786
  if (error.code !== "EEXIST") throw error;
4787
+ if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
4668
4788
  if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4669
4789
  throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
4670
4790
  }
4671
4791
  await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
4672
4792
  }
4673
4793
  }
4674
- const directory = await lstat(lockPath);
4675
- const identity = randomUUID2();
4676
- const markerName = "owner-" + identity + ".json";
4677
- const markerPath = join3(lockPath, markerName);
4678
- const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4679
- await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
4794
+ const directory = lstatSync(lockPath);
4680
4795
  const assertDirectory = async () => {
4681
4796
  const current = await lstat(lockPath);
4682
4797
  if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
@@ -4700,10 +4815,14 @@ async function withConfigWriteLock(operation) {
4700
4815
  throw new ConfigOwnershipError();
4701
4816
  };
4702
4817
  const assertOwnedSync = () => {
4703
- assertDirectorySync();
4704
- const entries = readdirSync(lockPath);
4705
- const entry = lstatSync(markerPath);
4706
- if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4818
+ try {
4819
+ assertDirectorySync();
4820
+ const entries = readdirSync(lockPath);
4821
+ const entry = lstatSync(markerPath);
4822
+ if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4823
+ } catch {
4824
+ throw new ConfigOwnershipError();
4825
+ }
4707
4826
  };
4708
4827
  const persist = async (update) => {
4709
4828
  const path = configPath();
@@ -4732,14 +4851,14 @@ async function withConfigWriteLock(operation) {
4732
4851
  if (temporaryOwned) unlinkSync(temporary);
4733
4852
  }
4734
4853
  };
4735
- const release = async () => {
4736
- await assertOwned();
4854
+ const release = () => {
4855
+ assertOwnedSync();
4737
4856
  const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
4738
- await mkdir2(quarantine, { mode: 448 });
4857
+ mkdirSync(quarantine, { mode: 448 });
4739
4858
  const captured = join3(quarantine, markerName);
4740
4859
  try {
4741
- await assertOwned();
4742
- await rename2(markerPath, captured);
4860
+ assertOwnedSync();
4861
+ renameSync(markerPath, captured);
4743
4862
  assertDirectorySync();
4744
4863
  const entry = lstatSync(captured);
4745
4864
  if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
@@ -4751,20 +4870,40 @@ async function withConfigWriteLock(operation) {
4751
4870
  } catch {
4752
4871
  if (existsSync2(captured)) {
4753
4872
  try {
4754
- await link(captured, markerPath);
4873
+ linkSync(captured, markerPath);
4755
4874
  } catch {
4756
4875
  }
4757
4876
  }
4758
4877
  throw new ConfigOwnershipError();
4759
4878
  }
4760
4879
  };
4880
+ let held = true;
4881
+ const releaseOnce = () => {
4882
+ if (!held) return;
4883
+ held = false;
4884
+ process.off("exit", onExit);
4885
+ for (const signal of RELEASE_SIGNALS) process.off(signal, onSignal);
4886
+ release();
4887
+ };
4888
+ const onExit = () => {
4889
+ try {
4890
+ releaseOnce();
4891
+ } catch {
4892
+ }
4893
+ };
4894
+ const onSignal = (signal) => {
4895
+ onExit();
4896
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
4897
+ };
4898
+ process.on("exit", onExit);
4899
+ for (const signal of RELEASE_SIGNALS) process.on(signal, onSignal);
4761
4900
  try {
4762
4901
  await assertOwned();
4763
4902
  const result = await operation(assertOwned, persist);
4764
4903
  await assertOwned();
4765
4904
  return result;
4766
4905
  } finally {
4767
- await release();
4906
+ releaseOnce();
4768
4907
  }
4769
4908
  }
4770
4909
  function renewalFailure(error) {
@@ -6382,7 +6521,7 @@ import { spawnSync as spawnSync4 } from "child_process";
6382
6521
  import { existsSync as existsSync6 } from "fs";
6383
6522
  import { readFile as readFile6, rm as rm2 } from "fs/promises";
6384
6523
  import { homedir as homedir5 } from "os";
6385
- import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
6524
+ import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
6386
6525
  import { fileURLToPath as fileURLToPath3 } from "url";
6387
6526
  import { createRequire } from "module";
6388
6527
 
@@ -6391,7 +6530,7 @@ import { spawnSync as spawnSync3 } from "child_process";
6391
6530
  import { existsSync as existsSync4 } from "fs";
6392
6531
  import { readFile as readFile4 } from "fs/promises";
6393
6532
  import { homedir as homedir4 } from "os";
6394
- import { isAbsolute, join as join6 } from "path";
6533
+ import { isAbsolute, join as join6, resolve as resolve2 } from "path";
6395
6534
  var SUPPORTED_AGENT_IDS = [
6396
6535
  "claude",
6397
6536
  "claude-desktop",
@@ -6431,6 +6570,18 @@ var AGENT_NAME_FOR = {
6431
6570
  goose: "goose",
6432
6571
  "amazon-q": "amazon-q"
6433
6572
  };
6573
+ function resolveAgentHostTarget(options) {
6574
+ const homeDir = resolve2(options.homeDir ?? homedir4());
6575
+ const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
6576
+ if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute(codexHome))) {
6577
+ throw new Error("Codex home must be an absolute nonempty directory.");
6578
+ }
6579
+ return Object.freeze({
6580
+ homeDir,
6581
+ ...options.projectDir ? { projectDir: resolve2(options.projectDir) } : {},
6582
+ codexHome: codexHome ?? join6(homeDir, ".codex")
6583
+ });
6584
+ }
6434
6585
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
6435
6586
  function isPublicRegistrationName(name) {
6436
6587
  return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
@@ -6446,14 +6597,14 @@ function stableNodeCommand() {
6446
6597
  });
6447
6598
  return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
6448
6599
  }
6449
- function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
6600
+ function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
6450
6601
  switch (agent) {
6451
6602
  case "claude":
6452
6603
  return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
6453
6604
  case "claude-desktop":
6454
6605
  return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
6455
6606
  case "codex":
6456
- return join6(homeDir, ".codex", "skills", "topolo", "SKILL.md");
6607
+ return join6(codexHome ?? join6(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
6457
6608
  case "cursor":
6458
6609
  return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
6459
6610
  case "grok":
@@ -6471,10 +6622,10 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
6471
6622
  return void 0;
6472
6623
  }
6473
6624
  }
6474
- function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
6625
+ function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
6475
6626
  switch (agent) {
6476
6627
  case "claude":
6477
- return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
6628
+ return [{ path: projectDir ? join6(projectDir, ".mcp.json") : join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
6478
6629
  case "claude-desktop":
6479
6630
  return [{
6480
6631
  path: claudeDesktopConfigPath(homeDir),
@@ -6482,7 +6633,7 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
6482
6633
  rootKey: "mcpServers"
6483
6634
  }];
6484
6635
  case "codex":
6485
- return [{ path: join6(homeDir, ".codex", "config.toml"), format: "toml" }];
6636
+ return [{ path: join6(codexHome ?? join6(homeDir, ".codex"), "config.toml"), format: "toml" }];
6486
6637
  case "cursor":
6487
6638
  return [{
6488
6639
  path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
@@ -6563,8 +6714,36 @@ function agentCliNames(agent) {
6563
6714
  return ["qchat", "q"];
6564
6715
  }
6565
6716
  }
6566
- function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
6567
- return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
6717
+ function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
6718
+ return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
6719
+ }
6720
+ async function assertClaudeProjectScope(homeDir, projectDir, name) {
6721
+ const path = join6(homeDir, ".claude.json");
6722
+ let contents;
6723
+ try {
6724
+ contents = await readFile4(path, "utf8");
6725
+ } catch (error) {
6726
+ if (error.code === "ENOENT") return;
6727
+ throw error;
6728
+ }
6729
+ let config;
6730
+ try {
6731
+ config = JSON.parse(contents);
6732
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
6733
+ } catch {
6734
+ throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
6735
+ }
6736
+ const object = (value) => {
6737
+ if (value === void 0) return void 0;
6738
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6739
+ throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
6740
+ }
6741
+ return value;
6742
+ };
6743
+ const local = object(object(config.projects)?.[resolve2(projectDir)]);
6744
+ if (Object.hasOwn(object(config.mcpServers) ?? {}, name) || Object.hasOwn(object(local?.mcpServers) ?? {}, name)) {
6745
+ throw new Error("Claude MCP scope conflict for " + name + ": a user or local definition can override the project registration. Resolve that scope before setup.");
6746
+ }
6568
6747
  }
6569
6748
  function claudeDesktopConfigPath(homeDir) {
6570
6749
  if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
@@ -6576,9 +6755,9 @@ function vscodeUserConfigPath(homeDir) {
6576
6755
  if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
6577
6756
  return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
6578
6757
  }
6579
- async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
6758
+ async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
6580
6759
  const registrations = [];
6581
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
6760
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
6582
6761
  if (!existsSync4(config.path)) continue;
6583
6762
  if (config.format === "json") {
6584
6763
  registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
@@ -6657,6 +6836,7 @@ async function readGooseRegistrations(path) {
6657
6836
  continue;
6658
6837
  }
6659
6838
  if (inExtensions && /^\S/.test(rawLine) && rawLine.trim()) {
6839
+ if (current?.command) registrations.push(current);
6660
6840
  inExtensions = false;
6661
6841
  inTopolo = false;
6662
6842
  current = void 0;
@@ -6664,6 +6844,7 @@ async function readGooseRegistrations(path) {
6664
6844
  if (!inExtensions) continue;
6665
6845
  const extension = rawLine.match(/^ ([A-Za-z0-9_-]+):\s*$/);
6666
6846
  if (extension) {
6847
+ if (current?.command) registrations.push(current);
6667
6848
  inTopolo = true;
6668
6849
  current = isPublicRegistrationName(extension[1]) ? { name: extension[1], command: "", args: [], env: {} } : void 0;
6669
6850
  continue;
@@ -6841,17 +7022,19 @@ function message(error) {
6841
7022
 
6842
7023
  // src/commands/agent-config.ts
6843
7024
  import { existsSync as existsSync5 } from "fs";
6844
- import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
7025
+ import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
7026
+ import { randomUUID as randomUUID3 } from "crypto";
6845
7027
  import { dirname as dirname5 } from "path";
6846
- async function installAgentSkill(agent, source, homeDir, projectDir) {
6847
- const path = agentSkillPath(agent, homeDir, projectDir);
7028
+ async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
7029
+ const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
6848
7030
  if (!path) return { installed: false };
6849
7031
  const contents = formatSkillForAgent(agent, source);
6850
7032
  await atomicWrite(path, contents);
6851
7033
  return { path, installed: true };
6852
7034
  }
6853
- async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir) {
6854
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
7035
+ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
7036
+ if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
7037
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
6855
7038
  if (config.format === "json") {
6856
7039
  await registerJson(config, agent, mcp, registration);
6857
7040
  } else if (config.format === "toml") {
@@ -6861,8 +7044,8 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
6861
7044
  }
6862
7045
  }
6863
7046
  }
6864
- async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
6865
- for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
7047
+ async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
7048
+ for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
6866
7049
  if (!existsSync5(config.path)) continue;
6867
7050
  if (config.format === "json") {
6868
7051
  await unregisterJson(config, agent, name);
@@ -6875,17 +7058,39 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
6875
7058
  }
6876
7059
  }
6877
7060
  function registrationEnvironment(agent, registration) {
7061
+ let privateAliases = {};
7062
+ if (registration.configPath) {
7063
+ const prefixes = {
7064
+ production: "TOPOLO_PRODUCTION",
7065
+ staging: "TOPOLO_STAGING",
7066
+ development: "TOPOLO_DEVELOPMENT",
7067
+ local: "TOPOLO_LOCAL"
7068
+ };
7069
+ const prefix = prefixes[registration.environment];
7070
+ if (!prefix) throw new Error("Cannot isolate a private profile for an unknown environment.");
7071
+ privateAliases = {
7072
+ TOPOLO_API_KEY: "",
7073
+ TOPOLO_ACCESS_TOKEN: "",
7074
+ [prefix + "_API_KEY"]: "",
7075
+ [prefix + "_ACCESS_TOKEN"]: "",
7076
+ TOPOLO_ENV: registration.environment
7077
+ };
7078
+ }
6878
7079
  return {
6879
7080
  TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
7081
+ ...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
6880
7082
  ...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
6881
- ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
7083
+ ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {},
7084
+ ...privateAliases
6882
7085
  };
6883
7086
  }
6884
7087
  async function registerJson(config, agent, mcp, registration) {
7088
+ const before = await fileSnapshot(config.path);
6885
7089
  let parsed = {};
6886
- if (existsSync5(config.path)) {
7090
+ if (before.contents !== void 0) {
6887
7091
  try {
6888
- parsed = JSON.parse(await readFile5(config.path, "utf8"));
7092
+ parsed = JSON.parse(before.contents);
7093
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
6889
7094
  } catch (error) {
6890
7095
  throw new Error(
6891
7096
  "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
@@ -6906,7 +7111,7 @@ async function registerJson(config, agent, mcp, registration) {
6906
7111
  if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
6907
7112
  servers[registration.name] = entry;
6908
7113
  parsed[rootKey] = servers;
6909
- await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
7114
+ await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
6910
7115
  }
6911
7116
  async function unregisterJson(config, agent, name) {
6912
7117
  let parsed;
@@ -6928,13 +7133,14 @@ async function unregisterJson(config, agent, name) {
6928
7133
  await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
6929
7134
  }
6930
7135
  async function registerToml(path, agent, mcp, registration) {
6931
- const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
7136
+ const before = await fileSnapshot(path);
7137
+ const existing = before.contents ?? "";
6932
7138
  const text2 = withoutMcpServerSections(existing, registration.name);
6933
7139
  const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
6934
7140
  const environment = registrationEnvironment(agent, registration);
6935
7141
  const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
6936
7142
  const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
6937
- await atomicWrite(path, text2 + block);
7143
+ await atomicWrite(path, text2 + block, before);
6938
7144
  }
6939
7145
  async function registerGoose(path, mcp, registration) {
6940
7146
  const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
@@ -7004,11 +7210,82 @@ function formatSkillForAgent(agent, source) {
7004
7210
  }
7005
7211
  return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
7006
7212
  }
7007
- async function atomicWrite(path, contents) {
7213
+ function fileIdentity(stat2) {
7214
+ return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeMs, stat2.ctimeMs, stat2.mode, stat2.nlink].join(":");
7215
+ }
7216
+ async function fileSnapshot(path) {
7217
+ const parents = [];
7218
+ for (let parent = dirname5(path); ; parent = dirname5(parent)) {
7219
+ try {
7220
+ const stat2 = await lstat2(parent);
7221
+ if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
7222
+ parents.push({ path: parent, identity: [stat2.dev, stat2.ino].join(":") });
7223
+ } catch (error) {
7224
+ if (error.code !== "ENOENT") throw error;
7225
+ }
7226
+ if (dirname5(parent) === parent) break;
7227
+ }
7228
+ try {
7229
+ const stat2 = await lstat2(path);
7230
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
7231
+ const contents = await readFile5(path, "utf8");
7232
+ if (fileIdentity(await lstat2(path)) !== fileIdentity(stat2)) throw new Error("Host configuration changed while reading at " + path + ".");
7233
+ return { contents, identity: fileIdentity(stat2), parents };
7234
+ } catch (error) {
7235
+ if (error.code === "ENOENT") return { parents };
7236
+ throw error;
7237
+ }
7238
+ }
7239
+ async function assertSnapshot(path, before) {
7240
+ const current = await fileSnapshot(path);
7241
+ if (current.contents !== before.contents || current.identity !== before.identity || before.parents.some((parent) => !current.parents.some((candidate) => candidate.path === parent.path && candidate.identity === parent.identity))) {
7242
+ throw new Error("Host destination changed before write at " + path + ".");
7243
+ }
7244
+ }
7245
+ async function atomicWrite(path, contents, before) {
7246
+ before ??= await fileSnapshot(path);
7247
+ await assertSnapshot(path, before);
7248
+ if (before.contents === contents) return;
7008
7249
  await mkdir3(dirname5(path), { recursive: true });
7009
- const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
7010
- await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
7011
- await rename3(temporaryPath, path);
7250
+ const destination = await fileSnapshot(path);
7251
+ await assertSnapshot(path, before);
7252
+ const temporaryPath = path + ".topolo-" + randomUUID3() + ".tmp";
7253
+ const handle = await open(temporaryPath, "wx", 384);
7254
+ let temporaryIdentity;
7255
+ let handleClosed = false;
7256
+ let temporaryPending = true;
7257
+ try {
7258
+ const created = await handle.stat();
7259
+ temporaryIdentity = [created.dev, created.ino].join(":");
7260
+ await assertSnapshot(path, destination);
7261
+ await writeFile3(handle, contents, { encoding: "utf8" });
7262
+ await handle.close();
7263
+ handleClosed = true;
7264
+ await assertSnapshot(path, destination);
7265
+ const temporary = await lstat2(temporaryPath);
7266
+ if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
7267
+ throw new Error("Host temporary file custody changed before rename at " + path + ".");
7268
+ }
7269
+ await rename2(temporaryPath, path);
7270
+ temporaryPending = false;
7271
+ const installed = await fileSnapshot(path);
7272
+ if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
7273
+ } finally {
7274
+ if (!handleClosed) await handle.close();
7275
+ if (temporaryPending && temporaryIdentity) {
7276
+ let cleanupOwned = false;
7277
+ try {
7278
+ const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
7279
+ const current2 = await lstat2(parent.path);
7280
+ return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
7281
+ }));
7282
+ const current = await lstat2(temporaryPath);
7283
+ cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
7284
+ } catch {
7285
+ }
7286
+ if (cleanupOwned) await unlink(temporaryPath);
7287
+ }
7288
+ }
7012
7289
  }
7013
7290
  function withoutMcpServerSections(text2, name) {
7014
7291
  const section = "[mcp_servers." + name + "]";
@@ -7029,8 +7306,8 @@ function withoutMcpServerSections(text2, name) {
7029
7306
  var AGENT_LABELS2 = AGENT_LABELS;
7030
7307
  function skillsSourceDir() {
7031
7308
  const here = dirname6(fileURLToPath3(import.meta.url));
7032
- const distCandidate = resolve2(here, "../skills");
7033
- const srcCandidate = resolve2(here, "../../skills");
7309
+ const distCandidate = resolve3(here, "../skills");
7310
+ const srcCandidate = resolve3(here, "../../skills");
7034
7311
  for (const candidate of [distCandidate, srcCandidate]) {
7035
7312
  if (existsSync6(join7(candidate, "topolo", "SKILL.md"))) return candidate;
7036
7313
  }
@@ -7072,17 +7349,32 @@ function resolveMcpRegistration(options) {
7072
7349
  if (installationId && /[\r\n]/.test(installationId)) {
7073
7350
  throw new Error("Installation id must be a single-line identifier.");
7074
7351
  }
7075
- return { name, ...context ? { context } : {}, environment, ...installationId ? { installationId } : {} };
7352
+ const configPath2 = process.env.TOPOLO_CONFIG_PATH;
7353
+ return {
7354
+ name,
7355
+ ...context ? { context } : {},
7356
+ environment,
7357
+ ...installationId ? { installationId } : {},
7358
+ ...configPath2 ? { configPath: configPath2 } : {}
7359
+ };
7076
7360
  }
7077
7361
  async function installForAgent(agent, sourceDir, mcp, opts) {
7362
+ const { homeDir, projectDir, codexHome } = opts.target;
7363
+ if (opts.withMcp && agent === "claude" && projectDir) {
7364
+ try {
7365
+ await assertClaudeProjectScope(homeDir, projectDir, opts.registration.name);
7366
+ } catch (err) {
7367
+ return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
7368
+ }
7369
+ }
7078
7370
  const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
7079
- const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
7371
+ const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
7080
7372
  if (!opts.withMcp) {
7081
7373
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
7082
7374
  }
7083
7375
  try {
7084
- await registerAgentConfig(agent, mcp, opts.homeDir, opts.registration, opts.projectDir);
7085
- const installed = (await readAgentRegistrations(agent, opts.homeDir, opts.projectDir)).find((registration) => registration.name === opts.registration.name);
7376
+ await registerAgentConfig(agent, mcp, homeDir, opts.registration, projectDir, codexHome);
7377
+ const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((registration) => registration.name === opts.registration.name);
7086
7378
  const expectedEnvironment = registrationEnvironment(agent, opts.registration);
7087
7379
  if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
7088
7380
  throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
@@ -7116,12 +7408,12 @@ function resolveTargets(opts) {
7116
7408
  if (opts.all) return [...SUPPORTED_AGENT_IDS];
7117
7409
  const homeDir = opts.homeDir ?? homedir5();
7118
7410
  const detectBinaries = homeDir === homedir5();
7119
- const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
7411
+ const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project, opts.codexHome) || detectBinaries && agentBinaryExists(agent));
7120
7412
  return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
7121
7413
  }
7122
7414
  function registerSkillsCommand(program2) {
7123
7415
  const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
7124
- skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
7416
+ skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
7125
7417
  async (opts) => {
7126
7418
  const mode = resolveOutputMode(opts.json);
7127
7419
  try {
@@ -7137,11 +7429,12 @@ function registerSkillsCommand(program2) {
7137
7429
  }
7138
7430
  }
7139
7431
  );
7140
- skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7432
+ skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7141
7433
  const mode = resolveOutputMode(opts.json);
7142
7434
  try {
7143
7435
  const rows = await agentSurfaceStatus({
7144
- homeDir: opts.agentHome ? resolve2(opts.agentHome) : void 0,
7436
+ homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0,
7437
+ codexHome: opts.codexHome,
7145
7438
  name: opts.name,
7146
7439
  agent: opts.agent,
7147
7440
  all: opts.all,
@@ -7152,12 +7445,12 @@ function registerSkillsCommand(program2) {
7152
7445
  fail2(err, mode);
7153
7446
  }
7154
7447
  });
7155
- skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7448
+ skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7156
7449
  const mode = resolveOutputMode(opts.json);
7157
7450
  try {
7158
7451
  const removed = await uninstallAgentSurfaces({
7159
7452
  ...opts,
7160
- homeDir: opts.agentHome ? resolve2(opts.agentHome) : void 0
7453
+ homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0
7161
7454
  });
7162
7455
  emit(
7163
7456
  { removed },
@@ -7176,20 +7469,20 @@ async function installAgentSurfaces(opts) {
7176
7469
  }
7177
7470
  const mcp = resolveMcpCommand();
7178
7471
  const registration = resolveMcpRegistration(opts);
7179
- const homeDir = opts.homeDir ?? homedir5();
7472
+ const target = resolveAgentHostTarget({ homeDir: opts.homeDir, projectDir: opts.project, codexHome: opts.codexHome });
7473
+ const { homeDir, projectDir, codexHome } = target;
7180
7474
  const results = [];
7181
- for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
7475
+ for (const agent of resolveTargets({ ...opts, homeDir, project: projectDir, codexHome })) {
7182
7476
  results.push(await installForAgent(agent, sourceDir, mcp, {
7183
7477
  withMcp: opts.mcp !== false,
7184
- projectDir: opts.project,
7185
- homeDir,
7478
+ target,
7186
7479
  registration
7187
7480
  }));
7188
7481
  }
7189
7482
  return { results, mcp };
7190
7483
  }
7191
7484
  function registerSetupCommand(program2) {
7192
- program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7485
+ program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
7193
7486
  const mode = resolveOutputMode(opts.json);
7194
7487
  try {
7195
7488
  const { results, mcp } = await installAgentSurfaces({
@@ -7211,18 +7504,20 @@ function assertAgentSetupSucceeded(results) {
7211
7504
  }
7212
7505
  }
7213
7506
  async function agentSurfaceStatus(options = {}) {
7214
- const homeDir = options.homeDir ?? homedir5();
7507
+ const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
7508
+ const { homeDir, projectDir, codexHome } = target;
7215
7509
  const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
7216
7510
  const agents = resolveTargets({
7217
7511
  agent: options.agent,
7218
7512
  all: options.all,
7219
7513
  homeDir,
7220
- project: options.project
7514
+ project: projectDir,
7515
+ codexHome
7221
7516
  });
7222
7517
  return Promise.all(agents.map(async (agent) => {
7223
- const skillPath = agentSkillPath(agent, homeDir, options.project);
7518
+ const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
7224
7519
  const installedSkill = skillPath && existsSync6(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
7225
- const registrations = (await readAgentRegistrations(agent, homeDir, options.project)).filter((registration) => !options.name || registration.name === options.name);
7520
+ const registrations = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).filter((registration) => !options.name || registration.name === options.name);
7226
7521
  return {
7227
7522
  agent,
7228
7523
  skillInstalled: installedSkill !== void 0,
@@ -7235,16 +7530,17 @@ async function agentSurfaceStatus(options = {}) {
7235
7530
  }));
7236
7531
  }
7237
7532
  async function uninstallAgentSurfaces(options) {
7238
- const homeDir = options.homeDir ?? homedir5();
7533
+ const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
7534
+ const { homeDir, projectDir, codexHome } = target;
7239
7535
  const name = options.name?.trim() || "topolo";
7240
7536
  resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
7241
7537
  const removed = [];
7242
- for (const agent of resolveTargets({ ...options, homeDir, project: options.project })) {
7243
- const before = await readAgentRegistrations(agent, homeDir, options.project);
7538
+ for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
7539
+ const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
7244
7540
  const registrationRemoved = before.some((entry) => entry.name === name);
7245
- await unregisterAgentConfig(agent, homeDir, name, options.project);
7246
- const remaining = await readAgentRegistrations(agent, homeDir, options.project);
7247
- const skillPath = agentSkillPath(agent, homeDir, options.project);
7541
+ await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
7542
+ const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
7543
+ const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
7248
7544
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
7249
7545
  if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
7250
7546
  removed.push({ agent, name, registrationRemoved, skillRemoved });
@@ -7277,7 +7573,7 @@ function formatStatus(rows) {
7277
7573
  // src/commands/doctor.ts
7278
7574
  import { access } from "fs/promises";
7279
7575
  import { homedir as homedir6 } from "os";
7280
- import { resolve as resolve3 } from "path";
7576
+ import { resolve as resolve4 } from "path";
7281
7577
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7282
7578
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7283
7579
  var defaultDependencies = {
@@ -7339,7 +7635,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
7339
7635
  }
7340
7636
  async function resolveInstalledDoctorTarget(options) {
7341
7637
  const name = options.name?.trim() || "topolo";
7342
- const homeDir = options.agentHome ? resolve3(options.agentHome) : homedir6();
7638
+ const homeDir = options.agentHome ? resolve4(options.agentHome) : homedir6();
7343
7639
  const expected = resolveMcpCommand();
7344
7640
  const requestedAgents = resolveTargets({
7345
7641
  ...options,