skydive-cli 0.5.0-beta.9 → 0.6.0-beta.10

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-Q-Kh3nn5.mjs → boot-BEvwMeO6.mjs} +4708 -1005
  7. package/dist/js/chunk-BbwQpWto.mjs +33 -0
  8. package/dist/js/{client-DabRpc_T.mjs → client-B-5eaVyt.mjs} +437 -39
  9. package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
  10. package/dist/js/client-DebOAhwd.mjs +5 -0
  11. package/dist/js/{daemon-D21wQ7DI.mjs → daemon-CLc3zjcv.mjs} +238 -50
  12. package/dist/js/daemon-ClXSvjcv.mjs +7 -0
  13. package/dist/js/{daemon-client-DPUNjhBB.mjs → daemon-client-DSIb7j28.mjs} +1 -1
  14. package/dist/js/daemon-client-pYWyRTyi.mjs +8 -0
  15. package/dist/js/dist-CRtjM7ba.mjs +1750 -0
  16. package/dist/js/forward-C-f04uyE.mjs +208 -0
  17. package/dist/js/{profiler-BkCV__ao.mjs → install-Bg_9I2-2.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-BpuyEfWX.mjs → print-CPBx8690.mjs} +257 -35
  21. package/dist/js/{print-Wakr3GJd.mjs → print-CT1G1LeA.mjs} +3 -3
  22. package/dist/js/{print-share-CKLPmsg0.mjs → print-share-DW5pHVDz.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-B2bynGwY.mjs} +153 -19
  26. package/dist/js/rest-DejyWmRu.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 +15 -6
  30. package/dist/js/api-DG5W6iwx.mjs +0 -131
  31. package/dist/js/client-BuU34IVE.mjs +0 -5
  32. package/dist/js/daemon-LSDSvMaC.mjs +0 -6
  33. package/dist/js/daemon-client-CUSq-Wuh.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.6.0-beta.10";
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,36 @@ 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
+ setConversationModel: async ({ conversationId, agentId, model, thinkingLevel }) => post(`/api/v1/conversations/${conversationId}/model`, {
299
+ agentId,
300
+ model,
301
+ thinkingLevel
302
+ }, conversationModelResponseSchema),
303
+ updateAgentEffort: async ({ agentId, thinkingLevel }) => {
304
+ const { agent } = await patchAgent(agentId, { thinkingLevel });
305
+ return { thinkingLevel: agent.thinkingLevel ?? null };
239
306
  },
240
307
  listConversations: async ({ agentId, limit, channels, archived, query, onPage }) => {
241
308
  const trimmedQuery = query?.trim();
@@ -260,9 +327,17 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
260
327
  } while (cursor && all.length < maxConversations);
261
328
  return limit ? all.slice(0, limit) : all;
262
329
  },
