skydive-cli 0.2.0-beta.615 → 0.2.0-beta.624

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.
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-B5sKLfRc.mjs";
3
- import { E as getSavedTheme, F as resolveWebUrl, R as saveTheme, S as getConfigPath, T as getReviewStateDir, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CNIvmpqW.mjs";
2
+ import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-DKTnh1gy.mjs";
3
+ import { L as saveTheme, P as resolveWebUrl, T as getSavedTheme, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, w as getReviewStateDir, x as getConfigPath, y as DEFAULT_APP_URL } from "./print-DaPpTqxu.mjs";
4
4
  import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-Q5yl_b2-.mjs";
5
- import "./api-CLTp-mKM.mjs";
6
- import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
7
- import { t as PortalClient } from "./client-BigvBsPk.mjs";
8
- import { t as runRawPtyPassthrough } from "./raw-pty-Dbi2kb9v.mjs";
5
+ import { t as PortalClient } from "./client-DT9bWi1D.mjs";
6
+ import { d as makeLineParser, f as parseDaemonMessage, l as daemonPaths, n as ensureDaemonRunning, s as LOCAL_PROTOCOL_VERSION, u as encodeLine } from "./daemon-DfMgVlYE.mjs";
7
+ import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
8
+ import { t as runRawPtyPassthrough } from "./raw-pty-BcjbTjHJ.mjs";
9
9
  import * as os$1 from "node:os";
10
10
  import { homedir, platform, release, tmpdir } from "node:os";
11
11
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
12
12
  import { z } from "zod";
13
13
  import open from "open";
14
14
  import { execFile, spawn } from "node:child_process";
15
- import { createHash } from "node:crypto";
15
+ import { createHash, randomUUID } from "node:crypto";
16
16
  import { constants } from "node:fs";
17
+ import { connect, createConnection } from "node:net";
18
+ import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
17
19
  import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
18
20
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
19
21
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
20
22
  import { create } from "zustand";
21
- import { connect, createConnection } from "node:net";
22
- import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
23
23
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
24
24
  import fuzzysort from "fuzzysort";
25
25
  import { fileURLToPath } from "node:url";
@@ -228,23 +228,222 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
228
228
  };
229
229
  }
230
230
 
