@jpcasabiancai/cayenne 0.3.0 → 0.3.1
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.
- package/dist/cli.js +126 -33
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -324,8 +324,35 @@ var init_command = __esm({
|
|
|
324
324
|
effort: Effort.optional(),
|
|
325
325
|
model: z4.string().optional(),
|
|
326
326
|
resumeProviderSessionId: z4.string().optional(),
|
|
327
|
+
// Duplicate a chat (D36): start from a fork of `resumeProviderSessionId` instead of appending
|
|
328
|
+
// to its transcript. Claude-only, no `worktree`. The runner additionally refuses a source that
|
|
329
|
+
// is not the caller's own Claude session on this runner, or a cwd other than the source's own directory.
|
|
330
|
+
forkSession: z4.boolean().optional(),
|
|
327
331
|
initialPrompt: z4.string().optional(),
|
|
328
332
|
title: z4.string().max(200).optional()
|
|
333
|
+
}).superRefine((p, ctx) => {
|
|
334
|
+
if (p.forkSession !== true) return;
|
|
335
|
+
if (p.resumeProviderSessionId === void 0) {
|
|
336
|
+
ctx.addIssue({
|
|
337
|
+
code: "custom",
|
|
338
|
+
message: "forkSession needs resumeProviderSessionId",
|
|
339
|
+
path: ["forkSession"]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (p.worktree !== void 0) {
|
|
343
|
+
ctx.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
message: "a forked session keeps its working directory",
|
|
346
|
+
path: ["worktree"]
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (p.provider === "codex") {
|
|
350
|
+
ctx.addIssue({
|
|
351
|
+
code: "custom",
|
|
352
|
+
message: "codex sessions cannot be forked",
|
|
353
|
+
path: ["provider"]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
329
356
|
}),
|
|
330
357
|
"session.prompt": z4.object({
|
|
331
358
|
text: z4.string().min(1).max(2e5),
|
|
@@ -634,6 +661,12 @@ var init_runner = __esm({
|
|
|
634
661
|
* cannot grant a terminal. Optional so an older row still parses.
|
|
635
662
|
*/
|
|
636
663
|
memberTerminalIds: z8.array(z8.uuid()).max(64).optional(),
|
|
664
|
+
/**
|
|
665
|
+
* Whether this runner honours `session.start { forkSession: true }` (D36, protocol §9). An older
|
|
666
|
+
* runner strips the field and then refuses the resume under R30, so a client sends
|
|
667
|
+
* `forkSession` only when this is true. Absent reads as false; optional so an older row parses.
|
|
668
|
+
*/
|
|
669
|
+
sessionFork: z8.boolean().optional(),
|
|
637
670
|
/**
|
|
638
671
|
* The raw Ed25519 public key of the running process (32 bytes, unpadded base64url), which signs
|
|
639
672
|
* the local server's `hello` (protocol §6.2). New on every start, republished by every capabilities refresh that changes the row;
|
|
@@ -650,6 +683,21 @@ var init_runner = __esm({
|
|
|
650
683
|
}
|
|
651
684
|
});
|
|
652
685
|
|
|
686
|
+
// ../../packages/shared/src/protocol/session.ts
|
|
687
|
+
import { z as z9 } from "zod";
|
|
688
|
+
var SlashCommand, SessionCommandsListed;
|
|
689
|
+
var init_session = __esm({
|
|
690
|
+
"../../packages/shared/src/protocol/session.ts"() {
|
|
691
|
+
"use strict";
|
|
692
|
+
SlashCommand = z9.object({
|
|
693
|
+
name: z9.string(),
|
|
694
|
+
description: z9.string(),
|
|
695
|
+
argumentHint: z9.string()
|
|
696
|
+
});
|
|
697
|
+
SessionCommandsListed = z9.object({ slashCommands: z9.array(SlashCommand) });
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
653
701
|
// ../../packages/shared/src/protocol/transport.ts
|
|
654
702
|
var init_transport = __esm({
|
|
655
703
|
"../../packages/shared/src/protocol/transport.ts"() {
|
|
@@ -668,6 +716,7 @@ var init_protocol = __esm({
|
|
|
668
716
|
init_github();
|
|
669
717
|
init_local();
|
|
670
718
|
init_runner();
|
|
719
|
+
init_session();
|
|
671
720
|
init_terminal();
|
|
672
721
|
init_transport();
|
|
673
722
|
}
|
|
@@ -817,7 +866,7 @@ import {
|
|
|
817
866
|
} from "fs";
|
|
818
867
|
import { homedir as homedir2 } from "os";
|
|
819
868
|
import { dirname, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
820
|
-
import { z as
|
|
869
|
+
import { z as z10 } from "zod";
|
|
821
870
|
function setProfileOverride(name) {
|
|
822
871
|
profileOverride = name;
|
|
823
872
|
}
|
|
@@ -892,24 +941,24 @@ var init_config = __esm({
|
|
|
892
941
|
init_cloud();
|
|
893
942
|
init_profile();
|
|
894
943
|
MEMBER_TERMINALS_ERROR = "allowMemberTerminals must be false or a list of user ids";
|
|
895
|
-
RunnerConfig =
|
|
896
|
-
supabaseUrl:
|
|
897
|
-
supabaseAnonKey:
|
|
898
|
-
workspaceId:
|
|
899
|
-
runnerId:
|
|
900
|
-
name:
|
|
901
|
-
roots:
|
|
902
|
-
port:
|
|
944
|
+
RunnerConfig = z10.object({
|
|
945
|
+
supabaseUrl: z10.url(),
|
|
946
|
+
supabaseAnonKey: z10.string().min(1),
|
|
947
|
+
workspaceId: z10.uuid().optional(),
|
|
948
|
+
runnerId: z10.uuid().optional(),
|
|
949
|
+
name: z10.string().optional(),
|
|
950
|
+
roots: z10.array(z10.string()).default([]),
|
|
951
|
+
port: z10.number().int().default(LOCAL_PORT),
|
|
903
952
|
/** Extra page origins the local server admits, beside the built-in list (`buildOrigins`). */
|
|
904
|
-
localOrigins:
|
|
905
|
-
allowBypassPermissions:
|
|
953
|
+
localOrigins: z10.array(z10.string()).default([]),
|
|
954
|
+
allowBypassPermissions: z10.boolean().default(false),
|
|
906
955
|
/** Absolute path to the `codex` binary, when it is not on the daemon's PATH. */
|
|
907
|
-
codexPath:
|
|
956
|
+
codexPath: z10.string().optional(),
|
|
908
957
|
/**
|
|
909
958
|
* The Claude Code config directory this profile's sessions run on (D33), handed to them as
|
|
910
959
|
* `CLAUDE_CONFIG_DIR`. Unset means the CLI's own default, `~/.claude`.
|
|
911
960
|
*/
|
|
912
|
-
claudeConfigDir:
|
|
961
|
+
claudeConfigDir: z10.string().optional(),
|
|
913
962
|
/**
|
|
914
963
|
* Who besides the runner owner may hold a terminal here (D34, plan ruling 14): `false`, or the
|
|
915
964
|
* user ids this machine's owner names. Machine-local like `allowBypassPermissions`: a workspace
|
|
@@ -917,15 +966,15 @@ var init_config = __esm({
|
|
|
917
966
|
* `true` is refused — there is no "every member" switch. A listed member's shell runs as this
|
|
918
967
|
* machine's OS user; the env allowlist is not a boundary.
|
|
919
968
|
*/
|
|
920
|
-
allowMemberTerminals:
|
|
969
|
+
allowMemberTerminals: z10.union(
|
|
921
970
|
[
|
|
922
|
-
|
|
923
|
-
|
|
971
|
+
z10.literal(false),
|
|
972
|
+
z10.array(z10.uuid({ error: MEMBER_TERMINALS_ERROR }), { error: MEMBER_TERMINALS_ERROR }).max(64, { error: MEMBER_TERMINALS_ERROR })
|
|
924
973
|
],
|
|
925
974
|
{ error: MEMBER_TERMINALS_ERROR }
|
|
926
975
|
).default(false),
|
|
927
976
|
/** Minutes a terminal nobody attends stays alive before the runner kills it (features §2). */
|
|
928
|
-
terminalIdleMinutes:
|
|
977
|
+
terminalIdleMinutes: z10.number().int().min(1).max(1440).default(10)
|
|
929
978
|
});
|
|
930
979
|
memberTerminalIdsOf = (cfg) => cfg.allowMemberTerminals === false ? [] : [...cfg.allowMemberTerminals];
|
|
931
980
|
configPath = (dir) => join3(dir, "config.json");
|
|
@@ -1170,7 +1219,7 @@ import {
|
|
|
1170
1219
|
} from "fs";
|
|
1171
1220
|
import { uptime } from "os";
|
|
1172
1221
|
import { join as join5 } from "path";
|
|
1173
|
-
import { z as
|
|
1222
|
+
import { z as z11 } from "zod";
|
|
1174
1223
|
function bootedAt(now = Date.now()) {
|
|
1175
1224
|
return new Date(Math.round((now - uptime() * 1e3) / 1e3) * 1e3).toISOString();
|
|
1176
1225
|
}
|
|
@@ -1313,13 +1362,13 @@ var LockRecord, BOOT_DRIFT_MS, ATTEMPTS, BACKOFF_MS, CLAIM_TTL_MS, errno, claimO
|
|
|
1313
1362
|
var init_lock = __esm({
|
|
1314
1363
|
"src/lock.ts"() {
|
|
1315
1364
|
"use strict";
|
|
1316
|
-
LockRecord =
|
|
1317
|
-
pid:
|
|
1318
|
-
profile:
|
|
1319
|
-
startedAt:
|
|
1365
|
+
LockRecord = z11.object({
|
|
1366
|
+
pid: z11.number().int().positive(),
|
|
1367
|
+
profile: z11.string(),
|
|
1368
|
+
startedAt: z11.string(),
|
|
1320
1369
|
/** `bootedAt()` when the lock was taken; a lock from another boot is stale whatever its pid. */
|
|
1321
|
-
bootedAt:
|
|
1322
|
-
version:
|
|
1370
|
+
bootedAt: z11.string(),
|
|
1371
|
+
version: z11.string()
|
|
1323
1372
|
});
|
|
1324
1373
|
BOOT_DRIFT_MS = 1e4;
|
|
1325
1374
|
ATTEMPTS = 8;
|
|
@@ -3962,11 +4011,12 @@ var init_refusals = __esm({
|
|
|
3962
4011
|
});
|
|
3963
4012
|
|
|
3964
4013
|
// src/sessions/handlers.ts
|
|
4014
|
+
import { ZodError } from "zod";
|
|
3965
4015
|
function parsePayload(row, kind) {
|
|
3966
4016
|
try {
|
|
3967
4017
|
return parseCommand(row, kind);
|
|
3968
4018
|
} catch (err) {
|
|
3969
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4019
|
+
const message = err instanceof ZodError ? err.issues.map((i) => i.message).join("; ") : err instanceof Error ? err.message : String(err);
|
|
3970
4020
|
throw new Error(message.startsWith("VALIDATION:") ? message : `VALIDATION: ${message}`);
|
|
3971
4021
|
}
|
|
3972
4022
|
}
|
|
@@ -4354,7 +4404,7 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
4354
4404
|
import { EventEmitter } from "events";
|
|
4355
4405
|
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
4356
4406
|
var ClaudeSession;
|
|
4357
|
-
var
|
|
4407
|
+
var init_session2 = __esm({
|
|
4358
4408
|
"src/claude/session.ts"() {
|
|
4359
4409
|
"use strict";
|
|
4360
4410
|
init_src();
|
|
@@ -4364,7 +4414,7 @@ var init_session = __esm({
|
|
|
4364
4414
|
constructor(opts) {
|
|
4365
4415
|
super();
|
|
4366
4416
|
this.opts = opts;
|
|
4367
|
-
this.providerSessionId = opts.resume ?? null;
|
|
4417
|
+
this.providerSessionId = opts.forkSession ? null : opts.resume ?? null;
|
|
4368
4418
|
}
|
|
4369
4419
|
opts;
|
|
4370
4420
|
provider = "claude";
|
|
@@ -4397,6 +4447,7 @@ var init_session = __esm({
|
|
|
4397
4447
|
// Present only when asked for, so the default call stays the CLI's own behaviour.
|
|
4398
4448
|
...this.opts.strictMcpConfig === void 0 ? {} : { strictMcpConfig: this.opts.strictMcpConfig },
|
|
4399
4449
|
...this.opts.mcpServers === void 0 ? {} : { mcpServers: this.opts.mcpServers },
|
|
4450
|
+
...this.opts.forkSession ? { forkSession: true } : {},
|
|
4400
4451
|
canUseTool,
|
|
4401
4452
|
abortController: this.abort,
|
|
4402
4453
|
stderr: (line) => {
|
|
@@ -4645,7 +4696,7 @@ function scrubSecrets(text) {
|
|
|
4645
4696
|
return SECRET_PATTERNS.reduce((acc, [pattern, to]) => acc.replace(pattern, to), text);
|
|
4646
4697
|
}
|
|
4647
4698
|
var STDERR_TAIL_MAX, SIGKILL_AFTER_MS, SECRET_PATTERNS, APPROVAL_METHODS, CodexSession;
|
|
4648
|
-
var
|
|
4699
|
+
var init_session3 = __esm({
|
|
4649
4700
|
"src/codex/session.ts"() {
|
|
4650
4701
|
"use strict";
|
|
4651
4702
|
init_src();
|
|
@@ -4991,8 +5042,8 @@ function createProviderSession(provider, opts) {
|
|
|
4991
5042
|
var init_create = __esm({
|
|
4992
5043
|
"src/providers/create.ts"() {
|
|
4993
5044
|
"use strict";
|
|
4994
|
-
init_session();
|
|
4995
5045
|
init_session2();
|
|
5046
|
+
init_session3();
|
|
4996
5047
|
}
|
|
4997
5048
|
});
|
|
4998
5049
|
|
|
@@ -5095,10 +5146,16 @@ var init_manager = __esm({
|
|
|
5095
5146
|
}
|
|
5096
5147
|
this.assertModeAllowed(p.permissionMode);
|
|
5097
5148
|
const provider = p.provider ?? "claude";
|
|
5149
|
+
if (p.forkSession && provider !== "claude") {
|
|
5150
|
+
throw new Error("VALIDATION: codex sessions cannot be forked");
|
|
5151
|
+
}
|
|
5098
5152
|
this.assertProviderAllowed(provider, createdBy);
|
|
5099
5153
|
const typed = resolveInsideRoots(p.cwd, this.deps.roots);
|
|
5100
5154
|
this.assertNotRemoving(typed.real);
|
|
5101
|
-
|
|
5155
|
+
const forking = p.forkSession === true && p.resumeProviderSessionId !== void 0;
|
|
5156
|
+
if (forking) {
|
|
5157
|
+
await this.assertForkable(p.resumeProviderSessionId, createdBy, typed.real);
|
|
5158
|
+
} else if (p.resumeProviderSessionId !== void 0) {
|
|
5102
5159
|
await this.assertResumable(sessionId, p.resumeProviderSessionId);
|
|
5103
5160
|
}
|
|
5104
5161
|
let { real: cwd, root } = typed;
|
|
@@ -5106,7 +5163,7 @@ var init_manager = __esm({
|
|
|
5106
5163
|
if (p.worktree) {
|
|
5107
5164
|
cwd = await this.startWorktree(typed.real, typed.root, p.worktree, createdBy);
|
|
5108
5165
|
inWorktree = true;
|
|
5109
|
-
} else if (p.resumeProviderSessionId !== void 0) {
|
|
5166
|
+
} else if (p.resumeProviderSessionId !== void 0 && !forking) {
|
|
5110
5167
|
const recorded = await this.recordedWorktree(sessionId);
|
|
5111
5168
|
if (recorded) ({ real: cwd, root } = recorded);
|
|
5112
5169
|
}
|
|
@@ -5127,6 +5184,7 @@ var init_manager = __esm({
|
|
|
5127
5184
|
permissionMode: p.permissionMode,
|
|
5128
5185
|
model: p.model,
|
|
5129
5186
|
resume: p.resumeProviderSessionId,
|
|
5187
|
+
...forking ? { forkSession: true } : {},
|
|
5130
5188
|
...provider === "codex" ? {
|
|
5131
5189
|
codexPath: this.deps.codexPath,
|
|
5132
5190
|
checkInput: this.codexGate(sessionId, cwd),
|
|
@@ -5137,7 +5195,9 @@ var init_manager = __esm({
|
|
|
5137
5195
|
started();
|
|
5138
5196
|
}
|
|
5139
5197
|
if (inWorktree) await this.recordSessionWorktree(sessionId, cwd, root);
|
|
5140
|
-
else if (p.resumeProviderSessionId === void 0)
|
|
5198
|
+
else if (p.resumeProviderSessionId === void 0 || forking) {
|
|
5199
|
+
this.forgetSessionWorktree(sessionId);
|
|
5200
|
+
}
|
|
5141
5201
|
live.status = "idle";
|
|
5142
5202
|
await this.updateSession(sessionId, { status: "idle", model: p.model ?? null });
|
|
5143
5203
|
await this.broadcast(sessionId, {
|
|
@@ -5584,6 +5644,37 @@ var init_manager = __esm({
|
|
|
5584
5644
|
throw new Error("VALIDATION: resumeProviderSessionId does not belong to this session");
|
|
5585
5645
|
}
|
|
5586
5646
|
}
|
|
5647
|
+
/**
|
|
5648
|
+
* D36, amending R30 for `forkSession: true`: the resume id may be held by another session, but
|
|
5649
|
+
* only one on this runner that `createdBy` owns — a member must not fork the runner owner's
|
|
5650
|
+
* conversation, or anyone else's. A command with no creator owns nothing. Only a Claude row with
|
|
5651
|
+
* a non-empty id can be a source. The new session must run where the source did, decided the way
|
|
5652
|
+
* a resume of the source would be: the worktree this runner recorded for it, else its canonical
|
|
5653
|
+
* `cwd`. Never `worktree_path`: the runner owner can rewrite that column on a member's chat, and
|
|
5654
|
+
* it must not steer where the member's fork runs. A source path that no longer resolves inside
|
|
5655
|
+
* the roots cannot be proved equal, so it is refused, not thrown through.
|
|
5656
|
+
*/
|
|
5657
|
+
async assertForkable(providerSessionId, createdBy, cwd) {
|
|
5658
|
+
if (providerSessionId === "") {
|
|
5659
|
+
throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
|
|
5660
|
+
}
|
|
5661
|
+
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();
|
|
5662
|
+
if (error) throw new Error(`PROVIDER_ERROR: could not read the session: ${error.message}`);
|
|
5663
|
+
if (!data || createdBy === null || data.owner_id !== createdBy) {
|
|
5664
|
+
throw new Error("VALIDATION: forkSession source is not a session you own on this runner");
|
|
5665
|
+
}
|
|
5666
|
+
let sourceCwd = (await this.recordedWorktree(data.id))?.real ?? null;
|
|
5667
|
+
if (sourceCwd === null) {
|
|
5668
|
+
try {
|
|
5669
|
+
sourceCwd = resolveInsideRoots(data.cwd, this.deps.roots).real;
|
|
5670
|
+
} catch {
|
|
5671
|
+
sourceCwd = null;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
if (sourceCwd !== cwd) {
|
|
5675
|
+
throw new Error("VALIDATION: a forked session keeps its working directory");
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5587
5678
|
/**
|
|
5588
5679
|
* Whether this runner may open a session on `provider` for `createdBy`.
|
|
5589
5680
|
*
|
|
@@ -7517,7 +7608,9 @@ async function compose(cfg, lock, host, preflight) {
|
|
|
7517
7608
|
claude: true,
|
|
7518
7609
|
providers,
|
|
7519
7610
|
// Who the runner's own config names (D34); every heartbeat refresh republishes it.
|
|
7520
|
-
memberTerminalIds: [...memberTerminalIds]
|
|
7611
|
+
memberTerminalIds: [...memberTerminalIds],
|
|
7612
|
+
// D36: this build forks a Claude session on `session.start { forkSession: true }`.
|
|
7613
|
+
sessionFork: true
|
|
7521
7614
|
});
|
|
7522
7615
|
const registered = capabilities();
|
|
7523
7616
|
let loggedCodex = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jpcasabiancai/cayenne",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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/
|
|
46
|
-
"@cayenne/
|
|
45
|
+
"@cayenne/db": "0.0.1",
|
|
46
|
+
"@cayenne/shared": "0.0.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"cli": "tsx src/cli.ts",
|