@topolo/cli 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.js +57 -16
- package/package.json +2 -2
- package/skills/topolo/SKILL.md +9 -2
package/README.md
CHANGED
|
@@ -78,6 +78,17 @@ topolo doctor # verifies auth, handshake, tool discovery, and identit
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
The MCP reads the same credential `topolo auth login` writes — no API key to paste.
|
|
81
|
+
For concurrent agents pinned to different organizations, use an exact context
|
|
82
|
+
key returned by `topolo context ls --json`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
topolo setup --name topolo-acme --context org:org_acme --env production
|
|
86
|
+
topolo doctor --context org:org_acme --env production
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This creates separately named Claude Code and Codex registrations using the
|
|
90
|
+
same exact nested MCP executable. `topolo-platform` is reserved for the
|
|
91
|
+
restricted operator toolchain.
|
|
81
92
|
|
|
82
93
|
## Cross-org isolation
|
|
83
94
|
|
package/dist/index.js
CHANGED
|
@@ -4273,6 +4273,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
4273
4273
|
import { createRequire } from "module";
|
|
4274
4274
|
var AGENT_LABELS = { claude: "Claude Code", codex: "Codex" };
|
|
4275
4275
|
var AGENT_NAME_FOR = { claude: "claude-code", codex: "codex" };
|
|
4276
|
+
var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
|
|
4276
4277
|
function skillsSourceDir() {
|
|
4277
4278
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
4278
4279
|
const distCandidate = resolve(here, "../skills");
|
|
@@ -4325,7 +4326,7 @@ function withoutMcpServerSections(text, name) {
|
|
|
4325
4326
|
return !skipping;
|
|
4326
4327
|
}).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
4327
4328
|
}
|
|
4328
|
-
async function registerClaudeViaConfig(mcp, homeDir) {
|
|
4329
|
+
async function registerClaudeViaConfig(mcp, homeDir, registration) {
|
|
4329
4330
|
const path = join4(homeDir, ".claude.json");
|
|
4330
4331
|
let config = {};
|
|
4331
4332
|
if (existsSync2(path)) {
|
|
@@ -4336,33 +4337,60 @@ async function registerClaudeViaConfig(mcp, homeDir) {
|
|
|
4336
4337
|
}
|
|
4337
4338
|
}
|
|
4338
4339
|
const servers = config.mcpServers ?? {};
|
|
4339
|
-
servers.
|
|
4340
|
+
servers[registration.name] = {
|
|
4340
4341
|
type: "stdio",
|
|
4341
4342
|
command: mcp.command,
|
|
4342
4343
|
args: mcp.args,
|
|
4343
|
-
env:
|
|
4344
|
+
env: registrationEnvironment(AGENT_NAME_FOR.claude, registration)
|
|
4344
4345
|
};
|
|
4345
4346
|
config.mcpServers = servers;
|
|
4346
4347
|
await atomicWrite(path, `${JSON.stringify(config, null, 2)}
|
|
4347
4348
|
`);
|
|
4348
4349
|
}
|
|
4349
|
-
async function registerCodexViaConfig(mcp, homeDir) {
|
|
4350
|
+
async function registerCodexViaConfig(mcp, homeDir, registration) {
|
|
4350
4351
|
const path = join4(homeDir, ".codex", "config.toml");
|
|
4351
4352
|
let text = "";
|
|
4352
4353
|
if (existsSync2(path)) text = await readFile3(path, "utf8");
|
|
4353
|
-
text = withoutMcpServerSections(text,
|
|
4354
|
+
text = withoutMcpServerSections(text, registration.name);
|
|
4354
4355
|
const argsToml = mcp.args.length ? `
|
|
4355
4356
|
args = [${mcp.args.map((a) => `"${a}"`).join(", ")}]` : "";
|
|
4356
4357
|
const block = `
|
|
4357
4358
|
|
|
4358
|
-
[mcp_servers.
|
|
4359
|
-
command =
|
|
4359
|
+
[mcp_servers.${registration.name}]
|
|
4360
|
+
command = ${JSON.stringify(mcp.command)}${argsToml}
|
|
4360
4361
|
|
|
4361
|
-
[mcp_servers.
|
|
4362
|
-
|
|
4363
|
-
`;
|
|
4362
|
+
[mcp_servers.${registration.name}.env]
|
|
4363
|
+
` + Object.entries(registrationEnvironment(AGENT_NAME_FOR.codex, registration)).map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join("\n") + "\n";
|
|
4364
4364
|
await atomicWrite(path, `${text}${block}`);
|
|
4365
4365
|
}
|
|
4366
|
+
function registrationEnvironment(agentName, registration) {
|
|
4367
|
+
return {
|
|
4368
|
+
TOPOLO_AGENT_NAME: agentName,
|
|
4369
|
+
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
function resolveMcpRegistration(options) {
|
|
4373
|
+
const name = options.name?.trim() || "topolo";
|
|
4374
|
+
const context = options.context?.trim();
|
|
4375
|
+
const environment = options.environment?.trim() || process.env.TOPOLO_ENV || "production";
|
|
4376
|
+
if (name === RESERVED_OPERATOR_REGISTRATION) {
|
|
4377
|
+
throw new Error("That registration name is reserved for restricted operator tooling.");
|
|
4378
|
+
}
|
|
4379
|
+
if (name !== "topolo" && !/^topolo-[a-z0-9][a-z0-9_-]*$/.test(name)) {
|
|
4380
|
+
throw new Error("Named registrations must use a topolo-<name> identifier.");
|
|
4381
|
+
}
|
|
4382
|
+
if (context && name === "topolo") {
|
|
4383
|
+
throw new Error("Pinned contexts require --name topolo-<name> so the active topolo registration remains independent.");
|
|
4384
|
+
}
|
|
4385
|
+
if (name !== "topolo" && !context) {
|
|
4386
|
+
throw new Error("Named registrations require --context <context>.");
|
|
4387
|
+
}
|
|
4388
|
+
if (context && /[\r\n]/.test(context)) throw new Error("Context must be a single-line identifier.");
|
|
4389
|
+
if (!["production", "staging", "development", "local"].includes(environment)) {
|
|
4390
|
+
throw new Error("Environment must be production, staging, development, or local.");
|
|
4391
|
+
}
|
|
4392
|
+
return { name, ...context ? { context } : {}, environment };
|
|
4393
|
+
}
|
|
4366
4394
|
async function unregisterFromConfig(agent, homeDir) {
|
|
4367
4395
|
if (agent === "claude") {
|
|
4368
4396
|
const path2 = join4(homeDir, ".claude.json");
|
|
@@ -4388,8 +4416,8 @@ async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
|
4388
4416
|
return { agent, skillPath, skillInstalled: true, mcp: "skipped" };
|
|
4389
4417
|
}
|
|
4390
4418
|
try {
|
|
4391
|
-
if (agent === "claude") await registerClaudeViaConfig(mcp, opts.homeDir);
|
|
4392
|
-
else await registerCodexViaConfig(mcp, opts.homeDir);
|
|
4419
|
+
if (agent === "claude") await registerClaudeViaConfig(mcp, opts.homeDir, opts.registration);
|
|
4420
|
+
else await registerCodexViaConfig(mcp, opts.homeDir, opts.registration);
|
|
4393
4421
|
return { agent, skillPath, skillInstalled: true, mcp: "config-file" };
|
|
4394
4422
|
} catch (err) {
|
|
4395
4423
|
return {
|
|
@@ -4408,7 +4436,7 @@ function resolveTargets(opts) {
|
|
|
4408
4436
|
}
|
|
4409
4437
|
function registerSkillsCommand(program2) {
|
|
4410
4438
|
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into Claude Code / Codex");
|
|
4411
|
-
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("--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(
|
|
4439
|
+
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(
|
|
4412
4440
|
async (opts) => {
|
|
4413
4441
|
const mode = resolveOutputMode(opts.json);
|
|
4414
4442
|
try {
|
|
@@ -4465,19 +4493,21 @@ async function installAgentSurfaces(opts) {
|
|
|
4465
4493
|
throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
|
|
4466
4494
|
}
|
|
4467
4495
|
const mcp = resolveMcpCommand();
|
|
4496
|
+
const registration = resolveMcpRegistration(opts);
|
|
4468
4497
|
const homeDir = opts.homeDir ?? homedir2();
|
|
4469
4498
|
const results = [];
|
|
4470
4499
|
for (const agent of resolveTargets(opts)) {
|
|
4471
4500
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
4472
4501
|
withMcp: opts.mcp !== false,
|
|
4473
4502
|
projectDir: opts.project,
|
|
4474
|
-
homeDir
|
|
4503
|
+
homeDir,
|
|
4504
|
+
registration
|
|
4475
4505
|
}));
|
|
4476
4506
|
}
|
|
4477
4507
|
return { results, mcp };
|
|
4478
4508
|
}
|
|
4479
4509
|
function registerSetupCommand(program2) {
|
|
4480
|
-
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("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
4510
|
+
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) => {
|
|
4481
4511
|
const mode = resolveOutputMode(opts.json);
|
|
4482
4512
|
try {
|
|
4483
4513
|
const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
|
|
@@ -4568,9 +4598,10 @@ async function verifyMcpProtocol(command, args) {
|
|
|
4568
4598
|
}
|
|
4569
4599
|
}
|
|
4570
4600
|
function registerDoctorCommand(program2) {
|
|
4571
|
-
program2.command("doctor").description("Verify the CLI credential and bundled MCP server end to end").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
4601
|
+
program2.command("doctor").description("Verify the CLI credential and bundled MCP server end to end").option("--context <context>", "Verify one exact personal or organization context").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
4572
4602
|
const mode = resolveOutputMode(opts.json);
|
|
4573
4603
|
try {
|
|
4604
|
+
if (opts.context !== void 0) applyDoctorContext(opts.context);
|
|
4574
4605
|
const result = await runDoctor();
|
|
4575
4606
|
if (!result.ok) throw new Error(formatDoctor(result));
|
|
4576
4607
|
emit(result, mode, () => formatDoctor(result));
|
|
@@ -4579,6 +4610,16 @@ function registerDoctorCommand(program2) {
|
|
|
4579
4610
|
}
|
|
4580
4611
|
});
|
|
4581
4612
|
}
|
|
4613
|
+
function normalizeDoctorContext(value) {
|
|
4614
|
+
const context = value.trim();
|
|
4615
|
+
if (!context || /[\r\n]/.test(context)) {
|
|
4616
|
+
throw new Error("Context must be a non-empty single-line identifier.");
|
|
4617
|
+
}
|
|
4618
|
+
return context;
|
|
4619
|
+
}
|
|
4620
|
+
function applyDoctorContext(value, environment = process.env) {
|
|
4621
|
+
environment.TOPOLO_CONTEXT = normalizeDoctorContext(value);
|
|
4622
|
+
}
|
|
4582
4623
|
function formatDoctor(result) {
|
|
4583
4624
|
return result.checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`).join("\n");
|
|
4584
4625
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"topoloPublishedVersion": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"topoloPublishedVersion": "0.8.2",
|
|
5
5
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/skills/topolo/SKILL.md
CHANGED
|
@@ -60,8 +60,15 @@ topolo whoami --json
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
For concurrent agents or multiple MCP connections, never coordinate by changing
|
|
63
|
-
one shared active context. Register
|
|
64
|
-
|
|
63
|
+
one shared active context. Register each pinned context through the CLI so it
|
|
64
|
+
uses the exact bundled MCP instead of a global binary:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
topolo setup --name topolo-acme --context org:org_acme --env production
|
|
68
|
+
topolo doctor --context org:org_acme --env production
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The context selector accepts an
|
|
65
72
|
exact key returned by `topolo context ls --json` (such as `personal` or
|
|
66
73
|
`org:<orgId>`) or a unique stored organization id, slug, or name. Each process
|
|
67
74
|
reads and refreshes only its pinned credential without changing the CLI or web
|