@topolo/cli 0.13.7 → 0.13.9
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/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
14
14
|
throw new Error("Exact closed smoke operation, role/case and UUID required.");
|
|
15
15
|
const store = packs.opts().store ?? resolve(homedir(), "AgentPacks");
|
|
16
16
|
if (options.harness === "claude") {
|
|
17
|
-
const { operateClaudeSmoke } = await import("./claude-pack-backend-
|
|
17
|
+
const { operateClaudeSmoke } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
18
18
|
console.log(JSON.stringify(await operateClaudeSmoke({
|
|
19
19
|
store,
|
|
20
20
|
kind: "smoke-" + operation,
|
|
@@ -44,7 +44,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
44
44
|
});
|
|
45
45
|
packs.command("configure").description("Qualify and configure an adopted Claude roster through its live native backend").argument("<pack>").option("--harness <name>", "Host adapter", "claude").requiredOption("--policy <path>", "Reviewed execution policy JSON").requiredOption("--endpoint <url>", "Explicit loopback main-process inspector endpoint, enabled separately with authorization").requiredOption("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply native groups, pins, models, effort and permission modes; otherwise preflight only").action(async (pack, options) => {
|
|
46
46
|
if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
|
|
47
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
47
|
+
const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
48
48
|
const result = await configureClaudePack({
|
|
49
49
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
50
50
|
pack,
|
|
@@ -96,7 +96,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
96
96
|
throw new Error("Only a one-request once/deny response is supported.");
|
|
97
97
|
decision = response.decision;
|
|
98
98
|
}
|
|
99
|
-
const { operateClaudeApprovals } = await import("./claude-pack-backend-
|
|
99
|
+
const { operateClaudeApprovals } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
100
100
|
console.log(JSON.stringify(await operateClaudeApprovals({
|
|
101
101
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
102
102
|
kind: args[0],
|
|
@@ -111,7 +111,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
111
111
|
if (options.policy || options.endpoint || options.pid || options.apply) {
|
|
112
112
|
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
113
113
|
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
114
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
114
|
+
const { configureClaudePack } = await import("./claude-pack-backend-4F4VHMI5.js");
|
|
115
115
|
const result2 = await configureClaudePack({
|
|
116
116
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
117
117
|
pack: args[0],
|
|
@@ -158,7 +158,7 @@ import { platform as platform2 } from "os";
|
|
|
158
158
|
import { createInterface } from "readline/promises";
|
|
159
159
|
import { stdin as input } from "process";
|
|
160
160
|
|
|
161
|
-
// node_modules/.pnpm/@topolo+sdk@0.23.
|
|
161
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/index.js
|
|
162
162
|
function record(value) {
|
|
163
163
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
164
164
|
}
|
|
@@ -1741,7 +1741,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1741
1741
|
} catch (error) {
|
|
1742
1742
|
const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
|
|
1743
1743
|
if (!transportFailure || attempt === maxAttempts) throw error;
|
|
1744
|
-
await new Promise((
|
|
1744
|
+
await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
1745
1745
|
}
|
|
1746
1746
|
}
|
|
1747
1747
|
throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
|
|
@@ -3740,7 +3740,7 @@ function createTopolo(options) {
|
|
|
3740
3740
|
async function pollUntilApproved(params) {
|
|
3741
3741
|
let interval = Math.max(params.intervalSeconds ?? 5, 1);
|
|
3742
3742
|
while (Date.now() < params.expiresAtMs) {
|
|
3743
|
-
await new Promise((
|
|
3743
|
+
await new Promise((resolve5) => setTimeout(resolve5, Math.min(interval * 1e3, params.expiresAtMs - Date.now())));
|
|
3744
3744
|
if (Date.now() >= params.expiresAtMs) break;
|
|
3745
3745
|
try {
|
|
3746
3746
|
return await params.oauth.pollDeviceToken({
|
|
@@ -3768,10 +3768,11 @@ async function pollUntilApproved(params) {
|
|
|
3768
3768
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3769
3769
|
}
|
|
3770
3770
|
|
|
3771
|
-
// node_modules/.pnpm/@topolo+sdk@0.23.
|
|
3772
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2
|
|
3771
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.20/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3772
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
|
|
3773
3773
|
import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
|
|
3774
3774
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
3775
|
+
import { constants, fstatSync, readSync } from "fs";
|
|
3775
3776
|
import { homedir as homedir2, platform } from "os";
|
|
3776
3777
|
import { dirname as dirname2, join as join3 } from "path";
|
|
3777
3778
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -4637,6 +4638,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4637
4638
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4638
4639
|
var CONFIG_LOCK_RETRY_MS = 25;
|
|
4639
4640
|
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
4641
|
+
var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
|
|
4640
4642
|
var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
4641
4643
|
constructor() {
|
|
4642
4644
|
super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
|
|
@@ -4651,32 +4653,145 @@ function lockEntryExists(path) {
|
|
|
4651
4653
|
throw new ConfigOwnershipError();
|
|
4652
4654
|
}
|
|
4653
4655
|
}
|
|
4656
|
+
function lockIdentity(entry) {
|
|
4657
|
+
return [entry.dev, entry.ino, entry.uid, entry.mode, entry.nlink, entry.size, entry.mtimeMs, entry.ctimeMs];
|
|
4658
|
+
}
|
|
4659
|
+
function privateLockDirectory(path) {
|
|
4660
|
+
const entry = lstatSync(path);
|
|
4661
|
+
if (typeof process.getuid !== "function" || !entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== process.getuid() || (entry.mode & 511) !== 448) throw new ConfigOwnershipError();
|
|
4662
|
+
return entry;
|
|
4663
|
+
}
|
|
4664
|
+
function readPrivateLockMarker(path) {
|
|
4665
|
+
const before = lstatSync(path);
|
|
4666
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size > 512 || before.uid !== process.getuid?.() || (before.mode & 511) !== 384) throw new ConfigOwnershipError();
|
|
4667
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
4668
|
+
try {
|
|
4669
|
+
if (JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
|
|
4670
|
+
const bytes = Buffer.alloc(513);
|
|
4671
|
+
const count = readSync(fd, bytes, 0, bytes.length, 0);
|
|
4672
|
+
if (count !== before.size || count > 512 || JSON.stringify(lockIdentity(fstatSync(fd))) !== JSON.stringify(lockIdentity(before)) || JSON.stringify(lockIdentity(lstatSync(path))) !== JSON.stringify(lockIdentity(before))) throw new ConfigOwnershipError();
|
|
4673
|
+
return bytes.subarray(0, count).toString("utf8");
|
|
4674
|
+
} finally {
|
|
4675
|
+
closeSync(fd);
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
var OWNER_MARKER_NAME = /^owner-[a-f0-9-]{36}\.json$/;
|
|
4679
|
+
function readOwnerMarker(lockPath, name) {
|
|
4680
|
+
const bytes = readPrivateLockMarker(join3(lockPath, name));
|
|
4681
|
+
const owner = JSON.parse(bytes);
|
|
4682
|
+
if (owner.version !== 1 || Object.keys(owner).sort().join(",") !== "identity,pid,version" || name !== "owner-" + owner.identity + ".json" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new ConfigOwnershipError();
|
|
4683
|
+
return { bytes, pid: owner.pid };
|
|
4684
|
+
}
|
|
4685
|
+
function writePrivateLockRecord(path, bytes) {
|
|
4686
|
+
const fd = openSync(path, "wx", 384);
|
|
4687
|
+
try {
|
|
4688
|
+
writeFileSync(fd, bytes);
|
|
4689
|
+
fsyncSync(fd);
|
|
4690
|
+
} finally {
|
|
4691
|
+
closeSync(fd);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
function deadOwnerMarker(lockPath, ignoreOwnFence = false) {
|
|
4695
|
+
try {
|
|
4696
|
+
if (!ignoreOwnFence && lockEntryExists(lockPath + ".reconcile")) return null;
|
|
4697
|
+
const directory = privateLockDirectory(lockPath);
|
|
4698
|
+
const entries = readdirSync(lockPath);
|
|
4699
|
+
if (entries.length !== 1 || !OWNER_MARKER_NAME.test(entries[0])) return null;
|
|
4700
|
+
const name = entries[0];
|
|
4701
|
+
const { bytes, pid } = readOwnerMarker(lockPath, name);
|
|
4702
|
+
if (Date.now() - lstatSync(join3(lockPath, name)).mtimeMs < CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS) return null;
|
|
4703
|
+
try {
|
|
4704
|
+
process.kill(pid, 0);
|
|
4705
|
+
return null;
|
|
4706
|
+
} catch (error) {
|
|
4707
|
+
if (error.code !== "ESRCH") return null;
|
|
4708
|
+
}
|
|
4709
|
+
return { directory, name, bytes, pid };
|
|
4710
|
+
} catch {
|
|
4711
|
+
return null;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
function reclaimDeadOwnerLock(lockPath, markerName, marker) {
|
|
4715
|
+
const observed = deadOwnerMarker(lockPath);
|
|
4716
|
+
if (!observed) return false;
|
|
4717
|
+
const fence = lockPath + ".reconcile";
|
|
4718
|
+
try {
|
|
4719
|
+
mkdirSync(fence, { mode: 448 });
|
|
4720
|
+
} catch {
|
|
4721
|
+
return false;
|
|
4722
|
+
}
|
|
4723
|
+
const fenceMarker = join3(fence, markerName);
|
|
4724
|
+
const quarantine = join3(dirname2(lockPath), ".credential-reclaim-" + randomUUID2());
|
|
4725
|
+
let captured;
|
|
4726
|
+
let reclaimed = false;
|
|
4727
|
+
try {
|
|
4728
|
+
writePrivateLockRecord(fenceMarker, marker);
|
|
4729
|
+
const current = deadOwnerMarker(lockPath, true);
|
|
4730
|
+
if (!current || current.name !== observed.name || current.bytes !== observed.bytes || current.directory.dev !== observed.directory.dev || current.directory.ino !== observed.directory.ino) return false;
|
|
4731
|
+
mkdirSync(quarantine, { mode: 448 });
|
|
4732
|
+
writePrivateLockRecord(join3(quarantine, "receipt.json"), JSON.stringify({
|
|
4733
|
+
schemaVersion: 1,
|
|
4734
|
+
disposition: "DEAD_OWNER_RECLAIMED",
|
|
4735
|
+
lockPath,
|
|
4736
|
+
deadOwnerMarker: current.name,
|
|
4737
|
+
deadOwnerPid: current.pid,
|
|
4738
|
+
deadOwnerEvidence: "ESRCH",
|
|
4739
|
+
reclaimedByPid: process.pid,
|
|
4740
|
+
reclaimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4741
|
+
configCredentialOrOAuthEffectsByThisReclaim: "none"
|
|
4742
|
+
}, null, 2));
|
|
4743
|
+
captured = join3(quarantine, current.name);
|
|
4744
|
+
renameSync(join3(lockPath, current.name), captured);
|
|
4745
|
+
const directory = privateLockDirectory(lockPath);
|
|
4746
|
+
if (directory.dev !== observed.directory.dev || directory.ino !== observed.directory.ino || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4747
|
+
writePrivateLockRecord(join3(lockPath, markerName), marker);
|
|
4748
|
+
reclaimed = true;
|
|
4749
|
+
} catch {
|
|
4750
|
+
if (captured && !reclaimed) {
|
|
4751
|
+
try {
|
|
4752
|
+
linkSync(captured, join3(lockPath, observed.name));
|
|
4753
|
+
} catch {
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
} finally {
|
|
4757
|
+
try {
|
|
4758
|
+
if (readPrivateLockMarker(fenceMarker) !== marker || readdirSync(fence).length !== 1) throw new ConfigOwnershipError();
|
|
4759
|
+
unlinkSync(fenceMarker);
|
|
4760
|
+
rmdirSync(fence);
|
|
4761
|
+
} catch {
|
|
4762
|
+
reclaimed = false;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
return reclaimed;
|
|
4766
|
+
}
|
|
4767
|
+
var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
4654
4768
|
async function withConfigWriteLock(operation) {
|
|
4655
4769
|
const lockPath = `${configPath()}.lock`;
|
|
4656
4770
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
4657
4771
|
const startedAt = performance.now();
|
|
4772
|
+
const identity = randomUUID2();
|
|
4773
|
+
const markerName = "owner-" + identity + ".json";
|
|
4774
|
+
const markerPath = join3(lockPath, markerName);
|
|
4775
|
+
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4658
4776
|
while (true) {
|
|
4659
4777
|
try {
|
|
4660
4778
|
if (lockEntryExists(lockPath + ".reconcile")) throw Object.assign(new Error(), { code: "EEXIST" });
|
|
4661
|
-
|
|
4779
|
+
mkdirSync(lockPath, { mode: 448 });
|
|
4662
4780
|
if (lockEntryExists(lockPath + ".reconcile")) {
|
|
4663
4781
|
throw new ConfigOwnershipError();
|
|
4664
4782
|
}
|
|
4783
|
+
writeFileSync(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4665
4784
|
break;
|
|
4666
4785
|
} catch (error) {
|
|
4667
4786
|
if (error.code !== "EEXIST") throw error;
|
|
4787
|
+
if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
|
|
4668
4788
|
if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4669
4789
|
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4670
4790
|
}
|
|
4671
4791
|
await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
|
|
4672
4792
|
}
|
|
4673
4793
|
}
|
|
4674
|
-
const directory =
|
|
4675
|
-
const identity = randomUUID2();
|
|
4676
|
-
const markerName = "owner-" + identity + ".json";
|
|
4677
|
-
const markerPath = join3(lockPath, markerName);
|
|
4678
|
-
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4679
|
-
await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4794
|
+
const directory = lstatSync(lockPath);
|
|
4680
4795
|
const assertDirectory = async () => {
|
|
4681
4796
|
const current = await lstat(lockPath);
|
|
4682
4797
|
if (lockEntryExists(lockPath + ".reconcile") || !current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
@@ -4700,10 +4815,14 @@ async function withConfigWriteLock(operation) {
|
|
|
4700
4815
|
throw new ConfigOwnershipError();
|
|
4701
4816
|
};
|
|
4702
4817
|
const assertOwnedSync = () => {
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4818
|
+
try {
|
|
4819
|
+
assertDirectorySync();
|
|
4820
|
+
const entries = readdirSync(lockPath);
|
|
4821
|
+
const entry = lstatSync(markerPath);
|
|
4822
|
+
if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
|
|
4823
|
+
} catch {
|
|
4824
|
+
throw new ConfigOwnershipError();
|
|
4825
|
+
}
|
|
4707
4826
|
};
|
|
4708
4827
|
const persist = async (update) => {
|
|
4709
4828
|
const path = configPath();
|
|
@@ -4732,14 +4851,14 @@ async function withConfigWriteLock(operation) {
|
|
|
4732
4851
|
if (temporaryOwned) unlinkSync(temporary);
|
|
4733
4852
|
}
|
|
4734
4853
|
};
|
|
4735
|
-
const release =
|
|
4736
|
-
|
|
4854
|
+
const release = () => {
|
|
4855
|
+
assertOwnedSync();
|
|
4737
4856
|
const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
|
|
4738
|
-
|
|
4857
|
+
mkdirSync(quarantine, { mode: 448 });
|
|
4739
4858
|
const captured = join3(quarantine, markerName);
|
|
4740
4859
|
try {
|
|
4741
|
-
|
|
4742
|
-
|
|
4860
|
+
assertOwnedSync();
|
|
4861
|
+
renameSync(markerPath, captured);
|
|
4743
4862
|
assertDirectorySync();
|
|
4744
4863
|
const entry = lstatSync(captured);
|
|
4745
4864
|
if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
@@ -4751,20 +4870,40 @@ async function withConfigWriteLock(operation) {
|
|
|
4751
4870
|
} catch {
|
|
4752
4871
|
if (existsSync2(captured)) {
|
|
4753
4872
|
try {
|
|
4754
|
-
|
|
4873
|
+
linkSync(captured, markerPath);
|
|
4755
4874
|
} catch {
|
|
4756
4875
|
}
|
|
4757
4876
|
}
|
|
4758
4877
|
throw new ConfigOwnershipError();
|
|
4759
4878
|
}
|
|
4760
4879
|
};
|
|
4880
|
+
let held = true;
|
|
4881
|
+
const releaseOnce = () => {
|
|
4882
|
+
if (!held) return;
|
|
4883
|
+
held = false;
|
|
4884
|
+
process.off("exit", onExit);
|
|
4885
|
+
for (const signal of RELEASE_SIGNALS) process.off(signal, onSignal);
|
|
4886
|
+
release();
|
|
4887
|
+
};
|
|
4888
|
+
const onExit = () => {
|
|
4889
|
+
try {
|
|
4890
|
+
releaseOnce();
|
|
4891
|
+
} catch {
|
|
4892
|
+
}
|
|
4893
|
+
};
|
|
4894
|
+
const onSignal = (signal) => {
|
|
4895
|
+
onExit();
|
|
4896
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
4897
|
+
};
|
|
4898
|
+
process.on("exit", onExit);
|
|
4899
|
+
for (const signal of RELEASE_SIGNALS) process.on(signal, onSignal);
|
|
4761
4900
|
try {
|
|
4762
4901
|
await assertOwned();
|
|
4763
4902
|
const result = await operation(assertOwned, persist);
|
|
4764
4903
|
await assertOwned();
|
|
4765
4904
|
return result;
|
|
4766
4905
|
} finally {
|
|
4767
|
-
|
|
4906
|
+
releaseOnce();
|
|
4768
4907
|
}
|
|
4769
4908
|
}
|
|
4770
4909
|
function renewalFailure(error) {
|
|
@@ -6382,7 +6521,7 @@ import { spawnSync as spawnSync4 } from "child_process";
|
|
|
6382
6521
|
import { existsSync as existsSync6 } from "fs";
|
|
6383
6522
|
import { readFile as readFile6, rm as rm2 } from "fs/promises";
|
|
6384
6523
|
import { homedir as homedir5 } from "os";
|
|
6385
|
-
import { dirname as dirname6, join as join7, resolve as
|
|
6524
|
+
import { dirname as dirname6, join as join7, resolve as resolve3 } from "path";
|
|
6386
6525
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6387
6526
|
import { createRequire } from "module";
|
|
6388
6527
|
|
|
@@ -6391,7 +6530,7 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
6391
6530
|
import { existsSync as existsSync4 } from "fs";
|
|
6392
6531
|
import { readFile as readFile4 } from "fs/promises";
|
|
6393
6532
|
import { homedir as homedir4 } from "os";
|
|
6394
|
-
import { isAbsolute, join as join6 } from "path";
|
|
6533
|
+
import { isAbsolute, join as join6, resolve as resolve2 } from "path";
|
|
6395
6534
|
var SUPPORTED_AGENT_IDS = [
|
|
6396
6535
|
"claude",
|
|
6397
6536
|
"claude-desktop",
|
|
@@ -6431,6 +6570,18 @@ var AGENT_NAME_FOR = {
|
|
|
6431
6570
|
goose: "goose",
|
|
6432
6571
|
"amazon-q": "amazon-q"
|
|
6433
6572
|
};
|
|
6573
|
+
function resolveAgentHostTarget(options) {
|
|
6574
|
+
const homeDir = resolve2(options.homeDir ?? homedir4());
|
|
6575
|
+
const codexHome = options.codexHome ?? (options.homeDir === void 0 ? process.env.CODEX_HOME || void 0 : void 0);
|
|
6576
|
+
if (codexHome !== void 0 && (!codexHome.trim() || !isAbsolute(codexHome))) {
|
|
6577
|
+
throw new Error("Codex home must be an absolute nonempty directory.");
|
|
6578
|
+
}
|
|
6579
|
+
return Object.freeze({
|
|
6580
|
+
homeDir,
|
|
6581
|
+
...options.projectDir ? { projectDir: resolve2(options.projectDir) } : {},
|
|
6582
|
+
codexHome: codexHome ?? join6(homeDir, ".codex")
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6434
6585
|
var RESERVED_OPERATOR_REGISTRATION = ["topolo", "platform"].join("-");
|
|
6435
6586
|
function isPublicRegistrationName(name) {
|
|
6436
6587
|
return name === "topolo" || name.startsWith("topolo-") && name !== RESERVED_OPERATOR_REGISTRATION;
|
|
@@ -6446,14 +6597,14 @@ function stableNodeCommand() {
|
|
|
6446
6597
|
});
|
|
6447
6598
|
return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
|
|
6448
6599
|
}
|
|
6449
|
-
function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
|
|
6600
|
+
function agentSkillPath(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6450
6601
|
switch (agent) {
|
|
6451
6602
|
case "claude":
|
|
6452
6603
|
return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
6453
6604
|
case "claude-desktop":
|
|
6454
6605
|
return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
6455
6606
|
case "codex":
|
|
6456
|
-
return join6(homeDir, ".codex", "skills", "topolo", "SKILL.md");
|
|
6607
|
+
return join6(codexHome ?? join6(homeDir, ".codex"), "skills", "topolo", "SKILL.md");
|
|
6457
6608
|
case "cursor":
|
|
6458
6609
|
return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
|
|
6459
6610
|
case "grok":
|
|
@@ -6471,10 +6622,10 @@ function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
|
|
|
6471
6622
|
return void 0;
|
|
6472
6623
|
}
|
|
6473
6624
|
}
|
|
6474
|
-
function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
|
|
6625
|
+
function agentConfigPaths(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6475
6626
|
switch (agent) {
|
|
6476
6627
|
case "claude":
|
|
6477
|
-
return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
6628
|
+
return [{ path: projectDir ? join6(projectDir, ".mcp.json") : join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
6478
6629
|
case "claude-desktop":
|
|
6479
6630
|
return [{
|
|
6480
6631
|
path: claudeDesktopConfigPath(homeDir),
|
|
@@ -6482,7 +6633,7 @@ function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
|
|
|
6482
6633
|
rootKey: "mcpServers"
|
|
6483
6634
|
}];
|
|
6484
6635
|
case "codex":
|
|
6485
|
-
return [{ path: join6(homeDir, ".codex", "config.toml"), format: "toml" }];
|
|
6636
|
+
return [{ path: join6(codexHome ?? join6(homeDir, ".codex"), "config.toml"), format: "toml" }];
|
|
6486
6637
|
case "cursor":
|
|
6487
6638
|
return [{
|
|
6488
6639
|
path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
|
|
@@ -6563,8 +6714,36 @@ function agentCliNames(agent) {
|
|
|
6563
6714
|
return ["qchat", "q"];
|
|
6564
6715
|
}
|
|
6565
6716
|
}
|
|
6566
|
-
function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
|
|
6567
|
-
return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
|
|
6717
|
+
function agentConfigExists(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6718
|
+
return agentConfigPaths(agent, homeDir, projectDir, codexHome).some((config) => existsSync4(config.path));
|
|
6719
|
+
}
|
|
6720
|
+
async function assertClaudeProjectScope(homeDir, projectDir, name) {
|
|
6721
|
+
const path = join6(homeDir, ".claude.json");
|
|
6722
|
+
let contents;
|
|
6723
|
+
try {
|
|
6724
|
+
contents = await readFile4(path, "utf8");
|
|
6725
|
+
} catch (error) {
|
|
6726
|
+
if (error.code === "ENOENT") return;
|
|
6727
|
+
throw error;
|
|
6728
|
+
}
|
|
6729
|
+
let config;
|
|
6730
|
+
try {
|
|
6731
|
+
config = JSON.parse(contents);
|
|
6732
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error();
|
|
6733
|
+
} catch {
|
|
6734
|
+
throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
|
|
6735
|
+
}
|
|
6736
|
+
const object = (value) => {
|
|
6737
|
+
if (value === void 0) return void 0;
|
|
6738
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6739
|
+
throw new Error("Cannot verify malformed Claude scope configuration at " + path + ".");
|
|
6740
|
+
}
|
|
6741
|
+
return value;
|
|
6742
|
+
};
|
|
6743
|
+
const local = object(object(config.projects)?.[resolve2(projectDir)]);
|
|
6744
|
+
if (Object.hasOwn(object(config.mcpServers) ?? {}, name) || Object.hasOwn(object(local?.mcpServers) ?? {}, name)) {
|
|
6745
|
+
throw new Error("Claude MCP scope conflict for " + name + ": a user or local definition can override the project registration. Resolve that scope before setup.");
|
|
6746
|
+
}
|
|
6568
6747
|
}
|
|
6569
6748
|
function claudeDesktopConfigPath(homeDir) {
|
|
6570
6749
|
if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
@@ -6576,9 +6755,9 @@ function vscodeUserConfigPath(homeDir) {
|
|
|
6576
6755
|
if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
6577
6756
|
return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
|
|
6578
6757
|
}
|
|
6579
|
-
async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
|
|
6758
|
+
async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir, codexHome) {
|
|
6580
6759
|
const registrations = [];
|
|
6581
|
-
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
6760
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
6582
6761
|
if (!existsSync4(config.path)) continue;
|
|
6583
6762
|
if (config.format === "json") {
|
|
6584
6763
|
registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
|
|
@@ -6657,6 +6836,7 @@ async function readGooseRegistrations(path) {
|
|
|
6657
6836
|
continue;
|
|
6658
6837
|
}
|
|
6659
6838
|
if (inExtensions && /^\S/.test(rawLine) && rawLine.trim()) {
|
|
6839
|
+
if (current?.command) registrations.push(current);
|
|
6660
6840
|
inExtensions = false;
|
|
6661
6841
|
inTopolo = false;
|
|
6662
6842
|
current = void 0;
|
|
@@ -6664,6 +6844,7 @@ async function readGooseRegistrations(path) {
|
|
|
6664
6844
|
if (!inExtensions) continue;
|
|
6665
6845
|
const extension = rawLine.match(/^ ([A-Za-z0-9_-]+):\s*$/);
|
|
6666
6846
|
if (extension) {
|
|
6847
|
+
if (current?.command) registrations.push(current);
|
|
6667
6848
|
inTopolo = true;
|
|
6668
6849
|
current = isPublicRegistrationName(extension[1]) ? { name: extension[1], command: "", args: [], env: {} } : void 0;
|
|
6669
6850
|
continue;
|
|
@@ -6841,17 +7022,19 @@ function message(error) {
|
|
|
6841
7022
|
|
|
6842
7023
|
// src/commands/agent-config.ts
|
|
6843
7024
|
import { existsSync as existsSync5 } from "fs";
|
|
6844
|
-
import { mkdir as mkdir3, readFile as readFile5, rename as
|
|
7025
|
+
import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
7026
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
6845
7027
|
import { dirname as dirname5 } from "path";
|
|
6846
|
-
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
6847
|
-
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
7028
|
+
async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
|
|
7029
|
+
const path = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
6848
7030
|
if (!path) return { installed: false };
|
|
6849
7031
|
const contents = formatSkillForAgent(agent, source);
|
|
6850
7032
|
await atomicWrite(path, contents);
|
|
6851
7033
|
return { path, installed: true };
|
|
6852
7034
|
}
|
|
6853
|
-
async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir) {
|
|
6854
|
-
|
|
7035
|
+
async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir, codexHome) {
|
|
7036
|
+
if (agent === "claude" && projectDir) await assertClaudeProjectScope(homeDir, projectDir, registration.name);
|
|
7037
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
6855
7038
|
if (config.format === "json") {
|
|
6856
7039
|
await registerJson(config, agent, mcp, registration);
|
|
6857
7040
|
} else if (config.format === "toml") {
|
|
@@ -6861,8 +7044,8 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
|
|
|
6861
7044
|
}
|
|
6862
7045
|
}
|
|
6863
7046
|
}
|
|
6864
|
-
async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
6865
|
-
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
7047
|
+
async function unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome) {
|
|
7048
|
+
for (const config of agentConfigPaths(agent, homeDir, projectDir, codexHome)) {
|
|
6866
7049
|
if (!existsSync5(config.path)) continue;
|
|
6867
7050
|
if (config.format === "json") {
|
|
6868
7051
|
await unregisterJson(config, agent, name);
|
|
@@ -6875,17 +7058,39 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
|
6875
7058
|
}
|
|
6876
7059
|
}
|
|
6877
7060
|
function registrationEnvironment(agent, registration) {
|
|
7061
|
+
let privateAliases = {};
|
|
7062
|
+
if (registration.configPath) {
|
|
7063
|
+
const prefixes = {
|
|
7064
|
+
production: "TOPOLO_PRODUCTION",
|
|
7065
|
+
staging: "TOPOLO_STAGING",
|
|
7066
|
+
development: "TOPOLO_DEVELOPMENT",
|
|
7067
|
+
local: "TOPOLO_LOCAL"
|
|
7068
|
+
};
|
|
7069
|
+
const prefix = prefixes[registration.environment];
|
|
7070
|
+
if (!prefix) throw new Error("Cannot isolate a private profile for an unknown environment.");
|
|
7071
|
+
privateAliases = {
|
|
7072
|
+
TOPOLO_API_KEY: "",
|
|
7073
|
+
TOPOLO_ACCESS_TOKEN: "",
|
|
7074
|
+
[prefix + "_API_KEY"]: "",
|
|
7075
|
+
[prefix + "_ACCESS_TOKEN"]: "",
|
|
7076
|
+
TOPOLO_ENV: registration.environment
|
|
7077
|
+
};
|
|
7078
|
+
}
|
|
6878
7079
|
return {
|
|
6879
7080
|
TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
|
|
7081
|
+
...registration.configPath ? { TOPOLO_CONFIG_PATH: registration.configPath } : {},
|
|
6880
7082
|
...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
|
|
6881
|
-
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
|
|
7083
|
+
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {},
|
|
7084
|
+
...privateAliases
|
|
6882
7085
|
};
|
|
6883
7086
|
}
|
|
6884
7087
|
async function registerJson(config, agent, mcp, registration) {
|
|
7088
|
+
const before = await fileSnapshot(config.path);
|
|
6885
7089
|
let parsed = {};
|
|
6886
|
-
if (
|
|
7090
|
+
if (before.contents !== void 0) {
|
|
6887
7091
|
try {
|
|
6888
|
-
parsed = JSON.parse(
|
|
7092
|
+
parsed = JSON.parse(before.contents);
|
|
7093
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected an object.");
|
|
6889
7094
|
} catch (error) {
|
|
6890
7095
|
throw new Error(
|
|
6891
7096
|
"Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
|
|
@@ -6906,7 +7111,7 @@ async function registerJson(config, agent, mcp, registration) {
|
|
|
6906
7111
|
if (agent === "claude" || agent === "claude-desktop" || rootKey === "servers") entry.type = "stdio";
|
|
6907
7112
|
servers[registration.name] = entry;
|
|
6908
7113
|
parsed[rootKey] = servers;
|
|
6909
|
-
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
7114
|
+
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n", before);
|
|
6910
7115
|
}
|
|
6911
7116
|
async function unregisterJson(config, agent, name) {
|
|
6912
7117
|
let parsed;
|
|
@@ -6928,13 +7133,14 @@ async function unregisterJson(config, agent, name) {
|
|
|
6928
7133
|
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
6929
7134
|
}
|
|
6930
7135
|
async function registerToml(path, agent, mcp, registration) {
|
|
6931
|
-
const
|
|
7136
|
+
const before = await fileSnapshot(path);
|
|
7137
|
+
const existing = before.contents ?? "";
|
|
6932
7138
|
const text2 = withoutMcpServerSections(existing, registration.name);
|
|
6933
7139
|
const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
|
|
6934
7140
|
const environment = registrationEnvironment(agent, registration);
|
|
6935
7141
|
const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
|
|
6936
7142
|
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";
|
|
6937
|
-
await atomicWrite(path, text2 + block);
|
|
7143
|
+
await atomicWrite(path, text2 + block, before);
|
|
6938
7144
|
}
|
|
6939
7145
|
async function registerGoose(path, mcp, registration) {
|
|
6940
7146
|
const existing = existsSync5(path) ? await readFile5(path, "utf8") : "";
|
|
@@ -7004,11 +7210,82 @@ function formatSkillForAgent(agent, source) {
|
|
|
7004
7210
|
}
|
|
7005
7211
|
return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
|
|
7006
7212
|
}
|
|
7007
|
-
|
|
7213
|
+
function fileIdentity(stat2) {
|
|
7214
|
+
return [stat2.dev, stat2.ino, stat2.size, stat2.mtimeMs, stat2.ctimeMs, stat2.mode, stat2.nlink].join(":");
|
|
7215
|
+
}
|
|
7216
|
+
async function fileSnapshot(path) {
|
|
7217
|
+
const parents = [];
|
|
7218
|
+
for (let parent = dirname5(path); ; parent = dirname5(parent)) {
|
|
7219
|
+
try {
|
|
7220
|
+
const stat2 = await lstat2(parent);
|
|
7221
|
+
if (!stat2.isDirectory() || stat2.isSymbolicLink()) throw new Error("Host destination contains a symbolic link or non-directory at " + parent + ".");
|
|
7222
|
+
parents.push({ path: parent, identity: [stat2.dev, stat2.ino].join(":") });
|
|
7223
|
+
} catch (error) {
|
|
7224
|
+
if (error.code !== "ENOENT") throw error;
|
|
7225
|
+
}
|
|
7226
|
+
if (dirname5(parent) === parent) break;
|
|
7227
|
+
}
|
|
7228
|
+
try {
|
|
7229
|
+
const stat2 = await lstat2(path);
|
|
7230
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1) throw new Error("Host configuration must be a single regular file at " + path + ".");
|
|
7231
|
+
const contents = await readFile5(path, "utf8");
|
|
7232
|
+
if (fileIdentity(await lstat2(path)) !== fileIdentity(stat2)) throw new Error("Host configuration changed while reading at " + path + ".");
|
|
7233
|
+
return { contents, identity: fileIdentity(stat2), parents };
|
|
7234
|
+
} catch (error) {
|
|
7235
|
+
if (error.code === "ENOENT") return { parents };
|
|
7236
|
+
throw error;
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7239
|
+
async function assertSnapshot(path, before) {
|
|
7240
|
+
const current = await fileSnapshot(path);
|
|
7241
|
+
if (current.contents !== before.contents || current.identity !== before.identity || before.parents.some((parent) => !current.parents.some((candidate) => candidate.path === parent.path && candidate.identity === parent.identity))) {
|
|
7242
|
+
throw new Error("Host destination changed before write at " + path + ".");
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
async function atomicWrite(path, contents, before) {
|
|
7246
|
+
before ??= await fileSnapshot(path);
|
|
7247
|
+
await assertSnapshot(path, before);
|
|
7248
|
+
if (before.contents === contents) return;
|
|
7008
7249
|
await mkdir3(dirname5(path), { recursive: true });
|
|
7009
|
-
const
|
|
7010
|
-
await
|
|
7011
|
-
|
|
7250
|
+
const destination = await fileSnapshot(path);
|
|
7251
|
+
await assertSnapshot(path, before);
|
|
7252
|
+
const temporaryPath = path + ".topolo-" + randomUUID3() + ".tmp";
|
|
7253
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
7254
|
+
let temporaryIdentity;
|
|
7255
|
+
let handleClosed = false;
|
|
7256
|
+
let temporaryPending = true;
|
|
7257
|
+
try {
|
|
7258
|
+
const created = await handle.stat();
|
|
7259
|
+
temporaryIdentity = [created.dev, created.ino].join(":");
|
|
7260
|
+
await assertSnapshot(path, destination);
|
|
7261
|
+
await writeFile3(handle, contents, { encoding: "utf8" });
|
|
7262
|
+
await handle.close();
|
|
7263
|
+
handleClosed = true;
|
|
7264
|
+
await assertSnapshot(path, destination);
|
|
7265
|
+
const temporary = await lstat2(temporaryPath);
|
|
7266
|
+
if (!temporary.isFile() || temporary.isSymbolicLink() || temporary.nlink !== 1 || [temporary.dev, temporary.ino].join(":") !== temporaryIdentity) {
|
|
7267
|
+
throw new Error("Host temporary file custody changed before rename at " + path + ".");
|
|
7268
|
+
}
|
|
7269
|
+
await rename2(temporaryPath, path);
|
|
7270
|
+
temporaryPending = false;
|
|
7271
|
+
const installed = await fileSnapshot(path);
|
|
7272
|
+
if (installed.contents !== contents) throw new Error("Host configuration read-back failed at " + path + ".");
|
|
7273
|
+
} finally {
|
|
7274
|
+
if (!handleClosed) await handle.close();
|
|
7275
|
+
if (temporaryPending && temporaryIdentity) {
|
|
7276
|
+
let cleanupOwned = false;
|
|
7277
|
+
try {
|
|
7278
|
+
const parentsOwned = await Promise.all(destination.parents.map(async (parent) => {
|
|
7279
|
+
const current2 = await lstat2(parent.path);
|
|
7280
|
+
return current2.isDirectory() && !current2.isSymbolicLink() && [current2.dev, current2.ino].join(":") === parent.identity;
|
|
7281
|
+
}));
|
|
7282
|
+
const current = await lstat2(temporaryPath);
|
|
7283
|
+
cleanupOwned = parentsOwned.every(Boolean) && current.isFile() && !current.isSymbolicLink() && current.nlink === 1 && [current.dev, current.ino].join(":") === temporaryIdentity;
|
|
7284
|
+
} catch {
|
|
7285
|
+
}
|
|
7286
|
+
if (cleanupOwned) await unlink(temporaryPath);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7012
7289
|
}
|
|
7013
7290
|
function withoutMcpServerSections(text2, name) {
|
|
7014
7291
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -7029,8 +7306,8 @@ function withoutMcpServerSections(text2, name) {
|
|
|
7029
7306
|
var AGENT_LABELS2 = AGENT_LABELS;
|
|
7030
7307
|
function skillsSourceDir() {
|
|
7031
7308
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
7032
|
-
const distCandidate =
|
|
7033
|
-
const srcCandidate =
|
|
7309
|
+
const distCandidate = resolve3(here, "../skills");
|
|
7310
|
+
const srcCandidate = resolve3(here, "../../skills");
|
|
7034
7311
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
7035
7312
|
if (existsSync6(join7(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
7036
7313
|
}
|
|
@@ -7072,17 +7349,32 @@ function resolveMcpRegistration(options) {
|
|
|
7072
7349
|
if (installationId && /[\r\n]/.test(installationId)) {
|
|
7073
7350
|
throw new Error("Installation id must be a single-line identifier.");
|
|
7074
7351
|
}
|
|
7075
|
-
|
|
7352
|
+
const configPath2 = process.env.TOPOLO_CONFIG_PATH;
|
|
7353
|
+
return {
|
|
7354
|
+
name,
|
|
7355
|
+
...context ? { context } : {},
|
|
7356
|
+
environment,
|
|
7357
|
+
...installationId ? { installationId } : {},
|
|
7358
|
+
...configPath2 ? { configPath: configPath2 } : {}
|
|
7359
|
+
};
|
|
7076
7360
|
}
|
|
7077
7361
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
7362
|
+
const { homeDir, projectDir, codexHome } = opts.target;
|
|
7363
|
+
if (opts.withMcp && agent === "claude" && projectDir) {
|
|
7364
|
+
try {
|
|
7365
|
+
await assertClaudeProjectScope(homeDir, projectDir, opts.registration.name);
|
|
7366
|
+
} catch (err) {
|
|
7367
|
+
return { agent, skillInstalled: false, mcp: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7078
7370
|
const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
|
|
7079
|
-
const skill = await installAgentSkill(agent, source,
|
|
7371
|
+
const skill = await installAgentSkill(agent, source, homeDir, projectDir, codexHome);
|
|
7080
7372
|
if (!opts.withMcp) {
|
|
7081
7373
|
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
|
|
7082
7374
|
}
|
|
7083
7375
|
try {
|
|
7084
|
-
await registerAgentConfig(agent, mcp,
|
|
7085
|
-
const installed = (await readAgentRegistrations(agent,
|
|
7376
|
+
await registerAgentConfig(agent, mcp, homeDir, opts.registration, projectDir, codexHome);
|
|
7377
|
+
const installed = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).find((registration) => registration.name === opts.registration.name);
|
|
7086
7378
|
const expectedEnvironment = registrationEnvironment(agent, opts.registration);
|
|
7087
7379
|
if (!installed || installed.command !== mcp.command || JSON.stringify(installed.args) !== JSON.stringify(mcp.args) || Object.entries(expectedEnvironment).some(([key, value]) => installed.env[key] !== value)) {
|
|
7088
7380
|
throw new Error(`Host configuration did not retain the ${opts.registration.name} MCP registration.`);
|
|
@@ -7116,12 +7408,12 @@ function resolveTargets(opts) {
|
|
|
7116
7408
|
if (opts.all) return [...SUPPORTED_AGENT_IDS];
|
|
7117
7409
|
const homeDir = opts.homeDir ?? homedir5();
|
|
7118
7410
|
const detectBinaries = homeDir === homedir5();
|
|
7119
|
-
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
|
|
7411
|
+
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project, opts.codexHome) || detectBinaries && agentBinaryExists(agent));
|
|
7120
7412
|
return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
|
|
7121
7413
|
}
|
|
7122
7414
|
function registerSkillsCommand(program2) {
|
|
7123
7415
|
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
|
|
7124
|
-
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("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").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(
|
|
7416
|
+
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("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").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("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").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(
|
|
7125
7417
|
async (opts) => {
|
|
7126
7418
|
const mode = resolveOutputMode(opts.json);
|
|
7127
7419
|
try {
|
|
@@ -7137,11 +7429,12 @@ function registerSkillsCommand(program2) {
|
|
|
7137
7429
|
}
|
|
7138
7430
|
}
|
|
7139
7431
|
);
|
|
7140
|
-
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) => {
|
|
7432
|
+
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("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").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) => {
|
|
7141
7433
|
const mode = resolveOutputMode(opts.json);
|
|
7142
7434
|
try {
|
|
7143
7435
|
const rows = await agentSurfaceStatus({
|
|
7144
|
-
homeDir: opts.agentHome ?
|
|
7436
|
+
homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0,
|
|
7437
|
+
codexHome: opts.codexHome,
|
|
7145
7438
|
name: opts.name,
|
|
7146
7439
|
agent: opts.agent,
|
|
7147
7440
|
all: opts.all,
|
|
@@ -7152,12 +7445,12 @@ function registerSkillsCommand(program2) {
|
|
|
7152
7445
|
fail2(err, mode);
|
|
7153
7446
|
}
|
|
7154
7447
|
});
|
|
7155
|
-
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) => {
|
|
7448
|
+
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("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").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) => {
|
|
7156
7449
|
const mode = resolveOutputMode(opts.json);
|
|
7157
7450
|
try {
|
|
7158
7451
|
const removed = await uninstallAgentSurfaces({
|
|
7159
7452
|
...opts,
|
|
7160
|
-
homeDir: opts.agentHome ?
|
|
7453
|
+
homeDir: opts.agentHome ? resolve3(opts.agentHome) : void 0
|
|
7161
7454
|
});
|
|
7162
7455
|
emit(
|
|
7163
7456
|
{ removed },
|
|
@@ -7176,20 +7469,20 @@ async function installAgentSurfaces(opts) {
|
|
|
7176
7469
|
}
|
|
7177
7470
|
const mcp = resolveMcpCommand();
|
|
7178
7471
|
const registration = resolveMcpRegistration(opts);
|
|
7179
|
-
const
|
|
7472
|
+
const target = resolveAgentHostTarget({ homeDir: opts.homeDir, projectDir: opts.project, codexHome: opts.codexHome });
|
|
7473
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7180
7474
|
const results = [];
|
|
7181
|
-
for (const agent of resolveTargets({ ...opts, homeDir, project:
|
|
7475
|
+
for (const agent of resolveTargets({ ...opts, homeDir, project: projectDir, codexHome })) {
|
|
7182
7476
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
7183
7477
|
withMcp: opts.mcp !== false,
|
|
7184
|
-
|
|
7185
|
-
homeDir,
|
|
7478
|
+
target,
|
|
7186
7479
|
registration
|
|
7187
7480
|
}));
|
|
7188
7481
|
}
|
|
7189
7482
|
return { results, mcp };
|
|
7190
7483
|
}
|
|
7191
7484
|
function registerSetupCommand(program2) {
|
|
7192
|
-
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("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7485
|
+
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("--codex-home <dir>", "Use an explicit native Codex home instead of <agent-home>/.codex").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("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
7193
7486
|
const mode = resolveOutputMode(opts.json);
|
|
7194
7487
|
try {
|
|
7195
7488
|
const { results, mcp } = await installAgentSurfaces({
|
|
@@ -7211,18 +7504,20 @@ function assertAgentSetupSucceeded(results) {
|
|
|
7211
7504
|
}
|
|
7212
7505
|
}
|
|
7213
7506
|
async function agentSurfaceStatus(options = {}) {
|
|
7214
|
-
const
|
|
7507
|
+
const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
|
|
7508
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7215
7509
|
const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
|
|
7216
7510
|
const agents = resolveTargets({
|
|
7217
7511
|
agent: options.agent,
|
|
7218
7512
|
all: options.all,
|
|
7219
7513
|
homeDir,
|
|
7220
|
-
project:
|
|
7514
|
+
project: projectDir,
|
|
7515
|
+
codexHome
|
|
7221
7516
|
});
|
|
7222
7517
|
return Promise.all(agents.map(async (agent) => {
|
|
7223
|
-
const skillPath = agentSkillPath(agent, homeDir,
|
|
7518
|
+
const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
7224
7519
|
const installedSkill = skillPath && existsSync6(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
|
|
7225
|
-
const registrations = (await readAgentRegistrations(agent, homeDir,
|
|
7520
|
+
const registrations = (await readAgentRegistrations(agent, homeDir, projectDir, codexHome)).filter((registration) => !options.name || registration.name === options.name);
|
|
7226
7521
|
return {
|
|
7227
7522
|
agent,
|
|
7228
7523
|
skillInstalled: installedSkill !== void 0,
|
|
@@ -7235,16 +7530,17 @@ async function agentSurfaceStatus(options = {}) {
|
|
|
7235
7530
|
}));
|
|
7236
7531
|
}
|
|
7237
7532
|
async function uninstallAgentSurfaces(options) {
|
|
7238
|
-
const
|
|
7533
|
+
const target = resolveAgentHostTarget({ homeDir: options.homeDir, projectDir: options.project, codexHome: options.codexHome });
|
|
7534
|
+
const { homeDir, projectDir, codexHome } = target;
|
|
7239
7535
|
const name = options.name?.trim() || "topolo";
|
|
7240
7536
|
resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
|
|
7241
7537
|
const removed = [];
|
|
7242
|
-
for (const agent of resolveTargets({ ...options, homeDir, project:
|
|
7243
|
-
const before = await readAgentRegistrations(agent, homeDir,
|
|
7538
|
+
for (const agent of resolveTargets({ ...options, homeDir, project: projectDir, codexHome })) {
|
|
7539
|
+
const before = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
|
|
7244
7540
|
const registrationRemoved = before.some((entry) => entry.name === name);
|
|
7245
|
-
await unregisterAgentConfig(agent, homeDir, name,
|
|
7246
|
-
const remaining = await readAgentRegistrations(agent, homeDir,
|
|
7247
|
-
const skillPath = agentSkillPath(agent, homeDir,
|
|
7541
|
+
await unregisterAgentConfig(agent, homeDir, name, projectDir, codexHome);
|
|
7542
|
+
const remaining = await readAgentRegistrations(agent, homeDir, projectDir, codexHome);
|
|
7543
|
+
const skillPath = agentSkillPath(agent, homeDir, projectDir, codexHome);
|
|
7248
7544
|
const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
|
|
7249
7545
|
if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
|
|
7250
7546
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
@@ -7277,7 +7573,7 @@ function formatStatus(rows) {
|
|
|
7277
7573
|
// src/commands/doctor.ts
|
|
7278
7574
|
import { access } from "fs/promises";
|
|
7279
7575
|
import { homedir as homedir6 } from "os";
|
|
7280
|
-
import { resolve as
|
|
7576
|
+
import { resolve as resolve4 } from "path";
|
|
7281
7577
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7282
7578
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7283
7579
|
var defaultDependencies = {
|
|
@@ -7339,7 +7635,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
|
|
|
7339
7635
|
}
|
|
7340
7636
|
async function resolveInstalledDoctorTarget(options) {
|
|
7341
7637
|
const name = options.name?.trim() || "topolo";
|
|
7342
|
-
const homeDir = options.agentHome ?
|
|
7638
|
+
const homeDir = options.agentHome ? resolve4(options.agentHome) : homedir6();
|
|
7343
7639
|
const expected = resolveMcpCommand();
|
|
7344
7640
|
const requestedAgents = resolveTargets({
|
|
7345
7641
|
...options,
|