skydive-cli 0.5.0-beta.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +61 -13
  3. package/dist/js/api-BFQ4PQDA.mjs +315 -0
  4. package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
  5. package/dist/js/bin.mjs +674 -307
  6. package/dist/js/{boot-BCClEpCM.mjs → boot-DD4T-61U.mjs} +4558 -930
  7. package/dist/js/chunk-BbwQpWto.mjs +33 -0
  8. package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
  9. package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
  10. package/dist/js/client-Ct0-JZSS.mjs +5 -0
  11. package/dist/js/daemon-CCgNLD0H.mjs +7 -0
  12. package/dist/js/{daemon-k2kVkJ8D.mjs → daemon-Do1jU2UF.mjs} +123 -43
  13. package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
  14. package/dist/js/{daemon-client-fxf1A25Z.mjs → daemon-client-Dvad009G.mjs} +1 -1
  15. package/dist/js/dist-CRtjM7ba.mjs +1750 -0
  16. package/dist/js/forward-C-f04uyE.mjs +208 -0
  17. package/dist/js/{profiler-DawY0V0Z.mjs → install-CtAVvERm.mjs} +545 -215
  18. package/dist/js/launcher.mjs +49 -0
  19. package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
  20. package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
  21. package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
  22. package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
  23. package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
  24. package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
  25. package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
  26. package/dist/js/rest-Dc0EEok3.mjs +6 -0
  27. package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
  28. package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
  29. package/package.json +12 -4
  30. package/dist/js/api-DG5W6iwx.mjs +0 -131
  31. package/dist/js/client-BuU34IVE.mjs +0 -5
  32. package/dist/js/daemon-BxU59xie.mjs +0 -6
  33. package/dist/js/daemon-client-C2BvZgKO.mjs +0 -7
  34. package/dist/js/forward-18QoL5dO.mjs +0 -68
  35. package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
  36. package/dist/js/rest-D29qNkto.mjs +0 -6
  37. /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
  38. /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
  39. /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
- import { a as billingBlockedOutcomeSchema } from "./billing-blocked-2wju4gC_.mjs";
2
+ import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
3
+ import { a as billingBlockedOutcomeSchema } from "./billing-blocked-D3l5kJlX.mjs";
4
4
  import { z } from "zod";
5
5
  import { createParser } from "eventsource-parser";
6
6
 
7
+ //#region package.json
8
+ var name = "skydive-cli";
9
+ var version = "0.5.0";
10
+
11
+ //#endregion
7
12
  //#region src/chat/util.ts
