mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Open existing agents from links and the CLI (#2324)
* feat(desktop): open existing agents from links Register a stable agent deep link and route it through the existing Desktop window. Add a matching CLI command that resolves the local server and activates the requested agent without creating or messaging it. * fix(desktop): recover agent link delivery
This commit is contained in:
@@ -34,4 +34,12 @@ describe("canonical CLI surface", () => {
|
||||
expect(run?.helpInformation()).toContain("--background");
|
||||
expect(run?.helpInformation()).not.toContain("--detach");
|
||||
});
|
||||
|
||||
it("offers opening an existing agent in the desktop app", () => {
|
||||
const agent = createCli().commands.find((command) => command.name() === "agent");
|
||||
const open = agent?.commands.find((command) => command.name() === "open");
|
||||
|
||||
expect(open?.helpInformation()).toContain("<agent-id>");
|
||||
expect(open?.helpInformation()).toContain("--server <server-id>");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { addReloadOptions, runReloadCommand } from "./reload.js";
|
||||
import { addImportOptions, runImportCommand } from "./import.js";
|
||||
import { runUpdateCommand } from "./update.js";
|
||||
import { runDetachCommand } from "./detach.js";
|
||||
import { addOpenOptions, runOpenCommand } from "./open.js";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import {
|
||||
addDaemonHostOption,
|
||||
@@ -39,6 +40,10 @@ export function createAgentCommand(): Command {
|
||||
|
||||
addDaemonHostOption(addLogsOptions(agent.command("logs"))).action(runLogsCommand);
|
||||
|
||||
addJsonAndDaemonHostOptions(addOpenOptions(agent.command("open"))).action(
|
||||
withOutput(runOpenCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(addStopOptions(agent.command("stop"))).action(
|
||||
withOutput(runStopCommand),
|
||||
);
|
||||
|
||||
82
packages/cli/src/commands/agent/open.ts
Normal file
82
packages/cli/src/commands/agent/open.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Command } from "commander";
|
||||
import { buildDaemonConnectionCommandError, connectToDaemon } from "../../utils/client.js";
|
||||
import { openDesktopWithAgent } from "../open.js";
|
||||
import type {
|
||||
CommandError,
|
||||
CommandOptions,
|
||||
OutputSchema,
|
||||
SingleResult,
|
||||
} from "../../output/index.js";
|
||||
|
||||
interface OpenAgentResult {
|
||||
agentId: string;
|
||||
serverId: string;
|
||||
status: "opened";
|
||||
}
|
||||
|
||||
const openAgentSchema: OutputSchema<OpenAgentResult> = {
|
||||
idField: "agentId",
|
||||
columns: [
|
||||
{ header: "AGENT ID", field: "agentId" },
|
||||
{ header: "SERVER ID", field: "serverId" },
|
||||
{ header: "STATUS", field: "status" },
|
||||
],
|
||||
};
|
||||
|
||||
export function addOpenOptions(command: Command): Command {
|
||||
return command
|
||||
.description("Open an existing agent in Paseo Desktop")
|
||||
.argument("<agent-id>", "Existing agent ID")
|
||||
.option("--server <server-id>", "Server ID (defaults to the local daemon)");
|
||||
}
|
||||
|
||||
async function resolveServerId(options: CommandOptions): Promise<string> {
|
||||
const explicitServerId = typeof options.server === "string" ? options.server.trim() : "";
|
||||
if (explicitServerId) {
|
||||
return explicitServerId;
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await connectToDaemon({ host: options.host });
|
||||
} catch (error) {
|
||||
throw buildDaemonConnectionCommandError({ host: options.host, error });
|
||||
}
|
||||
try {
|
||||
const serverId = client.getLastServerInfoMessage()?.serverId.trim();
|
||||
if (!serverId) {
|
||||
const error: CommandError = {
|
||||
code: "SERVER_ID_UNAVAILABLE",
|
||||
message: "The daemon did not report a server ID.",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
return serverId;
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export async function runOpenCommand(
|
||||
agentIdArg: string,
|
||||
options: CommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<OpenAgentResult>> {
|
||||
const agentId = agentIdArg.trim();
|
||||
if (!agentId) {
|
||||
const error: CommandError = {
|
||||
code: "MISSING_AGENT_ID",
|
||||
message: "Agent ID is required.",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
const serverId = await resolveServerId(options);
|
||||
await openDesktopWithAgent({ serverId, agentId });
|
||||
|
||||
return {
|
||||
type: "single",
|
||||
data: { agentId, serverId, status: "opened" },
|
||||
schema: openAgentSchema,
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnProcess } from "@getpaseo/server";
|
||||
import { buildAgentDeepLink, type AgentDeepLinkTarget } from "@getpaseo/protocol/agent-deep-link";
|
||||
|
||||
function findDesktopApp(): string | null {
|
||||
if (process.platform === "darwin") {
|
||||
@@ -67,35 +68,39 @@ function spawnDetached(command: string, args: string[]): void {
|
||||
}).unref();
|
||||
}
|
||||
|
||||
function launchDesktop(args: string[]): void {
|
||||
if (process.env.PASEO_DESKTOP_CLI === "1") {
|
||||
throw new Error("Cannot open Paseo Desktop while running in desktop CLI passthrough mode.");
|
||||
}
|
||||
|
||||
const desktopApp = findDesktopApp();
|
||||
if (!desktopApp) {
|
||||
throw new Error(
|
||||
"Paseo desktop app not found. Install it from https://github.com/getpaseo/paseo/releases",
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
// -n forces a new instance even if the app is already running. The new
|
||||
// instance relays its argv to the existing one through Electron's
|
||||
// single-instance lock. -g keeps the terminal in the foreground.
|
||||
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", ...args]);
|
||||
return;
|
||||
}
|
||||
|
||||
spawnDetached(desktopApp, args);
|
||||
}
|
||||
|
||||
export async function openDesktopWithProject(projectPath: string): Promise<void> {
|
||||
try {
|
||||
if (process.env.PASEO_DESKTOP_CLI === "1") {
|
||||
throw new Error(
|
||||
"Cannot open a desktop project while running in desktop CLI passthrough mode.",
|
||||
);
|
||||
}
|
||||
|
||||
const desktopApp = findDesktopApp();
|
||||
if (!desktopApp) {
|
||||
throw new Error(
|
||||
"Paseo desktop app not found. Install it from https://github.com/getpaseo/paseo/releases",
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
// -n forces a new instance even if the app is already running.
|
||||
// The new instance hits requestSingleInstanceLock(), fails, and relays
|
||||
// the argv to the first instance via the second-instance event.
|
||||
// -g keeps the terminal in the foreground (better CLI UX).
|
||||
// Without -n, macOS just activates the existing window and drops --args.
|
||||
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", projectPath]);
|
||||
return;
|
||||
}
|
||||
|
||||
spawnDetached(desktopApp, [projectPath]);
|
||||
launchDesktop([projectPath]);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openDesktopWithAgent(target: AgentDeepLinkTarget): Promise<void> {
|
||||
launchDesktop([buildAgentDeepLink(target)]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user