@camerontaylor/paseo-client 0.12.0-fork.1 → 0.13.0-fork.1

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.
@@ -0,0 +1,22 @@
1
+ import { DaemonClient } from "../daemon-client.js";
2
+ import type { DaemonTransportFactory } from "../daemon-client-transport-types.js";
3
+ import type { OwnedSubscription } from "../connection/index.js";
4
+ import type { PaseoApi, createPaseoApi } from "../index.js";
5
+ import type { HostAdd, HostUpdate, HostSummary, HostDirectory } from "@camerontaylor/paseo-protocol/hosts";
6
+ export interface PaseoHostActions {
7
+ list(): Promise<HostDirectory>;
8
+ observe(): OwnedSubscription<HostDirectory>;
9
+ add(input: HostAdd): Promise<HostSummary>;
10
+ update(registrationId: string, patch: HostUpdate): Promise<HostSummary>;
11
+ remove(registrationId: string): Promise<void>;
12
+ client(serverId: string): Promise<PaseoApi>;
13
+ }
14
+ export declare function createHostChannelTransport(origin: DaemonClient, serverId: string, scopeId: string, initialRouteId: string | null, link?: boolean): DaemonTransportFactory;
15
+ export declare function createHostActions(origin: DaemonClient, owner: {
16
+ own<T extends {
17
+ release(): Promise<void>;
18
+ }>(create: () => T): T;
19
+ signal: AbortSignal;
20
+ api: typeof createPaseoApi;
21
+ }): PaseoHostActions;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,188 @@
1
+ import { safeRandomId } from "../daemon-client-transport-utils.js";
2
+ import { DaemonClient } from "../daemon-client.js";
3
+ export function createHostChannelTransport(origin, serverId, scopeId, initialRouteId, link = false) {
4
+ let first = initialRouteId;
5
+ return () => {
6
+ const opens = new Set();
7
+ const closes = new Set();
8
+ const messages = new Set();
9
+ const errors = new Set();
10
+ let routeId = null;
11
+ let closed = false;
12
+ const cleanups = [];
13
+ const close = (error) => {
14
+ if (closed)
15
+ return;
16
+ closed = true;
17
+ if (error instanceof Error)
18
+ Object.assign(error, { serverId });
19
+ for (const cleanup of cleanups)
20
+ cleanup();
21
+ try {
22
+ if (routeId)
23
+ origin.closeHostRoute(routeId);
24
+ else
25
+ origin.cancelHostRoute(scopeId);
26
+ }
27
+ catch {
28
+ /* Transport already closed. */
29
+ }
30
+ if (error)
31
+ for (const handler of errors)
32
+ handler(error);
33
+ for (const handler of closes)
34
+ handler(error);
35
+ };
36
+ cleanups.push(origin.subscribeRawMessages((message) => {
37
+ if (message.type === "hosts.route.frame" && message.payload.subscriptionId === routeId) {
38
+ for (const handler of messages)
39
+ handler(message.payload.data, false);
40
+ }
41
+ if (message.type === "hosts.route.closed" && message.payload.subscriptionId === routeId)
42
+ close(Object.assign(new Error(message.payload.message), {
43
+ code: message.payload.code,
44
+ serverId,
45
+ }));
46
+ }));
47
+ cleanups.push(origin.subscribeHostFrames((frame) => {
48
+ if (frame.routeId === routeId)
49
+ for (const handler of messages)
50
+ handler(frame.payload, true);
51
+ }));
52
+ cleanups.push(origin.subscribeConnectionStatus((connectionState) => {
53
+ if (connectionState.status !== "connected")
54
+ close();
55
+ }));
56
+ let opening;
57
+ if (first)
58
+ opening = Promise.resolve(first);
59
+ else
60
+ opening = link ? origin.openHostLink(scopeId) : origin.openHostRoute(serverId, scopeId);
61
+ first = null;
62
+ void opening
63
+ .then((id) => {
64
+ if (closed) {
65
+ origin.closeHostRoute(id);
66
+ return;
67
+ }
68
+ routeId = id;
69
+ for (const handler of opens)
70
+ handler();
71
+ return undefined;
72
+ })
73
+ .catch(close);
74
+ return {
75
+ send(data) {
76
+ if (closed || !routeId)
77
+ throw Object.assign(new Error(`Host ${serverId} is offline`), {
78
+ code: "host_offline",
79
+ serverId,
80
+ });
81
+ origin.sendHostFrame(routeId, typeof data === "string" || data instanceof Uint8Array ? data : new Uint8Array(data));
82
+ },
83
+ close: () => close(),
84
+ onOpen(handler) {
85
+ opens.add(handler);
86
+ return () => opens.delete(handler);
87
+ },
88
+ onClose(handler) {
89
+ closes.add(handler);
90
+ return () => closes.delete(handler);
91
+ },
92
+ onError(handler) {
93
+ errors.add(handler);
94
+ return () => errors.delete(handler);
95
+ },
96
+ onMessage(handler) {
97
+ messages.add(handler);
98
+ return () => messages.delete(handler);
99
+ },
100
+ };
101
+ };
102
+ }
103
+ export function createHostActions(origin, owner) {
104
+ const assertActive = () => {
105
+ if (owner.signal.aborted)
106
+ throw new Error("Paseo API is disposed");
107
+ };
108
+ return {
109
+ list: () => {
110
+ assertActive();
111
+ return origin.listHosts();
112
+ },
113
+ observe: () => owner.own(() => origin.observeHosts()),
114
+ add: (input) => {
115
+ assertActive();
116
+ return origin.addHost(input);
117
+ },
118
+ update: (id, patch) => {
119
+ assertActive();
120
+ return origin.updateHost(id, patch);
121
+ },
122
+ remove: (id) => {
123
+ assertActive();
124
+ return origin.removeHost(id);
125
+ },
126
+ async client(serverId) {
127
+ assertActive();
128
+ origin.requireConnectedHosts();
129
+ if (origin.getLastServerInfoMessage()?.serverId === serverId) {
130
+ const api = owner.api(origin, { signal: owner.signal });
131
+ const scope = owner.own(() => ({ release: () => api.dispose() }));
132
+ return { ...api, dispose: () => scope.release() };
133
+ }
134
+ const scopeId = safeRandomId();
135
+ const cancel = () => origin.cancelHostRoute(scopeId);
136
+ owner.signal.addEventListener("abort", cancel, { once: true });
137
+ let routeId;
138
+ try {
139
+ routeId = await origin.openHostRoute(serverId, scopeId);
140
+ }
141
+ catch (error) {
142
+ throw Object.assign(error instanceof Error ? error : new Error(String(error)), {
143
+ serverId,
144
+ });
145
+ }
146
+ finally {
147
+ owner.signal.removeEventListener("abort", cancel);
148
+ }
149
+ if (owner.signal.aborted) {
150
+ origin.closeHostRoute(routeId);
151
+ assertActive();
152
+ }
153
+ const client = new DaemonClient({
154
+ url: "ws://routed-host/ws",
155
+ clientId: scopeId,
156
+ transportFactory: createHostChannelTransport(origin, serverId, scopeId, routeId),
157
+ // Initial acquisition has a deadline; established scopes restore demand on loss.
158
+ reconnect: { enabled: false },
159
+ });
160
+ const api = owner.api(client, { signal: owner.signal });
161
+ const scope = owner.own(() => ({
162
+ release: async () => {
163
+ try {
164
+ await api.dispose();
165
+ }
166
+ finally {
167
+ await client.close();
168
+ }
169
+ },
170
+ }));
171
+ try {
172
+ await client.connect();
173
+ client.setReconnectEnabled(true);
174
+ assertActive();
175
+ return { ...api, dispose: () => scope.release() };
176
+ }
177
+ catch (error) {
178
+ const failure = Object.assign(error instanceof Error ? error : new Error("Host handshake failed"), {
179
+ code: error instanceof Error && "code" in error ? error.code : "host_offline",
180
+ serverId,
181
+ });
182
+ await scope.release();
183
+ throw failure;
184
+ }
185
+ },
186
+ };
187
+ }
188
+ //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import type { AgentAddress } from "@camerontaylor/paseo-protocol/addresses";
2
+ export type { AgentAddress } from "@camerontaylor/paseo-protocol/addresses";
3
+ import { type PaseoHostActions } from "./hosts/index.js";
4
+ export type { PaseoHostActions } from "./hosts/index.js";
5
+ export type { HostAdd, HostUpdate, HostSummary, HostDirectory } from "@camerontaylor/paseo-protocol/hosts";
1
6
  import type { OwnedSubscription } from "./connection/index.js";
