@getpaseo/cli 0.2.5 → 0.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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")
@@ -31,6 +31,9 @@ function envWithHome(home) {
31
31
  }
32
32
  function buildRunnerArgs(options) {
33
33
  const args = [];
34
+ if (options.relay === true) {
35
+ args.push("--relay");
36
+ }
34
37
  if (options.relay === false) {
35
38
  args.push("--no-relay");
36
39
  }
@@ -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 runPairCommand(options: PairOptions): Promise<void>;
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, resolveTcpHostFromListen } from "./local-daemon.js";
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 runPairCommand(options) {
17
- if (options.home) {
18
- process.env.PASEO_HOME = options.home;
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 paseoHome = resolvePaseoHome();
21
- const state = resolveLocalDaemonState({ home: paseoHome });
22
- const host = resolveTcpHostFromListen(state.listen);
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
- // Fall back to local pairing offer generation.
47
- const config = loadConfig(paseoHome);
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 outputPairingResult(pairing, options) {
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
- console.error(chalk.red("Relay pairing is disabled for this daemon config."));
63
- console.error(chalk.yellow("Enable relay and run this command again."));
64
- process.exit(1);
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
- process.stdout.write(`${JSON.stringify({
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
- process.stdout.write(formatPairingInstructions({
161
+ output.writeStdout(formatPairingInstructions({
75
162
  url: pairing.url,
76
163
  qr: pairing.qr,
77
- columns: process.stdout.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, resolveTcpHostFromListen } from "./local-daemon.js";
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 formatRelayStatus(state) {
268
- if (!state.relayEnabled)
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 = state.relayPublicUseTls ? "wss" : "ws";
271
- return `${scheme}://${state.relayEndpoint}`;
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 host = resolveTcpHostFromListen(state.listen);
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 (host) {
290
- const probe = await probeDaemonOverWebsocket({ host, state });
291
- ({ connectedDaemon, localDaemon, daemonNode, daemonVersion, daemonProviders, note } =
292
- applyProbeToStatus({
293
- probe,
294
- connectedDaemon,
295
- localDaemon,
296
- daemonNode,
297
- daemonVersion,
298
- daemonProviders,
299
- note,
300
- }));
301
- }
302
- else {
303
- note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
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: formatRelayStatus(state),
343
+ relay: relayStatus,
319
344
  hostname: state.pidInfo?.hostname ?? null,
320
345
  pid: state.pidInfo?.pid ?? null,
321
346
  startedAt: state.pidInfo?.startedAt ?? null,
@@ -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 { generateLocalPairingOffer, loadConfig, loadPersistedConfig, } from "@getpaseo/server";
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 (config.relayEnabled === false) {
385
- log.warn("Relay is disabled; pairing offer is unavailable for this daemon.");
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
- const pairing = await generateLocalPairingOffer({
366
+ let pairing = await resolveLocalPairingOffer({
393
367
  paseoHome,
394
- relayEnabled: config.relayEnabled,
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 cliArgv = invocation.argv.length === 0 ? ["onboard"] : invocation.argv;
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 = {}) {
@@ -141,7 +141,10 @@ function stripIpcPrefix(trimmed) {
141
141
  return trimmed;
142
142
  }
143
143
  export function resolveDaemonTarget(host) {
144
- const trimmed = host.trim();
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.2.5",
3
+ "version": "0.3.0-beta.2",
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.2.5",
32
- "@getpaseo/protocol": "0.2.5",
33
- "@getpaseo/server": "0.2.5",
31
+ "@getpaseo/client": "0.3.0-beta.2",
32
+ "@getpaseo/protocol": "0.3.0-beta.2",
33
+ "@getpaseo/server": "0.3.0-beta.2",
34
34
  "chalk": "^5.3.0",
35
35
  "commander": "^12.0.0",
36
36
  "mime-types": "^2.1.35",