Files
paseo/packages/cli/src/cli.ts
Mohamed Boudra a414f8ea85 Connect your Paseo daemon to Hub (#2035)
* feat(hub): connect daemons to Paseo Hub

Make Hub an explicit daemon-owned relationship with local-only management and scoped access to Hub-owned executions.

* fix(hub): harden relationship boundaries

* fix(hub): harden relationship lifecycle

* fix(hub): isolate CLI test entrypoint

* fix(hub): run CLI tests from workspace source

* fix(hub): settle failed relationship connections

* fix(hub): resume interrupted owned turns

Provider session rehydration does not continue foreground work lost during daemon shutdown. Persist narrowly scoped Hub execution intent and replay only an interrupted running initial turn.

* fix(hub): harden relationship lifecycle

Keep optional Hub authority from blocking daemon startup, revoke ambiguous enrollments durably, and close owned agents when their relationship no longer exists. Reject remote CLI connect targets before transmitting enrollment authority.

* fix(hub): stop replaying interrupted turns

Daemon restart cannot safely guarantee prompt idempotency across providers. Persist the normal closed session state while retaining Hub relationship, execution, and agent identity.

* fix(hub): fail creates when prompts cannot start

* fix: make Hub lifecycle cleanup deterministic

* fix(hub): preserve fresh enrollment authority

* fix(hub): contain enrollment retry failures

* fix(hub): reject invalid socket transport URLs

* fix(hub): bind socket transport to Hub authority

* fix(hub): close relationship lifecycle gaps

* test(hub): stabilize lifecycle coverage on Windows

* fix(hub): close execution authority races

* fix(hub): return relationship command errors

* fix(app): preserve workspace navigation compatibility

* fix(hub): correct relationship trust boundaries

Authenticated daemon sessions own relationship management regardless of transport. The separate Hub session remains operation-allowlisted, rejects malformed execution inputs, and uses bounded outbound handshakes.

* refactor(hub): authorize execution through sessions

* fix(hub): validate persisted origins

* fix(hub): retain local execution grants

* fix(hub): enforce session scope boundaries

Make session authority explicit and mutable without adding scope negotiation. Fence persisted Hub scopes and retire in-flight execution authority during cleanup and re-enrollment.

* fix(server): preserve main session compatibility
2026-07-17 20:20:57 +02:00

192 lines
7.3 KiB
TypeScript

import { Command, Option } from "commander";
import { createAgentCommand } from "./commands/agent/index.js";
import { createDaemonCommand } from "./commands/daemon/index.js";
import { createChatCommand } from "./commands/chat/index.js";
import { createLoopCommand } from "./commands/loop/index.js";
import { createPermitCommand } from "./commands/permit/index.js";
import { createProviderCommand } from "./commands/provider/index.js";
import { createScheduleCommand } from "./commands/schedule/index.js";
import { createSpeechCommand } from "./commands/speech/index.js";
import { createTerminalCommand } from "./commands/terminal/index.js";
import { createWorktreeCommand } from "./commands/worktree/index.js";
import { createHubCommand } from "./commands/hub/index.js";
import { createHooksCommand } from "./commands/hooks.js";
import { startCommand as daemonStartCommand } from "./commands/daemon/start.js";
import { runStatusCommand as runDaemonStatusCommand } from "./commands/daemon/status.js";
import { runRestartCommand as runDaemonRestartCommand } from "./commands/daemon/restart.js";
import { addLsOptions, runLsCommand } from "./commands/agent/ls.js";
import { addRunOptions, runRunCommand } from "./commands/agent/run.js";
import { addLogsOptions, runLogsCommand } from "./commands/agent/logs.js";
import { addDeleteOptions, runDeleteCommand } from "./commands/agent/delete.js";
import { addStopOptions, runStopCommand } from "./commands/agent/stop.js";
import { addSendOptions, runSendCommand } from "./commands/agent/send.js";
import { addInspectOptions, runInspectCommand } from "./commands/agent/inspect.js";
import { addWaitOptions, runWaitCommand } from "./commands/agent/wait.js";
import { addArchiveOptions, runArchiveCommand } from "./commands/agent/archive.js";
import { addAttachOptions, runAttachCommand } from "./commands/agent/attach.js";
import { addImportOptions, runImportCommand } from "./commands/agent/import.js";
import { withOutput } from "./output/index.js";
import { runCloneCommand } from "./commands/clone.js";
import { onboardCommand } from "./commands/onboard.js";
import {
addDaemonHostOption,
addJsonAndDaemonHostOptions,
addJsonOption,
} from "./utils/command-options.js";
import { resolveCliVersion } from "./version.js";
const VERSION = resolveCliVersion();
function resolveHostnamesOption(hostnames: unknown, allowedHosts: unknown): string | undefined {
if (typeof hostnames === "string") return hostnames;
if (typeof allowedHosts === "string") return allowedHosts;
return undefined;
}
export function createCli(): Command {
const program = new Command();
program
.name("paseo")
.description("Paseo CLI - control your AI coding agents from the command line")
.version(VERSION, "-v, --version", "output the version number")
// Global output options
.option("-o, --format <format>", "output format: table, json, yaml", "table")
.option("--json", "output in JSON format (alias for --format json)")
.option("-q, --quiet", "minimal output (IDs only)")
.option("--no-headers", "omit table headers")
.option("--no-color", "disable colored output");
// Primary agent commands (top-level)
addJsonAndDaemonHostOptions(addLsOptions(program.command("ls"))).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(addRunOptions(program.command("run"))).action(
withOutput(runRunCommand),
);
addJsonAndDaemonHostOptions(addImportOptions(program.command("import"))).action(
withOutput(runImportCommand),
);
addJsonAndDaemonHostOptions(
program
.command("clone")
.description("Clone a GitHub repo and register it as a Paseo workspace")
.argument("<repo>", "GitHub repo in owner/repo format or a full git remote URL")
.requiredOption("--dir <path>", "Parent directory to clone into (for example: ~/workspace)"),
)
.addOption(
new Option("--protocol <protocol>", "Protocol for owner/repo shorthand repositories").choices(
["https", "ssh"],
),
)
.action(withOutput(runCloneCommand));
addDaemonHostOption(addAttachOptions(program.command("attach"))).action(runAttachCommand);
addDaemonHostOption(addLogsOptions(program.command("logs"))).action(runLogsCommand);
addJsonAndDaemonHostOptions(addStopOptions(program.command("stop"))).action(
withOutput(runStopCommand),
);
addJsonAndDaemonHostOptions(addDeleteOptions(program.command("delete"))).action(
withOutput(runDeleteCommand),
);
addJsonAndDaemonHostOptions(addSendOptions(program.command("send"))).action(
withOutput(runSendCommand),
);
addJsonAndDaemonHostOptions(addInspectOptions(program.command("inspect"))).action(
withOutput(runInspectCommand),
);
addJsonAndDaemonHostOptions(addWaitOptions(program.command("wait"))).action(
withOutput(runWaitCommand),
);
addJsonAndDaemonHostOptions(addArchiveOptions(program.command("archive"))).action(
withOutput(runArchiveCommand),
);
// Top-level local daemon shortcuts
program.addCommand(onboardCommand());
program.addCommand(daemonStartCommand());
program.addCommand(createHooksCommand());
addJsonOption(
program
.command("status")
.description('Show local daemon status (alias for "paseo daemon status")'),
)
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.action(withOutput(runDaemonStatusCommand));
addJsonOption(
program
.command("restart")
.description('Restart local daemon (alias for "paseo daemon restart")'),
)
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.option("--timeout <seconds>", "Wait timeout before force step (default: 15)")
.option("--force", "Send SIGKILL if graceful stop times out")
.option(
"--listen <listen>",
"Listen target for restarted daemon (host:port, port, or unix socket)",
)
.option("--port <port>", "Port for restarted daemon listen target")
.option("--no-relay", "Disable relay on restarted daemon")
.option("--no-mcp", "Disable Agent MCP on restarted daemon")
.option(
"--hostnames <hosts>",
'Daemon hostnames (comma-separated, e.g. "myhost,.example.com" or "true" for any)',
)
.addOption(new Option("--allowed-hosts <hosts>").hideHelp())
.action(
withOutput((...args) => {
const [options, command] = args.slice(-2) as [(typeof args)[number], Command];
return runDaemonRestartCommand(
{
...options,
hostnames: resolveHostnamesOption(options.hostnames, options.allowedHosts),
},
command,
);
}),
);
// Advanced agent commands (less common operations)
program.addCommand(createAgentCommand());
// Daemon commands
program.addCommand(createDaemonCommand());
program.addCommand(createHubCommand());
// Chat commands
program.addCommand(createChatCommand());
// Terminal commands
program.addCommand(createTerminalCommand());
// Loop commands
program.addCommand(createLoopCommand());
// Schedule commands
program.addCommand(createScheduleCommand());
// Permission commands
program.addCommand(createPermitCommand());
// Provider commands
program.addCommand(createProviderCommand());
// Speech model commands
program.addCommand(createSpeechCommand());
// Worktree commands
program.addCommand(createWorktreeCommand());
return program;
}