@nuvin/session 0.1.0-rc.8 → 0.1.0

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.
Files changed (48) hide show
  1. package/dist/chunk-7HGJO7ZX.js +9 -0
  2. package/dist/chunk-HJLRL64D.js +13 -0
  3. package/dist/{chunk-3IPZO7LP.js → chunk-TUNCGICQ.js} +19 -1
  4. package/dist/{chunk-FR3R66RD.js → chunk-WAMOWOCO.js} +20 -2
  5. package/dist/{chunk-3J44LBXZ.js → chunk-WB4FEWBI.js} +1 -1
  6. package/dist/client/daemon-client.d.ts +50 -0
  7. package/dist/client/daemon-client.d.ts.map +1 -0
  8. package/dist/client/data-client.d.ts +21 -3
  9. package/dist/client/data-client.d.ts.map +1 -1
  10. package/dist/client/directory.d.ts +7 -12
  11. package/dist/client/directory.d.ts.map +1 -1
  12. package/dist/client/http-data-client.d.ts +2 -0
  13. package/dist/client/http-data-client.d.ts.map +1 -1
  14. package/dist/client/index.d.ts +1 -0
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +1300 -979
  17. package/dist/client/session-client.d.ts +19 -6
  18. package/dist/client/session-client.d.ts.map +1 -1
  19. package/dist/client/transport.d.ts +18 -2
  20. package/dist/client/transport.d.ts.map +1 -1
  21. package/dist/client/websocket.d.ts +5 -0
  22. package/dist/client/websocket.d.ts.map +1 -1
  23. package/dist/controller/index.js +129 -40
  24. package/dist/controller/session-controller.d.ts +18 -5
  25. package/dist/controller/session-controller.d.ts.map +1 -1
  26. package/dist/grant/index.d.ts +1 -1
  27. package/dist/grant/index.d.ts.map +1 -1
  28. package/dist/grant/index.js +2 -1
  29. package/dist/protocol/data-rpc.d.ts +70 -3
  30. package/dist/protocol/data-rpc.d.ts.map +1 -1
  31. package/dist/protocol/index.d.ts +1 -0
  32. package/dist/protocol/index.d.ts.map +1 -1
  33. package/dist/protocol/index.js +12 -3
  34. package/dist/protocol/model-identity.d.ts +7 -0
  35. package/dist/protocol/model-identity.d.ts.map +1 -0
  36. package/dist/protocol/types.d.ts +176 -83
  37. package/dist/protocol/types.d.ts.map +1 -1
  38. package/dist/protocol/version.d.ts +1 -1
  39. package/dist/state/approvals.d.ts +6 -3
  40. package/dist/state/approvals.d.ts.map +1 -1
  41. package/dist/state/index.js +5 -1
  42. package/dist/state/session.d.ts.map +1 -1
  43. package/dist/state/tool-preview.d.ts.map +1 -1
  44. package/dist/state/tool-preview.js +50 -1
  45. package/dist/test-utils/fake-relay.d.ts.map +1 -1
  46. package/dist/test-utils/index.js +6 -4
  47. package/package.json +5 -4
  48. package/dist/chunk-NN43LVAS.js +0 -0
@@ -1,935 +1,1096 @@
1
1
  import {
2
2
  createSessionViewState,
3
3
  reduceSessionEvent
4
- } from "../chunk-FR3R66RD.js";
4
+ } from "../chunk-WAMOWOCO.js";
5
5
  import {
6
6
  PROTOCOL_VERSION
7
- } from "../chunk-3J44LBXZ.js";
7
+ } from "../chunk-WB4FEWBI.js";
8
8
 