263
- listMessages: async ({ conversationId }) => {
264
- const { messages } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, listMessagesResponseSchema);
265
- return messages;
330
+ listMessages: async ({ conversationId, limit, before }) => {
331
+ const params = new URLSearchParams();
332
+ if (limit !== void 0) params.set("limit", String(limit));
333
+ if (before !== void 0) params.set("before", before);
334
+ const query = params.toString();
335
+ const { messages, hasMore, nextBefore } = await get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages${query ? `?${query}` : ""}`, listMessagesResponseSchema);
336
+ return {
337
+ messages,
338
+ hasMore: hasMore ?? false,
339
+ nextBefore: nextBefore ?? null
340
+ };
266
341
  },
267
342
  listWorkspaceFiles: async ({ agentId }) => {
268
343
  const { files } = await get(`/api/v1/workspace-files?${new URLSearchParams({ agentId }).toString()}`, listWorkspaceFilesResponseSchema);
@@ -289,6 +364,13 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
289
364
  const { result } = await get(`/api/v1/trpc/v2.subagentTasks.list?input=${encodeURIComponent(JSON.stringify({ conversationId }))}`, subagentTasksResponseSchema);
290
365
  return new Map(result.data.subagentTasks.map((task) => [task.id, task]));
291
366
  },
367
+ computeLiveStats: async (agentId) => {
368
+ const { stats } = await get(`/api/v1/compute/live-stats?agentId=${encodeURIComponent(agentId)}`, liveStatsResponseSchema);
369
+ return stats;
370
+ },
371
+ restartAgent: async (agentId) => {
372
+ await post(`/api/v1/agents/${encodeURIComponent(agentId)}/restart`, {}, z.object({ ok: z.boolean() }));
373
+ },
292
374
  uploadAttachment: async ({ agentId, fileName, mediaType, data }) => {
293
375
  const size = data.byteLength;
294
376
  const presign = await post("/api/v1/attachments/presign", {
@@ -316,6 +398,17 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
316
398
  sizeBytes: finalized.sizeBytes ?? size
317
399
  };
318
400
  },
401
+ downloadAttachment: async ({ attachmentId }) => {
402
+ const res = await fetch(`${appUrl}/api/v1/attachments/${encodeURIComponent(attachmentId)}/download?disposition=inline`, { headers: baseHeaders });
403
+ if (!res.ok) throw new HttpError(res.status, await res.text().catch(() => ""));
404
+ const data = new Uint8Array(await res.arrayBuffer());
405
+ const mediaType = res.headers.get("content-type") ?? "application/octet-stream";
406
+ return {
407
+ data,
408
+ fileName: filenameFromContentDisposition(res.headers.get("content-disposition")),
409
+ mediaType
410
+ };
411
+ },
319
412
  forkConversation: async ({ conversationId }) => {
320
413
  const { conversation } = await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, {}, forkConversationResponseSchema);
321
414
  return conversation;
@@ -329,6 +422,9 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
329
422
  setConversationArchived: async ({ conversationId, archived }) => {
330
423
  await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/archive`, { archived }, z.object({ archived: z.boolean() }));
331
424
  },
