@topolo/cli 0.13.1 → 0.13.3

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/commands/agent-packs.ts
4
4
  import { spawnSync } from "child_process";
5
- import { existsSync } from "fs";
5
+ import { existsSync, readFileSync } from "fs";
6
6
  import { dirname, resolve } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { homedir } from "os";
@@ -10,7 +10,7 @@ function registerAgentPacksCommand(program2) {
10
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
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
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");
13
+ const { configureClaudePack } = await import("./claude-pack-backend-5DWMIDRA.js");
14
14
  const result = await configureClaudePack({
15
15
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
16
16
  pack,
@@ -34,6 +34,8 @@ function registerAgentPacksCommand(program2) {
34
34
  "recover",
35
35
  "doctor",
36
36
  "policy",
37
+ "permissions",
38
+ "approvals",
37
39
  "remote-recover",
38
40
  "connect",
39
41
  "remote-context",
@@ -44,12 +46,38 @@ function registerAgentPacksCommand(program2) {
44
46
  for (const name of commands) {
45
47
  const command = packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`);
46
48
  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");
49
+ if (name === "approvals") command.option("--intake <id>", "Exact owning native intake identity").option("--fingerprint <hash>", "Exact pending native request fingerprint").option("--response <path>", "Recorded one-request response JSON").option("--human-decision-recorded", "Assert that the exact human decision is recorded in the intake");
47
50
  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
51
  if (!["codex", "claude"].includes(options.harness)) throw new Error(`The ${options.harness} pack adapter is not available. Supported adapters: codex, claude.`);
52
+ if (name === "approvals" && options.harness === "claude" && !options.adapterHelp) {
53
+ if (!["list", "details", "respond"].includes(args[0] ?? "") || args.length > 2)
54
+ throw new Error("Approvals requires list, details KEY, or respond KEY.");
55
+ if (args[0] !== "list" && (!args[1] || !options.intake || !options.fingerprint))
56
+ throw new Error("Exact request key, owning intake and fingerprint required.");
57
+ let decision;
58
+ if (args[0] === "respond") {
59
+ if (!options.response || !options.humanDecisionRecorded) throw new Error("Recorded human response required.");
60
+ const response = JSON.parse(readFileSync(options.response, "utf8"));
61
+ if (Object.keys(response).join(",") !== "decision" || !["once", "deny"].includes(response.decision))
62
+ throw new Error("Only a one-request once/deny response is supported.");
63
+ decision = response.decision;
64
+ }
65
+ const { operateClaudeApprovals } = await import("./claude-pack-backend-5DWMIDRA.js");
66
+ console.log(JSON.stringify(await operateClaudeApprovals({
67
+ store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
68
+ kind: args[0],
69
+ key: args[1],
70
+ intake: options.intake,
71
+ fingerprint: options.fingerprint,
72
+ decision,
73
+ humanDecisionRecorded: options.humanDecisionRecorded
74
+ }), null, 2));
75
+ return;
76
+ }
49
77
  if (options.policy || options.endpoint || options.pid || options.apply) {
50
78
  if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
51
79
  throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
52
- const { configureClaudePack } = await import("./claude-pack-backend-IDQQX3DG.js");
80
+ const { configureClaudePack } = await import("./claude-pack-backend-5DWMIDRA.js");
53
81
  const result2 = await configureClaudePack({
54
82
  store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
55
83
  pack: args[0],
@@ -67,6 +95,12 @@ function registerAgentPacksCommand(program2) {
67
95
  if (!adapter) throw new Error("This CLI installation is missing its bundled Topolo Agent Pack adapter. Reinstall @topolo/cli.");
68
96
  const store = packs.opts().store;
69
97
  const forwarded = (options.adapterHelp ? ["--help"] : args).map((value) => value === "--group" ? "--set" : value);
98
+ if (name === "approvals" && !options.adapterHelp) {
99
+ if (options.intake) forwarded.push("--intake", options.intake);
100
+ if (options.fingerprint) forwarded.push("--fingerprint", options.fingerprint);
101
+ if (options.response) forwarded.push("--response", options.response);
102
+ if (options.humanDecisionRecorded) forwarded.push("--human-decision-recorded");
103
+ }
70
104
  const result = spawnSync("python3", [
71
105
  adapter,
72
106
  ...store ? ["--store", store] : [],
@@ -90,7 +124,7 @@ import { platform as platform2 } from "os";
90
124
  import { createInterface } from "readline/promises";
91
125
  import { stdin as input } from "process";
92
126
 
93
- // node_modules/.pnpm/@topolo+sdk@0.23.17/node_modules/@topolo/sdk/dist/index.js
127
+ // node_modules/@topolo/sdk/dist/index.js
94
128
  function record(value) {
95
129
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
96
130
  }
@@ -3187,9 +3221,18 @@ var TopoloOAuth = class {
3187
3221
  if (!res.ok) {
3188
3222
  if (parsed && typeof parsed === "object" && "error" in parsed) {
3189
3223
  const err = parsed;
3190
- throw new TopoloAuthError(err.error_description || err.error, err.error);
3224
+ throw new TopoloAuthError(err.error_description || err.error, err.error, {
3225
+ status: res.status,
3226
+ service: "auth",
3227
+ path,
3228
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
3229
+ retryAfter: res.headers.get("retry-after") ?? void 0
3230
+ });
3191
3231
  }
3192
- throw new TopoloHttpError("auth", path, res.status, parsed);
3232
+ throw new TopoloHttpError("auth", path, res.status, parsed, void 0, {
3233
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
3234
+ retryAfter: res.headers.get("retry-after") ?? void 0
3235
+ });
3193
3236
  }
3194
3237
  return parsed;
3195
3238
  }
@@ -3691,9 +3734,10 @@ async function pollUntilApproved(params) {
3691
3734
  throw new Error("Device code expired. Run `topolo auth login` again.");
3692
3735
  }
3693
3736
 
3694
- // node_modules/.pnpm/@topolo+sdk@0.23.17/node_modules/@topolo/sdk/dist/node/credential-store.js
3695
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3696
- import { existsSync as existsSync2 } from "fs";
3737
+ // node_modules/@topolo/sdk/dist/node/credential-store.js
3738
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
3739
+ import { existsSync as existsSync2, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync } from "fs";
3740
+ import { randomUUID as randomUUID2 } from "crypto";
3697
3741
  import { homedir as homedir2, platform } from "os";
3698
3742
  import { dirname as dirname2, join as join3 } from "path";
3699
3743
  import { createHash, randomUUID } from "crypto";
@@ -4101,9 +4145,18 @@ var TopoloOAuth2 = class {
4101
4145
  if (!res.ok) {
4102
4146
  if (parsed && typeof parsed === "object" && "error" in parsed) {
4103
4147
  const err = parsed;
4104
- throw new TopoloAuthError2(err.error_description || err.error, err.error);
4148
+ throw new TopoloAuthError2(err.error_description || err.error, err.error, {
4149
+ status: res.status,
4150
+ service: "auth",
4151
+ path,
4152
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
4153
+ retryAfter: res.headers.get("retry-after") ?? void 0
4154
+ });
4105
4155
  }
4106
- throw new TopoloHttpError2("auth", path, res.status, parsed);
4156
+ throw new TopoloHttpError2("auth", path, res.status, parsed, void 0, {
4157
+ requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
4158
+ retryAfter: res.headers.get("retry-after") ?? void 0
4159
+ });
4107
4160
  }
4108
4161
  return parsed;
4109
4162
  }
@@ -4550,39 +4603,150 @@ async function resolveCredential(environment = selectedEnvironment()) {
4550
4603
  var REFRESH_SKEW_SECONDS = 60;
4551
4604
  var CONFIG_LOCK_RETRY_MS = 25;
4552
4605
  var CONFIG_LOCK_TIMEOUT_MS = 5e3;
4553
- var CONFIG_LOCK_STALE_MS = 12e4;
4606
+ var ConfigOwnershipError = class extends TopoloSdkError2 {
4607
+ constructor() {
4608
+ super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
4609
+ }
4610
+ };
4554
4611
  async function withConfigWriteLock(operation) {
4555
4612
  const lockPath = `${configPath()}.lock`;
4556
4613
  await mkdir2(dirname2(lockPath), { recursive: true });
4557
- const startedAt = Date.now();
4614
+ const startedAt = performance.now();
4558
4615
  while (true) {
4559
4616
  try {
4560
- await mkdir2(lockPath);
4617
+ await mkdir2(lockPath, { mode: 448 });
4561
4618
  break;
4562
4619
  } catch (error) {
4563
4620
  if (error.code !== "EEXIST") throw error;
4564
- try {
4565
- const lock = await stat(lockPath);
4566
- if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
4567
- await rm2(lockPath, { recursive: true, force: true });
4568
- continue;
4569
- }
4570
- } catch (inspectionError) {
4571
- if (inspectionError.code !== "ENOENT") throw inspectionError;
4572
- continue;
4573
- }
4574
- if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4575
- throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
4621
+ if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
4622
+ throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
4576
4623
  }
4577
4624
  await new Promise((resolve4) => setTimeout(resolve4, CONFIG_LOCK_RETRY_MS));
4578
4625
  }
4579
4626
  }
4627
+ const directory = await lstat(lockPath);
4628
+ const identity = randomUUID2();
4629
+ const markerName = "owner-" + identity + ".json";
4630
+ const markerPath = join3(lockPath, markerName);
4631
+ const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
4632
+ await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
4633
+ const assertDirectory = async () => {
4634
+ const current = await lstat(lockPath);
4635
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
4636
+ throw new ConfigOwnershipError();
4637
+ };
4638
+ const assertOwned = async () => {
4639
+ try {
4640
+ await assertDirectory();
4641
+ const entries = await readdir2(lockPath);
4642
+ if (entries.length !== 1 || entries[0] !== markerName) throw new ConfigOwnershipError();
4643
+ const entry = await lstat(markerPath);
4644
+ if (!entry.isFile() || entry.isSymbolicLink() || await readFile2(markerPath, "utf8") !== marker)
4645
+ throw new ConfigOwnershipError();
4646
+ } catch {
4647
+ throw new ConfigOwnershipError();
4648
+ }
4649
+ };
4650
+ const assertDirectorySync = () => {
4651
+ const current = lstatSync(lockPath);
4652
+ if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
4653
+ throw new ConfigOwnershipError();
4654
+ };
4655
+ const assertOwnedSync = () => {
4656
+ assertDirectorySync();
4657
+ const entries = readdirSync(lockPath);
4658
+ const entry = lstatSync(markerPath);
4659
+ if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
4660
+ };
4661
+ const persist = async (update) => {
4662
+ const path = configPath();
4663
+ await mkdir2(dirname2(path), { recursive: true });
4664
+ const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
4665
+ let temporaryOwned = false;
4666
+ try {
4667
+ assertOwnedSync();
4668
+ const entry = lstatSync(path);
4669
+ if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
4670
+ const before = readFileSync2(path, "utf8");
4671
+ const updated = update(JSON.parse(before));
4672
+ const fd = openSync(temporary, "wx", 384);
4673
+ temporaryOwned = true;
4674
+ try {
4675
+ writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
4676
+ fsyncSync(fd);
4677
+ } finally {
4678
+ closeSync(fd);
4679
+ }
4680
+ assertOwnedSync();
4681
+ if (readFileSync2(path, "utf8") !== before) throw new ConfigOwnershipError();
4682
+ renameSync(temporary, path);
4683
+ temporaryOwned = false;
4684
+ } finally {
4685
+ if (temporaryOwned) unlinkSync(temporary);
4686
+ }
4687
+ };
4688
+ const release = async () => {
4689
+ await assertOwned();
4690
+ const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
4691
+ await mkdir2(quarantine, { mode: 448 });
4692
+ const captured = join3(quarantine, markerName);
4693
+ try {
4694
+ await assertOwned();
4695
+ await rename2(markerPath, captured);
4696
+ assertDirectorySync();
4697
+ const entry = lstatSync(captured);
4698
+ if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
4699
+ unlinkSync(captured);
4700
+ rmdirSync(quarantine);
4701
+ assertDirectorySync();
4702
+ if (readdirSync(lockPath).length) throw new ConfigOwnershipError();
4703
+ rmdirSync(lockPath);
4704
+ } catch {
4705
+ if (existsSync2(captured)) {
4706
+ try {
4707
+ await link(captured, markerPath);
4708
+ } catch {
4709
+ }
4710
+ }
4711
+ throw new ConfigOwnershipError();
4712
+ }
4713
+ };
4580
4714
  try {
4581
- return await operation();
4715
+ await assertOwned();
4716
+ const result = await operation(assertOwned, persist);
4717
+ await assertOwned();
4718
+ return result;
4582
4719
  } finally {
4583
- await rm2(lockPath, { recursive: true, force: true });
4720
+ await release();
4584
4721
  }
4585
4722
  }
4723
+ function renewalFailure(error) {
4724
+ const path = "/api/developer-oauth/token";
4725
+ if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
4726
+ if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
4727
+ return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
4728
+ status: error.status,
4729
+ service: "auth",
4730
+ path,
4731
+ requestId: error.requestId
4732
+ });
4733
+ }
4734
+ if (error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) {
4735
+ return new TopoloHttpError2(
4736
+ "auth",
4737
+ path,
4738
+ error.status,
4739
+ void 0,
4740
+ "Topolo sign-in renewal failed. Retry the request; the saved sign-in has been retained.",
4741
+ { requestId: error.requestId, retryAfter: error.retryAfter }
4742
+ );
4743
+ }
4744
+ const timeout = error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
4745
+ return new TopoloSdkError2(
4746
+ timeout ? "request_timeout" : "network_error",
4747
+ "Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
4748
+ );
4749
+ }
4586
4750
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
4587
4751
  const prefix = envVarPrefix(environment);
4588
4752
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
@@ -4591,16 +4755,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
4591
4755
  const observedProfile = profileForEnvironment(await readConfig(), environment);
4592
4756
  const observed = observedProfile.credential;
4593
4757
  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;
4594
- return withConfigWriteLock(async () => {
4758
+ return withConfigWriteLock(async (_assertOwned, persist) => {
4595
4759
  const config = await readConfig();
4596
4760
  const profile = profileForEnvironment(config, environment);
4597
4761
  const cred = profile.credential;
4598
4762
  if (!cred || cred.kind !== "access_token") return cred ?? null;
4599
4763
  if (!cred.refreshToken || !profile.oauthClientId) return cred;
4600
- if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken) {
4764
+ const now = Math.floor(Date.now() / 1e3);
4765
+ if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken && cred.expiresAt !== void 0 && cred.expiresAt > now) {
4601
4766
  return cred;
4602
4767
  }
4603
- const now = Math.floor(Date.now() / 1e3);
4604
4768
  if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
4605
4769
  const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
4606
4770
  if (environment === "local" && !serviceUrls?.auth) {
@@ -4623,23 +4787,27 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
4623
4787
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
4624
4788
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
4625
4789
  };
4626
- const activeContext = profile.activeContext;
4627
- const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
4628
- const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
4629
- credential: refreshed,
4630
- contexts: {
4631
- ...profile.contexts ?? {},
4632
- [activeContext]: {
4790
+ await persist((current) => {
4791
+ const latest = profileForEnvironment(current, environment);
4792
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
4793
+ if (!same(latest.credential, cred) || latest.oauthClientId !== profile.oauthClientId || latest.activeContext !== profile.activeContext)
4794
+ throw new ConfigOwnershipError();
4795
+ const activeContext = latest.activeContext;
4796
+ const activeEntry = activeContext ? latest.contexts?.[activeContext] : void 0;
4797
+ const previousEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
4798
+ if (!same(activeEntry?.credential, previousEntry?.credential)) throw new ConfigOwnershipError();
4799
+ return activeContext && activeEntry ? writeProfileConfig(current, environment, {
4800
+ credential: refreshed,
4801
+ contexts: { ...latest.contexts, [activeContext]: {
4633
4802
  ...activeEntry,
4634
4803
  credential: refreshed,
4635
- oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
4636
- }
4637
- }
4638
- }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
4639
- await writeConfig(updated);
4804
+ oauthClientId: activeEntry.oauthClientId ?? latest.oauthClientId
4805
+ } }
4806
+ }) : environment === "production" && !current.profiles?.production ? { ...current, credential: refreshed } : writeProfileConfig(current, environment, { credential: refreshed });
4807
+ });
4640
4808
  return refreshed;
4641
- } catch {
4642
- return cred;
4809
+ } catch (error) {
4810
+ throw renewalFailure(error);
4643
4811
  }
4644
4812
  });
4645
4813
  }
@@ -4648,7 +4816,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4648
4816
  const observedContext = observedProfile.contexts?.[key];
4649
4817
  const observed = observedContext?.credential;
4650
4818
  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;
4651
- return withConfigWriteLock(async () => {
4819
+ return withConfigWriteLock(async (_assertOwned, persist) => {
4652
4820
  const config = await readConfig();
4653
4821
  const profile = profileForEnvironment(config, environment);
4654
4822
  const context = profile.contexts?.[key];
@@ -4657,6 +4825,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4657
4825
  const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
4658
4826
  if (!credential.refreshToken || !oauthClientId) return credential;
4659
4827
  const now = Math.floor(Date.now() / 1e3);
4828
+ if (options.force && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return credential;
4660
4829
  if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
4661
4830
  const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
4662
4831
  if (environment === "local" && !serviceUrls?.auth) {
@@ -4677,16 +4846,21 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
4677
4846
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
4678
4847
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
4679
4848
  };
4680
- await writeConfig(writeProfileConfig(config, environment, {
4681
- ...profile.activeContext === key ? { credential: refreshed } : {},
4682
- contexts: {
4683
- ...profile.contexts ?? {},
4684
- [key]: { ...context, credential: refreshed, oauthClientId }
4685
- }
4686
- }));
4849
+ await persist((current) => {
4850
+ const latest = profileForEnvironment(current, environment);
4851
+ const latestContext = latest.contexts?.[key];
4852
+ if (!latestContext || JSON.stringify(latestContext.credential) !== JSON.stringify(credential) || (latestContext.oauthClientId ?? latest.oauthClientId) !== oauthClientId || latestContext.contextType !== context.contextType || latestContext.organizationId !== context.organizationId)
4853
+ throw new ConfigOwnershipError();
4854
+ if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
4855
+ throw new ConfigOwnershipError();
4856
+ return writeProfileConfig(current, environment, {
4857
+ ...latest.activeContext === key ? { credential: refreshed } : {},
4858
+ contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
4859
+ });
4860
+ });
4687
4861
  return refreshed;
4688
- } catch {
4689
- return credential;
4862
+ } catch (error) {
4863
+ throw renewalFailure(error);
4690
4864
  }
4691
4865
  });
4692
4866
  }
@@ -4845,7 +5019,7 @@ function normalizeError(err) {
4845
5019
  }
4846
5020
 
4847
5021
  // src/version.ts
4848
- import { readFileSync } from "fs";
5022
+ import { readFileSync as readFileSync3 } from "fs";
4849
5023
  import { dirname as dirname4, join as join4 } from "path";
4850
5024
  import { fileURLToPath as fileURLToPath2 } from "url";
4851
5025
  function readCliVersion() {
@@ -4857,7 +5031,7 @@ function readCliVersion() {
4857
5031
  ];
4858
5032
  for (const path of candidates) {
4859
5033
  try {
4860
- const pkg = JSON.parse(readFileSync(path, "utf8"));
5034
+ const pkg = JSON.parse(readFileSync3(path, "utf8"));
4861
5035
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
4862
5036
  } catch {
4863
5037
  }
@@ -5193,19 +5367,19 @@ function formatPretty(info) {
5193
5367
 
5194
5368
  // src/commands/actions.ts
5195
5369
  import { spawn as spawn2 } from "child_process";
5196
- import { readFileSync as readFileSync3 } from "fs";
5370
+ import { readFileSync as readFileSync5 } from "fs";
5197
5371
  import { platform as platform3 } from "os";
5198
5372
  import { basename, extname } from "path";
5199
5373
 
5200
5374
  // src/json-input.ts
5201
- import { readFileSync as readFileSync2 } from "fs";
5375
+ import { readFileSync as readFileSync4 } from "fs";
5202
5376
  function parseJsonInput(value) {
5203
5377
  let source = value;
5204
5378
  if (value.startsWith("@")) {
5205
5379
  const path = value.slice(1);
5206
5380
  if (!path) throw new Error("--data @file path must not be empty");
5207
5381
  try {
5208
- source = readFileSync2(path, "utf8");
5382
+ source = readFileSync4(path, "utf8");
5209
5383
  } catch (err) {
5210
5384
  throw new Error(`Could not read --data file ${path}: ${err.message}`);
5211
5385
  }
@@ -5402,7 +5576,7 @@ Approval required. ${opened ? "Opening" : "Open"} ${approval.verificationUrl}` :
5402
5576
  const resource = parseResourceOption(opts.resourceType, opts.resourceId);
5403
5577
  const result = await topolo.client.uploadAction(
5404
5578
  action,
5405
- readFileSync3(opts.file),
5579
+ readFileSync5(opts.file),
5406
5580
  parseJsonObject(opts.data || "{}"),
5407
5581
  {
5408
5582
  fileName: basename(opts.file),
@@ -5974,7 +6148,7 @@ function normalizeList(values, fallback = []) {
5974
6148
  }
5975
6149
 
5976
6150
  // src/commands/integrations.ts
5977
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
6151
+ import { readdir as readdir3, readFile as readFile3, stat } from "fs/promises";
5978
6152
  import { join as join5, relative } from "path";
5979
6153
  function registerIntegrationsCommand(program2) {
5980
6154
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
@@ -6128,7 +6302,7 @@ async function collectFiles(repoRoot) {
6128
6302
  const root = repoRoot;
6129
6303
  async function walk(dir) {
6130
6304
  if (result.length > 2e3) return;
6131
- const entries = await readdir2(dir, { withFileTypes: true });
6305
+ const entries = await readdir3(dir, { withFileTypes: true });
6132
6306
  for (const entry of entries) {
6133
6307
  const fullPath = join5(dir, entry.name);
6134
6308
  if (entry.isDirectory()) {
@@ -6137,7 +6311,7 @@ async function collectFiles(repoRoot) {
6137
6311
  continue;
6138
6312
  }
6139
6313
  if (!entry.isFile() || !isInspectable(entry.name)) continue;
6140
- const info = await stat2(fullPath);
6314
+ const info = await stat(fullPath);
6141
6315
  if (info.size > 25e4) continue;
6142
6316
  result.push({
6143
6317
  path: relative(root, fullPath),
@@ -6159,7 +6333,7 @@ function formatCheckResult(result) {
6159
6333
  // src/commands/skills.ts
6160
6334
  import { spawnSync as spawnSync4 } from "child_process";
6161
6335
  import { existsSync as existsSync6 } from "fs";
6162
- import { readFile as readFile6, rm as rm3 } from "fs/promises";
6336
+ import { readFile as readFile6, rm as rm2 } from "fs/promises";
6163
6337
  import { homedir as homedir5 } from "os";
6164
6338
  import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
6165
6339
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -6620,7 +6794,7 @@ function message(error) {
6620
6794
 
6621
6795
  // src/commands/agent-config.ts
6622
6796
  import { existsSync as existsSync5 } from "fs";
6623
- import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
6797
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
6624
6798
  import { dirname as dirname5 } from "path";
6625
6799
  async function installAgentSkill(agent, source, homeDir, projectDir) {
6626
6800
  const path = agentSkillPath(agent, homeDir, projectDir);
@@ -6787,7 +6961,7 @@ async function atomicWrite(path, contents) {
6787
6961
  await mkdir3(dirname5(path), { recursive: true });
6788
6962
  const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
6789
6963
  await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
6790
- await rename2(temporaryPath, path);
6964
+ await rename3(temporaryPath, path);
6791
6965
  }
6792
6966
  function withoutMcpServerSections(text2, name) {
6793
6967
  const section = "[mcp_servers." + name + "]";
@@ -7025,7 +7199,7 @@ async function uninstallAgentSurfaces(options) {
7025
7199
  const remaining = await readAgentRegistrations(agent, homeDir, options.project);
7026
7200
  const skillPath = agentSkillPath(agent, homeDir, options.project);
7027
7201
  const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
7028
- if (skillRemoved && skillPath) await rm3(dirname6(skillPath), { recursive: true, force: true });
7202
+ if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
7029
7203
  removed.push({ agent, name, registrationRemoved, skillRemoved });
7030
7204
  }
7031
7205
  return removed;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
3
  "packageManager": "pnpm@10.17.1",
4
- "version": "0.13.1",
5
- "topoloPublishedVersion": "0.13.1",
4
+ "version": "0.13.3",
5
+ "topoloPublishedVersion": "0.13.3",
6
6
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
7
7
  "license": "UNLICENSED",
8
8
  "author": "Topolo",
@@ -27,12 +27,12 @@
27
27
  "test:watch": "vitest",
28
28
  "coverage": "pnpm run coverage:node && pnpm run coverage:adapter",
29
29
  "coverage:node": "vitest run --coverage",
30
- "coverage:adapter": "python3 -m venv node_modules/.cache/agent-pack-test && node_modules/.cache/agent-pack-test/bin/python -m pip install --disable-pip-version-check --quiet --timeout 15 --retries 0 -r agent-packs/requirements-test.txt && PYTHONPATH=agent-packs/src COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage run --branch --source=topolo_agent_pack.topolo,topolo_agent_pack.claude,topolo_agent_pack.policy -m unittest discover -s agent-packs/tests && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report --fail-under=100"
30
+ "coverage:adapter": "python3 -m venv node_modules/.cache/agent-pack-test && node_modules/.cache/agent-pack-test/bin/python -m pip install --disable-pip-version-check --quiet --timeout 15 --retries 0 -r agent-packs/requirements-test.txt && PYTHONPATH=agent-packs/src:agent-packs COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage run --branch --source=topolo_agent_pack.topolo,topolo_agent_pack.claude,topolo_agent_pack.policy,topolo_agent_pack.permissions,topolo_agent_pack.approvals,topolo_agent_pack.execution,topolo_agent_pack.live,topolo_agent_pack.cli,coverage_changed -m unittest discover -s agent-packs/tests && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report --include='*/topolo_agent_pack/topolo.py,*/topolo_agent_pack/claude.py,*/topolo_agent_pack/policy.py,*/topolo_agent_pack/permissions.py,*/topolo_agent_pack/approvals.py,*/topolo_agent_pack/execution.py,*/agent-packs/coverage_changed.py' --fail-under=100 && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage json -o node_modules/.cache/agent-pack-test/coverage.json && node_modules/.cache/agent-pack-test/bin/python agent-packs/coverage_changed.py"
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.0.0",
34
- "@topolo/mcp": "0.14.22",
35
- "@topolo/sdk": "0.23.17",
34
+ "@topolo/mcp": "0.14.23",
35
+ "@topolo/sdk": "0.23.18",
36
36
  "commander": "^12.1.0",
37
37
  "undici": "6.29.0"
38
38
  },