@topolo/cli 0.13.0 → 0.13.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/agent-packs/README.md +154 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +265 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +29 -2
- package/agent-packs/src/topolo_agent_pack/execution.py +154 -0
- package/agent-packs/src/topolo_agent_pack/live.py +49 -8
- package/agent-packs/src/topolo_agent_pack/policy.py +92 -0
- package/dist/claude-pack-backend-IDQQX3DG.js +555 -0
- package/dist/index.js +256 -82
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -5,8 +5,22 @@ import { spawnSync } from "child_process";
|
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import { dirname, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
+
import { homedir } from "os";
|
|
8
9
|
function registerAgentPacksCommand(program2) {
|
|
9
10
|
const packs = program2.command("agent-packs").description("Install and operate Topolo Agent Packs in supported local hosts").option("--store <path>", "Portable pack library (default ~/AgentPacks)");
|
|
11
|
+
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) => {
|
|
12
|
+
if (options.harness !== "claude") throw new Error("Native configure is currently qualified only for Claude Desktop.");
|
|
13
|
+
const { configureClaudePack } = await import("./claude-pack-backend-IDQQX3DG.js");
|
|
14
|
+
const result = await configureClaudePack({
|
|
15
|
+
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
16
|
+
pack,
|
|
17
|
+
policy: options.policy,
|
|
18
|
+
endpoint: options.endpoint,
|
|
19
|
+
pid: Number(options.pid),
|
|
20
|
+
apply: options.apply === true
|
|
21
|
+
});
|
|
22
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23
|
+
});
|
|
10
24
|
const commands = [
|
|
11
25
|
"add",
|
|
12
26
|
"replace",
|
|
@@ -19,6 +33,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
19
33
|
"list",
|
|
20
34
|
"recover",
|
|
21
35
|
"doctor",
|
|
36
|
+
"policy",
|
|
22
37
|
"remote-recover",
|
|
23
38
|
"connect",
|
|
24
39
|
"remote-context",
|
|
@@ -27,8 +42,26 @@ function registerAgentPacksCommand(program2) {
|
|
|
27
42
|
"artifact-get"
|
|
28
43
|
];
|
|
29
44
|
for (const name of commands) {
|
|
30
|
-
packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`)
|
|
31
|
-
|
|
45
|
+
const command = packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`);
|
|
46
|
+
if (name === "switch" || name === "recover") command.option("--policy <path>", "Reviewed policy for an adopted Claude roster").option("--endpoint <url>", "Explicit authorized Claude main-process inspector endpoint").option("--pid <pid>", "Expected Claude main-process PID").option("--apply", "Apply a native switch; recovery is always read-only");
|
|
47
|
+
command.option("--harness <name>", "Local host adapter (codex, claude Desktop Code sessions)", "codex").option("--adapter-help", "Show every option supported by the selected adapter").argument("[arguments...]").allowUnknownOption().allowExcessArguments().addHelpText("after", name === "add" ? "\nExamples:\n topolo agent-packs add founder\n topolo agent-packs add founder --group specialists\n topolo agent-packs add founder --agent strategy\n" : "\nRun topolo agent-packs <command> --adapter-help for all adapter options. Cloud commands require explicit --environment and --context-key.\n").action(async (args, options) => {
|
|
48
|
+
if (!["codex", "claude"].includes(options.harness)) throw new Error(`The ${options.harness} pack adapter is not available. Supported adapters: codex, claude.`);
|
|
49
|
+
if (options.policy || options.endpoint || options.pid || options.apply) {
|
|
50
|
+
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
51
|
+
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
52
|
+
const { configureClaudePack } = await import("./claude-pack-backend-IDQQX3DG.js");
|
|
53
|
+
const result2 = await configureClaudePack({
|
|
54
|
+
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
55
|
+
pack: args[0],
|
|
56
|
+
policy: options.policy,
|
|
57
|
+
endpoint: options.endpoint,
|
|
58
|
+
pid: Number(options.pid),
|
|
59
|
+
mode: name,
|
|
60
|
+
apply: options.apply === true
|
|
61
|
+
});
|
|
62
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
32
65
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
66
|
const adapter = [resolve(here, "../agent-packs/topolo-agent-pack"), resolve(here, "../../agent-packs/topolo-agent-pack")].find(existsSync);
|
|
34
67
|
if (!adapter) throw new Error("This CLI installation is missing its bundled Topolo Agent Pack adapter. Reinstall @topolo/cli.");
|
|
@@ -37,6 +70,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
37
70
|
const result = spawnSync("python3", [
|
|
38
71
|
adapter,
|
|
39
72
|
...store ? ["--store", store] : [],
|
|
73
|
+
...options.harness === "claude" ? ["--harness", "claude"] : [],
|
|
40
74
|
name,
|
|
41
75
|
...forwarded,
|
|
42
76
|
...name === "add" && !options.adapterHelp ? ["--catalog"] : []
|
|
@@ -56,7 +90,7 @@ import { platform as platform2 } from "os";
|
|
|
56
90
|
import { createInterface } from "readline/promises";
|
|
57
91
|
import { stdin as input } from "process";
|
|
58
92
|
|
|
59
|
-
// node_modules
|
|
93
|
+
// node_modules/@topolo/sdk/dist/index.js
|
|
60
94
|
function record(value) {
|
|
61
95
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
62
96
|
}
|
|
@@ -3153,9 +3187,18 @@ var TopoloOAuth = class {
|
|
|
3153
3187
|
if (!res.ok) {
|
|
3154
3188
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
3155
3189
|
const err = parsed;
|
|
3156
|
-
throw new TopoloAuthError(err.error_description || err.error, err.error
|
|
3190
|
+
throw new TopoloAuthError(err.error_description || err.error, err.error, {
|
|
3191
|
+
status: res.status,
|
|
3192
|
+
service: "auth",
|
|
3193
|
+
path,
|
|
3194
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
3195
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
3196
|
+
});
|
|
3157
3197
|
}
|
|
3158
|
-
throw new TopoloHttpError("auth", path, res.status, parsed
|
|
3198
|
+
throw new TopoloHttpError("auth", path, res.status, parsed, void 0, {
|
|
3199
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
3200
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
3201
|
+
});
|
|
3159
3202
|
}
|
|
3160
3203
|
return parsed;
|
|
3161
3204
|
}
|
|
@@ -3657,9 +3700,10 @@ async function pollUntilApproved(params) {
|
|
|
3657
3700
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3658
3701
|
}
|
|
3659
3702
|
|
|
3660
|
-
// node_modules
|
|
3661
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2,
|
|
3662
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3703
|
+
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3704
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
|
|
3705
|
+
import { existsSync as existsSync2, lstatSync, readdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync } from "fs";
|
|
3706
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3663
3707
|
import { homedir as homedir2, platform } from "os";
|
|
3664
3708
|
import { dirname as dirname2, join as join3 } from "path";
|
|
3665
3709
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -3667,7 +3711,7 @@ import { join as join2 } from "path";
|
|
|
3667
3711
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3668
3712
|
import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
|
|
3669
3713
|
import { existsSync as existsSync3 } from "fs";
|
|
3670
|
-
import { homedir, platform as hostPlatform } from "os";
|
|
3714
|
+
import { homedir as homedir3, platform as hostPlatform } from "os";
|
|
3671
3715
|
import { dirname as dirname3, join } from "path";
|
|
3672
3716
|
var TopoloSdkError2 = class extends Error {
|
|
3673
3717
|
code;
|
|
@@ -4067,9 +4111,18 @@ var TopoloOAuth2 = class {
|
|
|
4067
4111
|
if (!res.ok) {
|
|
4068
4112
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
4069
4113
|
const err = parsed;
|
|
4070
|
-
throw new TopoloAuthError2(err.error_description || err.error, err.error
|
|
4114
|
+
throw new TopoloAuthError2(err.error_description || err.error, err.error, {
|
|
4115
|
+
status: res.status,
|
|
4116
|
+
service: "auth",
|
|
4117
|
+
path,
|
|
4118
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
4119
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
4120
|
+
});
|
|
4071
4121
|
}
|
|
4072
|
-
throw new TopoloHttpError2("auth", path, res.status, parsed
|
|
4122
|
+
throw new TopoloHttpError2("auth", path, res.status, parsed, void 0, {
|
|
4123
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
4124
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
4125
|
+
});
|
|
4073
4126
|
}
|
|
4074
4127
|
return parsed;
|
|
4075
4128
|
}
|
|
@@ -4097,7 +4150,7 @@ var MACOS_KEYCHAIN_WRITE_SCRIPT = [
|
|
|
4097
4150
|
function installationStorePath() {
|
|
4098
4151
|
const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
|
|
4099
4152
|
if (override) return override;
|
|
4100
|
-
const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(
|
|
4153
|
+
const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir3(), "AppData", "Roaming") : join(homedir3(), ".config"));
|
|
4101
4154
|
return join(base, "topolo", "installations");
|
|
4102
4155
|
}
|
|
4103
4156
|
function installationPath(environment, id) {
|
|
@@ -4516,38 +4569,149 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4516
4569
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4517
4570
|
var CONFIG_LOCK_RETRY_MS = 25;
|
|
4518
4571
|
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
4519
|
-
var
|
|
4572
|
+
var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
4573
|
+
constructor() {
|
|
4574
|
+
super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4520
4577
|
async function withConfigWriteLock(operation) {
|
|
4521
4578
|
const lockPath = `${configPath()}.lock`;
|
|
4522
4579
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
4523
|
-
const startedAt =
|
|
4580
|
+
const startedAt = performance.now();
|
|
4524
4581
|
while (true) {
|
|
4525
4582
|
try {
|
|
4526
|
-
await mkdir2(lockPath);
|
|
4583
|
+
await mkdir2(lockPath, { mode: 448 });
|
|
4527
4584
|
break;
|
|
4528
4585
|
} catch (error) {
|
|
4529
4586
|
if (error.code !== "EEXIST") throw error;
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
4533
|
-
await rm2(lockPath, { recursive: true, force: true });
|
|
4534
|
-
continue;
|
|
4535
|
-
}
|
|
4536
|
-
} catch (inspectionError) {
|
|
4537
|
-
if (inspectionError.code !== "ENOENT") throw inspectionError;
|
|
4538
|
-
continue;
|
|
4539
|
-
}
|
|
4540
|
-
if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4541
|
-
throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
|
|
4587
|
+
if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4588
|
+
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4542
4589
|
}
|
|
4543
4590
|
await new Promise((resolve4) => setTimeout(resolve4, CONFIG_LOCK_RETRY_MS));
|
|
4544
4591
|
}
|
|
4545
4592
|
}
|
|
4593
|
+
const directory = await lstat(lockPath);
|
|
4594
|
+
const identity = randomUUID2();
|
|
4595
|
+
const markerName = "owner-" + identity + ".json";
|
|
4596
|
+
const markerPath = join3(lockPath, markerName);
|
|
4597
|
+
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4598
|
+
await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4599
|
+
const assertDirectory = async () => {
|
|
4600
|
+
const current = await lstat(lockPath);
|
|
4601
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4602
|
+
throw new ConfigOwnershipError();
|
|
4603
|
+
};
|
|
4604
|
+
const assertOwned = async () => {
|
|
4605
|
+
try {
|
|
4606
|
+
await assertDirectory();
|
|
4607
|
+
const entries = await readdir2(lockPath);
|
|
4608
|
+
if (entries.length !== 1 || entries[0] !== markerName) throw new ConfigOwnershipError();
|
|
4609
|
+
const entry = await lstat(markerPath);
|
|
4610
|
+
if (!entry.isFile() || entry.isSymbolicLink() || await readFile2(markerPath, "utf8") !== marker)
|
|
4611
|
+
throw new ConfigOwnershipError();
|
|
4612
|
+
} catch {
|
|
4613
|
+
throw new ConfigOwnershipError();
|
|
4614
|
+
}
|
|
4615
|
+
};
|
|
4616
|
+
const assertDirectorySync = () => {
|
|
4617
|
+
const current = lstatSync(lockPath);
|
|
4618
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4619
|
+
throw new ConfigOwnershipError();
|
|
4620
|
+
};
|
|
4621
|
+
const assertOwnedSync = () => {
|
|
4622
|
+
assertDirectorySync();
|
|
4623
|
+
const entries = readdirSync(lockPath);
|
|
4624
|
+
const entry = lstatSync(markerPath);
|
|
4625
|
+
if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
|
|
4626
|
+
};
|
|
4627
|
+
const persist = async (update) => {
|
|
4628
|
+
const path = configPath();
|
|
4629
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
4630
|
+
const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
|
|
4631
|
+
let temporaryOwned = false;
|
|
4632
|
+
try {
|
|
4633
|
+
assertOwnedSync();
|
|
4634
|
+
const entry = lstatSync(path);
|
|
4635
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
|
|
4636
|
+
const before = readFileSync(path, "utf8");
|
|
4637
|
+
const updated = update(JSON.parse(before));
|
|
4638
|
+
const fd = openSync(temporary, "wx", 384);
|
|
4639
|
+
temporaryOwned = true;
|
|
4640
|
+
try {
|
|
4641
|
+
writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
|
|
4642
|
+
fsyncSync(fd);
|
|
4643
|
+
} finally {
|
|
4644
|
+
closeSync(fd);
|
|
4645
|
+
}
|
|
4646
|
+
assertOwnedSync();
|
|
4647
|
+
if (readFileSync(path, "utf8") !== before) throw new ConfigOwnershipError();
|
|
4648
|
+
renameSync(temporary, path);
|
|
4649
|
+
temporaryOwned = false;
|
|
4650
|
+
} finally {
|
|
4651
|
+
if (temporaryOwned) unlinkSync(temporary);
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
const release = async () => {
|
|
4655
|
+
await assertOwned();
|
|
4656
|
+
const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
|
|
4657
|
+
await mkdir2(quarantine, { mode: 448 });
|
|
4658
|
+
const captured = join3(quarantine, markerName);
|
|
4659
|
+
try {
|
|
4660
|
+
await assertOwned();
|
|
4661
|
+
await rename2(markerPath, captured);
|
|
4662
|
+
assertDirectorySync();
|
|
4663
|
+
const entry = lstatSync(captured);
|
|
4664
|
+
if (!entry.isFile() || entry.isSymbolicLink() || readFileSync(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4665
|
+
unlinkSync(captured);
|
|
4666
|
+
rmdirSync(quarantine);
|
|
4667
|
+
assertDirectorySync();
|
|
4668
|
+
if (readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4669
|
+
rmdirSync(lockPath);
|
|
4670
|
+
} catch {
|
|
4671
|
+
if (existsSync2(captured)) {
|
|
4672
|
+
try {
|
|
4673
|
+
await link(captured, markerPath);
|
|
4674
|
+
} catch {
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
throw new ConfigOwnershipError();
|
|
4678
|
+
}
|
|
4679
|
+
};
|
|
4546
4680
|
try {
|
|
4547
|
-
|
|
4681
|
+
await assertOwned();
|
|
4682
|
+
const result = await operation(assertOwned, persist);
|
|
4683
|
+
await assertOwned();
|
|
4684
|
+
return result;
|
|
4548
4685
|
} finally {
|
|
4549
|
-
await
|
|
4686
|
+
await release();
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
function renewalFailure(error) {
|
|
4690
|
+
const path = "/api/developer-oauth/token";
|
|
4691
|
+
if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
|
|
4692
|
+
if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
|
|
4693
|
+
return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
|
|
4694
|
+
status: error.status,
|
|
4695
|
+
service: "auth",
|
|
4696
|
+
path,
|
|
4697
|
+
requestId: error.requestId
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
if (error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) {
|
|
4701
|
+
return new TopoloHttpError2(
|
|
4702
|
+
"auth",
|
|
4703
|
+
path,
|
|
4704
|
+
error.status,
|
|
4705
|
+
void 0,
|
|
4706
|
+
"Topolo sign-in renewal failed. Retry the request; the saved sign-in has been retained.",
|
|
4707
|
+
{ requestId: error.requestId, retryAfter: error.retryAfter }
|
|
4708
|
+
);
|
|
4550
4709
|
}
|
|
4710
|
+
const timeout = error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
4711
|
+
return new TopoloSdkError2(
|
|
4712
|
+
timeout ? "request_timeout" : "network_error",
|
|
4713
|
+
"Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
|
|
4714
|
+
);
|
|
4551
4715
|
}
|
|
4552
4716
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
4553
4717
|
const prefix = envVarPrefix(environment);
|
|
@@ -4557,16 +4721,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4557
4721
|
const observedProfile = profileForEnvironment(await readConfig(), environment);
|
|
4558
4722
|
const observed = observedProfile.credential;
|
|
4559
4723
|
if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !observedProfile.oauthClientId || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
|
|
4560
|
-
return withConfigWriteLock(async () => {
|
|
4724
|
+
return withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4561
4725
|
const config = await readConfig();
|
|
4562
4726
|
const profile = profileForEnvironment(config, environment);
|
|
4563
4727
|
const cred = profile.credential;
|
|
4564
4728
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
4565
4729
|
if (!cred.refreshToken || !profile.oauthClientId) return cred;
|
|
4566
|
-
|
|
4730
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4731
|
+
if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken && cred.expiresAt !== void 0 && cred.expiresAt > now) {
|
|
4567
4732
|
return cred;
|
|
4568
4733
|
}
|
|
4569
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4570
4734
|
if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
4571
4735
|
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4572
4736
|
if (environment === "local" && !serviceUrls?.auth) {
|
|
@@ -4589,23 +4753,27 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4589
4753
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
4590
4754
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
4591
4755
|
};
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
credential
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4756
|
+
await persist((current) => {
|
|
4757
|
+
const latest = profileForEnvironment(current, environment);
|
|
4758
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
4759
|
+
if (!same(latest.credential, cred) || latest.oauthClientId !== profile.oauthClientId || latest.activeContext !== profile.activeContext)
|
|
4760
|
+
throw new ConfigOwnershipError();
|
|
4761
|
+
const activeContext = latest.activeContext;
|
|
4762
|
+
const activeEntry = activeContext ? latest.contexts?.[activeContext] : void 0;
|
|
4763
|
+
const previousEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
4764
|
+
if (!same(activeEntry?.credential, previousEntry?.credential)) throw new ConfigOwnershipError();
|
|
4765
|
+
return activeContext && activeEntry ? writeProfileConfig(current, environment, {
|
|
4766
|
+
credential: refreshed,
|
|
4767
|
+
contexts: { ...latest.contexts, [activeContext]: {
|
|
4599
4768
|
...activeEntry,
|
|
4600
4769
|
credential: refreshed,
|
|
4601
|
-
oauthClientId: activeEntry.oauthClientId ??
|
|
4602
|
-
}
|
|
4603
|
-
}
|
|
4604
|
-
})
|
|
4605
|
-
await writeConfig(updated);
|
|
4770
|
+
oauthClientId: activeEntry.oauthClientId ?? latest.oauthClientId
|
|
4771
|
+
} }
|
|
4772
|
+
}) : environment === "production" && !current.profiles?.production ? { ...current, credential: refreshed } : writeProfileConfig(current, environment, { credential: refreshed });
|
|
4773
|
+
});
|
|
4606
4774
|
return refreshed;
|
|
4607
|
-
} catch {
|
|
4608
|
-
|
|
4775
|
+
} catch (error) {
|
|
4776
|
+
throw renewalFailure(error);
|
|
4609
4777
|
}
|
|
4610
4778
|
});
|
|
4611
4779
|
}
|
|
@@ -4614,7 +4782,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4614
4782
|
const observedContext = observedProfile.contexts?.[key];
|
|
4615
4783
|
const observed = observedContext?.credential;
|
|
4616
4784
|
if (!observed || observed.kind !== "access_token" || !observed.refreshToken || !(observedContext.oauthClientId ?? observedProfile.oauthClientId) || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > Math.floor(Date.now() / 1e3)) return observed ?? null;
|
|
4617
|
-
return withConfigWriteLock(async () => {
|
|
4785
|
+
return withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4618
4786
|
const config = await readConfig();
|
|
4619
4787
|
const profile = profileForEnvironment(config, environment);
|
|
4620
4788
|
const context = profile.contexts?.[key];
|
|
@@ -4623,6 +4791,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4623
4791
|
const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
|
|
4624
4792
|
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
4625
4793
|
const now = Math.floor(Date.now() / 1e3);
|
|
4794
|
+
if (options.force && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return credential;
|
|
4626
4795
|
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
4627
4796
|
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4628
4797
|
if (environment === "local" && !serviceUrls?.auth) {
|
|
@@ -4643,16 +4812,21 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4643
4812
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
|
|
4644
4813
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
4645
4814
|
};
|
|
4646
|
-
await
|
|
4647
|
-
|
|
4648
|
-
contexts
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4815
|
+
await persist((current) => {
|
|
4816
|
+
const latest = profileForEnvironment(current, environment);
|
|
4817
|
+
const latestContext = latest.contexts?.[key];
|
|
4818
|
+
if (!latestContext || JSON.stringify(latestContext.credential) !== JSON.stringify(credential) || (latestContext.oauthClientId ?? latest.oauthClientId) !== oauthClientId || latestContext.contextType !== context.contextType || latestContext.organizationId !== context.organizationId)
|
|
4819
|
+
throw new ConfigOwnershipError();
|
|
4820
|
+
if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
|
|
4821
|
+
throw new ConfigOwnershipError();
|
|
4822
|
+
return writeProfileConfig(current, environment, {
|
|
4823
|
+
...latest.activeContext === key ? { credential: refreshed } : {},
|
|
4824
|
+
contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
|
|
4825
|
+
});
|
|
4826
|
+
});
|
|
4653
4827
|
return refreshed;
|
|
4654
|
-
} catch {
|
|
4655
|
-
|
|
4828
|
+
} catch (error) {
|
|
4829
|
+
throw renewalFailure(error);
|
|
4656
4830
|
}
|
|
4657
4831
|
});
|
|
4658
4832
|
}
|
|
@@ -4811,7 +4985,7 @@ function normalizeError(err) {
|
|
|
4811
4985
|
}
|
|
4812
4986
|
|
|
4813
4987
|
// src/version.ts
|
|
4814
|
-
import { readFileSync } from "fs";
|
|
4988
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4815
4989
|
import { dirname as dirname4, join as join4 } from "path";
|
|
4816
4990
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4817
4991
|
function readCliVersion() {
|
|
@@ -4823,7 +4997,7 @@ function readCliVersion() {
|
|
|
4823
4997
|
];
|
|
4824
4998
|
for (const path of candidates) {
|
|
4825
4999
|
try {
|
|
4826
|
-
const pkg = JSON.parse(
|
|
5000
|
+
const pkg = JSON.parse(readFileSync2(path, "utf8"));
|
|
4827
5001
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4828
5002
|
} catch {
|
|
4829
5003
|
}
|
|
@@ -5159,19 +5333,19 @@ function formatPretty(info) {
|
|
|
5159
5333
|
|
|
5160
5334
|
// src/commands/actions.ts
|
|
5161
5335
|
import { spawn as spawn2 } from "child_process";
|
|
5162
|
-
import { readFileSync as
|
|
5336
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5163
5337
|
import { platform as platform3 } from "os";
|
|
5164
5338
|
import { basename, extname } from "path";
|
|
5165
5339
|
|
|
5166
5340
|
// src/json-input.ts
|
|
5167
|
-
import { readFileSync as
|
|
5341
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5168
5342
|
function parseJsonInput(value) {
|
|
5169
5343
|
let source = value;
|
|
5170
5344
|
if (value.startsWith("@")) {
|
|
5171
5345
|
const path = value.slice(1);
|
|
5172
5346
|
if (!path) throw new Error("--data @file path must not be empty");
|
|
5173
5347
|
try {
|
|
5174
|
-
source =
|
|
5348
|
+
source = readFileSync3(path, "utf8");
|
|
5175
5349
|
} catch (err) {
|
|
5176
5350
|
throw new Error(`Could not read --data file ${path}: ${err.message}`);
|
|
5177
5351
|
}
|
|
@@ -5368,7 +5542,7 @@ Approval required. ${opened ? "Opening" : "Open"} ${approval.verificationUrl}` :
|
|
|
5368
5542
|
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
5369
5543
|
const result = await topolo.client.uploadAction(
|
|
5370
5544
|
action,
|
|
5371
|
-
|
|
5545
|
+
readFileSync4(opts.file),
|
|
5372
5546
|
parseJsonObject(opts.data || "{}"),
|
|
5373
5547
|
{
|
|
5374
5548
|
fileName: basename(opts.file),
|
|
@@ -5940,7 +6114,7 @@ function normalizeList(values, fallback = []) {
|
|
|
5940
6114
|
}
|
|
5941
6115
|
|
|
5942
6116
|
// src/commands/integrations.ts
|
|
5943
|
-
import { readdir as
|
|
6117
|
+
import { readdir as readdir3, readFile as readFile3, stat } from "fs/promises";
|
|
5944
6118
|
import { join as join5, relative } from "path";
|
|
5945
6119
|
function registerIntegrationsCommand(program2) {
|
|
5946
6120
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -6094,7 +6268,7 @@ async function collectFiles(repoRoot) {
|
|
|
6094
6268
|
const root = repoRoot;
|
|
6095
6269
|
async function walk(dir) {
|
|
6096
6270
|
if (result.length > 2e3) return;
|
|
6097
|
-
const entries = await
|
|
6271
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
6098
6272
|
for (const entry of entries) {
|
|
6099
6273
|
const fullPath = join5(dir, entry.name);
|
|
6100
6274
|
if (entry.isDirectory()) {
|
|
@@ -6103,7 +6277,7 @@ async function collectFiles(repoRoot) {
|
|
|
6103
6277
|
continue;
|
|
6104
6278
|
}
|
|
6105
6279
|
if (!entry.isFile() || !isInspectable(entry.name)) continue;
|
|
6106
|
-
const info = await
|
|
6280
|
+
const info = await stat(fullPath);
|
|
6107
6281
|
if (info.size > 25e4) continue;
|
|
6108
6282
|
result.push({
|
|
6109
6283
|
path: relative(root, fullPath),
|
|
@@ -6125,8 +6299,8 @@ function formatCheckResult(result) {
|
|
|
6125
6299
|
// src/commands/skills.ts
|
|
6126
6300
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
6127
6301
|
import { existsSync as existsSync6 } from "fs";
|
|
6128
|
-
import { readFile as readFile6, rm as
|
|
6129
|
-
import { homedir as
|
|
6302
|
+
import { readFile as readFile6, rm as rm2 } from "fs/promises";
|
|
6303
|
+
import { homedir as homedir5 } from "os";
|
|
6130
6304
|
import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
|
|
6131
6305
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
6132
6306
|
import { createRequire } from "module";
|
|
@@ -6135,7 +6309,7 @@ import { createRequire } from "module";
|
|
|
6135
6309
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6136
6310
|
import { existsSync as existsSync4 } from "fs";
|
|
6137
6311
|
import { readFile as readFile4 } from "fs/promises";
|
|
6138
|
-
import { homedir as
|
|
6312
|
+
import { homedir as homedir4 } from "os";
|
|
6139
6313
|
import { isAbsolute, join as join6 } from "path";
|
|
6140
6314
|
var SUPPORTED_AGENT_IDS = [
|
|
6141
6315
|
"claude",
|
|
@@ -6191,7 +6365,7 @@ function stableNodeCommand() {
|
|
|
6191
6365
|
});
|
|
6192
6366
|
return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
|
|
6193
6367
|
}
|
|
6194
|
-
function agentSkillPath(agent, homeDir =
|
|
6368
|
+
function agentSkillPath(agent, homeDir = homedir4(), projectDir) {
|
|
6195
6369
|
switch (agent) {
|
|
6196
6370
|
case "claude":
|
|
6197
6371
|
return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
@@ -6216,7 +6390,7 @@ function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
|
|
|
6216
6390
|
return void 0;
|
|
6217
6391
|
}
|
|
6218
6392
|
}
|
|
6219
|
-
function agentConfigPaths(agent, homeDir =
|
|
6393
|
+
function agentConfigPaths(agent, homeDir = homedir4(), projectDir) {
|
|
6220
6394
|
switch (agent) {
|
|
6221
6395
|
case "claude":
|
|
6222
6396
|
return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
@@ -6308,7 +6482,7 @@ function agentCliNames(agent) {
|
|
|
6308
6482
|
return ["qchat", "q"];
|
|
6309
6483
|
}
|
|
6310
6484
|
}
|
|
6311
|
-
function agentConfigExists(agent, homeDir =
|
|
6485
|
+
function agentConfigExists(agent, homeDir = homedir4(), projectDir) {
|
|
6312
6486
|
return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync4(config.path));
|
|
6313
6487
|
}
|
|
6314
6488
|
function claudeDesktopConfigPath(homeDir) {
|
|
@@ -6321,7 +6495,7 @@ function vscodeUserConfigPath(homeDir) {
|
|
|
6321
6495
|
if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
|
|
6322
6496
|
return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
|
|
6323
6497
|
}
|
|
6324
|
-
async function readAgentRegistrations(agent, homeDir =
|
|
6498
|
+
async function readAgentRegistrations(agent, homeDir = homedir4(), projectDir) {
|
|
6325
6499
|
const registrations = [];
|
|
6326
6500
|
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
6327
6501
|
if (!existsSync4(config.path)) continue;
|
|
@@ -6586,7 +6760,7 @@ function message(error) {
|
|
|
6586
6760
|
|
|
6587
6761
|
// src/commands/agent-config.ts
|
|
6588
6762
|
import { existsSync as existsSync5 } from "fs";
|
|
6589
|
-
import { mkdir as mkdir3, readFile as readFile5, rename as
|
|
6763
|
+
import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
6590
6764
|
import { dirname as dirname5 } from "path";
|
|
6591
6765
|
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
6592
6766
|
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
@@ -6753,7 +6927,7 @@ async function atomicWrite(path, contents) {
|
|
|
6753
6927
|
await mkdir3(dirname5(path), { recursive: true });
|
|
6754
6928
|
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
6755
6929
|
await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
6756
|
-
await
|
|
6930
|
+
await rename3(temporaryPath, path);
|
|
6757
6931
|
}
|
|
6758
6932
|
function withoutMcpServerSections(text2, name) {
|
|
6759
6933
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -6859,8 +7033,8 @@ function resolveTargets(opts) {
|
|
|
6859
7033
|
}))];
|
|
6860
7034
|
}
|
|
6861
7035
|
if (opts.all) return [...SUPPORTED_AGENT_IDS];
|
|
6862
|
-
const homeDir = opts.homeDir ??
|
|
6863
|
-
const detectBinaries = homeDir ===
|
|
7036
|
+
const homeDir = opts.homeDir ?? homedir5();
|
|
7037
|
+
const detectBinaries = homeDir === homedir5();
|
|
6864
7038
|
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
|
|
6865
7039
|
return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
|
|
6866
7040
|
}
|
|
@@ -6921,7 +7095,7 @@ async function installAgentSurfaces(opts) {
|
|
|
6921
7095
|
}
|
|
6922
7096
|
const mcp = resolveMcpCommand();
|
|
6923
7097
|
const registration = resolveMcpRegistration(opts);
|
|
6924
|
-
const homeDir = opts.homeDir ??
|
|
7098
|
+
const homeDir = opts.homeDir ?? homedir5();
|
|
6925
7099
|
const results = [];
|
|
6926
7100
|
for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
|
|
6927
7101
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
@@ -6956,7 +7130,7 @@ function assertAgentSetupSucceeded(results) {
|
|
|
6956
7130
|
}
|
|
6957
7131
|
}
|
|
6958
7132
|
async function agentSurfaceStatus(options = {}) {
|
|
6959
|
-
const homeDir = options.homeDir ??
|
|
7133
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
6960
7134
|
const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
|
|
6961
7135
|
const agents = resolveTargets({
|
|
6962
7136
|
agent: options.agent,
|
|
@@ -6980,7 +7154,7 @@ async function agentSurfaceStatus(options = {}) {
|
|
|
6980
7154
|
}));
|
|
6981
7155
|
}
|
|
6982
7156
|
async function uninstallAgentSurfaces(options) {
|
|
6983
|
-
const homeDir = options.homeDir ??
|
|
7157
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
6984
7158
|
const name = options.name?.trim() || "topolo";
|
|
6985
7159
|
resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
|
|
6986
7160
|
const removed = [];
|
|
@@ -6991,7 +7165,7 @@ async function uninstallAgentSurfaces(options) {
|
|
|
6991
7165
|
const remaining = await readAgentRegistrations(agent, homeDir, options.project);
|
|
6992
7166
|
const skillPath = agentSkillPath(agent, homeDir, options.project);
|
|
6993
7167
|
const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
|
|
6994
|
-
if (skillRemoved && skillPath) await
|
|
7168
|
+
if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
|
|
6995
7169
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
6996
7170
|
}
|
|
6997
7171
|
return removed;
|
|
@@ -7021,7 +7195,7 @@ function formatStatus(rows) {
|
|
|
7021
7195
|
|
|
7022
7196
|
// src/commands/doctor.ts
|
|
7023
7197
|
import { access } from "fs/promises";
|
|
7024
|
-
import { homedir as
|
|
7198
|
+
import { homedir as homedir6 } from "os";
|
|
7025
7199
|
import { resolve as resolve3 } from "path";
|
|
7026
7200
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7027
7201
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -7084,7 +7258,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
|
|
|
7084
7258
|
}
|
|
7085
7259
|
async function resolveInstalledDoctorTarget(options) {
|
|
7086
7260
|
const name = options.name?.trim() || "topolo";
|
|
7087
|
-
const homeDir = options.agentHome ? resolve3(options.agentHome) :
|
|
7261
|
+
const homeDir = options.agentHome ? resolve3(options.agentHome) : homedir6();
|
|
7088
7262
|
const expected = resolveMcpCommand();
|
|
7089
7263
|
const requestedAgents = resolveTargets({
|
|
7090
7264
|
...options,
|