@jpcasabiancai/cayenne 0.3.0 → 0.3.2

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 (2) hide show
  1. package/dist/cli.js +444 -80
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -73,6 +73,38 @@ var init_constants = __esm({
73
73
  }
74
74
  });
75
75
 
76
+ // ../../packages/shared/src/protocol/attachments.ts
77
+ function sniffImage(bytes) {
78
+ if (startsWith(bytes, PNG)) return "image/png";
79
+ if (startsWith(bytes, JPEG)) return "image/jpeg";
80
+ if (startsWith(bytes, GIF87A) || startsWith(bytes, GIF89A)) return "image/gif";
81
+ if (startsWith(bytes, RIFF) && startsWith(bytes, WEBP, 8)) return "image/webp";
82
+ return null;
83
+ }
84
+ var ATTACHMENT_MAX_BYTES, ATTACHMENT_MAX_COUNT, ATTACHMENT_MEDIA_TYPES, ATTACHMENT_NAME, startsWith, ascii, PNG, JPEG, GIF87A, GIF89A, RIFF, WEBP;
85
+ var init_attachments = __esm({
86
+ "../../packages/shared/src/protocol/attachments.ts"() {
87
+ "use strict";
88
+ ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024;
89
+ ATTACHMENT_MAX_COUNT = 4;
90
+ ATTACHMENT_MEDIA_TYPES = {
91
+ "image/png": "png",
92
+ "image/jpeg": "jpg",
93
+ "image/webp": "webp",
94
+ "image/gif": "gif"
95
+ };
96
+ ATTACHMENT_NAME = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(png|jpg|webp|gif)$/;
97
+ startsWith = (bytes, prefix, offset = 0) => bytes.length >= offset + prefix.length && prefix.every((b, i) => bytes[offset + i] === b);
98
+ ascii = (s) => Array.from(s, (c) => c.charCodeAt(0));
99
+ PNG = [137, 80, 78, 71, 13, 10, 26, 10];
100
+ JPEG = [255, 216, 255];
101
+ GIF87A = ascii("GIF87a");
102
+ GIF89A = ascii("GIF89a");
103
+ RIFF = ascii("RIFF");
104
+ WEBP = ascii("WEBP");
105
+ }
106
+ });
107
+
76
108
  // ../../packages/shared/src/protocol/git.ts
77
109
  import { z } from "zod";
78
110
  var WORKTREE_NAME, GIT_REF, REF_FORBIDDEN, isGitRef, ABSOLUTE, escapes, NUL, AbsoluteDir, RepoRelativePath, GitRef, WorktreeName, GitPayloads, GitFileStatus, GitStatusFile, GitStatus, GitDiffFile, GitDiff, GitBranch, GitBranches, GitWorktreeCreated, GitWorktreeRemoved;
