@topolo/cli 0.9.9 → 0.10.1
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 +16 -6
- package/dist/index.js +564 -168
- package/package.json +9 -4
- package/postinstall.mjs +5 -3
- package/skills/topolo/SKILL.md +1 -1
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
|
|
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
|
|
65
|
+
### Wire into agent hosts
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
|
-
topolo setup #
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
@@ -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
|
-
//
|
|
12
|
+
// node_modules/.pnpm/@topolo+sdk@0.14.4/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);
|
|
@@ -2948,7 +2948,7 @@ function createTopolo(options) {
|
|
|
2948
2948
|
};
|
|
2949
2949
|
}
|
|
2950
2950
|
|
|
2951
|
-
//
|
|
2951
|
+
// node_modules/.pnpm/@topolo+sdk@0.14.4/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
2952
2952
|
import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
|
|
2953
2953
|
import { existsSync } from "fs";
|
|
2954
2954
|
import { homedir, platform } from "os";
|
|
@@ -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
|
|
5059
|
-
import {
|
|
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
|
|
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,28 +5122,162 @@ function stableNodeCommand() {
|
|
|
5083
5122
|
});
|
|
5084
5123
|
return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
|
|
5085
5124
|
}
|
|
5086
|
-
function agentSkillPath(agent, homeDir = homedir2()) {
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
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
|
|
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(
|
|
5274
|
+
throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
|
|
5100
5275
|
}
|
|
5101
|
-
const servers = parsed
|
|
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
|
|
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;
|
|
5110
5283
|
let pending;
|
|
@@ -5112,11 +5285,11 @@ async function readCodexRegistrations(homeDir) {
|
|
|
5112
5285
|
const line = stripTomlComment(rawLine).trim();
|
|
5113
5286
|
if (pending) {
|
|
5114
5287
|
if (line.startsWith("[")) {
|
|
5115
|
-
throw new Error(
|
|
5288
|
+
throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
|
|
5116
5289
|
}
|
|
5117
|
-
pending.value =
|
|
5290
|
+
pending.value = pending.value + " " + line;
|
|
5118
5291
|
if (isTomlValueComplete(pending.value)) {
|
|
5119
|
-
|
|
5292
|
+
applyTomlAssignment(path, agent, section, pending, registrations);
|
|
5120
5293
|
pending = void 0;
|
|
5121
5294
|
}
|
|
5122
5295
|
continue;
|
|
@@ -5136,29 +5309,90 @@ async function readCodexRegistrations(homeDir) {
|
|
|
5136
5309
|
if (!section || !isPublicRegistrationName(section.name) || !line || line.startsWith("#")) continue;
|
|
5137
5310
|
const assignment = line.match(/^([A-Za-z0-9_]+)\s*=\s*(.+)$/);
|
|
5138
5311
|
if (!assignment) continue;
|
|
5139
|
-
if (!section.env && assignment[1] !== "command" && assignment[1] !== "args") continue;
|
|
5312
|
+
if (!section.env && assignment[1] !== "command" && assignment[1] !== "args" && assignment[1] !== "env") continue;
|
|
5140
5313
|
pending = { key: assignment[1], value: assignment[2] };
|
|
5141
5314
|
if (isTomlValueComplete(pending.value)) {
|
|
5142
|
-
|
|
5315
|
+
applyTomlAssignment(path, agent, section, pending, registrations);
|
|
5143
5316
|
pending = void 0;
|
|
5144
5317
|
}
|
|
5145
5318
|
}
|
|
5146
|
-
if (pending) throw new Error(
|
|
5319
|
+
if (pending) throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
|
|
5147
5320
|
return [...registrations.values()].filter((registration) => registration.command);
|
|
5148
5321
|
}
|
|
5149
|
-
function
|
|
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('"')) {
|
|
5367
|
+
try {
|
|
5368
|
+
return JSON.parse(trimmed);
|
|
5369
|
+
} catch {
|
|
5370
|
+
return trimmed.slice(1, -1);
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
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) {
|
|
5150
5379
|
const registration = registrations.get(section.name);
|
|
5151
5380
|
let value;
|
|
5152
5381
|
try {
|
|
5153
5382
|
value = parseTomlValue(assignment.value);
|
|
5154
5383
|
} catch {
|
|
5155
|
-
throw new Error(
|
|
5384
|
+
throw new Error("Cannot read unsupported MCP value in " + AGENT_LABELS[agent] + " configuration at " + path + ".");
|
|
5156
5385
|
}
|
|
5157
5386
|
if (section.env && typeof value === "string") registration.env[assignment.key] = value;
|
|
5158
5387
|
if (!section.env && assignment.key === "command" && typeof value === "string") registration.command = value;
|
|
5159
5388
|
if (!section.env && assignment.key === "args" && Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
5160
5389
|
registration.args = value;
|
|
5161
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
|
+
}
|
|
5162
5396
|
}
|
|
5163
5397
|
function stripTomlComment(line) {
|
|
5164
5398
|
let quote;
|
|
@@ -5185,6 +5419,7 @@ function isTomlValueComplete(value) {
|
|
|
5185
5419
|
let quote;
|
|
5186
5420
|
let escaped = false;
|
|
5187
5421
|
let arrayDepth = 0;
|
|
5422
|
+
let tableDepth = 0;
|
|
5188
5423
|
for (let index = 0; index < value.length; index += 1) {
|
|
5189
5424
|
const character = value[index];
|
|
5190
5425
|
if (quote === '"') {
|
|
@@ -5201,8 +5436,10 @@ function isTomlValueComplete(value) {
|
|
|
5201
5436
|
if (character === '"' || character === "'") quote = character;
|
|
5202
5437
|
else if (character === "[") arrayDepth += 1;
|
|
5203
5438
|
else if (character === "]") arrayDepth -= 1;
|
|
5439
|
+
else if (character === "{") tableDepth += 1;
|
|
5440
|
+
else if (character === "}") tableDepth -= 1;
|
|
5204
5441
|
}
|
|
5205
|
-
return !quote && arrayDepth === 0;
|
|
5442
|
+
return !quote && arrayDepth === 0 && tableDepth === 0;
|
|
5206
5443
|
}
|
|
5207
5444
|
function parseTomlValue(rawValue) {
|
|
5208
5445
|
const value = rawValue.trim();
|
|
@@ -5216,6 +5453,19 @@ function parseTomlValue(rawValue) {
|
|
|
5216
5453
|
if (!body) return [];
|
|
5217
5454
|
return splitTomlArray(body).map((item) => parseTomlValue(item));
|
|
5218
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
|
+
}
|
|
5219
5469
|
throw new Error("Unsupported TOML value");
|
|
5220
5470
|
}
|
|
5221
5471
|
function splitTomlArray(value) {
|
|
@@ -5224,6 +5474,7 @@ function splitTomlArray(value) {
|
|
|
5224
5474
|
let quote;
|
|
5225
5475
|
let escaped = false;
|
|
5226
5476
|
let arrayDepth = 0;
|
|
5477
|
+
let tableDepth = 0;
|
|
5227
5478
|
for (let index = 0; index < value.length; index += 1) {
|
|
5228
5479
|
const character = value[index];
|
|
5229
5480
|
if (quote === '"') {
|
|
@@ -5240,7 +5491,9 @@ function splitTomlArray(value) {
|
|
|
5240
5491
|
if (character === '"' || character === "'") quote = character;
|
|
5241
5492
|
else if (character === "[") arrayDepth += 1;
|
|
5242
5493
|
else if (character === "]") arrayDepth -= 1;
|
|
5243
|
-
else if (character === "
|
|
5494
|
+
else if (character === "{") tableDepth += 1;
|
|
5495
|
+
else if (character === "}") tableDepth -= 1;
|
|
5496
|
+
else if (character === "," && arrayDepth === 0 && tableDepth === 0) {
|
|
5244
5497
|
const item = value.slice(start, index).trim();
|
|
5245
5498
|
if (item) items.push(item);
|
|
5246
5499
|
start = index + 1;
|
|
@@ -5262,50 +5515,179 @@ function message(error) {
|
|
|
5262
5515
|
return error instanceof Error ? error.message : String(error);
|
|
5263
5516
|
}
|
|
5264
5517
|
|
|
5265
|
-
// src/commands/
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
const
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
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
|
+
}
|
|
5274
5538
|
}
|
|
5275
|
-
return distCandidate;
|
|
5276
5539
|
}
|
|
5277
|
-
function
|
|
5278
|
-
const
|
|
5279
|
-
|
|
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
|
+
}
|
|
5280
5552
|
}
|
|
5281
|
-
function
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
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
|
+
};
|
|
5286
5558
|
}
|
|
5287
|
-
function
|
|
5288
|
-
|
|
5289
|
-
|
|
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");
|
|
5290
5585
|
}
|
|
5291
|
-
function
|
|
5292
|
-
|
|
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));
|
|
5293
5630
|
}
|
|
5294
|
-
async function
|
|
5295
|
-
await
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5631
|
+
async function unregisterGoose(path, name) {
|
|
5632
|
+
const existing = await readFile4(path, "utf8");
|
|
5633
|
+
await atomicWrite(path, removeGooseExtension(existing, name));
|
|
5634
|
+
}
|
|
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;
|
|
5299
5681
|
}
|
|
5300
5682
|
async function atomicWrite(path, contents) {
|
|
5301
5683
|
await mkdir2(dirname3(path), { recursive: true });
|
|
5302
|
-
const temporaryPath =
|
|
5684
|
+
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
5303
5685
|
await writeFile2(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
5304
5686
|
await rename(temporaryPath, path);
|
|
5305
5687
|
}
|
|
5306
5688
|
function withoutMcpServerSections(text, name) {
|
|
5307
|
-
const section =
|
|
5308
|
-
const environmentSection =
|
|
5689
|
+
const section = "[mcp_servers." + name + "]";
|
|
5690
|
+
const environmentSection = "[mcp_servers." + name + ".env]";
|
|
5309
5691
|
let skipping = false;
|
|
5310
5692
|
return text.split(/\r?\n/).filter((line) => {
|
|
5311
5693
|
const trimmed = line.trim();
|
|
@@ -5317,48 +5699,29 @@ function withoutMcpServerSections(text, name) {
|
|
|
5317
5699
|
return !skipping;
|
|
5318
5700
|
}).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
5319
5701
|
}
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
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;
|
|
5329
5711
|
}
|
|
5330
|
-
|
|
5331
|
-
servers[registration.name] = {
|
|
5332
|
-
type: "stdio",
|
|
5333
|
-
command: mcp.command,
|
|
5334
|
-
args: mcp.args,
|
|
5335
|
-
env: registrationEnvironment(AGENT_NAME_FOR.claude, registration)
|
|
5336
|
-
};
|
|
5337
|
-
config.mcpServers = servers;
|
|
5338
|
-
await atomicWrite(path, `${JSON.stringify(config, null, 2)}
|
|
5339
|
-
`);
|
|
5712
|
+
return distCandidate;
|
|
5340
5713
|
}
|
|
5341
|
-
|
|
5342
|
-
const
|
|
5343
|
-
|
|
5344
|
-
if (existsSync3(path)) text = await readFile4(path, "utf8");
|
|
5345
|
-
text = withoutMcpServerSections(text, registration.name);
|
|
5346
|
-
const argsToml = mcp.args.length ? `
|
|
5347
|
-
args = [${mcp.args.map((a) => `"${a}"`).join(", ")}]` : "";
|
|
5348
|
-
const block = `
|
|
5349
|
-
|
|
5350
|
-
[mcp_servers.${registration.name}]
|
|
5351
|
-
command = ${JSON.stringify(mcp.command)}${argsToml}
|
|
5352
|
-
|
|
5353
|
-
[mcp_servers.${registration.name}.env]
|
|
5354
|
-
` + Object.entries(registrationEnvironment(AGENT_NAME_FOR.codex, registration)).map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join("\n") + "\n";
|
|
5355
|
-
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")] };
|
|
5356
5717
|
}
|
|
5357
|
-
function
|
|
5358
|
-
return {
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
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
|
+
});
|
|
5362
5725
|
}
|
|
5363
5726
|
function resolveMcpRegistration(options) {
|
|
5364
5727
|
const name = options.name?.trim() || "topolo";
|
|
@@ -5382,57 +5745,54 @@ function resolveMcpRegistration(options) {
|
|
|
5382
5745
|
}
|
|
5383
5746
|
return { name, ...context ? { context } : {}, environment };
|
|
5384
5747
|
}
|
|
5385
|
-
async function unregisterFromConfig(agent, homeDir, name) {
|
|
5386
|
-
if (agent === "claude") {
|
|
5387
|
-
const path2 = join5(homeDir, ".claude.json");
|
|
5388
|
-
if (!existsSync3(path2)) return;
|
|
5389
|
-
const config = JSON.parse(await readFile4(path2, "utf8"));
|
|
5390
|
-
const servers = config.mcpServers ?? {};
|
|
5391
|
-
delete servers[name];
|
|
5392
|
-
config.mcpServers = servers;
|
|
5393
|
-
await atomicWrite(path2, `${JSON.stringify(config, null, 2)}
|
|
5394
|
-
`);
|
|
5395
|
-
return;
|
|
5396
|
-
}
|
|
5397
|
-
const path = join5(homeDir, ".codex", "config.toml");
|
|
5398
|
-
if (!existsSync3(path)) return;
|
|
5399
|
-
const text = withoutMcpServerSections(await readFile4(path, "utf8"), name);
|
|
5400
|
-
await atomicWrite(path, text ? `${text}
|
|
5401
|
-
` : "");
|
|
5402
|
-
}
|
|
5403
5748
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
5404
|
-
const
|
|
5405
|
-
const
|
|
5749
|
+
const source = await readFile5(join5(sourceDir, "topolo", "SKILL.md"), "utf8");
|
|
5750
|
+
const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
|
|
5406
5751
|
if (!opts.withMcp) {
|
|
5407
|
-
return { agent, skillPath, skillInstalled:
|
|
5752
|
+
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
|
|
5408
5753
|
}
|
|
5409
5754
|
try {
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
const
|
|
5413
|
-
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);
|
|
5414
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)) {
|
|
5415
5759
|
throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
|
|
5416
5760
|
}
|
|
5417
|
-
return { agent, skillPath, skillInstalled:
|
|
5761
|
+
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "config-file" };
|
|
5418
5762
|
} catch (err) {
|
|
5419
5763
|
return {
|
|
5420
5764
|
agent,
|
|
5421
|
-
skillPath,
|
|
5422
|
-
skillInstalled:
|
|
5765
|
+
skillPath: skill.path,
|
|
5766
|
+
skillInstalled: skill.installed,
|
|
5423
5767
|
mcp: "failed",
|
|
5424
5768
|
detail: err instanceof Error ? err.message : String(err)
|
|
5425
5769
|
};
|
|
5426
5770
|
}
|
|
5427
5771
|
}
|
|
5428
5772
|
function resolveTargets(opts) {
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
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])];
|
|
5432
5792
|
}
|
|
5433
5793
|
function registerSkillsCommand(program2) {
|
|
5434
|
-
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into
|
|
5435
|
-
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>", "
|
|
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(
|
|
5436
5796
|
async (opts) => {
|
|
5437
5797
|
const mode = resolveOutputMode(opts.json);
|
|
5438
5798
|
try {
|
|
@@ -5444,19 +5804,22 @@ function registerSkillsCommand(program2) {
|
|
|
5444
5804
|
}
|
|
5445
5805
|
}
|
|
5446
5806
|
);
|
|
5447
|
-
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) => {
|
|
5448
5808
|
const mode = resolveOutputMode(opts.json);
|
|
5449
5809
|
try {
|
|
5450
5810
|
const rows = await agentSurfaceStatus({
|
|
5451
5811
|
homeDir: opts.agentHome ? resolve(opts.agentHome) : void 0,
|
|
5452
|
-
name: opts.name
|
|
5812
|
+
name: opts.name,
|
|
5813
|
+
agent: opts.agent,
|
|
5814
|
+
all: opts.all,
|
|
5815
|
+
project: opts.project
|
|
5453
5816
|
});
|
|
5454
5817
|
emit({ agents: rows }, mode, () => formatStatus(rows));
|
|
5455
5818
|
} catch (err) {
|
|
5456
5819
|
fail(err, mode);
|
|
5457
5820
|
}
|
|
5458
5821
|
});
|
|
5459
|
-
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) => {
|
|
5460
5823
|
const mode = resolveOutputMode(opts.json);
|
|
5461
5824
|
try {
|
|
5462
5825
|
const removed = await uninstallAgentSurfaces({
|
|
@@ -5466,7 +5829,7 @@ function registerSkillsCommand(program2) {
|
|
|
5466
5829
|
emit(
|
|
5467
5830
|
{ removed },
|
|
5468
5831
|
mode,
|
|
5469
|
-
() => removed.map((r) => `${
|
|
5832
|
+
() => removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
|
|
5470
5833
|
);
|
|
5471
5834
|
} catch (err) {
|
|
5472
5835
|
fail(err, mode);
|
|
@@ -5475,14 +5838,14 @@ function registerSkillsCommand(program2) {
|
|
|
5475
5838
|
}
|
|
5476
5839
|
async function installAgentSurfaces(opts) {
|
|
5477
5840
|
const sourceDir = skillsSourceDir();
|
|
5478
|
-
if (!
|
|
5841
|
+
if (!existsSync4(join5(sourceDir, "topolo", "SKILL.md"))) {
|
|
5479
5842
|
throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
|
|
5480
5843
|
}
|
|
5481
5844
|
const mcp = resolveMcpCommand();
|
|
5482
5845
|
const registration = resolveMcpRegistration(opts);
|
|
5483
5846
|
const homeDir = opts.homeDir ?? homedir3();
|
|
5484
5847
|
const results = [];
|
|
5485
|
-
for (const agent of resolveTargets(opts)) {
|
|
5848
|
+
for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
|
|
5486
5849
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
5487
5850
|
withMcp: opts.mcp !== false,
|
|
5488
5851
|
projectDir: opts.project,
|
|
@@ -5493,7 +5856,7 @@ async function installAgentSurfaces(opts) {
|
|
|
5493
5856
|
return { results, mcp };
|
|
5494
5857
|
}
|
|
5495
5858
|
function registerSetupCommand(program2) {
|
|
5496
|
-
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) => {
|
|
5497
5860
|
const mode = resolveOutputMode(opts.json);
|
|
5498
5861
|
try {
|
|
5499
5862
|
const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
|
|
@@ -5507,20 +5870,27 @@ function registerSetupCommand(program2) {
|
|
|
5507
5870
|
function assertAgentSetupSucceeded(results) {
|
|
5508
5871
|
const failed = results.filter((result) => result.mcp === "failed");
|
|
5509
5872
|
if (failed.length) {
|
|
5510
|
-
throw new Error(failed.map((result) => `${
|
|
5873
|
+
throw new Error(failed.map((result) => `${AGENT_LABELS2[result.agent]}: ${result.detail ?? "MCP registration failed"}`).join("; "));
|
|
5511
5874
|
}
|
|
5512
5875
|
}
|
|
5513
5876
|
async function agentSurfaceStatus(options = {}) {
|
|
5514
5877
|
const homeDir = options.homeDir ?? homedir3();
|
|
5515
|
-
const sourceSkill = await
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
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);
|
|
5520
5889
|
return {
|
|
5521
5890
|
agent,
|
|
5522
5891
|
skillInstalled: installedSkill !== void 0,
|
|
5523
|
-
skillCurrent: installedSkill === sourceSkill,
|
|
5892
|
+
skillCurrent: !skillPath || (agent === "cursor" || agent === "windsurf" ? Boolean(installedSkill?.includes("Topolo")) : installedSkill === sourceSkill),
|
|
5893
|
+
skillSupported: Boolean(skillPath),
|
|
5524
5894
|
skillPath,
|
|
5525
5895
|
cliAvailable: agentBinaryExists(agent),
|
|
5526
5896
|
registrations
|
|
@@ -5532,14 +5902,14 @@ async function uninstallAgentSurfaces(options) {
|
|
|
5532
5902
|
const name = options.name?.trim() || "topolo";
|
|
5533
5903
|
resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
|
|
5534
5904
|
const removed = [];
|
|
5535
|
-
for (const agent of resolveTargets(options)) {
|
|
5536
|
-
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);
|
|
5537
5907
|
const registrationRemoved = before.some((entry) => entry.name === name);
|
|
5538
|
-
await
|
|
5539
|
-
const remaining = await readAgentRegistrations(agent, homeDir);
|
|
5540
|
-
const skillPath = agentSkillPath(agent, homeDir);
|
|
5541
|
-
const skillRemoved = remaining.length === 0 &&
|
|
5542
|
-
if (skillRemoved) await rm2(
|
|
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 });
|
|
5543
5913
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
5544
5914
|
}
|
|
5545
5915
|
return removed;
|
|
@@ -5548,8 +5918,8 @@ function formatInstall(results, mcp) {
|
|
|
5548
5918
|
const lines = [];
|
|
5549
5919
|
const mcpStr = [mcp.command, ...mcp.args].join(" ");
|
|
5550
5920
|
for (const r of results) {
|
|
5551
|
-
lines.push(`${
|
|
5552
|
-
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)");
|
|
5553
5923
|
if (r.mcp === "official-cli") lines.push(` MCP registered via \`${r.agent} mcp add\` (${mcpStr})`);
|
|
5554
5924
|
else if (r.mcp === "config-file") lines.push(` MCP registered by editing the config file (${mcpStr})`);
|
|
5555
5925
|
else if (r.mcp === "skipped") lines.push(" MCP registration skipped (--no-mcp)");
|
|
@@ -5563,7 +5933,7 @@ function formatInstall(results, mcp) {
|
|
|
5563
5933
|
}
|
|
5564
5934
|
function formatStatus(rows) {
|
|
5565
5935
|
return rows.map(
|
|
5566
|
-
(r) => `${
|
|
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"}`
|
|
5567
5937
|
).join("\n");
|
|
5568
5938
|
}
|
|
5569
5939
|
|
|
@@ -5634,15 +6004,19 @@ async function resolveInstalledDoctorTarget(options) {
|
|
|
5634
6004
|
const name = options.name?.trim() || "topolo";
|
|
5635
6005
|
const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir4();
|
|
5636
6006
|
const expected = resolveMcpCommand();
|
|
5637
|
-
const requestedAgents =
|
|
5638
|
-
|
|
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));
|
|
5639
6013
|
const preflightChecks = [];
|
|
5640
6014
|
let selected = rows.flatMap((row) => row.registrations)[0];
|
|
5641
6015
|
for (const row of rows) {
|
|
5642
6016
|
preflightChecks.push({
|
|
5643
6017
|
name: "skill",
|
|
5644
|
-
ok: row.skillCurrent,
|
|
5645
|
-
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"}`
|
|
5646
6020
|
});
|
|
5647
6021
|
const registration = row.registrations.find((entry) => entry.name === name);
|
|
5648
6022
|
const exact = Boolean(
|
|
@@ -5669,7 +6043,7 @@ function definedEnvironment(environment) {
|
|
|
5669
6043
|
return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
|
|
5670
6044
|
}
|
|
5671
6045
|
function registerDoctorCommand(program2) {
|
|
5672
|
-
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) => {
|
|
5673
6047
|
const mode = resolveOutputMode(opts.json);
|
|
5674
6048
|
try {
|
|
5675
6049
|
const target = await resolveInstalledDoctorTarget(opts);
|
|
@@ -5713,8 +6087,15 @@ async function fetchMe(credential, serviceUrls) {
|
|
|
5713
6087
|
const user = data.user ?? {};
|
|
5714
6088
|
const rawMemberships = user.organizationMemberships || data.organizations || [];
|
|
5715
6089
|
const memberships = rawMemberships.filter((m) => m && m.id);
|
|
5716
|
-
const
|
|
5717
|
-
|
|
6090
|
+
const availableContexts = Array.isArray(user.availableContexts) ? user.availableContexts : [];
|
|
6091
|
+
const activeType = user.activeContextType === "personal" ? "personal" : "organization";
|
|
6092
|
+
const activeOrgId = activeType === "organization" ? user.activeContextId || void 0 : void 0;
|
|
6093
|
+
return {
|
|
6094
|
+
memberships,
|
|
6095
|
+
activeType,
|
|
6096
|
+
activeOrgId,
|
|
6097
|
+
personalAvailable: availableContexts.some((candidate) => candidate?.type === "personal")
|
|
6098
|
+
};
|
|
5718
6099
|
}
|
|
5719
6100
|
function credentialFromTokens(tokens) {
|
|
5720
6101
|
const accessToken = tokens.accessToken || tokens.access_token;
|
|
@@ -5758,7 +6139,7 @@ function hasSameAccessToken(left, right) {
|
|
|
5758
6139
|
return leftCredential?.kind === "access_token" && rightCredential?.kind === "access_token" && leftCredential.accessToken === rightCredential.accessToken;
|
|
5759
6140
|
}
|
|
5760
6141
|
function identityMatchesContext(me, context) {
|
|
5761
|
-
if (context.contextType === "personal") return me.activeType === "personal";
|
|
6142
|
+
if (context.contextType === "personal") return me.personalAvailable && me.activeType === "personal";
|
|
5762
6143
|
return me.activeType === "organization" && me.activeOrgId === context.organizationId;
|
|
5763
6144
|
}
|
|
5764
6145
|
function registerContextCommand(program2) {
|
|
@@ -5773,6 +6154,7 @@ function registerContextCommand(program2) {
|
|
|
5773
6154
|
const activeKey = profile.activeContext;
|
|
5774
6155
|
const rows = /* @__PURE__ */ new Map();
|
|
5775
6156
|
for (const [key, ctx] of Object.entries(stored)) {
|
|
6157
|
+
if (ctx.contextType === "personal") continue;
|
|
5776
6158
|
rows.set(key, {
|
|
5777
6159
|
key,
|
|
5778
6160
|
type: ctx.contextType,
|
|
@@ -5798,15 +6180,22 @@ function registerContextCommand(program2) {
|
|
|
5798
6180
|
if (!contextCredential) return;
|
|
5799
6181
|
const identity = key === activeKey && activeIdentity ? activeIdentity : await fetchMe(contextCredential, serviceUrls);
|
|
5800
6182
|
if (!identityMatchesContext(identity, ctx)) return;
|
|
5801
|
-
const row = rows.get(key)
|
|
5802
|
-
|
|
6183
|
+
const row = rows.get(key) ?? {
|
|
6184
|
+
key,
|
|
6185
|
+
type: ctx.contextType,
|
|
6186
|
+
label: labelFor(ctx),
|
|
6187
|
+
signedIn: false,
|
|
6188
|
+
available: false,
|
|
6189
|
+
active: key === activeKey
|
|
6190
|
+
};
|
|
6191
|
+
rows.set(key, { ...row, signedIn: true, available: true });
|
|
5803
6192
|
} catch {
|
|
5804
6193
|
}
|
|
5805
6194
|
}));
|
|
5806
6195
|
if (activeIdentity) {
|
|
5807
6196
|
try {
|
|
5808
6197
|
const personalKey = contextKey("personal");
|
|
5809
|
-
if (!rows.has(personalKey)) {
|
|
6198
|
+
if (activeIdentity.personalAvailable && !rows.has(personalKey)) {
|
|
5810
6199
|
rows.set(personalKey, {
|
|
5811
6200
|
key: personalKey,
|
|
5812
6201
|
type: "personal",
|
|
@@ -5815,7 +6204,7 @@ function registerContextCommand(program2) {
|
|
|
5815
6204
|
available: true,
|
|
5816
6205
|
active: personalKey === activeKey
|
|
5817
6206
|
});
|
|
5818
|
-
} else {
|
|
6207
|
+
} else if (activeIdentity.personalAvailable) {
|
|
5819
6208
|
const row = rows.get(personalKey);
|
|
5820
6209
|
rows.set(personalKey, { ...row, available: true });
|
|
5821
6210
|
}
|
|
@@ -5891,8 +6280,9 @@ function registerContextCommand(program2) {
|
|
|
5891
6280
|
);
|
|
5892
6281
|
if (!match) {
|
|
5893
6282
|
const names = me.memberships.map((m) => m.slug || m.name || m.id);
|
|
6283
|
+
const available = [me.personalAvailable ? "personal" : "", ...names].filter(Boolean);
|
|
5894
6284
|
throw new Error(
|
|
5895
|
-
`You are not a member of "${target}". Available:
|
|
6285
|
+
`You are not a member of "${target}". Available: ${available.join(", ") || "none"}.`
|
|
5896
6286
|
);
|
|
5897
6287
|
}
|
|
5898
6288
|
resolved = {
|
|
@@ -5918,6 +6308,12 @@ function registerContextCommand(program2) {
|
|
|
5918
6308
|
}
|
|
5919
6309
|
}
|
|
5920
6310
|
if (!newCredential) {
|
|
6311
|
+
if (resolved.contextType === "personal") {
|
|
6312
|
+
const identity = await fetchMe(cred, serviceUrls);
|
|
6313
|
+
if (!identity.personalAvailable) {
|
|
6314
|
+
throw new Error("Personal context is not available for this account.");
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
5921
6317
|
try {
|
|
5922
6318
|
newCredential = await exchangeForContext(cred, serviceUrls, {
|
|
5923
6319
|
type: resolved.contextType,
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"packageManager": "pnpm@10.17.1",
|
|
4
|
+
"version": "0.10.1",
|
|
5
|
+
"topoloPublishedVersion": "0.10.1",
|
|
5
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
6
7
|
"license": "UNLICENSED",
|
|
7
8
|
"author": "Topolo",
|
|
@@ -21,9 +22,9 @@
|
|
|
21
22
|
"dev": "tsup --watch",
|
|
22
23
|
"typecheck": "tsc --noEmit",
|
|
23
24
|
"start": "node dist/index.js",
|
|
24
|
-
"test": "
|
|
25
|
+
"test": "vitest run",
|
|
25
26
|
"test:watch": "vitest",
|
|
26
|
-
"coverage": "
|
|
27
|
+
"coverage": "vitest run --coverage"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -40,5 +41,9 @@
|
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/Topolo-io/TopoloCli.git"
|
|
43
48
|
}
|
|
44
49
|
}
|
package/postinstall.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const message = [
|
|
2
2
|
'Topolo CLI installed successfully.',
|
|
3
3
|
'',
|
|
4
|
-
'AI assistant: continue
|
|
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.
|
|
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
|
|
package/skills/topolo/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ topolo setup
|
|
|
19
19
|
topolo doctor
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`topolo setup`
|
|
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
|
|