@getpaseo/cli 0.2.5 → 0.3.0-beta.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 +1 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/local-daemon.js +3 -0
- package/dist/commands/daemon/pair.d.ts +27 -1
- package/dist/commands/daemon/pair.js +133 -46
- package/dist/commands/daemon/start.js +1 -0
- package/dist/commands/daemon/status.d.ts +11 -0
- package/dist/commands/daemon/status.js +47 -22
- package/dist/commands/onboard.js +20 -40
- package/dist/run.js +8 -1
- package/dist/utils/client.js +4 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -89,6 +89,7 @@ export function createCli() {
|
|
|
89
89
|
.option("--force", "Send SIGKILL if graceful stop times out")
|
|
90
90
|
.option("--listen <listen>", "Listen target for restarted daemon (host:port, port, or unix socket)")
|
|
91
91
|
.option("--port <port>", "Port for restarted daemon listen target")
|
|
92
|
+
.option("--relay", "Enable relay on restarted daemon")
|
|
92
93
|
.option("--no-relay", "Disable relay on restarted daemon")
|
|
93
94
|
.option("--no-mcp", "Disable Agent MCP on restarted daemon")
|
|
94
95
|
.option("--hostnames <hosts>", 'Daemon hostnames (comma-separated, e.g. "myhost,.example.com" or "true" for any)')
|
|
@@ -33,6 +33,7 @@ export function createDaemonCommand() {
|
|
|
33
33
|
.option("--force", "Send SIGKILL if graceful stop times out")
|
|
34
34
|
.option("--listen <listen>", "Listen target for restarted daemon (host:port, port, or unix socket)")
|
|
35
35
|
.option("--port <port>", "Port for restarted daemon listen target")
|
|
36
|
+
.option("--relay", "Enable relay on restarted daemon")
|
|
36
37
|
.option("--no-relay", "Disable relay on restarted daemon")
|
|
37
38
|
.option("--no-mcp", "Disable Agent MCP on restarted daemon")
|
|
38
39
|
.option("--no-inject-mcp", "Disable auto-injecting the Paseo MCP into created agents")
|
|
@@ -2,8 +2,34 @@ import { Command } from "commander";
|
|
|
2
2
|
interface PairOptions {
|
|
3
3
|
home?: string;
|
|
4
4
|
json?: boolean;
|
|
5
|
+
relay?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PairCommandDependencies {
|
|
8
|
+
resolveOffer: typeof resolveLocalPairingOffer;
|
|
9
|
+
confirmRelay: typeof confirmRelayPairing;
|
|
10
|
+
printDirectGuidance: typeof printDirectConnectionGuidance;
|
|
11
|
+
isInteractive: () => boolean;
|
|
12
|
+
output: PairCommandOutput;
|
|
13
|
+
}
|
|
14
|
+
export interface PairCommandOutput {
|
|
15
|
+
columns: number | undefined;
|
|
16
|
+
writeStdout(message: string): void;
|
|
17
|
+
writeStderr(message: string): void;
|
|
18
|
+
setExitCode(code: number): void;
|
|
19
|
+
success(message: string): void;
|
|
20
|
+
}
|
|
21
|
+
export interface PairingOffer {
|
|
22
|
+
relayEnabled: boolean;
|
|
23
|
+
url: string | null;
|
|
24
|
+
qr: string | null;
|
|
5
25
|
}
|
|
6
26
|
export declare function pairCommand(): Command;
|
|
7
|
-
export declare function
|
|
27
|
+
export declare function resolveLocalPairingOffer(options: {
|
|
28
|
+
paseoHome: string;
|
|
29
|
+
enableRelay?: boolean;
|
|
30
|
+
}): Promise<PairingOffer>;
|
|
31
|
+
export declare function confirmRelayPairing(): Promise<boolean>;
|
|
32
|
+
export declare function printDirectConnectionGuidance(): void;
|
|
33
|
+
export declare function runPairCommand(options: PairOptions, dependencyOverrides?: Partial<PairCommandDependencies>): Promise<void>;
|
|
8
34
|
export {};
|
|
9
35
|
//# sourceMappingURL=pair.d.ts.map
|
|
@@ -1,52 +1,53 @@
|
|
|
1
|
+
import { confirm, isCancel, log } from "@clack/prompts";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import chalk from "chalk";
|
|
3
|
-
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
|
4
|
+
import { generateLocalPairingOffer, getOrCreateServerId, loadConfig, resolvePaseoHome, } from "@getpaseo/server";
|
|
4
5
|
import { tryConnectToDaemon } from "../../utils/client.js";
|
|
5
|
-
import { resolveLocalDaemonState
|
|
6
|
+
import { resolveLocalDaemonState } from "./local-daemon.js";
|
|
6
7
|
import { addJsonOption } from "../../utils/command-options.js";
|
|
7
8
|
import { formatPairingInstructions } from "../../output/pairing.js";
|
|
8
9
|
const PAIRING_DAEMON_RPC_TIMEOUT_MS = 1500;
|
|
10
|
+
const RELAY_DOCS_URL = "https://paseo.sh/docs/security";
|
|
11
|
+
function createProcessOutput() {
|
|
12
|
+
return {
|
|
13
|
+
columns: process.stdout.columns,
|
|
14
|
+
writeStdout(message) {
|
|
15
|
+
process.stdout.write(message);
|
|
16
|
+
},
|
|
17
|
+
writeStderr(message) {
|
|
18
|
+
process.stderr.write(message);
|
|
19
|
+
},
|
|
20
|
+
setExitCode(code) {
|
|
21
|
+
process.exitCode = code;
|
|
22
|
+
},
|
|
23
|
+
success(message) {
|
|
24
|
+
log.success(message);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
9
28
|
export function pairCommand() {
|
|
10
29
|
return addJsonOption(new Command("pair").description("Print the daemon pairing QR code and link"))
|
|
11
30
|
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
|
31
|
+
.option("--relay", "Enable relay without prompting")
|
|
12
32
|
.action(async (_options, command) => {
|
|
13
33
|
await runPairCommand(command.optsWithGlobals());
|
|
14
34
|
});
|
|
15
35
|
}
|
|
16
|
-
export async function
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
export async function resolveLocalPairingOffer(options) {
|
|
37
|
+
const state = resolveLocalDaemonState({ home: options.paseoHome });
|
|
38
|
+
const serverId = getOrCreateServerId(state.home);
|
|
39
|
+
const daemonOffer = await resolveDaemonPairingOffer(state.listen, serverId, options.enableRelay);
|
|
40
|
+
if (daemonOffer)
|
|
41
|
+
return daemonOffer;
|
|
42
|
+
if (state.running) {
|
|
43
|
+
throw new Error("The running daemon did not provide a pairing offer. Check daemon connectivity or update the daemon.");
|
|
19
44
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Try to get the pairing offer from the running daemon first.
|
|
24
|
-
if (host) {
|
|
25
|
-
const client = await tryConnectToDaemon({ host, timeout: 1500 });
|
|
26
|
-
if (client) {
|
|
27
|
-
const supportsDaemonStatusRpc = client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
|
|
28
|
-
if (supportsDaemonStatusRpc) {
|
|
29
|
-
try {
|
|
30
|
-
const offer = await client.getDaemonPairingOffer({
|
|
31
|
-
timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
|
|
32
|
-
});
|
|
33
|
-
await client.close().catch(() => { });
|
|
34
|
-
outputPairingResult({ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null }, options);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// COMPAT(daemon-rpc-rollout): fall back to CLI-side pairing generation while
|
|
39
|
-
// old daemons lack daemonStatusRpc. Remove once the daemon floor is past
|
|
40
|
-
// v0.1.76; pairing should come from daemon.get_pairing_offer.
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
await client.close().catch(() => { });
|
|
44
|
-
}
|
|
45
|
+
const config = loadConfig(options.paseoHome);
|
|
46
|
+
if (options.enableRelay && !config.relayEnabled) {
|
|
47
|
+
throw new Error("Start the daemon before enabling relay for pairing.");
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const pairing = await generateLocalPairingOffer({
|
|
49
|
-
paseoHome,
|
|
49
|
+
return generateLocalPairingOffer({
|
|
50
|
+
paseoHome: options.paseoHome,
|
|
50
51
|
relayEnabled: config.relayEnabled,
|
|
51
52
|
relayEndpoint: config.relayEndpoint,
|
|
52
53
|
relayPublicEndpoint: config.relayPublicEndpoint,
|
|
@@ -55,26 +56,112 @@ export async function runPairCommand(options) {
|
|
|
55
56
|
appBaseUrl: config.appBaseUrl,
|
|
56
57
|
includeQr: true,
|
|
57
58
|
});
|
|
58
|
-
outputPairingResult(pairing, options);
|
|
59
59
|
}
|
|
60
|
-
function
|
|
60
|
+
async function resolveDaemonPairingOffer(listen, expectedServerId, enableRelay) {
|
|
61
|
+
const client = await tryConnectToDaemon({
|
|
62
|
+
host: listen,
|
|
63
|
+
timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
|
|
64
|
+
});
|
|
65
|
+
if (!client)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const serverInfo = client.getLastServerInfoMessage();
|
|
69
|
+
if (serverInfo?.serverId.trim() !== expectedServerId) {
|
|
70
|
+
throw new Error("The reachable daemon belongs to a different Paseo home. Check --home or the daemon listen configuration.");
|
|
71
|
+
}
|
|
72
|
+
if (serverInfo?.features?.daemonStatusRpc !== true) {
|
|
73
|
+
throw new Error("Update the Paseo daemon before pairing from this command.");
|
|
74
|
+
}
|
|
75
|
+
let offer = await client.getDaemonPairingOffer({
|
|
76
|
+
timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
|
|
77
|
+
});
|
|
78
|
+
if (!offer.relayEnabled && enableRelay) {
|
|
79
|
+
if (serverInfo.features.relayConfig !== true) {
|
|
80
|
+
throw new Error("Update the Paseo daemon before enabling relay from this command.");
|
|
81
|
+
}
|
|
82
|
+
await client.patchDaemonConfig({ relay: { enabled: true } });
|
|
83
|
+
offer = await client.getDaemonPairingOffer({
|
|
84
|
+
timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
relayEnabled: offer.relayEnabled,
|
|
89
|
+
url: offer.url || null,
|
|
90
|
+
qr: offer.qr ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await client.close().catch(() => undefined);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function confirmRelayPairing() {
|
|
98
|
+
log.message("Your connection is end-to-end encrypted. Paseo cannot read your code or messages.");
|
|
99
|
+
log.message(`Learn how it works: ${RELAY_DOCS_URL}`);
|
|
100
|
+
const answer = await confirm({
|
|
101
|
+
message: "Enable relay to pair a device?",
|
|
102
|
+
initialValue: false,
|
|
103
|
+
});
|
|
104
|
+
return !isCancel(answer) && answer;
|
|
105
|
+
}
|
|
106
|
+
export function printDirectConnectionGuidance() {
|
|
107
|
+
console.log("Daemon is running with relay off.");
|
|
108
|
+
console.log("To connect another device directly, use the daemon's TCP address over your LAN, Tailscale, or another VPN.");
|
|
109
|
+
console.log(`Learn more: ${RELAY_DOCS_URL}#direct-connections`);
|
|
110
|
+
}
|
|
111
|
+
export async function runPairCommand(options, dependencyOverrides = {}) {
|
|
112
|
+
if (options.home)
|
|
113
|
+
process.env.PASEO_HOME = options.home;
|
|
114
|
+
const dependencies = {
|
|
115
|
+
resolveOffer: resolveLocalPairingOffer,
|
|
116
|
+
confirmRelay: confirmRelayPairing,
|
|
117
|
+
printDirectGuidance: printDirectConnectionGuidance,
|
|
118
|
+
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
119
|
+
output: createProcessOutput(),
|
|
120
|
+
...dependencyOverrides,
|
|
121
|
+
};
|
|
122
|
+
const paseoHome = resolvePaseoHome();
|
|
123
|
+
let pairing = await dependencies.resolveOffer({
|
|
124
|
+
paseoHome,
|
|
125
|
+
enableRelay: options.relay === true,
|
|
126
|
+
});
|
|
127
|
+
const canPrompt = dependencies.isInteractive() && options.json !== true;
|
|
128
|
+
if (!pairing.relayEnabled && canPrompt) {
|
|
129
|
+
const shouldEnable = await dependencies.confirmRelay();
|
|
130
|
+
if (!shouldEnable) {
|
|
131
|
+
dependencies.printDirectGuidance();
|
|
132
|
+
dependencies.output.writeStderr(`${chalk.yellow("No pairing QR was created.")}\n`);
|
|
133
|
+
dependencies.output.setExitCode(1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
pairing = await dependencies.resolveOffer({ paseoHome, enableRelay: true });
|
|
137
|
+
dependencies.output.success("Relay enabled");
|
|
138
|
+
}
|
|
139
|
+
outputPairingResult(pairing, options, dependencies.output);
|
|
140
|
+
}
|
|
141
|
+
function outputPairingResult(pairing, options, output) {
|
|
61
142
|
if (!pairing.relayEnabled || !pairing.url) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
143
|
+
if (options.json) {
|
|
144
|
+
output.writeStderr(`${JSON.stringify({
|
|
145
|
+
code: "RELAY_DISABLED",
|
|
146
|
+
message: "Relay pairing is disabled for this daemon.",
|
|
147
|
+
action: "Run paseo daemon pair --relay --json to enable it explicitly.",
|
|
148
|
+
})}\n`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
output.writeStderr(`${chalk.red("Relay pairing is disabled for this daemon.")}\n`);
|
|
152
|
+
output.writeStderr(`${chalk.yellow("Run paseo daemon pair --relay to enable it.")}\n`);
|
|
153
|
+
}
|
|
154
|
+
output.setExitCode(1);
|
|
155
|
+
return;
|
|
65
156
|
}
|
|
66
157
|
if (options.json) {
|
|
67
|
-
|
|
68
|
-
relayEnabled: pairing.relayEnabled,
|
|
69
|
-
url: pairing.url,
|
|
70
|
-
qr: pairing.qr,
|
|
71
|
-
}, null, 2)}\n`);
|
|
158
|
+
output.writeStdout(`${JSON.stringify({ relayEnabled: pairing.relayEnabled, url: pairing.url, qr: pairing.qr }, null, 2)}\n`);
|
|
72
159
|
return;
|
|
73
160
|
}
|
|
74
|
-
|
|
161
|
+
output.writeStdout(formatPairingInstructions({
|
|
75
162
|
url: pairing.url,
|
|
76
163
|
qr: pairing.qr,
|
|
77
|
-
columns:
|
|
164
|
+
columns: output.columns,
|
|
78
165
|
}));
|
|
79
166
|
}
|
|
80
167
|
//# sourceMappingURL=pair.js.map
|
|
@@ -9,6 +9,7 @@ export function startCommand() {
|
|
|
9
9
|
.option("--port <port>", "Port to listen on (default: 6767)")
|
|
10
10
|
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
|
11
11
|
.option("--foreground", "Run in foreground (don't daemonize)")
|
|
12
|
+
.option("--relay", "Enable relay connection")
|
|
12
13
|
.option("--no-relay", "Disable relay connection")
|
|
13
14
|
.option("--relay-use-tls", "Use wss:// for the relay connection and pairing offers")
|
|
14
15
|
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
|
|
@@ -4,6 +4,17 @@ interface StatusRow {
|
|
|
4
4
|
key: string;
|
|
5
5
|
value: string;
|
|
6
6
|
}
|
|
7
|
+
interface RelayStatusConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
publicEndpoint: string;
|
|
11
|
+
useTls: boolean;
|
|
12
|
+
publicUseTls: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function selectRelayStatus(input: {
|
|
15
|
+
persisted: RelayStatusConfig;
|
|
16
|
+
live?: RelayStatusConfig;
|
|
17
|
+
}): string;
|
|
7
18
|
export type StatusResult = ListResult<StatusRow>;
|
|
8
19
|
export declare function runStatusCommand(options: CommandOptions, _command: Command): Promise<StatusResult>;
|
|
9
20
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { getOrCreateServerId, findExecutable, execCommand } from "@getpaseo/server";
|
|
3
3
|
import { connectToDaemon } from "../../utils/client.js";
|
|
4
|
-
import { resolveLocalDaemonState
|
|
4
|
+
import { resolveLocalDaemonState } from "./local-daemon.js";
|
|
5
5
|
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
|
|
6
6
|
const DAEMON_STATUS_PROBE_TIMEOUT_MS = 1500;
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
@@ -203,12 +203,19 @@ async function probeDaemonOverWebsocket(args) {
|
|
|
203
203
|
version: p.available ? null : (p.error ?? null),
|
|
204
204
|
source: "daemon",
|
|
205
205
|
}));
|
|
206
|
+
const relayStatus = statusPayload.relay == null
|
|
207
|
+
? undefined
|
|
208
|
+
: selectRelayStatus({
|
|
209
|
+
persisted: relayConfigFromLocalState(state),
|
|
210
|
+
live: statusPayload.relay,
|
|
211
|
+
});
|
|
206
212
|
if (!state.running) {
|
|
207
213
|
return {
|
|
208
214
|
connectedDaemon: "reachable",
|
|
209
215
|
daemonVersion: statusPayload.version ?? daemonVersion,
|
|
210
216
|
daemonNodeOverride: statusPayload.nodePath,
|
|
211
217
|
daemonProviders,
|
|
218
|
+
relayStatus,
|
|
212
219
|
note: state.pidInfo
|
|
213
220
|
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
|
|
214
221
|
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
|
|
@@ -219,6 +226,7 @@ async function probeDaemonOverWebsocket(args) {
|
|
|
219
226
|
daemonVersion: statusPayload.version ?? daemonVersion,
|
|
220
227
|
daemonNodeOverride: statusPayload.nodePath,
|
|
221
228
|
daemonProviders,
|
|
229
|
+
relayStatus,
|
|
222
230
|
};
|
|
223
231
|
}
|
|
224
232
|
catch {
|
|
@@ -242,6 +250,7 @@ function applyProbeToStatus(input) {
|
|
|
242
250
|
daemonNode: probe.daemonNodeOverride ?? input.daemonNode,
|
|
243
251
|
daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
|
|
244
252
|
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
|
|
253
|
+
relayStatus: probe.relayStatus ?? input.relayStatus,
|
|
245
254
|
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
|
246
255
|
};
|
|
247
256
|
}
|
|
@@ -264,16 +273,26 @@ async function resolveDaemonNodeLabel(state) {
|
|
|
264
273
|
const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
|
|
265
274
|
return fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
|
|
266
275
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
276
|
+
function relayConfigFromLocalState(state) {
|
|
277
|
+
return {
|
|
278
|
+
enabled: state.relayEnabled,
|
|
279
|
+
endpoint: state.relayEndpoint,
|
|
280
|
+
publicEndpoint: state.relayEndpoint,
|
|
281
|
+
useTls: state.relayUseTls,
|
|
282
|
+
publicUseTls: state.relayPublicUseTls,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
export function selectRelayStatus(input) {
|
|
286
|
+
const relay = input.live ?? input.persisted;
|
|
287
|
+
if (!relay.enabled)
|
|
269
288
|
return "disabled";
|
|
270
|
-
const scheme =
|
|
271
|
-
return `${scheme}://${
|
|
289
|
+
const scheme = relay.publicUseTls ? "wss" : "ws";
|
|
290
|
+
return `${scheme}://${relay.publicEndpoint}`;
|
|
272
291
|
}
|
|
273
292
|
export async function runStatusCommand(options, _command) {
|
|
274
293
|
const home = typeof options.home === "string" ? options.home : undefined;
|
|
275
294
|
const state = resolveLocalDaemonState({ home });
|
|
276
|
-
const
|
|
295
|
+
const daemonTarget = state.listen.trim();
|
|
277
296
|
const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname);
|
|
278
297
|
let daemonNode = await resolveDaemonNodeLabel(state);
|
|
279
298
|
const cliNode = process.execPath;
|
|
@@ -281,26 +300,32 @@ export async function runStatusCommand(options, _command) {
|
|
|
281
300
|
let connectedDaemon = "not_probed";
|
|
282
301
|
let daemonVersion = null;
|
|
283
302
|
let daemonProviders;
|
|
303
|
+
let relayStatus = selectRelayStatus({ persisted: relayConfigFromLocalState(state) });
|
|
284
304
|
let note;
|
|
285
305
|
if (!state.running && state.stalePidFile && state.pidInfo) {
|
|
286
306
|
localDaemon = "stale_pid";
|
|
287
307
|
note = `Stale PID file found for PID ${state.pidInfo.pid}`;
|
|
288
308
|
}
|
|
289
|
-
if (
|
|
290
|
-
const probe = await probeDaemonOverWebsocket({ host, state });
|
|
291
|
-
({
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
if (daemonTarget) {
|
|
310
|
+
const probe = await probeDaemonOverWebsocket({ host: daemonTarget, state });
|
|
311
|
+
({
|
|
312
|
+
connectedDaemon,
|
|
313
|
+
localDaemon,
|
|
314
|
+
daemonNode,
|
|
315
|
+
daemonVersion,
|
|
316
|
+
daemonProviders,
|
|
317
|
+
relayStatus,
|
|
318
|
+
note,
|
|
319
|
+
} = applyProbeToStatus({
|
|
320
|
+
probe,
|
|
321
|
+
connectedDaemon,
|
|
322
|
+
localDaemon,
|
|
323
|
+
daemonNode,
|
|
324
|
+
daemonVersion,
|
|
325
|
+
daemonProviders,
|
|
326
|
+
relayStatus,
|
|
327
|
+
note,
|
|
328
|
+
}));
|
|
304
329
|
}
|
|
305
330
|
const cliVersion = resolveCliVersion();
|
|
306
331
|
const serverIdResult = resolveServerIdSafely(state.home);
|
|
@@ -315,7 +340,7 @@ export async function runStatusCommand(options, _command) {
|
|
|
315
340
|
connectedDaemon,
|
|
316
341
|
home: state.home,
|
|
317
342
|
listen: state.listen,
|
|
318
|
-
relay:
|
|
343
|
+
relay: relayStatus,
|
|
319
344
|
hostname: state.pidInfo?.hostname ?? null,
|
|
320
345
|
pid: state.pidInfo?.pid ?? null,
|
|
321
346
|
startedAt: state.pidInfo?.startedAt ?? null,
|
package/dist/commands/onboard.js
CHANGED
|
@@ -2,10 +2,11 @@ import { cancel, confirm, intro, isCancel, log, note, outro, spinner } from "@cl
|
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { loadPersistedConfig } from "@getpaseo/server";
|
|
6
6
|
import { resolveLocalPaseoHome, resolveLocalDaemonState, resolveTcpHostFromListen, startLocalDaemonDetached, tailDaemonLog, } from "./daemon/local-daemon.js";
|
|
7
7
|
import { tryConnectToDaemon } from "../utils/client.js";
|
|
8
8
|
import { formatPairingInstructions } from "../output/pairing.js";
|
|
9
|
+
import { confirmRelayPairing, printDirectConnectionGuidance, resolveLocalPairingOffer, } from "./daemon/pair.js";
|
|
9
10
|
const DEFAULT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
10
11
|
const READY_PROBE_TIMEOUT_MS = 1200;
|
|
11
12
|
class OnboardCancelledError extends Error {
|
|
@@ -29,32 +30,6 @@ function parseTimeoutMs(raw) {
|
|
|
29
30
|
}
|
|
30
31
|
return Math.ceil(seconds * 1000);
|
|
31
32
|
}
|
|
32
|
-
function toCliOverrides(options) {
|
|
33
|
-
const cliOverrides = {};
|
|
34
|
-
if (options.listen) {
|
|
35
|
-
cliOverrides.listen = options.listen;
|
|
36
|
-
}
|
|
37
|
-
else if (options.port) {
|
|
38
|
-
cliOverrides.listen = `127.0.0.1:${options.port}`;
|
|
39
|
-
}
|
|
40
|
-
if (options.relay === false) {
|
|
41
|
-
cliOverrides.relayEnabled = false;
|
|
42
|
-
}
|
|
43
|
-
if (options.hostnames) {
|
|
44
|
-
const raw = options.hostnames.trim();
|
|
45
|
-
cliOverrides.hostnames =
|
|
46
|
-
raw.toLowerCase() === "true"
|
|
47
|
-
? true
|
|
48
|
-
: raw
|
|
49
|
-
.split(",")
|
|
50
|
-
.map((host) => host.trim())
|
|
51
|
-
.filter(Boolean);
|
|
52
|
-
}
|
|
53
|
-
if (options.mcp === false) {
|
|
54
|
-
cliOverrides.mcpEnabled = false;
|
|
55
|
-
}
|
|
56
|
-
return cliOverrides;
|
|
57
|
-
}
|
|
58
33
|
function savePersistedConfig(paseoHome, config) {
|
|
59
34
|
const configPath = path.join(paseoHome, "config.json");
|
|
60
35
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
@@ -243,6 +218,7 @@ export function onboardCommand() {
|
|
|
243
218
|
.option("--listen <listen>", "Listen target (host:port, port, or unix socket path)")
|
|
244
219
|
.option("--port <port>", "Port to listen on (default: 6767)")
|
|
245
220
|
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
|
|
221
|
+
.option("--relay", "Enable relay connection without prompting")
|
|
246
222
|
.option("--no-relay", "Disable relay connection")
|
|
247
223
|
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
|
|
248
224
|
.option("--hostnames <hosts>", 'Daemon hostnames (comma-separated, e.g. "myhost,.example.com" or "true" for any)')
|
|
@@ -371,7 +347,6 @@ export async function runOnboard(options) {
|
|
|
371
347
|
renderNote(paseoHome, "Paseo home");
|
|
372
348
|
}
|
|
373
349
|
const voiceEnabled = await resolveAndPersistVoice(paseoHome, options);
|
|
374
|
-
const config = loadConfig(paseoHome, { cli: toCliOverrides(options) });
|
|
375
350
|
log.message(voiceEnabled
|
|
376
351
|
? "Voice features enabled. Local speech models will be downloaded automatically if missing."
|
|
377
352
|
: "Voice features disabled. Local speech models will not be downloaded.");
|
|
@@ -381,24 +356,29 @@ export async function runOnboard(options) {
|
|
|
381
356
|
timeoutMs,
|
|
382
357
|
richUi,
|
|
383
358
|
});
|
|
384
|
-
if (
|
|
385
|
-
log.
|
|
359
|
+
if (options.relay === false) {
|
|
360
|
+
log.message("Relay pairing skipped because --no-relay was provided.");
|
|
386
361
|
printNextSteps(null, paseoHome, richUi);
|
|
387
|
-
if (richUi)
|
|
362
|
+
if (richUi)
|
|
388
363
|
outro("Paseo daemon is running.");
|
|
389
|
-
}
|
|
390
364
|
return;
|
|
391
365
|
}
|
|
392
|
-
|
|
366
|
+
let pairing = await resolveLocalPairingOffer({
|
|
393
367
|
paseoHome,
|
|
394
|
-
|
|
395
|
-
relayEndpoint: config.relayEndpoint,
|
|
396
|
-
relayPublicEndpoint: config.relayPublicEndpoint,
|
|
397
|
-
relayUseTls: config.relayUseTls,
|
|
398
|
-
relayPublicUseTls: config.relayPublicUseTls,
|
|
399
|
-
appBaseUrl: config.appBaseUrl,
|
|
400
|
-
includeQr: true,
|
|
368
|
+
enableRelay: options.relay === true,
|
|
401
369
|
});
|
|
370
|
+
if (!pairing.relayEnabled) {
|
|
371
|
+
const shouldEnable = richUi ? await confirmRelayPairing() : false;
|
|
372
|
+
if (!shouldEnable) {
|
|
373
|
+
printDirectConnectionGuidance();
|
|
374
|
+
printNextSteps(null, paseoHome, richUi);
|
|
375
|
+
if (richUi)
|
|
376
|
+
outro("Paseo daemon is running.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
pairing = await resolveLocalPairingOffer({ paseoHome, enableRelay: true });
|
|
380
|
+
log.success("Relay enabled");
|
|
381
|
+
}
|
|
402
382
|
if (!pairing.url) {
|
|
403
383
|
log.warn("Relay pairing URL is unavailable for this daemon configuration.");
|
|
404
384
|
printNextSteps(null, paseoHome, richUi);
|
package/dist/run.js
CHANGED
|
@@ -13,7 +13,14 @@ export function createCliParseArgv(input) {
|
|
|
13
13
|
return invocation;
|
|
14
14
|
}
|
|
15
15
|
const nodeArgv = input.nodeArgv ?? ["paseo", "paseo"];
|
|
16
|
-
const
|
|
16
|
+
const isOnboardRootFlag = invocation.argv[0] === "--relay" || invocation.argv[0] === "--no-relay";
|
|
17
|
+
let cliArgv = invocation.argv;
|
|
18
|
+
if (invocation.argv.length === 0) {
|
|
19
|
+
cliArgv = ["onboard"];
|
|
20
|
+
}
|
|
21
|
+
else if (isOnboardRootFlag) {
|
|
22
|
+
cliArgv = ["onboard", ...invocation.argv];
|
|
23
|
+
}
|
|
17
24
|
return [...nodeArgv, ...cliArgv];
|
|
18
25
|
}
|
|
19
26
|
export async function runCli(argv, options = {}) {
|
package/dist/utils/client.js
CHANGED
|
@@ -141,7 +141,10 @@ function stripIpcPrefix(trimmed) {
|
|
|
141
141
|
return trimmed;
|
|
142
142
|
}
|
|
143
143
|
export function resolveDaemonTarget(host) {
|
|
144
|
-
const trimmed = host
|
|
144
|
+
const trimmed = normalizeDaemonHost(host);
|
|
145
|
+
if (!trimmed) {
|
|
146
|
+
throw new Error(`Invalid daemon target: ${host}`);
|
|
147
|
+
}
|
|
145
148
|
if (trimmed.startsWith("unix://") ||
|
|
146
149
|
trimmed.startsWith("pipe://") ||
|
|
147
150
|
trimmed.startsWith("\\\\.\\pipe\\")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.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
|
-
"@getpaseo/client": "0.
|
|
32
|
-
"@getpaseo/protocol": "0.
|
|
33
|
-
"@getpaseo/server": "0.
|
|
31
|
+
"@getpaseo/client": "0.3.0-beta.1",
|
|
32
|
+
"@getpaseo/protocol": "0.3.0-beta.1",
|
|
33
|
+
"@getpaseo/server": "0.3.0-beta.1",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
36
|
"mime-types": "^2.1.35",
|