@topolo/cli 0.8.2 → 0.8.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/README.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # TopoloCli
2
2
 
3
3
  Public customer command-line interface for Topolo organizations, developers,
4
- and agents. First-party engineering, seed, scaffold, audit, provisioning,
5
- deployment, and release commands belong to the restricted `topolo-platform`
6
- toolchain and are deliberately absent.
4
+ and agents. Internal engineering and operations commands are deliberately
5
+ absent.
7
6
 
8
7
  ## Current scope
9
8
 
10
9
  - API-key authentication (long-lived, service-scoped).
11
10
  - OAuth device-flow sign-in with a **built-in first-party client** — `topolo auth login` needs no client id; any Topolo user can sign in.
12
11
  - Stored access-token refresh; the `@topolo/mcp` server reads the same credential, so agents need no API-key plumbing.
13
- - First-class `production`, `staging`, `development`, and `local` environments with separate stored credentials and service URL overrides.
12
+ - Production credentials by default, with isolated profiles for approved self-hosted environments.
14
13
  - **Per-context credentials** — hold and switch between your personal space and multiple organizations with `topolo context`.
15
14
  - **One-command agent setup** — `topolo setup` wires the bundled compatible skill + MCP into Claude Code / Codex.
16
15
  - **End-to-end diagnostics** — `topolo doctor` proves authentication and the MCP protocol path.
@@ -28,19 +27,12 @@ npx @topolo/cli whoami
28
27
 
29
28
  Credentials come from (in priority order):
30
29
 
31
- 1. Environment-specific vars such as `TOPOLO_STAGING_API_KEY` or `TOPOLO_PRODUCTION_ACCESS_TOKEN`
30
+ 1. Environment-specific credential variables
32
31
  2. Global vars `TOPOLO_API_KEY` or `TOPOLO_ACCESS_TOKEN`
33
32
  3. The selected environment profile in the config file (`$XDG_CONFIG_HOME/topolo/config.json`, mode `0600`)
34
33
 
35
- Production is the default. Select staging with `--env staging` anywhere in the command, or set `TOPOLO_ENV=staging`:
36
-
37
- ```bash
38
- topolo auth status --env staging --json
39
- topolo auth login --env staging
40
- topolo whoami --env staging --json
41
- ```
42
-
43
- The staging profile uses `auth.stg.topolo.us`, `developers.stg.topolo.us`, and `seed.stg.topolo.us` by default. Other SDK service calls in staging must have an explicit staging URL override; the CLI refuses to call a production SDK URL while `--env staging` is selected.
34
+ Production is the default customer environment. Use another environment only
35
+ when your Topolo administrator provides its complete service configuration.
44
36
 
45
37
  For interactive use:
46
38
 
@@ -83,12 +75,12 @@ key returned by `topolo context ls --json`:
83
75
 
84
76
  ```bash
85
77
  topolo setup --name topolo-acme --context org:org_acme --env production
86
- topolo doctor --context org:org_acme --env production
78
+ topolo doctor --name topolo-acme
87
79
  ```
88
80
 
89
81
  This creates separately named Claude Code and Codex registrations using the
90
- same exact nested MCP executable. `topolo-platform` is reserved for the
91
- restricted operator toolchain.
82
+ same exact nested MCP executable. Reserved operator registrations cannot be
83
+ created or removed by the public CLI.
92
84
 
93
85
  ## Cross-org isolation
94
86
 
