@topolo/cli 0.9.8 → 0.10.0

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
@@ -11,7 +11,7 @@ absent.
11
11
  - Stored access-token refresh; the `@topolo/mcp` server reads the same credential, so agents need no API-key plumbing.
12
12
  - Production credentials by default, with isolated profiles for approved self-hosted environments.
13
13
  - **Per-context credentials** — hold and switch between your personal space and multiple organizations with `topolo context`.
14
- - **One-command agent setup** — `topolo setup` wires the bundled compatible skill + MCP into Claude Code / Codex.
14
+ - **One-command agent setup** — `topolo setup` detects and wires the bundled skill + MCP into Claude Code, Claude Desktop, Codex, Cursor, Grok Build, Gemini CLI, GitHub Copilot/VS Code, Windsurf, Cline, Goose, and Amazon Q Developer.
15
15
  - **End-to-end diagnostics** — `topolo doctor` proves authentication and the MCP protocol path.
16
16
  - Commands: `auth`, `whoami`, `context`, `setup`, `doctor`, `skills`, `apps`, `services`, `resources`, `workspaces`, credential-scoped `actions`, `oauth`, `integrations`, and generic `api` passthrough.
17
17
 
@@ -62,10 +62,11 @@ topolo context ls # personal + your orgs, and which is
62
62
  topolo context use <organization|personal> # switch (validated + a fresh token minted server-side)
63
63
  ```
64
64
 
65
- ### Wire into Claude Code / Codex
65
+ ### Wire into agent hosts
66
66
 
67
67
  ```bash
68
- topolo setup # copies the skill + registers the exact bundled MCP
68
+ topolo setup # detects installed local hosts and registers the exact bundled MCP
69
+ topolo setup --all # configure every supported local host
69
70
  topolo doctor # verifies auth, handshake, tool discovery, and identity
70
71
  ```
71
72
 
@@ -78,9 +79,18 @@ topolo setup --name topolo-acme --context org:org_acme --env production
78
79
  topolo doctor --name topolo-acme
79
80
  ```
80
81
 
81
- This creates separately named Claude Code and Codex registrations using the
82
- same exact nested MCP executable. Reserved operator registrations cannot be
83
- created or removed by the public CLI.
82
+ This creates native registrations for each detected host using the same exact
83
+ bundled MCP executable. Use `--agent <host>` to target one host explicitly, or
84
+ `--project <dir>` when a host should receive project-scoped MCP and skill files.
85
+ Reserved operator registrations cannot be created or removed by the public CLI.
86
+
87
+ Host ids accepted by `--agent` are `claude`, `claude-desktop`, `codex`,
88
+ `cursor`, `grok`, `gemini`, `copilot`, `windsurf`, `cline`,
89
+ `goose`, and `amazon-q`.
90
+
91
+ The local installer does not edit JetBrains AI Assistant settings or web-only
92
+ connectors because those are managed through their own UI or remote connector
93
+ configuration. Use the generated native MCP entry as the import source there.
84
94
 
85
95
  ## Cross-org isolation
86
96
 
package/dist/index.js CHANGED
@@ -5055,10 +5055,10 @@ function formatCheckResult(result) {
5055
5055
 
5056
5056
  // src/commands/skills.ts
5057
5057
  import { spawnSync as spawnSync2 } from "child_process";
5058
- import { existsSync as existsSync3 } from "fs";
5059
- import { mkdir as mkdir2, copyFile, readFile as readFile4, writeFile as writeFile2, rm as rm2, rename } from "fs/promises";
5058
+ import { existsSync as existsSync4 } from "fs";
5059
+ import { readFile as readFile5, rm as rm2 } from "fs/promises";
5060
5060
  import { homedir as homedir3 } from "os";
5061
- import { dirname as dirname3, join as join5, resolve } from "path";
5061
+ import { dirname as dirname4, join as join5, resolve } from "path";
5062
5062
  import { fileURLToPath as fileURLToPath2 } from "url";
5063
5063
  import { createRequire } from "module";
5064
5064
 
@@ -5068,6 +5068,45 @@ import { existsSync as existsSync2 } from "fs";
5068
5068
  import { readFile as readFile3 } from "fs/promises";
5069
5069
  import { homedir as homedir2 } from "os";
5070
5070
  import { isAbsolute, join as join4 } from "path";
5071
+ var SUPPORTED_AGENT_IDS = [
5072
+ "claude",
5073
+ "claude-desktop",
5074
+ "codex",
5075
+ "cursor",
5076
+ "grok",
5077
+ "gemini",
5078
+ "copilot",
5079
+ "windsurf",
5080
+ "cline",
5081
+ "goose",
5082
+ "amazon-q"
5083
+ ];
5084
+ var AGENT_LABELS = {
5085
+ claude: "Claude Code",
5086
+ "claude-desktop": "Claude Desktop",
5087
+ codex: "Codex",
5088
+ cursor: "Cursor",
5089
+ grok: "Grok Build",
5090
+ gemini: "Gemini CLI",
5091
+ copilot: "GitHub Copilot / VS Code",
5092
+ windsurf: "Windsurf",
5093
+ cline: "Cline",
5094
+ goose: "Goose",
5095
+ "amazon-q": "Amazon Q Developer"
5096
+ };
5097
+ var AGENT_NAME_FOR = {
5098
+ claude: "claude-code",
5099
+ "claude-desktop": "claude-desktop",
5100
+ codex: "codex",
5101
+ cursor: "cursor",
5102
+ grok: "grok",
5103
+ gemini: "gemini",
5104
+ copilot: "github-copilot",
5105
+ windsurf: "windsurf",
5106
+ cline: "cline",
5107
+ goose: "goose",
5108
+ "amazon-q": "amazon-q"
5109
+ };
5071
5110
  var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
5072
5111
  function isPublicRegistrationName(name) {
5073
5112
  return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
@@ -5083,32 +5122,178 @@ function stableNodeCommand() {
5083
5122
  });
5084
5123
  return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
5085
5124
  }