2
7
  export type { OwnedSubscription, SubscriptionObserver } from "./connection/index.js";
3
8
  import type { DaemonClientConfig } from "./daemon-client.js";
@@ -167,7 +172,7 @@ export interface PaseoAgentCreateOptions {
167
172
  onEvent?: (snapshot: CreationSnapshot) => void;
168
173
  config: PaseoAgentConfig;
169
174
  cwd: string;
170
- parent?: string | PaseoAgentHandle;
175
+ parent?: string | AgentAddress | PaseoAgentHandle;
171
176
  title?: PaseoAgentSessionConfig["title"];
172
177
  env?: CreateAgentRequestMessage["env"];
173
178
  prompt?: string;
@@ -259,6 +264,7 @@ export interface PaseoAgentTimelineHandle {
259
264
  }
260
265
  export interface PaseoAgentHandle {
261
266
  readonly id: string;
267
+ readonly address: AgentAddress;
262
268
  /**
263
269
  * `workspaceId` through `archivedAt` mirror the last snapshot this handle
264
270
  * observed. A handle from `ref()` reads `null` for all of them until
@@ -390,6 +396,7 @@ export interface PaseoConfigActions {
390
396
  }>;
391
397
  }
392
398
  export interface PaseoApi {
399
+ readonly hosts: PaseoHostActions;
393
400
  dispose(): Promise<void>;
394
401
  observeEvents: DaemonClient["observeEvents"];
395
402
  readonly terminals: PaseoTerminalActions;
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { parentAddressLabels } from "@camerontaylor/paseo-protocol/agent-labels";
2
+ import { createHostActions } from "./hosts/index.js";
1
3
  import { DaemonClient } from "./daemon-client.js";
2
4
  import { createTerminalActions, } from "./terminals/index.js";
3
5
  /**
@@ -27,10 +29,11 @@ export function createPaseoClient(config) {
27
29
  getConnectionState: () => daemonClient.getConnectionState(),
28
30
  };
29
31
  }
30
- function toDaemonAgentCreateOptions(options, placement) {
32
+ function toDaemonAgentCreateOptions(options, placement, serverId) {
31
33
  const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
32
34
  const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
33
35
  const { provider, model } = parseProviderModel(providerModel);
36
+ const relationship = parent ? encodeParent(parent, serverId) : {};
34
37
  return {
35
38
  ...requestOptions,
36
39
  config: {
@@ -42,7 +45,12 @@ function toDaemonAgentCreateOptions(options, placement) {
42
45
  ...(providerOptions !== undefined ? { providerOptions } : {}),
43
46
  },
44
47
  ...(placement ? { workspaceId: placement.workspaceId } : {}),
45
- ...(parent ? { callerAgentId: resolveAgentId(parent) } : {}),
48
+ ...(parent
49
+ ? {
50
+ ...relationship,
51
+ labels: { ...requestOptions.labels, ...relationship.labels },
52
+ }
53
+ : {}),
46
54
  ...(prompt !== undefined ? { initialPrompt: prompt } : {}),
47
55
  };
48
56
  }
@@ -81,7 +89,7 @@ export function createPaseoApi(daemonClient, scopeOptions) {
81
89
  };
82
90
  const createAgentHandle = createAgentHandleFactory(daemonClient, listenAgents, (agentId, handler) => own(() => daemonClient.subscribeAgentTimeline(agentId, handler)));
83
91
  const createAgent = async (options, placement) => {
84
- const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement));
92
+ const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement, daemonClient.getLastServerInfoMessage()?.serverId));
85
93
  return createAgentHandle(agent);
86
94
  };
87
95
  const terminals = createTerminalActions(daemonClient, async (workspaceId) => {
@@ -160,6 +168,7 @@ export function createPaseoApi(daemonClient, scopeOptions) {
160
168
  }
161
169
  return {
162
170
  dispose,
171
+ hosts: createHostActions(daemonClient, { own, signal: lifetime.signal, api: createPaseoApi }),
163
172
  observeEvents,
164
173
  terminals,
165
174
  projects: {
@@ -176,7 +185,14 @@ export function createPaseoApi(daemonClient, scopeOptions) {
176
185
  ref: (workspace) => createWorkspaceHandle(workspace),
177
186
  open: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
178
187
  create: async ({ requestId, agent, ...options }) => {
179
- const result = await daemonClient.createWorkspace({ ...options, ...(agent ? { agent: toDaemonAgentCreateOptions(agent) } : {}) }, requestId);
188
+ const result = await daemonClient.createWorkspace({
189
+ ...options,
190
+ ...(agent
191
+ ? {
192
+ agent: toDaemonAgentCreateOptions(agent, undefined, daemonClient.getLastServerInfoMessage()?.serverId),
193
+ }
194
+ : {}),
195
+ }, requestId);
180
196
  if (result.error || !result.workspace) {
181
197
  throw new Error(result.error ?? "The daemon did not create a workspace");
182
198
  }
@@ -296,8 +312,15 @@ function createAgentHandleFactory(daemonClient, listen, subscribeTimeline) {
296
312
  return (agent) => {
297
313
  const id = typeof agent === "string" ? agent : agent.id;
298
314
  let current = typeof agent === "string" ? null : agent;
315
+ let serverId = daemonClient.getLastServerInfoMessage()?.serverId;
299
316
  const handle = {
300
317
  id,
318
+ get address() {
319
+ serverId ?? (serverId = daemonClient.getLastServerInfoMessage()?.serverId);
320
+ if (!serverId)
321
+ throw new Error("Connect to a host before obtaining an agent address");
322
+ return { serverId, agentId: id };
323
+ },
301
324
  timeline: {
302
325
  append: (item) => daemonClient.appendAgentTimelineItem(id, item),
303
326
  refetch: async (options) => {
@@ -429,8 +452,15 @@ async function openWorkspace(daemonClient, createWorkspaceHandle, input, request
429
452
  function resolveWorkspaceId(workspace) {
430
453
  return typeof workspace === "string" ? workspace : workspace.id;
431
454
  }
432
- function resolveAgentId(agent) {
433
- return typeof agent === "string" ? agent : agent.id;
455
+ function encodeParent(parent, serverId) {
456
+ if (typeof parent === "string")
457
+ return { callerAgentId: parent };
458
+ const address = "address" in parent ? parent.address : parent;
459
+ if (!serverId)
460
+ throw new Error("Connect to a host before creating a child");
461
+ return address.serverId === serverId
462
+ ? { callerAgentId: address.agentId, labels: parentAddressLabels(address) }
463
+ : { labels: parentAddressLabels(address) };
434
464
  }
435
465
  function parseProviderModel(selection) {
436
466
  const separator = selection.indexOf("/");
@@ -443,6 +473,7 @@ function parseProviderModel(selection) {
443
473
  };
444
474
  }
445
475
  function listProviderUsage(daemonClient, options) {
476
+ daemonClient.ensureConnected();
446
477
  // COMPAT(providerUsageList): added in v0.1.98, remove after 2027-02-28 once daemon floor >= v0.1.98.
447
478
  if (daemonClient.getLastServerInfoMessage()?.features?.providerUsageList !== true) {
448
479
  return Promise.reject(new Error("Update the host to list provider usage."));
@@ -0,0 +1,10 @@
1
+ /** Counts bytes until the socket's send callback, shared by all links owned by a daemon. */
2
+ export declare class HostTransportBudget {
3
+ readonly limit: number;
4
+ private used;
5
+ constructor(limit?: number);
6
+ get bufferedBytes(): number;
7
+ assertCapacity(bytes: number): void;
8
+ reserve(bytes: number): () => void;
9
+ }
10
+ //# sourceMappingURL=buffering.d.ts.map
@@ -0,0 +1,28 @@
1
+ /** Counts bytes until the socket's send callback, shared by all links owned by a daemon. */
2
+ export class HostTransportBudget {
3
+ constructor(limit = 256 * 1024 * 1024) {
4
+ this.limit = limit;
5
+ this.used = 0;
6
+ }
7
+ get bufferedBytes() {
8
+ return this.used;
9
+ }
10
+ assertCapacity(bytes) {
11
+ if (bytes > this.limit - this.used)
12
+ throw Object.assign(new Error("Connected host buffer limit reached"), {
13
+ code: "resource_limit",
14
+ });
15
+ }
16
+ reserve(bytes) {
17
+ this.assertCapacity(bytes);
18
+ this.used += bytes;
19
+ let released = false;
20
+ return () => {
21
+ if (!released) {
22
+ released = true;
23
+ this.used -= bytes;
24
+ }
25
+ };
26
+ }
27
+ }
28
+ //# sourceMappingURL=buffering.js.map
@@ -0,0 +1,13 @@
1
+ export { createHostChannelTransport } from "../hosts/index.js";
2
+ import { HostTransportBudget } from "./buffering.js";
3
+ export { HostTransportBudget } from "./buffering.js";
4
+ import { parseHostTarget } from "@camerontaylor/paseo-protocol/host-target";
5
+ import type { DaemonTransportFactory } from "../daemon-client-transport-types.js";
6
+ import { DaemonClient, type DaemonClientConfig } from "../daemon-client.js";
7
+ export { parseHostTarget };
8
+ export type { SshTunnel } from "./ssh-tunnel.js";
9
+ export { createSshTunnel, resolveSshFailureDetail } from "./ssh-tunnel.js";
10
+ /** A factory owns one attempt, including a fresh SSH process and its teardown. */
11
+ export declare function createHostTransportFactory(connection: string, budget?: HostTransportBudget): DaemonTransportFactory;
12
+ export declare function createHostClient(connection: string, config: Omit<DaemonClientConfig, "url" | "transportFactory">, budget?: HostTransportBudget): DaemonClient;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,188 @@
1
+ export { createHostChannelTransport } from "../hosts/index.js";
2
+ import { HostTransportBudget } from "./buffering.js";
3
+ export { HostTransportBudget } from "./buffering.js";
4
+ import { homedir } from "node:os";
5
+ import { WebSocket } from "ws";
6
+ import { parseHostTarget } from "@camerontaylor/paseo-protocol/host-target";
7
+ import { createSshTunnel } from "./ssh-tunnel.js";
8
+ import { createWebSocketTransportFactory } from "../daemon-client-websocket-transport.js";
9
+ import { createRelayE2eeTransportFactory } from "../daemon-client-relay-e2ee-transport.js";
10
+ import { DaemonClient } from "../daemon-client.js";
11
+ export { parseHostTarget };
12
+ export { createSshTunnel, resolveSshFailureDetail } from "./ssh-tunnel.js";
13
+ /** A factory owns one attempt, including a fresh SSH process and its teardown. */
14
+ export function createHostTransportFactory(connection, budget = new HostTransportBudget()) {
15
+ const target = parseHostTarget(connection);
16
+ // Rises to "protected" once the daemon negotiates it, so a later attempt
17
+ // cannot be downgraded to legacy endpoint security.
18
+ let securityFloor = target.kind === "socket" ? target.offer?.minimumSecurityMode : undefined;
19
+ return (attemptOptions) => {
20
+ const opens = new Set();
21
+ const closes = new Set();
22
+ const errors = new Set();
23
+ const messages = new Set();
24
+ let closed = false;
25
+ let closeEvent;
26
+ let transport;
27
+ let teardown = () => { };
28
+ let disposeSocket = () => { };
29
+ let assertOutboundCapacity = (bytes) => budget.assertCapacity(bytes);
30
+ let sshTunnel = null;
31
+ const close = (code, reason) => {
32
+ if (closed)
33
+ return;
34
+ closed = true;
35
+ try {
36
+ transport?.close(code, reason);
37
+ }
38
+ finally {
39
+ try {
40
+ disposeSocket();
41
+ teardown();
42
+ }
43
+ finally {
44
+ for (const handler of closes)
45
+ handler(closeEvent);
46
+ }
47
+ }
48
+ };
49
+ void (async () => {
50
+ let socketTarget;
51
+ if (target.kind === "ssh") {
52
+ const tunnel = await createSshTunnel(target.ssh);
53
+ sshTunnel = tunnel;
54
+ teardown = () => tunnel.close();
55
+ socketTarget = parseHostTarget(tunnel.endpoint);
56
+ }
57
+ else
58
+ socketTarget = target;
59
+ if (closed) {
60
+ teardown();
61
+ return;
62
+ }
63
+ const socketPath = socketTarget.socketPath?.replace(/^~(?=\/)/, homedir());
64
+ let factory = createWebSocketTransportFactory((url, options) => {
65
+ const socket = new WebSocket(socketPath && url.startsWith("ws+unix:") ? `ws+unix://${socketPath}:/ws` : url, options?.protocols, {
66
+ headers: options?.headers,
67
+ ...(socketPath ? { socketPath } : {}),
68
+ maxPayload: 100 * 1024 * 1024,
69
+ });
70
+ disposeSocket = () => socket.terminate();
71
+ assertOutboundCapacity = (bytes) => {
72
+ if (socket.bufferedAmount + bytes > 128 * 1024 * 1024) {
73
+ throw Object.assign(new Error("Host socket buffer limit reached"), {
74
+ code: "resource_limit",
75
+ });
76
+ }
77
+ budget.assertCapacity(bytes);
78
+ };
79
+ const pending = new Set();
80
+ const send = socket.send.bind(socket);
81
+ socket.send = (data, ...args) => {
82
+ const bytes = typeof data === "string"
83
+ ? Buffer.byteLength(data)
84
+ : Buffer.byteLength(data);
85
+ assertOutboundCapacity(bytes);
86
+ const release = budget.reserve(bytes);
87
+ pending.add(release);
88
+ const callback = args.find((value) => typeof value === "function");
89
+ const sendOptions = args.find((value) => value !== null && typeof value === "object");
90
+ try {
91
+ send(data, sendOptions ?? {}, (error) => {
92
+ release();
93
+ pending.delete(release);
94
+ callback?.(error);
95
+ });
96
+ }
97
+ catch (error) {
98
+ release();
99
+ pending.delete(release);
100
+ throw error;
101
+ }
102
+ };
103
+ socket.once("close", () => {
104
+ for (const release of pending)
105
+ release();
106
+ pending.clear();
107
+ });
108
+ return socket;
109
+ });
110
+ if (socketTarget.offer)
111
+ factory = createRelayE2eeTransportFactory({
112
+ baseFactory: factory,
113
+ daemonPublicKeyB64: socketTarget.offer.daemonPublicKeyB64,
114
+ ...(securityFloor ? { minimumMode: securityFloor } : {}),
115
+ onSecurityMode: (mode) => {
116
+ if (mode === "protected")
117
+ securityFloor = "protected";
118
+ },
119
+ logger: { warn() { } },
120
+ assertOutboundCapacity: (bytes) => assertOutboundCapacity(bytes),
121
+ });
122
+ transport = factory({ ...attemptOptions, url: socketTarget.url });
123
+ transport.onOpen(() => {
124
+ if (!closed)
125
+ for (const handler of opens)
126
+ handler();
127
+ });
128
+ transport.onClose((event) => {
129
+ closeEvent = event;
130
+ close();
131
+ });
132
+ transport.onError((error) => {
133
+ if (closed)
134
+ return;
135
+ const detail = sshTunnel?.failureDetail() ?? "";
136
+ const failure = /permission denied|authentication failed|host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(detail)
137
+ ? Object.assign(new Error("SSH authentication or host verification failed. Check the SSH configuration on this daemon."), { code: "authentication_failed" })
138
+ : error;
139
+ for (const handler of errors)
140
+ handler(failure);
141
+ close();
142
+ });
143
+ transport.onMessage((data, binary) => {
144
+ if (!closed)
145
+ for (const handler of messages)
146
+ handler(data, binary);
147
+ });
148
+ })().catch(() => {
149
+ for (const handler of errors)
150
+ handler(new Error("Host transport failed"));
151
+ close();
152
+ });
153
+ return {
154
+ send(data) {
155
+ if (closed || !transport)
156
+ throw new Error("Host transport is unavailable");
157
+ transport.send(data);
158
+ },
159
+ close,
160
+ onOpen(handler) {
161
+ opens.add(handler);
162
+ return () => opens.delete(handler);
163
+ },
164
+ onClose(handler) {
165
+ closes.add(handler);
166
+ return () => closes.delete(handler);
167
+ },
168
+ onError(handler) {
169
+ errors.add(handler);
170
+ return () => errors.delete(handler);
171
+ },
172
+ onMessage(handler) {
173
+ messages.add(handler);
174
+ return () => messages.delete(handler);
175
+ },
176
+ };
177
+ };
178
+ }
179
+ export function createHostClient(connection, config, budget) {
180
+ const target = parseHostTarget(connection);
181
+ return new DaemonClient({
182
+ ...config,
183
+ url: "ws://host-connector/ws",
184
+ ...(target.password ? { password: target.password } : {}),
185
+ transportFactory: createHostTransportFactory(connection, budget),
186
+ });
187
+ }
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import { type SshTransportTarget } from "@camerontaylor/paseo-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