@@ -225,8 +217,8 @@ topolo apps get mail --json
225
217
  ```
226
218
 
227
219
  Only live, credential-scoped applications are returned. The public CLI contains
228
- no first-party repository metadata or compiled fallback catalog. Internal app
229
- creation and conformance workflows use `topolo-platform apps`.
220
+ no first-party repository metadata or compiled fallback catalog. Internal
221
+ application lifecycle workflows are outside this customer CLI.
230
222
 
231
223
  ### `topolo oauth clients`
232
224
 
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // ../../../public-platform-tooling-boundary-20260813/packages/topolo-sdk/dist/index.js
12
+ // node_modules/@topolo/sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
@@ -30,8 +30,8 @@ function generateRequestId() {
30
30
  }
31
31
  var TopoloSdkError = class extends Error {
32
32
  code;
33
- constructor(code, message) {
34
- super(message);
33
+ constructor(code, message2) {
34
+ super(message2);
35
35
  this.name = "TopoloSdkError";
36
36
  this.code = code;
37
37
  }
@@ -128,8 +128,8 @@ var TopoloAuthError = class extends TopoloSdkError {
128
128
  requestId;
129
129
  retryAfter;
130
130
  details;
131
- constructor(message, code = "auth_error", options = {}) {
132
- super(canonicalTopoloErrorCode(code) ?? "auth_error", message);
131
+ constructor(message2, code = "auth_error", options = {}) {
132
+ super(canonicalTopoloErrorCode(code) ?? "auth_error", message2);
133
133
  this.name = "TopoloAuthError";
134
134
  this.status = options.status ?? 401;
135
135
  this.body = options.body;
@@ -151,10 +151,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
151
151
  requestId;
152
152
  retryAfter;
153
153
  details;
154
- constructor(message, required, options = {}) {
154
+ constructor(message2, required, options = {}) {
155
155
  super(
156
156
  canonicalTopoloErrorCode(options.code) ?? serviceErrorCode(options.body, "permission_denied"),
157
- message
157
+ message2
158
158
  );
159
159
  this.name = "TopoloPermissionError";
160
160
  this.required = canonicalTopoloRequired(required);
@@ -286,8 +286,8 @@ function topoloErrorHint(error) {
286
286
  return void 0;
287
287
  }
288
288
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
289
- constructor(message) {
290
- super("confirmation_required", message);
289
+ constructor(message2) {
290
+ super("confirmation_required", message2);
291
291
  this.name = "TopoloConfirmationRequiredError";
292
292
  }
293
293
  };
@@ -299,8 +299,8 @@ var TopoloHttpError = class extends TopoloSdkError {
299
299
  requestId;
300
300
  retryAfter;
301
301
  details;
302
- constructor(service, path, status, body, message, options = {}) {
303
- super(serviceErrorCode(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
302
+ constructor(service, path, status, body, message2, options = {}) {
303
+ super(serviceErrorCode(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
304
304
  this.name = "TopoloHttpError";
305
305
  this.status = status;
306
306
  this.body = body;
@@ -999,7 +999,7 @@ var TopoloClient = class _TopoloClient {
999
999
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
1000
1000
  if (!res.ok) {
1001
1001
  const durationMs = Date.now() - startedAt;
1002
- const message = describeError(parsed, `HTTP ${res.status}`);
1002
+ const message2 = describeError(parsed, `HTTP ${res.status}`);
1003
1003
  const publicCode = extractTopoloServiceErrorCode(parsed) ?? "http_error";
1004
1004
  this.emit({
1005
1005
  phase: "error",
@@ -1039,7 +1039,7 @@ var TopoloClient = class _TopoloClient {
1039
1039
  opts.path,
1040
1040
  res.status,
1041
1041
  parsed,
1042
- message,
1042
+ message2,
1043
1043
  errorContext
1044
1044
  );
1045
1045
  }
@@ -2253,15 +2253,15 @@ function createTopolo(options) {
2253
2253
  };
2254
2254
  }
2255
2255
 
2256
- // ../../../public-platform-tooling-boundary-20260813/packages/topolo-sdk/dist/node/credential-store.js
2257
- import { mkdir, readFile, writeFile, chmod } from "fs/promises";
2256
+ // node_modules/@topolo/sdk/dist/node/credential-store.js
2257
+ import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
2258
2258
  import { existsSync } from "fs";
2259
2259
  import { homedir, platform } from "os";
2260
2260
  import { dirname, join } from "path";
2261
2261
  var TopoloSdkError2 = class extends Error {
2262
2262
  code;
2263
- constructor(code, message) {
2264
- super(message);
2263
+ constructor(code, message2) {
2264
+ super(message2);
2265
2265
  this.name = "TopoloSdkError";
2266
2266
  this.code = code;
2267
2267
  }
@@ -2358,8 +2358,8 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
2358
2358
  requestId;
2359
2359
  retryAfter;
2360
2360
  details;
2361
- constructor(message, code = "auth_error", options = {}) {
2362
- super(canonicalTopoloErrorCode2(code) ?? "auth_error", message);
2361
+ constructor(message2, code = "auth_error", options = {}) {
2362
+ super(canonicalTopoloErrorCode2(code) ?? "auth_error", message2);
2363
2363
  this.name = "TopoloAuthError";
2364
2364
  this.status = options.status ?? 401;
2365
2365
  this.body = options.body;
@@ -2451,8 +2451,8 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
2451
2451
  requestId;
2452
2452
  retryAfter;
2453
2453
  details;
2454
- constructor(service, path, status, body, message, options = {}) {
2455
- super(serviceErrorCode2(body, "http_error"), message ?? `HTTP ${status} from ${service}${path}`);
2454
+ constructor(service, path, status, body, message2, options = {}) {
2455
+ super(serviceErrorCode2(body, "http_error"), message2 ?? `HTTP ${status} from ${service}${path}`);
2456
2456
  this.name = "TopoloHttpError";
2457
2457
  this.status = status;
2458
2458
  this.body = body;
@@ -2755,6 +2755,41 @@ async function resolveCredential(environment = selectedEnvironment()) {
2755
2755
  return profileForEnvironment(config, environment).credential ?? null;
2756
2756
  }
2757
2757
  var REFRESH_SKEW_SECONDS = 60;
2758
+ var CONFIG_LOCK_RETRY_MS = 25;
2759
+ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
2760
+ var CONFIG_LOCK_STALE_MS = 12e4;
2761
+ async function withConfigWriteLock(operation) {
2762
+ const lockPath = `${configPath()}.lock`;
2763
+ await mkdir(dirname(lockPath), { recursive: true });
2764
+ const startedAt = Date.now();
2765
+ while (true) {
2766
+ try {
2767
+ await mkdir(lockPath);
2768
+ break;
2769
+ } catch (error) {
2770
+ if (error.code !== "EEXIST") throw error;
2771
+ try {
2772
+ const lock = await stat(lockPath);
2773
+ if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
2774
+ await rm(lockPath, { recursive: true, force: true });
2775
+ continue;
2776
+ }
2777
+ } catch (inspectionError) {
2778
+ if (inspectionError.code !== "ENOENT") throw inspectionError;
2779
+ continue;
2780
+ }
2781
+ if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
2782
+ throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
2783
+ }
2784
+ await new Promise((resolve3) => setTimeout(resolve3, CONFIG_LOCK_RETRY_MS));
2785
+ }
2786
+ }
2787
+ try {
2788
+ return await operation();
2789
+ } finally {
2790
+ await rm(lockPath, { recursive: true, force: true });
2791
+ }
2792
+ }
2758
2793
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
2759
2794
  const prefix = envVarPrefix(environment);
2760
2795
  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) {
@@ -2808,51 +2843,47 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
2808
2843
  }
2809
2844
  }
2810
2845
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
2811
- const config = await readConfig();
2812
- const profile = profileForEnvironment(config, environment);
2813
- const context = profile.contexts?.[key];
2814
- const credential = context?.credential;
2815
- if (!credential || credential.kind !== "access_token") return credential ?? null;
2816
- const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
2817
- if (!credential.refreshToken || !oauthClientId) return credential;
2818
- const now = Math.floor(Date.now() / 1e3);
2819
- if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
2820
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
2821
- if (environment === "local" && !serviceUrls?.auth) {
2822
- throw new TopoloSdkError2(
2823
- "environment_service_url_required",
2824
- "Local Topolo context refresh requires an explicit local Auth service origin."
2825
- );
2826
- }
2827
- try {
2828
- const oauth = new TopoloOAuth2({
2829
- serviceUrls
2830
- });
2831
- const tokens = await oauth.refreshToken({
2832
- clientId: oauthClientId,
2833
- refreshToken: credential.refreshToken
2834
- });
2835
- const refreshed = {
2836
- kind: "access_token",
2837
- accessToken: tokens.access_token,
2838
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
2839
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
2840
- };
2841
- await writeConfig(writeProfileConfig(config, environment, {
2842
- ...profile.activeContext === key ? { credential: refreshed } : {},
2843
- contexts: {
2844
- ...profile.contexts ?? {},
2845
- [key]: {
2846
- ...context,
2847
- credential: refreshed,
2848
- oauthClientId
2846
+ return withConfigWriteLock(async () => {
2847
+ const config = await readConfig();
2848
+ const profile = profileForEnvironment(config, environment);
2849
+ const context = profile.contexts?.[key];
2850
+ const credential = context?.credential;
2851
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
2852
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
2853
+ if (!credential.refreshToken || !oauthClientId) return credential;
2854
+ const now = Math.floor(Date.now() / 1e3);
2855
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
2856
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
2857
+ if (environment === "local" && !serviceUrls?.auth) {
2858
+ throw new TopoloSdkError2(
2859
+ "environment_service_url_required",
2860
+ "Local Topolo context refresh requires an explicit local Auth service origin."
2861
+ );
2862
+ }
2863
+ try {
2864
+ const oauth = new TopoloOAuth2({ serviceUrls });
2865
+ const tokens = await oauth.refreshToken({
2866
+ clientId: oauthClientId,
2867
+ refreshToken: credential.refreshToken
2868
+ });
2869
+ const refreshed = {
2870
+ kind: "access_token",
2871
+ accessToken: tokens.access_token,
2872
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
2873
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
2874
+ };
2875
+ await writeConfig(writeProfileConfig(config, environment, {
2876
+ ...profile.activeContext === key ? { credential: refreshed } : {},
2877
+ contexts: {
2878
+ ...profile.contexts ?? {},
2879
+ [key]: { ...context, credential: refreshed, oauthClientId }
2849
2880
  }
2850
- }
2851
- }));
2852
- return refreshed;
2853
- } catch {
2854
- return credential;
2855
- }
2881
+ }));
2882
+ return refreshed;
2883
+ } catch {
2884
+ return credential;
2885
+ }
2886
+ });
2856
2887
  }
2857
2888
  function resolveAgentName(configAgentName) {
2858
2889
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
@@ -2906,9 +2937,6 @@ function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
2906
2937
  return `${developerConsoleUrl(environment)}/credentials`;
2907
2938
  }
2908
2939
  var DEFAULT_CLI_CLIENT_IDS = {
2909
- // First-party `topolo-cli` device-flow client, seeded per environment by Auth
2910
- // (core/TopoloAuth/scripts/seed-first-party-oauth-clients.sql). Each env's
2911
- // Auth must have this client before the CLI ships pointing at it.
2912
2940
  production: "topolo-cli",
2913
2941
  staging: "topolo-cli",
2914
2942
  development: "topolo-cli"
@@ -3270,13 +3298,13 @@ async function pollUntilApproved(params) {
3270
3298
  throw new Error("Device code expired. Run `topolo auth login` again.");
3271
3299
  }
3272
3300
  function sleep(ms) {
3273
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3301
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3274
3302
  }
3275
3303
 
3276
3304
  // src/client.ts
3277
3305
  async function buildClient(options) {
3278
- const environment = selectedEnvironment();
3279
- const credential = await refreshCredentialIfNeeded(environment);
3306
+ const environment = options?.environment ?? selectedEnvironment();
3307
+ const credential = options?.context ? await refreshContextCredentialIfNeeded(environment, options.context) : await refreshCredentialIfNeeded(environment);
3280
3308
  if (!credential) return null;
3281
3309
  const config = await readConfig();
3282
3310
  const profile = resolvedProfileConfig(config, environment);
@@ -3290,7 +3318,9 @@ async function buildClient(options) {
3290
3318
  },
3291
3319
  serviceUrls: profile.serviceUrls,
3292
3320
  requireConfirmForWrites: options?.requireConfirmForWrites,
3293
- ...credential.kind === "access_token" && credential.refreshToken ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {},
3321
+ ...credential.kind === "access_token" && credential.refreshToken ? {
3322
+ refreshCredential: () => options?.context ? refreshContextCredentialIfNeeded(environment, options.context, { force: true }) : refreshCredentialIfNeeded(environment, { force: true })
3323
+ } : {},
3294
3324
  ...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
3295
3325
  });
3296
3326
  }
@@ -4081,7 +4111,7 @@ function normalizeList(values, fallback = []) {
4081
4111
  }
4082
4112
 
4083
4113
  // src/commands/integrations.ts
4084
- import { readdir, readFile as readFile2, stat } from "fs/promises";
4114
+ import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
4085
4115
  import { join as join3, relative } from "path";
4086
4116
  function registerIntegrationsCommand(program2) {
4087
4117
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
@@ -4244,7 +4274,7 @@ async function collectFiles(repoRoot) {
4244
4274
  continue;
4245
4275
  }
4246
4276
  if (!entry.isFile() || !isInspectable(entry.name)) continue;
4247
- const info = await stat(fullPath);
4277
+ const info = await stat2(fullPath);
4248
4278
  if (info.size > 25e4) continue;
4249
4279
  result.push({
4250
4280
  path: relative(root, fullPath),
@@ -4264,46 +4294,135 @@ function formatCheckResult(result) {
4264
4294
  }
4265
4295
 
4266
4296
  // src/commands/skills.ts
4297
+ import { spawnSync as spawnSync2 } from "child_process";
4298
+ import { existsSync as existsSync3 } from "fs";
4299
+ import { mkdir as mkdir2, copyFile, readFile as readFile4, writeFile as writeFile2, rm as rm2, rename } from "fs/promises";
4300
+ import { homedir as homedir3 } from "os";
4301
+ import { dirname as dirname3, join as join5, resolve } from "path";
4302
+ import { fileURLToPath as fileURLToPath2 } from "url";
4303
+ import { createRequire } from "module";
4304
+
4305
+ // src/commands/agent-host.ts
4267
4306
  import { spawnSync } from "child_process";
4268
4307
  import { existsSync as existsSync2 } from "fs";
4269
- import { mkdir as mkdir2, copyFile, readFile as readFile3, writeFile as writeFile2, rm, rename } from "fs/promises";
4308
+ import { readFile as readFile3 } from "fs/promises";
4270
4309
  import { homedir as homedir2 } from "os";
4271
- import { dirname as dirname3, join as join4, resolve } from "path";
4272
- import { fileURLToPath as fileURLToPath2 } from "url";
4273
- import { createRequire } from "module";
4310
+ import { isAbsolute, join as join4 } from "path";
4311
+ var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
4312
+ function isPublicRegistrationName(name) {
4313
+ return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
4314
+ }
4315
+ function selectStableNodeCommand(whichOutput, fallback = process.execPath) {
4316
+ const candidate = whichOutput?.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
4317
+ return candidate && isAbsolute(candidate) ? candidate : fallback;
4318
+ }
4319
+ function stableNodeCommand() {
4320
+ const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["node"], {
4321
+ encoding: "utf8",
4322
+ stdio: ["ignore", "pipe", "ignore"]
4323
+ });
4324
+ return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
4325
+ }
4326
+ function agentSkillPath(agent, homeDir = homedir2()) {
4327
+ return agent === "claude" ? join4(homeDir, ".claude", "skills", "topolo", "SKILL.md") : join4(homeDir, ".codex", "skills", "topolo", "SKILL.md");
4328
+ }
4329
+ async function readAgentRegistrations(agent, homeDir = homedir2()) {
4330
+ return agent === "claude" ? readClaudeRegistrations(homeDir) : readCodexRegistrations(homeDir);
4331
+ }
4332
+ async function readClaudeRegistrations(homeDir) {
4333
+ const path = join4(homeDir, ".claude.json");
4334
+ if (!existsSync2(path)) return [];
4335
+ let parsed;
4336
+ try {
4337
+ parsed = JSON.parse(await readFile3(path, "utf8"));
4338
+ } catch (error) {
4339
+ throw new Error(`Cannot read malformed Claude configuration at ${path}: ${message(error)}`);
4340
+ }
4341
+ const servers = parsed.mcpServers;
4342
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
4343
+ return Object.entries(servers).filter(([name]) => isPublicRegistrationName(name)).flatMap(([name, value]) => normalizeRegistration(name, value));
4344
+ }
4345
+ async function readCodexRegistrations(homeDir) {
4346
+ const path = join4(homeDir, ".codex", "config.toml");
4347
+ if (!existsSync2(path)) return [];
4348
+ const registrations = /* @__PURE__ */ new Map();
4349
+ let section;
4350
+ for (const rawLine of (await readFile3(path, "utf8")).split(/\r?\n/)) {
4351
+ const line = rawLine.trim();
4352
+ const heading = line.match(/^\[mcp_servers\.([A-Za-z0-9_-]+)(\.env)?\]$/);
4353
+ if (heading) {
4354
+ section = { name: heading[1], env: Boolean(heading[2]) };
4355
+ if (isPublicRegistrationName(section.name) && !registrations.has(section.name)) {
4356
+ registrations.set(section.name, { name: section.name, command: "", args: [], env: {} });
4357
+ }
4358
+ continue;
4359
+ }
4360
+ if (line.startsWith("[")) {
4361
+ section = void 0;
4362
+ continue;
4363
+ }
4364
+ if (!section || !isPublicRegistrationName(section.name) || !line || line.startsWith("#")) continue;
4365
+ const assignment = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
4366
+ if (!assignment) continue;
4367
+ const registration = registrations.get(section.name);
4368
+ try {
4369
+ const value = JSON.parse(assignment[2]);
4370
+ if (section.env && typeof value === "string") registration.env[assignment[1]] = value;
4371
+ if (!section.env && assignment[1] === "command" && typeof value === "string") registration.command = value;
4372
+ if (!section.env && assignment[1] === "args" && Array.isArray(value) && value.every((item) => typeof item === "string")) {
4373
+ registration.args = value;
4374
+ }
4375
+ } catch {
4376
+ throw new Error(`Cannot read unsupported MCP value in Codex configuration at ${path}.`);
4377
+ }
4378
+ }
4379
+ return [...registrations.values()].filter((registration) => registration.command);
4380
+ }
4381
+ function normalizeRegistration(name, value) {
4382
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
4383
+ const candidate = value;
4384
+ if (typeof candidate.command !== "string") return [];
4385
+ const args = Array.isArray(candidate.args) && candidate.args.every((arg) => typeof arg === "string") ? candidate.args : [];
4386
+ const env = candidate.env && typeof candidate.env === "object" && !Array.isArray(candidate.env) ? Object.fromEntries(Object.entries(candidate.env).filter((entry) => typeof entry[1] === "string")) : {};
4387
+ return [{ name, command: candidate.command, args, env }];
4388
+ }
4389
+ function message(error) {
4390
+ return error instanceof Error ? error.message : String(error);
4391
+ }
4392
+
4393
+ // src/commands/skills.ts
4274
4394
  var AGENT_LABELS = { claude: "Claude Code", codex: "Codex" };
4275
4395
  var AGENT_NAME_FOR = { claude: "claude-code", codex: "codex" };
4276
- var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
4277
4396
  function skillsSourceDir() {
4278
4397
  const here = dirname3(fileURLToPath2(import.meta.url));
4279
4398
  const distCandidate = resolve(here, "../skills");
4280
4399
  const srcCandidate = resolve(here, "../../skills");
4281
4400
  for (const candidate of [distCandidate, srcCandidate]) {
4282
- if (existsSync2(join4(candidate, "topolo", "SKILL.md"))) return candidate;
4401
+ if (existsSync3(join5(candidate, "topolo", "SKILL.md"))) return candidate;
4283
4402
  }
4284
4403
  return distCandidate;
4285
4404
  }
4286
4405
  function resolveMcpCommand() {
4287
4406
  const require2 = createRequire(import.meta.url);
4288
- return { command: process.execPath, args: [require2.resolve("@topolo/mcp/stdio")] };
4407
+ return { command: stableNodeCommand(), args: [require2.resolve("@topolo/mcp/stdio")] };
4289
4408
  }
4290
4409
  function agentBinaryExists(bin) {
4291
- const probe = spawnSync(process.platform === "win32" ? "where" : "which", [bin], {
4410
+ const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
4292
4411
  stdio: "ignore"
4293
4412
  });
4294
4413
  return probe.status === 0;
4295
4414
  }
4296
- function claudeSkillDir(projectDir, homeDir = homedir2()) {
4297
- const base = projectDir ? resolve(projectDir, ".claude") : join4(homeDir, ".claude");
4298
- return join4(base, "skills", "topolo");
4415
+ function claudeSkillDir(projectDir, homeDir = homedir3()) {
4416
+ const base = projectDir ? resolve(projectDir, ".claude") : join5(homeDir, ".claude");
4417
+ return join5(base, "skills", "topolo");
4299
4418
  }
4300
- function codexSkillDir(homeDir = homedir2()) {
4301
- return join4(homeDir, ".codex", "skills", "topolo");
4419
+ function codexSkillDir(homeDir = homedir3()) {
4420
+ return join5(homeDir, ".codex", "skills", "topolo");
4302
4421
  }
4303
4422
  async function installSkillFile(sourceDir, targetDir) {
4304
4423
  await mkdir2(targetDir, { recursive: true });
4305
- const target = join4(targetDir, "SKILL.md");
4306
- await copyFile(join4(sourceDir, "topolo", "SKILL.md"), target);
4424
+ const target = join5(targetDir, "SKILL.md");
4425
+ await copyFile(join5(sourceDir, "topolo", "SKILL.md"), target);
4307
4426
  return target;
4308
4427
  }
4309
4428
  async function atomicWrite(path, contents) {
@@ -4327,11 +4446,11 @@ function withoutMcpServerSections(text, name) {
4327
4446
  }).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
4328
4447
  }
4329
4448
  async function registerClaudeViaConfig(mcp, homeDir, registration) {
4330
- const path = join4(homeDir, ".claude.json");
4449
+ const path = join5(homeDir, ".claude.json");
4331
4450
  let config = {};
4332
- if (existsSync2(path)) {
4451
+ if (existsSync3(path)) {
4333
4452
  try {
4334
- config = JSON.parse(await readFile3(path, "utf8"));
4453
+ config = JSON.parse(await readFile4(path, "utf8"));
4335
4454
  } catch (error) {
4336
4455
  throw new Error(`Cannot update malformed Claude configuration at ${path}: ${error instanceof Error ? error.message : String(error)}`);
4337
4456
  }
@@ -4348,9 +4467,9 @@ async function registerClaudeViaConfig(mcp, homeDir, registration) {
4348
4467
  `);
