fix(cli): query daemon for status and pairing offer over RPC

Closes #1081
This commit is contained in:
Zexin Yuan
2026-05-19 03:15:52 +00:00
committed by GitHub
parent 8ff63a6d71
commit 383b380d8a
9 changed files with 334 additions and 15 deletions
@@ -392,9 +392,15 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
const env: NodeJS.ProcessEnv = {
...envWithHome(options.home),
// Status should reflect local persisted config + pid file, not inherited daemon env overrides.
// This is CLI-side defensive scrubbing; the daemon RPC is authoritative when available.
PASEO_LISTEN: undefined,
PASEO_HOSTNAMES: undefined,
PASEO_ALLOWED_HOSTS: undefined,
PASEO_RELAY_ENABLED: undefined,
PASEO_RELAY_ENDPOINT: undefined,
PASEO_RELAY_PUBLIC_ENDPOINT: undefined,
PASEO_RELAY_USE_TLS: undefined,
PASEO_RELAY_PUBLIC_USE_TLS: undefined,
};
const home = resolvePaseoHome(env);
const config = loadConfig(home, { env });
+38
View File
@@ -1,6 +1,8 @@
import { Command } from "commander";
import chalk from "chalk";
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpaseo/server";
import { tryConnectToDaemon } from "../../utils/client.js";
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
import { addJsonOption } from "../../utils/command-options.js";
interface PairOptions {
@@ -22,6 +24,35 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
}
const paseoHome = resolvePaseoHome();
const state = resolveLocalDaemonState({ home: paseoHome });
const host = resolveTcpHostFromListen(state.listen);
// Try to get the pairing offer from the running daemon first.
if (host) {
const client = await tryConnectToDaemon({ host, timeout: 1500 });
if (client) {
const supportsDaemonStatusRpc =
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
if (supportsDaemonStatusRpc) {
try {
const offer = await client.getDaemonPairingOffer();
await client.close().catch(() => {});
outputPairingResult(
{ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null },
options,
);
return;
} catch {
// COMPAT(daemon-rpc-rollout): fall back to CLI-side pairing generation while
// old daemons lack daemonStatusRpc. Remove once the daemon floor is past
// v0.1.76; pairing should come from daemon.get_pairing_offer.
}
}
await client.close().catch(() => {});
}
}
// Fall back to local pairing offer generation.
const config = loadConfig(paseoHome);
const pairing = await generateLocalPairingOffer({
paseoHome,
@@ -34,6 +65,13 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
includeQr: true,
});
outputPairingResult(pairing, options);
}
function outputPairingResult(
pairing: { relayEnabled: boolean; url: string | null; qr: string | null },
options: PairOptions,
): void {
if (!pairing.relayEnabled || !pairing.url) {
console.error(chalk.red("Relay pairing is disabled for this daemon config."));
console.error(chalk.yellow("Enable relay and run this command again."));
+56 -14
View File
@@ -10,6 +10,7 @@ interface ProviderBinaryStatus {
label: string;
path: string | null;
version: string | null;
source?: "daemon" | "local";
}
interface DaemonStatus {
@@ -99,7 +100,7 @@ function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
return "red";
}
if (item.key.startsWith(" ")) {
if (item.value === "not found") return "red";
if (item.value === "not found" || item.value === "not found (daemon)") return "red";
if (item.value.endsWith("(--version failed)")) return "yellow";
return "green";
}
@@ -149,7 +150,13 @@ function toStatusRows(status: DaemonStatus): StatusRow[] {
rows.push({ key: "", value: "" });
rows.push({ key: "Providers", value: "" });
for (const provider of status.providers) {
if (!provider.path) {
if (provider.source === "daemon") {
if (!provider.path) {
rows.push({ key: ` ${provider.label}`, value: "not found (daemon)" });
} else {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (daemon)` });
}
} else if (!provider.path) {
rows.push({ key: ` ${provider.label}`, value: "not found" });
} else if (!provider.version) {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` });
@@ -210,6 +217,7 @@ interface DaemonProbeResult {
runningAgents?: number;
idleAgents?: number;
daemonNodeOverride?: string;
daemonProviders?: ProviderBinaryStatus[];
note?: string;
}
@@ -231,12 +239,32 @@ async function probeDaemonOverWebsocket(args: {
}
const daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
const supportsDaemonStatusRpc =
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
const runningAgents = agents.filter((a) => a.status === "running").length;
const idleAgents = agents.filter((a) => a.status === "idle").length;
let daemonProviders: ProviderBinaryStatus[] | undefined;
if (supportsDaemonStatusRpc) {
try {
const statusPayload = await client.getDaemonStatus();
const labelMap = new Map(PROVIDER_BINARIES.map((p) => [p.binary, p.label]));
daemonProviders = statusPayload.providers.map((p) => ({
label: labelMap.get(p.provider) ?? p.provider,
path: p.available ? "available" : null,
version: p.available ? null : (p.error ?? null),
source: "daemon" as const,
}));
} catch {
// COMPAT(daemon-rpc-rollout): fall back to CLI-side provider resolution while
// old daemons lack daemonStatusRpc. Remove once the daemon floor is past
// v0.1.76; status should come from daemon.get_status.
}
}
if (!state.running) {
return {
connectedDaemon: "reachable",
@@ -244,6 +272,7 @@ async function probeDaemonOverWebsocket(args: {
runningAgents,
idleAgents,
daemonNodeOverride: "unknown (API reachable, PID unresolved)",
daemonProviders,
note: state.pidInfo
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
@@ -255,6 +284,7 @@ async function probeDaemonOverWebsocket(args: {
daemonVersion,
runningAgents,
idleAgents,
daemonProviders,
};
} catch {
return {
@@ -278,6 +308,7 @@ interface ProbeMergeState {
daemonVersion: string | null;
runningAgents: number | null;
idleAgents: number | null;
daemonProviders: ProviderBinaryStatus[] | undefined;
note: string | undefined;
}
@@ -290,6 +321,7 @@ function applyProbeToStatus(input: ProbeMergeState): Omit<ProbeMergeState, "prob
daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
note: probe.note ? appendNote(input.note, probe.note) : input.note,
};
}
@@ -338,6 +370,7 @@ export async function runStatusCommand(
let runningAgents: number | null = null;
let idleAgents: number | null = null;
let daemonVersion: string | null = null;
let daemonProviders: ProviderBinaryStatus[] | undefined;
let note: string | undefined;
if (!state.running && state.stalePidFile && state.pidInfo) {
@@ -347,17 +380,26 @@ export async function runStatusCommand(
if (host) {
const probe = await probeDaemonOverWebsocket({ host, state });
({ connectedDaemon, localDaemon, daemonNode, daemonVersion, runningAgents, idleAgents, note } =
applyProbeToStatus({
probe,
connectedDaemon,
localDaemon,
daemonNode,
daemonVersion,
runningAgents,
idleAgents,
note,
}));
({
connectedDaemon,
localDaemon,
daemonNode,
daemonVersion,
runningAgents,
idleAgents,
daemonProviders,
note,
} = applyProbeToStatus({
probe,
connectedDaemon,
localDaemon,
daemonNode,
daemonVersion,
runningAgents,
idleAgents,
daemonProviders,
note,
}));
} else {
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
}
@@ -370,7 +412,7 @@ export async function runStatusCommand(
note = appendNote(note, serverIdResult.error);
}
const providers = await checkProviderBinaries();
const providers = daemonProviders ?? (await checkProviderBinaries());
const daemonStatus: DaemonStatus = {
serverId,