@topolo/cli 0.8.0 → 0.8.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 CHANGED
@@ -78,6 +78,16 @@ 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
+ ```
87
+
88
+ This creates separately named Claude Code and Codex registrations using the
89
+ same exact nested MCP executable. `topolo-platform` is reserved for the
90
+ restricted operator toolchain.
81
91
 
82
92
  ## Cross-org isolation
83
93
 
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.topolo = {
4340
+ servers[registration.name] = {
4340
4341
  type: "stdio",
4341
4342
  command: mcp.command,
4342
4343
  args: mcp.args,
4343
- env: { TOPOLO_AGENT_NAME: AGENT_NAME_FOR.claude }
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, "topolo");
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.topolo]
4359
- command = "${mcp.command}"${argsToml}
4359
+ [mcp_servers.${registration.name}]
4360
+ command = ${JSON.stringify(mcp.command)}${argsToml}
4360
4361
 
4361
- [mcp_servers.topolo.env]
4362
- TOPOLO_AGENT_NAME = "${AGENT_NAME_FOR.codex}"
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 });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.8.0",
4
- "topoloPublishedVersion": "0.8.0",
3
+ "version": "0.8.1",
4
+ "topoloPublishedVersion": "0.8.1",
5
5
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -60,8 +60,14 @@ 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 one named `topolo-mcp` process per context
64
- with both `TOPOLO_ENV` and `TOPOLO_CONTEXT`. The context selector accepts an
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
+ ```
69
+
70
+ The context selector accepts an
65
71
  exact key returned by `topolo context ls --json` (such as `personal` or
66
72
  `org:<orgId>`) or a unique stored organization id, slug, or name. Each process
67
73
  reads and refreshes only its pinned credential without changing the CLI or web