231
+ //#endregion
232
+ //#region src/chat/portal/daemon-client.ts
233
+ var PortalDaemonClient = class {
234
+ sessionId = randomUUID();
235
+ socket = null;
236
+ disposed = false;
237
+ reconnectTimer = null;
238
+ lastBind = null;
239
+ wantEnabled = false;
240
+ pendingGrants = /* @__PURE__ */ new Set();
241
+ grantedAgentIds = /* @__PURE__ */ new Set();
242
+ constructor(opts) {
243
+ this.opts = opts;
244
+ }
245
+ /** Spawn the daemon if needed and attach. Idempotent. */
246
+ async start() {
247
+ if (this.disposed) return;
248
+ await ensureDaemonRunning(this.opts.appUrl);
249
+ this.connect();
250
+ }
251
+ connect() {
252
+ if (this.disposed) return;
253
+ const { socketPath } = daemonPaths(this.opts.appUrl);
254
+ const socket = connect(socketPath);
255
+ socket.setEncoding("utf8");
256
+ this.socket = socket;
257
+ const parse = makeLineParser();
258
+ socket.on("connect", () => {
259
+ this.send({
260
+ t: "hello",
261
+ v: LOCAL_PROTOCOL_VERSION,
262
+ sessionId: this.sessionId,
263
+ token: this.opts.sessionToken
264
+ });
265
+ if (this.wantEnabled) this.send({ t: "enable" });
266
+ if (this.lastBind) this.send({
267
+ t: "bind",
268
+ sessionId: this.sessionId,
269
+ conversationId: this.lastBind.conversationId,
270
+ cwd: this.lastBind.cwd
271
+ });
272
+ for (const agentId of this.pendingGrants) this.send({
273
+ t: "grant",
274
+ agentId
275
+ });
276
+ });
277
+ socket.on("data", (chunk) => {
278
+ for (const line of parse(chunk)) {
279
+ const msg = parseDaemonMessage(line);
280
+ if (msg) this.onMessage(msg);
281
+ }
282
+ });
283
+ const onClose = () => {
284
+ if (this.socket === socket) this.socket = null;
285
+ this.scheduleReconnect();
286
+ };
287
+ socket.on("close", onClose);
288
+ socket.on("error", onClose);
289
+ }
290
+ scheduleReconnect() {
291
+ if (this.disposed || this.reconnectTimer) return;
292
+ this.reconnectTimer = setTimeout(() => {
293
+ this.reconnectTimer = null;
294
+ this.start();
295
+ }, 500);
296
+ this.reconnectTimer.unref();
297
+ }
298
+ onMessage(msg) {
299
+ if (msg.t === "state") {
300
+ this.grantedAgentIds = new Set(msg.grantedAgentIds);
301
+ this.opts.onState({
302
+ status: msg.status,
303
+ machineName: msg.machineName,
304
+ friendlyName: msg.friendlyName,
305
+ error: msg.error,
306
+ grantedAgentIds: msg.grantedAgentIds
307
+ });
308
+ }
309
+ }
310
+ /** Whether this agent is currently authorized on the machine (last daemon state). */
311
+ isGranted(agentId) {
312
+ return this.grantedAgentIds.has(agentId);
313
+ }
314
+ /** Whether the user has turned machine sharing on from this CLI. */
315
+ isEnabled() {
316
+ return this.wantEnabled;
317
+ }
318
+ send(msg) {
319
+ if (this.socket?.writable) this.socket.write(encodeLine(msg));
320
+ }
321
+ /** Turn machine sharing on (the daemon connects the portal). */
322
+ enable() {
323
+ this.wantEnabled = true;
324
+ this.send({ t: "enable" });
325
+ }
326
+ /** Turn machine sharing off. */
327
+ disable() {
328
+ this.wantEnabled = false;
329
+ this.send({ t: "disable" });
330
+ }
331
+ /** Register / update this conversation's working directory for exec routing. */
332
+ bind(conversationId, cwd) {
333
+ this.lastBind = {
334
+ conversationId,
335
+ cwd
336
+ };
337
+ this.send({
338
+ t: "bind",
339
+ sessionId: this.sessionId,
340
+ conversationId,
341
+ cwd
342
+ });
343
+ }
344
+ /**
345
+ * Authorize one agent to run commands on this machine. Resolves once the
346
+ * request is sent to the daemon (the daemon performs the grant and pushes the
347
+ * updated state); kept async so it's a drop-in for the old in-process client's
348
+ * awaited `grantAgent`.
349
+ */
350
+ grantAgent(agentId) {
351
+ this.pendingGrants.add(agentId);
352
+ this.send({
353
+ t: "grant",
354
+ agentId
355
+ });
356
+ return Promise.resolve();
357
+ }
358
+ /** Detach this CLI. The daemon keeps running for other clients. */
359
+ dispose() {
360
+ this.disposed = true;
361
+ if (this.reconnectTimer) {
362
+ clearTimeout(this.reconnectTimer);
363
+ this.reconnectTimer = null;
364
+ }
365
+ this.send({
366
+ t: "bye",
367
+ sessionId: this.sessionId
368
+ });
369
+ try {
370
+ this.socket?.destroy();
371
+ } catch (_error) {}
372
+ this.socket = null;
373
+ }
374
+ };
375
+
376
+ //#endregion
377
+ //#region src/chat/portal/create-portal-client.ts
378
+ /**
379
+ * Escape hatch: setting `SKYDIVE_NO_DAEMON` (to any non-empty value)
380
+ * makes the CLI bypass the portal daemon and open the portal connection
381
+ * in-process, the pre-daemon behavior. This is a LOCAL env switch, not a remote
382
+ * flag: it takes effect on the next CLI start and lets a user (or us) fall back
383
+ * instantly if the daemon misbehaves in the wild, without shipping a new build.
384
+ * The daemon is the default because it fixes the multi-process presence flap.
385
+ */
386
+ function portalDaemonDisabled() {
387
+ return !!process.env.SKYDIVE_NO_DAEMON;
388
+ }
389
+ /**
390
+ * Build the portal client the app should use: the daemon client by default, or
391
+ * the in-process `PortalClient` when the kill switch is set. The in-process
392
+ * client always runs exec in the launch cwd (it has no conversation map), which
393
+ * is the exact pre-daemon behavior.
394
+ */
395
+ function createPortalClient(opts) {
396
+ if (portalDaemonDisabled()) {
397
+ const client = new PortalClient({
398
+ appUrl: opts.appUrl,
399
+ sessionToken: opts.sessionToken,
400
+ resolveCwd: () => process.cwd(),
401
+ onState: (state) => opts.onState({
402
+ status: state.status,
403
+ machineName: state.machineName,
404
+ friendlyName: state.friendlyName,
405
+ error: state.error,
406
+ grantedAgentIds: state.grantedAgentIds
407
+ })
408
+ });
409
+ return {
410
+ start: () => Promise.resolve(),
411
+ enable: () => client.enable(),
412
+ disable: () => client.disable(),
413
+ grantAgent: (agentId) => client.grantAgent(agentId),
414
+ isGranted: (agentId) => client.isGranted(agentId),
415
+ bind: () => {},
416
+ dispose: () => client.dispose()
417
+ };
418
+ }
419
+ return new PortalDaemonClient(opts);
420
+ }
421
+
231
422
  //#endregion