8
13
  /** Narrowing helper for the many `unknown` payloads the chat stream and
9
14
  * tool inputs/outputs carry. A type predicate (not an `as` cast), so call
@@ -18,8 +23,24 @@ function errorMessage(err) {
18
23
 
19
24
  //#endregion
20
25
  //#region src/chat/api/rest.ts
26
+ const CLI_VERSION_HEADER = "x-skydive-cli-version";
21
27
  const ERROR_DETAIL_MAX_BODY = 2e3;
22
28
  /**
29
+ * Pull the filename out of a `Content-Disposition` header. Handles the
30
+ * RFC 5987 `filename*=UTF-8''...` form (percent-decoded) and the plain
31
+ * quoted/bare `filename=` form, preferring the extended form when both are
32
+ * present (the API emits both for non-ASCII names). Returns '' when the header
33
+ * is absent or carries no filename, so callers fall back to a cached name.
34
+ */
35
+ function filenameFromContentDisposition(header) {
36
+ if (!header) return "";
37
+ const extended = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(header);
38
+ if (extended?.[1]) try {
39
+ return decodeURIComponent(extended[1].trim().replace(/^"|"$/g, ""));
40
+ } catch (_error) {}
41
+ return /filename="?([^";]+)"?/i.exec(header)?.[1]?.trim() ?? "";
42
+ }
43
+ /**
23
44
  * Fullest renderable text for a thrown value. `HttpError.message` clips the
24
45
  * response body to 200 chars (it flows into logs and one-line UIs); the
25
46
  * transcript renders errors collapsed to a single line, so it can afford the
@@ -104,7 +125,8 @@ const MAX_STREAM_RECONNECTS = 5;
104
125
  function createRestClient({ appUrl, sessionToken, workspaceId }) {
105
126
  const baseHeaders = {
106
127
  authorization: `Bearer ${sessionToken}`,
107
- accept: "application/json"
128
+ accept: "application/json",
129
+ [CLI_VERSION_HEADER]: version
108
130
  };
109
131
  if (workspaceId) baseHeaders["x-workspace-id"] = workspaceId;
110
132
  async function get(path, schema) {
@@ -124,6 +146,13 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
124
146
  if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
125
147
  return schema.parse(await res.json());
126
148
  }
149
+ /**
150
+ * The agent PATCH is a merge: only the keys present are written, so each
151
+ * caller sends exactly the settings it means to change.
152
+ */
153
+ function patchAgent(agentId, body) {
154
+ return post(`/api/v1/agents/${encodeURIComponent(agentId)}`, body, updateAgentResponseSchema, "PATCH");
155
+ }
127
156
  async function del(path) {
128
157
  const res = await fetch(`${appUrl}${path}`, {
129
158
  method: "DELETE",
@@ -140,7 +169,8 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
140
169
  try {
141
170
  const headers = {
142
171
  authorization: `Bearer ${sessionToken}`,
143
- accept: "text/event-stream"
172
+ accept: "text/event-stream",
173
+ [CLI_VERSION_HEADER]: version
144
174
  };
145
175
  if (workspaceId) headers["x-workspace-id"] = workspaceId;
146
176
  if (lastEventId) headers["last-event-id"] = lastEventId;
@@ -214,9 +244,19 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
214
244
  return all;
215
245
  },
216
246
  createAgent: async ({ name }) => {
217
- const { agent } = await post("/api/v1/agents", { name }, createAgentResponseSchema);
247
+ const { agent } = await post("/api/v1/agents", name == null ? {} : { name }, createAgentResponseSchema);
218
248
  return agent;
219
249
  },
250
+ createOnboardingConversation: async ({ agentId, projectDir, os }) => {
251
+ const result = await post(`/api/v1/agents/${encodeURIComponent(agentId)}/onboarding-conversation`, {
252
+ projectDir,
253
+ os
254
+ }, onboardingConversationResponseSchema);
255
+ return {
256
+ conversationId: result.conversationId,
257
+ runId: result.runId
258
+ };
259
+ },
220
260
  getAgent: async ({ agentId }) => {
221
261
  const { agent } = await get(`/api/v1/agents/${encodeURIComponent(agentId)}`, getAgentResponseSchema);
222
262
  return agent;
@@ -233,9 +273,31 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
233
273
  const { models } = await get("/api/v1/models", listModelsResponseSchema);
234
274
  return models;
235
275
  },
236
- updateAgentModel: async ({ agentId, model }) => {
237
- const { agent } = await post(`/api/v1/agents/${encodeURIComponent(agentId)}`, { model }, updateAgentResponseSchema, "PATCH");
238
- return { model: agent.model ?? null };
276
+ listModelEfforts: async () => {
277
+ const { modelConfigOverrides } = await get("/api/v1/models/preferred", modelEffortPreferencesResponseSchema);
278
+ const efforts = {};
279
+ for (const [modelId, config] of Object.entries(modelConfigOverrides ?? {})) if (config.thinkingLevel) efforts[modelId] = config.thinkingLevel;
280
+ return efforts;
281
+ },
282
+ setModelEffort: async ({ modelId, thinkingLevel }) => {
283
+ await post("/api/v1/models/config", {
284
+ modelId,
285
+ config: { thinkingLevel }
286
+ }, z.unknown(), "PATCH");
287
+ },
288
+ updateAgentModel: async ({ agentId, model, thinkingLevel }) => {
289
+ const { agent } = await patchAgent(agentId, {
290
+ model,
291
+ thinkingLevel
292
+ });
293
+ return {
294
+ model: agent.model ?? null,
295
+ thinkingLevel: agent.thinkingLevel ?? null
296
+ };
297
+ },
298
+ updateAgentEffort: async ({ agentId, thinkingLevel }) => {
299
+ const { agent } = await patchAgent(agentId, { thinkingLevel });
300
+ return { thinkingLevel: agent.thinkingLevel ?? null };
239
301
  },
240
302
  listConversations: async ({ agentId, limit, channels, archived, query, onPage }) => {
241
303
  const trimmedQuery = query?.trim();
@@ -260,9 +322,17 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
260
322
  } while (cursor && all.length < maxConversations);
261
323
  return limit ? all.slice(0, limit) : all;
262
324
  },
263
- listMessages: async ({ conversationId }) => {
264
- const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
265
- return messages;
325
+ listMessages: async ({ conversationId, limit, before }) => {
326
+ const params = new URLSearchParams();
327
+ if (limit !== void 0) params.set("limit", String(limit));
328
+ if (before !== void 0) params.set("before", before);
329
+ const query = params.toString();
330
+ const { messages, hasMore, nextBefore } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages${query ? `?${query}` : ""}`, listMessagesResponseSchema);
331
+ return {
332
+ messages,
333
+ hasMore: hasMore ?? false,
334
+ nextBefore: nextBefore ?? null
335
+ };
266
336
  },
267
337
  listWorkspaceFiles: async ({ agentId }) => {
268
338
  const { files } = await get(`/api/v1/workspace-files?${new URLSearchParams({ agentId }).toString()}`, listWorkspaceFilesResponseSchema);
@@ -289,6 +359,13 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
289
359
  const { result } = await get(`/api/v1/trpc/v2.subagentTasks.list?input=${encodeURIComponent(JSON.stringify({ conversationId }))}`, subagentTasksResponseSchema);
290
360
  return new Map(result.data.subagentTasks.map((task) => [task.id, task]));
291
361
  },
362
+ computeLiveStats: async (agentId) => {
363
+ const { stats } = await get(`/api/v1/compute/live-stats?agentId=${encodeURIComponent(agentId)}`, liveStatsResponseSchema);
364
+ return stats;
365
+ },
366
+ restartAgent: async (agentId) => {
367
+ await post(`/api/v1/agents/${encodeURIComponent(agentId)}/restart`, {}, z.object({ ok: z.boolean() }));
368
+ },
292
369
  uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
293
370
  const size = data.byteLength;
294
371
  const presign = await post("/api/v1/attachments/presign", {
@@ -316,6 +393,17 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
316
393
  sizeBytes: finalized.sizeBytes ?? size
317
394
  };
318
395
  },
396
+ downloadAttachment: async ({ attachmentId }) => {
397
+ const res = await fetch(`${appUrl}/api/v1/attachments/${encodeURIComponent(attachmentId)}/download?disposition=inline`, { headers: baseHeaders });
398
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
399
+ const data = new Uint8Array(await res.arrayBuffer());
400
+ const mediaType = res.headers.get("content-type") ?? "application/octet-stream";
401
+ return {
402
+ data,
403
+ fileName: filenameFromContentDisposition(res.headers.get("content-disposition")),
404
+ mediaType
405
+ };
406
+ },
319
407
  forkConversation: async ({ conversationId }) => {
320
408
  const { conversation } = await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, {}, forkConversationResponseSchema);
321
409
  return conversation;
@@ -329,6 +417,9 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
329
417
  setConversationArchived: async ({ conversationId, archived }) => {
330
418
  await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/archive`, { archived }, z.object({ archived: z.boolean() }));
331
419
  },
420
+ markConversationRead: async ({ conversationId }) => {
421
+ await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/read`, {}, z.object({ read: z.boolean() }));
422
+ },
332
423
  renameConversation: async ({ conversationId, title }) => {
333
424
  await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, { title }, z.object({ conversation: z.object({ id: z.string() }) }), "PATCH");
334
425
  },
@@ -350,6 +441,7 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
350
441
  const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
351
442
  return { authorizationUrl: authorizationUrl ?? null };
352
443
  },
444
+ decideComputeRequest: async ({ requestId, decision }) => post(`/api/v1/compute-requests/${encodeURIComponent(requestId)}/decision`, { decision }, computeDecisionResponseSchema),
353
445
  fulfillCredential: async ({ url, body }) => {
354
446
  const target = new URL(url, appUrl).toString();
355
447
  const res = await fetch(target, {
@@ -383,6 +475,7 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
383
475
  headers: {
384
476
  authorization: `Bearer ${sessionToken}`,
385
477
  accept: "text/event-stream",
478
+ [CLI_VERSION_HEADER]: version,
386
479
  ...workspaceId ? { "x-workspace-id": workspaceId } : {}
387
480
  },
388
481
  signal
@@ -434,23 +527,34 @@ const agentSummarySchema = z.object({
434
527
  createdAt: z.string(),
435
528
  creatorName: z.string().nullable().optional(),
436
529
  model: z.string().nullable().optional(),
437
- modelLocked: z.boolean().optional()
530
+ modelLocked: z.boolean().optional(),
531
+ thinkingLevel: z.string().nullable().optional()
438
532
  });
439
533
  const platformModelSchema = z.object({
440
534
  id: z.string(),
441
535
  displayName: z.string(),
442
536
  providerDisplay: z.string().optional(),
443
537
  reasoning: z.boolean().optional(),
444
- compliant: z.boolean().optional()
538
+ compliant: z.boolean().optional(),
539
+ thinkingLevels: z.array(z.string()).optional(),
540
+ thinkingLevelLabels: z.record(z.string(), z.string()).optional()
445
541
  }).passthrough();
446
542
  const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
447
- const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
543
+ const modelEffortPreferencesResponseSchema = z.object({ modelConfigOverrides: z.record(z.string(), z.object({ thinkingLevel: z.string().optional() })).optional() });
544
+ const updateAgentResponseSchema = z.object({ agent: z.object({
545
+ model: z.string().nullable().optional(),
546
+ thinkingLevel: z.string().nullable().optional()
547
+ }).passthrough() });
448
548
  const listAgentsResponseSchema = z.object({
449
549
  agents: z.array(agentSummarySchema),
450
550
  nextCursor: z.string().nullable().optional(),
451
551
  totalCount: z.number().nullable().optional()
452
552
  });
453
553
  const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
554
+ const onboardingConversationResponseSchema = z.object({
555
+ conversationId: z.string(),
556
+ runId: z.string().nullable()
557
+ });
454
558
  const getAgentResponseSchema = z.object({ agent: agentSummarySchema });
455
559
  const agentSuggestionSchema = z.object({ name: z.string() });
456
560
  const suggestAgentResponseSchema = z.object({ suggestion: agentSuggestionSchema.nullable() });
@@ -469,6 +573,7 @@ const conversationSummarySchema = z.object({
469
573
  channel: z.string().nullable(),
470
574
  channelLabel: z.string().nullable(),
471
575
  viewerArchivedAt: z.string().nullable().optional(),
576
+ unread: z.boolean().optional(),
472
577
  agent: conversationAgentSchema,
473
578
  agents: z.array(conversationAgentSchema).optional()
474
579
  });
@@ -479,7 +584,8 @@ const conversationDetailSchema = z.object({
479
584
  createdAt: z.string(),
480
585
  updatedAt: z.string(),
481
586
  channel: z.string().nullable(),
482
- channelLabel: z.string().nullable()
587
+ channelLabel: z.string().nullable(),
588
+ parentConversationId: z.string().uuid().nullable().optional()
483
589
  });
484
590
  const conversationTitleSchema = z.object({
485
591
  id: z.string().uuid(),
@@ -527,10 +633,27 @@ const subagentTaskSnapshotSchema = z.object({
527
633
  status: z.string(),
528
634
  title: z.string().nullable(),
529
635
  createdAt: z.string(),
530
- completedAt: z.string().nullable()
636
+ completedAt: z.string().nullable(),
637
+ childConversationId: z.string().nullish(),
638
+ childRunStartedAt: z.string().nullish(),
639
+ contextTokens: z.number().nullish(),
640
+ latestActivity: z.string().nullish()
531
641
  });
532
642
  const subagentTasksResponseSchema = z.object({ result: z.object({ data: z.object({ subagentTasks: z.array(subagentTaskSnapshotSchema) }) }) });
533
- const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
643
+ const liveStatsResponseSchema = z.object({ stats: z.object({
644
+ sampledAt: z.number(),
645
+ cpuUsedPct: z.number(),
646
+ cpuCount: z.number(),
647
+ memUsedBytes: z.number(),
648
+ memTotalBytes: z.number(),
649
+ diskUsedBytes: z.number(),
650
+ diskTotalBytes: z.number()
651
+ }).nullable() });
652
+ const listMessagesResponseSchema = z.object({
653
+ messages: z.array(uiMessageSchema),
654
+ hasMore: z.boolean().optional(),
655
+ nextBefore: z.string().nullish()
656
+ });
534
657
  const workspaceFileSchema = z.object({
535
658
  id: z.string(),
536
659
  path: z.string(),
@@ -564,6 +687,7 @@ const finalizeResponseSchema = z.object({
564
687
  });
565
688
  const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
566
689
  const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
690
+ const computeDecisionResponseSchema = z.object({ status: z.string() }).passthrough();
567
691
  const runStreamEventSchema = z.union([z.object({
568
692
  kind: z.literal("chunk"),
569
693
  chunk: z.record(z.unknown())
@@ -584,7 +708,8 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [
584
708
  kind: z.literal("agent"),
585
709
  id: z.string(),
586
710
  model: z.string().nullable(),
587
- modelLocked: z.boolean()
711
+ modelLocked: z.boolean(),
712
+ thinkingLevel: z.string().nullish()
588
713
  }),
589
714
  z.object({
590
715
  kind: z.literal("run"),
@@ -602,4 +727,4 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [
602
727
  ]);
603
728
 
604
729
  //#endregion
605
- export { isRecord as a, errorMessage as i, errorDetail as n, sendErrorMessage as r, createRestClient as t };
730
+ export { errorMessage as a, version as c, sendErrorMessage as i, errorDetail as n, isRecord as o, filenameFromContentDisposition as r, name as s, createRestClient as t };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { i as sendErrorMessage, n as errorDetail, r as filenameFromContentDisposition, t as createRestClient } from "./rest-B9__Zsuk.mjs";
3
+ import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
+ import "./billing-blocked-D3l5kJlX.mjs";
5
+
6
+ export { createRestClient };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { n as envTlsCertSource, r as warnOnRebindProtection, t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
3
+
4
+ export { defaultTlsCertSource };
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import dns from "node:dns";
4
+
5
+ //#region ../portal-daemon/src/tls-cert.ts
6
+ /**
7
+ * File-backed cert source, configured by environment:
8
+ * SKYDIVE_PORTAL_TLS_KEY / SKYDIVE_PORTAL_TLS_CERT (PEM file paths) and
9
+ * SKYDIVE_PORTAL_TLS_HOST (the name the cert covers). All three or nothing —
10
+ * a partial configuration is reported once and treated as absent, so a typo
11
+ * degrades to today's behavior instead of a crash loop.
12
+ */
13
+ function envTlsCertSource(env, log) {
14
+ return async () => {
15
+ const keyPath = env.SKYDIVE_PORTAL_TLS_KEY;
16
+ const certPath = env.SKYDIVE_PORTAL_TLS_CERT;
17
+ const hostname = env.SKYDIVE_PORTAL_TLS_HOST;
18
+ if (!keyPath && !certPath && !hostname) return null;
19
+ if (!keyPath || !certPath || !hostname) {
20
+ log("forward: SKYDIVE_PORTAL_TLS_KEY, SKYDIVE_PORTAL_TLS_CERT and SKYDIVE_PORTAL_TLS_HOST must all be set to enable the TLS origin; continuing with http only");
21
+ return null;
22
+ }
23
+ try {
24
+ const [key, cert] = await Promise.all([readFile(keyPath, "utf8"), readFile(certPath, "utf8")]);
25
+ return {
26
+ key,
27
+ cert,
28
+ hostname
29
+ };
30
+ } catch (err) {
31
+ log(`forward: could not read TLS key/cert (${err instanceof Error ? err.message : String(err)}); continuing with http only`);
32
+ return null;
33
+ }
34
+ };
35
+ }
36
+ /**
37
+ * The default cert source for both tunnel surfaces: an explicit env/file
38
+ * configuration wins (BYO cert — also the escape hatch for machines where the
39
+ * trust prompt is unwanted, e.g. CI), otherwise the fully automatic
40
+ * devcert-backed localhost source.
41
+ */
42
+ function defaultTlsCertSource(env, log) {
43
+ const fromEnv = envTlsCertSource(env, log);
44
+ return async () => {
45
+ if (env.SKYDIVE_PORTAL_TLS_KEY || env.SKYDIVE_PORTAL_TLS_CERT || env.SKYDIVE_PORTAL_TLS_HOST) return fromEnv();
46
+ const { localhostCertSource } = await import("./localhost-cert-Bn-UBUmj.mjs");
47
+ return localhostCertSource(log)();
48
+ };
49
+ }
50
+ /**
51
+ * Detect resolver-level DNS rebind protection: some routers/resolvers refuse
52
+ * public names that answer with loopback addresses (Plex documents this exact
53
+ * failure for *.plex.direct). The https origin simply won't resolve for such
54
+ * users — warn with the cause, and leave the http origin (unaffected) as the
55
+ * fallback. Never fatal: the browser may use a different resolver than the
56
+ * OS, so we start the listener regardless.
57
+ */
58
+ async function warnOnRebindProtection(hostname, log) {
59
+ try {
60
+ if (!(await dns.promises.lookup(hostname, { all: true })).some((a) => a.address === "127.0.0.1" || a.address === "::1")) log(`forward: ${hostname} did not resolve to 127.0.0.1 — a resolver on this network may block public names pointing at loopback (DNS rebind protection). The https origin may not work here; http://localhost is unaffected.`);
61
+ } catch (error) {
62
+ log(`forward: ${hostname} did not resolve (${error instanceof Error ? error.message : String(error)}) — a resolver on this network may block public names pointing at loopback (DNS rebind protection). The https origin may not work here; http://localhost is unaffected.`);
63
+ }
64
+ }
65
+
66
+ //#endregion
67
+ export { envTlsCertSource as n, warnOnRebindProtection as r, defaultTlsCertSource as t };
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.8",
3
+ "version": "0.5.0",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
7
7
  "bin": {
8
- "skydive": "./dist/js/bin.mjs"
8
+ "skydive": "./dist/js/launcher.mjs"
9
9
  },
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
13
  "type": "module",
14
14
  "exports": {
15
- "./bin": "./dist/js/bin.mjs"
15
+ "./bin": "./dist/js/bin.mjs",
16
+ "./launcher": "./dist/js/launcher.mjs"
16
17
  },
17
18
  "publishConfig": {
18
19
  "access": "public",
@@ -41,7 +42,7 @@
41
42
  "react-devtools-core": "^7.0.1",
42
43
  "safe-stable-stringify": "^2.3.1",
43
44
  "semver": "^7.7.4",
44
- "shell-quote": "1.9.0",
45
+ "tar": "7.5.21",
45
46
  "web-tree-sitter": "0.25.10",
46
47
  "ws": "^8.21.0",
47
48
  "yargs": "^17.7.2",
@@ -66,5 +67,12 @@
66
67
  "engines": {
67
68
  "bun": ">=1.3.14",
68
69
  "node": ">=20.0.0"
70
+ },
71
+ "//optionalDependencies": "The per-platform binary packages (skydive-cli-<os>-<arch>) are INTENTIONALLY not committed here. They are published by release-skydive-cli-binaries.yml and injected — pinned to the exact version being published — into the published launcher's optionalDependencies at release time (scripts/prepare-platform-packages.mjs --pin-version, run in release-skydive-cli.yml). Committing them would make `yarn install` try to resolve versions that only exist post-publish, breaking local dev and CI. npm installs of a published skydive-cli still get them (os/cpu-gated); the launcher (src/launcher.ts) resolves and execs the matching one. Local dev never needs them: it runs the JS bundle or `build:binary` directly.",
72
+ "optionalDependencies": {
73
+ "skydive-cli-darwin-arm64": "0.5.0",
74
+ "skydive-cli-darwin-x64": "0.5.0",
75
+ "skydive-cli-linux-x64": "0.5.0",
76
+ "skydive-cli-linux-arm64": "0.5.0"
69
77
  }
70
78
  }
@@ -1,131 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
- import { z } from "zod";
4
-
5
- //#region ../portal-daemon/src/api.ts
6
- /**
7
- * The portal's session-authed REST surface, shared by `PortalClient` (the
8
- * TUI/`portal open` connection) and the `skydive portal` management
9
- * commands, so the endpoint contracts and response schemas live in exactly
10
- * one place.
11
- */
12
- const deviceSchema = z.object({
13
- id: z.string(),
14
- machineName: z.string(),
15
- friendlyName: z.string(),
16
- connected: z.boolean(),
17
- lastSeen: z.string().nullable(),
18
- grantedAgentIds: z.array(z.string())
19
- });
20
- const devicesResponseSchema = z.object({
21
- devices: z.array(deviceSchema),
22
- agents: z.array(z.object({
23
- id: z.string(),
24
- name: z.string()
25
- }))
26
- });
27
- const deviceTokenSchema = z.object({ token: z.string().min(1) });
28
- async function portalFetch(auth, path, init) {
29
- const res = await fetch(`${auth.appUrl}${path}`, {
30
- method: init.method,
31
- headers: {
32
- authorization: `Bearer ${auth.sessionToken}`,
33
- accept: "application/json",
34
- ...init.body ? { "content-type": "application/json" } : {}
35
- },
36
- ...init.body ? { body: init.body } : {}
37
- });
38
- if (!res.ok) {
39
- const body = await res.text().catch(() => "");
40
- throw new HttpError(res.status, body);
41
- }
42
- return res.json();
43
- }
44
- async function fetchPortalDevices(auth) {
45
- const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
46
- return devicesResponseSchema.parse(json);
47
- }
48
- const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
49
- /**
50
- * Register this machine's device row without connecting. Connecting registers
51
- * as a side effect; this covers granting an agent on a machine that has never
52
- * shared yet (the grant references the device row).
53
- */
54
- async function registerPortalDevice(auth, { machineName, friendlyName }) {
55
- const json = await portalFetch(auth, "/api/v1/portal/devices", {
56
- method: "POST",
57
- body: JSON.stringify({
58
- machineName,
59
- friendlyName
60
- })
61
- });
62
- return registerResponseSchema.parse(json).device;
63
- }
64
- /** Short-lived token the machine presents when dialing the portal WebSocket. */
65
- async function mintPortalDeviceToken(auth) {
66
- const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
67
- return deviceTokenSchema.parse(json).token;
68
- }
69
- const forwardTargetSchema = z.object({
70
- daemonOrigin: z.string().min(1),
71
- token: z.string().min(1),
72
- expiresInSeconds: z.number()
73
- });
74
- /**
75
- * Everything `portal forward` needs to dial an agent's sandbox daemon through
76
- * the agent-webserver edge Worker: the daemon's stable public origin and a
77
- * daemon auth token (canUse-gated server-side).
78
- */
79
- async function fetchForwardTarget(auth, agentId) {
80
- const json = await portalFetch(auth, "/api/v1/portal/forward-target", {
81
- method: "POST",
82
- body: JSON.stringify({ agentId })
83
- });
84
- return forwardTargetSchema.parse(json);
85
- }
86
- async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
87
- await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
88
- method: "POST",
89
- body: JSON.stringify({
90
- agentId,
91
- conversationId
92
- })
93
- });
94
- }
95
- async function revokePortalAccess(auth, { deviceId, agentId }) {
96
- await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
97
- }
98
- /**
99
- * The device row for a given machine identity. Matching is by `machineName`
100
- * equality — the stable handle the machine registers under, not the display
101
- * label.
102
- */
103
- function findThisDevice(devices, machineName) {
104
- return devices.find((device) => device.machineName === machineName) ?? null;
105
- }
106
- /**
107
- * One-time grant migration onto the merged device. Earlier CLI builds
108
- * registered a separate `<machineName>-cli` device, so a user's existing
109
- * approvals hang off that row; the merged device would start with zero grants
110
- * and every already-authorized agent would ask again. Copy any grant the
111
- * merged device is missing (the grant endpoint upserts, so re-runs are
112
- * no-ops). The legacy row is left in place — an old CLI build may still
113
- * connect under it. Returns how many grants were copied.
114
- */
115
- async function unifyLegacyCliGrants(auth, machineName) {
116
- const { devices } = await fetchPortalDevices(auth);
117
- const merged = findThisDevice(devices, machineName);
118
- const legacy = findThisDevice(devices, `${machineName}-cli`);
119
- if (!merged || !legacy) return 0;
120
- const have = new Set(merged.grantedAgentIds);
121
- const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
122
- for (const agentId of missing) await grantPortalAccess(auth, {
123
- deviceId: merged.id,
124
- agentId,
125
- conversationId: null
126
- });
127
- return missing.length;
128
- }
129
-
130
- //#endregion
131
- export { mintPortalDeviceToken as a, unifyLegacyCliGrants as c, grantPortalAccess as i, fetchPortalDevices as n, registerPortalDevice as o, findThisDevice as r, revokePortalAccess as s, fetchForwardTarget as t };
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-DabRpc_T.mjs";
3
- import "./api-DG5W6iwx.mjs";
4
-
5
- export { PortalClient };
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DabRpc_T.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-k2kVkJ8D.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
-
6
- export { runPortalDaemon };
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-DabRpc_T.mjs";
3
- import "./daemon-k2kVkJ8D.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-fxf1A25Z.mjs";
6
-
7
- export { PortalDaemonClient };
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
3
- import net from "node:net";
4
- import { WebSocket, createWebSocketStream } from "ws";
5
-
6
- //#region src/chat/portal/forward.ts
7
- const TARGET_REFRESH_SAFETY_MS = 12e4;
8
- /**
9
- * The reverse portal's client half: listen on the local machine's loopback and
10
- * pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
11
- * which pipes to the sandbox's own loopback. The daemon is reached through the
12
- * agent-webserver edge Worker (sandboxes have public ingress disabled; the
13
- * Worker owns boot-resolution and injects the sandbox edge-auth token), so a
14
- * sandbox recycle just costs the next connection a cold-start wait rather
15
- * than invalidating the forward.
16
- */
17
- async function startForward({ auth, agentId, localPort, targetPort, log }) {
18
- let target = await fetchForwardTarget(auth, agentId);
19
- let mintedAt = Date.now();
20
- async function freshTarget() {
21
- const ttlMs = target.expiresInSeconds * 1e3;
22
- if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
23
- target = await fetchForwardTarget(auth, agentId);
24
- mintedAt = Date.now();
25
- }
26
- return target;
27
- }
28
- const server = net.createServer((sock) => {
29
- sock.pause();
30
- (async () => {
31
- let resolved;
32
- try {
33
- resolved = await freshTarget();
34
- } catch (err) {
35
- log(`forward: token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
36
- sock.destroy();
37
- return;
38
- }
39
- const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
40
- ws.on("open", () => {
41
- const stream = createWebSocketStream(ws);
42
- stream.on("error", () => sock.destroy());
43
- sock.on("error", () => stream.destroy());
44
- sock.pipe(stream).pipe(sock);
45
- sock.resume();
46
- });
47
- ws.on("error", (err) => {
48
- log(`forward: tunnel connect failed: ${err.message}`);
49
- sock.destroy();
50
- });
51
- })();
52
- });
53
- await new Promise((resolve, reject) => {
54
- server.once("error", reject);
55
- server.listen(localPort, "127.0.0.1", () => {
56
- server.removeListener("error", reject);
57
- resolve();
58
- });
59
- });
60
- const addr = server.address();
61
- return {
62
- port: addr && typeof addr === "object" ? addr.port : localPort,
63
- close: () => new Promise((resolve) => server.close(() => resolve()))
64
- };
65
- }
66
-
67
- //#endregion
68
- export { startForward };
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-c4c5MmgN.mjs";
3
- import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
4
-
5
- export { runRawPtyPassthrough };