@getpaseo/cli 0.7.0-beta.1 → 0.7.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.
@@ -80,13 +80,16 @@ function toListItem(agent) {
80
80
  }
81
81
  function daemonConnectionFailure(host, cause) {
82
82
  const reason = cause instanceof Error ? cause.message : String(cause);
83
+ const isSsh = host.trim().startsWith("ssh://");
83
84
  return {
84
85
  code: "DAEMON_NOT_RUNNING",
85
86
  message: `Cannot reach the daemon at ${host}: ${reason}`,
86
- details: [
87
- "Start a local daemon with: paseo daemon start",
88
- "To use another daemon, pass --host <host:port> or set PASEO_HOST.",
89
- ].join("\n"),
87
+ details: isSsh
88
+ ? "Start the Paseo daemon on the SSH host; SSH transport does not install or start it."
89
+ : [
90
+ "Start a local daemon with: paseo daemon start",
91
+ "To use another daemon, pass --host <host:port> or set PASEO_HOST.",
92
+ ].join("\n"),
90
93
  };
91
94
  }
92
95
  function parseLabelFilters(labels) {
@@ -55,6 +55,46 @@ const SDK_DECLARATIONS = `declare module "@getpaseo/plugin/server" {
55
55
  export const PluginAttachmentSearchPayloadSchema: import("zod").ZodType<PluginAttachmentSearchPayload>;
56
56
  }
57
57
 
58
+ declare module "@getpaseo/plugin/react-native" {
59
+ import type { ComponentType, FunctionComponent, ReactNode } from "react";
60
+
61
+ export interface PluginIconProps {
62
+ name: string;
63
+ size?: number;
64
+ color?: string;
65
+ }
66
+
67
+ export interface ModalProps {
68
+ title: string;
69
+ icon?: ReactNode;
70
+ open: boolean;
71
+ onOpenChange(open: boolean): void;
72
+ children: ReactNode;
73
+ }
74
+
75
+ export interface ModalContentProps {
76
+ children: ReactNode;
77
+ }
78
+
79
+ export interface ModalComponent extends FunctionComponent<ModalProps> {
80
+ Content: ComponentType<ModalContentProps>;
81
+ }
82
+
83
+ export type ToastVariant = "default" | "info" | "success" | "warning" | "error";
84
+ export interface ToastOptions {
85
+ variant?: ToastVariant;
86
+ durationMs?: number;
87
+ }
88
+ export interface ToastApi {
89
+ show(message: string, options?: ToastOptions): void;
90
+ error(message: string): void;
91
+ }
92
+
93
+ export const Icon: ComponentType<PluginIconProps>;
94
+ export const Modal: ModalComponent;
95
+ export function useToast(): ToastApi;
96
+ }
97
+
58
98
  declare module "@getpaseo/plugin" {
59
99
  import type { ComponentType } from "react";
60
100
  import type { PaseoApi } from "@getpaseo/client";
@@ -154,8 +194,26 @@ declare module "@getpaseo/plugin" {
154
194
  agentId: string;
155
195
  }
156
196
 
197
+ export interface PluginComposerPillProps extends PluginHostProps {
198
+ workspaceId: string;
199
+ agentId: string;
200
+ }
201
+
202
+ export interface PluginComposerPillContribution {
203
+ id: string;
204
+ title: string;
205
+ workspaceId: string;
206
+ agentId: string;
207
+ Component: ComponentType<PluginComposerPillProps>;
208
+ onPress(): void | Promise<void>;
209
+ }
210
+
157
211
  export type PluginPanelLocation = "workspace" | "explorer";
158
212
  export interface PluginOpenPanelOptions { location?: PluginPanelLocation; }
213
+ export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions {
214
+ workspaceId: string;
215
+ agentId?: string;
216
+ }
159
217
 
160
218
  export type PluginWorkspacePanelContribution =
161
219
  | { id: string; title: string; icon: string; locations?: readonly PluginPanelLocation[]; context: "workspace"; Component: ComponentType<PluginWorkspacePanelProps> }
@@ -240,6 +298,12 @@ declare module "@getpaseo/plugin" {
240
298
  openPanel(id: string, options?: PluginOpenPanelOptions): void;
241
299
  }
242
300
 
301
+ export interface PluginClientContext extends PluginCommandCapabilities {
302
+ addComposerPill(contribution: PluginComposerPillContribution): PluginCleanup;
303
+ openPanel(id: string, options: PluginClientOpenPanelOptions): void;
304
+ }
305
+ export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
306
+
243
307
  export type PluginCommandCenterItemContribution =
244
308
  | { id: string; title: string; icon: string; keywords?: readonly string[]; context: "global"; onSelect(context: PluginGlobalCommandContext): void | Promise<void> }
245
309
  | { id: string; title: string; icon: string; keywords?: readonly string[]; context: "workspace"; onSelect(context: PluginWorkspaceCommandContext): void | Promise<void> }
@@ -257,6 +321,7 @@ declare module "@getpaseo/plugin" {
257
321
  addSidebarItem(contribution: PluginSidebarContribution): void;
258
322
  addWorkspacePanel(contribution: PluginWorkspacePanelContribution): void;
259
323
  addCommandCenterItem(contribution: PluginCommandCenterItemContribution): void;
324
+ addClientSide(contribution: PluginClientContribution): void;
260
325
  addAttachmentSource(contribution: PluginAttachmentSourceContribution): void;
261
326
  addTheme(contribution: PluginThemeContribution): void;
262
327
  addTimelineTransformer<ItemType extends AgentTimelineItem["type"]>(contribution: PluginTimelineTransformerContribution<ItemType>): void;
@@ -0,0 +1,9 @@
1
+ import { type SshTransportTarget } from "@getpaseo/protocol/ssh-transport";
2
+ export interface SshTunnel {
3
+ endpoint: string;
4
+ close(): void;
5
+ failureDetail(): string | null;
6
+ }
7
+ export declare function resolveSshFailureDetail(failure: string | null, stderr: string): string | null;
8
+ export declare function createSshTunnel(target: SshTransportTarget): Promise<SshTunnel>;
9
+ //# sourceMappingURL=ssh-tunnel.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createServer } from "node:net";
3
+ import { buildSshTunnelArgs } from "@getpaseo/protocol/ssh-transport";
4
+ const SSH_STDERR_LIMIT = 8192;
5
+ function formatSshFailure(stderr, code, signal) {
6
+ const detail = stderr.trim();
7
+ if (detail)
8
+ return detail;
9
+ if (signal)
10
+ return `ssh exited with signal ${signal}`;
11
+ return `ssh exited with code ${code ?? "unknown"}`;
12
+ }
13
+ export function resolveSshFailureDetail(failure, stderr) {
14
+ return failure ?? (stderr.trim() || null);
15
+ }
16
+ export function createSshTunnel(target) {
17
+ let server = null;
18
+ let socket = null;
19
+ let child = null;
20
+ let stderr = "";
21
+ let failure = null;
22
+ function close() {
23
+ server?.close();
24
+ server = null;
25
+ socket?.destroy();
26
+ socket = null;
27
+ if (child && !child.killed)
28
+ child.kill();
29
+ child = null;
30
+ }
31
+ return new Promise((resolve, reject) => {
32
+ server = createServer((acceptedSocket) => {
33
+ socket = acceptedSocket;
34
+ server?.close();
35
+ server = null;
36
+ child = spawn("ssh", buildSshTunnelArgs(target), {
37
+ stdio: ["pipe", "pipe", "pipe"],
38
+ windowsHide: true,
39
+ });
40
+ child.stderr.on("data", (chunk) => {
41
+ stderr = `${stderr}${chunk.toString()}`.slice(-SSH_STDERR_LIMIT);
42
+ });
43
+ child.on("error", (error) => {
44
+ failure = error.message;
45
+ acceptedSocket.destroy(error);
46
+ });
47
+ child.on("exit", (code, signal) => {
48
+ if (code !== 0 || signal)
49
+ failure = formatSshFailure(stderr, code, signal);
50
+ acceptedSocket.destroy(failure ? new Error(failure) : undefined);
51
+ });
52
+ acceptedSocket.on("error", () => undefined);
53
+ acceptedSocket.on("close", () => {
54
+ if (child && !child.killed)
55
+ child.kill();
56
+ });
57
+ acceptedSocket.pipe(child.stdin);
58
+ child.stdout.pipe(acceptedSocket);
59
+ });
60
+ server.once("error", (error) => {
61
+ close();
62
+ reject(error);
63
+ });
64
+ server.listen(0, "127.0.0.1", () => {
65
+ const address = server?.address();
66
+ if (!address || typeof address === "string") {
67
+ close();
68
+ reject(new Error("Failed to allocate the SSH tunnel port"));
69
+ return;
70
+ }
71
+ resolve({
72
+ endpoint: `127.0.0.1:${address.port}`,
73
+ close,
74
+ failureDetail: () => resolveSshFailureDetail(failure, stderr),
75
+ });
76
+ });
77
+ });
78
+ }
79
+ //# sourceMappingURL=ssh-tunnel.js.map
@@ -2,11 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { loadConfig, resolvePaseoHome } from "@getpaseo/server";
3
3
  import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl, normalizeHostPort, parseConnectionUri, shouldUseTlsForDefaultHostedRelay, } from "@getpaseo/protocol/daemon-endpoints";
4
4
  import { parseConnectionOfferFromUrl, } from "@getpaseo/protocol/connection-offer";
5
+ import { parseSshTransportUri } from "@getpaseo/protocol/ssh-transport";
5
6
  import { DaemonClient } from "@getpaseo/client/internal/daemon-client";
6
7
  import path from "node:path";
7
8
  import { WebSocket } from "ws";
8
9
  import { getOrCreateCliClientId } from "./client-id.js";
9
10
  import { resolveCliVersion } from "../version.js";
11
+ import { createSshTunnel } from "../ssh/ssh-tunnel.js";
10
12
  const DEFAULT_HOST = "localhost:6767";
11
13
  const DEFAULT_TIMEOUT = 15000;
12
14
  const PID_FILENAME = "paseo.pid";
@@ -22,7 +24,9 @@ export function buildDaemonConnectionCommandError(options) {
22
24
  return {
23
25
  code: "DAEMON_NOT_RUNNING",
24
26
  message: `Cannot connect to daemon at ${host}: ${message}`,
25
- details: "Start the daemon with: paseo daemon start",
27
+ details: host.trim().startsWith("ssh://")
28
+ ? "Start the Paseo daemon on the SSH host; SSH transport does not install or start it."
29
+ : "Start the daemon with: paseo daemon start",
26
30
  };
27
31
  }
28
32
  export function normalizeDaemonHost(raw) {
@@ -266,6 +270,19 @@ export async function connectToDaemon(options) {
266
270
  const clientId = await getOrCreateCliClientId();
267
271
  const nodeWebSocketFactory = createNodeWebSocketFactory();
268
272
  const explicitHost = options?.host ?? process.env.PASEO_HOST;
273
+ if (explicitHost?.trim().startsWith("ssh://")) {
274
+ const target = parseSshTransportUri(explicitHost.trim());
275
+ const tunnel = await createSshTunnel(target);
276
+ const password = resolveDaemonPassword(explicitHost);
277
+ const result = await tryConnectHost(tunnel.endpoint, password, clientId, timeout, nodeWebSocketFactory);
278
+ if ("client" in result)
279
+ return result.client;
280
+ const failure = tunnel.failureDetail();
281
+ tunnel.close();
282
+ if (failure)
283
+ throw new Error(`SSH connection failed: ${failure}`, { cause: result.error });
284
+ throw result.error;
285
+ }
269
286
  const offer = parseHostOfferOrNull(explicitHost);
270
287
  if (offer) {
271
288
  return connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFactory);
@@ -1,5 +1,5 @@
1
1
  const JSON_OPTION_DESCRIPTION = "Output in JSON format";
2
- const DAEMON_HOST_OPTION_DESCRIPTION = "Daemon host target: host:port or tcp://host:port?ssl=true&password=secret (default: local socket/pipe, then localhost:6767)";
2
+ const DAEMON_HOST_OPTION_DESCRIPTION = "Daemon host target: host:port, tcp://host:port, or ssh://user@host (default: local socket/pipe, then localhost:6767)";
3
3
  export function collectMultiple(value, previous) {
4
4
  return previous.concat([value]);
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/cli",
3
- "version": "0.7.0-beta.1",
3
+ "version": "0.7.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.7.0-beta.1",
32
- "@getpaseo/protocol": "0.7.0-beta.1",
33
- "@getpaseo/server": "0.7.0-beta.1",
31
+ "@getpaseo/client": "0.7.0-beta.2",
32
+ "@getpaseo/protocol": "0.7.0-beta.2",
33
+ "@getpaseo/server": "0.7.0-beta.2",
34
34
  "chalk": "^5.3.0",
35
35
  "commander": "^12.0.0",
36
36
  "mime-types": "^2.1.35",