@topolo/cli 0.13.0 → 0.13.2

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
@@ -5,8 +5,22 @@ import { spawnSync } from "child_process";
5
5
  import { existsSync } from "fs";
6
6
  import { dirname, resolve } from "path";
7
7
  import { fileURLToPath } from "url";
8
+ import { homedir } from "os";
8
9
  function registerAgentPacksCommand(program2) {
9
10
  const packs = program2.command("agent-packs").description("Install and operate Topolo Agent Packs in supported local hosts").option("--store <path>", "Portable pack library (default ~/AgentPacks)");
11
+ 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) => {
12
+ if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
13
+ const { configureClaudePack } = await import("./claude-pack-backend-IDQQX3DG.js");
14
+ const result = await configureClaudePack({
15
+ store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
16
+ pack,
17
+ policy: options.policy,
18
+ endpoint: options.endpoint,
19
+ pid: Number(options.pid),
20
+ apply: options.apply === true
21
+ });
22
+ console.log(JSON.stringify(result, null, 2));
23
+ });
10
24
  const commands = [
11
25
  "add",
12
26
  "replace",
@@ -19,6 +33,7 @@ function registerAgentPacksCommand(program2) {
19
33
  "list",
20
34
  "recover",
21
35
  "doctor",
36
+ "policy",
22
37
  "remote-recover",
23
38
  "connect",
24
39
  "remote-context",
@@ -27,8 +42,26 @@ function registerAgentPacksCommand(program2) {
27
42
  "artifact-get"
28
43
  ];
29
44
  for (const name of commands) {
30
- packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`).option("--harness <name>", "Local host adapter (currently codex)", "codex").option("--adapter-help", "Show every option supported by the selected adapter").argument("[arguments...]").allowUnknownOption().allowExcessArguments().addHelpText("after", name === "add" ? "\nExamples:\n topolo agent-packs add founder\n topolo agent-packs add founder --group specialists\n topolo agent-packs add founder --agent strategy\n" : "\nRun topolo agent-packs <command> --adapter-help for all adapter options. Cloud commands require explicit --environment and --context-key.\n").action((args, options) => {
31
- if (options.harness !== "codex") throw new Error(`The ${options.harness} pack adapter is not available. Only Codex switching has been verified.`);
45
+ const command = packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`);
46
+ if (name === "switch" || name === "recover") command.option("--policy <path>", "Reviewed policy for an adopted Claude roster").option("--endpoint <url>", "Explicit authorized Claude main-process inspector endpoint").option("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply a native switch; recovery is always read-only");
47
+ command.option("--harness <name>", "Local host adapter (codex, claude Desktop Code sessions)", "codex").option("--adapter-help", "Show every option supported by the selected adapter").argument("[arguments...]").allowUnknownOption().allowExcessArguments().addHelpText("after", name === "add" ? "\nExamples:\n topolo agent-packs add founder\n topolo agent-packs add founder --group specialists\n topolo agent-packs add founder --agent strategy\n" : "\nRun topolo agent-packs <command> --adapter-help for all adapter options. Cloud commands require explicit --environment and --context-key.\n").action(async (args, options) => {
48
+ if (!["codex", "claude"].includes(options.harness)) throw new Error(`The ${options.harness} pack adapter is not available. Supported adapters: codex, claude.`);
49
+ if (options.policy || options.endpoint || options.pid || options.apply) {
50
+ if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
51
+ throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
52
+ const { configureClaudePack } = await import("./claude-pack-backend-IDQQX3DG.js");
53
+ const result2 = await configureClaudePack({
54
+ store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
55
+ pack: args[0],
56
+ policy: options.policy,
57
+ endpoint: options.endpoint,
58
+ pid: Number(options.pid),
59
+ mode: name,
60
+ apply: options.apply === true
61
+ });
62
+ console.log(JSON.stringify(result2, null, 2));
63
+ return;
64
+ }
32
65
  const here = dirname(fileURLToPath(import.meta.url));
33
66
  const adapter = [resolve(here, "../agent-packs/topolo-agent-pack"), resolve(here, "../../agent-packs/topolo-agent-pack")].find(existsSync);
34
67
  if (!adapter) throw new Error("This CLI installation is missing its bundled Topolo Agent Pack adapter. Reinstall @topolo/cli.");
@@ -37,6 +70,7 @@ function registerAgentPacksCommand(program2) {
37
70
  const result = spawnSync("python3", [
38
71
  adapter,
39
72
  ...store ? ["--store", store] : [],
73
+ ...options.harness === "claude" ? ["--harness", "claude"] : [],
40
74
  name,
41
75
  ...forwarded,
42
76
  ...name === "add" && !options.adapterHelp ? ["--catalog"] : []
@@ -56,7 +90,7 @@ import { platform as platform2 } from "os";
56
90
  import { createInterface } from "readline/promises";
57
91
  import { stdin as input } from "process";
58
92
 
59
- // node_modules/.pnpm/@topolo+sdk@0.23.17/node_modules/@topolo/sdk/dist/index.js
93
+ // node_modules/@topolo/sdk/dist/index.js
60
94
  function record(value) {
61
95
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
62
96
  }
@@ -3153,9 +3187,18 @@ var TopoloOAuth = class {
3153
3187
  if (!res.ok) {
3154
3188
  if (parsed && typeof parsed === "object" && "error" in parsed) {
3155
3189
  const err = parsed;
3156
- throw new TopoloAuthError(err.error_description || err.error, err.error);
3190
+ throw new TopoloAuthError(err.error_description || err.error, err.error, {
3191
+ status: res.status,
3192
+ service: "auth",
3193
+ path,
3194
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
3195
+ retryAfter: res.headers.get("retry-after") ?? void 0
3196
+ });
3157
3197
  }
3158
- throw new TopoloHttpError("auth", path, res.status, parsed);
3198
+ throw new TopoloHttpError("auth", path, res.status, parsed, void 0, {
3199
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
3200
+ retryAfter: res.headers.get("retry-after") ?? void 0
3201
+ });
3159
3202
  }
3160
3203
  return parsed;
3161
3204
  }
@@ -3657,9 +3700,10 @@ async function pollUntilApproved(params) {
3657
3700
  throw new Error("Device code expired. Run `topolo auth login` again.");
3658
3701
  }
3659
3702
 
3660
- // node_modules/.pnpm/@topolo+sdk@0.23.17/node_modules/@topolo/sdk/dist/node/credential-store.js
3661
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3662
- import { existsSync as existsSync2 } from "fs";
3703
+ // node_modules/@topolo/sdk/dist/node/credential-store.js
3704
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
3705
+ import { existsSync as existsSync2, lstatSync, readdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync } from "fs";
3706
+ import { randomUUID as randomUUID2 } from "crypto";
3663
3707
  import { homedir as homedir2, platform } from "os";
3664
3708
  import { dirname as dirname2, join as join3 } from "path";
3665
3709
  import { createHash, randomUUID } from "crypto";
@@ -3667,7 +3711,7 @@ import { join as join2 } from "path";
3667
3711
  import { spawnSync as spawnSync2 } from "child_process";
3668
3712
  import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
3669
3713
  import { existsSync as existsSync3 } from "fs";
3670
- import { homedir, platform as hostPlatform } from "os";
3714
+ import { homedir as homedir3, platform as hostPlatform } from "os";
3671
3715
  import { dirname as dirname3, join } from "path";
3672
3716
  var TopoloSdkError2 = class extends Error {
3673
3717
  code;
@@ -4067,9 +4111,18 @@ var TopoloOAuth2 = class {
4067
4111
  if (!res.ok) {
4068
4112
  if (parsed && typeof parsed === "object" && "error" in parsed) {
4069
4113
  const err = parsed;
4070
- throw new TopoloAuthError2(err.error_description || err.error, err.error);
4114
+ throw new TopoloAuthError2(err.error_description || err.error, err.error, {
4115
+ status: res.status,
4116
+ service: "auth",
4117
+ path,
4118
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
4119
+ retryAfter: res.headers.get("retry-after") ?? void 0
4120
+ });
4071
4121
  }
4072
- throw new TopoloHttpError2("auth", path, res.status, parsed);
4122
+ throw new TopoloHttpError2("auth", path, res.status, parsed, void 0, {
4123
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
4124
+ retryAfter: res.headers.get("retry-after") ?? void 0
4125
+ });
4073
4126
  }
4074
4127
  return parsed;
4075
4128
  }
@@ -4097,7 +4150,7 @@ var MACOS_KEYCHAIN_WRITE_SCRIPT = [
4097
4150
  function installationStorePath() {
4098
4151
  const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
4099
4152
  if (override) return override;
4100
- const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
4153
+ const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir3(), "AppData", "Roaming") : join(homedir3(), ".config"));
4101
4154
  return join(base, "topolo", "installations");
4102
4155
  }
4103
4156
  function installationPath(environment, id) {
@@ -4516,38 +4569,149 @@ async function resolveCredential(environment = selectedEnvironment()) {
4516
4569
  var REFRESH_SKEW_SECONDS = 60;
4517
4570
  var CONFIG_LOCK_RETRY_MS = 25;
4518
4571
  var CONFIG_LOCK_TIMEOUT_MS = 5e3;
4519
- var CONFIG_LOCK_STALE_MS = 12e4;
4572
+ var ConfigOwnershipError = class extends TopoloSdkError2 {
4573
+ constructor() {
4574
+ super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
4575
+ }
4576
+ };
4520
4577
  async function withConfigWriteLock(operation) {
4521
4578
  const lockPath = `${configPath()}.lock`;
4522
4579
  await mkdir2(dirname2(lockPath), { recursive: true });
4523
- const startedAt = Date.now();
4580
+ const startedAt = performance.now();
4524
4581
  while (true) {
4525
4582
  try {
4526
- await mkdir2(lockPath);
4583
+ await mkdir2(lockPath, { mode: 448 });
4527
4584
  break;
4528
4585
  } catch (error) {
4529
4586
  if (error.code !== "EEXIST") throw error;
4530
- try {
4531
- const lock = await stat(lockPath);
4532
- if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
4533
- await rm2(lockPath, { recursive: true, force: true });
4534
- continue;
4535
- }
4536
- } catch (inspectionError) {
4537
- if (inspectionError.code !== "ENOENT") throw inspectionError;
4538
- continue;
4539
- }
4540
- if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4541
- throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
4587
+ if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4588
+ throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
4542
4589
  }
4543
4590
  await new Promise((resolve4) => setTimeout(resolve4, CONFIG_LOCK_RETRY_MS));
4544
4591
  }
4545
4592
  }
4593
+ const directory = await lstat(lockPath);
4594
+ const identity = randomUUID2();
4595
+ const markerName = "owner-" + identity + ".json";
4596
+ const markerPath = join3(lockPath, markerName);
4597
+ const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4598
+ await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
4599
+ const assertDirectory = async () => {
4600
+ const current = await lstat(lockPath);
4601
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
4602
+ throw new ConfigOwnershipError();
4603
+ };
4604
+ const assertOwned = async () => {
4605
+ try {
4606
+ await assertDirectory();
4607
+ const entries = await readdir2(lockPath);
4608
+ if (entries.length !== 1 || entries[0] !== markerName) throw new ConfigOwnershipError();
4609
+ const entry = await lstat(markerPath);
4610
+ if (!entry.isFile() || entry.isSymbolicLink() || await readFile2(markerPath, "utf8") !== marker)
4611
+ throw new ConfigOwnershipError();
4612
+ } catch {
4613
+ throw new ConfigOwnershipError();
4614
+ }
4615
+ };
4616
+ const assertDirectorySync = () => {
4617
+ const current = lstatSync(lockPath);
4618
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
4619
+ throw new ConfigOwnershipError();
4620
+ };
4621
+ const assertOwnedSync = () => {
4622
+ assertDirectorySync();
4623
+ const entries = readdirSync(lockPath);
4624
+ const entry = lstatSync(markerPath);
4625
+ if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4626
+ };
4627
+ const persist = async (update) => {
4628
+ const path = configPath();
4629
+ await mkdir2(dirname2(path), { recursive: true });
4630
+ const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
4631
+ let temporaryOwned = false;
4632
+ try {
4633
+ assertOwnedSync();
4634
+ const entry = lstatSync(path);
4635
+ if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
4636
+ const before = readFileSync(path, "utf8");
4637
+ const updated = update(JSON.parse(before));
4638
+ const fd = openSync(temporary, "wx", 384);
4639
+ temporaryOwned = true;
4640
+ try {
4641
+ writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
4642
+ fsyncSync(fd);
4643
+ } finally {
4644
+ closeSync(fd);
4645
+ }
4646
+ assertOwnedSync();
4647
+ if (readFileSync(path, "utf8") !== before) throw new ConfigOwnershipError();
4648
+ renameSync(temporary, path);
4649
+ temporaryOwned = false;
4650
+ } finally {
4651
+ if (temporaryOwned) unlinkSync(temporary);
4652
+ }
4653
+ };
4654
+ const release = async () => {
4655
+ await assertOwned();
4656
+ const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
4657
+ await mkdir2(quarantine, { mode: 448 });
4658
+ const captured = join3(quarantine, markerName);
4659
+ try {
4660
+ await assertOwned();
4661
+ await rename2(markerPath, captured);
4662
+ assertDirectorySync();
4663
+ const entry = lstatSync(captured);
4664
+ if (!entry.isFile() || entry.isSymbolicLink() || readFileSync(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
4665
+ unlinkSync(captured);
4666
+ rmdirSync(quarantine);
4667
+ assertDirectorySync();
4668
+ if (readdirSync(lockPath).length) throw new ConfigOwnershipError();
4669
+ rmdirSync(lockPath);
4670
+ } catch {
4671
+ if (existsSync2(captured)) {
4672
+ try {
4673
+ await link(captured, markerPath);
4674
+ } catch {
4675
+ }
4676
+ }
4677
+ throw new ConfigOwnershipError();
4678
+ }
4679
+ };
4546
4680
  try {
4547
- return await operation();
4681
+ await assertOwned();
4682
+ const result = await operation(assertOwned, persist);
4683
+ await assertOwned();
4684
+ return result;
4548
4685
  } finally {
4549
- await rm2(lockPath, { recursive: true, force: true });
4686
+ await release();
4687
+ }
4688
+ }
4689
+ function renewalFailure(error) {
4690
+ const path = "/api/developer-oauth/token";
4691
+ if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
4692
+ if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
4693
+ return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
4694
+ status: error.status,
4695
+ service: "auth",
4696
+ path,
4697
+ requestId: error.requestId
4698
+ });
4699
+ }
4700
+ if (error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) {
4701
+ return new TopoloHttpError2(
4702
+ "auth",
4703
+ path,
4704
+ error.status,
4705
+ void 0,
4706
+ "Topolo sign-in renewal failed. Retry the request; the saved sign-in has been retained.",
4707
+ { requestId: error.requestId, retryAfter: error.retryAfter }
4708
+ );
4550
4709
  }
4710
+ const timeout = error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
4711
+ return new TopoloSdkError2(
4712
+ timeout ? "request_timeout" : "network_error",
4713
+ "Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
4714
+ );
4551
4715
  }
4552
4716
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
4553
4717
  const prefix = envVarPrefix(environment);
@@ -4557,16 +4721,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
4557
4721
  const observedProfile = profileForEnvironment(await readConfig(), environment);
4558
4722
  const observed = observedProfile.credential;
4559
4723
  if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !observedProfile.oauthClientId || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
4560
- return withConfigWriteLock(async () => {
4724
+ return withConfigWriteLock(async (_assertOwned, persist) => {
4561
4725
  const config = await readConfig();
4562
4726
  const profile = profileForEnvironment(config, environment);
4563
4727
  const cred = profile.credential;
4564
4728
  if (!cred || cred.kind !== "access_token") return cred ?? null;
4565
4729
  if (!cred.refreshToken || !profile.oauthClientId) return cred;
4566
- if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken) {
4730
+ const now = Math.floor(Date.now() / 1e3);
4731
+ if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken && cred.expiresAt !== void 0 && cred.expiresAt > now) {
4567
4732
  return cred;
4568
4733
  }
4569
- const now = Math.floor(Date.now() / 1e3);
4570
4734
  if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
4571
4735
  const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
4572
4736
  if (environment === "local" && !serviceUrls?.auth) {
@@ -4589,23 +4753,27 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
4589
4753
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
4590
4754
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
4591
4755
  };
4592
- const activeContext = profile.activeContext;
4593
- const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
4594
- const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
4595
- credential: refreshed,
4596
- contexts: {
4597
- ...profile.contexts ?? {},
4598
- [activeContext]: {
4756
+ await persist((current) => {
4757
+ const latest = profileForEnvironment(current, environment);
4758
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
4759
+ if (!same(latest.credential, cred) || latest.oauthClientId !== profile.oauthClientId || latest.activeContext !== profile.activeContext)
4760
+ throw new ConfigOwnershipError();
4761
+ const activeContext = latest.activeContext;
4762
+ const activeEntry = activeContext ? latest.contexts?.[activeContext] : void 0;
4763
+ const previousEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
4764
+ if (!same(activeEntry?.credential, previousEntry?.credential)) throw new ConfigOwnershipError();
4765
+ return activeContext && activeEntry ? writeProfileConfig(current, environment, {
4766
+ credential: refreshed,
4767
+ contexts: { ...latest.contexts, [activeContext]: {
4599
4768
  ...activeEntry,
4600
4769
  credential: refreshed,
4601
- oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
4602
- }
4603
- }
4604
- }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
4605
- await writeConfig(updated);
4770
+ oauthClientId: activeEntry.oauthClientId ?? latest.oauthClientId
4771
+ } }
4772
+ }) : environment === "production" && !current.profiles?.production ? { ...current, credential: refreshed } : writeProfileConfig(current, environment, { credential: refreshed });
4773
+ });
4606
4774
  return refreshed;
4607
- } catch {
4608
- return cred;
4775
+ } catch (error) {
4776
+ throw renewalFailure(error);
4609
4777
  }
4610
4778
  });
4611
4779
  }
@@ -4614,7 +4782,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4614
4782
  const observedContext = observedProfile.contexts?.[key];
4615
4783
  const observed = observedContext?.credential;
4616
4784
  if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !(observedContext.oauthClientId ?? observedProfile.oauthClientId) || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
4617
- return withConfigWriteLock(async () => {
4785
+ return withConfigWriteLock(async (_assertOwned, persist) => {
4618
4786
  const config = await readConfig();
4619
4787
  const profile = profileForEnvironment(config, environment);
4620
4788
  const context = profile.contexts?.[key];
@@ -4623,6 +4791,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4623
4791
  const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
4624
4792
  if (!credential.refreshToken || !oauthClientId) return credential;
4625
4793
  const now = Math.floor(Date.now() / 1e3);
4794
+ if (options.force && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return credential;
4626
4795
  if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
4627
4796
  const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
4628
4797
  if (environment === "local" && !serviceUrls?.auth) {
@@ -4643,16 +4812,21 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4643
4812
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
4644
4813
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
4645
4814
  };
4646
- await writeConfig(writeProfileConfig(config, environment, {
4647
- ...profile.activeContext === key ? { credential: refreshed } : {},
4648
- contexts: {
4649
- ...profile.contexts ?? {},
4650
- [key]: { ...context, credential: refreshed, oauthClientId }
4651
- }
4652
- }));
4815
+ await persist((current) => {
4816
+ const latest = profileForEnvironment(current, environment);
4817
+ const latestContext = latest.contexts?.[key];
4818
+ if (!latestContext || JSON.stringify(latestContext.credential) !== JSON.stringify(credential) || (latestContext.oauthClientId ?? latest.oauthClientId) !== oauthClientId || latestContext.contextType !== context.contextType || latestContext.organizationId !== context.organizationId)
4819
+ throw new ConfigOwnershipError();
4820
+ if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
4821
+ throw new ConfigOwnershipError();
4822
+ return writeProfileConfig(current, environment, {
4823
+ ...latest.activeContext === key ? { credential: refreshed } : {},
4824
+ contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
4825
+ });
4826
+ });
4653
4827
  return refreshed;
4654
- } catch {
4655
- return credential;
4828
+ } catch (error) {
4829
+ throw renewalFailure(error);
4656
4830
  }
4657
4831
  });
4658
4832
  }
@@ -4811,7 +4985,7 @@ function normalizeError(err) {
4811
4985
  }
4812
4986
 
4813
4987
  // src/version.ts
4814
- import { readFileSync } from "fs";
4988
+ import { readFileSync as readFileSync2 } from "fs";
4815
4989
  import { dirname as dirname4, join as join4 } from "path";
4816
4990
  import { fileURLToPath as fileURLToPath2 } from "url";
4817
4991
  function readCliVersion() {
@@ -4823,7 +4997,7 @@ function readCliVersion() {
4823
4997
  ];
4824
4998
  for (const path of candidates) {
4825
4999
  try {
4826
- const pkg = JSON.parse(readFileSync(path, "utf8"));
5000
+ const pkg = JSON.parse(readFileSync2(path, "utf8"));
4827
5001
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
4828
5002
  } catch {
4829
5003
  }
@@ -5159,19 +5333,19 @@ function formatPretty(info) {
5159
5333
 
5160
5334
  // src/commands/actions.ts
5161
5335
  import { spawn as spawn2 } from "child_process";
5162
- import { readFileSync as readFileSync3 } from "fs";
5336
+ import { readFileSync as readFileSync4 } from "fs";
5163
5337
  import { platform as platform3 } from "os";
5164
5338
  import { basename, extname } from "path";
5165
5339
 
5166
5340
  // src/json-input.ts
5167
- import { readFileSync as readFileSync2 } from "fs";
5341
+ import { readFileSync as readFileSync3 } from "fs";
5168
5342
  function parseJsonInput(value) {
5169
5343
  let source = value;
5170
5344
  if (value.startsWith("@")) {
5171
5345
  const path = value.slice(1);
5172
5346
  if (!path) throw new Error("--data @file path must not be empty");
5173
5347
  try {
5174
- source = readFileSync2(path, "utf8");
5348
+ source = readFileSync3(path, "utf8");
5175
5349
  } catch (err) {
5176
5350
  throw new Error(`Could not read --data file ${path}: ${err.message}`);
5177
5351
  }
@@ -5368,7 +5542,7 @@ Approval required. ${opened ? "Opening" : "Open"} ${approval.verificationUrl}` :
5368
5542
  const resource = parseResourceOption(opts.resourceType, opts.resourceId);
5369
5543
  const result = await topolo.client.uploadAction(
5370
5544
  action,
5371
- readFileSync3(opts.file),
5545
+ readFileSync4(opts.file),
5372
5546
  parseJsonObject(opts.data || "{}"),
5373
5547
  {
5374
5548
  fileName: basename(opts.file),
@@ -5940,7 +6114,7 @@ function normalizeList(values, fallback = []) {
5940
6114
  }
5941
6115
 
5942
6116
  // src/commands/integrations.ts
5943
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
6117
+ import { readdir as readdir3, readFile as readFile3, stat } from "fs/promises";
5944
6118
  import { join as join5, relative } from "path";
5945
6119
  function registerIntegrationsCommand(program2) {
5946
6120
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
@@ -6094,7 +6268,7 @@ async function collectFiles(repoRoot) {
6094
6268
  const root = repoRoot;
6095
6269
  async function walk(dir) {
6096
6270
  if (result.length > 2e3) return;
6097
- const entries = await readdir2(dir, { withFileTypes: true });
6271
+ const entries = await readdir3(dir, { withFileTypes: true });
6098
6272
  for (const entry of entries) {
6099
6273
  const fullPath = join5(dir, entry.name);
6100
6274
  if (entry.isDirectory()) {
@@ -6103,7 +6277,7 @@ async function collectFiles(repoRoot) {
6103
6277
  continue;
6104
6278
  }
6105
6279
  if (!entry.isFile() || !isInspectable(entry.name)) continue;
6106
- const info = await stat2(fullPath);
6280
+ const info = await stat(fullPath);
6107
6281
  if (info.size > 25e4) continue;
6108
6282
  result.push({
6109
6283
  path: relative(root, fullPath),
@@ -6125,8 +6299,8 @@ function formatCheckResult(result) {
6125
6299
  // src/commands/skills.ts
6126
6300
  import { spawnSync as spawnSync4 } from "child_process";
6127
6301
  import { existsSync as existsSync6 } from "fs";
6128
- import { readFile as readFile6, rm as rm3 } from "fs/promises";
6129
- import { homedir as homedir4 } from "os";
6302
+ import { readFile as readFile6, rm as rm2 } from "fs/promises";
6303
+ import { homedir as homedir5 } from "os";
6130
6304
  import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
6131
6305
  import { fileURLToPath as fileURLToPath3 } from "url";
6132
6306
  import { createRequire } from "module";
@@ -6135,7 +6309,7 @@ import { createRequire } from "module";
6135
6309
  import { spawnSync as spawnSync3 } from "child_process";
6136
6310
  import { existsSync as existsSync4 } from "fs";
6137
6311
  import { readFile as readFile4 } from "fs/promises";
6138
- import { homedir as homedir3 } from "os";
6312
+ import { homedir as homedir4 } from "os";
6139
6313
  import { isAbsolute, join as join6 } from "path";
6140
6314
  var SUPPORTED_AGENT_IDS = [
6141
6315
  "claude",
@@ -6191,7 +6365,7 @@ function stableNodeCommand() {
6191
6365
  });
6192
6366
  return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
6193
6367
  }
6194
- function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
6368
+ function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
6195
6369
  switch (agent) {
6196
6370
  case "claude":
6197
6371
  return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
@@ -6216,7 +6390,7 @@ function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
6216
6390
  return void 0;
6217
6391
  }
6218
6392
  }
6219
- function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
6393
+ function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
6220
6394
  switch (agent) {
6221
6395
  case "claude":
6222
6396
  return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
@@ -6308,7 +6482,7 @@ function agentCliNames(agent) {
6308
6482
  return ["qchat", "q"];
6309
6483
  }
6310
6484
  }
6311
- function agentConfigExists(agent, homeDir = homedir3(), projectDir) {
6485
+ function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
6312
6486
  return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
6313
6487
  }
6314
6488
  function claudeDesktopConfigPath(homeDir) {
@@ -6321,7 +6495,7 @@ function vscodeUserConfigPath(homeDir) {
6321
6495
  if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
6322
6496
  return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
6323
6497
  }
6324
- async function readAgentRegistrations(agent, homeDir = homedir3(), projectDir) {
6498
+ async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
6325
6499
  const registrations = [];
6326
6500
  for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
6327
6501
  if (!existsSync4(config.path)) continue;
@@ -6586,7 +6760,7 @@ function message(error) {
6586
6760
 
6587
6761
  // src/commands/agent-config.ts
6588
6762
  import { existsSync as existsSync5 } from "fs";
6589
- import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
6763
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
6590
6764
  import { dirname as dirname5 } from "path";
6591
6765
  async function installAgentSkill(agent, source, homeDir, projectDir) {
6592
6766
  const path = agentSkillPath(agent, homeDir, projectDir);
@@ -6753,7 +6927,7 @@ async function atomicWrite(path, contents) {
6753
6927
  await mkdir3(dirname5(path), { recursive: true });
6754
6928
  const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
6755
6929
  await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
6756
- await rename2(temporaryPath, path);
6930
+ await rename3(temporaryPath, path);
6757
6931
  }
6758
6932
  function withoutMcpServerSections(text2, name) {
6759
6933
  const section = "[mcp_servers." + name + "]";
@@ -6859,8 +7033,8 @@ function resolveTargets(opts) {
6859
7033
  }))];
6860
7034
  }
6861
7035
  if (opts.all) return [...SUPPORTED_AGENT_IDS];
6862
- const homeDir = opts.homeDir ?? homedir4();
6863
- const detectBinaries = homeDir === homedir4();
7036
+ const homeDir = opts.homeDir ?? homedir5();
7037
+ const detectBinaries = homeDir === homedir5();
6864
7038
  const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
6865
7039
  return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
6866
7040
  }
@@ -6921,7 +7095,7 @@ async function installAgentSurfaces(opts) {
6921
7095
  }
6922
7096
  const mcp = resolveMcpCommand();
6923
7097
  const registration = resolveMcpRegistration(opts);
6924
- const homeDir = opts.homeDir ?? homedir4();
7098
+ const homeDir = opts.homeDir ?? homedir5();
6925
7099
  const results = [];
6926
7100
  for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
6927
7101
  results.push(await installForAgent(agent, sourceDir, mcp, {
@@ -6956,7 +7130,7 @@ function assertAgentSetupSucceeded(results) {
6956
7130
  }
6957
7131
  }
6958
7132
  async function agentSurfaceStatus(options = {}) {
6959
- const homeDir = options.homeDir ?? homedir4();
7133
+ const homeDir = options.homeDir ?? homedir5();
6960
7134
  const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
6961
7135
  const agents = resolveTargets({
6962
7136
  agent: options.agent,
@@ -6980,7 +7154,7 @@ async function agentSurfaceStatus(options = {}) {
6980
7154
  }));
6981
7155
  }
6982
7156
  async function uninstallAgentSurfaces(options) {
6983
- const homeDir = options.homeDir ?? homedir4();
7157
+ const homeDir = options.homeDir ?? homedir5();
6984
7158
  const name = options.name?.trim() || "topolo";
6985
7159
  resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
6986
7160
  const removed = [];
@@ -6991,7 +7165,7 @@ async function uninstallAgentSurfaces(options) {
6991
7165
  const remaining = await readAgentRegistrations(agent, homeDir, options.project);
6992
7166
  const skillPath = agentSkillPath(agent, homeDir, options.project);
6993
7167
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
6994
- if (skillRemoved && skillPath) await rm3(dirname6(skillPath), { recursive: true, force: true });
7168
+ if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
6995
7169
  removed.push({ agent, name, registrationRemoved, skillRemoved });
6996
7170
  }
6997
7171
  return removed;
@@ -7021,7 +7195,7 @@ function formatStatus(rows) {
7021
7195
 
7022
7196
  // src/commands/doctor.ts
7023
7197
  import { access } from "fs/promises";
7024
- import { homedir as homedir5 } from "os";
7198
+ import { homedir as homedir6 } from "os";
7025
7199
  import { resolve as resolve3 } from "path";
7026
7200
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7027
7201
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -7084,7 +7258,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
7084
7258
  }
7085
7259
  async function resolveInstalledDoctorTarget(options) {
7086
7260
  const name = options.name?.trim() || "topolo";
7087
- const homeDir = options.agentHome ? resolve3(options.agentHome) : homedir5();
7261
+ const homeDir = options.agentHome ? resolve3(options.agentHome) : homedir6();
7088
7262
  const expected = resolveMcpCommand();
7089
7263
  const requestedAgents = resolveTargets({
7090
7264
  ...options,