@@ -284,6 +316,7 @@ var PermissionMode, PermissionDecision, Provider, Effort, CommandPayloads, kinds
284
316
  var init_command = __esm({
285
317
  "../../packages/shared/src/protocol/command.ts"() {
286
318
  "use strict";
319
+ init_attachments();
287
320
  init_git();
288
321
  init_github();
289
322
  init_terminal();
@@ -324,13 +357,42 @@ var init_command = __esm({
324
357
  effort: Effort.optional(),
325
358
  model: z4.string().optional(),
326
359
  resumeProviderSessionId: z4.string().optional(),
360
+ // Duplicate a chat (D36): start from a fork of `resumeProviderSessionId` instead of appending
361
+ // to its transcript. Claude-only, no `worktree`. The runner additionally refuses a source that
362
+ // is not the caller's own Claude session on this runner, or a cwd other than the source's own directory.
363
+ forkSession: z4.boolean().optional(),
327
364
  initialPrompt: z4.string().optional(),
328
365
  title: z4.string().max(200).optional()
366
+ }).superRefine((p, ctx) => {
367
+ if (p.forkSession !== true) return;
368
+ if (p.resumeProviderSessionId === void 0) {
369
+ ctx.addIssue({
370
+ code: "custom",
371
+ message: "forkSession needs resumeProviderSessionId",
372
+ path: ["forkSession"]
373
+ });
374
+ }
375
+ if (p.worktree !== void 0) {
376
+ ctx.addIssue({
377
+ code: "custom",
378
+ message: "a forked session keeps its working directory",
379
+ path: ["worktree"]
380
+ });
381
+ }
382
+ if (p.provider === "codex") {
383
+ ctx.addIssue({
384
+ code: "custom",
385
+ message: "codex sessions cannot be forked",
386
+ path: ["provider"]
387
+ });
388
+ }
329
389
  }),
330
390
  "session.prompt": z4.object({
331
391
  text: z4.string().min(1).max(2e5),
332
392
  clientUuid: z4.uuid(),
333
- attachments: z4.array(z4.object({ kind: z4.literal("image"), storagePath: z4.string() })).default([]),
393
+ // Images uploaded to the private `attachments` bucket (D37). No mediaType: the runner sniffs
394
+ // the bytes. The runner still checks each path against the session's own row (R31).
395
+ attachments: z4.array(z4.object({ kind: z4.literal("image"), storagePath: z4.string().regex(ATTACHMENT_NAME) })).max(ATTACHMENT_MAX_COUNT).default([]),
334
396
  memoryContext: z4.string().max(2e4).optional()
335
397
  }),
336
398
  "session.permission.respond": z4.object({
@@ -401,6 +463,7 @@ var ContentBlock, MessageRole, ChatMessageRow, PermissionRequestRow, CodexQuesti
401
463
  var init_content = __esm({
402
464
  "../../packages/shared/src/protocol/content.ts"() {
403
465
  "use strict";
466
+ init_attachments();
404
467
  ContentBlock = z5.discriminatedUnion("type", [
405
468
  z5.object({ type: z5.literal("text"), text: z5.string() }),
406
469
  z5.object({ type: z5.literal("thinking"), text: z5.string() }),
@@ -417,7 +480,14 @@ var init_content = __esm({
417
480
  isError: z5.boolean().default(false),
418
481
  structured: z5.unknown().optional()
419
482
  }),
420
- z5.object({ type: z5.literal("image"), storagePath: z5.string(), mediaType: z5.string() }),
483
+ // The path is checked here, not only where it is used: the session owner may insert any
484
+ // `content` (0003 `messages_insert`), and a reader's browser signs whatever path a block names.
485
+ // A block that is not a canonical attachment name drops the row rather than steering that request.
486
+ z5.object({
487
+ type: z5.literal("image"),
488
+ storagePath: z5.string().regex(ATTACHMENT_NAME),
489
+ mediaType: z5.enum(Object.keys(ATTACHMENT_MEDIA_TYPES))
490
+ }),
421
491
  z5.object({
422
492
  type: z5.literal("result"),
423
493
  isError: z5.boolean(),
@@ -634,6 +704,18 @@ var init_runner = __esm({
634
704
  * cannot grant a terminal. Optional so an older row still parses.
635
705
  */
636
706
  memberTerminalIds: z8.array(z8.uuid()).max(64).optional(),
707
+ /**
708
+ * Whether this runner honours `session.start { forkSession: true }` (D36, protocol §9). An older
709
+ * runner strips the field and then refuses the resume under R30, so a client sends
710
+ * `forkSession` only when this is true. Absent reads as false; optional so an older row parses.
711
+ */
712
+ sessionFork: z8.boolean().optional(),
713
+ /**
714
+ * Whether this runner sends `session.prompt` attachments to the provider (D37, R31, protocol §9).
715
+ * An older runner parses `attachments` and ignores them, so a client offers images only when
716
+ * this is true. Absent reads as false; optional so an older row parses.
717
+ */
718
+ promptImages: z8.boolean().optional(),
637
719
  /**
638
720
  * The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
639
721
  * the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
@@ -650,6 +732,21 @@ var init_runner = __esm({
650
732
  }
651
733
  });
652
734
 
735
+ // ../../packages/shared/src/protocol/session.ts
736
+ import { z as z9 } from "zod";
737
+ var SlashCommand, SessionCommandsListed;
738
+ var init_session = __esm({
739
+ "../../packages/shared/src/protocol/session.ts"() {
740
+ "use strict";
741
+ SlashCommand = z9.object({
742
+ name: z9.string(),
743
+ description: z9.string(),
744
+ argumentHint: z9.string()
745
+ });
746
+ SessionCommandsListed = z9.object({ slashCommands: z9.array(SlashCommand) });
747
+ }
748
+ });
749
+
653
750
  // ../../packages/shared/src/protocol/transport.ts
654
751
  var init_transport = __esm({
655
752
  "../../packages/shared/src/protocol/transport.ts"() {
@@ -661,6 +758,7 @@ var init_transport = __esm({
661
758
  var init_protocol = __esm({
662
759
  "../../packages/shared/src/protocol/index.ts"() {
663
760
  "use strict";
761
+ init_attachments();
664
762
  init_command();
665
763
  init_content();
666
764
  init_events();
@@ -668,6 +766,7 @@ var init_protocol = __esm({
668
766
  init_github();
669
767
  init_local();
670
768
  init_runner();
769
+ init_session();
671
770
  init_terminal();
672
771
  init_transport();
673
772
  }
@@ -817,7 +916,7 @@ import {
817
916
  } from "fs";
818
917
  import { homedir as homedir2 } from "os";
819
918
  import { dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
820
- import { z as z9 } from "zod";
919
+ import { z as z10 } from "zod";
821
920
  function setProfileOverride(name) {
822
921
  profileOverride = name;
823
922
  }
@@ -892,24 +991,24 @@ var init_config = __esm({
892
991
  init_cloud();
893
992
  init_profile();
894
993
  MEMBER_TERMINALS_ERROR = "allowMemberTerminals must be false or a list of user ids";
895
- RunnerConfig = z9.object({
896
- supabaseUrl: z9.url(),
897
- supabaseAnonKey: z9.string().min(1),
898
- workspaceId: z9.uuid().optional(),
899
- runnerId: z9.uuid().optional(),
900
- name: z9.string().optional(),
901
- roots: z9.array(z9.string()).default([]),
902
- port: z9.number().int().default(LOCAL_PORT),
994
+ RunnerConfig = z10.object({
995
+ supabaseUrl: z10.url(),
996
+ supabaseAnonKey: z10.string().min(1),
997
+ workspaceId: z10.uuid().optional(),
998
+ runnerId: z10.uuid().optional(),
999
+ name: z10.string().optional(),
1000
+ roots: z10.array(z10.string()).default([]),
1001
+ port: z10.number().int().default(LOCAL_PORT),
903
1002
  /** Extra page origins the local server admits, beside the built-in list (`buildOrigins`). */
904
- localOrigins: z9.array(z9.string()).default([]),
905
- allowBypassPermissions: z9.boolean().default(false),
1003
+ localOrigins: z10.array(z10.string()).default([]),
1004
+ allowBypassPermissions: z10.boolean().default(false),
906
1005
  /** Absolute path to the `codex` binary, when it is not on the daemon's PATH. */
907
- codexPath: z9.string().optional(),
1006
+ codexPath: z10.string().optional(),
908
1007
  /**
909
1008
  * The Claude Code config directory this profile's sessions run on (D33), handed to them as
910
1009
  * `CLAUDE_CONFIG_DIR`. Unset means the CLI's own default, `~/.claude`.
911
1010
  */
912
- claudeConfigDir: z9.string().optional(),
1011
+ claudeConfigDir: z10.string().optional(),
913
1012
  /**
914
1013
  * Who besides the runner owner may hold a terminal here (D34, plan ruling 14): `false`, or the
915
1014
  * user ids this machine's owner names. Machine-local like `allowBypassPermissions`: a workspace
@@ -917,15 +1016,15 @@ var init_config = __esm({
917
1016
  * `true` is refused — there is no "every member" switch. A listed member's shell runs as this
918
1017
  * machine's OS user; the env allowlist is not a boundary.
919
1018
  */
920
- allowMemberTerminals: z9.union(
1019
+ allowMemberTerminals: z10.union(
921
1020
  [
922
- z9.literal(false),
923
- z9.array(z9.uuid({ error: MEMBER_TERMINALS_ERROR }), { error: MEMBER_TERMINALS_ERROR }).max(64, { error: MEMBER_TERMINALS_ERROR })
1021
+ z10.literal(false),
1022
+ z10.array(z10.uuid({ error: MEMBER_TERMINALS_ERROR }), { error: MEMBER_TERMINALS_ERROR }).max(64, { error: MEMBER_TERMINALS_ERROR })
924
1023
  ],
925
1024
  { error: MEMBER_TERMINALS_ERROR }
926
1025
  ).default(false),
927
1026
  /** Minutes a terminal nobody attends stays alive before the runner kills it (features §2). */
928
- terminalIdleMinutes: z9.number().int().min(1).max(1440).default(10)
1027
+ terminalIdleMinutes: z10.number().int().min(1).max(1440).default(10)
929
1028
  });
930
1029
  memberTerminalIdsOf = (cfg) => cfg.allowMemberTerminals === false ? [] : [...cfg.allowMemberTerminals];
931
1030
  configPath = (dir) => join3(dir, "config.json");
@@ -1170,7 +1269,7 @@ import {
1170
1269
  } from "fs";
1171
1270
  import { uptime } from "os";
1172
1271
  import { join as join5 } from "path";
1173
- import { z as z10 } from "zod";
1272
+ import { z as z11 } from "zod";
1174
1273
  function bootedAt(now = Date.now()) {
1175
1274
  return new Date(Math.round((now - uptime() * 1e3) / 1e3) * 1e3).toISOString();
1176
1275
  }
@@ -1313,13 +1412,13 @@ var LockRecord, BOOT_DRIFT_MS, ATTEMPTS, BACKOFF_MS, CLAIM_TTL_MS, errno, claimO
1313
1412
  var init_lock = __esm({
1314
1413
  "src/lock.ts"() {
1315
1414
  "use strict";
1316
- LockRecord = z10.object({
1317
- pid: z10.number().int().positive(),
1318
- profile: z10.string(),
1319
- startedAt: z10.string(),
1415
+ LockRecord = z11.object({
1416
+ pid: z11.number().int().positive(),
1417
+ profile: z11.string(),
1418
+ startedAt: z11.string(),
1320
1419
  /** `bootedAt()` when the lock was taken; a lock from another boot is stale whatever its pid. */
1321
- bootedAt: z10.string(),
1322
- version: z10.string()
1420
+ bootedAt: z11.string(),
1421
+ version: z11.string()
1323
1422
  });
1324
1423
  BOOT_DRIFT_MS = 1e4;
1325
1424
  ATTEMPTS = 8;
@@ -3062,7 +3161,7 @@ function createTokenVerifier({
3062
3161
  }
3063
3162
  return false;
3064
3163
  };
3065
- const refuse = (k) => {
3164
+ const refuse2 = (k) => {
3066
3165
  cache.set(k, { userId: null, exp: 0, until: now() + negativeTtlMs });
3067
3166
  return { status: "refused" };
3068
3167
  };
@@ -3073,19 +3172,19 @@ function createTokenVerifier({
3073
3172
  const hit = cache.get(k);
3074
3173
  if (hit && hit.until > now()) {
3075
3174
  if (hit.userId === null) return { status: "refused" };
3076
- if (hit.exp * 1e3 <= now()) return refuse(k);
3175
+ if (hit.exp * 1e3 <= now()) return refuse2(k);
3077
3176
  return { status: "valid", userId: hit.userId, exp: hit.exp };
3078
3177
  }
3079
3178
  cache.delete(k);
3080
3179
  const claims = claimsOf(token);
3081
- if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse(k);
3180
+ if (!claims || claims.exp * 1e3 <= now() || !admit(claims.sub)) return refuse2(k);
3082
3181
  if (!budgeted()) return { status: "unavailable" };
3083
3182
  const user = await db.auth.getUser(token).then(
3084
3183
  (r) => r.error ? retryable(r.error) ? void 0 : null : r.data.user,
3085
3184
  () => void 0
3086
3185
  );
3087
3186
  if (user === void 0) return { status: "unavailable" };
3088
- if (!user || user.id !== claims.sub) return refuse(k);
3187
+ if (!user || user.id !== claims.sub) return refuse2(k);
3089
3188
  cache.set(k, { userId: user.id, exp: claims.exp, until: now() + ttlMs });
3090
3189
  return { status: "valid", userId: user.id, exp: claims.exp };
3091
3190
  }
@@ -3451,7 +3550,7 @@ function createLocalServer({
3451
3550
  server.headersTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
3452
3551
  server.requestTimeout = LOCAL_HANDSHAKE_TIMEOUT_MS;
3453
3552
  server.keepAliveTimeout = 1e3;
3454
- const refuse = (socket, status) => {
3553
+ const refuse2 = (socket, status) => {
3455
3554
  const text = status === 403 ? "Forbidden" : "Service Unavailable";
3456
3555
  socket.write(`HTTP/1.1 ${status} ${text}\r
3457
3556
  Connection: close\r
@@ -3463,11 +3562,11 @@ Content-Length: 0\r
3463
3562
  server.on("upgrade", (req, socket, head) => {
3464
3563
  const origin = req.headers.origin ?? "";
3465
3564
  if (req.url !== LOCAL_WS_PATH || !hostAllowed(req.headers.host) || !originAllowed(origin, origins)) {
3466
- refuse(socket, 403);
3565
+ refuse2(socket, 403);
3467
3566
  return;
3468
3567
  }
3469
3568
  if (pendingUpgrades >= LOCAL_MAX_PENDING_UPGRADES || conns.size >= maxSockets) {
3470
- refuse(socket, 503);
3569
+ refuse2(socket, 503);
3471
3570
  return;
3472
3571
  }
3473
3572
  pendingUpgrades += 1;
@@ -3811,6 +3910,69 @@ var init_roster = __esm({
3811
3910
  }
3812
3911
  });
3813
3912
 
3913
+ // src/sessions/attachments.ts
3914
+ import { rmSync as rmSync5 } from "fs";
3915
+ import { join as join15 } from "path";
3916
+ async function loadAttachments(db, scope, attachments) {
3917
+ const prefix = `${scope.workspaceId.toLowerCase()}/${scope.sessionId.toLowerCase()}/`;
3918
+ const named = attachments.map(({ storagePath }, i) => {
3919
+ const match = ATTACHMENT_NAME.exec(storagePath);
3920
+ const declared = match?.[3] === void 0 ? void 0 : TYPE_OF_EXT.get(match[3]);
3921
+ if (!match || !storagePath.startsWith(prefix) || !declared)
3922
+ throw refuse(i + 1, "not in this chat");
3923
+ return { storagePath, declared };
3924
+ });
3925
+ const loaded = [];
3926
+ for (const [i, { storagePath, declared }] of named.entries()) {
3927
+ const n = i + 1;
3928
+ let blob;
3929
+ try {
3930
+ const { data: data2, error } = await db.storage.from(ATTACHMENTS_BUCKET).download(storagePath);
3931
+ if (error || !data2) throw refuse(n, "could not be read");
3932
+ blob = data2;
3933
+ } catch {
3934
+ throw refuse(n, "could not be read");
3935
+ }
3936
+ if (blob.size > ATTACHMENT_MAX_BYTES) throw refuse(n, "too large");
3937
+ let data;
3938
+ try {
3939
+ data = Buffer.from(await blob.arrayBuffer());
3940
+ } catch {
3941
+ throw refuse(n, "could not be read");
3942
+ }
3943
+ if (data.byteLength > ATTACHMENT_MAX_BYTES) throw refuse(n, "too large");
3944
+ const sniffed = sniffImage(data);
3945
+ if (!sniffed) throw refuse(n, "not a supported image");
3946
+ if (sniffed !== declared) throw refuse(n, "not the image type its name says");
3947
+ loaded.push({ storagePath, mediaType: sniffed, data });
3948
+ }
3949
+ return loaded;
3950
+ }
3951
+ function attachmentsTempDir(home, runnerId, sessionId) {
3952
+ return join15(home, "tmp", "attachments", runnerId, sessionId);
3953
+ }
3954
+ function assertStagingInsideHome(home, dir) {
3955
+ return assertInsideRootsOrAncestor(dir, [home]);
3956
+ }
3957
+ function clearAttachmentsTemp(home, runnerId) {
3958
+ const dir = join15(home, "tmp", "attachments", runnerId);
3959
+ assertStagingInsideHome(home, dir);
3960
+ rmSync5(dir, { recursive: true, force: true });
3961
+ }
3962
+ var ATTACHMENTS_BUCKET, TYPE_OF_EXT, refuse;
3963
+ var init_attachments2 = __esm({
3964
+ "src/sessions/attachments.ts"() {
3965
+ "use strict";
3966
+ init_src();
3967
+ init_paths();
3968
+ ATTACHMENTS_BUCKET = "attachments";
3969
+ TYPE_OF_EXT = new Map(
3970
+ Object.entries(ATTACHMENT_MEDIA_TYPES).map(([type, ext]) => [ext, type])
3971
+ );
3972
+ refuse = (n, reason) => new Error(`VALIDATION: attachment ${n}: ${reason}`);
3973
+ }
3974
+ });
3975
+
3814
3976
  // src/github/gh.ts
3815
3977
  import { execFile as execFile4 } from "child_process";
3816
3978
  import { promisify as promisify3 } from "util";
@@ -3962,11 +4124,12 @@ var init_refusals = __esm({
3962
4124
  });
3963
4125
 
3964
4126
  // src/sessions/handlers.ts
4127
+ import { ZodError } from "zod";
3965
4128
  function parsePayload(row, kind) {
3966
4129
  try {
3967
4130
  return parseCommand(row, kind);
3968
4131
  } catch (err) {
3969
- const message = err instanceof Error ? err.message : String(err);
4132
+ const message = err instanceof ZodError ? err.issues.map((i) => i.message).join("; ") : err instanceof Error ? err.message : String(err);
3970
4133
  throw new Error(message.startsWith("VALIDATION:") ? message : `VALIDATION: ${message}`);
3971
4134
  }
3972
4135
  }
@@ -4354,7 +4517,7 @@ import { randomUUID as randomUUID3 } from "crypto";
4354
4517
  import { EventEmitter } from "events";
4355
4518
  import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
4356
4519
  var ClaudeSession;
4357
- var init_session = __esm({
4520
+ var init_session2 = __esm({
4358
4521
  "src/claude/session.ts"() {
4359
4522
  "use strict";
4360
4523
  init_src();
@@ -4364,7 +4527,7 @@ var init_session = __esm({
4364
4527
  constructor(opts) {
4365
4528
  super();
4366
4529
  this.opts = opts;
4367
- this.providerSessionId = opts.resume ?? null;
4530
+ this.providerSessionId = opts.forkSession ? null : opts.resume ?? null;
4368
4531
  }
4369
4532
  opts;
4370
4533
  provider = "claude";
@@ -4397,6 +4560,7 @@ var init_session = __esm({
4397
4560
  // Present only when asked for, so the default call stays the CLI's own behaviour.
4398
4561
  ...this.opts.strictMcpConfig === void 0 ? {} : { strictMcpConfig: this.opts.strictMcpConfig },
4399
4562
  ...this.opts.mcpServers === void 0 ? {} : { mcpServers: this.opts.mcpServers },
4563
+ ...this.opts.forkSession ? { forkSession: true } : {},
4400
4564
  canUseTool,
4401
4565
  abortController: this.abort,
4402
4566
  stderr: (line) => {
@@ -4408,10 +4572,21 @@ var init_session = __esm({
4408
4572
  });
4409
4573
  this.pumping = this.pump();
4410
4574
  }
4411
- prompt(text, uuid) {
4575
+ prompt(text, uuid, { images }) {
4576
+ const content = images.length ? [
4577
+ ...images.map((image) => ({
4578
+ type: "image",
4579
+ source: {
4580
+ type: "base64",
4581
+ media_type: image.mediaType,
4582
+ data: image.data.toString("base64")
4583
+ }
4584
+ })),
4585
+ { type: "text", text }
4586
+ ] : text;
4412
4587
  this.inbox.push({
4413
4588
  type: "user",
4414
- message: { role: "user", content: text },
4589
+ message: { role: "user", content },
4415
4590
  parent_tool_use_id: null,
4416
4591
  uuid,
4417
4592
  session_id: this.providerSessionId ?? void 0
@@ -4640,15 +4815,22 @@ var init_policy = __esm({
4640
4815
 
4641
4816
  // src/codex/session.ts
4642
4817
  import { spawn as spawn2 } from "child_process";
4818
+ import { randomUUID as randomUUID4 } from "crypto";
4643
4819
  import { EventEmitter as EventEmitter2 } from "events";
4820
+ import { chmodSync as chmodSync4, lstatSync as lstatSync4, mkdirSync as mkdirSync6, rmdirSync, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
4821
+ import { dirname as dirname7, join as join16 } from "path";
4822
+ function removeFiles(files) {
4823
+ for (const file of files) rmSync6(file, { force: true });
4824
+ }
4644
4825
  function scrubSecrets(text) {
4645
4826
  return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
4646
4827
  }
4647
4828
  var STDERR_TAIL_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
4648
- var init_session2 = __esm({
4829
+ var init_session3 = __esm({
4649
4830
  "src/codex/session.ts"() {
4650
4831
  "use strict";
4651
4832
  init_src();
4833
+ init_attachments2();
4652
4834
  init_normalize2();
4653
4835
  init_policy();
4654
4836
  init_rpc();
@@ -4692,6 +4874,8 @@ var init_session2 = __esm({
4692
4874
  turnId = null;
4693
4875
  running = false;
4694
4876
  queue = [];
4877
+ /** The image files of the turn in flight, removed when it ends. */
4878
+ turnFiles = [];
4695
4879
  approvals = /* @__PURE__ */ new Map();
4696
4880
  streaming = /* @__PURE__ */ new Set();
4697
4881
  stderrTail = [];
@@ -4715,6 +4899,7 @@ var init_session2 = __esm({
4715
4899
  const tail = this.stderrTail.length ? `
4716
4900
  ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
4717
4901
  const error = this.stopping || code === 0 ? null : `PROVIDER_ERROR: codex exited with ${code}${tail}`;
4902
+ this.removeAllFiles();
4718
4903
  this.emit("exit", { error });
4719
4904
  });
4720
4905
  const rpc = new JsonRpc(child.stdin, child.stdout);
@@ -4754,19 +4939,88 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
4754
4939
  });
4755
4940
  this.flush();
4756
4941
  }
4757
- prompt(text, _uuid) {
4758
- this.queue.push(text);
4942
+ /**
4943
+ * Queue one turn with images `stageImages` already wrote. From here the session owns the files
4944
+ * and removes them when the turn ends, fails, or the session stops.
4945
+ */
4946
+ prompt(text, _uuid, staged) {
4947
+ if (staged.files.length !== staged.images.length) {
4948
+ throw new Error("PROVIDER_ERROR: codex images must be staged before the prompt");
4949
+ }
4950
+ this.queue.push({ text, files: [...staged.files] });
4759
4951
  this.flush();
4760
4952
  }
4953
+ /**
4954
+ * Write the images Codex will read, synchronously, before the manager persists anything. A
4955
+ * failure removes whatever was written and throws, so a refused prompt leaves no file behind.
4956
+ */
4957
+ stageImages(images) {
4958
+ const files = this.writeImages(images);
4959
+ return { images, files, discard: () => removeFiles(files) };
4960
+ }
4961
+ writeImages(images) {
4962
+ if (images.length === 0) return [];
4963
+ const dir = this.opts.attachmentsDir;
4964
+ const home = this.opts.attachmentsHome;
4965
+ if (!dir || !home) {
4966
+ throw new Error("PROVIDER_ERROR: this codex session has no attachments directory");
4967
+ }
4968
+ const written = [];
4969
+ try {
4970
+ assertStagingInsideHome(home, dir);
4971
+ mkdirSync6(dir, { recursive: true, mode: 448 });
4972
+ assertStagingInsideHome(home, dir);
4973
+ for (const path of [dirname7(dir), dir]) {
4974
+ const st = lstatSync4(path);
4975
+ if (st.isSymbolicLink() || !st.isDirectory()) {
4976
+ throw new Error(`${path} is not a plain directory`);
4977
+ }
4978
+ }
4979
+ chmodSync4(dir, 448);
4980
+ for (const image of images) {
4981
+ const path = join16(dir, `${randomUUID4()}.${ATTACHMENT_MEDIA_TYPES[image.mediaType]}`);
4982
+ writeFileSync5(path, image.data, { mode: 384, flag: "wx" });
4983
+ written.push(path);
4984
+ }
4985
+ } catch (err) {
4986
+ removeFiles(written);
4987
+ const detail = err instanceof Error ? err.message : String(err);
4988
+ throw new Error(`PROVIDER_ERROR: could not write an image for codex: ${detail}`);
4989
+ }
4990
+ return written;
4991
+ }
4992
+ /** The turn in flight is over: its images have been read, or never will be. */
4993
+ endTurnFiles() {
4994
+ removeFiles(this.turnFiles);
4995
+ this.turnFiles = [];
4996
+ }
4997
+ removeAllFiles() {
4998
+ this.endTurnFiles();
4999
+ for (const turn of this.queue) {
5000
+ removeFiles(turn.files);
5001
+ turn.files = [];
5002
+ }
5003
+ if (this.opts.attachmentsDir) {
5004
+ try {
5005
+ rmdirSync(this.opts.attachmentsDir);
5006
+ } catch {
5007
+ }
5008
+ }
5009
+ }
4761
5010
  /** One turn at a time: Codex has no notion of a second concurrent turn on a thread. */
4762
5011
  flush() {
4763
5012
  if (this.running || !this.rpc || !this.providerSessionId) return;
4764
5013
  const next = this.queue.shift();
4765
5014
  if (next === void 0) return;
4766
5015
  this.running = true;
5016
+ this.turnFiles = next.files;
4767
5017
  void this.rpc.request("turn/start", {
4768
5018
  threadId: this.providerSessionId,
4769
- input: [{ type: "text", text: next }],
5019
+ // app-server v2 `UserInput`: images as `localImage` paths, then the text.
5020
+ input: [
5021
+ ...next.files.map((path) => ({ type: "localImage", path })),
5022
+ { type: "text", text: next.text }
5023
+ ],
4770
5024
  sandboxPolicy: this.sandbox,
4771
5025
  ...this.pendingModel ? { model: this.pendingModel } : {},
4772
5026
  ...this.pendingEffort ? { effort: this.pendingEffort } : {}
@@ -4774,6 +5028,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
4774
5028
  this.turnId = res.turnId ?? null;
4775
5029
  }).catch((err) => {
4776
5030
  this.running = false;
5031
+ this.endTurnFiles();
4777
5032
  this.emit("result", {
4778
5033
  isError: true,
4779
5034
  durationMs: 0,
@@ -4810,6 +5065,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
4810
5065
  case "turn/failed": {
4811
5066
  this.running = false;
4812
5067
  this.turnId = null;
5068
+ this.endTurnFiles();
4813
5069
  const status = params.status ?? (method === "turn/failed" ? "failed" : "completed");
4814
5070
  this.emit("result", {
4815
5071
  isError: status === "failed",
@@ -4964,6 +5220,7 @@ ${scrubSecrets(this.stderrTail.join("")).slice(-STDERR_TAIL_MAX)}` : "";
4964
5220
  this.emit("permissionTimeout", { id });
4965
5221
  }
4966
5222
  await this.interrupt();
5223
+ this.removeAllFiles();
4967
5224
  this.rpc?.close();
4968
5225
  this.child?.stdin?.end();
4969
5226
  const child = this.child;
@@ -4991,13 +5248,27 @@ function createProviderSession(provider, opts) {
4991
5248
  var init_create = __esm({
4992
5249
  "src/providers/create.ts"() {
4993
5250
  "use strict";
4994
- init_session();
4995
5251
  init_session2();
5252
+ init_session3();
5253
+ }
5254
+ });
5255
+
5256
+ // src/providers/types.ts
5257
+ var inMemoryImages;
5258
+ var init_types = __esm({
5259
+ "src/providers/types.ts"() {
5260
+ "use strict";
5261
+ inMemoryImages = (images) => ({
5262
+ images,
5263
+ files: [],
5264
+ discard: () => {
5265
+ }
5266
+ });
4996
5267
  }
4997
5268
  });
4998
5269
 
4999
5270
  // src/sessions/manager.ts
5000
- import { randomUUID as randomUUID4 } from "crypto";
5271
+ import { randomUUID as randomUUID5 } from "crypto";
5001
5272
  import { realpathSync as realpathSync7 } from "fs";
5002
5273
  import { isAbsolute as isAbsolute10, sep as sep5 } from "path";
5003
5274
  import { isDeepStrictEqual } from "util";
@@ -5026,12 +5297,15 @@ var init_manager = __esm({
5026
5297
  "src/sessions/manager.ts"() {
5027
5298
  "use strict";
5028
5299
  init_src();
5300
+ init_config();
5029
5301
  init_introspect();
5030
5302
  init_watch();
5031
5303
  init_worktree();
5032
5304
  init_worktree_owners();
5033
5305
  init_paths();
5034
5306
  init_create();
5307
+ init_types();
5308
+ init_attachments2();
5035
5309
  isAtOrBelow = (p, dir) => p === dir || p.startsWith(dir.endsWith(sep5) ? dir : `${dir}${sep5}`);
5036
5310
  DENY_MESSAGE_MAX = 2e3;
5037
5311
  EXIT_ERROR_MAX = 8 * 1024;
@@ -5095,10 +5369,16 @@ var init_manager = __esm({
5095
5369
  }
5096
5370
  this.assertModeAllowed(p.permissionMode);
5097
5371
  const provider = p.provider ?? "claude";
5372
+ if (p.forkSession && provider !== "claude") {
5373
+ throw new Error("VALIDATION: codex sessions cannot be forked");
5374
+ }
5098
5375
  this.assertProviderAllowed(provider, createdBy);
5099
5376
  const typed = resolveInsideRoots(p.cwd, this.deps.roots);
5100
5377
  this.assertNotRemoving(typed.real);
5101
- if (p.resumeProviderSessionId !== void 0) {
5378
+ const forking = p.forkSession === true && p.resumeProviderSessionId !== void 0;
5379
+ if (forking) {
5380
+ await this.assertForkable(p.resumeProviderSessionId, createdBy, typed.real);
5381
+ } else if (p.resumeProviderSessionId !== void 0) {
5102
5382
  await this.assertResumable(sessionId, p.resumeProviderSessionId);
5103
5383
  }
5104
5384
  let { real: cwd, root } = typed;
@@ -5106,7 +5386,7 @@ var init_manager = __esm({
5106
5386
  if (p.worktree) {
5107
5387
  cwd = await this.startWorktree(typed.real, typed.root, p.worktree, createdBy);
5108
5388
  inWorktree = true;
5109
- } else if (p.resumeProviderSessionId !== void 0) {
5389
+ } else if (p.resumeProviderSessionId !== void 0 && !forking) {
5110
5390
  const recorded = await this.recordedWorktree(sessionId);
5111
5391
  if (recorded) ({ real: cwd, root } = recorded);
5112
5392
  }
@@ -5127,9 +5407,16 @@ var init_manager = __esm({
5127
5407
  permissionMode: p.permissionMode,
5128
5408
  model: p.model,
5129
5409
  resume: p.resumeProviderSessionId,
5410
+ ...forking ? { forkSession: true } : {},
5130
5411
  ...provider === "codex" ? {
5131
5412
  codexPath: this.deps.codexPath,
5132
5413
  checkInput: this.codexGate(sessionId, cwd),
5414
+ attachmentsHome: this.deps.cayenneHome ?? cayenneHome(),
5415
+ attachmentsDir: attachmentsTempDir(
5416
+ this.deps.cayenneHome ?? cayenneHome(),
5417
+ this.deps.runnerId,
5418
+ sessionId
5419
+ ),
5133
5420
  ...p.effort ? { effort: p.effort } : {}
5134
5421
  } : this.isolation(createdBy)
5135
5422
  });
@@ -5137,7 +5424,9 @@ var init_manager = __esm({
5137
5424
  started();
5138
5425
  }
5139
5426
  if (inWorktree) await this.recordSessionWorktree(sessionId, cwd, root);
5140
- else if (p.resumeProviderSessionId === void 0) this.forgetSessionWorktree(sessionId);
5427
+ else if (p.resumeProviderSessionId === void 0 || forking) {
5428
+ this.forgetSessionWorktree(sessionId);
5429
+ }
5141
5430
  live.status = "idle";
5142
5431
  await this.updateSession(sessionId, { status: "idle", model: p.model ?? null });
5143
5432
  await this.broadcast(sessionId, {
@@ -5157,30 +5446,61 @@ var init_manager = __esm({
5157
5446
  if (p.initialPrompt) {
5158
5447
  await this.prompt(
5159
5448
  sessionId,
5160
- { text: p.initialPrompt, clientUuid: randomUUID4(), attachments: [] },
5449
+ { text: p.initialPrompt, clientUuid: randomUUID5(), attachments: [] },
5161
5450
  createdBy
5162
5451
  );
5163
5452
  }
5164
5453
  return { providerSessionId: live.session.providerSessionId };
5165
5454
  }
5166
5455
  async prompt(sessionId, p, createdBy) {
5456
+ this.assertCodexOwner(sessionId, createdBy);
5457
+ const images = p.attachments.length ? await loadAttachments(
5458
+ this.deps.db,
5459
+ { workspaceId: await this.sessionWorkspace(sessionId), sessionId },
5460
+ p.attachments
5461
+ ) : [];
5167
5462
  const live = await this.ensureLive(sessionId, createdBy);
5168
5463
  this.assertCodexOwner(sessionId, createdBy);
5169
5464
  const text = p.memoryContext ? wrapMemory(p.memoryContext, p.text) : p.text;
5170
- live.echoed.add(p.clientUuid);
5171
- await this.persist(
5172
- sessionId,
5173
- live,
5174
- "user",
5175
- [{ type: "text", text: p.text }],
5176
- p.clientUuid,
5177
- null,
5178
- null
5179
- );
5180
- await this.setStatus(sessionId, "running");
5181
- live.session.prompt(text, p.clientUuid);
5465
+ const staged = images.length ? live.session.stageImages?.(images) ?? inMemoryImages(images) : inMemoryImages([]);
5466
+ try {
5467
+ live.echoed.add(p.clientUuid);
5468
+ await this.persist(
5469
+ sessionId,
5470
+ live,
5471
+ "user",
5472
+ [
5473
+ { type: "text", text: p.text },
5474
+ ...images.map(
5475
+ (image) => ({
5476
+ type: "image",
5477
+ storagePath: image.storagePath,
5478
+ mediaType: image.mediaType
5479
+ })
5480
+ )
5481
+ ],
5482
+ p.clientUuid,
5483
+ null,
5484
+ null
5485
+ );
5486
+ await this.setStatus(sessionId, "running");
5487
+ live.session.prompt(text, p.clientUuid, staged);
5488
+ } catch (err) {
5489
+ staged.discard();
5490
+ throw err;
5491
+ }
5182
5492
  return { accepted: true };
5183
5493
  }
5494
+ /**
5495
+ * The session row's own workspace, which R31 checks attachment paths against. Read, never taken
5496
+ * from the payload or assumed to be the runner's: the path prefix is the whole of the check.
5497
+ */
5498
+ async sessionWorkspace(sessionId) {
5499
+ const { data, error } = await this.deps.db.from("chat_sessions").select("workspace_id").eq("id", sessionId).eq("runner_id", this.deps.runnerId).maybeSingle();
5500
+ if (error) this.deps.log.warn({ err: error.message, sessionId }, "session read failed");
5501
+ if (!data?.workspace_id) throw new Error("SESSION_NOT_FOUND");
5502
+ return data.workspace_id;
5503
+ }
5184
5504
  /**
5185
5505
  * Refuse unless `requestId` is a request this session is still holding.
5186
5506
  *
@@ -5584,6 +5904,37 @@ var init_manager = __esm({
5584
5904
  throw new Error("VALIDATION: resumeProviderSessionId does not belong to this session");
5585
5905
  }
5586
5906
  }
5907
+ /**
5908
+ * D36, amending R30 for `forkSession: true`: the resume id may be held by another session, but
5909
+ * only one on this runner that `createdBy` owns — a member must not fork the runner owner's
5910
+ * conversation, or anyone else's. A command with no creator owns nothing. Only a Claude row with
5911
+ * a non-empty id can be a source. The new session must run where the source did, decided the way
5912
+ * a resume of the source would be: the worktree this runner recorded for it, else its canonical
5913
+ * `cwd`. Never `worktree_path`: the runner owner can rewrite that column on a member's chat, and
5914
+ * it must not steer where the member's fork runs. A source path that no longer resolves inside
5915
+ * the roots cannot be proved equal, so it is refused, not thrown through.
5916
+ */
5917
+ async assertForkable(providerSessionId, createdBy, cwd) {
5918
+ if (providerSessionId === "") {
5919
+ throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
5920
+ }
5921
+ const { data, error } = await this.deps.db.from("chat_sessions").select("id, owner_id, cwd").eq("provider_session_id", providerSessionId).eq("runner_id", this.deps.runnerId).eq("provider", "claude").maybeSingle();
5922
+ if (error) throw new Error(`PROVIDER_ERROR: could not read the session: ${error.message}`);
5923
+ if (!data || createdBy === null || data.owner_id !== createdBy) {
5924
+ throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
5925
+ }
5926
+ let sourceCwd = (await this.recordedWorktree(data.id))?.real ?? null;
5927
+ if (sourceCwd === null) {
5928
+ try {
5929
+ sourceCwd = resolveInsideRoots(data.cwd, this.deps.roots).real;
5930
+ } catch {
5931
+ sourceCwd = null;
5932
+ }
5933
+ }
5934
+ if (sourceCwd !== cwd) {
5935
+ throw new Error("VALIDATION: a forked session keeps its working directory");
5936
+ }
5937
+ }
5587
5938
  /**
5588
5939
  * Whether this runner may open a session on `provider` for `createdBy`.
5589
5940
  *
@@ -6259,9 +6610,9 @@ function createTerminalChannels({
6259
6610
  const inflight = joining.get(terminalId);
6260
6611
  if (inflight) return inflight;
6261
6612
  if (joined.has(terminalId)) return;
6262
- const join17 = attachOne(terminalId).finally(() => joining.delete(terminalId));
6263
- joining.set(terminalId, join17);
6264
- return join17;
6613
+ const join19 = attachOne(terminalId).finally(() => joining.delete(terminalId));
6614
+ joining.set(terminalId, join19);
6615
+ return join19;
6265
6616
  },
6266
6617
  flush,
6267
6618
  detach,
@@ -6318,7 +6669,7 @@ var init_channels = __esm({
6318
6669
  // src/terminal/env.ts
6319
6670
  import { readFileSync as readFileSync7, realpathSync as realpathSync8 } from "fs";
6320
6671
  import { lstat } from "fs/promises";
6321
- import { dirname as dirname7, isAbsolute as isAbsolute11, relative as relative4 } from "path";
6672
+ import { dirname as dirname8, isAbsolute as isAbsolute11, relative as relative4 } from "path";
6322
6673
  function terminalEnv(base, { runnerOwner, claudeConfigDir }) {
6323
6674
  const env = {};
6324
6675
  for (const [key, value] of Object.entries(base)) {
@@ -6374,7 +6725,7 @@ async function rootOwnedPath(fs, real) {
6374
6725
  return false;
6375
6726
  }
6376
6727
  if (st.isSymbolicLink() || st.uid !== 0 || (st.mode & 18) !== 0) return false;
6377
- const parent = dirname7(path);
6728
+ const parent = dirname8(path);
6378
6729
  if (parent === path) return true;
6379
6730
  path = parent;
6380
6731
  }
@@ -7358,7 +7709,7 @@ __export(start_exports, {
7358
7709
  start: () => start
7359
7710
  });
7360
7711
  import { homedir as homedir3, hostname } from "os";
7361
- import { join as join15 } from "path";
7712
+ import { join as join17 } from "path";
7362
7713
  function protectedPaths(claudeConfigDir, home = homedir3()) {
7363
7714
  const inHome = [
7364
7715
  ".claude",
@@ -7366,12 +7717,12 @@ function protectedPaths(claudeConfigDir, home = homedir3()) {
7366
7717
  ".codex",
7367
7718
  ".ssh",
7368
7719
  ".aws",
7369
- join15(".config", "gh"),
7720
+ join17(".config", "gh"),
7370
7721
  ".gnupg",
7371
7722
  ".git-credentials",
7372
7723
  ".npmrc",
7373
7724
  ".netrc"
7374
- ].map((p) => join15(home, p));
7725
+ ].map((p) => join17(home, p));
7375
7726
  return [cayenneHome(), ...claudeConfigDir ? [claudeConfigDir] : [], ...inHome];
7376
7727
  }
7377
7728
  function logCodex(codex) {
@@ -7517,7 +7868,11 @@ async function compose(cfg, lock, host, preflight) {
7517
7868
  claude: true,
7518
7869
  providers,
7519
7870
  // Who the runner's own config names (D34); every heartbeat refresh republishes it.
7520
- memberTerminalIds: [...memberTerminalIds]
7871
+ memberTerminalIds: [...memberTerminalIds],
7872
+ // D36: this build forks a Claude session on `session.start { forkSession: true }`.
7873
+ sessionFork: true,
7874
+ // D37: this build loads image attachments on `session.prompt` under R31.
7875
+ promptImages: true
7521
7876
  });
7522
7877
  const registered = capabilities();
7523
7878
  let loggedCodex = null;
@@ -7551,6 +7906,14 @@ async function compose(cfg, lock, host, preflight) {
7551
7906
  await local.close();
7552
7907
  throw err;
7553
7908
  }
7909
+ try {
7910
+ clearAttachmentsTemp(cayenneHome(), identity.runnerId);
7911
+ } catch (err) {
7912
+ logger.warn(
7913
+ { err: err instanceof Error ? err.message : String(err) },
7914
+ "could not clear staged attachment images"
7915
+ );
7916
+ }
7554
7917
  let stopHeartbeat = null;
7555
7918
  let roster = null;
7556
7919
  try {
@@ -7576,9 +7939,9 @@ async function compose(cfg, lock, host, preflight) {
7576
7939
  // The roster's own array, mutated in place: `serves` reads the live list with no restart.
7577
7940
  allowedMemberIds: liveRoster.allowedMemberIds,
7578
7941
  // Outside every profile and repository: the creator of a worktree is the machine's record.
7579
- worktreeOwners: createWorktreeOwners(join15(cayenneHome(), "worktrees")),
7942
+ worktreeOwners: createWorktreeOwners(join17(cayenneHome(), "worktrees")),
7580
7943
  // Beside it: where each session was spawned, the only source of a resumed directory.
7581
- sessionWorktrees: createSessionWorktrees(join15(cayenneHome(), "session-worktrees")),
7944
+ sessionWorktrees: createSessionWorktrees(join17(cayenneHome(), "session-worktrees")),
7582
7945
  roots,
7583
7946
  allowBypass: cfg.allowBypassPermissions,
7584
7947
  codexPath,
@@ -7838,6 +8201,7 @@ var init_start = __esm({
7838
8201
  init_profile();
7839
8202
  init_registry();
7840
8203
  init_roster();
8204
+ init_attachments2();
7841
8205
  init_handlers();
7842
8206
  init_manager();
7843
8207
  init_supabase();
@@ -7851,9 +8215,9 @@ var init_start = __esm({
7851
8215
  });
7852
8216
 
7853
8217
  // src/cli.ts
7854
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync8, realpathSync as realpathSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
8218
+ import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync8, realpathSync as realpathSync9, rmSync as rmSync7, writeFileSync as writeFileSync6 } from "fs";
7855
8219
  import { homedir as homedir4, hostname as hostname2, userInfo } from "os";
7856
- import { dirname as dirname8, join as join16 } from "path";
8220
+ import { dirname as dirname9, join as join18 } from "path";
7857
8221
  import { Command } from "commander";
7858
8222
  import prompts from "prompts";
7859
8223
 
@@ -8308,8 +8672,8 @@ var service = () => {
8308
8672
  return { exec: exec4, launchd: makeLaunchd(exec4, userInfo().uid), label: plistLabel(currentProfile()) };
8309
8673
  };
8310
8674
  var writePlistFile = (path, xml2) => {
8311
- mkdirSync6(dirname8(path), { recursive: true });
8312
- writeFileSync5(path, xml2, { mode: 420 });
8675
+ mkdirSync7(dirname9(path), { recursive: true });
8676
+ writeFileSync6(path, xml2, { mode: 420 });
8313
8677
  };
8314
8678
  var requireMac = () => {
8315
8679
  if (process.platform !== "darwin") {
@@ -8455,7 +8819,7 @@ program.command("doctor").description("Check the install and say how to fix what
8455
8819
  if (checks.some((c) => !c.ok)) process.exitCode = 1;
8456
8820
  });
8457
8821
  program.command("logs").description("Show the service log").option("-f, --follow", "keep streaming").action(async (opts) => {
8458
- const file = join16(cayenneDir(), "logs", "runner.log");
8822
+ const file = join18(cayenneDir(), "logs", "runner.log");
8459
8823
  if (!existsSync5(file)) throw new Error(`no log yet at ${file}. Run \`cayenne up\` first.`);
8460
8824
  const print = (chunk) => {
8461
8825
  for (const line of chunk.split("\n")) {
@@ -8501,7 +8865,7 @@ program.command("update").description("Re-pin the installed app and restart the
8501
8865
  cliPath: record.cliPath,
8502
8866
  profile: currentProfile(),
8503
8867
  pathEnv: record.pathEnv,
8504
- logDir: join16(dir, "logs")
8868
+ logDir: join18(dir, "logs")
8505
8869
  })
8506
8870
  );
8507
8871
  await launchd.bootout(label);
@@ -8512,7 +8876,7 @@ program.command("uninstall").description("Stop the service and remove the launch
8512
8876
  requireMac();
8513
8877
  const { launchd, label } = service();
8514
8878
  await launchd.bootout(label);
8515
- rmSync5(plistPath(label, homedir4()), { force: true });
8879
+ rmSync7(plistPath(label, homedir4()), { force: true });
8516
8880
  if (!opts.purge) {
8517
8881
  console.log(`removed ${label}`);
8518
8882
  return;
@@ -8522,7 +8886,7 @@ program.command("uninstall").description("Stop the service and remove the launch
8522
8886
  if (dir === "/" || dir === home || home.startsWith(`${dir}/`)) {
8523
8887
  throw new Error(`refusing to purge ${dir}: it is the home directory or an ancestor of it`);
8524
8888
  }
8525
- if (!existsSync5(join16(dir, "config.json")) && !existsSync5(join16(dir, "app", "install.json"))) {
8889
+ if (!existsSync5(join18(dir, "config.json")) && !existsSync5(join18(dir, "app", "install.json"))) {
8526
8890
  throw new Error(`refusing to purge ${dir}: it does not look like a Cayenne profile`);
8527
8891
  }
8528
8892
  if (!opts.yes) {
@@ -8536,7 +8900,7 @@ program.command("uninstall").description("Stop the service and remove the launch
8536
8900
  "could not revoke the stored session (offline?) \u2014 sign out from the web app to be sure"
8537
8901
  );
8538
8902
  }
8539
- rmSync5(dir, { recursive: true, force: true });
8903
+ rmSync7(dir, { recursive: true, force: true });
8540
8904
  console.log(`removed ${label} and ${dir}`);
8541
8905
  });
8542
8906
  program.parseAsync(process.argv).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpcasabiancai/cayenne",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "files": [
5
5
  "dist/cli.js"
6
6
  ],
@@ -42,8 +42,8 @@
42
42
  "tsx": "4.23.13",
43
43
  "typescript": "~5.9.3",
44
44
  "vitest": "5.0.0",
45
- "@cayenne/shared": "0.0.1",
46
- "@cayenne/db": "0.0.1"
45
+ "@cayenne/db": "0.0.1",
46
+ "@cayenne/shared": "0.0.1"
47
47
  },
48
48
  "scripts": {
49
49
  "cli": "tsx src/cli.ts",