skydive-cli 0.1.0-beta.239 → 0.1.0-beta.276

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+ import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
3
+ import { a as buildEnv, g as errorMessage, i as mintPortalDeviceToken, n as findThisDevice, o as machineIdentity, r as grantPortalAccess, s as portalWsUrl, t as fetchPortalDevices } from "./bin.mjs";
4
+ import { z } from "zod";
5
+ import { spawn } from "node:child_process";
6
+ import { WebSocket } from "ws";
7
+
8
+ //#region ../portal-protocol/src/index.ts
9
+ const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
10
+ const T_DATA = 1;
11
+ const T_CTRL = 2;
12
+ const STREAM = {
13
+ stdout: 0,
14
+ stderr: 1,
15
+ stdin: 2
16
+ };
17
+ const uuidToBytes = (id) => Buffer.from(id.replace(/-/g, ""), "hex");
18
+ const bytesToUuid = (b) => {
19
+ const h = b.toString("hex");
20
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
21
+ };
22
+ function encodeData(id, stream, seq, payload) {
23
+ const head = Buffer.allocUnsafe(22);
24
+ head[0] = T_DATA;
25
+ uuidToBytes(id).copy(head, 1);
26
+ head[17] = stream;
27
+ head.writeUInt32BE(seq >>> 0, 18);
28
+ return Buffer.concat([head, payload]);
29
+ }
30
+ const ctrlMessageSchema = z.discriminatedUnion("t", [
31
+ z.object({
32
+ t: z.literal("open"),
33
+ argv: z.array(z.string()),
34
+ env: z.record(z.string()).nullable()
35
+ }),
36
+ z.object({ t: z.literal("stdin_eof") }),
37
+ z.object({ t: z.literal("pause") }),
38
+ z.object({ t: z.literal("resume") }),
39
+ z.object({ t: z.literal("cancel") }),
40
+ z.object({
41
+ t: z.literal("close"),
42
+ exitCode: z.number()
43
+ }),
44
+ z.object({
45
+ t: z.literal("error"),
46
+ message: z.string()
47
+ })
48
+ ]);
49
+ function encodeCtrl(id, obj) {
50
+ const head = Buffer.allocUnsafe(17);
51
+ head[0] = T_CTRL;
52
+ uuidToBytes(id).copy(head, 1);
53
+ return Buffer.concat([head, Buffer.from(JSON.stringify(obj), "utf8")]);
54
+ }
55
+ function decodeFrame(frame) {
56
+ const id = bytesToUuid(frame.subarray(1, 17));
57
+ if (frame[0] === T_DATA) return {
58
+ kind: "data",
59
+ id,
60
+ stream: frame[17] ?? 0,
61
+ seq: frame.readUInt32BE(18),
62
+ payload: frame.subarray(22)
63
+ };
64
+ return {
65
+ kind: "ctrl",
66
+ id,
67
+ obj: ctrlMessageSchema.parse(JSON.parse(frame.subarray(17).toString("utf8")))
68
+ };
69
+ }
70
+
71
+ //#endregion
72
+ //#region src/chat/portal/exec.ts
73
+ /**
74
+ * Runs portal `exec` directives locally. Each `open` spawns a child process
75
+ * whose stdout/stderr stream back as data frames and whose stdin is fed by
76
+ * inbound data frames, with pause/resume backpressure and cancel/teardown that
77
+ * kill the child. This is the TypeScript counterpart of the desktop's Rust
78
+ * `portal/mod.rs` job machinery, minus the connection supervision (which lives
79
+ * in the client).
80
+ */
81
+ var JobManager = class {
82
+ jobs = /* @__PURE__ */ new Map();
83
+ constructor(opts) {
84
+ this.opts = opts;
85
+ }
86
+ handleFrame(raw) {
87
+ let decoded;
88
+ try {
89
+ decoded = decodeFrame(raw);
90
+ } catch (_error) {
91
+ return;
92
+ }
93
+ if (decoded.kind === "ctrl") this.handleCtrl(decoded.id, decoded.obj);
94
+ else if (decoded.stream === STREAM.stdin) this.jobs.get(decoded.id)?.child.stdin.write(decoded.payload);
95
+ }
96
+ killAll() {
97
+ for (const job of this.jobs.values()) {
98
+ job.settled = true;
99
+ job.child.kill("SIGKILL");
100
+ }
101
+ this.jobs.clear();
102
+ }
103
+ handleCtrl(id, msg) {
104
+ switch (msg.t) {
105
+ case "open":
106
+ this.startJob(id, msg.argv, msg.env);
107
+ return;
108
+ case "stdin_eof":
109
+ this.jobs.get(id)?.child.stdin.end();
110
+ return;
111
+ case "pause":
112
+ this.setPaused(id, true);
113
+ return;
114
+ case "resume":
115
+ this.setPaused(id, false);
116
+ return;
117
+ case "cancel":
118
+ this.jobs.get(id)?.child.kill("SIGKILL");
119
+ return;
120
+ case "close":
121
+ case "error": return;
122
+ default: return msg;
123
+ }
124
+ }
125
+ setPaused(id, paused) {
126
+ const job = this.jobs.get(id);
127
+ if (!job) return;
128
+ if (paused) {
129
+ job.child.stdout.pause();
130
+ job.child.stderr.pause();
131
+ } else {
132
+ job.child.stdout.resume();
133
+ job.child.stderr.resume();
134
+ }
135
+ }
136
+ startJob(id, argv, env) {
137
+ const [program, ...args] = argv;
138
+ if (!program) {
139
+ this.opts.send(encodeCtrl(id, {
140
+ t: "error",
141
+ message: "empty argv"
142
+ }));
143
+ return;
144
+ }
145
+ let child;
146
+ try {
147
+ child = spawn(program, args, {
148
+ cwd: this.opts.cwd,
149
+ env: buildEnv(env),
150
+ stdio: [
151
+ "pipe",
152
+ "pipe",
153
+ "pipe"
154
+ ]
155
+ });
156
+ } catch (err) {
157
+ this.opts.send(encodeCtrl(id, {
158
+ t: "error",
159
+ message: `spawn failed: ${errorMessage(err)}`
160
+ }));
161
+ return;
162
+ }
163
+ const job = {
164
+ child,
165
+ seq: 0,
166
+ settled: false
167
+ };
168
+ this.jobs.set(id, job);
169
+ child.on("error", (err) => {
170
+ if (job.settled) return;
171
+ job.settled = true;
172
+ this.jobs.delete(id);
173
+ this.opts.send(encodeCtrl(id, {
174
+ t: "error",
175
+ message: `spawn failed: ${errorMessage(err)}`
176
+ }));
177
+ });
178
+ child.stdout.on("data", (chunk) => this.sendData(job, id, STREAM.stdout, chunk));
179
+ child.stderr.on("data", (chunk) => this.sendData(job, id, STREAM.stderr, chunk));
180
+ child.on("close", (code) => {
181
+ if (job.settled) return;
182
+ job.settled = true;
183
+ this.jobs.delete(id);
184
+ this.opts.send(encodeCtrl(id, {
185
+ t: "close",
186
+ exitCode: code ?? -1
187
+ }));
188
+ });
189
+ }
190
+ sendData(job, id, stream, chunk) {
191
+ if (job.settled) return;
192
+ this.opts.send(encodeData(id, stream, job.seq, chunk));
193
+ job.seq = job.seq + 1 >>> 0;
194
+ }
195
+ };
196
+
197
+ //#endregion
198
+ //#region src/chat/portal/client.ts
199
+ var client_exports = /* @__PURE__ */ __exportAll({ PortalClient: () => PortalClient });
200
+ const INITIAL_BACKOFF_MS = 500;
201
+ const MAX_BACKOFF_MS = 1e4;
202
+ /**
203
+ * Shares the local machine with agents over the portal: dials OUT to the api's
204
+ * desktop-portal WebSocket (authenticating with a short-lived device token
205
+ * minted from the CLI session), then runs inbound `exec` directives via a
206
+ * `JobManager`. Reconnects with backoff while enabled; disabling drops presence
207
+ * and kills any in-flight children. No inbound port is ever opened.
208
+ *
209
+ * Access stays default-deny: connecting only makes the machine reachable — an
210
+ * agent can't run anything until the user grants it (`grantAgent`).
211
+ */
212
+ var PortalClient = class {
213
+ enabled = false;
214
+ disposed = false;
215
+ ws = null;
216
+ jobs = null;
217
+ status = "off";
218
+ error = null;
219
+ deviceId = null;
220
+ granted = /* @__PURE__ */ new Set();
221
+ machineName;
222
+ friendlyName;
223
+ constructor(opts) {
224
+ this.opts = opts;
225
+ const identity = machineIdentity();
226
+ this.machineName = identity.machineName;
227
+ this.friendlyName = identity.friendlyName;
228
+ }
229
+ isEnabled() {
230
+ return this.enabled;
231
+ }
232
+ isGranted(agentId) {
233
+ return this.granted.has(agentId);
234
+ }
235
+ enable() {
236
+ if (this.enabled || this.disposed) return;
237
+ this.enabled = true;
238
+ this.error = null;
239
+ this.connectLoop();
240
+ }
241
+ disable() {
242
+ if (!this.enabled) return;
243
+ this.enabled = false;
244
+ this.jobs?.killAll();
245
+ this.ws?.close();
246
+ this.ws = null;
247
+ this.deviceId = null;
248
+ this.granted = /* @__PURE__ */ new Set();
249
+ this.setStatus("off");
250
+ }
251
+ /**
252
+ * Tear down for good (app quit). Kills children synchronously and closes the
253
+ * socket so it stops holding the event loop open — otherwise the process
254
+ * would hang after the TUI is destroyed.
255
+ */
256
+ dispose() {
257
+ this.disposed = true;
258
+ this.enabled = false;
259
+ this.jobs?.killAll();
260
+ this.jobs = null;
261
+ this.ws?.close();
262
+ this.ws = null;
263
+ }
264
+ /** Grant one agent access to this machine (default-deny; user-initiated). */
265
+ async grantAgent(agentId) {
266
+ const deviceId = await this.ensureDeviceId();
267
+ await grantPortalAccess(this.opts, {
268
+ deviceId,
269
+ agentId
270
+ });
271
+ this.granted.add(agentId);
272
+ this.emit();
273
+ }
274
+ setStatus(status, error = null) {
275
+ this.status = status;
276
+ this.error = error;
277
+ this.emit();
278
+ }
279
+ emit() {
280
+ this.opts.onState({
281
+ status: this.status,
282
+ machineName: this.machineName,
283
+ friendlyName: this.friendlyName,
284
+ error: this.error,
285
+ grantedAgentIds: [...this.granted]
286
+ });
287
+ }
288
+ async connectLoop() {
289
+ let backoff = INITIAL_BACKOFF_MS;
290
+ while (this.enabled && !this.disposed) {
291
+ this.setStatus("connecting");
292
+ try {
293
+ const token = await mintPortalDeviceToken(this.opts);
294
+ await this.runConnection(token);
295
+ backoff = INITIAL_BACKOFF_MS;
296
+ } catch (err) {
297
+ if (!this.enabled || this.disposed) break;
298
+ this.setStatus("error", errorMessage(err));
299
+ }
300
+ if (!this.enabled || this.disposed) break;
301
+ await sleep(backoff);
302
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
303
+ }
304
+ }
305
+ runConnection(token) {
306
+ return new Promise((resolve) => {
307
+ const ws = new WebSocket(portalWsUrl(this.opts.appUrl, this.machineName, this.friendlyName), {
308
+ headers: { authorization: `Bearer ${token}` },
309
+ maxPayload: MAX_WS_FRAME_BYTES
310
+ });
311
+ this.ws = ws;
312
+ const jobs = new JobManager({
313
+ cwd: this.opts.cwd,
314
+ send: (frame) => {
315
+ if (ws.readyState === WebSocket.OPEN) ws.send(frame);
316
+ }
317
+ });
318
+ this.jobs = jobs;
319
+ ws.on("open", () => {
320
+ this.setStatus("connected");
321
+ this.refreshDevice();
322
+ });
323
+ ws.on("message", (data, isBinary) => {
324
+ if (isBinary) jobs.handleFrame(toBuffer(data));
325
+ });
326
+ ws.on("error", (err) => {
327
+ this.error = errorMessage(err);
328
+ });
329
+ ws.on("close", () => {
330
+ jobs.killAll();
331
+ if (this.jobs === jobs) this.jobs = null;
332
+ if (this.ws === ws) this.ws = null;
333
+ resolve();
334
+ });
335
+ });
336
+ }
337
+ async ensureDeviceId() {
338
+ if (this.deviceId) return this.deviceId;
339
+ for (let attempt = 0; attempt < 10; attempt += 1) {
340
+ await this.refreshDevice();
341
+ if (this.deviceId) return this.deviceId;
342
+ await sleep(300);
343
+ }
344
+ throw new Error("this machine is not connected yet");
345
+ }
346
+ async refreshDevice() {
347
+ try {
348
+ const { devices } = await fetchPortalDevices(this.opts);
349
+ const mine = findThisDevice(devices, this.machineName);
350
+ if (!mine) return;
351
+ this.deviceId = mine.id;
352
+ this.granted = new Set(mine.grantedAgentIds);
353
+ this.emit();
354
+ } catch (_error) {}
355
+ }
356
+ };
357
+ function toBuffer(data) {
358
+ if (Buffer.isBuffer(data)) return data;
359
+ if (Array.isArray(data)) return Buffer.concat(data);
360
+ return Buffer.from(data);
361
+ }
362
+ function sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+
366
+ //#endregion
367
+ export { client_exports as n, PortalClient as t };
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ //#region \0rolldown/runtime.js
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) {
7
+ __defProp(target, name, {
8
+ get: all[name],
9
+ enumerable: true
10
+ });
11
+ }
12
+ if (!no_symbols) {
13
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ }
15
+ return target;
16
+ };
17
+
18
+ //#endregion
19
+ export { __exportAll as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.239",
3
+ "version": "0.1.0-beta.276",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "diff": "9.0.0",
34
34
  "eventsource-parser": "^3.0.8",
35
35
  "file-type": "^21.3.4",
36
+ "fuzzysort": "^3.1.0",
36
37
  "neverthrow": "^8.2.0",
37
38
  "open": "^10.1.0",
38
39
  "react": "^19.0.0",