@camerontaylor/paseo-cli 0.12.0-beta.1.fork.1 → 0.13.0-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -0
- package/dist/commands/agent/archive.js +2 -2
- package/dist/commands/agent/ls.js +2 -12
- package/dist/commands/agent/send.d.ts +3 -1
- package/dist/commands/agent/send.js +10 -4
- package/dist/commands/daemon/pair.js +1 -0
- package/dist/commands/host/index.d.ts +3 -0
- package/dist/commands/host/index.js +92 -0
- package/dist/commands/plugin/scaffold.js +4 -0
- package/dist/commands/usage/presentation.js +52 -28
- package/dist/ssh/ssh-tunnel.d.ts +2 -8
- package/dist/ssh/ssh-tunnel.js +1 -78
- package/dist/utils/agent-directory.d.ts +17 -0
- package/dist/utils/agent-directory.js +17 -0
- package/dist/utils/client.js +22 -125
- package/dist/utils/daemon-target.js +22 -10
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHostCommand } from "./commands/host/index.js";
|
|
1
2
|
import { pairCommand } from "./commands/daemon/pair.js";
|
|
2
3
|
import { Command, Option } from "commander";
|
|
3
4
|
import { createAgentCommand } from "./commands/agent/index.js";
|
|
@@ -50,6 +51,7 @@ export function createCli() {
|
|
|
50
51
|
.option("--no-headers", "omit table headers")
|
|
51
52
|
.option("--no-color", "disable colored output");
|
|
52
53
|
addDaemonHostOption(program);
|
|
54
|
+
program.addCommand(createHostCommand());
|
|
53
55
|
// Primary agent commands (top-level)
|
|
54
56
|
addJsonAndDaemonHostOptions(addLsOptions(program.command("ls"))).action(withOutput(runLsCommand));
|
|
55
57
|
addJsonAndDaemonHostOptions(addRunOptions(program.command("run"))).action(withOutput(runRunCommand));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectToDaemon, resolveAgentId } from "../../utils/client.js";
|
|
2
|
+
import { fetchAllAgents } from "../../utils/agent-directory.js";
|
|
2
3
|
/** Schema for archive command output */
|
|
3
4
|
export const archiveSchema = {
|
|
4
5
|
idField: "agentId",
|
|
@@ -26,8 +27,7 @@ export async function runArchiveCommand(agentIdArg, options, _command) {
|
|
|
26
27
|
}
|
|
27
28
|
const client = await connectToDaemon({ target: options.daemonTarget });
|
|
28
29
|
try {
|
|
29
|
-
const
|
|
30
|
-
const agents = agentsPayload.entries.map((entry) => entry.agent);
|
|
30
|
+
const agents = await fetchAllAgents(client, { filter: { includeArchived: true } });
|
|
31
31
|
const agentId = resolveAgentId(agentIdArg, agents);
|
|
32
32
|
if (!agentId) {
|
|
33
33
|
const error = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectToDaemon } from "../../utils/client.js";
|
|
2
|
+
import { fetchAllAgents } from "../../utils/agent-directory.js";
|
|
2
3
|
import { collectMultiple } from "../../utils/command-options.js";
|
|
3
4
|
import { isSameOrDescendantPath } from "../../utils/paths.js";
|
|
4
5
|
export function addLsOptions(cmd) {
|
|
@@ -139,18 +140,7 @@ export async function listAgents(options, connect = connectToDaemon) {
|
|
|
139
140
|
}
|
|
140
141
|
const labelFilters = parseLabelFilters(options.label);
|
|
141
142
|
const fetchOptions = buildAgentLsFetchOptions(options);
|
|
142
|
-
let agents =
|
|
143
|
-
let cursor;
|
|
144
|
-
// createdAt never changes, so agents cannot move past the cursor while the pages are read.
|
|
145
|
-
do {
|
|
146
|
-
const payload = await client.fetchAgents({
|
|
147
|
-
...fetchOptions,
|
|
148
|
-
sort: [{ key: "created_at", direction: "desc" }],
|
|
149
|
-
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
|
150
|
-
});
|
|
151
|
-
agents.push(...payload.entries.map((entry) => entry.agent));
|
|
152
|
-
cursor = payload.pageInfo.nextCursor ?? undefined;
|
|
153
|
-
} while (cursor);
|
|
143
|
+
let agents = await fetchAllAgents(client, fetchOptions);
|
|
154
144
|
// By default, exclude archived agents. `-a` includes them.
|
|
155
145
|
if (!options.all) {
|
|
156
146
|
agents = agents.filter((a) => !a.archivedAt);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { type Command } from "commander";
|
|
2
|
+
import type { ActiveTurnBehavior } from "@camerontaylor/paseo-protocol/messages";
|
|
2
3
|
import type { CommandOptions, SingleResult, OutputSchema } from "../../output/index.js";
|
|
3
4
|
/** Result type for agent send command */
|
|
4
5
|
export interface AgentSendResult {
|
|
5
6
|
agentId: string;
|
|
6
|
-
status: "sent" | "completed" | "timeout" | "permission" | "error";
|
|
7
|
+
status: "queued" | "sent" | "completed" | "timeout" | "permission" | "error";
|
|
7
8
|
message: string;
|
|
8
9
|
}
|
|
9
10
|
/** Schema for agent send output */
|
|
10
11
|
export declare const agentSendSchema: OutputSchema<AgentSendResult>;
|
|
11
12
|
export interface AgentSendOptions extends CommandOptions {
|
|
13
|
+
behavior?: ActiveTurnBehavior;
|
|
12
14
|
wait?: boolean;
|
|
13
15
|
image?: string[];
|
|
14
16
|
prompt?: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
1
2
|
import { collectMultiple } from "../../utils/command-options.js";
|
|
2
3
|
import { connectToDaemon } from "../../utils/client.js";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
@@ -19,6 +20,7 @@ export function addSendOptions(cmd) {
|
|
|
19
20
|
.option("--prompt <text>", "Provide the message inline as a flag")
|
|
20
21
|
.option("--prompt-file <path>", "Read the message from a UTF-8 text file")
|
|
21
22
|
.option("--image <path>", "Attach image(s) to the message", collectMultiple, [])
|
|
23
|
+
.addOption(new Option("--behavior <behavior>", "Active-turn behavior (agent default: steer; otherwise: interrupt)").choices(["interrupt", "steer"]))
|
|
22
24
|
.option("--no-wait", "Return immediately without waiting for completion");
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
@@ -138,10 +140,12 @@ export async function runSendCommand(agentIdArg, prompt, options, _command) {
|
|
|
138
140
|
try {
|
|
139
141
|
// Read image files if provided
|
|
140
142
|
const images = options.image && options.image.length > 0 ? await readImageFiles(options.image) : undefined;
|
|
143
|
+
const sourceAgentId = process.env.PASEO_AGENT_ID?.trim() || undefined;
|
|
141
144
|
// Send the message
|
|
142
|
-
await client.sendAgentMessage(agentIdArg, promptInput, {
|
|
145
|
+
const receipt = await client.sendAgentMessage(agentIdArg, promptInput, {
|
|
143
146
|
images,
|
|
144
|
-
sourceAgentId
|
|
147
|
+
sourceAgentId,
|
|
148
|
+
activeTurnBehavior: options.behavior ?? (sourceAgentId ? "steer" : "interrupt"),
|
|
145
149
|
});
|
|
146
150
|
// If --no-wait, return immediately
|
|
147
151
|
if (options.wait === false) {
|
|
@@ -150,8 +154,10 @@ export async function runSendCommand(agentIdArg, prompt, options, _command) {
|
|
|
150
154
|
type: "single",
|
|
151
155
|
data: {
|
|
152
156
|
agentId: agentIdArg,
|
|
153
|
-
status: "sent",
|
|
154
|
-
message:
|
|
157
|
+
status: receipt.deliveryStatus ?? "sent",
|
|
158
|
+
message: receipt.deliveryStatus === "queued"
|
|
159
|
+
? "Message queued for delivery after two quiet seconds"
|
|
160
|
+
: "Message sent, not waiting for completion",
|
|
155
161
|
},
|
|
156
162
|
schema: agentSendSchema,
|
|
157
163
|
};
|
|
@@ -47,6 +47,7 @@ export async function resolveLocalPairingOffer(options) {
|
|
|
47
47
|
relayPublicEndpoint: config.relayPublicEndpoint,
|
|
48
48
|
relayUseTls: config.relayUseTls,
|
|
49
49
|
relayPublicUseTls: config.relayPublicUseTls,
|
|
50
|
+
relayRequireProtectedSecurity: config.relayRequireProtectedSecurity,
|
|
50
51
|
appBaseUrl: config.appBaseUrl,
|
|
51
52
|
includeQr: true,
|
|
52
53
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { withOutput, } from "../../output/index.js";
|
|
3
|
+
import { addJsonAndDaemonHostOptions, collectMultiple } from "../../utils/command-options.js";
|
|
4
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
5
|
+
const schema = {
|
|
6
|
+
idField: "registrationId",
|
|
7
|
+
columns: [
|
|
8
|
+
{ header: "ID", field: "registrationId" },
|
|
9
|
+
{ header: "LABEL", field: "label" },
|
|
10
|
+
{ header: "SERVER", field: "serverId" },
|
|
11
|
+
{ header: "ENABLED", field: "enabled" },
|
|
12
|
+
{ header: "STATUS", field: "status" },
|
|
13
|
+
{ header: "SOURCE", field: (row) => `${row.source.id}/${row.source.key}` },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
function metadata(values = []) {
|
|
17
|
+
return Object.fromEntries(values.map((entry) => {
|
|
18
|
+
const split = entry.indexOf("=");
|
|
19
|
+
if (split < 1)
|
|
20
|
+
throw { code: "invalid_metadata", message: "Metadata must use key=value" };
|
|
21
|
+
return [entry.slice(0, split), entry.slice(split + 1)];
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
async function run(action, id, options) {
|
|
25
|
+
const client = await connectToDaemon({ target: options.daemonTarget });
|
|
26
|
+
try {
|
|
27
|
+
if (action === "add")
|
|
28
|
+
return {
|
|
29
|
+
type: "single",
|
|
30
|
+
data: await client.addHost({
|
|
31
|
+
connection: id,
|
|
32
|
+
label: options.label,
|
|
33
|
+
metadata: metadata(options.meta),
|
|
34
|
+
}),
|
|
35
|
+
schema,
|
|
36
|
+
};
|
|
37
|
+
if (action === "ls")
|
|
38
|
+
return { type: "list", data: (await client.listHosts()).hosts, schema };
|
|
39
|
+
const host = (await client.listHosts()).hosts.find((entry) => entry.registrationId === id);
|
|
40
|
+
if (!host)
|
|
41
|
+
throw { code: "host_unknown", message: `Unknown registration ${id}` };
|
|
42
|
+
if (action === "inspect")
|
|
43
|
+
return {
|
|
44
|
+
type: "single",
|
|
45
|
+
data: host,
|
|
46
|
+
schema: { ...schema, renderHuman: () => JSON.stringify(host, null, 2) },
|
|
47
|
+
};
|
|
48
|
+
if (action === "rm") {
|
|
49
|
+
await client.removeHost(id);
|
|
50
|
+
return { type: "single", data: host, schema };
|
|
51
|
+
}
|
|
52
|
+
const patch = {};
|
|
53
|
+
if (action === "enable" || action === "disable")
|
|
54
|
+
patch.enabled = action === "enable";
|
|
55
|
+
if (options.label !== undefined)
|
|
56
|
+
patch.label = options.label || null;
|
|
57
|
+
if (options.connection !== undefined)
|
|
58
|
+
patch.connection = options.connection;
|
|
59
|
+
if (options.replaceIdentity)
|
|
60
|
+
patch.replaceIdentity = true;
|
|
61
|
+
if (options.meta?.length || options.removeMeta?.length) {
|
|
62
|
+
patch.metadata = { ...host.metadata, ...metadata(options.meta) };
|
|
63
|
+
for (const key of options.removeMeta ?? [])
|
|
64
|
+
delete patch.metadata[key];
|
|
65
|
+
}
|
|
66
|
+
return { type: "single", data: await client.updateHost(id, patch), schema };
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await client.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function createHostCommand() {
|
|
73
|
+
const host = new Command("host").description("Manage hosts connected to the selected daemon");
|
|
74
|
+
addJsonAndDaemonHostOptions(host.command("ls").description("List saved hosts, including disabled and failed entries")).action(withOutput((options, _command) => run("ls", undefined, options)));
|
|
75
|
+
addJsonAndDaemonHostOptions(host
|
|
76
|
+
.command("add")
|
|
77
|
+
.argument("<connection>")
|
|
78
|
+
.option("--label <name>")
|
|
79
|
+
.option("--meta <key=value>", "Set metadata", collectMultiple, [])).action(withOutput((connection, options, _command) => run("add", connection, options)));
|
|
80
|
+
addJsonAndDaemonHostOptions(host
|
|
81
|
+
.command("update")
|
|
82
|
+
.argument("<registration-id>")
|
|
83
|
+
.option("--label <name>")
|
|
84
|
+
.option("--connection <connection>")
|
|
85
|
+
.option("--replace-identity", "Accept a new destination identity")
|
|
86
|
+
.option("--meta <key=value>", "Replace supplied metadata keys", collectMultiple, [])
|
|
87
|
+
.option("--remove-meta <key>", "Remove a metadata key", collectMultiple, [])).action(withOutput((id, options, _command) => run("update", id, options)));
|
|
88
|
+
for (const action of ["inspect", "enable", "disable", "rm"])
|
|
89
|
+
addJsonAndDaemonHostOptions(host.command(action).argument("<registration-id>")).action(withOutput((id, options, _command) => run(action, id, options)));
|
|
90
|
+
return host;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -27,6 +27,9 @@ when they help. Use sentence case and plain factual language, with no em dashes.
|
|
|
27
27
|
Omit installation commands, badges, changelog, contributing or license sections,
|
|
28
28
|
marketing, and unsupported claims. Avoid implementation filler such as empty
|
|
29
29
|
cleanup functions, theme-token field lists, or lists of absent features.
|
|
30
|
+
|
|
31
|
+
To translate the overview, add a whole copy beside this file named for the
|
|
32
|
+
language, such as OVERVIEW.zh-CN.md. Readers in that language see it instead.
|
|
30
33
|
-->
|
|
31
34
|
`;
|
|
32
35
|
const TSCONFIG = {
|
|
@@ -166,6 +169,7 @@ export async function scaffoldPluginDirectory(targetDirectory, requestedId) {
|
|
|
166
169
|
files: [
|
|
167
170
|
"paseo-plugin.json",
|
|
168
171
|
"OVERVIEW.md",
|
|
172
|
+
"OVERVIEW.*.md",
|
|
169
173
|
"index.client.ts",
|
|
170
174
|
"index.client.tsx",
|
|
171
175
|
"index.server.ts",
|
|
@@ -34,8 +34,17 @@ function summary(entry) {
|
|
|
34
34
|
const lines = [failureText(report)];
|
|
35
35
|
for (const login of entry.loginErrors ?? [])
|
|
36
36
|
lines.push(`${login.harness}: ${failureText(login.report)}`);
|
|
37
|
+
if (entry.lastSuccessful) {
|
|
38
|
+
lines.push(`Last successful: ${entry.lastSuccessful.fetchedAt}`);
|
|
39
|
+
lines.push(...availableSummary(entry.lastSuccessful.report).map((line) => `Retained: ${line}`));
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
lines.push("No successful update");
|
|
37
43
|
return lines;
|
|
38
44
|
}
|
|
45
|
+
return [...availableSummary(report), `Last successful: ${successfulTime(entry)}`];
|
|
46
|
+
}
|
|
47
|
+
function availableSummary(report) {
|
|
39
48
|
const lines = report.windows.map(windowText);
|
|
40
49
|
lines.push(...(report.balances ?? []).map(balanceText));
|
|
41
50
|
return lines.length ? lines : ["-"];
|
|
@@ -48,7 +57,7 @@ const summarySchema = {
|
|
|
48
57
|
{ header: "ACCOUNT", field: "account" },
|
|
49
58
|
{ header: "STATUS", field: "status" },
|
|
50
59
|
{ header: "USAGE", field: "usage" },
|
|
51
|
-
{ header: "FETCHED", field: "fetched" },
|
|
60
|
+
{ header: "FETCHED (LATEST ATTEMPT)", field: "fetched" },
|
|
52
61
|
],
|
|
53
62
|
};
|
|
54
63
|
function inspect(entry) {
|
|
@@ -58,34 +67,17 @@ function inspect(entry) {
|
|
|
58
67
|
`Source ID: ${entry.sourceId}`,
|
|
59
68
|
`Account: ${value(entry.account.label)}`,
|
|
60
69
|
`Status: ${entry.report.status}`,
|
|
61
|
-
`
|
|
70
|
+
`Latest attempt: ${value(entry.fetchedAt)}`,
|
|
62
71
|
];
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (window.shortfallPct != null)
|
|
73
|
-
lines.push(` Shortfall: ${percent(window.shortfallPct)}`);
|
|
74
|
-
}
|
|
75
|
-
lines.push("Balances:");
|
|
76
|
-
if (!report.balances?.length)
|
|
77
|
-
lines.push(" -");
|
|
78
|
-
for (const balance of report.balances ?? []) {
|
|
79
|
-
lines.push(` ${balanceText(balance)} [${balance.id}]`, ` Resets: ${value(balance.resetsAt)}`);
|
|
80
|
-
}
|
|
81
|
-
lines.push("Details:");
|
|
82
|
-
if (!report.details?.length)
|
|
83
|
-
lines.push(" -");
|
|
84
|
-
for (const detail of report.details ?? [])
|
|
85
|
-
lines.push(` ${detail.label}: ${detail.value}`);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
lines.push(`Reason: ${failureText(report)}`);
|
|
72
|
+
const report = entry.report.status === "available" ? entry.report : entry.lastSuccessful?.report;
|
|
73
|
+
const successfulAt = successfulTime(entry);
|
|
74
|
+
lines.push(successfulAt ? `Last successful: ${successfulAt}` : "No successful update");
|
|
75
|
+
if (entry.report.status !== "available")
|
|
76
|
+
lines.push(`Reason: ${failureText(entry.report)}`);
|
|
77
|
+
if (report) {
|
|
78
|
+
if (entry.report.status !== "available")
|
|
79
|
+
lines.push("Retained usage:");
|
|
80
|
+
lines.push(...availableDetails(report));
|
|
89
81
|
}
|
|
90
82
|
if (entry.loginErrors?.length) {
|
|
91
83
|
lines.push("Login failures:");
|
|
@@ -119,4 +111,36 @@ export const usageSchema = {
|
|
|
119
111
|
return renderTable({ type: "list", data: rows, schema: summarySchema }, options);
|
|
120
112
|
},
|
|
121
113
|
};
|
|
114
|
+
function availableDetails(report) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(`Plan: ${value(report.planLabel)}`, "Windows:");
|
|
117
|
+
if (!report.windows.length)
|
|
118
|
+
lines.push(" -");
|
|
119
|
+
for (const window of report.windows) {
|
|
120
|
+
lines.push(` ${windowText(window)} [${window.id}]`, ` Resets: ${value(window.resetsAt)}`);
|
|
121
|
+
if (window.runsOutAt != null)
|
|
122
|
+
lines.push(` Runs out: ${window.runsOutAt}`);
|
|
123
|
+
if (window.shortfallPct != null)
|
|
124
|
+
lines.push(` Shortfall: ${percent(window.shortfallPct)}`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("Balances:");
|
|
127
|
+
if (!report.balances?.length)
|
|
128
|
+
lines.push(" -");
|
|
129
|
+
for (const balance of report.balances ?? []) {
|
|
130
|
+
lines.push(` ${balanceText(balance)} [${balance.id}]`, ` Resets: ${value(balance.resetsAt)}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("Details:");
|
|
133
|
+
if (!report.details?.length)
|
|
134
|
+
lines.push(" -");
|
|
135
|
+
for (const detail of report.details ?? [])
|
|
136
|
+
lines.push(` ${detail.label}: ${detail.value}`);
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
function successfulTime(entry) {
|
|
140
|
+
// COMPAT(usageSuccessfulAt): added in v0.12.0, remove after 2027-04-17 once daemon floor >= v0.12.0.
|
|
141
|
+
// Only available reports from older daemons can use their attempt timestamp as freshness.
|
|
142
|
+
if (entry.report.status === "available")
|
|
143
|
+
return entry.successfulAt ?? entry.fetchedAt;
|
|
144
|
+
return entry.lastSuccessful?.fetchedAt;
|
|
145
|
+
}
|
|
122
146
|
//# sourceMappingURL=presentation.js.map
|
package/dist/ssh/ssh-tunnel.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
endpoint: string;
|
|
4
|
-
close(): void;
|
|
5
|
-
failureDetail(): string | null;
|
|
6
|
-
}
|
|
7
|
-
export declare function resolveSshFailureDetail(failure: string | null, stderr: string): string | null;
|
|
8
|
-
export declare function createSshTunnel(target: SshTransportTarget): Promise<SshTunnel>;
|
|
1
|
+
export { createSshTunnel, resolveSshFailureDetail } from "@camerontaylor/paseo-client/node";
|
|
2
|
+
export type { SshTunnel } from "@camerontaylor/paseo-client/node";
|
|
9
3
|
//# sourceMappingURL=ssh-tunnel.d.ts.map
|
package/dist/ssh/ssh-tunnel.js
CHANGED
|
@@ -1,79 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { createServer } from "node:net";
|
|
3
|
-
import { buildSshTunnelArgs } from "@camerontaylor/paseo-protocol/ssh-transport";
|
|
4
|
-
const SSH_STDERR_LIMIT = 8192;
|
|
5
|
-
function formatSshFailure(stderr, code, signal) {
|
|
6
|
-
const detail = stderr.trim();
|
|
7
|
-
if (detail)
|
|
8
|
-
return detail;
|
|
9
|
-
if (signal)
|
|
10
|
-
return `ssh exited with signal ${signal}`;
|
|
11
|
-
return `ssh exited with code ${code ?? "unknown"}`;
|
|
12
|
-
}
|
|
13
|
-
export function resolveSshFailureDetail(failure, stderr) {
|
|
14
|
-
return failure ?? (stderr.trim() || null);
|
|
15
|
-
}
|
|
16
|
-
export function createSshTunnel(target) {
|
|
17
|
-
let server = null;
|
|
18
|
-
let socket = null;
|
|
19
|
-
let child = null;
|
|
20
|
-
let stderr = "";
|
|
21
|
-
let failure = null;
|
|
22
|
-
function close() {
|
|
23
|
-
server?.close();
|
|
24
|
-
server = null;
|
|
25
|
-
socket?.destroy();
|
|
26
|
-
socket = null;
|
|
27
|
-
if (child && !child.killed)
|
|
28
|
-
child.kill();
|
|
29
|
-
child = null;
|
|
30
|
-
}
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
server = createServer((acceptedSocket) => {
|
|
33
|
-
socket = acceptedSocket;
|
|
34
|
-
server?.close();
|
|
35
|
-
server = null;
|
|
36
|
-
child = spawn("ssh", buildSshTunnelArgs(target), {
|
|
37
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
-
windowsHide: true,
|
|
39
|
-
});
|
|
40
|
-
child.stderr.on("data", (chunk) => {
|
|
41
|
-
stderr = `${stderr}${chunk.toString()}`.slice(-SSH_STDERR_LIMIT);
|
|
42
|
-
});
|
|
43
|
-
child.on("error", (error) => {
|
|
44
|
-
failure = error.message;
|
|
45
|
-
acceptedSocket.destroy(error);
|
|
46
|
-
});
|
|
47
|
-
child.on("exit", (code, signal) => {
|
|
48
|
-
if (code !== 0 || signal)
|
|
49
|
-
failure = formatSshFailure(stderr, code, signal);
|
|
50
|
-
acceptedSocket.destroy(failure ? new Error(failure) : undefined);
|
|
51
|
-
});
|
|
52
|
-
acceptedSocket.on("error", () => undefined);
|
|
53
|
-
acceptedSocket.on("close", () => {
|
|
54
|
-
if (child && !child.killed)
|
|
55
|
-
child.kill();
|
|
56
|
-
});
|
|
57
|
-
acceptedSocket.pipe(child.stdin);
|
|
58
|
-
child.stdout.pipe(acceptedSocket);
|
|
59
|
-
});
|
|
60
|
-
server.once("error", (error) => {
|
|
61
|
-
close();
|
|
62
|
-
reject(error);
|
|
63
|
-
});
|
|
64
|
-
server.listen(0, "127.0.0.1", () => {
|
|
65
|
-
const address = server?.address();
|
|
66
|
-
if (!address || typeof address === "string") {
|
|
67
|
-
close();
|
|
68
|
-
reject(new Error("Failed to allocate the SSH tunnel port"));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
resolve({
|
|
72
|
-
endpoint: `127.0.0.1:${address.port}`,
|
|
73
|
-
close,
|
|
74
|
-
failureDetail: () => resolveSshFailureDetail(failure, stderr),
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
}
|
|
1
|
+
export { createSshTunnel, resolveSshFailureDetail } from "@camerontaylor/paseo-client/node";
|
|
79
2
|
//# sourceMappingURL=ssh-tunnel.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentSnapshotPayload } from "@camerontaylor/paseo-protocol/messages";
|
|
2
|
+
import type { connectToDaemon } from "./client.js";
|
|
3
|
+
type FetchAgentsOptions = NonNullable<Parameters<Awaited<ReturnType<typeof connectToDaemon>>["fetchAgents"]>[0]>;
|
|
4
|
+
interface AgentDirectoryClient {
|
|
5
|
+
fetchAgents(options?: FetchAgentsOptions): Promise<{
|
|
6
|
+
entries: {
|
|
7
|
+
agent: AgentSnapshotPayload;
|
|
8
|
+
}[];
|
|
9
|
+
pageInfo: {
|
|
10
|
+
nextCursor: string | null;
|
|
11
|
+
};
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** Read the complete directory without agents moving between pages as they update. */
|
|
15
|
+
export declare function fetchAllAgents(client: AgentDirectoryClient, options?: Omit<FetchAgentsOptions, "page" | "sort" | "subscribe">): Promise<AgentSnapshotPayload[]>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=agent-directory.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Read the complete directory without agents moving between pages as they update. */
|
|
2
|
+
export async function fetchAllAgents(client, options = {}) {
|
|
3
|
+
const agents = [];
|
|
4
|
+
let cursor;
|
|
5
|
+
// createdAt never changes, so agents cannot move past the cursor while the pages are read.
|
|
6
|
+
do {
|
|
7
|
+
const payload = await client.fetchAgents({
|
|
8
|
+
...options,
|
|
9
|
+
sort: [{ key: "created_at", direction: "desc" }],
|
|
10
|
+
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
|
11
|
+
});
|
|
12
|
+
agents.push(...payload.entries.map((entry) => entry.agent));
|
|
13
|
+
cursor = payload.pageInfo.nextCursor ?? undefined;
|
|
14
|
+
} while (cursor);
|
|
15
|
+
return agents;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=agent-directory.js.map
|
package/dist/utils/client.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
+
import { createHostClient, parseHostTarget } from "@camerontaylor/paseo-client/node";
|
|
1
2
|
import { waitForDaemonReady, resolvePaseoHome, readLocalCredentialForTarget, } from "@camerontaylor/paseo-server/daemon-control";
|
|
2
3
|
import { describeDaemonTarget } from "./daemon-target.js";
|
|
3
|
-
import { buildDaemonWebSocketUrl,
|
|
4
|
-
import { parseConnectionOfferFromUrl, } from "@camerontaylor/paseo-protocol/connection-offer";
|
|
5
|
-
import { parseSshTransportUri } from "@camerontaylor/paseo-protocol/ssh-transport";
|
|
6
|
-
import { DaemonClient } from "@camerontaylor/paseo-client/internal/daemon-client";
|
|
7
|
-
import { WebSocket } from "ws";
|
|
4
|
+
import { buildDaemonWebSocketUrl, normalizeHostPort, parseConnectionUri, } from "@camerontaylor/paseo-protocol/daemon-endpoints";
|
|
8
5
|
import { getOrCreateCliClientId } from "./client-id.js";
|
|
9
6
|
import { resolveCliVersion } from "../version.js";
|
|
10
|
-
import { createSshTunnel } from "../ssh/ssh-tunnel.js";
|
|
11
7
|
export function resolveClientPaseoHome(target, env = process.env) {
|
|
12
8
|
return target.kind === "instance" ? target.home : resolvePaseoHome(env);
|
|
13
9
|
}
|
|
@@ -22,8 +18,6 @@ export function buildDaemonConnectionCommandError(options) {
|
|
|
22
18
|
message = String(error.message);
|
|
23
19
|
if (options.target.kind === "endpoint")
|
|
24
20
|
message = message.replaceAll(options.target.host, describeDaemonTarget(options.target));
|
|
25
|
-
if (message.startsWith("Cannot connect to daemon at "))
|
|
26
|
-
return error;
|
|
27
21
|
let code = "DAEMON_UNREACHABLE";
|
|
28
22
|
if (typeof error === "object" && error !== null && "code" in error)
|
|
29
23
|
code = String(error.code);
|
|
@@ -33,7 +27,9 @@ export function buildDaemonConnectionCommandError(options) {
|
|
|
33
27
|
code = "AUTH_FAILED";
|
|
34
28
|
return {
|
|
35
29
|
code,
|
|
36
|
-
message:
|
|
30
|
+
message: message.startsWith("Cannot connect to daemon at ")
|
|
31
|
+
? message
|
|
32
|
+
: `Cannot connect to daemon at ${describeDaemonTarget(options.target)}: ${message}`,
|
|
37
33
|
details: describeConnectionRemedy(code, options.target),
|
|
38
34
|
};
|
|
39
35
|
}
|
|
@@ -127,12 +123,9 @@ export function resolveDaemonTarget(host) {
|
|
|
127
123
|
};
|
|
128
124
|
}
|
|
129
125
|
export function resolveDaemonPassword(host) {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
if (fromUri)
|
|
134
|
-
return fromUri;
|
|
135
|
-
}
|
|
126
|
+
const fromUri = parseHostTarget(host).password;
|
|
127
|
+
if (fromUri)
|
|
128
|
+
return fromUri;
|
|
136
129
|
const fromEnv = process.env.PASEO_PASSWORD;
|
|
137
130
|
return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
|
|
138
131
|
}
|
|
@@ -143,26 +136,18 @@ export function resolveDaemonCredential(host, home) {
|
|
|
143
136
|
const token = readLocalCredentialForTarget(home, host);
|
|
144
137
|
return token ? { kind: "localCredential", token } : null;
|
|
145
138
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
* Create and connect a daemon client
|
|
159
|
-
* Returns the connected client or throws if connection fails
|
|
160
|
-
*/
|
|
161
|
-
async function tryConnectHost(host, credential, home, clientId, timeout, nodeWebSocketFactory) {
|
|
162
|
-
const target = resolveDaemonTarget(host);
|
|
163
|
-
const client = new DaemonClient({
|
|
164
|
-
url: target.url,
|
|
165
|
-
clientId,
|
|
139
|
+
async function connectSelectedDaemon(options) {
|
|
140
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
141
|
+
const host = options.target.kind === "endpoint"
|
|
142
|
+
? options.target.host
|
|
143
|
+
: (await waitForDaemonReady(options.target.home, {
|
|
144
|
+
timeoutMs: timeout,
|
|
145
|
+
instance: options.instance,
|
|
146
|
+
})).listen;
|
|
147
|
+
const home = resolveClientPaseoHome(options.target);
|
|
148
|
+
const credential = resolveDaemonCredential(host, home);
|
|
149
|
+
const client = createHostClient(host, {
|
|
150
|
+
clientId: await getOrCreateCliClientId(home),
|
|
166
151
|
clientType: "cli",
|
|
167
152
|
appVersion: resolveCliVersion(),
|
|
168
153
|
...(credential?.kind === "password" ? { password: credential.password } : {}),
|
|
@@ -170,37 +155,6 @@ async function tryConnectHost(host, credential, home, clientId, timeout, nodeWeb
|
|
|
170
155
|
? { localCredential: () => readLocalCredentialForTarget(home, host) ?? undefined }
|
|
171
156
|
: {}),
|
|
172
157
|
connectTimeoutMs: timeout,
|
|
173
|
-
webSocketFactory: (url, config) => nodeWebSocketFactory(url, {
|
|
174
|
-
headers: config?.headers,
|
|
175
|
-
protocols: config?.protocols,
|
|
176
|
-
...(target.type === "ipc" ? { socketPath: target.socketPath } : {}),
|
|
177
|
-
}),
|
|
178
|
-
reconnect: { enabled: false },
|
|
179
|
-
});
|
|
180
|
-
try {
|
|
181
|
-
await client.connect();
|
|
182
|
-
return { client };
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
await client.close().catch(() => { });
|
|
186
|
-
return { error };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async function connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFactory) {
|
|
190
|
-
const url = buildRelayWebSocketUrl({
|
|
191
|
-
endpoint: offer.relay.endpoint,
|
|
192
|
-
serverId: offer.serverId,
|
|
193
|
-
role: "client",
|
|
194
|
-
useTls: offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint),
|
|
195
|
-
});
|
|
196
|
-
const client = new DaemonClient({
|
|
197
|
-
url,
|
|
198
|
-
clientId,
|
|
199
|
-
clientType: "cli",
|
|
200
|
-
appVersion: resolveCliVersion(),
|
|
201
|
-
connectTimeoutMs: timeout,
|
|
202
|
-
webSocketFactory: (target, config) => nodeWebSocketFactory(target, { headers: config?.headers, protocols: config?.protocols }),
|
|
203
|
-
e2ee: { enabled: true, daemonPublicKeyB64: offer.daemonPublicKeyB64 },
|
|
204
158
|
reconnect: { enabled: false },
|
|
205
159
|
});
|
|
206
160
|
try {
|
|
@@ -208,66 +162,9 @@ async function connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFacto
|
|
|
208
162
|
return client;
|
|
209
163
|
}
|
|
210
164
|
catch (error) {
|
|
211
|
-
await client.close()
|
|
212
|
-
|
|
213
|
-
const lastError = client.lastError ? ` (${client.lastError})` : "";
|
|
214
|
-
throw new Error(`Failed to connect via relay offer: ${message}${lastError}`, { cause: error });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function parseHostOfferOrNull(host) {
|
|
218
|
-
if (!host)
|
|
219
|
-
return null;
|
|
220
|
-
try {
|
|
221
|
-
return parseConnectionOfferFromUrl(host);
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
-
throw new Error(`Invalid pairing offer URL: ${message}`, { cause: error });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
async function connectSelectedDaemon(options) {
|
|
229
|
-
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
230
|
-
const deadline = Date.now() + timeout;
|
|
231
|
-
const explicitHost = options.target.kind === "endpoint"
|
|
232
|
-
? options.target.host
|
|
233
|
-
: (await waitForDaemonReady(options.target.home, {
|
|
234
|
-
timeoutMs: timeout,
|
|
235
|
-
instance: options.instance,
|
|
236
|
-
})).listen;
|
|
237
|
-
const home = resolveClientPaseoHome(options.target);
|
|
238
|
-
const clientId = await getOrCreateCliClientId(home);
|
|
239
|
-
const nodeWebSocketFactory = createNodeWebSocketFactory();
|
|
240
|
-
if (explicitHost?.trim().startsWith("ssh://")) {
|
|
241
|
-
const target = parseSshTransportUri(explicitHost.trim());
|
|
242
|
-
const tunnel = await createSshTunnel(target);
|
|
243
|
-
const password = resolveDaemonPassword(explicitHost);
|
|
244
|
-
const result = await tryConnectHost(tunnel.endpoint, password ? { kind: "password", password } : null, home, clientId, Math.max(1, deadline - Date.now()), nodeWebSocketFactory);
|
|
245
|
-
if ("client" in result) {
|
|
246
|
-
const close = result.client.close.bind(result.client);
|
|
247
|
-
result.client.close = async () => {
|
|
248
|
-
try {
|
|
249
|
-
await close();
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
tunnel.close();
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
return result.client;
|
|
256
|
-
}
|
|
257
|
-
const failure = tunnel.failureDetail();
|
|
258
|
-
tunnel.close();
|
|
259
|
-
if (failure)
|
|
260
|
-
throw new Error(`SSH connection failed: ${failure}`, { cause: result.error });
|
|
261
|
-
throw result.error;
|
|
262
|
-
}
|
|
263
|
-
const offer = parseHostOfferOrNull(explicitHost);
|
|
264
|
-
if (offer) {
|
|
265
|
-
return connectViaRelayOffer(offer, clientId, Math.max(1, deadline - Date.now()), nodeWebSocketFactory);
|
|
165
|
+
await client.close();
|
|
166
|
+
throw error;
|
|
266
167
|
}
|
|
267
|
-
const result = await tryConnectHost(explicitHost, resolveDaemonCredential(explicitHost, home), home, clientId, Math.max(1, deadline - Date.now()), nodeWebSocketFactory);
|
|
268
|
-
if ("client" in result)
|
|
269
|
-
return result.client;
|
|
270
|
-
throw result.error;
|
|
271
168
|
}
|
|
272
169
|
export async function connectToDaemon(options) {
|
|
273
170
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseHostTarget } from "@camerontaylor/paseo-protocol/host-target";
|
|
1
2
|
import { resolvePaseoHome } from "@camerontaylor/paseo-server/daemon-control";
|
|
2
3
|
export function selectDaemonTarget(options, env = process.env, localOnly = false) {
|
|
3
4
|
for (const key of ["home", "host"]) {
|
|
@@ -34,18 +35,29 @@ export function describeDaemonTarget(target) {
|
|
|
34
35
|
if (target.kind === "instance")
|
|
35
36
|
return `home ${target.home}`;
|
|
36
37
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
url.
|
|
45
|
-
|
|
38
|
+
const input = target.host.trim();
|
|
39
|
+
const parsed = parseHostTarget(input);
|
|
40
|
+
// Discovery descriptions are display text; command hints must preserve the address syntax.
|
|
41
|
+
if (parsed.kind === "ssh" || (!parsed.offer && !parsed.socketPath && input.includes("://"))) {
|
|
42
|
+
const url = new URL(input);
|
|
43
|
+
if (decodeURIComponent(url.username).includes(":"))
|
|
44
|
+
url.username = "REDACTED";
|
|
45
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
46
|
+
const routingOption = (parsed.kind === "ssh" && key === "daemonPort") ||
|
|
47
|
+
(key === "ssl" &&
|
|
48
|
+
url.searchParams.getAll(key).every((value) => value === "true" || value === "false"));
|
|
49
|
+
if (!routingOption)
|
|
50
|
+
url.searchParams.set(key, "REDACTED");
|
|
51
|
+
}
|
|
52
|
+
return url.toString();
|
|
53
|
+
}
|
|
54
|
+
// Pairing offers must always use safe display text, including prefix-free fragments.
|
|
55
|
+
return parsed.kind === "socket" && !parsed.offer && !input.includes("://")
|
|
56
|
+
? input
|
|
57
|
+
: parsed.description;
|
|
46
58
|
}
|
|
47
59
|
catch {
|
|
48
|
-
return
|
|
60
|
+
return "<invalid endpoint>";
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
//# sourceMappingURL=daemon-target.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camerontaylor/paseo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0-fork.1",
|
|
4
4
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
|
5
5
|
"bin": {
|
|
6
6
|
"paseo": "bin/paseo"
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@clack/prompts": "^1.0.0",
|
|
31
|
-
"@camerontaylor/paseo-client": "0.
|
|
32
|
-
"@camerontaylor/paseo-protocol": "0.
|
|
33
|
-
"@camerontaylor/paseo-server": "0.
|
|
31
|
+
"@camerontaylor/paseo-client": "0.13.0-fork.1",
|
|
32
|
+
"@camerontaylor/paseo-protocol": "0.13.0-fork.1",
|
|
33
|
+
"@camerontaylor/paseo-server": "0.13.0-fork.1",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
36
|
"mime-types": "^2.1.35",
|