5086
- function agentSkillPath(agent, homeDir = homedir2()) {
5087
- return agent === "claude" ? join4(homeDir, ".claude", "skills", "topolo", "SKILL.md") : join4(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5088
- }
5089
- async function readAgentRegistrations(agent, homeDir = homedir2()) {
5090
- return agent === "claude" ? readClaudeRegistrations(homeDir) : readCodexRegistrations(homeDir);
5125
+ function agentSkillPath(agent, homeDir = homedir2(), projectDir) {
5126
+ switch (agent) {
5127
+ case "claude":
5128
+ return projectDir ? join4(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join4(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5129
+ case "claude-desktop":
5130
+ return join4(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5131
+ case "codex":
5132
+ return join4(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5133
+ case "cursor":
5134
+ return join4(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
5135
+ case "grok":
5136
+ return join4(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
5137
+ case "gemini":
5138
+ return join4(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
5139
+ case "copilot":
5140
+ return projectDir ? join4(projectDir, ".github", "skills", "topolo", "SKILL.md") : join4(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
5141
+ case "windsurf":
5142
+ return projectDir ? join4(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
5143
+ case "cline":
5144
+ return join4(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
5145
+ case "goose":
5146
+ case "amazon-q":
5147
+ return void 0;
5148
+ }
5149
+ }
5150
+ function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
5151
+ switch (agent) {
5152
+ case "claude":
5153
+ return [{ path: join4(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
5154
+ case "claude-desktop":
5155
+ return [{
5156
+ path: claudeDesktopConfigPath(homeDir),
5157
+ format: "json",
5158
+ rootKey: "mcpServers"
5159
+ }];
5160
+ case "codex":
5161
+ return [{ path: join4(homeDir, ".codex", "config.toml"), format: "toml" }];
5162
+ case "cursor":
5163
+ return [{
5164
+ path: projectDir ? join4(projectDir, ".cursor", "mcp.json") : join4(homeDir, ".cursor", "mcp.json"),
5165
+ format: "json",
5166
+ rootKey: "mcpServers"
5167
+ }];
5168
+ case "grok":
5169
+ return [{
5170
+ path: projectDir ? join4(projectDir, ".grok", "config.toml") : join4(homeDir, ".grok", "config.toml"),
5171
+ format: "toml"
5172
+ }];
5173
+ case "gemini":
5174
+ return [{
5175
+ path: projectDir ? join4(projectDir, ".gemini", "settings.json") : join4(homeDir, ".gemini", "settings.json"),
5176
+ format: "json",
5177
+ rootKey: "mcpServers"
5178
+ }];
5179
+ case "copilot":
5180
+ return projectDir ? [
5181
+ { path: join4(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
5182
+ { path: join4(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
5183
+ ] : [
5184
+ {
5185
+ path: join4(homeDir, ".copilot", "mcp-config.json"),
5186
+ format: "json",
5187
+ rootKey: "mcpServers"
5188
+ },
5189
+ { path: vscodeUserConfigPath(homeDir), format: "json", rootKey: "servers" }
5190
+ ];
5191
+ case "windsurf":
5192
+ return [{
5193
+ path: join4(homeDir, ".codeium", "windsurf", "mcp_config.json"),
5194
+ format: "json",
5195
+ rootKey: "mcpServers"
5196
+ }];
5197
+ case "cline":
5198
+ return [{
5199
+ path: projectDir ? join4(projectDir, ".cline", "mcp.json") : join4(homeDir, ".cline", "mcp.json"),
5200
+ format: "json",
5201
+ rootKey: "mcpServers"
5202
+ }];
5203
+ case "goose":
5204
+ return [{ path: join4(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
5205
+ case "amazon-q":
5206
+ if (projectDir) {
5207
+ return [{ path: join4(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
5208
+ }
5209
+ return [{
5210
+ path: existsSync2(join4(homeDir, ".aws", "amazonq", "mcp.json")) ? join4(homeDir, ".aws", "amazonq", "mcp.json") : join4(homeDir, ".aws", "amazonq", "default.json"),
5211
+ format: "json",
5212
+ rootKey: "mcpServers"
5213
+ }];
5214
+ }
5215
+ }
5216
+ function agentCliNames(agent) {
5217
+ switch (agent) {
5218
+ case "claude":
5219
+ return ["claude"];
5220
+ case "claude-desktop":
5221
+ return [];
5222
+ case "codex":
5223
+ return ["codex"];
5224
+ case "cursor":
5225
+ return ["cursor"];
5226
+ case "grok":
5227
+ return ["grok"];
5228
+ case "gemini":
5229
+ return ["gemini"];
5230
+ case "copilot":
5231
+ return ["copilot"];
5232
+ case "windsurf":
5233
+ return ["windsurf"];
5234
+ case "cline":
5235
+ return ["cline"];
5236
+ case "goose":
5237
+ return ["goose"];
5238
+ case "amazon-q":
5239
+ return ["qchat", "q"];
5240
+ }
5241
+ }
5242
+ function agentConfigExists(agent, homeDir = homedir2(), projectDir) {
5243
+ return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync2(config.path));
5244
+ }
5245
+ function claudeDesktopConfigPath(homeDir) {
5246
+ if (process.platform === "darwin") return join4(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
5247
+ if (process.platform === "win32") return join4(process.env.APPDATA ?? join4(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5248
+ return join4(process.env.XDG_CONFIG_HOME ?? join4(homeDir, ".config"), "Claude", "claude_desktop_config.json");
5249
+ }
5250
+ function vscodeUserConfigPath(homeDir) {
5251
+ if (process.platform === "darwin") return join4(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
5252
+ if (process.platform === "win32") return join4(process.env.APPDATA ?? join4(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
5253
+ return join4(process.env.XDG_CONFIG_HOME ?? join4(homeDir, ".config"), "Code", "User", "mcp.json");
5254
+ }
5255
+ async function readAgentRegistrations(agent, homeDir = homedir2(), projectDir) {
5256
+ const registrations = [];
5257
+ for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
5258
+ if (!existsSync2(config.path)) continue;
5259
+ if (config.format === "json") {
5260
+ registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
5261
+ } else if (config.format === "toml") {
5262
+ registrations.push(...await readTomlRegistrations(config.path, agent));
5263
+ } else {
5264
+ registrations.push(...await readGooseRegistrations(config.path));
5265
+ }
5266
+ }
5267
+ return registrations.filter((registration, index, all) => all.findIndex((candidate) => candidate.name === registration.name) === index);
5091
5268
  }
5092
- async function readClaudeRegistrations(homeDir) {
5093
- const path = join4(homeDir, ".claude.json");
5094
- if (!existsSync2(path)) return [];
5269
+ async function readJsonRegistrations(path, rootKey, agent) {
5095
5270
  let parsed;
5096
5271
  try {
5097
5272
  parsed = JSON.parse(await readFile3(path, "utf8"));
5098
5273
  } catch (error) {
5099
- throw new Error(`Cannot read malformed Claude configuration at ${path}: ${message(error)}`);
5274
+ throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
5100
5275
  }
5101
- const servers = parsed.mcpServers;
5276
+ const servers = parsed[rootKey];
5102
5277
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
5103
5278
  return Object.entries(servers).filter(([name]) => isPublicRegistrationName(name)).flatMap(([name, value]) => normalizeRegistration(name, value));
5104
5279
  }
5105
- async function readCodexRegistrations(homeDir) {
5106
- const path = join4(homeDir, ".codex", "config.toml");
5107
- if (!existsSync2(path)) return [];
5280
+ async function readTomlRegistrations(path, agent) {
5108
5281
  const registrations = /* @__PURE__ */ new Map();
5109
5282
  let section;
5283
+ let pending;
5110
5284
  for (const rawLine of (await readFile3(path, "utf8")).split(/\r?\n/)) {
5111
- const line = rawLine.trim();
5285
+ const line = stripTomlComment(rawLine).trim();
5286
+ if (pending) {
5287
+ if (line.startsWith("[")) {
5288
+ throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
5289
+ }
5290
+ pending.value = pending.value + " " + line;
5291
+ if (isTomlValueComplete(pending.value)) {
5292
+ applyTomlAssignment(path, agent, section, pending, registrations);
5293
+ pending = void 0;
5294
+ }
5295
+ continue;
5296
+ }
5112
5297
  const heading = line.match(/^\[mcp_servers\.([A-Za-z0-9_-]+)(\.env)?\]$/);
5113
5298
  if (heading) {
5114
5299
  section = { name: heading[1], env: Boolean(heading[2]) };
@@ -5124,19 +5309,199 @@ async function readCodexRegistrations(homeDir) {
5124
5309
  if (!section || !isPublicRegistrationName(section.name) || !line || line.startsWith("#")) continue;
5125
5310
  const assignment = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
5126
5311
  if (!assignment) continue;
5127
- const registration = registrations.get(section.name);
5312
+ if (!section.env && assignment[1] !== "command" && assignment[1] !== "args" && assignment[1] !== "env") continue;
5313
+ pending = { key: assignment[1], value: assignment[2] };
5314
+ if (isTomlValueComplete(pending.value)) {
5315
+ applyTomlAssignment(path, agent, section, pending, registrations);
5316
+ pending = void 0;
5317
+ }
5318
+ }
5319
+ if (pending) throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
5320
+ return [...registrations.values()].filter((registration) => registration.command);
5321
+ }
5322
+ async function readGooseRegistrations(path) {
5323
+ const text = await readFile3(path, "utf8");
5324
+ const registrations = [];
5325
+ const lines = text.split(/\r?\n/);
5326
+ let inExtensions = false;
5327
+ let inTopolo = false;
5328
+ let current;
5329
+ for (const rawLine of lines) {
5330
+ if (/^extensions:\s*$/.test(rawLine)) {
5331
+ inExtensions = true;
5332
+ inTopolo = false;
5333
+ continue;
5334
+ }
5335
+ if (inExtensions && /^\S/.test(rawLine) && rawLine.trim()) {
5336
+ inExtensions = false;
5337
+ inTopolo = false;
5338
+ current = void 0;
5339
+ }
5340
+ if (!inExtensions) continue;
5341
+ const extension = rawLine.match(/^ ([A-Za-z0-9_-]+):\s*$/);
5342
+ if (extension) {
5343
+ inTopolo = true;
5344
+ current = isPublicRegistrationName(extension[1]) ? { name: extension[1], command: "", args: [], env: {} } : void 0;
5345
+ continue;
5346
+ }
5347
+ if (inTopolo && /^\s{2}\S/.test(rawLine) && !/^\s{4}/.test(rawLine)) {
5348
+ inTopolo = false;
5349
+ if (current?.command) registrations.push(current);
5350
+ current = void 0;
5351
+ }
5352
+ if (!inTopolo || !current) continue;
5353
+ const command = rawLine.match(/^\s{4}cmd:\s*(.+)$/);
5354
+ if (command) current.command = parseYamlScalar(command[1]);
5355
+ const args = rawLine.match(/^\s{4}args:\s*\[(.*)\]\s*$/);
5356
+ if (args) current.args = splitYamlArray(args[1]).map(parseYamlScalar);
5357
+ const env = rawLine.match(/^\s{6}([A-Za-z0-9_]+):\s*(.+)$/);
5358
+ if (env) current.env[env[1]] = parseYamlScalar(env[2]);
5359
+ }
5360
+ if (current?.command) registrations.push(current);
5361
+ return registrations;
5362
+ }
5363
+ function parseYamlScalar(value) {
5364
+ const trimmed = value.trim();
5365
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1).replace(/''/g, "'");
5366
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
5128
5367
  try {
5129
- const value = JSON.parse(assignment[2]);
5130
- if (section.env && typeof value === "string") registration.env[assignment[1]] = value;
5131
- if (!section.env && assignment[1] === "command" && typeof value === "string") registration.command = value;
5132
- if (!section.env && assignment[1] === "args" && Array.isArray(value) && value.every((item) => typeof item === "string")) {
5133
- registration.args = value;
5134
- }
5368
+ return JSON.parse(trimmed);
5135
5369
  } catch {
5136
- throw new Error(`Cannot read unsupported MCP value in Codex configuration at ${path}.`);
5370
+ return trimmed.slice(1, -1);
5137
5371
  }
5138
5372
  }
5139
- return [...registrations.values()].filter((registration) => registration.command);
5373
+ return trimmed;
5374
+ }
5375
+ function splitYamlArray(value) {
5376
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
5377
+ }
5378
+ function applyTomlAssignment(path, agent, section, assignment, registrations) {
5379
+ const registration = registrations.get(section.name);
5380
+ let value;
5381
+ try {
5382
+ value = parseTomlValue(assignment.value);
5383
+ } catch {
5384
+ throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
5385
+ }
5386
+ if (section.env && typeof value === "string") registration.env[assignment.key] = value;
5387
+ if (!section.env && assignment.key === "command" && typeof value === "string") registration.command = value;
5388
+ if (!section.env && assignment.key === "args" && Array.isArray(value) && value.every((item) => typeof item === "string")) {
5389
+ registration.args = value;
5390
+ }
5391
+ if (!section.env && assignment.key === "env" && value && typeof value === "object" && !Array.isArray(value)) {
5392
+ registration.env = Object.fromEntries(Object.entries(value).filter(
5393
+ (entry) => typeof entry[1] === "string"
5394
+ ));
5395
+ }
5396
+ }
5397
+ function stripTomlComment(line) {
5398
+ let quote;
5399
+ let escaped = false;
5400
+ for (let index = 0; index < line.length; index += 1) {
5401
+ const character = line[index];
5402
+ if (quote === '"') {
5403
+ if (escaped) escaped = false;
5404
+ else if (character === "\\") escaped = true;
5405
+ else if (character === '"') quote = void 0;
5406
+ continue;
5407
+ }
5408
+ if (quote === "'") {
5409
+ if (character === "'" && line[index + 1] === "'") index += 1;
5410
+ else if (character === "'") quote = void 0;
5411
+ continue;
5412
+ }
5413
+ if (character === '"' || character === "'") quote = character;
5414
+ else if (character === "#") return line.slice(0, index);
5415
+ }
5416
+ return line;
5417
+ }
5418
+ function isTomlValueComplete(value) {
5419
+ let quote;
5420
+ let escaped = false;
5421
+ let arrayDepth = 0;
5422
+ let tableDepth = 0;
5423
+ for (let index = 0; index < value.length; index += 1) {
5424
+ const character = value[index];
5425
+ if (quote === '"') {
5426
+ if (escaped) escaped = false;
5427
+ else if (character === "\\") escaped = true;
5428
+ else if (character === '"') quote = void 0;
5429
+ continue;
5430
+ }
5431
+ if (quote === "'") {
5432
+ if (character === "'" && value[index + 1] === "'") index += 1;
5433
+ else if (character === "'") quote = void 0;
5434
+ continue;
5435
+ }
5436
+ if (character === '"' || character === "'") quote = character;
5437
+ else if (character === "[") arrayDepth += 1;
5438
+ else if (character === "]") arrayDepth -= 1;
5439
+ else if (character === "{") tableDepth += 1;
5440
+ else if (character === "}") tableDepth -= 1;
5441
+ }
5442
+ return !quote && arrayDepth === 0 && tableDepth === 0;
5443
+ }
5444
+ function parseTomlValue(rawValue) {
5445
+ const value = rawValue.trim();
5446
+ try {
5447
+ return JSON.parse(value);
5448
+ } catch {
5449
+ }
5450
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
5451
+ if (value.startsWith("[") && value.endsWith("]")) {
5452
+ const body = value.slice(1, -1).trim();
5453
+ if (!body) return [];
5454
+ return splitTomlArray(body).map((item) => parseTomlValue(item));
5455
+ }
5456
+ if (value.startsWith("{") && value.endsWith("}")) {
5457
+ const body = value.slice(1, -1).trim();
5458
+ if (!body) return {};
5459
+ return Object.fromEntries(splitTomlArray(body).map((item) => {
5460
+ const separator = item.indexOf("=");
5461
+ if (separator < 1) throw new Error("Unsupported TOML inline table");
5462
+ const key = item.slice(0, separator).trim();
5463
+ return [
5464
+ key.startsWith('"') && key.endsWith('"') ? parseTomlValue(key) : key,
5465
+ parseTomlValue(item.slice(separator + 1))
5466
+ ];
5467
+ }));
5468
+ }
5469
+ throw new Error("Unsupported TOML value");
5470
+ }
5471
+ function splitTomlArray(value) {
5472
+ const items = [];
5473
+ let start = 0;
5474
+ let quote;
5475
+ let escaped = false;
5476
+ let arrayDepth = 0;
5477
+ let tableDepth = 0;
5478
+ for (let index = 0; index < value.length; index += 1) {
5479
+ const character = value[index];
5480
+ if (quote === '"') {
5481
+ if (escaped) escaped = false;
5482
+ else if (character === "\\") escaped = true;
5483
+ else if (character === '"') quote = void 0;
5484
+ continue;
5485
+ }
5486
+ if (quote === "'") {
5487
+ if (character === "'" && value[index + 1] === "'") index += 1;
5488
+ else if (character === "'") quote = void 0;
5489
+ continue;
5490
+ }
5491
+ if (character === '"' || character === "'") quote = character;
5492
+ else if (character === "[") arrayDepth += 1;
5493
+ else if (character === "]") arrayDepth -= 1;
5494
+ else if (character === "{") tableDepth += 1;
5495
+ else if (character === "}") tableDepth -= 1;
5496
+ else if (character === "," && arrayDepth === 0 && tableDepth === 0) {
5497
+ const item = value.slice(start, index).trim();
5498
+ if (item) items.push(item);
5499
+ start = index + 1;
5500
+ }
5501
+ }
5502
+ const last = value.slice(start).trim();
5503
+ if (last) items.push(last);
5504
+ return items;
5140
5505
  }
5141
5506
  function normalizeRegistration(name, value) {
5142
5507
  if (!value || typeof value !== "object" || Array.isArray(value)) return [];
@@ -5150,50 +5515,179 @@ function message(error) {
5150
5515
  return error instanceof Error ? error.message : String(error);
5151
5516
  }
5152
5517
 
5153
- // src/commands/skills.ts
5154
- var AGENT_LABELS = { claude: "Claude Code", codex: "Codex" };
5155
- var AGENT_NAME_FOR = { claude: "claude-code", codex: "codex" };
5156
- function skillsSourceDir() {
5157
- const here = dirname3(fileURLToPath2(import.meta.url));
5158
- const distCandidate = resolve(here, "../skills");
5159
- const srcCandidate = resolve(here, "../../skills");
5160
- for (const candidate of [distCandidate, srcCandidate]) {
5161
- if (existsSync3(join5(candidate, "topolo", "SKILL.md"))) return candidate;
5518
+ // src/commands/agent-config.ts
5519
+ import { existsSync as existsSync3 } from "fs";
5520
+ import { mkdir as mkdir2, readFile as readFile4, rename, writeFile as writeFile2 } from "fs/promises";
5521
+ import { dirname as dirname3 } from "path";
5522
+ async function installAgentSkill(agent, source, homeDir, projectDir) {
5523
+ const path = agentSkillPath(agent, homeDir, projectDir);
5524
+ if (!path) return { installed: false };
5525
+ const contents = formatSkillForAgent(agent, source);
5526
+ await atomicWrite(path, contents);
5527
+ return { path, installed: true };
5528
+ }
5529
+ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir) {
5530
+ for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
5531
+ if (config.format === "json") {
5532
+ await registerJson(config, agent, mcp, registration);
5533
+ } else if (config.format === "toml") {
5534
+ await registerToml(config.path, agent, mcp, registration);
5535
+ } else {
5536
+ await registerGoose(config.path, mcp, registration);
5537
+ }
5162
5538
  }
5163
- return distCandidate;
5164
5539
  }
5165
- function resolveMcpCommand() {
5166
- const require2 = createRequire(import.meta.url);
5167
- return { command: stableNodeCommand(), args: [require2.resolve("@topolo/mcp/stdio")] };
5540
+ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
5541
+ for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
5542
+ if (!existsSync3(config.path)) continue;
5543
+ if (config.format === "json") {
5544
+ await unregisterJson(config, agent, name);
5545
+ } else if (config.format === "toml") {
5546
+ const text = withoutMcpServerSections(await readFile4(config.path, "utf8"), name);
5547
+ await atomicWrite(config.path, text ? text + "\n" : "");
5548
+ } else {
5549
+ await unregisterGoose(config.path, name);
5550
+ }
5551
+ }
5168
5552
  }
5169
- function agentBinaryExists(bin) {
5170
- const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
5171
- stdio: "ignore"
5172
- });
5173
- return probe.status === 0;
5553
+ function registrationEnvironment(agent, registration) {
5554
+ return {
5555
+ TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
5556
+ ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
5557
+ };
5174
5558
  }
5175
- function claudeSkillDir(projectDir, homeDir = homedir3()) {
5176
- const base = projectDir ? resolve(projectDir, ".claude") : join5(homeDir, ".claude");
5177
- return join5(base, "skills", "topolo");
5559
+ async function registerJson(config, agent, mcp, registration) {
5560
+ let parsed = {};
5561
+ if (existsSync3(config.path)) {
5562
+ try {
5563
+ parsed = JSON.parse(await readFile4(config.path, "utf8"));
5564
+ } catch (error) {
5565
+ throw new Error(
5566
+ "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
5567
+ );
5568
+ }
5569
+ }
5570
+ const rootKey = config.rootKey ?? "mcpServers";
5571
+ const existingServers = parsed[rootKey];
5572
+ if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
5573
+ throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
5574
+ }
5575
+ const servers = existingServers ?? {};
5576
+ const entry = {
5577
+ command: mcp.command,
5578
+ args: mcp.args,
5579
+ env: registrationEnvironment(agent, registration)
5580
+ };
5581
+ if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
5582
+ servers[registration.name] = entry;
5583
+ parsed[rootKey] = servers;
5584
+ await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
5178
5585
  }
5179
- function codexSkillDir(homeDir = homedir3()) {
5180
- return join5(homeDir, ".codex", "skills", "topolo");
5586
+ async function unregisterJson(config, agent, name) {
5587
+ let parsed;
5588
+ try {
5589
+ parsed = JSON.parse(await readFile4(config.path, "utf8"));
5590
+ } catch (error) {
5591
+ throw new Error(
5592
+ "Cannot update malformed " + AGENT_LABELS[agent] + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
5593
+ );
5594
+ }
5595
+ const rootKey = config.rootKey ?? "mcpServers";
5596
+ const existingServers = parsed[rootKey];
5597
+ if (existingServers !== void 0 && (!existingServers || typeof existingServers !== "object" || Array.isArray(existingServers))) {
5598
+ throw new Error("Cannot update malformed " + AGENT_LABELS[agent] + " MCP configuration at " + config.path + ".");
5599
+ }
5600
+ const servers = existingServers ?? {};
5601
+ delete servers[name];
5602
+ parsed[rootKey] = servers;
5603
+ await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
5604
+ }
5605
+ async function registerToml(path, agent, mcp, registration) {
5606
+ const existing = existsSync3(path) ? await readFile4(path, "utf8") : "";
5607
+ const text = withoutMcpServerSections(existing, registration.name);
5608
+ const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
5609
+ const environment = registrationEnvironment(agent, registration);
5610
+ const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
5611
+ const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
5612
+ await atomicWrite(path, text + block);
5613
+ }
5614
+ async function registerGoose(path, mcp, registration) {
5615
+ const existing = existsSync3(path) ? await readFile4(path, "utf8") : "";
5616
+ const withoutTopolo = removeGooseExtension(existing, registration.name);
5617
+ const environment = registrationEnvironment("goose", registration);
5618
+ const block = [
5619
+ " " + registration.name + ":",
5620
+ " name: Topolo",
5621
+ " cmd: " + yamlScalar(mcp.command),
5622
+ " args: [" + mcp.args.map(yamlScalar).join(", ") + "]",
5623
+ " enabled: true",
5624
+ " type: stdio",
5625
+ " timeout: 300",
5626
+ " envs:",
5627
+ ...Object.entries(environment).map(([key, value]) => " " + key + ": " + yamlScalar(value))
5628
+ ].join("\n");
5629
+ await atomicWrite(path, insertGooseExtension(withoutTopolo, block));
5630
+ }
5631
+ async function unregisterGoose(path, name) {
5632
+ const existing = await readFile4(path, "utf8");
5633
+ await atomicWrite(path, removeGooseExtension(existing, name));
5181
5634
  }
5182
- async function installSkillFile(sourceDir, targetDir) {
5183
- await mkdir2(targetDir, { recursive: true });
5184
- const target = join5(targetDir, "SKILL.md");
5185
- await copyFile(join5(sourceDir, "topolo", "SKILL.md"), target);
5186
- return target;
5635
+ function removeGooseExtension(text, name) {
5636
+ const lines = text.split(/\r?\n/);
5637
+ const kept = [];
5638
+ let inExtensions = false;
5639
+ let skipping = false;
5640
+ for (const line of lines) {
5641
+ if (/^extensions:\s*$/.test(line)) {
5642
+ inExtensions = true;
5643
+ kept.push(line);
5644
+ continue;
5645
+ }
5646
+ if (inExtensions && /^\S/.test(line) && line.trim()) inExtensions = false;
5647
+ if (inExtensions && line.trim() === name + ":") {
5648
+ skipping = true;
5649
+ continue;
5650
+ }
5651
+ if (skipping && inExtensions && /^ \S/.test(line) && !/^\s{4}/.test(line)) skipping = false;
5652
+ if (!skipping) kept.push(line);
5653
+ }
5654
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
5655
+ }
5656
+ function insertGooseExtension(text, block) {
5657
+ if (!text.trim()) return "extensions:\n" + block + "\n";
5658
+ const lines = text.split(/\r?\n/);
5659
+ const extensionIndex = lines.findIndex((line) => /^extensions:\s*$/.test(line));
5660
+ if (extensionIndex < 0) return text.trimEnd() + "\n\nextensions:\n" + block + "\n";
5661
+ let end = lines.length;
5662
+ for (let index = extensionIndex + 1; index < lines.length; index += 1) {
5663
+ if (/^\S/.test(lines[index]) && lines[index].trim()) {
5664
+ end = index;
5665
+ break;
5666
+ }
5667
+ }
5668
+ lines.splice(end, 0, block);
5669
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
5670
+ }
5671
+ function yamlScalar(value) {
5672
+ return "'" + value.replace(/'/g, "''") + "'";
5673
+ }
5674
+ function formatSkillForAgent(agent, source) {
5675
+ if (agent !== "cursor" && agent !== "windsurf") return source;
5676
+ const body = source.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
5677
+ if (agent === "cursor") {
5678
+ return "---\ndescription: Use Topolo MCP for Topolo platform operations.\nalwaysApply: false\n---\n\n" + body;
5679
+ }
5680
+ return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
5187
5681
  }
5188
5682
  async function atomicWrite(path, contents) {
5189
5683
  await mkdir2(dirname3(path), { recursive: true });
5190
- const temporaryPath = `${path}.topolo-${process.pid}.tmp`;
5684
+ const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
5191
5685
  await writeFile2(temporaryPath, contents, { encoding: "utf8", mode: 384 });
5192
5686
  await rename(temporaryPath, path);
5193
5687
  }
5194
5688
  function withoutMcpServerSections(text, name) {
5195
- const section = `[mcp_servers.${name}]`;
5196
- const environmentSection = `[mcp_servers.${name}.env]`;
5689
+ const section = "[mcp_servers." + name + "]";
5690
+ const environmentSection = "[mcp_servers." + name + ".env]";
5197
5691
  let skipping = false;
5198
5692
  return text.split(/\r?\n/).filter((line) => {
5199
5693
  const trimmed = line.trim();
@@ -5205,48 +5699,29 @@ function withoutMcpServerSections(text, name) {
5205
5699
  return !skipping;
5206
5700
  }).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
5207
5701
  }
5208
- async function registerClaudeViaConfig(mcp, homeDir, registration) {
5209
- const path = join5(homeDir, ".claude.json");
5210
- let config = {};
5211
- if (existsSync3(path)) {
5212
- try {
5213
- config = JSON.parse(await readFile4(path, "utf8"));
5214
- } catch (error) {
5215
- throw new Error(`Cannot update malformed Claude configuration at ${path}: ${error instanceof Error ? error.message : String(error)}`);
5216
- }
5702
+
5703
+ // src/commands/skills.ts
5704
+ var AGENT_LABELS2 = AGENT_LABELS;
5705
+ function skillsSourceDir() {
5706
+ const here = dirname4(fileURLToPath2(import.meta.url));
5707
+ const distCandidate = resolve(here, "../skills");
5708
+ const srcCandidate = resolve(here, "../../skills");
5709
+ for (const candidate of [distCandidate, srcCandidate]) {
5710
+ if (existsSync4(join5(candidate, "topolo", "SKILL.md"))) return candidate;
5217
5711
  }
5218
- const servers = config.mcpServers ?? {};
5219
- servers[registration.name] = {
5220
- type: "stdio",
5221
- command: mcp.command,
5222
- args: mcp.args,
5223
- env: registrationEnvironment(AGENT_NAME_FOR.claude, registration)
5224
- };
5225
- config.mcpServers = servers;
5226
- await atomicWrite(path, `${JSON.stringify(config, null, 2)}
5227
- `);
5712
+ return distCandidate;
5228
5713
  }
5229
- async function registerCodexViaConfig(mcp, homeDir, registration) {
5230
- const path = join5(homeDir, ".codex", "config.toml");
5231
- let text = "";
5232
- if (existsSync3(path)) text = await readFile4(path, "utf8");
5233
- text = withoutMcpServerSections(text, registration.name);
5234
- const argsToml = mcp.args.length ? `
5235
- args = [${mcp.args.map((a) => `"${a}"`).join(", ")}]` : "";
5236
- const block = `
5237
-
5238
- [mcp_servers.${registration.name}]
5239
- command = ${JSON.stringify(mcp.command)}${argsToml}
5240
-
5241
- [mcp_servers.${registration.name}.env]
5242
- ` + Object.entries(registrationEnvironment(AGENT_NAME_FOR.codex, registration)).map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join("\n") + "\n";
5243
- await atomicWrite(path, `${text}${block}`);
5714
+ function resolveMcpCommand() {
5715
+ const require2 = createRequire(import.meta.url);
5716
+ return { command: stableNodeCommand(), args: [require2.resolve("@topolo/mcp/stdio")] };
5244
5717
  }
5245
- function registrationEnvironment(agentName, registration) {
5246
- return {
5247
- TOPOLO_AGENT_NAME: agentName,
5248
- ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
5249
- };
5718
+ function agentBinaryExists(agent) {
5719
+ return agentCliNames(agent).some((bin) => {
5720
+ const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
5721
+ stdio: "ignore"
5722
+ });
5723
+ return probe.status === 0;
5724
+ });
5250
5725
  }
5251
5726
  function resolveMcpRegistration(options) {
5252
5727
  const name = options.name?.trim() || "topolo";
@@ -5270,57 +5745,54 @@ function resolveMcpRegistration(options) {
5270
5745
  }
5271
5746
  return { name, ...context ? { context } : {}, environment };
5272
5747
  }
5273
- async function unregisterFromConfig(agent, homeDir, name) {
5274
- if (agent === "claude") {
5275
- const path2 = join5(homeDir, ".claude.json");
5276
- if (!existsSync3(path2)) return;
5277
- const config = JSON.parse(await readFile4(path2, "utf8"));
5278
- const servers = config.mcpServers ?? {};
5279
- delete servers[name];
5280
- config.mcpServers = servers;
5281
- await atomicWrite(path2, `${JSON.stringify(config, null, 2)}
5282
- `);
5283
- return;
5284
- }
5285
- const path = join5(homeDir, ".codex", "config.toml");
5286
- if (!existsSync3(path)) return;
5287
- const text = withoutMcpServerSections(await readFile4(path, "utf8"), name);
5288
- await atomicWrite(path, text ? `${text}
5289
- ` : "");
5290
- }
5291
5748
  async function installForAgent(agent, sourceDir, mcp, opts) {
5292
- const skillDir = agent === "claude" ? claudeSkillDir(opts.projectDir, opts.homeDir) : codexSkillDir(opts.homeDir);
5293
- const skillPath = await installSkillFile(sourceDir, skillDir);
5749
+ const source = await readFile5(join5(sourceDir, "topolo", "SKILL.md"), "utf8");
5750
+ const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
5294
5751
  if (!opts.withMcp) {
5295
- return { agent, skillPath, skillInstalled: true, mcp: "skipped" };
5752
+ return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
5296
5753
  }
5297
5754
  try {
5298
- if (agent === "claude") await registerClaudeViaConfig(mcp, opts.homeDir, opts.registration);
5299
- else await registerCodexViaConfig(mcp, opts.homeDir, opts.registration);
5300
- const installed = (await readAgentRegistrations(agent, opts.homeDir)).find((registration) => registration.name === opts.registration.name);
5301
- const expectedEnvironment = registrationEnvironment(AGENT_NAME_FOR[agent], opts.registration);
5755
+ await registerAgentConfig(agent, mcp, opts.homeDir, opts.registration, opts.projectDir);
5756
+ const installed = (await readAgentRegistrations(agent, opts.homeDir, opts.projectDir)).find((registration) => registration.name === opts.registration.name);
5757
+ const expectedEnvironment = registrationEnvironment(agent, opts.registration);
5302
5758
  if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
5303
5759
  throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
5304
5760
  }
5305
- return { agent, skillPath, skillInstalled: true, mcp: "config-file" };
5761
+ return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "config-file" };
5306
5762
  } catch (err) {
5307
5763
  return {
5308
5764
  agent,
5309
- skillPath,
5310
- skillInstalled: true,
5765
+ skillPath: skill.path,
5766
+ skillInstalled: skill.installed,
5311
5767
  mcp: "failed",
5312
5768
  detail: err instanceof Error ? err.message : String(err)
5313
5769
  };
5314
5770
  }
5315
5771
  }
5316
5772
  function resolveTargets(opts) {
5317
- if (opts.claude && !opts.codex) return ["claude"];
5318
- if (opts.codex && !opts.claude) return ["codex"];
5319
- return ["claude", "codex"];
5773
+ const explicit = [
5774
+ ...opts.agents ?? [],
5775
+ ...opts.agent ?? [],
5776
+ ...opts.claude ? ["claude"] : [],
5777
+ ...opts.codex ? ["codex"] : []
5778
+ ];
5779
+ if (explicit.length) {
5780
+ return [...new Set(explicit.map((value) => {
5781
+ if (!SUPPORTED_AGENT_IDS.includes(value)) {
5782
+ throw new Error('Unsupported agent host "' + value + '". Supported hosts: ' + SUPPORTED_AGENT_IDS.join(", ") + ".");
5783
+ }
5784
+ return value;
5785
+ }))];
5786
+ }
5787
+ if (opts.all) return [...SUPPORTED_AGENT_IDS];
5788
+ const homeDir = opts.homeDir ?? homedir3();
5789
+ const detectBinaries = homeDir === homedir3();
5790
+ const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
5791
+ return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
5320
5792
  }
5321
5793
  function registerSkillsCommand(program2) {
5322
- const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into Claude Code / Codex");
5323
- skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--project <dir>", "Install the Claude skill into <dir>/.claude instead of ~/.claude").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
5794
+ const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
5795
+ skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
5324
5796
  async (opts) => {
5325
5797
  const mode = resolveOutputMode(opts.json);
5326
5798
  try {
@@ -5332,19 +5804,22 @@ function registerSkillsCommand(program2) {
5332
5804
  }
5333
5805
  }
5334
5806
  );
5335
- 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) => {
5807
+ skills.command("status").description("List public Topolo skill and MCP registrations for each agent").option("--agent <host...>", "Inspect one or more hosts by id").option("--all", "Inspect every supported host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--name <name>", "Inspect one registration name").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5336
5808
  const mode = resolveOutputMode(opts.json);
5337
5809
  try {
5338
5810
  const rows = await agentSurfaceStatus({
5339
5811
  homeDir: opts.agentHome ? resolve(opts.agentHome) : void 0,
5340
- name: opts.name
5812
+ name: opts.name,
5813
+ agent: opts.agent,
5814
+ all: opts.all,
5815
+ project: opts.project
5341
5816
  });
5342
5817
  emit({ agents: rows }, mode, () => formatStatus(rows));
5343
5818
  } catch (err) {
5344
5819
  fail(err, mode);
5345
5820
  }
5346
5821
  });
5347
- 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) => {
5822
+ skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5348
5823
  const mode = resolveOutputMode(opts.json);
5349
5824
  try {
5350
5825
  const removed = await uninstallAgentSurfaces({
@@ -5354,7 +5829,7 @@ function registerSkillsCommand(program2) {
5354
5829
  emit(
5355
5830
  { removed },
5356
5831
  mode,
5357
- () => removed.map((r) => `${AGENT_LABELS[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
5832
+ () => removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
5358
5833
  );
5359
5834
  } catch (err) {
5360
5835
  fail(err, mode);
@@ -5363,14 +5838,14 @@ function registerSkillsCommand(program2) {
5363
5838
  }
5364
5839
  async function installAgentSurfaces(opts) {
5365
5840
  const sourceDir = skillsSourceDir();
5366
- if (!existsSync3(join5(sourceDir, "topolo", "SKILL.md"))) {
5841
+ if (!existsSync4(join5(sourceDir, "topolo", "SKILL.md"))) {
5367
5842
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
5368
5843
  }
5369
5844
  const mcp = resolveMcpCommand();
5370
5845
  const registration = resolveMcpRegistration(opts);
5371
5846
  const homeDir = opts.homeDir ?? homedir3();
5372
5847
  const results = [];
5373
- for (const agent of resolveTargets(opts)) {
5848
+ for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
5374
5849
  results.push(await installForAgent(agent, sourceDir, mcp, {
5375
5850
  withMcp: opts.mcp !== false,
5376
5851
  projectDir: opts.project,
@@ -5381,7 +5856,7 @@ async function installAgentSurfaces(opts) {
5381
5856
  return { results, mcp };
5382
5857
  }
5383
5858
  function registerSetupCommand(program2) {
5384
- program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5859
+ program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5385
5860
  const mode = resolveOutputMode(opts.json);
5386
5861
  try {
5387
5862
  const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
@@ -5395,20 +5870,27 @@ function registerSetupCommand(program2) {
5395
5870
  function assertAgentSetupSucceeded(results) {
5396
5871
  const failed = results.filter((result) => result.mcp === "failed");
5397
5872
  if (failed.length) {
5398
- throw new Error(failed.map((result) => `${AGENT_LABELS[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; "));
5873
+ throw new Error(failed.map((result) => `${AGENT_LABELS2[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; "));
5399
5874
  }
5400
5875
  }
5401
5876
  async function agentSurfaceStatus(options = {}) {
5402
5877
  const homeDir = options.homeDir ?? homedir3();
5403
- const sourceSkill = await readFile4(join5(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
5404
- return Promise.all(["claude", "codex"].map(async (agent) => {
5405
- const skillPath = agentSkillPath(agent, homeDir);
5406
- const installedSkill = existsSync3(skillPath) ? await readFile4(skillPath, "utf8") : void 0;
5407
- const registrations = (await readAgentRegistrations(agent, homeDir)).filter((registration) => !options.name || registration.name === options.name);
5878
+ const sourceSkill = await readFile5(join5(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
5879
+ const agents = resolveTargets({
5880
+ agent: options.agent,
5881
+ all: options.all,
5882
+ homeDir,
5883
+ project: options.project
5884
+ });
5885
+ return Promise.all(agents.map(async (agent) => {
5886
+ const skillPath = agentSkillPath(agent, homeDir, options.project);
5887
+ const installedSkill = skillPath && existsSync4(skillPath) ? await readFile5(skillPath, "utf8") : void 0;
5888
+ const registrations = (await readAgentRegistrations(agent, homeDir, options.project)).filter((registration) => !options.name || registration.name === options.name);
5408
5889
  return {
5409
5890
  agent,
5410
5891
  skillInstalled: installedSkill !== void 0,
5411
- skillCurrent: installedSkill === sourceSkill,
5892
+ skillCurrent: !skillPath || (agent === "cursor" || agent === "windsurf" ? Boolean(installedSkill?.includes("Topolo")) : installedSkill === sourceSkill),
5893
+ skillSupported: Boolean(skillPath),
5412
5894
  skillPath,
5413
5895
  cliAvailable: agentBinaryExists(agent),
5414
5896
  registrations
@@ -5420,14 +5902,14 @@ async function uninstallAgentSurfaces(options) {
5420
5902
  const name = options.name?.trim() || "topolo";
5421
5903
  resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
5422
5904
  const removed = [];
5423
- for (const agent of resolveTargets(options)) {
5424
- const before = await readAgentRegistrations(agent, homeDir);
5905
+ for (const agent of resolveTargets({ ...options, homeDir, project: options.project })) {
5906
+ const before = await readAgentRegistrations(agent, homeDir, options.project);
5425
5907
  const registrationRemoved = before.some((entry) => entry.name === name);
5426
- await unregisterFromConfig(agent, homeDir, name);
5427
- const remaining = await readAgentRegistrations(agent, homeDir);
5428
- const skillPath = agentSkillPath(agent, homeDir);
5429
- const skillRemoved = remaining.length === 0 && existsSync3(skillPath);
5430
- if (skillRemoved) await rm2(dirname3(skillPath), { recursive: true, force: true });
5908
+ await unregisterAgentConfig(agent, homeDir, name, options.project);
5909
+ const remaining = await readAgentRegistrations(agent, homeDir, options.project);
5910
+ const skillPath = agentSkillPath(agent, homeDir, options.project);
5911
+ const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync4(skillPath));
5912
+ if (skillRemoved && skillPath) await rm2(dirname4(skillPath), { recursive: true, force: true });
5431
5913
  removed.push({ agent, name, registrationRemoved, skillRemoved });
5432
5914
  }
5433
5915
  return removed;
@@ -5436,8 +5918,8 @@ function formatInstall(results, mcp) {
5436
5918
  const lines = [];
5437
5919
  const mcpStr = [mcp.command, ...mcp.args].join(" ");
5438
5920
  for (const r of results) {
5439
- lines.push(`${AGENT_LABELS[r.agent]}:`);
5440
- lines.push(` skill -> ${r.skillPath}`);
5921
+ lines.push(`${AGENT_LABELS2[r.agent]}:`);
5922
+ lines.push(r.skillPath ? ` skill -> ${r.skillPath}` : " skill -> host-managed (MCP only)");
5441
5923
  if (r.mcp === "official-cli") lines.push(` MCP registered via \`${r.agent} mcp add\` (${mcpStr})`);
5442
5924
  else if (r.mcp === "config-file") lines.push(` MCP registered by editing the config file (${mcpStr})`);
5443
5925
  else if (r.mcp === "skipped") lines.push(" MCP registration skipped (--no-mcp)");
@@ -5451,7 +5933,7 @@ function formatInstall(results, mcp) {
5451
5933
  }
5452
5934
  function formatStatus(rows) {
5453
5935
  return rows.map(
5454
- (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"}`
5936
+ (r) => `${AGENT_LABELS2[r.agent]}: skill ${!r.skillPath ? "host-managed" : r.skillInstalled ? r.skillCurrent ? "current" : "stale" : "not installed"} (${r.skillPath ?? "native host surface"}), ${r.agent} CLI ${r.cliAvailable ? "available" : "not found"}, MCP registrations: ${r.registrations.map((registration) => registration.name).join(", ") || "none"}`
5455
5937
  ).join("\n");
5456
5938
  }
5457
5939
 
@@ -5522,15 +6004,19 @@ async function resolveInstalledDoctorTarget(options) {
5522
6004
  const name = options.name?.trim() || "topolo";
5523
6005
  const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir4();
5524
6006
  const expected = resolveMcpCommand();
5525
- const requestedAgents = options.claude && !options.codex ? ["claude"] : options.codex && !options.claude ? ["codex"] : ["claude", "codex"];
5526
- const rows = (await agentSurfaceStatus({ homeDir, name })).filter((row) => requestedAgents.includes(row.agent));
6007
+ const requestedAgents = resolveTargets({
6008
+ ...options,
6009
+ homeDir,
6010
+ project: options.project
6011
+ });
6012
+ const rows = (await agentSurfaceStatus({ homeDir, name, agent: requestedAgents, project: options.project })).filter((row) => requestedAgents.includes(row.agent));
5527
6013
  const preflightChecks = [];
5528
6014
  let selected = rows.flatMap((row) => row.registrations)[0];
5529
6015
  for (const row of rows) {
5530
6016
  preflightChecks.push({
5531
6017
  name: "skill",
5532
- ok: row.skillCurrent,
5533
- detail: `${row.agent}: ${row.skillCurrent ? "bundled skill is current" : row.skillInstalled ? "installed skill is stale" : "skill is missing"}`
6018
+ ok: !row.skillPath || row.skillCurrent,
6019
+ detail: `${row.agent}: ${!row.skillPath ? "host-managed skill surface" : row.skillCurrent ? "bundled skill is current" : row.skillInstalled ? "installed skill is stale" : "skill is missing"}`
5534
6020
  });
5535
6021
  const registration = row.registrations.find((entry) => entry.name === name);
5536
6022
  const exact = Boolean(
@@ -5557,7 +6043,7 @@ function definedEnvironment(environment) {
5557
6043
  return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
5558
6044
  }
5559
6045
  function registerDoctorCommand(program2) {
5560
- 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) => {
6046
+ 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 <host...>", "Verify one or more hosts by id").option("--all", "Verify every supported local host").option("--project <dir>", "Inspect project-scoped host files under <dir>").option("--agent-home <dir>", "Inspect an alternate agent home").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5561
6047
  const mode = resolveOutputMode(opts.json);
5562
6048
  try {
5563
6049
  const target = await resolveInstalledDoctorTarget(opts);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.9.8",
4
- "topoloPublishedVersion": "0.9.8",
3
+ "version": "0.10.0",
4
+ "topoloPublishedVersion": "0.10.0",
5
5
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
6
6
  "license": "UNLICENSED",
7
7
  "author": "Topolo",
package/postinstall.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  const message = [
2
2
  'Topolo CLI installed successfully.',
3
3
  '',
4
- 'AI assistant: continue automatically with these setup actions:',
4
+ 'AI assistant: continue this onboarding automatically:',
5
5
  ' 1. Run: topolo setup',
6
+ ' (Use --all only when every supported local host should be configured.)',
6
7
  ' 2. Authenticate: topolo auth login',
7
- ' 3. Inspect contexts: topolo context ls --json',
8
- ' 4. Verify: topolo doctor',
8
+ ' 3. Verify: topolo doctor',
9
9
  '',
10
+ 'The setup command detects native config files for Claude Code/Desktop, Codex, Cursor, Grok Build, Gemini CLI, GitHub Copilot/VS Code, Windsurf, Cline, Goose, and Amazon Q Developer.',
10
11
  'If multiple contexts are listed, use the exact intended context returned by the CLI; do not guess organization IDs.',
12
+ 'JetBrains AI Assistant and web-only connectors use their own UI or remote connector setup and are not edited by this local installer.',
11
13
  'Only pause for the browser sign-in or an operating-system permission prompt.',
12
14
  ].join('\n');
13
15
 
@@ -19,7 +19,7 @@ topolo setup
19
19
  topolo doctor
20
20
  ```
21
21
 
22
- `topolo setup` installs this skill and registers the exact MCP server bundled with the CLI for Claude Code and Codex. It does not depend on `npx`, `PATH`, a source checkout, or a second global package. Both surfaces use the same credential written by `topolo auth login`. `topolo doctor` then verifies the credential, MCP protocol handshake, tool discovery, and an identity call end to end.
22
+ `topolo setup` detects supported local hosts and registers the exact MCP server bundled with the CLI using each host's native configuration format. It supports Claude Code, Claude Desktop, Codex, Cursor, Grok Build, Gemini CLI, GitHub Copilot/VS Code, Windsurf, Cline, Goose, and Amazon Q Developer. It does not depend on `npx`, a source checkout, or a second global package. All local surfaces use the same credential written by `topolo auth login`. `topolo doctor` then verifies the credential, MCP protocol handshake, tool discovery, and an identity call end to end. JetBrains AI Assistant and web-only connectors use their own UI or remote connector configuration.
23
23
 
24
24
  Before diagnosing behavior, verify the installed customer tooling against the package registry:
25
25