9
- // src/client/socket.ts
10
- function adaptWhatwgSocket(socket) {
11
- const on = (event, listener) => {
12
- switch (event) {
13
- case "open":
14
- socket.addEventListener("open", () => listener(void 0));
15
- return;
16
- case "message":
17
- socket.addEventListener("message", (messageEvent) => listener(messageEvent.data));
18
- return;
19
- case "close":
20
- socket.addEventListener("close", () => listener(void 0));
21
- return;
22
- case "error":
23
- socket.addEventListener("error", (errorEvent) => listener(errorEvent));
24
- return;
25
- }
26
- };
27
- return {
28
- on,
29
- send: (data) => socket.send(data),
30
- close: () => socket.close()
31
- };
32
- }
33
- function globalWebSocketConstructor() {
34
- const candidate = globalThis.WebSocket;
35
- if (typeof candidate !== "function") {
36
- throw new Error(
37
- "No global WebSocket constructor is available. Pass `socketFactory` (node < 22 callers can wrap the 'ws' package)."
38
- );
9
+ // src/client/endpoint.ts
10
+ var TerminalEndpointError = class extends Error {
11
+ reason;
12
+ constructor(reason, message) {
13
+ super(message ?? reason);
14
+ this.name = "TerminalEndpointError";
15
+ this.reason = reason;
39
16
  }
40
- return candidate;
17
+ };
18
+
19
+ // src/client/http-data-client.ts
20
+ function httpBaseFromWsUrl(wsUrl) {
21
+ const u = new URL(wsUrl);
22
+ const scheme = u.protocol === "wss:" ? "https:" : "http:";
23
+ return `${scheme}//${u.host}`;
41
24
  }
42
- function defaultSocketFactory(url) {
43
- const WebSocketCtor = globalWebSocketConstructor();
44
- return adaptWhatwgSocket(new WebSocketCtor(url));
25
+ function queryString(params) {
26
+ const q = new URLSearchParams();
27
+ for (const [key, value] of Object.entries(params)) {
28
+ if (value !== void 0) q.set(key, String(value));
29
+ }
30
+ const s = q.toString();
31
+ return s ? `?${s}` : "";
45
32
  }
46
-
47
- // src/client/directory.ts
48
- var toText = (data) => {
49
- if (typeof data === "string") return data;
50
- if (data instanceof Buffer) return data.toString("utf8");
51
- if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
52
- if (Array.isArray(data) && data.every((chunk) => chunk instanceof Buffer)) {
53
- return Buffer.concat(data).toString("utf8");
33
+ async function throwFromResponse(response, fallback, terminal, onUnauthorized) {
34
+ const body = await response.json().catch(() => null);
35
+ if (response.status === 401 || body?.code === "unauthorized") {
36
+ onUnauthorized?.();
37
+ throw new TerminalEndpointError(
38
+ "unauthorized",
39
+ typeof body?.message === "string" ? body.message : "Authentication required."
40
+ );
54
41
  }
55
- return null;
56
- };
57
- var parseMessage = (data) => {
58
- const text = toText(data);
59
- if (text === null) return null;
60
- try {
61
- const parsed = JSON.parse(text);
62
- return typeof parsed === "object" && parsed !== null && typeof parsed.type === "string" ? parsed : null;
63
- } catch {
64
- return null;
42
+ const mapped = terminal?.[response.status];
43
+ if (mapped) {
44
+ throw new TerminalEndpointError(
45
+ mapped.reason,
46
+ typeof body?.message === "string" ? body.message : mapped.message
47
+ );
65
48
  }
66
- };
67
- function createServerDirectory(options) {
68
- const socketFactory = options.socketFactory ?? defaultSocketFactory;
69
- const connectTimeoutMs = options.connectTimeoutMs ?? 5e3;
70
- const listeners = /* @__PURE__ */ new Set();
71
- const links = /* @__PURE__ */ new Map();
72
- let closed = false;
73
- let view = { servers: [] };
74
- const rebuildView = () => {
75
- view = {
76
- servers: Array.from(links.values(), (link) => ({
77
- name: link.config.name,
78
- url: link.resolvedUrl ?? link.config.url ?? "",
79
- status: link.status
80
- }))
81
- };
82
- for (const listener of listeners) {
83
- try {
84
- listener();
85
- } catch {
86
- }
87
- }
88
- };
89
- const clearTimer = (link) => {
90
- if (link.timer !== null) {
91
- clearTimeout(link.timer);
92
- link.timer = null;
93
- }
94
- };
95
- const rejectPending = (link, message) => {
96
- const error = new Error(message);
97
- for (const entry of link.pending.values()) {
98
- if (entry.kind !== "list") entry.reject(error);
99
- }
100
- link.pending.clear();
101
- };
102
- const markOffline = (link, message) => {
103
- clearTimer(link);
104
- link.ready = false;
105
- const current = link.socket;
106
- link.socket = null;
107
- current?.close();
108
- rejectPending(link, message);
109
- link.status = { state: "offline", error: message };
110
- rebuildView();
49
+ throw new Error(
50
+ typeof body?.message === "string" ? body.message : `${fallback} (HTTP ${response.status}).`
51
+ );
52
+ }
53
+ function createHttpDataClient(options = {}) {
54
+ const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
55
+ const baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
56
+ const rows = /* @__PURE__ */ new Map();
57
+ const resolveToken = async (id) => {
58
+ const endpoint = rows.get(id)?.endpoint;
59
+ if (endpoint) return endpoint.token;
60
+ return options.getToken ? await options.getToken() : null;
111
61
  };
112
- const sendList = (link) => {
113
- if (!link.ready || link.socket === null) return;
114
- link.commandCounter += 1;
115
- const id = `dir-${link.commandCounter}`;
116
- link.pending.set(id, { kind: "list" });
117
- link.socket.send(JSON.stringify({ id, type: "list-sessions" }));
62
+ const resolveBase = (id) => {
63
+ const endpoint = rows.get(id)?.endpoint;
64
+ return endpoint ? httpBaseFromWsUrl(endpoint.wsUrl) : baseUrl;
118
65
  };
119
- const handleMessage = (link, message) => {
120
- if (message.type === "rejected") {
121
- markOffline(
122
- link,
123
- message.reason === "auth" ? `Token rejected by "${link.config.name}" \u2014 check the configured token.` : `Protocol version mismatch with "${link.config.name}" \u2014 upgrade the daemon or this client.`
124
- );
125
- return;
126
- }
127
- if (message.type === "welcome") {
128
- link.ready = true;
129
- clearTimer(link);
130
- rebuildView();
131
- sendList(link);
132
- return;
133
- }
134
- if (message.type === "session-list") {
135
- const entry = link.pending.get(message.id);
136
- if (entry?.kind !== "list") return;
137
- link.pending.delete(message.id);
138
- link.status = { state: "online", sessions: message.sessions };
139
- rebuildView();
140
- return;
141
- }
142
- if (message.type === "session-created") {
143
- const entry = link.pending.get(message.id);
144
- if (entry?.kind !== "create" && entry?.kind !== "resume") return;
145
- link.pending.delete(message.id);
146
- entry.resolve(message.session);
147
- if (link.status.state === "online") {
148
- const sessions = link.status.sessions.some((existing) => existing.id === message.session.id) ? link.status.sessions : [...link.status.sessions, message.session];
149
- link.status = { state: "online", sessions };
150
- rebuildView();
66
+ const daemonFetch = async (id, suffix, init = {}) => {
67
+ const token = await resolveToken(id);
68
+ const response = await fetchImpl(
69
+ `${resolveBase(id)}/api/daemons/${encodeURIComponent(id)}${suffix}`,
70
+ {
71
+ ...init,
72
+ cache: "no-store",
73
+ headers: {
74
+ ...init.headers ?? {},
75
+ ...token !== null ? { Authorization: `Bearer ${token}` } : {}
76
+ }
151
77
  }
152
- return;
78
+ );
79
+ if (response.status === 401) {
80
+ await throwFromResponse(
81
+ response,
82
+ "Authentication required",
83
+ void 0,
84
+ options.onUnauthorized
85
+ );
153
86
  }
154
- if (message.type === "ack") {
155
- const entry = link.pending.get(message.id);
156
- if (!entry || entry.kind === "list") return;
157
- link.pending.delete(message.id);
158
- if (!message.ok) {
159
- entry.reject(new Error(message.error?.message ?? "Command failed."));
160
- return;
161
- }
162
- if (entry.kind === "kill") {
163
- entry.resolve();
164
- sendList(link);
165
- return;
87
+ return response;
88
+ };
89
+ return {
90
+ async listDaemons() {
91
+ const token = options.getToken ? await options.getToken() : null;
92
+ const response = await fetchImpl(`${baseUrl}/api/daemons`, {
93
+ cache: "no-store",
94
+ headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
95
+ });
96
+ if (response.status === 401) {
97
+ options.onUnauthorized?.();
98
+ throw new TerminalEndpointError("unauthorized", "Signed out \u2014 authentication required.");
166
99
  }
167
- if (entry.kind === "complete") {
168
- entry.resolve(
169
- Array.isArray(message.result) ? message.result.filter((item) => typeof item === "string") : []
170
- );
171
- return;
100
+ if (!response.ok) await throwFromResponse(response, "Failed to fetch the daemon list");
101
+ const body = await response.json();
102
+ rows.clear();
103
+ for (const row of body.daemons) rows.set(row.id, row);
104
+ return {
105
+ daemons: body.daemons,
106
+ ...body.defaultSessionId !== void 0 ? { defaultSessionId: body.defaultSessionId } : {}
107
+ };
108
+ },
109
+ async acquireEndpoint(daemonId) {
110
+ const endpoint = rows.get(daemonId)?.endpoint;
111
+ if (endpoint) {
112
+ return { url: endpoint.wsUrl, credential: { token: endpoint.token } };
172
113
  }
173
- if (entry.kind === "list-profiles") {
174
- entry.resolve(Array.isArray(message.result) ? message.result : []);
175
- return;
114
+ const request = (token) => fetchImpl(`${baseUrl}/api/daemons/${encodeURIComponent(daemonId)}/connect`, {
115
+ method: "POST",
116
+ cache: "no-store",
117
+ headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
118
+ });
119
+ const throwUnauthorized = () => {
120
+ options.onUnauthorized?.();
121
+ throw new TerminalEndpointError("unauthorized", "Signed out \u2014 authentication required.");
122
+ };
123
+ const failedToken = options.getToken ? await options.getToken() : null;
124
+ let response = await request(failedToken);
125
+ if (response.status === 401 && options.refreshAccessToken) {
126
+ let retryToken = null;
127
+ try {
128
+ const currentToken = options.getToken ? await options.getToken() : null;
129
+ retryToken = currentToken && currentToken !== failedToken ? currentToken : null;
130
+ if (!retryToken) retryToken = await options.refreshAccessToken();
131
+ } catch {
132
+ throwUnauthorized();
133
+ }
134
+ if (!retryToken) {
135
+ throwUnauthorized();
136
+ }
137
+ response = await request(retryToken);
176
138
  }
177
- if (entry.kind === "list-providers") {
178
- entry.resolve(Array.isArray(message.result) ? message.result : []);
179
- return;
139
+ if (response.status === 401) {
140
+ throwUnauthorized();
180
141
  }
181
- if (entry.kind === "provider-mutation") {
182
- entry.resolve();
183
- return;
142
+ if (response.status === 404) {
143
+ throw new TerminalEndpointError(
144
+ "daemon-revoked",
145
+ `Daemon "${daemonId}" is no longer registered.`
146
+ );
184
147
  }
185
- entry.reject(new Error("Unexpected ack for create-session / resume-session."));
186
- return;
187
- }
188
- };
189
- const openLinkSocket = (link, endpoint) => {
190
- link.resolvedUrl = endpoint.url;
191
- const socket = (link.config.socketFactory ?? socketFactory)(endpoint.url);
192
- link.socket = socket;
193
- socket.on("open", () => {
194
- if (closed || link.socket !== socket) return;
195
- socket.send(
196
- JSON.stringify({
197
- type: "hello",
198
- protocolVersion: PROTOCOL_VERSION,
199
- ...endpoint.credential
148
+ if (!response.ok)
149
+ await throwFromResponse(
150
+ response,
151
+ response.status === 409 ? `Daemon "${daemonId}" is offline` : `Connect failed for "${daemonId}"`
152
+ );
153
+ const body = await response.json();
154
+ return { url: body.wsUrl, credential: { grant: body.grant } };
155
+ },
156
+ async fetchTranscript(daemonId, historyId, opts) {
157
+ const suffix = `/transcript/${encodeURIComponent(historyId)}${queryString({
158
+ workspace: opts?.workspace,
159
+ profile: opts?.profile
160
+ })}`;
161
+ const response = await daemonFetch(daemonId, suffix);
162
+ if (response.status === 404) {
163
+ throw new TerminalEndpointError(
164
+ "transcript-not-found",
165
+ "This session's transcript is no longer available."
166
+ );
167
+ }
168
+ if (!response.ok) await throwFromResponse(response, "Failed to load the transcript");
169
+ return await response.json();
170
+ },
171
+ async fetchHistory(daemonId, opts) {
172
+ const suffix = `/history${queryString({
173
+ profile: opts?.profile,
174
+ cwd: opts?.cwd,
175
+ limit: opts?.limit,
176
+ cursor: opts?.cursor
177
+ })}`;
178
+ const response = await daemonFetch(daemonId, suffix);
179
+ if (!response.ok)
180
+ await throwFromResponse(response, "Failed to load history", {
181
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
182
+ });
183
+ return await response.json();
184
+ },
185
+ async listWorkspaces(daemonId, opts) {
186
+ const suffix = `/workspaces${queryString({ profile: opts?.profile })}`;
187
+ const response = await daemonFetch(daemonId, suffix);
188
+ if (!response.ok)
189
+ await throwFromResponse(response, "Failed to load workspaces", {
190
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
191
+ });
192
+ return await response.json();
193
+ },
194
+ async createWorkspace(daemonId, opts) {
195
+ const response = await daemonFetch(daemonId, "/workspaces", {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify({
199
+ name: opts.name,
200
+ ...opts.root !== void 0 ? { root: opts.root } : {},
201
+ ...opts.profile !== void 0 ? { profile: opts.profile } : {}
200
202
  })
201
- );
202
- });
203
- socket.on("message", (data) => {
204
- if (closed || link.socket !== socket) return;
205
- const message = parseMessage(data);
206
- if (message) handleMessage(link, message);
207
- });
208
- socket.on("error", () => {
209
- });
210
- socket.on("close", () => {
211
- if (link.socket !== socket) return;
212
- link.socket = null;
213
- if (closed) return;
214
- if (link.status.state !== "offline") {
215
- markOffline(link, `Connection to "${link.config.name}" closed.`);
203
+ });
204
+ if (!response.ok) await throwFromResponse(response, "Failed to create the workspace");
205
+ return await response.json();
206
+ },
207
+ async deleteSession(daemonId, historyId, opts) {
208
+ const suffix = `/sessions/${encodeURIComponent(historyId)}${queryString({
209
+ workspace: opts?.workspace,
210
+ profile: opts?.profile
211
+ })}`;
212
+ const response = await daemonFetch(daemonId, suffix, { method: "DELETE" });
213
+ if (response.status === 404) {
214
+ throw new TerminalEndpointError("session-not-found", "This session no longer exists.");
216
215
  }
217
- });
218
- };
219
- const connect = (link) => {
220
- if (closed) return;
221
- link.ready = false;
222
- link.status = { state: "connecting" };
223
- clearTimer(link);
224
- link.timer = setTimeout(() => {
225
- link.timer = null;
226
- if (!link.ready) {
227
- markOffline(link, `Timed out connecting to "${link.config.name}".`);
216
+ if (!response.ok) await throwFromResponse(response, "Failed to delete the session");
217
+ },
218
+ async listProfiles(daemonId) {
219
+ const response = await daemonFetch(daemonId, "/profiles");
220
+ if (!response.ok) {
221
+ await throwFromResponse(response, "Failed to load profiles", {
222
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
223
+ });
228
224
  }
229
- }, connectTimeoutMs);
230
- const getEndpoint = link.config.getEndpoint;
231
- if (!getEndpoint) {
232
- openLinkSocket(link, {
233
- url: link.config.url,
234
- credential: { token: link.config.token }
235
- });
236
- rebuildView();
237
- return;
238
- }
239
- void getEndpoint().then(
240
- (endpoint) => {
241
- if (closed || link.status.state !== "connecting" || link.socket !== null) return;
242
- openLinkSocket(link, endpoint);
243
- rebuildView();
244
- },
245
- (error) => {
246
- if (closed || link.status.state !== "connecting") return;
247
- markOffline(
248
- link,
249
- `Endpoint for "${link.config.name}" unavailable: ${error instanceof Error ? error.message : String(error)}`
250
- );
225
+ return await response.json();
226
+ },
227
+ async listProviders(daemonId, opts) {
228
+ const suffix = `/providers${queryString({ profile: opts?.profile })}`;
229
+ const response = await daemonFetch(daemonId, suffix);
230
+ if (!response.ok) {
231
+ await throwFromResponse(response, "Failed to load providers", {
232
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
233
+ });
251
234
  }
252
- );
253
- rebuildView();
254
- };
255
- const linkFor = (server) => {
256
- const link = links.get(server);
257
- if (!link) return new Error(`Unknown server "${server}".`);
258
- if (closed || !link.ready || link.socket === null) {
259
- return new Error(`Server "${server}" is not connected.`);
260
- }
261
- return link;
262
- };
263
- for (const config of options.servers) {
264
- if (!config.getEndpoint && (config.url === void 0 || config.token === void 0)) {
265
- throw new Error(`Server "${config.name}" needs either url+token or a getEndpoint provider.`);
266
- }
267
- links.set(config.name, {
268
- config,
269
- status: { state: "connecting" },
270
- socket: null,
271
- ready: false,
272
- pending: /* @__PURE__ */ new Map(),
273
- timer: null,
274
- commandCounter: 0
275
- });
276
- }
277
- rebuildView();
278
- for (const link of links.values()) {
279
- connect(link);
280
- }
281
- return {
282
- getView() {
283
- return view;
235
+ return await response.json();
284
236
  },
285
- subscribe(listener) {
286
- listeners.add(listener);
287
- return () => {
288
- listeners.delete(listener);
289
- };
237
+ async addProvider(daemonId, input) {
238
+ const response = await daemonFetch(daemonId, "/providers", {
239
+ method: "POST",
240
+ headers: { "Content-Type": "application/json" },
241
+ body: JSON.stringify(input)
242
+ });
243
+ if (!response.ok) await throwFromResponse(response, "Failed to add provider");
244
+ return await response.json();
290
245
  },
291
- refresh() {
292
- if (closed) return;
293
- for (const link of links.values()) {
294
- if (link.status.state === "offline") {
295
- connect(link);
296
- } else if (link.ready) {
297
- sendList(link);
246
+ async removeProvider(daemonId, providerId, opts) {
247
+ const response = await daemonFetch(daemonId, `/providers/${encodeURIComponent(providerId)}`, {
248
+ method: "DELETE",
249
+ headers: { "Content-Type": "application/json" },
250
+ body: JSON.stringify(opts?.profile !== void 0 ? { profile: opts.profile } : {})
251
+ });
252
+ if (!response.ok) await throwFromResponse(response, "Failed to remove provider");
253
+ return await response.json();
254
+ },
255
+ async setDefaultProvider(daemonId, providerId, opts) {
256
+ const response = await daemonFetch(daemonId, "/providers/default", {
257
+ method: "PUT",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({
260
+ providerId,
261
+ ...opts?.profile !== void 0 ? { profile: opts.profile } : {}
262
+ })
263
+ });
264
+ if (!response.ok) await throwFromResponse(response, "Failed to set default provider");
265
+ return await response.json();
266
+ },
267
+ async setProviderAuth(daemonId, providerId, credential, opts) {
268
+ const response = await daemonFetch(
269
+ daemonId,
270
+ `/providers/${encodeURIComponent(providerId)}/credential`,
271
+ {
272
+ method: "PUT",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify({
275
+ credential,
276
+ ...opts?.profile !== void 0 ? { profile: opts.profile } : {}
277
+ })
298
278
  }
299
- }
279
+ );
280
+ if (!response.ok) await throwFromResponse(response, "Failed to update provider auth");
281
+ return await response.json();
300
282
  },
301
- setServers(configs) {
302
- if (closed) return;
303
- for (const config of configs) {
304
- if (!links.has(config.name) && !config.getEndpoint) {
305
- if (config.url === void 0 || config.token === void 0) {
306
- throw new Error(
307
- `Server "${config.name}" needs either url+token or a getEndpoint provider.`
308
- );
309
- }
283
+ async clearProviderAuth(daemonId, providerId, opts) {
284
+ const response = await daemonFetch(
285
+ daemonId,
286
+ `/providers/${encodeURIComponent(providerId)}/credential`,
287
+ {
288
+ method: "DELETE",
289
+ headers: { "Content-Type": "application/json" },
290
+ body: JSON.stringify(opts?.profile !== void 0 ? { profile: opts.profile } : {})
291
+ }
292
+ );
293
+ if (!response.ok) await throwFromResponse(response, "Failed to clear provider auth");
294
+ return await response.json();
295
+ },
296
+ async updateWorkspace(daemonId, name, patch) {
297
+ const response = await daemonFetch(daemonId, `/workspaces/${encodeURIComponent(name)}`, {
298
+ method: "PATCH",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify(patch)
301
+ });
302
+ if (!response.ok) await throwFromResponse(response, "Failed to update workspace");
303
+ return await response.json();
304
+ }
305
+ };
306
+ }
307
+
308
+ // src/client/session-client.ts
309
+ var EMPTY_MODEL_COMPLETION = {
310
+ providers: [],
311
+ perProvider: {},
312
+ recentModels: []
313
+ };
314
+ function toModelCompletion(result) {
315
+ if (typeof result !== "object" || result === null || Array.isArray(result)) {
316
+ return EMPTY_MODEL_COMPLETION;
317
+ }
318
+ const record = result;
319
+ const providers = Array.isArray(record.providers) ? record.providers.filter((p) => typeof p === "string") : [];
320
+ const perProvider = {};
321
+ if (typeof record.perProvider === "object" && record.perProvider !== null) {
322
+ for (const [provider, models] of Object.entries(
323
+ record.perProvider
324
+ )) {
325
+ if (!Array.isArray(models)) continue;
326
+ perProvider[provider] = models.flatMap((entry) => {
327
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
328
+ const id = entry.id;
329
+ if (typeof id !== "string") return [];
330
+ const name = entry.name;
331
+ return [typeof name === "string" ? { id, name } : { id }];
332
+ });
333
+ }
334
+ }
335
+ const recentModels = Array.isArray(record.recentModels) ? record.recentModels.flatMap((entry) => {
336
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
337
+ const provider = entry.provider;
338
+ const model = entry.model;
339
+ return typeof provider === "string" && typeof model === "string" ? [{ provider, model }] : [];
340
+ }) : [];
341
+ return { providers, perProvider, recentModels };
342
+ }
343
+ function createSessionClient(transport) {
344
+ let state = createSessionViewState();
345
+ let seq = 0;
346
+ let closed = false;
347
+ let notifyScheduled = false;
348
+ let commandCounter = 0;
349
+ let serverCommandList = [];
350
+ const stateListeners = /* @__PURE__ */ new Set();
351
+ const eventListeners = /* @__PURE__ */ new Set();
352
+ const pending = /* @__PURE__ */ new Map();
353
+ const rejectAllPending = (message) => {
354
+ if (pending.size === 0) return;
355
+ const error = new Error(message);
356
+ for (const { reject } of pending.values()) {
357
+ reject(error);
358
+ }
359
+ pending.clear();
360
+ };
361
+ const scheduleNotify = () => {
362
+ if (notifyScheduled) return;
363
+ notifyScheduled = true;
364
+ queueMicrotask(() => {
365
+ notifyScheduled = false;
366
+ for (const listener of stateListeners) {
367
+ try {
368
+ listener();
369
+ } catch {
310
370
  }
311
371
  }
312
- const nextNames = new Set(configs.map((config) => config.name));
313
- for (const [name, link] of links) {
314
- if (nextNames.has(name)) continue;
315
- clearTimer(link);
316
- rejectPending(link, `Server "${name}" was removed from the directory.`);
317
- const socket = link.socket;
318
- link.socket = null;
319
- socket?.close();
320
- links.delete(name);
372
+ });
373
+ };
374
+ const unsubscribe = transport.onFrame((frame) => {
375
+ if (frame.type === "snapshot") {
376
+ state = frame.snapshot.state;
377
+ seq = frame.snapshot.seq;
378
+ serverCommandList = frame.snapshot.serverCommands;
379
+ scheduleNotify();
380
+ return;
381
+ }
382
+ if (frame.type === "ack") {
383
+ const entry = pending.get(frame.id);
384
+ if (!entry) return;
385
+ pending.delete(frame.id);
386
+ if (frame.ok) {
387
+ entry.resolve(frame.result);
388
+ } else {
389
+ entry.reject(new Error(frame.error?.message ?? "Command failed."));
321
390
  }
322
- const added = [];
323
- for (const config of configs) {
324
- const existing = links.get(config.name);
325
- if (existing) {
326
- existing.config = config;
327
- continue;
328
- }
329
- const link = {
330
- config,
331
- status: { state: "connecting" },
332
- socket: null,
333
- ready: false,
334
- pending: /* @__PURE__ */ new Map(),
335
- timer: null,
336
- commandCounter: 0
337
- };
338
- links.set(config.name, link);
339
- added.push(link);
391
+ return;
392
+ }
393
+ state = reduceSessionEvent(state, frame);
394
+ seq = frame.seq;
395
+ for (const listener of eventListeners) {
396
+ try {
397
+ listener(frame);
398
+ } catch {
340
399
  }
341
- rebuildView();
342
- for (const link of added) {
343
- connect(link);
400
+ }
401
+ scheduleNotify();
402
+ });
403
+ const unsubscribeUndeliverable = transport.onUndeliverable?.(() => {
404
+ rejectAllPending("Command not delivered: connection lost before acknowledgment.");
405
+ });
406
+ const sendCommand = (build) => {
407
+ if (closed) {
408
+ return Promise.reject(new Error("SessionClient is closed."));
409
+ }
410
+ commandCounter += 1;
411
+ const id = `cmd-${commandCounter}`;
412
+ return new Promise((resolve, reject) => {
413
+ pending.set(id, { resolve, reject });
414
+ const delivered = transport.send(build(id));
415
+ if (delivered === false) {
416
+ pending.delete(id);
417
+ reject(new Error("Command not delivered: transport is not attached."));
344
418
  }
419
+ });
420
+ };
421
+ return {
422
+ getState() {
423
+ return state;
345
424
  },
346
- createSession(server, createOptions) {
347
- return new Promise((resolve, reject) => {
348
- const link = linkFor(server);
349
- if (link instanceof Error) {
350
- reject(link);
351
- return;
352
- }
353
- link.commandCounter += 1;
354
- const id = `dir-${link.commandCounter}`;
355
- link.pending.set(id, { kind: "create", resolve, reject });
356
- link.socket?.send(
357
- JSON.stringify({
358
- id,
359
- type: "create-session",
360
- cwd: createOptions.cwd,
361
- ...createOptions.name !== void 0 ? { name: createOptions.name } : {},
362
- ...createOptions.workspace !== void 0 ? { workspace: createOptions.workspace } : {},
363
- ...createOptions.profile !== void 0 ? { profile: createOptions.profile } : {}
364
- })
365
- );
366
- });
425
+ getSeq() {
426
+ return seq;
367
427
  },
368
- resumeSession(server, historyId, resumeOptions) {
369
- return new Promise((resolve, reject) => {
370
- const link = linkFor(server);
371
- if (link instanceof Error) {
372
- reject(link);
373
- return;
374
- }
375
- link.commandCounter += 1;
376
- const id = `dir-${link.commandCounter}`;
377
- link.pending.set(id, { kind: "resume", resolve, reject });
378
- link.socket?.send(
379
- JSON.stringify({
380
- id,
381
- type: "resume-session",
382
- historyId,
383
- cwd: resumeOptions.cwd,
384
- ...resumeOptions.name !== void 0 ? { name: resumeOptions.name } : {},
385
- ...resumeOptions.workspace !== void 0 ? { workspace: resumeOptions.workspace } : {},
386
- ...resumeOptions.profile !== void 0 ? { profile: resumeOptions.profile } : {}
387
- })
388
- );
389
- });
428
+ subscribe(listener) {
429
+ stateListeners.add(listener);
430
+ return () => stateListeners.delete(listener);
390
431
  },
391
- killSession(server, sessionId, options2) {
392
- return new Promise((resolve, reject) => {
393
- const link = linkFor(server);
394
- if (link instanceof Error) {
395
- reject(link);
396
- return;
397
- }
398
- link.commandCounter += 1;
399
- const id = `dir-${link.commandCounter}`;
400
- link.pending.set(id, { kind: "kill", resolve, reject });
401
- link.socket?.send(
402
- JSON.stringify({ id, type: "kill-session", sessionId, delete: options2?.delete })
403
- );
404
- });
432
+ onEvent(listener) {
433
+ eventListeners.add(listener);
434
+ return () => eventListeners.delete(listener);
405
435
  },
406
- completePath(server, prefix) {
407
- return new Promise((resolve, reject) => {
408
- const link = linkFor(server);
409
- if (link instanceof Error) {
410
- reject(link);
411
- return;
412
- }
413
- link.commandCounter += 1;
414
- const id = `dir-${link.commandCounter}`;
415
- link.pending.set(id, { kind: "complete", resolve, reject });
416
- link.socket?.send(JSON.stringify({ id, type: "complete-path", prefix }));
436
+ submit(input, opts) {
437
+ return sendCommand((id) => ({
438
+ id,
439
+ type: "submit",
440
+ input,
441
+ displayText: opts.displayText,
442
+ ...opts.attachmentLabels?.length ? { attachmentLabels: opts.attachmentLabels } : {},
443
+ ...opts.turnId !== void 0 ? { turnId: opts.turnId } : {}
444
+ })).then(() => {
417
445
  });
418
446
  },
419
- listProfiles(server) {
420
- return new Promise((resolve, reject) => {
421
- const link = linkFor(server);
422
- if (link instanceof Error) {
423
- reject(link);
424
- return;
425
- }
426
- link.commandCounter += 1;
427
- const id = `dir-${link.commandCounter}`;
428
- link.pending.set(id, { kind: "list-profiles", resolve, reject });
429
- link.socket?.send(JSON.stringify({ id, type: "list-profiles" }));
447
+ decideApproval(toolCallId, decision, comment, grantDir) {
448
+ return sendCommand((id) => ({
449
+ id,
450
+ type: "decide-approval",
451
+ toolCallId,
452
+ decision,
453
+ ...comment !== void 0 ? { comment } : {},
454
+ ...grantDir !== void 0 ? { grantDir } : {}
455
+ })).then(() => {
430
456
  });
431
457
  },
432
- listProviders(server) {
433
- return new Promise((resolve, reject) => {
434
- const link = linkFor(server);
435
- if (link instanceof Error) {
436
- reject(link);
437
- return;
458
+ answerQuestion(questionId, answers) {
459
+ return sendCommand((id) => ({ id, type: "answer-question", questionId, answers })).then(
460
+ () => {
438
461
  }
439
- link.commandCounter += 1;
440
- const id = `dir-${link.commandCounter}`;
441
- link.pending.set(id, { kind: "list-providers", resolve, reject });
442
- link.socket?.send(JSON.stringify({ id, type: "list-providers" }));
462
+ );
463
+ },
464
+ abort() {
465
+ return sendCommand((id) => ({ id, type: "abort" })).then(() => {
443
466
  });
444
467
  },
445
- setDefaultProvider(server, providerId) {
446
- return new Promise((resolve, reject) => {
447
- const link = linkFor(server);
448
- if (link instanceof Error) {
449
- reject(link);
450
- return;
451
- }
452
- link.commandCounter += 1;
453
- const id = `dir-${link.commandCounter}`;
454
- link.pending.set(id, { kind: "provider-mutation", resolve, reject });
455
- link.socket?.send(JSON.stringify({ id, type: "set-default-provider", providerId }));
468
+ dequeueMessage(queuedId) {
469
+ return sendCommand((id) => ({ id, type: "dequeue", queuedId })).then(() => {
456
470
  });
457
471
  },
458
- setProviderAuth(server, providerId, credential) {
459
- return new Promise((resolve, reject) => {
460
- const link = linkFor(server);
461
- if (link instanceof Error) {
462
- reject(link);
463
- return;
464
- }
465
- link.commandCounter += 1;
466
- const id = `dir-${link.commandCounter}`;
467
- link.pending.set(id, { kind: "provider-mutation", resolve, reject });
468
- link.socket?.send(
469
- JSON.stringify({ id, type: "set-provider-auth", providerId, credential })
470
- );
472
+ runSlashCommand(raw) {
473
+ return sendCommand((id) => ({ id, type: "slash-command", raw })).then(() => {
471
474
  });
472
475
  },
473
- clearProviderAuth(server, providerId) {
474
- return new Promise((resolve, reject) => {
475
- const link = linkFor(server);
476
- if (link instanceof Error) {
477
- reject(link);
478
- return;
479
- }
480
- link.commandCounter += 1;
481
- const id = `dir-${link.commandCounter}`;
482
- link.pending.set(id, { kind: "provider-mutation", resolve, reject });
483
- link.socket?.send(JSON.stringify({ id, type: "clear-provider-auth", providerId }));
476
+ loadHistory(historyId) {
477
+ return sendCommand((id) => ({ id, type: "load-history", historyId })).then(() => {
484
478
  });
485
479
  },
486
- close() {
487
- if (closed) return;
488
- closed = true;
489
- for (const link of links.values()) {
490
- clearTimer(link);
491
- rejectPending(link, "ServerDirectory closed.");
492
- const socket = link.socket;
493
- link.socket = null;
494
- socket?.close();
495
- }
496
- listeners.clear();
497
- }
498
- };
499
- }
500
-
501
- // src/client/endpoint.ts
502
- var TerminalEndpointError = class extends Error {
503
- reason;
504
- constructor(reason, message) {
505
- super(message ?? reason);
506
- this.name = "TerminalEndpointError";
507
- this.reason = reason;
508
- }
509
- };
510
-
511
- // src/client/http-data-client.ts
512
- function httpBaseFromWsUrl(wsUrl) {
513
- const u = new URL(wsUrl);
514
- const scheme = u.protocol === "wss:" ? "https:" : "http:";
515
- return `${scheme}//${u.host}`;
516
- }
517
- function queryString(params) {
518
- const q = new URLSearchParams();
519
- for (const [key, value] of Object.entries(params)) {
520
- if (value !== void 0) q.set(key, String(value));
521
- }
522
- const s = q.toString();
523
- return s ? `?${s}` : "";
524
- }
525
- async function throwFromResponse(response, fallback, terminal) {
526
- const body = await response.json().catch(() => null);
527
- const mapped = terminal?.[response.status];
528
- if (mapped) throw new TerminalEndpointError(mapped.reason, body?.message ?? mapped.message);
529
- throw new Error(body?.message ?? `${fallback} (HTTP ${response.status}).`);
530
- }
531
- function createHttpDataClient(options = {}) {
532
- const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
533
- const baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
534
- const rows = /* @__PURE__ */ new Map();
535
- const resolveToken = async (id) => {
536
- const endpoint = rows.get(id)?.endpoint;
537
- if (endpoint) return endpoint.token;
538
- return options.getToken ? await options.getToken() : null;
539
- };
540
- const resolveBase = (id) => {
541
- const endpoint = rows.get(id)?.endpoint;
542
- return endpoint ? httpBaseFromWsUrl(endpoint.wsUrl) : baseUrl;
543
- };
544
- const daemonFetch = async (id, suffix, init = {}) => {
545
- const token = await resolveToken(id);
546
- const response = await fetchImpl(
547
- `${resolveBase(id)}/api/daemons/${encodeURIComponent(id)}${suffix}`,
548
- {
549
- ...init,
550
- cache: "no-store",
551
- headers: {
552
- ...init.headers ?? {},
553
- ...token !== null ? { Authorization: `Bearer ${token}` } : {}
554
- }
555
- }
556
- );
557
- if (response.status === 401) {
558
- options.onUnauthorized?.();
559
- throw new TerminalEndpointError("unauthorized", "Signed out \u2014 authentication required.");
560
- }
561
- return response;
562
- };
563
- return {
564
- async listDaemons() {
565
- const token = options.getToken ? await options.getToken() : null;
566
- const response = await fetchImpl(`${baseUrl}/api/daemons`, {
567
- cache: "no-store",
568
- headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
480
+ cancelAuthFlow() {
481
+ return sendCommand((id) => ({ id, type: "cancel-auth-flow" })).then(() => {
569
482
  });
570
- if (response.status === 401) {
571
- options.onUnauthorized?.();
572
- throw new TerminalEndpointError("unauthorized", "Signed out \u2014 authentication required.");
573
- }
574
- if (!response.ok) await throwFromResponse(response, "Failed to fetch the daemon list");
575
- const body = await response.json();
576
- rows.clear();
577
- for (const row of body.daemons) rows.set(row.id, row);
578
- return {
579
- daemons: body.daemons,
580
- ...body.defaultSessionId !== void 0 ? { defaultSessionId: body.defaultSessionId } : {}
581
- };
582
483
  },
583
- async acquireEndpoint(daemonId) {
584
- const endpoint = rows.get(daemonId)?.endpoint;
585
- if (endpoint) {
586
- return { url: endpoint.wsUrl, credential: { token: endpoint.token } };
587
- }
588
- const token = options.getToken ? await options.getToken() : null;
589
- const response = await fetchImpl(
590
- `${baseUrl}/api/daemons/${encodeURIComponent(daemonId)}/connect`,
591
- {
592
- method: "POST",
593
- cache: "no-store",
594
- headers: token !== null ? { Authorization: `Bearer ${token}` } : {}
595
- }
484
+ mcpAction(action, serverName) {
485
+ return sendCommand((id) => ({ id, type: "mcp-action", action, serverName })).then(
486
+ (result) => result
596
487
  );
597
- if (response.status === 401) {
598
- options.onUnauthorized?.();
599
- throw new TerminalEndpointError("unauthorized", "Signed out \u2014 authentication required.");
600
- }
601
- if (response.status === 404) {
602
- throw new TerminalEndpointError(
603
- "daemon-revoked",
604
- `Daemon "${daemonId}" is no longer registered.`
605
- );
606
- }
607
- if (!response.ok)
608
- await throwFromResponse(
609
- response,
610
- response.status === 409 ? `Daemon "${daemonId}" is offline` : `Connect failed for "${daemonId}"`
611
- );
612
- const body = await response.json();
613
- return { url: body.wsUrl, credential: { grant: body.grant } };
614
488
  },
615
- async fetchTranscript(daemonId, historyId, opts) {
616
- const suffix = `/transcript/${encodeURIComponent(historyId)}${queryString({
617
- workspace: opts?.workspace,
618
- profile: opts?.profile
619
- })}`;
620
- const response = await daemonFetch(daemonId, suffix);
621
- if (response.status === 404) {
622
- throw new TerminalEndpointError(
623
- "transcript-not-found",
624
- "This session's transcript is no longer available."
625
- );
626
- }
627
- if (!response.ok) await throwFromResponse(response, "Failed to load the transcript");
628
- return await response.json();
489
+ serverCommands() {
490
+ return serverCommandList;
491
+ },
492
+ completePath(prefix) {
493
+ return sendCommand((id) => ({ id, type: "complete-path", prefix })).then(
494
+ (result) => Array.isArray(result) ? result.filter((entry) => typeof entry === "string") : []
495
+ );
496
+ },
497
+ completeModels() {
498
+ return sendCommand((id) => ({ id, type: "complete-models" })).then(toModelCompletion);
499
+ },
500
+ setConfig(patches) {
501
+ return sendCommand((id) => ({ id, type: "set-config", patches })).then(
502
+ (result) => result
503
+ );
504
+ },
505
+ listSkills() {
506
+ return sendCommand((id) => ({ id, type: "list-skills" })).then(
507
+ (result) => Array.isArray(result) ? result : []
508
+ );
509
+ },
510
+ getSkill(skillId) {
511
+ return sendCommand((id) => ({ id, type: "get-skill", skillId })).then(
512
+ (result) => result
513
+ );
514
+ },
515
+ listAgents() {
516
+ return sendCommand((id) => ({ id, type: "list-agents" })).then(
517
+ (result) => Array.isArray(result) ? result : []
518
+ );
519
+ },
520
+ getAgent(agentId) {
521
+ return sendCommand((id) => ({ id, type: "get-agent", agentId })).then(
522
+ (result) => result
523
+ );
524
+ },
525
+ getCommand(name) {
526
+ return sendCommand((id) => ({ id, type: "get-command", name })).then(
527
+ (result) => result
528
+ );
529
+ },
530
+ listMcpServers() {
531
+ return sendCommand((id) => ({ id, type: "list-mcp-servers" })).then(
532
+ (result) => Array.isArray(result) ? result : []
533
+ );
629
534
  },
630
- async fetchHistory(daemonId, opts) {
631
- const suffix = `/history${queryString({
632
- profile: opts?.profile,
633
- cwd: opts?.cwd,
634
- limit: opts?.limit,
635
- cursor: opts?.cursor
636
- })}`;
637
- const response = await daemonFetch(daemonId, suffix);
638
- if (!response.ok)
639
- await throwFromResponse(response, "Failed to load history", {
640
- 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
641
- });
642
- return await response.json();
535
+ capabilities() {
536
+ return transport.capabilities?.() ?? [];
643
537
  },
644
- async listWorkspaces(daemonId, opts) {
645
- const suffix = `/workspaces${queryString({ profile: opts?.profile })}`;
646
- const response = await daemonFetch(daemonId, suffix);
647
- if (!response.ok)
648
- await throwFromResponse(response, "Failed to load workspaces", {
649
- 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
650
- });
651
- return await response.json();
538
+ completeProviderAuth(credential) {
539
+ return sendCommand((id) => ({
540
+ id,
541
+ type: "complete-provider-auth",
542
+ providerId: "openai-codex",
543
+ credential
544
+ })).then(() => {
545
+ });
652
546
  },
653
- async createWorkspace(daemonId, opts) {
654
- const response = await daemonFetch(daemonId, "/workspaces", {
655
- method: "POST",
656
- headers: { "Content-Type": "application/json" },
657
- body: JSON.stringify({
658
- name: opts.name,
659
- ...opts.root !== void 0 ? { root: opts.root } : {},
660
- ...opts.profile !== void 0 ? { profile: opts.profile } : {}
661
- })
547
+ addCustomProvider(provider) {
548
+ return sendCommand((id) => ({
549
+ id,
550
+ type: "add-custom-provider",
551
+ provider
552
+ })).then(() => {
662
553
  });
663
- if (!response.ok) await throwFromResponse(response, "Failed to create the workspace");
664
- return await response.json();
665
554
  },
666
- async deleteSession(daemonId, historyId, opts) {
667
- const suffix = `/sessions/${encodeURIComponent(historyId)}${queryString({
668
- workspace: opts?.workspace,
669
- profile: opts?.profile
670
- })}`;
671
- const response = await daemonFetch(daemonId, suffix, { method: "DELETE" });
672
- if (response.status === 404) {
673
- throw new TerminalEndpointError("session-not-found", "This session no longer exists.");
674
- }
675
- if (!response.ok) await throwFromResponse(response, "Failed to delete the session");
555
+ close() {
556
+ if (closed) return;
557
+ closed = true;
558
+ unsubscribe();
559
+ unsubscribeUndeliverable?.();
560
+ rejectAllPending("SessionClient closed before the command was acknowledged.");
676
561
  }
677
562
  };
678
563
  }
679
564
 
680
- // src/client/session-client.ts
681
- var EMPTY_MODEL_COMPLETION = {
682
- providers: [],
683
- perProvider: {},
684
- recentModels: []
565
+ // src/client/socket.ts
566
+ function adaptWhatwgSocket(socket) {
567
+ const on = (event, listener) => {
568
+ switch (event) {
569
+ case "open":
570
+ socket.addEventListener("open", () => listener(void 0));
571
+ return;
572
+ case "message":
573
+ socket.addEventListener("message", (messageEvent) => listener(messageEvent.data));
574
+ return;
575
+ case "close":
576
+ socket.addEventListener("close", () => listener(void 0));
577
+ return;
578
+ case "error":
579
+ socket.addEventListener("error", (errorEvent) => listener(errorEvent));
580
+ return;
581
+ }
582
+ };
583
+ return {
584
+ on,
585
+ send: (data) => socket.send(data),
586
+ close: () => socket.close()
587
+ };
588
+ }
589
+ function globalWebSocketConstructor() {
590
+ const candidate = globalThis.WebSocket;
591
+ if (typeof candidate !== "function") {
592
+ throw new Error(
593
+ "No global WebSocket constructor is available. Pass `socketFactory` (node < 22 callers can wrap the 'ws' package)."
594
+ );
595
+ }
596
+ return candidate;
597
+ }
598
+ function defaultSocketFactory(url) {
599
+ const WebSocketCtor = globalWebSocketConstructor();
600
+ return adaptWhatwgSocket(new WebSocketCtor(url));
601
+ }
602
+
603
+ // src/client/uds-socket.ts
604
+ import { WebSocket } from "ws";
605
+ function udsUrl(socketPath) {
606
+ return `ws+unix://${socketPath}:/`;
607
+ }
608
+ function ensureLocalhostNoProxy() {
609
+ const ga = globalThis.GLOBAL_AGENT;
610
+ if (!ga) return;
611
+ const current = ga.NO_PROXY ?? "";
612
+ const entries = current.split(",").map((s) => s.trim()).filter(Boolean);
613
+ let changed = false;
614
+ for (const host of ["localhost", "127.0.0.1"]) {
615
+ if (!entries.includes(host)) {
616
+ entries.push(host);
617
+ changed = true;
618
+ }
619
+ }
620
+ if (changed) {
621
+ ga.NO_PROXY = entries.join(",");
622
+ }
623
+ }
624
+ function udsSocketFactory(socketPath) {
625
+ const target = udsUrl(socketPath);
626
+ return (_url) => {
627
+ ensureLocalhostNoProxy();
628
+ return new WebSocket(target);
629
+ };
630
+ }
631
+
632
+ // src/client/websocket.ts
633
+ var toText = (data) => {
634
+ if (typeof data === "string") return data;
635
+ if (data instanceof Buffer) return data.toString("utf8");
636
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
637
+ if (Array.isArray(data) && data.every((chunk) => chunk instanceof Buffer)) {
638
+ return Buffer.concat(data).toString("utf8");
639
+ }
640
+ return null;
685
641
  };
686
- function toModelCompletion(result) {
687
- if (typeof result !== "object" || result === null || Array.isArray(result)) {
688
- return EMPTY_MODEL_COMPLETION;
642
+ var parseMessage = (data) => {
643
+ const text = toText(data);
644
+ if (text === null) return null;
645
+ try {
646
+ const parsed = JSON.parse(text);
647
+ return typeof parsed === "object" && parsed !== null && typeof parsed.type === "string" ? parsed : null;
648
+ } catch {
649
+ return null;
689
650
  }
690
- const record = result;
691
- const providers = Array.isArray(record.providers) ? record.providers.filter((p) => typeof p === "string") : [];
692
- const perProvider = {};
693
- if (typeof record.perProvider === "object" && record.perProvider !== null) {
694
- for (const [provider, models] of Object.entries(
695
- record.perProvider
696
- )) {
697
- if (!Array.isArray(models)) continue;
698
- perProvider[provider] = models.flatMap((entry) => {
699
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
700
- const id = entry.id;
701
- if (typeof id !== "string") return [];
702
- const name = entry.name;
703
- return [typeof name === "string" ? { id, name } : { id }];
704
- });
651
+ };
652
+ function createWebSocketTransport(options) {
653
+ if (!options.getEndpoint && (options.url === void 0 || options.token === void 0)) {
654
+ throw new Error("createWebSocketTransport requires either url+token or getEndpoint.");
655
+ }
656
+ const socketFactory = options.socketFactory ?? defaultSocketFactory;
657
+ const backoffInitialMs = options.backoffInitialMs ?? 500;
658
+ const backoffMaxMs = options.backoffMaxMs ?? 3e4;
659
+ const backoffFactor = options.backoffFactor ?? 2;
660
+ const frameListeners = /* @__PURE__ */ new Set();
661
+ const stateListeners = /* @__PURE__ */ new Set();
662
+ const undeliverableListeners = /* @__PURE__ */ new Set();
663
+ let state = "reconnecting";
664
+ let phase = "awaiting-welcome";
665
+ let socket = null;
666
+ let lastSeq = null;
667
+ let attempts = 0;
668
+ let attachCounter = 0;
669
+ let reconnectTimer = null;
670
+ let closed = false;
671
+ let cachedCapabilities = [];
672
+ const setState = (next) => {
673
+ if (state === next) return;
674
+ state = next;
675
+ for (const listener of stateListeners) {
676
+ try {
677
+ listener(next);
678
+ } catch {
679
+ }
680
+ }
681
+ };
682
+ const deliver = (frame) => {
683
+ for (const listener of frameListeners) {
684
+ try {
685
+ listener(frame);
686
+ } catch {
687
+ }
688
+ }
689
+ };
690
+ const notifyUndeliverable = () => {
691
+ for (const listener of undeliverableListeners) {
692
+ try {
693
+ listener();
694
+ } catch {
695
+ }
696
+ }
697
+ };
698
+ const terminate = () => {
699
+ const wasAttached = phase === "attached";
700
+ closed = true;
701
+ cachedCapabilities = [];
702
+ if (reconnectTimer !== null) {
703
+ clearTimeout(reconnectTimer);
704
+ reconnectTimer = null;
705
+ }
706
+ const current = socket;
707
+ socket = null;
708
+ current?.close();
709
+ setState("closed");
710
+ if (wasAttached) notifyUndeliverable();
711
+ };
712
+ const scheduleReconnect = () => {
713
+ if (closed || reconnectTimer !== null) return;
714
+ setState("reconnecting");
715
+ const delay = Math.min(backoffInitialMs * backoffFactor ** attempts, backoffMaxMs);
716
+ attempts += 1;
717
+ reconnectTimer = setTimeout(() => {
718
+ reconnectTimer = null;
719
+ connect();
720
+ }, delay);
721
+ };
722
+ const completeAttach = () => {
723
+ phase = "attached";
724
+ attempts = 0;
725
+ setState("connected");
726
+ };
727
+ const handleMessage = (message) => {
728
+ if (message.type === "rejected") {
729
+ terminate();
730
+ options.onRejected?.(message.reason);
731
+ return;
732
+ }
733
+ if (message.type === "detached") {
734
+ terminate();
735
+ options.onDetached?.(message.reason);
736
+ return;
737
+ }
738
+ if (phase === "awaiting-welcome") {
739
+ if (message.type !== "welcome") return;
740
+ cachedCapabilities = Array.isArray(message.capabilities) ? [...message.capabilities] : [];
741
+ phase = "awaiting-attach";
742
+ setState("replaying");
743
+ attachCounter += 1;
744
+ socket?.send(
745
+ JSON.stringify({
746
+ id: `attach-${attachCounter}`,
747
+ type: "attach",
748
+ sessionId: options.sessionId,
749
+ ...lastSeq !== null ? { lastSeq } : {}
750
+ })
751
+ );
752
+ return;
705
753
  }
706
- }
707
- const recentModels = Array.isArray(record.recentModels) ? record.recentModels.flatMap((entry) => {
708
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return [];
709
- const provider = entry.provider;
710
- const model = entry.model;
711
- return typeof provider === "string" && typeof model === "string" ? [{ provider, model }] : [];
712
- }) : [];
713
- return { providers, perProvider, recentModels };
714
- }
715
- function createSessionClient(transport) {
716
- let state = createSessionViewState();
717
- let seq = 0;
718
- let closed = false;
719
- let notifyScheduled = false;
720
- let commandCounter = 0;
721
- let serverCommandList = [];
722
- const stateListeners = /* @__PURE__ */ new Set();
723
- const eventListeners = /* @__PURE__ */ new Set();
724
- const pending = /* @__PURE__ */ new Map();
725
- const scheduleNotify = () => {
726
- if (notifyScheduled) return;
727
- notifyScheduled = true;
728
- queueMicrotask(() => {
729
- notifyScheduled = false;
730
- for (const listener of stateListeners) {
731
- try {
732
- listener();
733
- } catch {
754
+ if (phase === "awaiting-attach") {
755
+ if (message.type === "replay") {
756
+ for (const event of message.events) {
757
+ lastSeq = event.seq;
758
+ deliver(event);
734
759
  }
760
+ completeAttach();
761
+ return;
762
+ }
763
+ if (message.type === "snapshot") {
764
+ lastSeq = message.snapshot.seq;
765
+ deliver(message);
766
+ completeAttach();
735
767
  }
736
- });
737
- };
738
- const unsubscribe = transport.onFrame((frame) => {
739
- if (frame.type === "snapshot") {
740
- state = frame.snapshot.state;
741
- seq = frame.snapshot.seq;
742
- serverCommandList = frame.snapshot.serverCommands;
743
- scheduleNotify();
744
768
  return;
745
769
  }
746
- if (frame.type === "ack") {
747
- const entry = pending.get(frame.id);
748
- if (!entry) return;
749
- pending.delete(frame.id);
750
- if (frame.ok) {
751
- entry.resolve(frame.result);
752
- } else {
753
- entry.reject(new Error(frame.error?.message ?? "Command failed."));
754
- }
770
+ if (message.type === "ack") {
771
+ deliver(message);
755
772
  return;
756
773
  }
757
- state = reduceSessionEvent(state, frame);
758
- seq = frame.seq;
759
- for (const listener of eventListeners) {
760
- try {
761
- listener(frame);
762
- } catch {
763
- }
774
+ if (message.type === "snapshot") {
775
+ lastSeq = message.snapshot.seq;
776
+ deliver(message);
777
+ return;
764
778
  }
765
- scheduleNotify();
766
- });
767
- const sendCommand = (build) => {
768
- if (closed) {
769
- return Promise.reject(new Error("SessionClient is closed."));
779
+ if (message.type === "welcome" || message.type === "replay" || message.type === "session-list" || message.type === "session-created") {
780
+ return;
770
781
  }
771
- commandCounter += 1;
772
- const id = `cmd-${commandCounter}`;
773
- return new Promise((resolve, reject) => {
774
- pending.set(id, { resolve, reject });
775
- transport.send(build(id));
782
+ lastSeq = message.seq;
783
+ deliver(message);
784
+ };
785
+ const openSocket = (endpoint) => {
786
+ if (closed) return;
787
+ phase = "awaiting-welcome";
788
+ cachedCapabilities = [];
789
+ const current = socketFactory(endpoint.url);
790
+ socket = current;
791
+ current.on("open", () => {
792
+ if (closed || socket !== current) return;
793
+ current.send(
794
+ JSON.stringify({
795
+ type: "hello",
796
+ protocolVersion: PROTOCOL_VERSION,
797
+ ...endpoint.credential
798
+ })
799
+ );
800
+ });
801
+ current.on("message", (data) => {
802
+ if (closed || socket !== current) return;
803
+ const message = parseMessage(data);
804
+ if (message) handleMessage(message);
805
+ });
806
+ current.on("error", () => {
807
+ });
808
+ current.on("close", () => {
809
+ if (socket !== current) return;
810
+ socket = null;
811
+ if (closed) return;
812
+ const wasAttached = phase === "attached";
813
+ phase = "awaiting-welcome";
814
+ if (wasAttached) notifyUndeliverable();
815
+ scheduleReconnect();
776
816
  });
777
817
  };
778
- return {
779
- getState() {
780
- return state;
781
- },
782
- getSeq() {
783
- return seq;
784
- },
785
- subscribe(listener) {
786
- stateListeners.add(listener);
787
- return () => stateListeners.delete(listener);
788
- },
789
- onEvent(listener) {
790
- eventListeners.add(listener);
791
- return () => eventListeners.delete(listener);
792
- },
793
- submit(input, opts) {
794
- return sendCommand((id) => ({
795
- id,
796
- type: "submit",
797
- input,
798
- displayText: opts.displayText,
799
- ...opts.attachmentLabels?.length ? { attachmentLabels: opts.attachmentLabels } : {}
800
- })).then(() => {
801
- });
802
- },
803
- decideApproval(toolCallId, decision, comment, grantDir) {
804
- return sendCommand((id) => ({
805
- id,
806
- type: "decide-approval",
807
- toolCallId,
808
- decision,
809
- ...comment !== void 0 ? { comment } : {},
810
- ...grantDir !== void 0 ? { grantDir } : {}
811
- })).then(() => {
812
- });
813
- },
814
- answerQuestion(questionId, answers) {
815
- return sendCommand((id) => ({ id, type: "answer-question", questionId, answers })).then(
816
- () => {
818
+ const connect = () => {
819
+ if (closed) return;
820
+ const getEndpoint = options.getEndpoint;
821
+ if (!getEndpoint) {
822
+ openSocket({ url: options.url, credential: { token: options.token } });
823
+ return;
824
+ }
825
+ void getEndpoint().then(
826
+ (endpoint) => {
827
+ if (closed) return;
828
+ openSocket(endpoint);
829
+ },
830
+ (error) => {
831
+ if (closed) return;
832
+ if (error instanceof TerminalEndpointError) {
833
+ terminate();
834
+ options.onDetached?.(error.reason);
835
+ return;
817
836
  }
818
- );
819
- },
820
- abort() {
821
- return sendCommand((id) => ({ id, type: "abort" })).then(() => {
822
- });
823
- },
824
- dequeueMessage(queuedId) {
825
- return sendCommand((id) => ({ id, type: "dequeue", queuedId })).then(() => {
826
- });
827
- },
828
- runSlashCommand(raw) {
829
- return sendCommand((id) => ({ id, type: "slash-command", raw })).then(() => {
830
- });
831
- },
832
- loadHistory(historyId) {
833
- return sendCommand((id) => ({ id, type: "load-history", historyId })).then(() => {
834
- });
835
- },
836
- reloadConfig() {
837
- return sendCommand((id) => ({ id, type: "reload-config" })).then(() => {
838
- });
839
- },
840
- cancelAuthFlow() {
841
- return sendCommand((id) => ({ id, type: "cancel-auth-flow" })).then(() => {
842
- });
843
- },
844
- serverCommands() {
845
- return serverCommandList;
846
- },
847
- completePath(prefix) {
848
- return sendCommand((id) => ({ id, type: "complete-path", prefix })).then(
849
- (result) => Array.isArray(result) ? result.filter((entry) => typeof entry === "string") : []
850
- );
851
- },
852
- completeModels() {
853
- return sendCommand((id) => ({ id, type: "complete-models" })).then(toModelCompletion);
854
- },
855
- setConfig(patches) {
856
- return sendCommand((id) => ({ id, type: "set-config", patches })).then(
857
- (result) => result
858
- );
859
- },
860
- listSkills() {
861
- return sendCommand((id) => ({ id, type: "list-skills" })).then(
862
- (result) => Array.isArray(result) ? result : []
863
- );
864
- },
865
- getSkill(skillId) {
866
- return sendCommand((id) => ({ id, type: "get-skill", skillId })).then(
867
- (result) => result
868
- );
837
+ scheduleReconnect();
838
+ }
839
+ );
840
+ };
841
+ connect();
842
+ return {
843
+ send(command) {
844
+ if (closed || phase !== "attached" || socket === null) return false;
845
+ socket.send(JSON.stringify(command));
846
+ return true;
869
847
  },
870
- listAgents() {
871
- return sendCommand((id) => ({ id, type: "list-agents" })).then(
872
- (result) => Array.isArray(result) ? result : []
873
- );
848
+ onFrame(listener) {
849
+ frameListeners.add(listener);
850
+ return () => {
851
+ frameListeners.delete(listener);
852
+ };
874
853
  },
875
- getAgent(agentId) {
876
- return sendCommand((id) => ({ id, type: "get-agent", agentId })).then(
877
- (result) => result
878
- );
854
+ close() {
855
+ if (closed) return;
856
+ terminate();
857
+ frameListeners.clear();
858
+ stateListeners.clear();
859
+ undeliverableListeners.clear();
879
860
  },
880
- getCommand(name) {
881
- return sendCommand((id) => ({ id, type: "get-command", name })).then(
882
- (result) => result
883
- );
861
+ connectionState() {
862
+ return state;
884
863
  },
885
- listMcpServers() {
886
- return sendCommand((id) => ({ id, type: "list-mcp-servers" })).then(
887
- (result) => Array.isArray(result) ? result : []
888
- );
864
+ onConnectionState(listener) {
865
+ stateListeners.add(listener);
866
+ return () => {
867
+ stateListeners.delete(listener);
868
+ };
889
869
  },
890
- close() {
891
- if (closed) return;
892
- closed = true;
893
- unsubscribe();
894
- const error = new Error("SessionClient closed before the command was acknowledged.");
895
- for (const { reject } of pending.values()) {
896
- reject(error);
897
- }
898
- pending.clear();
870
+ onUndeliverable(listener) {
871
+ undeliverableListeners.add(listener);
872
+ return () => {
873
+ undeliverableListeners.delete(listener);
874
+ };
875
+ },
876
+ capabilities() {
877
+ return [...cachedCapabilities];
899
878
  }
900
879
  };
901
880
  }
902
881
 
903
- // src/client/uds-socket.ts
904
- import { WebSocket } from "ws";
905
- function udsUrl(socketPath) {
906
- return `ws+unix://${socketPath}:/`;
882
+ // src/client/daemon-client.ts
883
+ var CLOSED_ERROR = "DaemonClient is closed.";
884
+ function throwCollectedErrors(errors, message) {
885
+ if (errors.length === 1) throw errors[0];
886
+ if (errors.length > 1) throw new AggregateError(errors, message);
907
887
  }
908
- function ensureLocalhostNoProxy() {
909
- const ga = globalThis.GLOBAL_AGENT;
910
- if (!ga) return;
911
- const current = ga.NO_PROXY ?? "";
912
- const entries = current.split(",").map((s) => s.trim()).filter(Boolean);
913
- let changed = false;
914
- for (const host of ["localhost", "127.0.0.1"]) {
915
- if (!entries.includes(host)) {
916
- entries.push(host);
917
- changed = true;
918
- }
919
- }
920
- if (changed) {
921
- ga.NO_PROXY = entries.join(",");
922
- }
888
+ function bindDaemonDataClient(client, daemonId) {
889
+ return {
890
+ fetchTranscript: (historyId, opts) => client.fetchTranscript(daemonId, historyId, opts),
891
+ fetchHistory: (opts) => client.fetchHistory(daemonId, opts),
892
+ listWorkspaces: (opts) => client.listWorkspaces(daemonId, opts),
893
+ createWorkspace: (opts) => client.createWorkspace(daemonId, opts),
894
+ deleteSession: (historyId, opts) => client.deleteSession(daemonId, historyId, opts),
895
+ listProfiles: () => client.listProfiles(daemonId),
896
+ listProviders: (opts) => client.listProviders(daemonId, opts),
897
+ addProvider: (input) => client.addProvider(daemonId, input),
898
+ removeProvider: (providerId, opts) => client.removeProvider(daemonId, providerId, opts),
899
+ setDefaultProvider: (providerId, opts) => client.setDefaultProvider(daemonId, providerId, opts),
900
+ setProviderAuth: (providerId, credential, opts) => client.setProviderAuth(daemonId, providerId, credential, opts),
901
+ clearProviderAuth: (providerId, opts) => client.clearProviderAuth(daemonId, providerId, opts),
902
+ updateWorkspace: (name, patch) => client.updateWorkspace(daemonId, name, patch)
903
+ };
923
904
  }
924
- function udsSocketFactory(socketPath) {
925
- const target = udsUrl(socketPath);
926
- return (_url) => {
927
- ensureLocalhostNoProxy();
928
- return new WebSocket(target);
905
+ function createDaemonClient(target, deps = {}) {
906
+ const dataClient = (() => {
907
+ switch (target.kind) {
908
+ case "local": {
909
+ if (!deps.udsFetch) {
910
+ throw new Error("A local DaemonClient requires deps.udsFetch.");
911
+ }
912
+ return createHttpDataClient({
913
+ baseUrl: "http://daemon",
914
+ getToken: async () => target.token,
915
+ fetchImpl: deps.udsFetch(target.socketPath)
916
+ });
917
+ }
918
+ case "direct":
919
+ return createHttpDataClient({
920
+ baseUrl: httpBaseFromWsUrl(target.wsUrl),
921
+ getToken: async () => target.token,
922
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
923
+ });
924
+ case "relay":
925
+ return createHttpDataClient({
926
+ baseUrl: target.apiBaseUrl,
927
+ getToken: target.getAccessToken,
928
+ refreshAccessToken: target.refreshAccessToken,
929
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
930
+ });
931
+ }
932
+ })();
933
+ const connections = /* @__PURE__ */ new Set();
934
+ const directorySockets = /* @__PURE__ */ new Set();
935
+ let closed = false;
936
+ const socketFactory = target.kind === "local" ? deps.socketFactory ?? udsSocketFactory(target.socketPath) : deps.socketFactory ?? defaultSocketFactory;
937
+ const requireOpen = () => {
938
+ if (closed) throw new Error(CLOSED_ERROR);
939
+ };
940
+ const directorySocketFactory = (url) => {
941
+ requireOpen();
942
+ const socket = socketFactory(url);
943
+ if (closed) {
944
+ socket.close();
945
+ throw new Error(CLOSED_ERROR);
946
+ }
947
+ let closeRequested = false;
948
+ let closeDelivered = false;
949
+ const closeListeners = /* @__PURE__ */ new Set();
950
+ let owned;
951
+ const deliverClose = () => {
952
+ if (closeDelivered) return;
953
+ closeDelivered = true;
954
+ closeRequested = true;
955
+ directorySockets.delete(owned);
956
+ const listeners = [...closeListeners];
957
+ closeListeners.clear();
958
+ const errors = [];
959
+ for (const listener of listeners) {
960
+ try {
961
+ listener();
962
+ } catch (error) {
963
+ errors.push(error);
964
+ }
965
+ }
966
+ throwCollectedErrors(errors, "Failed to deliver directory socket close listeners.");
967
+ };
968
+ owned = {
969
+ on(event, listener) {
970
+ if (event === "close") {
971
+ closeListeners.add(listener);
972
+ return owned;
973
+ }
974
+ return socket.on(event, listener);
975
+ },
976
+ send: (data) => socket.send(data),
977
+ close() {
978
+ if (closeRequested) return;
979
+ closeRequested = true;
980
+ directorySockets.delete(owned);
981
+ const errors = [];
982
+ try {
983
+ socket.close();
984
+ } catch (error) {
985
+ errors.push(error);
986
+ } finally {
987
+ try {
988
+ deliverClose();
989
+ } catch (error) {
990
+ errors.push(error);
991
+ }
992
+ }
993
+ throwCollectedErrors(errors, "Failed to close directory socket.");
994
+ }
995
+ };
996
+ directorySockets.add(owned);
997
+ socket.on("close", deliverClose);
998
+ return owned;
999
+ };
1000
+ const acquireEndpoint = async () => {
1001
+ requireOpen();
1002
+ const endpoint = await dataClient.acquireEndpoint(target.id);
1003
+ requireOpen();
1004
+ return endpoint;
1005
+ };
1006
+ const endpointConfig = (selectedSocketFactory) => {
1007
+ switch (target.kind) {
1008
+ case "local":
1009
+ return {
1010
+ url: udsUrl(target.socketPath),
1011
+ token: target.token,
1012
+ socketFactory: selectedSocketFactory
1013
+ };
1014
+ case "direct":
1015
+ return {
1016
+ url: target.wsUrl,
1017
+ token: target.token,
1018
+ socketFactory: selectedSocketFactory
1019
+ };
1020
+ case "relay":
1021
+ return {
1022
+ getEndpoint: acquireEndpoint,
1023
+ socketFactory: selectedSocketFactory
1024
+ };
1025
+ }
1026
+ };
1027
+ return {
1028
+ target,
1029
+ data: bindDaemonDataClient(dataClient, target.id),
1030
+ directoryEndpoint() {
1031
+ requireOpen();
1032
+ return { name: target.name, ...endpointConfig(directorySocketFactory) };
1033
+ },
1034
+ openSession(sessionId, options = {}) {
1035
+ requireOpen();
1036
+ const transport = createWebSocketTransport({
1037
+ ...endpointConfig(socketFactory),
1038
+ sessionId,
1039
+ ...options
1040
+ });
1041
+ const client = createSessionClient(transport);
1042
+ let connectionClosed = false;
1043
+ const connection = {
1044
+ client,
1045
+ transport,
1046
+ close() {
1047
+ if (connectionClosed) return;
1048
+ connectionClosed = true;
1049
+ connections.delete(connection);
1050
+ const errors = [];
1051
+ try {
1052
+ client.close();
1053
+ } catch (error) {
1054
+ errors.push(error);
1055
+ }
1056
+ try {
1057
+ transport.close();
1058
+ } catch (error) {
1059
+ errors.push(error);
1060
+ }
1061
+ throwCollectedErrors(errors, "Failed to close session connection.");
1062
+ }
1063
+ };
1064
+ connections.add(connection);
1065
+ return connection;
1066
+ },
1067
+ close() {
1068
+ if (closed) return;
1069
+ closed = true;
1070
+ const errors = [];
1071
+ for (const connection of [...connections]) {
1072
+ try {
1073
+ connection.close();
1074
+ } catch (error) {
1075
+ errors.push(error);
1076
+ }
1077
+ }
1078
+ for (const socket of [...directorySockets]) {
1079
+ directorySockets.delete(socket);
1080
+ try {
1081
+ socket.close();
1082
+ } catch (error) {
1083
+ errors.push(error);
1084
+ }
1085
+ }
1086
+ connections.clear();
1087
+ directorySockets.clear();
1088
+ throwCollectedErrors(errors, "Failed to close DaemonClient resources.");
1089
+ }
929
1090
  };
930
1091
  }
931
1092
 
932
- // src/client/websocket.ts
1093
+ // src/client/directory.ts
933
1094
  var toText2 = (data) => {
934
1095
  if (typeof data === "string") return data;
935
1096
  if (data instanceof Buffer) return data.toString("utf8");
@@ -949,133 +1110,123 @@ var parseMessage2 = (data) => {
949
1110
  return null;
950
1111
  }
951
1112
  };
952
- function createWebSocketTransport(options) {
953
- if (!options.getEndpoint && (options.url === void 0 || options.token === void 0)) {
954
- throw new Error("createWebSocketTransport requires either url+token or getEndpoint.");
955
- }
1113
+ function createServerDirectory(options) {
956
1114
  const socketFactory = options.socketFactory ?? defaultSocketFactory;
957
- const backoffInitialMs = options.backoffInitialMs ?? 500;
958
- const backoffMaxMs = options.backoffMaxMs ?? 3e4;
959
- const backoffFactor = options.backoffFactor ?? 2;
960
- const frameListeners = /* @__PURE__ */ new Set();
961
- const stateListeners = /* @__PURE__ */ new Set();
962
- let state = "reconnecting";
963
- let phase = "awaiting-welcome";
964
- let socket = null;
965
- let lastSeq = null;
966
- let attempts = 0;
967
- let attachCounter = 0;
968
- let reconnectTimer = null;
1115
+ const connectTimeoutMs = options.connectTimeoutMs ?? 5e3;
1116
+ const listeners = /* @__PURE__ */ new Set();
1117
+ const links = /* @__PURE__ */ new Map();
969
1118
  let closed = false;
970
- const setState = (next) => {
971
- if (state === next) return;
972
- state = next;
973
- for (const listener of stateListeners) {
1119
+ let view = { servers: [] };
1120
+ const rebuildView = () => {
1121
+ view = {
1122
+ servers: Array.from(links.values(), (link) => ({
1123
+ name: link.config.name,
1124
+ url: link.resolvedUrl ?? link.config.url ?? "",
1125
+ status: link.status
1126
+ }))
1127
+ };
1128
+ for (const listener of listeners) {
974
1129
  try {
975
- listener(next);
1130
+ listener();
976
1131
  } catch {
977
1132
  }
978
1133
  }
979
1134
  };
980
- const deliver = (frame) => {
981
- for (const listener of frameListeners) {
982
- try {
983
- listener(frame);
984
- } catch {
985
- }
1135
+ const clearTimer = (link) => {
1136
+ if (link.timer !== null) {
1137
+ clearTimeout(link.timer);
1138
+ link.timer = null;
986
1139
  }
987
1140
  };
988
- const terminate = () => {
989
- closed = true;
990
- if (reconnectTimer !== null) {
991
- clearTimeout(reconnectTimer);
992
- reconnectTimer = null;
1141
+ const rejectPending = (link, message) => {
1142
+ const error = new Error(message);
1143
+ for (const entry of link.pending.values()) {
1144
+ if (entry.kind !== "list") entry.reject(error);
993
1145
  }
994
- const current = socket;
995
- socket = null;
996
- current?.close();
997
- setState("closed");
1146
+ link.pending.clear();
998
1147
  };
999
- const scheduleReconnect = () => {
1000
- if (closed || reconnectTimer !== null) return;
1001
- setState("reconnecting");
1002
- const delay = Math.min(backoffInitialMs * backoffFactor ** attempts, backoffMaxMs);
1003
- attempts += 1;
1004
- reconnectTimer = setTimeout(() => {
1005
- reconnectTimer = null;
1006
- connect();
1007
- }, delay);
1148
+ const markOffline = (link, message) => {
1149
+ clearTimer(link);
1150
+ link.ready = false;
1151
+ const current = link.socket;
1152
+ link.socket = null;
1153
+ current?.close();
1154
+ rejectPending(link, message);
1155
+ link.status = { state: "offline", error: message };
1156
+ rebuildView();
1008
1157
  };
1009
- const completeAttach = () => {
1010
- phase = "attached";
1011
- attempts = 0;
1012
- setState("connected");
1158
+ const sendList = (link) => {
1159
+ if (!link.ready || link.socket === null) return;
1160
+ link.commandCounter += 1;
1161
+ const id = `dir-${link.commandCounter}`;
1162
+ link.pending.set(id, { kind: "list" });
1163
+ link.socket.send(JSON.stringify({ id, type: "list-sessions" }));
1013
1164
  };
1014
- const handleMessage = (message) => {
1165
+ const handleMessage = (link, message) => {
1015
1166
  if (message.type === "rejected") {
1016
- terminate();
1017
- options.onRejected?.(message.reason);
1167
+ markOffline(
1168
+ link,
1169
+ message.reason === "auth" ? `Token rejected by "${link.config.name}" \u2014 check the configured token.` : `Protocol version mismatch with "${link.config.name}" \u2014 upgrade the daemon or this client.`
1170
+ );
1018
1171
  return;
1019
1172
  }
1020
- if (message.type === "detached") {
1021
- terminate();
1022
- options.onDetached?.(message.reason);
1173
+ if (message.type === "welcome") {
1174
+ link.ready = true;
1175
+ clearTimer(link);
1176
+ rebuildView();
1177
+ sendList(link);
1023
1178
  return;
1024
1179
  }
1025
- if (phase === "awaiting-welcome") {
1026
- if (message.type !== "welcome") return;
1027
- phase = "awaiting-attach";
1028
- setState("replaying");
1029
- attachCounter += 1;
1030
- socket?.send(
1031
- JSON.stringify({
1032
- id: `attach-${attachCounter}`,
1033
- type: "attach",
1034
- sessionId: options.sessionId,
1035
- ...lastSeq !== null ? { lastSeq } : {}
1036
- })
1037
- );
1180
+ if (message.type === "session-list") {
1181
+ const entry = link.pending.get(message.id);
1182
+ if (entry?.kind !== "list") return;
1183
+ link.pending.delete(message.id);
1184
+ link.status = { state: "online", sessions: message.sessions };
1185
+ rebuildView();
1038
1186
  return;
1039
1187
  }
1040
- if (phase === "awaiting-attach") {
1041
- if (message.type === "replay") {
1042
- for (const event of message.events) {
1043
- lastSeq = event.seq;
1044
- deliver(event);
1045
- }
1046
- completeAttach();
1047
- return;
1048
- }
1049
- if (message.type === "snapshot") {
1050
- lastSeq = message.snapshot.seq;
1051
- deliver(message);
1052
- completeAttach();
1188
+ if (message.type === "session-created") {
1189
+ const entry = link.pending.get(message.id);
1190
+ if (entry?.kind !== "create" && entry?.kind !== "resume") return;
1191
+ link.pending.delete(message.id);
1192
+ entry.resolve(message.session);
1193
+ if (link.status.state === "online") {
1194
+ const sessions = link.status.sessions.some((existing) => existing.id === message.session.id) ? link.status.sessions : [...link.status.sessions, message.session];
1195
+ link.status = { state: "online", sessions };
1196
+ rebuildView();
1053
1197
  }
1054
1198
  return;
1055
1199
  }
1056
1200
  if (message.type === "ack") {
1057
- deliver(message);
1058
- return;
1059
- }
1060
- if (message.type === "snapshot") {
1061
- lastSeq = message.snapshot.seq;
1062
- deliver(message);
1063
- return;
1064
- }
1065
- if (message.type === "welcome" || message.type === "replay" || message.type === "session-list" || message.type === "session-created") {
1201
+ const entry = link.pending.get(message.id);
1202
+ if (!entry || entry.kind === "list") return;
1203
+ link.pending.delete(message.id);
1204
+ if (!message.ok) {
1205
+ entry.reject(new Error(message.error?.message ?? "Command failed."));
1206
+ return;
1207
+ }
1208
+ if (entry.kind === "kill") {
1209
+ entry.resolve();
1210
+ sendList(link);
1211
+ return;
1212
+ }
1213
+ if (entry.kind === "complete") {
1214
+ entry.resolve(
1215
+ Array.isArray(message.result) ? message.result.filter((item) => typeof item === "string") : []
1216
+ );
1217
+ return;
1218
+ }
1219
+ entry.reject(new Error("Unexpected ack for create-session / resume-session."));
1066
1220
  return;
1067
1221
  }
1068
- lastSeq = message.seq;
1069
- deliver(message);
1070
1222
  };
1071
- const openSocket = (endpoint) => {
1072
- if (closed) return;
1073
- phase = "awaiting-welcome";
1074
- const current = socketFactory(endpoint.url);
1075
- socket = current;
1076
- current.on("open", () => {
1077
- if (closed || socket !== current) return;
1078
- current.send(
1223
+ const openLinkSocket = (link, endpoint) => {
1224
+ link.resolvedUrl = endpoint.url;
1225
+ const socket = (link.config.socketFactory ?? socketFactory)(endpoint.url);
1226
+ link.socket = socket;
1227
+ socket.on("open", () => {
1228
+ if (closed || link.socket !== socket) return;
1229
+ socket.send(
1079
1230
  JSON.stringify({
1080
1231
  type: "hello",
1081
1232
  protocolVersion: PROTOCOL_VERSION,
@@ -1083,75 +1234,245 @@ function createWebSocketTransport(options) {
1083
1234
  })
1084
1235
  );
1085
1236
  });
1086
- current.on("message", (data) => {
1087
- if (closed || socket !== current) return;
1237
+ socket.on("message", (data) => {
1238
+ if (closed || link.socket !== socket) return;
1088
1239
  const message = parseMessage2(data);
1089
- if (message) handleMessage(message);
1240
+ if (message) handleMessage(link, message);
1090
1241
  });
1091
- current.on("error", () => {
1242
+ socket.on("error", () => {
1092
1243
  });
1093
- current.on("close", () => {
1094
- if (socket !== current) return;
1095
- socket = null;
1244
+ socket.on("close", () => {
1245
+ if (link.socket !== socket) return;
1246
+ link.socket = null;
1096
1247
  if (closed) return;
1097
- scheduleReconnect();
1248
+ if (link.status.state !== "offline") {
1249
+ markOffline(link, `Connection to "${link.config.name}" closed.`);
1250
+ }
1098
1251
  });
1099
1252
  };
1100
- const connect = () => {
1253
+ const connect = (link) => {
1101
1254
  if (closed) return;
1102
- const getEndpoint = options.getEndpoint;
1255
+ link.ready = false;
1256
+ link.status = { state: "connecting" };
1257
+ clearTimer(link);
1258
+ link.timer = setTimeout(() => {
1259
+ link.timer = null;
1260
+ if (!link.ready) {
1261
+ markOffline(link, `Timed out connecting to "${link.config.name}".`);
1262
+ }
1263
+ }, connectTimeoutMs);
1264
+ const getEndpoint = link.config.getEndpoint;
1103
1265
  if (!getEndpoint) {
1104
- openSocket({ url: options.url, credential: { token: options.token } });
1266
+ openLinkSocket(link, {
1267
+ url: link.config.url,
1268
+ credential: { token: link.config.token }
1269
+ });
1270
+ rebuildView();
1105
1271
  return;
1106
1272
  }
1107
1273
  void getEndpoint().then(
1108
1274
  (endpoint) => {
1109
- if (closed) return;
1110
- openSocket(endpoint);
1275
+ if (closed || link.status.state !== "connecting" || link.socket !== null) return;
1276
+ openLinkSocket(link, endpoint);
1277
+ rebuildView();
1111
1278
  },
1112
1279
  (error) => {
1113
- if (closed) return;
1114
- if (error instanceof TerminalEndpointError) {
1115
- terminate();
1116
- options.onDetached?.(error.reason);
1117
- return;
1118
- }
1119
- scheduleReconnect();
1280
+ if (closed || link.status.state !== "connecting") return;
1281
+ markOffline(
1282
+ link,
1283
+ `Endpoint for "${link.config.name}" unavailable: ${error instanceof Error ? error.message : String(error)}`
1284
+ );
1120
1285
  }
1121
1286
  );
1287
+ rebuildView();
1122
1288
  };
1123
- connect();
1289
+ const linkFor = (server) => {
1290
+ const link = links.get(server);
1291
+ if (!link) return new Error(`Unknown server "${server}".`);
1292
+ if (closed || !link.ready || link.socket === null) {
1293
+ return new Error(`Server "${server}" is not connected.`);
1294
+ }
1295
+ return link;
1296
+ };
1297
+ for (const config of options.servers) {
1298
+ if (!config.getEndpoint && (config.url === void 0 || config.token === void 0)) {
1299
+ throw new Error(`Server "${config.name}" needs either url+token or a getEndpoint provider.`);
1300
+ }
1301
+ links.set(config.name, {
1302
+ config,
1303
+ status: { state: "connecting" },
1304
+ socket: null,
1305
+ ready: false,
1306
+ pending: /* @__PURE__ */ new Map(),
1307
+ timer: null,
1308
+ commandCounter: 0
1309
+ });
1310
+ }
1311
+ rebuildView();
1312
+ for (const link of links.values()) {
1313
+ connect(link);
1314
+ }
1124
1315
  return {
1125
- send(command) {
1126
- if (closed || phase !== "attached" || socket === null) return;
1127
- socket.send(JSON.stringify(command));
1316
+ getView() {
1317
+ return view;
1128
1318
  },
1129
- onFrame(listener) {
1130
- frameListeners.add(listener);
1319
+ subscribe(listener) {
1320
+ listeners.add(listener);
1131
1321
  return () => {
1132
- frameListeners.delete(listener);
1322
+ listeners.delete(listener);
1133
1323
  };
1134
1324
  },
1135
- close() {
1325
+ refresh() {
1136
1326
  if (closed) return;
1137
- terminate();
1138
- frameListeners.clear();
1139
- stateListeners.clear();
1327
+ for (const link of links.values()) {
1328
+ if (link.status.state === "offline") {
1329
+ connect(link);
1330
+ } else if (link.ready) {
1331
+ sendList(link);
1332
+ }
1333
+ }
1140
1334
  },
1141
- connectionState() {
1142
- return state;
1335
+ setServers(configs) {
1336
+ if (closed) return;
1337
+ for (const config of configs) {
1338
+ if (!links.has(config.name) && !config.getEndpoint) {
1339
+ if (config.url === void 0 || config.token === void 0) {
1340
+ throw new Error(
1341
+ `Server "${config.name}" needs either url+token or a getEndpoint provider.`
1342
+ );
1343
+ }
1344
+ }
1345
+ }
1346
+ const nextNames = new Set(configs.map((config) => config.name));
1347
+ for (const [name, link] of links) {
1348
+ if (nextNames.has(name)) continue;
1349
+ clearTimer(link);
1350
+ rejectPending(link, `Server "${name}" was removed from the directory.`);
1351
+ const socket = link.socket;
1352
+ link.socket = null;
1353
+ socket?.close();
1354
+ links.delete(name);
1355
+ }
1356
+ const added = [];
1357
+ for (const config of configs) {
1358
+ const existing = links.get(config.name);
1359
+ if (existing) {
1360
+ existing.config = config;
1361
+ continue;
1362
+ }
1363
+ const link = {
1364
+ config,
1365
+ status: { state: "connecting" },
1366
+ socket: null,
1367
+ ready: false,
1368
+ pending: /* @__PURE__ */ new Map(),
1369
+ timer: null,
1370
+ commandCounter: 0
1371
+ };
1372
+ links.set(config.name, link);
1373
+ added.push(link);
1374
+ }
1375
+ rebuildView();
1376
+ for (const link of added) {
1377
+ connect(link);
1378
+ }
1143
1379
  },
1144
- onConnectionState(listener) {
1145
- stateListeners.add(listener);
1146
- return () => {
1147
- stateListeners.delete(listener);
1148
- };
1380
+ createSession(server, createOptions) {
1381
+ return new Promise((resolve, reject) => {
1382
+ const link = linkFor(server);
1383
+ if (link instanceof Error) {
1384
+ reject(link);
1385
+ return;
1386
+ }
1387
+ link.commandCounter += 1;
1388
+ const id = `dir-${link.commandCounter}`;
1389
+ link.pending.set(id, { kind: "create", resolve, reject });
1390
+ link.socket?.send(
1391
+ JSON.stringify({
1392
+ id,
1393
+ type: "create-session",
1394
+ cwd: createOptions.cwd,
1395
+ ...createOptions.name !== void 0 ? { name: createOptions.name } : {},
1396
+ ...createOptions.workspace !== void 0 ? { workspace: createOptions.workspace } : {},
1397
+ ...createOptions.profile !== void 0 ? { profile: createOptions.profile } : {},
1398
+ ...createOptions.headless === true ? { headless: true } : {},
1399
+ ...createOptions.approvalMode !== void 0 ? { approvalMode: createOptions.approvalMode } : {},
1400
+ ...createOptions.activeProvider !== void 0 ? { activeProvider: createOptions.activeProvider } : {},
1401
+ ...createOptions.activeModel !== void 0 ? { activeModel: createOptions.activeModel } : {}
1402
+ })
1403
+ );
1404
+ });
1405
+ },
1406
+ resumeSession(server, historyId, resumeOptions) {
1407
+ return new Promise((resolve, reject) => {
1408
+ const link = linkFor(server);
1409
+ if (link instanceof Error) {
1410
+ reject(link);
1411
+ return;
1412
+ }
1413
+ link.commandCounter += 1;
1414
+ const id = `dir-${link.commandCounter}`;
1415
+ link.pending.set(id, { kind: "resume", resolve, reject });
1416
+ link.socket?.send(
1417
+ JSON.stringify({
1418
+ id,
1419
+ type: "resume-session",
1420
+ historyId,
1421
+ cwd: resumeOptions.cwd,
1422
+ ...resumeOptions.name !== void 0 ? { name: resumeOptions.name } : {},
1423
+ ...resumeOptions.workspace !== void 0 ? { workspace: resumeOptions.workspace } : {},
1424
+ ...resumeOptions.profile !== void 0 ? { profile: resumeOptions.profile } : {}
1425
+ })
1426
+ );
1427
+ });
1428
+ },
1429
+ killSession(server, sessionId, options2) {
1430
+ return new Promise((resolve, reject) => {
1431
+ const link = linkFor(server);
1432
+ if (link instanceof Error) {
1433
+ reject(link);
1434
+ return;
1435
+ }
1436
+ link.commandCounter += 1;
1437
+ const id = `dir-${link.commandCounter}`;
1438
+ link.pending.set(id, { kind: "kill", resolve, reject });
1439
+ link.socket?.send(
1440
+ JSON.stringify({ id, type: "kill-session", sessionId, delete: options2?.delete })
1441
+ );
1442
+ });
1443
+ },
1444
+ completePath(server, prefix) {
1445
+ return new Promise((resolve, reject) => {
1446
+ const link = linkFor(server);
1447
+ if (link instanceof Error) {
1448
+ reject(link);
1449
+ return;
1450
+ }
1451
+ link.commandCounter += 1;
1452
+ const id = `dir-${link.commandCounter}`;
1453
+ link.pending.set(id, { kind: "complete", resolve, reject });
1454
+ link.socket?.send(JSON.stringify({ id, type: "complete-path", prefix }));
1455
+ });
1456
+ },
1457
+ close() {
1458
+ if (closed) return;
1459
+ closed = true;
1460
+ for (const link of links.values()) {
1461
+ clearTimer(link);
1462
+ rejectPending(link, "ServerDirectory closed.");
1463
+ const socket = link.socket;
1464
+ link.socket = null;
1465
+ socket?.close();
1466
+ }
1467
+ listeners.clear();
1149
1468
  }
1150
1469
  };
1151
1470
  }
1152
1471
  export {
1153
1472
  TerminalEndpointError,
1154
1473
  adaptWhatwgSocket,
1474
+ bindDaemonDataClient,
1475
+ createDaemonClient,
1155
1476
  createHttpDataClient,
1156
1477
  createServerDirectory,
1157
1478
  createSessionClient,