@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.
- package/dist/chunk-7HGJO7ZX.js +9 -0
- package/dist/chunk-HJLRL64D.js +13 -0
- package/dist/{chunk-3IPZO7LP.js → chunk-TUNCGICQ.js} +19 -1
- package/dist/{chunk-FR3R66RD.js → chunk-WAMOWOCO.js} +20 -2
- package/dist/{chunk-3J44LBXZ.js → chunk-WB4FEWBI.js} +1 -1
- package/dist/client/daemon-client.d.ts +50 -0
- package/dist/client/daemon-client.d.ts.map +1 -0
- package/dist/client/data-client.d.ts +21 -3
- package/dist/client/data-client.d.ts.map +1 -1
- package/dist/client/directory.d.ts +7 -12
- package/dist/client/directory.d.ts.map +1 -1
- package/dist/client/http-data-client.d.ts +2 -0
- package/dist/client/http-data-client.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1300 -979
- package/dist/client/session-client.d.ts +19 -6
- package/dist/client/session-client.d.ts.map +1 -1
- package/dist/client/transport.d.ts +18 -2
- package/dist/client/transport.d.ts.map +1 -1
- package/dist/client/websocket.d.ts +5 -0
- package/dist/client/websocket.d.ts.map +1 -1
- package/dist/controller/index.js +129 -40
- package/dist/controller/session-controller.d.ts +18 -5
- package/dist/controller/session-controller.d.ts.map +1 -1
- package/dist/grant/index.d.ts +1 -1
- package/dist/grant/index.d.ts.map +1 -1
- package/dist/grant/index.js +2 -1
- package/dist/protocol/data-rpc.d.ts +70 -3
- package/dist/protocol/data-rpc.d.ts.map +1 -1
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +12 -3
- package/dist/protocol/model-identity.d.ts +7 -0
- package/dist/protocol/model-identity.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +176 -83
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/state/approvals.d.ts +6 -3
- package/dist/state/approvals.d.ts.map +1 -1
- package/dist/state/index.js +5 -1
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/tool-preview.d.ts.map +1 -1
- package/dist/state/tool-preview.js +50 -1
- package/dist/test-utils/fake-relay.d.ts.map +1 -1
- package/dist/test-utils/index.js +6 -4
- package/package.json +5 -4
- package/dist/chunk-NN43LVAS.js +0 -0
package/dist/client/index.js
CHANGED
|
@@ -1,935 +1,1096 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSessionViewState,
|
|
3
3
|
reduceSessionEvent
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-WAMOWOCO.js";
|
|
5
5
|
import {
|
|
6
6
|
PROTOCOL_VERSION
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-WB4FEWBI.js";
|
|
8
8
|
|
|
9
|
-
// src/client/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
const
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
78
|
+
);
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
await throwFromResponse(
|
|
81
|
+
response,
|
|
82
|
+
"Authentication required",
|
|
83
|
+
void 0,
|
|
84
|
+
options.onUnauthorized
|
|
85
|
+
);
|
|
153
86
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 (
|
|
178
|
-
|
|
179
|
-
return;
|
|
139
|
+
if (response.status === 401) {
|
|
140
|
+
throwUnauthorized();
|
|
180
141
|
}
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
142
|
+
if (response.status === 404) {
|
|
143
|
+
throw new TerminalEndpointError(
|
|
144
|
+
"daemon-revoked",
|
|
145
|
+
`Daemon "${daemonId}" is no longer registered.`
|
|
146
|
+
);
|
|
184
147
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
return
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
407
|
-
return
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
433
|
-
return
|
|
434
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
462
|
+
);
|
|
463
|
+
},
|
|
464
|
+
abort() {
|
|
465
|
+
return sendCommand((id) => ({ id, type: "abort" })).then(() => {
|
|
443
466
|
});
|
|
444
467
|
},
|
|
445
|
-
|
|
446
|
-
return
|
|
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
|
-
|
|
459
|
-
return
|
|
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
|
-
|
|
474
|
-
return
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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/
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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 (
|
|
747
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
listener(frame);
|
|
762
|
-
} catch {
|
|
763
|
-
}
|
|
774
|
+
if (message.type === "snapshot") {
|
|
775
|
+
lastSeq = message.snapshot.seq;
|
|
776
|
+
deliver(message);
|
|
777
|
+
return;
|
|
764
778
|
}
|
|
765
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
return
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
848
|
+
onFrame(listener) {
|
|
849
|
+
frameListeners.add(listener);
|
|
850
|
+
return () => {
|
|
851
|
+
frameListeners.delete(listener);
|
|
852
|
+
};
|
|
874
853
|
},
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
);
|
|
854
|
+
close() {
|
|
855
|
+
if (closed) return;
|
|
856
|
+
terminate();
|
|
857
|
+
frameListeners.clear();
|
|
858
|
+
stateListeners.clear();
|
|
859
|
+
undeliverableListeners.clear();
|
|
879
860
|
},
|
|
880
|
-
|
|
881
|
-
return
|
|
882
|
-
(result) => result
|
|
883
|
-
);
|
|
861
|
+
connectionState() {
|
|
862
|
+
return state;
|
|
884
863
|
},
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
864
|
+
onConnectionState(listener) {
|
|
865
|
+
stateListeners.add(listener);
|
|
866
|
+
return () => {
|
|
867
|
+
stateListeners.delete(listener);
|
|
868
|
+
};
|
|
889
869
|
},
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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/
|
|
904
|
-
|
|
905
|
-
function
|
|
906
|
-
|
|
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
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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/
|
|
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
|
|
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
|
|
958
|
-
const
|
|
959
|
-
const
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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(
|
|
1130
|
+
listener();
|
|
976
1131
|
} catch {
|
|
977
1132
|
}
|
|
978
1133
|
}
|
|
979
1134
|
};
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
995
|
-
socket = null;
|
|
996
|
-
current?.close();
|
|
997
|
-
setState("closed");
|
|
1146
|
+
link.pending.clear();
|
|
998
1147
|
};
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
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 === "
|
|
1021
|
-
|
|
1022
|
-
|
|
1173
|
+
if (message.type === "welcome") {
|
|
1174
|
+
link.ready = true;
|
|
1175
|
+
clearTimer(link);
|
|
1176
|
+
rebuildView();
|
|
1177
|
+
sendList(link);
|
|
1023
1178
|
return;
|
|
1024
1179
|
}
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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 (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1058
|
-
return;
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
socket
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1087
|
-
if (closed || socket !==
|
|
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
|
-
|
|
1242
|
+
socket.on("error", () => {
|
|
1092
1243
|
});
|
|
1093
|
-
|
|
1094
|
-
if (socket !==
|
|
1095
|
-
socket = null;
|
|
1244
|
+
socket.on("close", () => {
|
|
1245
|
+
if (link.socket !== socket) return;
|
|
1246
|
+
link.socket = null;
|
|
1096
1247
|
if (closed) return;
|
|
1097
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
socket.send(JSON.stringify(command));
|
|
1316
|
+
getView() {
|
|
1317
|
+
return view;
|
|
1128
1318
|
},
|
|
1129
|
-
|
|
1130
|
-
|
|
1319
|
+
subscribe(listener) {
|
|
1320
|
+
listeners.add(listener);
|
|
1131
1321
|
return () => {
|
|
1132
|
-
|
|
1322
|
+
listeners.delete(listener);
|
|
1133
1323
|
};
|
|
1134
1324
|
},
|
|
1135
|
-
|
|
1325
|
+
refresh() {
|
|
1136
1326
|
if (closed) return;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1142
|
-
return
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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,
|