@topolo/cli 0.13.1 → 0.13.3
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 +68 -2
- package/agent-packs/coverage_changed.py +122 -0
- package/agent-packs/src/topolo_agent_pack/approvals.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +42 -4
- package/agent-packs/src/topolo_agent_pack/execution.py +15 -5
- package/agent-packs/src/topolo_agent_pack/live.py +186 -5
- package/agent-packs/src/topolo_agent_pack/permissions.py +167 -0
- package/agent-packs/src/topolo_agent_pack/policy.py +11 -6
- package/agent-packs/src/topolo_agent_pack/topolo.py +12 -0
- package/dist/{claude-pack-backend-IDQQX3DG.js → claude-pack-backend-5DWMIDRA.js} +442 -10
- package/dist/index.js +242 -68
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/commands/agent-packs.ts
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
|
-
import { existsSync } from "fs";
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
6
|
import { dirname, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { homedir } from "os";
|
|
@@ -10,7 +10,7 @@ function registerAgentPacksCommand(program2) {
|
|
|
10
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
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
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-
|
|
13
|
+
const { configureClaudePack } = await import("./claude-pack-backend-5DWMIDRA.js");
|
|
14
14
|
const result = await configureClaudePack({
|
|
15
15
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
16
16
|
pack,
|
|
@@ -34,6 +34,8 @@ function registerAgentPacksCommand(program2) {
|
|
|
34
34
|
"recover",
|
|
35
35
|
"doctor",
|
|
36
36
|
"policy",
|
|
37
|
+
"permissions",
|
|
38
|
+
"approvals",
|
|
37
39
|
"remote-recover",
|
|
38
40
|
"connect",
|
|
39
41
|
"remote-context",
|
|
@@ -44,12 +46,38 @@ function registerAgentPacksCommand(program2) {
|
|
|
44
46
|
for (const name of commands) {
|
|
45
47
|
const command = packs.command(name).description(name === "add" ? "Add an installed or bundled pack, group, or agent" : `Run the pack ${name} operation`);
|
|
46
48
|
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");
|
|
49
|
+
if (name === "approvals") command.option("--intake <id>", "Exact owning native intake identity").option("--fingerprint <hash>", "Exact pending native request fingerprint").option("--response <path>", "Recorded one-request response JSON").option("--human-decision-recorded", "Assert that the exact human decision is recorded in the intake");
|
|
47
50
|
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
51
|
if (!["codex", "claude"].includes(options.harness)) throw new Error(`The ${options.harness} pack adapter is not available. Supported adapters: codex, claude.`);
|
|
52
|
+
if (name === "approvals" && options.harness === "claude" && !options.adapterHelp) {
|
|
53
|
+
if (!["list", "details", "respond"].includes(args[0] ?? "") || args.length > 2)
|
|
54
|
+
throw new Error("Approvals requires list, details KEY, or respond KEY.");
|
|
55
|
+
if (args[0] !== "list" && (!args[1] || !options.intake || !options.fingerprint))
|
|
56
|
+
throw new Error("Exact request key, owning intake and fingerprint required.");
|
|
57
|
+
let decision;
|
|
58
|
+
if (args[0] === "respond") {
|
|
59
|
+
if (!options.response || !options.humanDecisionRecorded) throw new Error("Recorded human response required.");
|
|
60
|
+
const response = JSON.parse(readFileSync(options.response, "utf8"));
|
|
61
|
+
if (Object.keys(response).join(",") !== "decision" || !["once", "deny"].includes(response.decision))
|
|
62
|
+
throw new Error("Only a one-request once/deny response is supported.");
|
|
63
|
+
decision = response.decision;
|
|
64
|
+
}
|
|
65
|
+
const { operateClaudeApprovals } = await import("./claude-pack-backend-5DWMIDRA.js");
|
|
66
|
+
console.log(JSON.stringify(await operateClaudeApprovals({
|
|
67
|
+
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
68
|
+
kind: args[0],
|
|
69
|
+
key: args[1],
|
|
70
|
+
intake: options.intake,
|
|
71
|
+
fingerprint: options.fingerprint,
|
|
72
|
+
decision,
|
|
73
|
+
humanDecisionRecorded: options.humanDecisionRecorded
|
|
74
|
+
}), null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
49
77
|
if (options.policy || options.endpoint || options.pid || options.apply) {
|
|
50
78
|
if (options.harness !== "claude" || !options.policy || !options.endpoint || !options.pid || args.length !== 1)
|
|
51
79
|
throw new Error("Native switch/recovery requires one Claude pack, --policy, --endpoint and --pid.");
|
|
52
|
-
const { configureClaudePack } = await import("./claude-pack-backend-
|
|
80
|
+
const { configureClaudePack } = await import("./claude-pack-backend-5DWMIDRA.js");
|
|
53
81
|
const result2 = await configureClaudePack({
|
|
54
82
|
store: packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
55
83
|
pack: args[0],
|
|
@@ -67,6 +95,12 @@ function registerAgentPacksCommand(program2) {
|
|
|
67
95
|
if (!adapter) throw new Error("This CLI installation is missing its bundled Topolo Agent Pack adapter. Reinstall @topolo/cli.");
|
|
68
96
|
const store = packs.opts().store;
|
|
69
97
|
const forwarded = (options.adapterHelp ? ["--help"] : args).map((value) => value === "--group" ? "--set" : value);
|
|
98
|
+
if (name === "approvals" && !options.adapterHelp) {
|
|
99
|
+
if (options.intake) forwarded.push("--intake", options.intake);
|
|
100
|
+
if (options.fingerprint) forwarded.push("--fingerprint", options.fingerprint);
|
|
101
|
+
if (options.response) forwarded.push("--response", options.response);
|
|
102
|
+
if (options.humanDecisionRecorded) forwarded.push("--human-decision-recorded");
|
|
103
|
+
}
|
|
70
104
|
const result = spawnSync("python3", [
|
|
71
105
|
adapter,
|
|
72
106
|
...store ? ["--store", store] : [],
|
|
@@ -90,7 +124,7 @@ import { platform as platform2 } from "os";
|
|
|
90
124
|
import { createInterface } from "readline/promises";
|
|
91
125
|
import { stdin as input } from "process";
|
|
92
126
|
|
|
93
|
-
// node_modules
|
|
127
|
+
// node_modules/@topolo/sdk/dist/index.js
|
|
94
128
|
function record(value) {
|
|
95
129
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
96
130
|
}
|
|
@@ -3187,9 +3221,18 @@ var TopoloOAuth = class {
|
|
|
3187
3221
|
if (!res.ok) {
|
|
3188
3222
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
3189
3223
|
const err = parsed;
|
|
3190
|
-
throw new TopoloAuthError(err.error_description || err.error, err.error
|
|
3224
|
+
throw new TopoloAuthError(err.error_description || err.error, err.error, {
|
|
3225
|
+
status: res.status,
|
|
3226
|
+
service: "auth",
|
|
3227
|
+
path,
|
|
3228
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
3229
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
3230
|
+
});
|
|
3191
3231
|
}
|
|
3192
|
-
throw new TopoloHttpError("auth", path, res.status, parsed
|
|
3232
|
+
throw new TopoloHttpError("auth", path, res.status, parsed, void 0, {
|
|
3233
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
3234
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
3235
|
+
});
|
|
3193
3236
|
}
|
|
3194
3237
|
return parsed;
|
|
3195
3238
|
}
|
|
@@ -3691,9 +3734,10 @@ async function pollUntilApproved(params) {
|
|
|
3691
3734
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3692
3735
|
}
|
|
3693
3736
|
|
|
3694
|
-
// node_modules
|
|
3695
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2,
|
|
3696
|
-
import { existsSync as existsSync2 } from "fs";
|
|
3737
|
+
// node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3738
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, lstat, readdir as readdir2, rename as rename2, link } from "fs/promises";
|
|
3739
|
+
import { existsSync as existsSync2, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync } from "fs";
|
|
3740
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3697
3741
|
import { homedir as homedir2, platform } from "os";
|
|
3698
3742
|
import { dirname as dirname2, join as join3 } from "path";
|
|
3699
3743
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -4101,9 +4145,18 @@ var TopoloOAuth2 = class {
|
|
|
4101
4145
|
if (!res.ok) {
|
|
4102
4146
|
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
4103
4147
|
const err = parsed;
|
|
4104
|
-
throw new TopoloAuthError2(err.error_description || err.error, err.error
|
|
4148
|
+
throw new TopoloAuthError2(err.error_description || err.error, err.error, {
|
|
4149
|
+
status: res.status,
|
|
4150
|
+
service: "auth",
|
|
4151
|
+
path,
|
|
4152
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
4153
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
4154
|
+
});
|
|
4105
4155
|
}
|
|
4106
|
-
throw new TopoloHttpError2("auth", path, res.status, parsed
|
|
4156
|
+
throw new TopoloHttpError2("auth", path, res.status, parsed, void 0, {
|
|
4157
|
+
requestId: res.headers.get("x-topolo-request-id") ?? res.headers.get("x-request-id") ?? void 0,
|
|
4158
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
4159
|
+
});
|
|
4107
4160
|
}
|
|
4108
4161
|
return parsed;
|
|
4109
4162
|
}
|
|
@@ -4550,39 +4603,150 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4550
4603
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4551
4604
|
var CONFIG_LOCK_RETRY_MS = 25;
|
|
4552
4605
|
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
4553
|
-
var
|
|
4606
|
+
var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
4607
|
+
constructor() {
|
|
4608
|
+
super("state_conflict", "Credential configuration ownership cannot be verified. Retain the unresolved operation instead of starting another renewal.");
|
|
4609
|
+
}
|
|
4610
|
+
};
|
|
4554
4611
|
async function withConfigWriteLock(operation) {
|
|
4555
4612
|
const lockPath = `${configPath()}.lock`;
|
|
4556
4613
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
4557
|
-
const startedAt =
|
|
4614
|
+
const startedAt = performance.now();
|
|
4558
4615
|
while (true) {
|
|
4559
4616
|
try {
|
|
4560
|
-
await mkdir2(lockPath);
|
|
4617
|
+
await mkdir2(lockPath, { mode: 448 });
|
|
4561
4618
|
break;
|
|
4562
4619
|
} catch (error) {
|
|
4563
4620
|
if (error.code !== "EEXIST") throw error;
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
4567
|
-
await rm2(lockPath, { recursive: true, force: true });
|
|
4568
|
-
continue;
|
|
4569
|
-
}
|
|
4570
|
-
} catch (inspectionError) {
|
|
4571
|
-
if (inspectionError.code !== "ENOENT") throw inspectionError;
|
|
4572
|
-
continue;
|
|
4573
|
-
}
|
|
4574
|
-
if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4575
|
-
throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
|
|
4621
|
+
if (performance.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
4622
|
+
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4576
4623
|
}
|
|
4577
4624
|
await new Promise((resolve4) => setTimeout(resolve4, CONFIG_LOCK_RETRY_MS));
|
|
4578
4625
|
}
|
|
4579
4626
|
}
|
|
4627
|
+
const directory = await lstat(lockPath);
|
|
4628
|
+
const identity = randomUUID2();
|
|
4629
|
+
const markerName = "owner-" + identity + ".json";
|
|
4630
|
+
const markerPath = join3(lockPath, markerName);
|
|
4631
|
+
const marker = JSON.stringify({ version: 1, identity, pid: process.pid });
|
|
4632
|
+
await writeFile2(markerPath, marker, { flag: "wx", mode: 384 });
|
|
4633
|
+
const assertDirectory = async () => {
|
|
4634
|
+
const current = await lstat(lockPath);
|
|
4635
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4636
|
+
throw new ConfigOwnershipError();
|
|
4637
|
+
};
|
|
4638
|
+
const assertOwned = async () => {
|
|
4639
|
+
try {
|
|
4640
|
+
await assertDirectory();
|
|
4641
|
+
const entries = await readdir2(lockPath);
|
|
4642
|
+
if (entries.length !== 1 || entries[0] !== markerName) throw new ConfigOwnershipError();
|
|
4643
|
+
const entry = await lstat(markerPath);
|
|
4644
|
+
if (!entry.isFile() || entry.isSymbolicLink() || await readFile2(markerPath, "utf8") !== marker)
|
|
4645
|
+
throw new ConfigOwnershipError();
|
|
4646
|
+
} catch {
|
|
4647
|
+
throw new ConfigOwnershipError();
|
|
4648
|
+
}
|
|
4649
|
+
};
|
|
4650
|
+
const assertDirectorySync = () => {
|
|
4651
|
+
const current = lstatSync(lockPath);
|
|
4652
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== directory.dev || current.ino !== directory.ino)
|
|
4653
|
+
throw new ConfigOwnershipError();
|
|
4654
|
+
};
|
|
4655
|
+
const assertOwnedSync = () => {
|
|
4656
|
+
assertDirectorySync();
|
|
4657
|
+
const entries = readdirSync(lockPath);
|
|
4658
|
+
const entry = lstatSync(markerPath);
|
|
4659
|
+
if (entries.length !== 1 || entries[0] !== markerName || !entry.isFile() || entry.isSymbolicLink() || readFileSync2(markerPath, "utf8") !== marker) throw new ConfigOwnershipError();
|
|
4660
|
+
};
|
|
4661
|
+
const persist = async (update) => {
|
|
4662
|
+
const path = configPath();
|
|
4663
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
4664
|
+
const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
|
|
4665
|
+
let temporaryOwned = false;
|
|
4666
|
+
try {
|
|
4667
|
+
assertOwnedSync();
|
|
4668
|
+
const entry = lstatSync(path);
|
|
4669
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
|
|
4670
|
+
const before = readFileSync2(path, "utf8");
|
|
4671
|
+
const updated = update(JSON.parse(before));
|
|
4672
|
+
const fd = openSync(temporary, "wx", 384);
|
|
4673
|
+
temporaryOwned = true;
|
|
4674
|
+
try {
|
|
4675
|
+
writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
|
|
4676
|
+
fsyncSync(fd);
|
|
4677
|
+
} finally {
|
|
4678
|
+
closeSync(fd);
|
|
4679
|
+
}
|
|
4680
|
+
assertOwnedSync();
|
|
4681
|
+
if (readFileSync2(path, "utf8") !== before) throw new ConfigOwnershipError();
|
|
4682
|
+
renameSync(temporary, path);
|
|
4683
|
+
temporaryOwned = false;
|
|
4684
|
+
} finally {
|
|
4685
|
+
if (temporaryOwned) unlinkSync(temporary);
|
|
4686
|
+
}
|
|
4687
|
+
};
|
|
4688
|
+
const release = async () => {
|
|
4689
|
+
await assertOwned();
|
|
4690
|
+
const quarantine = join3(dirname2(lockPath), ".credential-release-" + randomUUID2());
|
|
4691
|
+
await mkdir2(quarantine, { mode: 448 });
|
|
4692
|
+
const captured = join3(quarantine, markerName);
|
|
4693
|
+
try {
|
|
4694
|
+
await assertOwned();
|
|
4695
|
+
await rename2(markerPath, captured);
|
|
4696
|
+
assertDirectorySync();
|
|
4697
|
+
const entry = lstatSync(captured);
|
|
4698
|
+
if (!entry.isFile() || entry.isSymbolicLink() || readFileSync2(captured, "utf8") !== marker || readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4699
|
+
unlinkSync(captured);
|
|
4700
|
+
rmdirSync(quarantine);
|
|
4701
|
+
assertDirectorySync();
|
|
4702
|
+
if (readdirSync(lockPath).length) throw new ConfigOwnershipError();
|
|
4703
|
+
rmdirSync(lockPath);
|
|
4704
|
+
} catch {
|
|
4705
|
+
if (existsSync2(captured)) {
|
|
4706
|
+
try {
|
|
4707
|
+
await link(captured, markerPath);
|
|
4708
|
+
} catch {
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
throw new ConfigOwnershipError();
|
|
4712
|
+
}
|
|
4713
|
+
};
|
|
4580
4714
|
try {
|
|
4581
|
-
|
|
4715
|
+
await assertOwned();
|
|
4716
|
+
const result = await operation(assertOwned, persist);
|
|
4717
|
+
await assertOwned();
|
|
4718
|
+
return result;
|
|
4582
4719
|
} finally {
|
|
4583
|
-
await
|
|
4720
|
+
await release();
|
|
4584
4721
|
}
|
|
4585
4722
|
}
|
|
4723
|
+
function renewalFailure(error) {
|
|
4724
|
+
const path = "/api/developer-oauth/token";
|
|
4725
|
+
if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
|
|
4726
|
+
if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
|
|
4727
|
+
return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
|
|
4728
|
+
status: error.status,
|
|
4729
|
+
service: "auth",
|
|
4730
|
+
path,
|
|
4731
|
+
requestId: error.requestId
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
if (error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) {
|
|
4735
|
+
return new TopoloHttpError2(
|
|
4736
|
+
"auth",
|
|
4737
|
+
path,
|
|
4738
|
+
error.status,
|
|
4739
|
+
void 0,
|
|
4740
|
+
"Topolo sign-in renewal failed. Retry the request; the saved sign-in has been retained.",
|
|
4741
|
+
{ requestId: error.requestId, retryAfter: error.retryAfter }
|
|
4742
|
+
);
|
|
4743
|
+
}
|
|
4744
|
+
const timeout = error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
4745
|
+
return new TopoloSdkError2(
|
|
4746
|
+
timeout ? "request_timeout" : "network_error",
|
|
4747
|
+
"Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
|
|
4748
|
+
);
|
|
4749
|
+
}
|
|
4586
4750
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
4587
4751
|
const prefix = envVarPrefix(environment);
|
|
4588
4752
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
@@ -4591,16 +4755,16 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4591
4755
|
const observedProfile = profileForEnvironment(await readConfig(), environment);
|
|
4592
4756
|
const observed = observedProfile.credential;
|
|
4593
4757
|
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;
|
|
4594
|
-
return withConfigWriteLock(async () => {
|
|
4758
|
+
return withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4595
4759
|
const config = await readConfig();
|
|
4596
4760
|
const profile = profileForEnvironment(config, environment);
|
|
4597
4761
|
const cred = profile.credential;
|
|
4598
4762
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
4599
4763
|
if (!cred.refreshToken || !profile.oauthClientId) return cred;
|
|
4600
|
-
|
|
4764
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4765
|
+
if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken && cred.expiresAt !== void 0 && cred.expiresAt > now) {
|
|
4601
4766
|
return cred;
|
|
4602
4767
|
}
|
|
4603
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
4604
4768
|
if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
4605
4769
|
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4606
4770
|
if (environment === "local" && !serviceUrls?.auth) {
|
|
@@ -4623,23 +4787,27 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
4623
4787
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
4624
4788
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
4625
4789
|
};
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
credential
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4790
|
+
await persist((current) => {
|
|
4791
|
+
const latest = profileForEnvironment(current, environment);
|
|
4792
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
4793
|
+
if (!same(latest.credential, cred) || latest.oauthClientId !== profile.oauthClientId || latest.activeContext !== profile.activeContext)
|
|
4794
|
+
throw new ConfigOwnershipError();
|
|
4795
|
+
const activeContext = latest.activeContext;
|
|
4796
|
+
const activeEntry = activeContext ? latest.contexts?.[activeContext] : void 0;
|
|
4797
|
+
const previousEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
4798
|
+
if (!same(activeEntry?.credential, previousEntry?.credential)) throw new ConfigOwnershipError();
|
|
4799
|
+
return activeContext && activeEntry ? writeProfileConfig(current, environment, {
|
|
4800
|
+
credential: refreshed,
|
|
4801
|
+
contexts: { ...latest.contexts, [activeContext]: {
|
|
4633
4802
|
...activeEntry,
|
|
4634
4803
|
credential: refreshed,
|
|
4635
|
-
oauthClientId: activeEntry.oauthClientId ??
|
|
4636
|
-
}
|
|
4637
|
-
}
|
|
4638
|
-
})
|
|
4639
|
-
await writeConfig(updated);
|
|
4804
|
+
oauthClientId: activeEntry.oauthClientId ?? latest.oauthClientId
|
|
4805
|
+
} }
|
|
4806
|
+
}) : environment === "production" && !current.profiles?.production ? { ...current, credential: refreshed } : writeProfileConfig(current, environment, { credential: refreshed });
|
|
4807
|
+
});
|
|
4640
4808
|
return refreshed;
|
|
4641
|
-
} catch {
|
|
4642
|
-
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
throw renewalFailure(error);
|
|
4643
4811
|
}
|
|
4644
4812
|
});
|
|
4645
4813
|
}
|
|
@@ -4648,7 +4816,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4648
4816
|
const observedContext = observedProfile.contexts?.[key];
|
|
4649
4817
|
const observed = observedContext?.credential;
|
|
4650
4818
|
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;
|
|
4651
|
-
return withConfigWriteLock(async () => {
|
|
4819
|
+
return withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4652
4820
|
const config = await readConfig();
|
|
4653
4821
|
const profile = profileForEnvironment(config, environment);
|
|
4654
4822
|
const context = profile.contexts?.[key];
|
|
@@ -4657,6 +4825,7 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4657
4825
|
const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
|
|
4658
4826
|
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
4659
4827
|
const now = Math.floor(Date.now() / 1e3);
|
|
4828
|
+
if (options.force && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return credential;
|
|
4660
4829
|
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
4661
4830
|
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
4662
4831
|
if (environment === "local" && !serviceUrls?.auth) {
|
|
@@ -4677,16 +4846,21 @@ async function refreshContextCredentialIfNeeded(environment, key, options = {})
|
|
|
4677
4846
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
|
|
4678
4847
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
4679
4848
|
};
|
|
4680
|
-
await
|
|
4681
|
-
|
|
4682
|
-
contexts
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4849
|
+
await persist((current) => {
|
|
4850
|
+
const latest = profileForEnvironment(current, environment);
|
|
4851
|
+
const latestContext = latest.contexts?.[key];
|
|
4852
|
+
if (!latestContext || JSON.stringify(latestContext.credential) !== JSON.stringify(credential) || (latestContext.oauthClientId ?? latest.oauthClientId) !== oauthClientId || latestContext.contextType !== context.contextType || latestContext.organizationId !== context.organizationId)
|
|
4853
|
+
throw new ConfigOwnershipError();
|
|
4854
|
+
if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
|
|
4855
|
+
throw new ConfigOwnershipError();
|
|
4856
|
+
return writeProfileConfig(current, environment, {
|
|
4857
|
+
...latest.activeContext === key ? { credential: refreshed } : {},
|
|
4858
|
+
contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
|
|
4859
|
+
});
|
|
4860
|
+
});
|
|
4687
4861
|
return refreshed;
|
|
4688
|
-
} catch {
|
|
4689
|
-
|
|
4862
|
+
} catch (error) {
|
|
4863
|
+
throw renewalFailure(error);
|
|
4690
4864
|
}
|
|
4691
4865
|
});
|
|
4692
4866
|
}
|
|
@@ -4845,7 +5019,7 @@ function normalizeError(err) {
|
|
|
4845
5019
|
}
|
|
4846
5020
|
|
|
4847
5021
|
// src/version.ts
|
|
4848
|
-
import { readFileSync } from "fs";
|
|
5022
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4849
5023
|
import { dirname as dirname4, join as join4 } from "path";
|
|
4850
5024
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4851
5025
|
function readCliVersion() {
|
|
@@ -4857,7 +5031,7 @@ function readCliVersion() {
|
|
|
4857
5031
|
];
|
|
4858
5032
|
for (const path of candidates) {
|
|
4859
5033
|
try {
|
|
4860
|
-
const pkg = JSON.parse(
|
|
5034
|
+
const pkg = JSON.parse(readFileSync3(path, "utf8"));
|
|
4861
5035
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
4862
5036
|
} catch {
|
|
4863
5037
|
}
|
|
@@ -5193,19 +5367,19 @@ function formatPretty(info) {
|
|
|
5193
5367
|
|
|
5194
5368
|
// src/commands/actions.ts
|
|
5195
5369
|
import { spawn as spawn2 } from "child_process";
|
|
5196
|
-
import { readFileSync as
|
|
5370
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5197
5371
|
import { platform as platform3 } from "os";
|
|
5198
5372
|
import { basename, extname } from "path";
|
|
5199
5373
|
|
|
5200
5374
|
// src/json-input.ts
|
|
5201
|
-
import { readFileSync as
|
|
5375
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5202
5376
|
function parseJsonInput(value) {
|
|
5203
5377
|
let source = value;
|
|
5204
5378
|
if (value.startsWith("@")) {
|
|
5205
5379
|
const path = value.slice(1);
|
|
5206
5380
|
if (!path) throw new Error("--data @file path must not be empty");
|
|
5207
5381
|
try {
|
|
5208
|
-
source =
|
|
5382
|
+
source = readFileSync4(path, "utf8");
|
|
5209
5383
|
} catch (err) {
|
|
5210
5384
|
throw new Error(`Could not read --data file ${path}: ${err.message}`);
|
|
5211
5385
|
}
|
|
@@ -5402,7 +5576,7 @@ Approval required. ${opened ? "Opening" : "Open"} ${approval.verificationUrl}` :
|
|
|
5402
5576
|
const resource = parseResourceOption(opts.resourceType, opts.resourceId);
|
|
5403
5577
|
const result = await topolo.client.uploadAction(
|
|
5404
5578
|
action,
|
|
5405
|
-
|
|
5579
|
+
readFileSync5(opts.file),
|
|
5406
5580
|
parseJsonObject(opts.data || "{}"),
|
|
5407
5581
|
{
|
|
5408
5582
|
fileName: basename(opts.file),
|
|
@@ -5974,7 +6148,7 @@ function normalizeList(values, fallback = []) {
|
|
|
5974
6148
|
}
|
|
5975
6149
|
|
|
5976
6150
|
// src/commands/integrations.ts
|
|
5977
|
-
import { readdir as
|
|
6151
|
+
import { readdir as readdir3, readFile as readFile3, stat } from "fs/promises";
|
|
5978
6152
|
import { join as join5, relative } from "path";
|
|
5979
6153
|
function registerIntegrationsCommand(program2) {
|
|
5980
6154
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
@@ -6128,7 +6302,7 @@ async function collectFiles(repoRoot) {
|
|
|
6128
6302
|
const root = repoRoot;
|
|
6129
6303
|
async function walk(dir) {
|
|
6130
6304
|
if (result.length > 2e3) return;
|
|
6131
|
-
const entries = await
|
|
6305
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
6132
6306
|
for (const entry of entries) {
|
|
6133
6307
|
const fullPath = join5(dir, entry.name);
|
|
6134
6308
|
if (entry.isDirectory()) {
|
|
@@ -6137,7 +6311,7 @@ async function collectFiles(repoRoot) {
|
|
|
6137
6311
|
continue;
|
|
6138
6312
|
}
|
|
6139
6313
|
if (!entry.isFile() || !isInspectable(entry.name)) continue;
|
|
6140
|
-
const info = await
|
|
6314
|
+
const info = await stat(fullPath);
|
|
6141
6315
|
if (info.size > 25e4) continue;
|
|
6142
6316
|
result.push({
|
|
6143
6317
|
path: relative(root, fullPath),
|
|
@@ -6159,7 +6333,7 @@ function formatCheckResult(result) {
|
|
|
6159
6333
|
// src/commands/skills.ts
|
|
6160
6334
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
6161
6335
|
import { existsSync as existsSync6 } from "fs";
|
|
6162
|
-
import { readFile as readFile6, rm as
|
|
6336
|
+
import { readFile as readFile6, rm as rm2 } from "fs/promises";
|
|
6163
6337
|
import { homedir as homedir5 } from "os";
|
|
6164
6338
|
import { dirname as dirname6, join as join7, resolve as resolve2 } from "path";
|
|
6165
6339
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
@@ -6620,7 +6794,7 @@ function message(error) {
|
|
|
6620
6794
|
|
|
6621
6795
|
// src/commands/agent-config.ts
|
|
6622
6796
|
import { existsSync as existsSync5 } from "fs";
|
|
6623
|
-
import { mkdir as mkdir3, readFile as readFile5, rename as
|
|
6797
|
+
import { mkdir as mkdir3, readFile as readFile5, rename as rename3, writeFile as writeFile3 } from "fs/promises";
|
|
6624
6798
|
import { dirname as dirname5 } from "path";
|
|
6625
6799
|
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
6626
6800
|
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
@@ -6787,7 +6961,7 @@ async function atomicWrite(path, contents) {
|
|
|
6787
6961
|
await mkdir3(dirname5(path), { recursive: true });
|
|
6788
6962
|
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
6789
6963
|
await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
6790
|
-
await
|
|
6964
|
+
await rename3(temporaryPath, path);
|
|
6791
6965
|
}
|
|
6792
6966
|
function withoutMcpServerSections(text2, name) {
|
|
6793
6967
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -7025,7 +7199,7 @@ async function uninstallAgentSurfaces(options) {
|
|
|
7025
7199
|
const remaining = await readAgentRegistrations(agent, homeDir, options.project);
|
|
7026
7200
|
const skillPath = agentSkillPath(agent, homeDir, options.project);
|
|
7027
7201
|
const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync6(skillPath));
|
|
7028
|
-
if (skillRemoved && skillPath) await
|
|
7202
|
+
if (skillRemoved && skillPath) await rm2(dirname6(skillPath), { recursive: true, force: true });
|
|
7029
7203
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
7030
7204
|
}
|
|
7031
7205
|
return removed;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.13.
|
|
5
|
-
"topoloPublishedVersion": "0.13.
|
|
4
|
+
"version": "0.13.3",
|
|
5
|
+
"topoloPublishedVersion": "0.13.3",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"test:watch": "vitest",
|
|
28
28
|
"coverage": "pnpm run coverage:node && pnpm run coverage:adapter",
|
|
29
29
|
"coverage:node": "vitest run --coverage",
|
|
30
|
-
"coverage:adapter": "python3 -m venv node_modules/.cache/agent-pack-test && node_modules/.cache/agent-pack-test/bin/python -m pip install --disable-pip-version-check --quiet --timeout 15 --retries 0 -r agent-packs/requirements-test.txt && PYTHONPATH=agent-packs/src COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage run --branch --source=topolo_agent_pack.topolo,topolo_agent_pack.claude,topolo_agent_pack.policy -m unittest discover -s agent-packs/tests && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report --fail-under=100"
|
|
30
|
+
"coverage:adapter": "python3 -m venv node_modules/.cache/agent-pack-test && node_modules/.cache/agent-pack-test/bin/python -m pip install --disable-pip-version-check --quiet --timeout 15 --retries 0 -r agent-packs/requirements-test.txt && PYTHONPATH=agent-packs/src:agent-packs COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage run --branch --source=topolo_agent_pack.topolo,topolo_agent_pack.claude,topolo_agent_pack.policy,topolo_agent_pack.permissions,topolo_agent_pack.approvals,topolo_agent_pack.execution,topolo_agent_pack.live,topolo_agent_pack.cli,coverage_changed -m unittest discover -s agent-packs/tests && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage report --include='*/topolo_agent_pack/topolo.py,*/topolo_agent_pack/claude.py,*/topolo_agent_pack/policy.py,*/topolo_agent_pack/permissions.py,*/topolo_agent_pack/approvals.py,*/topolo_agent_pack/execution.py,*/agent-packs/coverage_changed.py' --fail-under=100 && COVERAGE_FILE=node_modules/.cache/agent-pack-test/.coverage node_modules/.cache/agent-pack-test/bin/python -m coverage json -o node_modules/.cache/agent-pack-test/coverage.json && node_modules/.cache/agent-pack-test/bin/python agent-packs/coverage_changed.py"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
-
"@topolo/mcp": "0.14.
|
|
35
|
-
"@topolo/sdk": "0.23.
|
|
34
|
+
"@topolo/mcp": "0.14.23",
|
|
35
|
+
"@topolo/sdk": "0.23.18",
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"undici": "6.29.0"
|
|
38
38
|
},
|