4349
4468
  }
4350
4469
  async function registerCodexViaConfig(mcp, homeDir, registration) {
4351
- const path = join4(homeDir, ".codex", "config.toml");
4470
+ const path = join5(homeDir, ".codex", "config.toml");
4352
4471
  let text = "";
4353
- if (existsSync2(path)) text = await readFile3(path, "utf8");
4472
+ if (existsSync3(path)) text = await readFile4(path, "utf8");
4354
4473
  text = withoutMcpServerSections(text, registration.name);
4355
4474
  const argsToml = mcp.args.length ? `
4356
4475
  args = [${mcp.args.map((a) => `"${a}"`).join(", ")}]` : "";
@@ -4391,21 +4510,21 @@ function resolveMcpRegistration(options) {
4391
4510
  }
4392
4511
  return { name, ...context ? { context } : {}, environment };
4393
4512
  }
4394
- async function unregisterFromConfig(agent, homeDir) {
4513
+ async function unregisterFromConfig(agent, homeDir, name) {
4395
4514
  if (agent === "claude") {
4396
- const path2 = join4(homeDir, ".claude.json");
4397
- if (!existsSync2(path2)) return;
4398
- const config = JSON.parse(await readFile3(path2, "utf8"));
4515
+ const path2 = join5(homeDir, ".claude.json");
4516
+ if (!existsSync3(path2)) return;
4517
+ const config = JSON.parse(await readFile4(path2, "utf8"));
4399
4518
  const servers = config.mcpServers ?? {};
4400
- delete servers.topolo;
4519
+ delete servers[name];
4401
4520
  config.mcpServers = servers;
4402
4521
  await atomicWrite(path2, `${JSON.stringify(config, null, 2)}
4403
4522
  `);
4404
4523
  return;
4405
4524
  }
4406
- const path = join4(homeDir, ".codex", "config.toml");
4407
- if (!existsSync2(path)) return;
4408
- const text = withoutMcpServerSections(await readFile3(path, "utf8"), "topolo");
4525
+ const path = join5(homeDir, ".codex", "config.toml");
4526
+ if (!existsSync3(path)) return;
4527
+ const text = withoutMcpServerSections(await readFile4(path, "utf8"), name);
4409
4528
  await atomicWrite(path, text ? `${text}
4410
4529
  ` : "");
4411
4530
  }
@@ -4418,6 +4537,11 @@ async function installForAgent(agent, sourceDir, mcp, opts) {
4418
4537
  try {
4419
4538
  if (agent === "claude") await registerClaudeViaConfig(mcp, opts.homeDir, opts.registration);
4420
4539
  else await registerCodexViaConfig(mcp, opts.homeDir, opts.registration);
4540
+ const installed = (await readAgentRegistrations(agent, opts.homeDir)).find((registration) => registration.name === opts.registration.name);
4541
+ const expectedEnvironment = registrationEnvironment(AGENT_NAME_FOR[agent], opts.registration);
4542
+ if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
4543
+ throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
4544
+ }
4421
4545
  return { agent, skillPath, skillInstalled: true, mcp: "config-file" };
4422
4546
  } catch (err) {
4423
4547
  return {
@@ -4441,46 +4565,36 @@ function registerSkillsCommand(program2) {
4441
4565
  const mode = resolveOutputMode(opts.json);
4442
4566
  try {
4443
4567
  const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
4568
+ assertAgentSetupSucceeded(results);
4444
4569
  emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
4445
4570
  } catch (err) {
4446
4571
  fail(err, mode);
4447
4572
  }
4448
4573
  }
4449
4574
  );
4450
- skills.command("status").description("Show whether the skill file + MCP server are installed for each agent").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
4575
+ skills.command("status").description("List public Topolo skill and MCP registrations for each agent").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) => {
4451
4576
  const mode = resolveOutputMode(opts.json);
4452
4577
  try {
4453
- const rows = ["claude", "codex"].map((agent) => {
4454
- const skillDir = agent === "claude" ? claudeSkillDir() : codexSkillDir();
4455
- return {
4456
- agent,
4457
- skillInstalled: existsSync2(join4(skillDir, "SKILL.md")),
4458
- skillPath: join4(skillDir, "SKILL.md"),
4459
- cliAvailable: agentBinaryExists(agent)
4460
- };
4578
+ const rows = await agentSurfaceStatus({
4579
+ homeDir: opts.agentHome ? resolve(opts.agentHome) : void 0,
4580
+ name: opts.name
4461
4581
  });
4462
4582
  emit({ agents: rows }, mode, () => formatStatus(rows));
4463
4583
  } catch (err) {
4464
4584
  fail(err, mode);
4465
4585
  }
4466
4586
  });
4467
- 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-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
4587
+ 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-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) => {
4468
4588
  const mode = resolveOutputMode(opts.json);
4469
4589
  try {
4470
- const homeDir = opts.agentHome ? resolve(opts.agentHome) : homedir2();
4471
- const removed = [];
4472
- for (const agent of resolveTargets(opts)) {
4473
- const skillDir = agent === "claude" ? claudeSkillDir(void 0, homeDir) : codexSkillDir(homeDir);
4474
- const skillFile = join4(skillDir, "SKILL.md");
4475
- const skillRemoved = existsSync2(skillFile);
4476
- if (skillRemoved) await rm(skillFile, { force: true });
4477
- await unregisterFromConfig(agent, homeDir);
4478
- removed.push({ agent, skillRemoved });
4479
- }
4590
+ const removed = await uninstallAgentSurfaces({
4591
+ ...opts,
4592
+ homeDir: opts.agentHome ? resolve(opts.agentHome) : void 0
4593
+ });
4480
4594
  emit(
4481
4595
  { removed },
4482
4596
  mode,
4483
- () => removed.map((r) => `${AGENT_LABELS[r.agent]}: skill removed=${r.skillRemoved}, MCP unregistered`).join("\n")
4597
+ () => removed.map((r) => `${AGENT_LABELS[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
4484
4598
  );
4485
4599
  } catch (err) {
4486
4600
  fail(err, mode);
@@ -4489,12 +4603,12 @@ function registerSkillsCommand(program2) {
4489
4603
  }
4490
4604
  async function installAgentSurfaces(opts) {
4491
4605
  const sourceDir = skillsSourceDir();
4492
- if (!existsSync2(join4(sourceDir, "topolo", "SKILL.md"))) {
4606
+ if (!existsSync3(join5(sourceDir, "topolo", "SKILL.md"))) {
4493
4607
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
4494
4608
  }
4495
4609
  const mcp = resolveMcpCommand();
4496
4610
  const registration = resolveMcpRegistration(opts);
4497
- const homeDir = opts.homeDir ?? homedir2();
4611
+ const homeDir = opts.homeDir ?? homedir3();
4498
4612
  const results = [];
4499
4613
  for (const agent of resolveTargets(opts)) {
4500
4614
  results.push(await installForAgent(agent, sourceDir, mcp, {
@@ -4511,12 +4625,53 @@ function registerSetupCommand(program2) {
4511
4625
  const mode = resolveOutputMode(opts.json);
4512
4626
  try {
4513
4627
  const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
4628
+ assertAgentSetupSucceeded(results);
4514
4629
  emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
4515
4630
  } catch (error) {
4516
4631
  fail(error, mode);
4517
4632
  }
4518
4633
  });
4519
4634
  }
4635
+ function assertAgentSetupSucceeded(results) {
4636
+ const failed = results.filter((result) => result.mcp === "failed");
4637
+ if (failed.length) {
4638
+ throw new Error(failed.map((result) => `${AGENT_LABELS[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; "));
4639
+ }
4640
+ }
4641
+ async function agentSurfaceStatus(options = {}) {
4642
+ const homeDir = options.homeDir ?? homedir3();
4643
+ const sourceSkill = await readFile4(join5(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
4644
+ return Promise.all(["claude", "codex"].map(async (agent) => {
4645
+ const skillPath = agentSkillPath(agent, homeDir);
4646
+ const installedSkill = existsSync3(skillPath) ? await readFile4(skillPath, "utf8") : void 0;
4647
+ const registrations = (await readAgentRegistrations(agent, homeDir)).filter((registration) => !options.name || registration.name === options.name);
4648
+ return {
4649
+ agent,
4650
+ skillInstalled: installedSkill !== void 0,
4651
+ skillCurrent: installedSkill === sourceSkill,
4652
+ skillPath,
4653
+ cliAvailable: agentBinaryExists(agent),
4654
+ registrations
4655
+ };
4656
+ }));
4657
+ }
4658
+ async function uninstallAgentSurfaces(options) {
4659
+ const homeDir = options.homeDir ?? homedir3();
4660
+ const name = options.name?.trim() || "topolo";
4661
+ resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
4662
+ const removed = [];
4663
+ for (const agent of resolveTargets(options)) {
4664
+ const before = await readAgentRegistrations(agent, homeDir);
4665
+ const registrationRemoved = before.some((entry) => entry.name === name);
4666
+ await unregisterFromConfig(agent, homeDir, name);
4667
+ const remaining = await readAgentRegistrations(agent, homeDir);
4668
+ const skillPath = agentSkillPath(agent, homeDir);
4669
+ const skillRemoved = remaining.length === 0 && existsSync3(skillPath);
4670
+ if (skillRemoved) await rm2(dirname3(skillPath), { recursive: true, force: true });
4671
+ removed.push({ agent, name, registrationRemoved, skillRemoved });
4672
+ }
4673
+ return removed;
4674
+ }
4520
4675
  function formatInstall(results, mcp) {
4521
4676
  const lines = [];
4522
4677
  const mcpStr = [mcp.command, ...mcp.args].join(" ");
@@ -4536,43 +4691,49 @@ function formatInstall(results, mcp) {
4536
4691
  }
4537
4692
  function formatStatus(rows) {
4538
4693
  return rows.map(
4539
- (r) => `${AGENT_LABELS[r.agent]}: skill ${r.skillInstalled ? "installed" : "not installed"} (${r.skillPath}), ${r.agent} CLI ${r.cliAvailable ? "available" : "not found"}`
4694
+ (r) => `${AGENT_LABELS[r.agent]}: skill ${r.skillInstalled ? r.skillCurrent ? "current" : "stale" : "not installed"} (${r.skillPath}), ${r.agent} CLI ${r.cliAvailable ? "available" : "not found"}, MCP registrations: ${r.registrations.map((registration) => registration.name).join(", ") || "none"}`
4540
4695
  ).join("\n");
4541
4696
  }
4542
4697
 
4543
4698
  // src/commands/doctor.ts
4544
4699
  import { access } from "fs/promises";
4700
+ import { homedir as homedir4 } from "os";
4701
+ import { resolve as resolve2 } from "path";
4545
4702
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4546
4703
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4547
4704
  var defaultDependencies = {
4548
4705
  resolveMcp: resolveMcpCommand,
4549
4706
  verifyFile: (path) => access(path),
4550
- verifyAuthentication: async () => {
4551
- const topolo = await buildClient();
4552
- if (!topolo) throw new Error("No credential configured. Run `topolo auth login`.");
4707
+ verifyAuthentication: async (context, environment) => {
4708
+ const topolo = await buildClient({ context, environment });
4709
+ if (!topolo) throw new Error(context ? `No credential configured for ${context}. Run \`topolo auth login\` for that context.` : "No credential configured. Run `topolo auth login`.");
4553
4710
  const identity = await topolo.identity.whoami();
4554
4711
  return identity.organization?.slug ?? identity.user.email;
4555
4712
  },
4556
4713
  verifyMcp: verifyMcpProtocol
4557
4714
  };
4558
- async function runDoctor(dependencies = defaultDependencies) {
4559
- const checks = [];
4715
+ async function runDoctor(dependencies = defaultDependencies, target) {
4716
+ const checks = [...target?.preflightChecks ?? []];
4560
4717
  let mcp;
4561
4718
  try {
4562
- mcp = dependencies.resolveMcp();
4719
+ mcp = target?.mcp ?? dependencies.resolveMcp();
4563
4720
  await dependencies.verifyFile(mcp.args[0]);
4564
4721
  checks.push({ name: "launcher", ok: true, detail: `${mcp.command} ${mcp.args.join(" ")}` });
4565
4722
  } catch (error) {
4566
4723
  checks.push({ name: "launcher", ok: false, detail: errorMessage(error) });
4567
4724
  }
4568
4725
  try {
4569
- checks.push({ name: "authentication", ok: true, detail: await dependencies.verifyAuthentication() });
4726
+ checks.push({
4727
+ name: "authentication",
4728
+ ok: true,
4729
+ detail: await dependencies.verifyAuthentication(target?.context, target?.environment)
4730
+ });
4570
4731
  } catch (error) {
4571
4732
  checks.push({ name: "authentication", ok: false, detail: errorMessage(error) });
4572
4733
  }
4573
4734
  if (mcp) {
4574
4735
  try {
4575
- checks.push({ name: "mcp", ok: true, detail: await dependencies.verifyMcp(mcp.command, mcp.args) });
4736
+ checks.push({ name: "mcp", ok: true, detail: await dependencies.verifyMcp(mcp.command, mcp.args, target?.env) });
4576
4737
  } catch (error) {
4577
4738
  checks.push({ name: "mcp", ok: false, detail: errorMessage(error) });
4578
4739
  }
@@ -4581,9 +4742,9 @@ async function runDoctor(dependencies = defaultDependencies) {
4581
4742
  }
4582
4743
  return { ok: checks.every((check) => check.ok), cliVersion: CLI_VERSION, checks };
4583
4744
  }
4584
- async function verifyMcpProtocol(command, args) {
4745
+ async function verifyMcpProtocol(command, args, env = definedEnvironment(process.env)) {
4585
4746
  const client = new Client({ name: "topolo-cli-doctor", version: CLI_VERSION });
4586
- const transport = new StdioClientTransport({ command, args, stderr: "pipe" });
4747
+ const transport = new StdioClientTransport({ command, args, stderr: "pipe", env });
4587
4748
  try {
4588
4749
  await client.connect(transport);
4589
4750
  const tools = await client.listTools();
@@ -4597,12 +4758,50 @@ async function verifyMcpProtocol(command, args) {
4597
4758
  await client.close();
4598
4759
  }
4599
4760
  }
4761
+ async function resolveInstalledDoctorTarget(options) {
4762
+ const name = options.name?.trim() || "topolo";
4763
+ const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir4();
4764
+ const expected = resolveMcpCommand();
4765
+ const requestedAgents = options.claude && !options.codex ? ["claude"] : options.codex && !options.claude ? ["codex"] : ["claude", "codex"];
4766
+ const rows = (await agentSurfaceStatus({ homeDir, name })).filter((row) => requestedAgents.includes(row.agent));
4767
+ const preflightChecks = [];
4768
+ let selected = rows.flatMap((row) => row.registrations)[0];
4769
+ for (const row of rows) {
4770
+ preflightChecks.push({
4771
+ name: "skill",
4772
+ ok: row.skillCurrent,
4773
+ detail: `${row.agent}: ${row.skillCurrent ? "bundled skill is current" : row.skillInstalled ? "installed skill is stale" : "skill is missing"}`
4774
+ });
4775
+ const registration = row.registrations.find((entry) => entry.name === name);
4776
+ const exact = Boolean(
4777
+ registration && registration.command === expected.command && JSON.stringify(registration.args) === JSON.stringify(expected.args)
4778
+ );
4779
+ preflightChecks.push({
4780
+ name: "registration",
4781
+ ok: exact,
4782
+ detail: `${row.agent}: ${exact ? `${name} uses the bundled launcher` : `${name} is missing or does not use the bundled launcher`}`
4783
+ });
4784
+ }
4785
+ const context = options.context !== void 0 ? normalizeDoctorContext(options.context) : selected?.env.TOPOLO_CONTEXT;
4786
+ const environment = normalizeEnvironment(options.environment ?? selected?.env.TOPOLO_ENV);
4787
+ if (name !== "topolo") resolveMcpRegistration({ name, context, environment });
4788
+ if (!selected) selected = { name, ...expected, env: {} };
4789
+ const env = {
4790
+ ...definedEnvironment(process.env),
4791
+ ...selected.env,
4792
+ ...context ? { TOPOLO_CONTEXT: context, TOPOLO_ENV: environment } : {}
4793
+ };
4794
+ return { mcp: selected, env, context, environment, preflightChecks };
4795
+ }
4796
+ function definedEnvironment(environment) {
4797
+ return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
4798
+ }
4600
4799
  function registerDoctorCommand(program2) {
4601
- program2.command("doctor").description("Verify the CLI credential and bundled MCP server end to end").option("--context <context>", "Verify one exact personal or organization context").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
4800
+ program2.command("doctor").description("Verify installed agent surfaces, exact credential, and MCP protocol end to end").option("--name <name>", "Verify one installed MCP registration (defaults to topolo)").option("--context <context>", "Verify one exact personal or organization context").option("--environment <env>", "Override the context environment").option("--claude", "Verify Claude Code only").option("--codex", "Verify Codex only").option("--agent-home <dir>", "Inspect an alternate agent home").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
4602
4801
  const mode = resolveOutputMode(opts.json);
4603
4802
  try {
4604
- if (opts.context !== void 0) applyDoctorContext(opts.context);
4605
- const result = await runDoctor();
4803
+ const target = await resolveInstalledDoctorTarget(opts);
4804
+ const result = await runDoctor(defaultDependencies, target);
4606
4805
  if (!result.ok) throw new Error(formatDoctor(result));
4607
4806
  emit(result, mode, () => formatDoctor(result));
4608
4807
  } catch (error) {
@@ -4617,9 +4816,6 @@ function normalizeDoctorContext(value) {
4617
4816
  }
4618
4817
  return context;
4619
4818
  }
4620
- function applyDoctorContext(value, environment = process.env) {
4621
- environment.TOPOLO_CONTEXT = normalizeDoctorContext(value);
4622
- }
4623
4819
  function formatDoctor(result) {
4624
4820
  return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`).join("\n");
4625
4821
  }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.8.2",
4
- "topoloPublishedVersion": "0.8.2",
3
+ "version": "0.8.3",
4
+ "topoloPublishedVersion": "0.8.3",
5
5
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
6
+ "license": "UNLICENSED",
7
+ "author": "Topolo",
8
+ "homepage": "https://docs.topolo.app/guides/agents-and-automation",
6
9
  "type": "module",
7
10
  "bin": {
8
11
  "topolo": "dist/index.js"
@@ -22,8 +25,8 @@
22
25
  },
23
26
  "dependencies": {
24
27
  "@modelcontextprotocol/sdk": "^1.0.0",
25
- "@topolo/mcp": "0.11.0",
26
- "@topolo/sdk": "0.11.0",
28
+ "@topolo/mcp": "0.11.1",
29
+ "@topolo/sdk": "0.11.1",
27
30
  "commander": "^12.1.0"
28
31
  },
29
32
  "devDependencies": {
@@ -31,7 +31,8 @@ npm view @topolo/cli version
31
31
 
32
32
  If the CLI version differs, update `@topolo/cli`, run `topolo setup`, run `topolo doctor`, and restart the agent. The CLI owns compatible SDK and MCP versions; customers must not assemble that dependency set manually. Record the CLI version used with results.
33
33
 
34
- Production is the customer environment. Do not use internal development, staging, seed, deployment, audit, scaffold, or release workflows from this skill; those belong to the restricted `topolo-platform` toolchain.
34
+ Production is the customer environment. Internal engineering, deployment,
35
+ audit, provisioning, and release workflows are outside this public skill.
35
36
 
36
37
  ## Authentication gate
37
38
 
@@ -65,7 +66,7 @@ uses the exact bundled MCP instead of a global binary:
65
66
 
66
67
  ```bash
67
68
  topolo setup --name topolo-acme --context org:org_acme --env production
68
- topolo doctor --context org:org_acme --env production
69
+ topolo doctor --name topolo-acme
69
70
  ```
70
71
 
71
72
  The context selector accepts an
@@ -100,6 +101,7 @@ Its `data.policy.canCreate` and `data.policy.reason` are the caller-specific cre
100
101
 
101
102
  MCP equivalents:
102
103
 
104
+ - `topolo_whoami` for the current credential identity
103
105
  - `topolo_search_applications`
104
106
  - `topolo_get_application`
105
107
  - `topolo_get_resource_types`
@@ -165,7 +167,9 @@ Before every call, validate exact input against the published JSON Schema:
165
167
  topolo actions validate <action-id> --data '<json>' --json
166
168
  ```
167
169
 
168
- MCP: `topolo_validate_action`.
170
+ MCP: `topolo_validate_action`. Use `topolo_prepare_action` for one compact
171
+ validate-and-plan response, and `topolo_upload_action` only when the selected
172
+ action publishes an upload contract.
169
173
 
170
174
  For large inputs, write one schema-valid JSON object to a file and pass it without shell escaping:
171
175