232
423
  //#region src/chat/portal/use-portal.ts
233
424
  /**
234
- * Owns the `PortalClient` lifecycle for the app session and keeps the store in
235
- * sync with its status. Constructed once from the resolved session; disabling
236
- * happens on unmount so quitting the TUI drops presence and kills any children.
425
+ * Owns the portal DAEMON CLIENT lifecycle for the app session and keeps the
426
+ * store in sync with the daemon's status.
427
+ *
428
+ * The CLI no longer opens the portal connection itself: it attaches to a
429
+ * per-host daemon (spawned on demand) that owns the single portal WebSocket, so
430
+ * multiple `skydive` processes on one machine share one portal identity instead
431
+ * of fighting over it (the flap). This hook just wires the daemon client into
432
+ * the store; the chat screen registers each conversation's working directory via
433
+ * `portalClient.bind(...)` so the daemon runs that conversation's exec in the
434
+ * right place.
435
+ *
237
436
  * `shareMachine` opts in from launch (the `--share-machine` flag); otherwise the
238
- * user turns sharing on from the chat screen.
437
+ * user turns sharing on from the chat screen. Detaching on unmount leaves the
438
+ * daemon running for any other attached CLIs.
239
439
  */
240
440
  function usePortal({ appUrl, sessionToken, shareMachine }) {
241
441
  const syncPortal = useStore((s) => s.syncPortal);
242
442
  const setPortalClient = useStore((s) => s.setPortalClient);
243
443
  useEffect(() => {
244
- const client = new PortalClient({
444
+ const client = createPortalClient({
245
445
  appUrl,
246
446
  sessionToken,
247
- cwd: process.cwd(),
248
447
  onState: (state) => syncPortal({
249
448
  status: state.status,
250
449
  machineName: state.machineName,
@@ -253,7 +452,9 @@ function usePortal({ appUrl, sessionToken, shareMachine }) {
253
452
  })
254
453
  });
255
454
  setPortalClient(client);
256
- if (shareMachine) client.enable();
455
+ client.start().then(() => {
456
+ if (shareMachine) client.enable();
457
+ });
257
458
  return () => {
258
459
  client.dispose();
259
460
  setPortalClient(null);
@@ -9191,6 +9392,16 @@ function ChatScreen({ agent, conversation }) {
9191
9392
  agentHost.setSession(conversationId);
9192
9393
  setResumeConversation(conversationId);
9193
9394
  }, [agentHost, conversationId]);
9395
+ const bindPortalCwd = useCallback(() => {
9396
+ if (!conversationId || !portalClient) return;
9397
+ if (portal.status === "off") return;
9398
+ const cwd = shellSessionRef.current?.cwd ?? process.cwd();
9399
+ portalClient.bind(conversationId, cwd);
9400
+ }, [
9401
+ conversationId,
9402
+ portalClient,
9403
+ portal.status
9404
+ ]);
9194
9405
  const scrollRef = useRef(null);
9195
9406
  const composerRef = useRef(null);
9196
9407
  const jumpToBottom = useCallback(() => {
@@ -9761,6 +9972,7 @@ function ChatScreen({ agent, conversation }) {
9761
9972
  ...m,
9762
9973
  exit: exitCode
9763
9974
  } : m));
9975
+ bindPortalCwd();
9764
9976
  } catch (err) {
9765
9977
  appendOutput(`\n${errorMessage(err)}`);
9766
9978
  exitCode = 1;
@@ -9770,7 +9982,7 @@ function ChatScreen({ agent, conversation }) {
9770
9982
  } : m));
9771
9983
  }
9772
9984
  sendContent(formatShellContext(command, captured, exitCode), [], { echo: false });
9773
- }, [sendContent]);
9985
+ }, [sendContent, bindPortalCwd]);
9774
9986
  const runSandboxExec = useCallback((command) => {
9775
9987
  if (!appUrl || !sessionToken) return;
9776
9988
  const id = crypto.randomUUID();
@@ -10099,6 +10311,7 @@ function ChatScreen({ agent, conversation }) {
10099
10311
  if (portal.status === "off") {
10100
10312
  declinedRef.current.delete(agent.id);
10101
10313
  portalClient.enable();
10314
+ if (conversationId) portalClient.bind(conversationId, shellSessionRef.current?.cwd ?? process.cwd());
10102
10315
  } else {
10103
10316
  portalClient.disable();
10104
10317
  setPortalPrompt(null);
@@ -10107,7 +10320,8 @@ function ChatScreen({ agent, conversation }) {
10107
10320
  portalClient,
10108
10321
  portal.status,
10109
10322
  agent.id,
10110
- setPortalPrompt
10323
+ setPortalPrompt,
10324
+ conversationId
10111
10325
  ]);
10112
10326
  const confirmGrant = useCallback(() => {
10113
10327
  if (!portalClient) return;
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-Q5yl_b2-.mjs";
3
+ import { t as PortalClient } from "./client-DT9bWi1D.mjs";
4
+
5
+ export { PortalClient };
@@ -1,10 +1,66 @@
1
1
  #!/usr/bin/env node
2
- import { a as errorMessage } from "./rest-Q5yl_b2-.mjs";
3
- import { c as machineIdentity, i as mintPortalDeviceToken, l as portalWsUrl, n as findThisDevice, r as grantPortalAccess, s as buildEnv, t as fetchPortalDevices } from "./api-CLTp-mKM.mjs";
2
+ import { a as errorMessage, t as HttpError } from "./rest-Q5yl_b2-.mjs";
3
+ import os from "node:os";
4
4
  import { z } from "zod";
5
5
  import { spawn } from "node:child_process";
6
6
  import { WebSocket } from "ws";
7
7
 
8
+ //#region src/chat/portal/machine.ts
9
+ /**
10
+ * Identity this machine registers under when the CLI shares it via the portal.
11
+ *
12
+ * The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
13
+ * device from the same host's Skydive Desktop app. `portal_device` is unique on
14
+ * (org, user, machineName), and directives route to whichever socket holds the
15
+ * device — if the CLI and desktop registered the same name they'd share a
16
+ * device row and both execute every directive. Distinct names also make the
17
+ * grant UI unambiguous about which surface is being authorized.
18
+ */
19
+ function machineIdentity() {
20
+ const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
21
+ return {
22
+ machineName: `${host}-cli`,
23
+ friendlyName: `${host} (CLI)`
24
+ };
25
+ }
26
+ const INHERITED_ENV = [
27
+ "HOME",
28
+ "USER",
29
+ "LOGNAME",
30
+ "SHELL",
31
+ "LANG",
32
+ "LC_ALL",
33
+ "TMPDIR",
34
+ "TERM",
35
+ "PATH"
36
+ ];
37
+ function buildEnv(extra) {
38
+ const env = {};
39
+ for (const key of INHERITED_ENV) {
40
+ const value = process.env[key];
41
+ if (value !== void 0) env[key] = value;
42
+ }
43
+ if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
44
+ return env;
45
+ }
46
+ /**
47
+ * Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
48
+ * desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
49
+ * machine/label ride as query pairs (percent-encoded by URL).
50
+ */
51
+ function portalWsUrl(appUrl, machine, label) {
52
+ const base = appUrl.replace(/\/+$/, "");
53
+ let wsBase;
54
+ if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
55
+ else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
56
+ else wsBase = `wss://${base}`;
57
+ const url = new URL(`${wsBase}/api/v1/portal/desktop`);
58
+ url.searchParams.set("machine", machine);
59
+ url.searchParams.set("label", label);
60
+ return url.toString();
61
+ }
62
+
63
+ //#endregion
8
64
  //#region ../portal-protocol/src/index.ts
9
65
  const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
10
66
  const T_DATA = 1;
@@ -31,7 +87,8 @@ const ctrlMessageSchema = z.discriminatedUnion("t", [
31
87
  z.object({
32
88
  t: z.literal("open"),
33
89
  argv: z.array(z.string()),
34
- env: z.record(z.string()).nullable()
90
+ env: z.record(z.string()).nullable(),
91
+ conversationId: z.string().nullable().optional()
35
92
  }),
36
93
  z.object({ t: z.literal("stdin_eof") }),
37
94
  z.object({ t: z.literal("pause") }),
@@ -103,7 +160,7 @@ var JobManager = class {
103
160
  handleCtrl(id, msg) {
104
161
  switch (msg.t) {
105
162
  case "open":
106
- this.startJob(id, msg.argv, msg.env);
163
+ this.startJob(id, msg.argv, msg.env, msg.conversationId ?? null);
107
164
  return;
108
165
  case "stdin_eof":
109
166
  this.jobs.get(id)?.child.stdin.end();
@@ -133,7 +190,7 @@ var JobManager = class {
133
190
  job.child.stderr.resume();
134
191
  }
135
192
  }
136
- startJob(id, argv, env) {
193
+ startJob(id, argv, env, conversationId) {
137
194
  const [program, ...args] = argv;
138
195
  if (!program) {
139
196
  this.opts.send(encodeCtrl(id, {
@@ -145,7 +202,7 @@ var JobManager = class {
145
202
  let child;
146
203
  try {
147
204
  child = spawn(program, args, {
148
- cwd: this.opts.cwd,
205
+ cwd: this.opts.resolveCwd(conversationId),
149
206
  env: buildEnv(env),
150
207
  stdio: [
151
208
  "pipe",
@@ -194,6 +251,89 @@ var JobManager = class {
194
251
  }
195
252
  };
196
253
 
254
+ //#endregion
255
+ //#region src/chat/portal/api.ts
256
+ /**
257
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
258
+ * TUI/`portal open` connection) and the `skydive portal` management
259
+ * commands, so the endpoint contracts and response schemas live in exactly
260
+ * one place.
261
+ */
262
+ const deviceSchema = z.object({
263
+ id: z.string(),
264
+ machineName: z.string(),
265
+ friendlyName: z.string(),
266
+ connected: z.boolean(),
267
+ lastSeen: z.string().nullable(),
268
+ grantedAgentIds: z.array(z.string())
269
+ });
270
+ const devicesResponseSchema = z.object({
271
+ devices: z.array(deviceSchema),
272
+ agents: z.array(z.object({
273
+ id: z.string(),
274
+ name: z.string()
275
+ }))
276
+ });
277
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
278
+ async function portalFetch(auth, path, init) {
279
+ const res = await fetch(`${auth.appUrl}${path}`, {
280
+ method: init.method,
281
+ headers: {
282
+ authorization: `Bearer ${auth.sessionToken}`,
283
+ accept: "application/json",
284
+ ...init.body ? { "content-type": "application/json" } : {}
285
+ },
286
+ ...init.body ? { body: init.body } : {}
287
+ });
288
+ if (!res.ok) {
289
+ const body = await res.text().catch(() => "");
290
+ throw new HttpError(res.status, body);
291
+ }
292
+ return res.json();
293
+ }
294
+ async function fetchPortalDevices(auth) {
295
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
296
+ return devicesResponseSchema.parse(json);
297
+ }
298
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
299
+ /**
300
+ * Register this machine's device row without connecting. Connecting registers
301
+ * as a side effect; this covers granting an agent on a machine that has never
302
+ * shared yet (the grant references the device row).
303
+ */
304
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
305
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
306
+ method: "POST",
307
+ body: JSON.stringify({
308
+ machineName,
309
+ friendlyName
310
+ })
311
+ });
312
+ return registerResponseSchema.parse(json).device;
313
+ }
314
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
315
+ async function mintPortalDeviceToken(auth) {
316
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
317
+ return deviceTokenSchema.parse(json).token;
318
+ }
319
+ async function grantPortalAccess(auth, { deviceId, agentId }) {
320
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
321
+ method: "POST",
322
+ body: JSON.stringify({ agentId })
323
+ });
324
+ }
325
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
326
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
327
+ }
328
+ /**
329
+ * The device row for a given machine identity. Matching is by `machineName`
330
+ * equality — the stable per-surface handle (`<host>-cli` vs the desktop's
331
+ * `<host>`), not the display label.
332
+ */
333
+ function findThisDevice(devices, machineName) {
334
+ return devices.find((device) => device.machineName === machineName) ?? null;
335
+ }
336
+
197
337
  //#endregion
198
338
  //#region src/chat/portal/client.ts
199
339
  const INITIAL_BACKOFF_MS = 500;
@@ -309,7 +449,7 @@ var PortalClient = class {
309
449
  });
310
450
  this.ws = ws;
311
451
  const jobs = new JobManager({
312
- cwd: this.opts.cwd,
452
+ resolveCwd: this.opts.resolveCwd,
313
453
  send: (frame) => {
314
454
  if (ws.readyState === WebSocket.OPEN) ws.send(frame);
315
455
  }
@@ -363,4 +503,4 @@ function sleep(ms) {
363
503
  }
364
504
 
365
505
  //#endregion
366
- export { PortalClient as t };
506
+ export { registerPortalDevice as a, grantPortalAccess as i, fetchPortalDevices as n, revokePortalAccess as o, findThisDevice as r, machineIdentity as s, PortalClient as t };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-Q5yl_b2-.mjs";
3
+ import "./client-DT9bWi1D.mjs";
4
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as stopDaemon, r as isDaemonListening, t as PortalDaemon } from "./daemon-DfMgVlYE.mjs";
5
+
6
+ export { runPortalDaemon };