425
+ markConversationRead: async ({ conversationId }) => {
426
+ await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/read`, {}, z.object({ read: z.boolean() }));
427
+ },
332
428
  renameConversation: async ({ conversationId, title }) => {
333
429
  await post(`/api/v1/conversations/${encodeURIComponent(conversationId)}`, { title }, z.object({ conversation: z.object({ id: z.string() }) }), "PATCH");
334
430
  },
@@ -350,6 +446,7 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
350
446
  const { authorizationUrl } = await post("/api/v1/external-oauth/connect", input, externalOauthConnectResponseSchema);
351
447
  return { authorizationUrl: authorizationUrl ?? null };
352
448
  },
449
+ decideComputeRequest: async ({ requestId, decision }) => post(`/api/v1/compute-requests/${encodeURIComponent(requestId)}/decision`, { decision }, computeDecisionResponseSchema),
353
450
  fulfillCredential: async ({ url, body }) => {
354
451
  const target = new URL(url, appUrl).toString();
355
452
  const res = await fetch(target, {
@@ -383,6 +480,7 @@ function createRestClient({ appUrl, sessionToken, workspaceId }) {
383
480
  headers: {
384
481
  authorization: `Bearer ${sessionToken}`,
385
482
  accept: "text/event-stream",
483
+ [CLI_VERSION_HEADER]: version,
386
484
  ...workspaceId ? { "x-workspace-id": workspaceId } : {}
387
485
  },
388
486
  signal
@@ -434,23 +532,38 @@ const agentSummarySchema = z.object({
434
532
  createdAt: z.string(),
435
533
  creatorName: z.string().nullable().optional(),
436
534
  model: z.string().nullable().optional(),
437
- modelLocked: z.boolean().optional()
535
+ modelLocked: z.boolean().optional(),
536
+ thinkingLevel: z.string().nullable().optional()
438
537
  });
439
538
  const platformModelSchema = z.object({
440
539
  id: z.string(),
441
540
  displayName: z.string(),
442
541
  providerDisplay: z.string().optional(),
443
542
  reasoning: z.boolean().optional(),
444
- compliant: z.boolean().optional()
543
+ compliant: z.boolean().optional(),
544
+ thinkingLevels: z.array(z.string()).optional(),
545
+ thinkingLevelLabels: z.record(z.string(), z.string()).optional()
445
546
  }).passthrough();
446
547
  const listModelsResponseSchema = z.object({ models: z.array(platformModelSchema) });
447
- const updateAgentResponseSchema = z.object({ agent: z.object({ model: z.string().nullable().optional() }).passthrough() });
548
+ const modelEffortPreferencesResponseSchema = z.object({ modelConfigOverrides: z.record(z.string(), z.object({ thinkingLevel: z.string().optional() })).optional() });
549
+ const conversationModelResponseSchema = z.object({
550
+ model: z.string().nullable(),
551
+ thinkingLevel: z.string().nullable()
552
+ });
553
+ const updateAgentResponseSchema = z.object({ agent: z.object({
554
+ model: z.string().nullable().optional(),
555
+ thinkingLevel: z.string().nullable().optional()
556
+ }).passthrough() });
448
557
  const listAgentsResponseSchema = z.object({
449
558
  agents: z.array(agentSummarySchema),
450
559
  nextCursor: z.string().nullable().optional(),
451
560
  totalCount: z.number().nullable().optional()
452
561
  });
453
562
  const createAgentResponseSchema = z.object({ agent: agentSummarySchema });
563
+ const onboardingConversationResponseSchema = z.object({
564
+ conversationId: z.string(),
565
+ runId: z.string().nullable()
566
+ });
454
567
  const getAgentResponseSchema = z.object({ agent: agentSummarySchema });
455
568
  const agentSuggestionSchema = z.object({ name: z.string() });
456
569
  const suggestAgentResponseSchema = z.object({ suggestion: agentSuggestionSchema.nullable() });
@@ -469,6 +582,7 @@ const conversationSummarySchema = z.object({
469
582
  channel: z.string().nullable(),
470
583
  channelLabel: z.string().nullable(),
471
584
  viewerArchivedAt: z.string().nullable().optional(),
585
+ unread: z.boolean().optional(),
472
586
  agent: conversationAgentSchema,
473
587
  agents: z.array(conversationAgentSchema).optional()
474
588
  });
@@ -479,7 +593,8 @@ const conversationDetailSchema = z.object({
479
593
  createdAt: z.string(),
480
594
  updatedAt: z.string(),
481
595
  channel: z.string().nullable(),
482
- channelLabel: z.string().nullable()
596
+ channelLabel: z.string().nullable(),
597
+ parentConversationId: z.string().uuid().nullable().optional()
483
598
  });
484
599
  const conversationTitleSchema = z.object({
485
600
  id: z.string().uuid(),
@@ -527,10 +642,27 @@ const subagentTaskSnapshotSchema = z.object({
527
642
  status: z.string(),
528
643
  title: z.string().nullable(),
529
644
  createdAt: z.string(),
530
- completedAt: z.string().nullable()
645
+ completedAt: z.string().nullable(),
646
+ childConversationId: z.string().nullish(),
647
+ childRunStartedAt: z.string().nullish(),
648
+ contextTokens: z.number().nullish(),
649
+ latestActivity: z.string().nullish()
531
650
  });
532
651
  const subagentTasksResponseSchema = z.object({ result: z.object({ data: z.object({ subagentTasks: z.array(subagentTaskSnapshotSchema) }) }) });
533
- const listMessagesResponseSchema = z.object({ messages: z.array(uiMessageSchema) });
652
+ const liveStatsResponseSchema = z.object({ stats: z.object({
653
+ sampledAt: z.number(),
654
+ cpuUsedPct: z.number(),
655
+ cpuCount: z.number(),
656
+ memUsedBytes: z.number(),
657
+ memTotalBytes: z.number(),
658
+ diskUsedBytes: z.number(),
659
+ diskTotalBytes: z.number()
660
+ }).nullable() });
661
+ const listMessagesResponseSchema = z.object({
662
+ messages: z.array(uiMessageSchema),
663
+ hasMore: z.boolean().optional(),
664
+ nextBefore: z.string().nullish()
665
+ });
534
666
  const workspaceFileSchema = z.object({
535
667
  id: z.string(),
536
668
  path: z.string(),
@@ -564,6 +696,7 @@ const finalizeResponseSchema = z.object({
564
696
  });
565
697
  const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
566
698
  const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
699
+ const computeDecisionResponseSchema = z.object({ status: z.string() }).passthrough();
567
700
  const runStreamEventSchema = z.union([z.object({
568
701
  kind: z.literal("chunk"),
569
702
  chunk: z.record(z.unknown())
@@ -584,7 +717,8 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [
584
717
  kind: z.literal("agent"),
585
718
  id: z.string(),
586
719
  model: z.string().nullable(),
587
- modelLocked: z.boolean()
720
+ modelLocked: z.boolean(),
721
+ thinkingLevel: z.string().nullish()
588
722
  }),
589
723
  z.object({
590
724
  kind: z.literal("run"),
@@ -602,4 +736,4 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [
602
736
  ]);
603
737
 
604
738
  //#endregion
605
- export { isRecord as a, errorMessage as i, errorDetail as n, sendErrorMessage as r, createRestClient as t };
739
+ 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-B2bynGwY.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.9",
3
+ "version": "0.6.0-beta.10",
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",
@@ -24,12 +25,13 @@
24
25
  "test:unit": "vitest run --passWithNoTests && yarn test:tui",
25
26
  "test:ci": "vitest run --coverage --coverage.reporter=lcovonly --reporter=default --reporter=github-actions --minWorkers=1 --maxWorkers=2 --passWithNoTests && yarn test:tui",
26
27
  "test:tui": "node scripts/run-tui-tests.mjs",
28
+ "test:blackbox": "bun test test/blackbox --timeout 120000",
27
29
  "render:frames": "bun scripts/render-frames.tsx",
28
30
  "render:send-errors": "bun scripts/render-send-errors.tsx",
29
31
  "typecheck": "tsgo --noEmit"
30
32
  },
31
33
  "dependencies": {
32
- "@opentui/react": "0.4.3",
34
+ "@opentui/react": "0.5.8",
33
35
  "conf": "^13.0.1",
34
36
  "diff": "9.0.0",
35
37
  "eventsource-parser": "^3.0.8",
@@ -37,11 +39,11 @@
37
39
  "fuzzysort": "^3.1.0",
38
40
  "neverthrow": "^8.2.0",
39
41
  "open": "^10.1.0",
40
- "react": "^19.0.0",
42
+ "react": "^19.2.0",
41
43
  "react-devtools-core": "^7.0.1",
42
44
  "safe-stable-stringify": "^2.3.1",
43
45
  "semver": "^7.7.4",
44
- "shell-quote": "1.9.0",
46
+ "tar": "7.5.21",
45
47
  "web-tree-sitter": "0.25.10",
46
48
  "ws": "^8.21.0",
47
49
  "yargs": "^17.7.2",
@@ -66,5 +68,12 @@
66
68
  "engines": {
67
69
  "bun": ">=1.3.14",
68
70
  "node": ">=20.0.0"
71
+ },
72
+ "//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.",
73
+ "optionalDependencies": {
74
+ "skydive-cli-darwin-arm64": "0.6.0-beta.10",
75
+ "skydive-cli-darwin-x64": "0.6.0-beta.10",
76
+ "skydive-cli-linux-x64": "0.6.0-beta.10",
77
+ "skydive-cli-linux-arm64": "0.6.0-beta.10"
69
78
  }
70
79
  }
@@ -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-D21wQ7DI.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-D21wQ7DI.mjs";
4
- import "./api-DG5W6iwx.mjs";
5
- import { t as PortalDaemonClient } from "./daemon-client-DPUNjhBB.mjs";
6
-
7
- export { PortalDaemonClient };