@holdgrenade/cli 1.0.48 → 1.0.49
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 +436 -45
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3242,7 +3242,7 @@ var require_stream = __commonJS({
|
|
|
3242
3242
|
};
|
|
3243
3243
|
duplex._final = function(callback) {
|
|
3244
3244
|
if (ws.readyState === ws.CONNECTING) {
|
|
3245
|
-
ws.once("open", function
|
|
3245
|
+
ws.once("open", function open6() {
|
|
3246
3246
|
duplex._final(callback);
|
|
3247
3247
|
});
|
|
3248
3248
|
return;
|
|
@@ -3263,7 +3263,7 @@ var require_stream = __commonJS({
|
|
|
3263
3263
|
};
|
|
3264
3264
|
duplex._write = function(chunk, encoding, callback) {
|
|
3265
3265
|
if (ws.readyState === ws.CONNECTING) {
|
|
3266
|
-
ws.once("open", function
|
|
3266
|
+
ws.once("open", function open6() {
|
|
3267
3267
|
duplex._write(chunk, encoding, callback);
|
|
3268
3268
|
});
|
|
3269
3269
|
return;
|
|
@@ -7025,7 +7025,7 @@ function isKnownAgent(kind) {
|
|
|
7025
7025
|
|
|
7026
7026
|
// src/cli.ts
|
|
7027
7027
|
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync18 } from "node:fs";
|
|
7028
|
-
import { dirname as
|
|
7028
|
+
import { dirname as dirname17 } from "node:path";
|
|
7029
7029
|
|
|
7030
7030
|
// node_modules/commander/lib/error.js
|
|
7031
7031
|
var CommanderError = class extends Error {
|
|
@@ -30900,6 +30900,78 @@ var VoiceTokenFrame = external_exports.object({
|
|
|
30900
30900
|
once: external_exports.literal(true).optional()
|
|
30901
30901
|
});
|
|
30902
30902
|
|
|
30903
|
+
// ../grenade-protocol/dist/canvas.js
|
|
30904
|
+
var CANVAS_FOLDER = ".grenade/canvas";
|
|
30905
|
+
var CANVAS_BOARDS_MAX = 500;
|
|
30906
|
+
var CANVAS_BOARD_MAX_BYTES = 2 * 1024 * 1024;
|
|
30907
|
+
var CANVAS_BOARD_FRAME_MAX_BYTES = 3e6;
|
|
30908
|
+
var CANVAS_SUBSCRIPTIONS_MAX = 8;
|
|
30909
|
+
var CANVAS_DEFAULT_WIDTH = 1280;
|
|
30910
|
+
var CANVAS_DEFAULT_HEIGHT = 800;
|
|
30911
|
+
var CANVAS_SIZE_MAX = 8e3;
|
|
30912
|
+
var CanvasCwd = external_exports.string().min(1).max(1024).refine((p) => p === "~" || p.startsWith("~/") || p.startsWith("/"), "absolute or starting with ~");
|
|
30913
|
+
var CanvasFileName = external_exports.string().min(1).max(255).refine((f) => !/[/\\\0]/.test(f) && !f.startsWith(".") && /\.html?$/i.test(f), "a board's file name in the canvas folder");
|
|
30914
|
+
var RequestId = external_exports.string().min(1).max(64);
|
|
30915
|
+
var CanvasBoard = external_exports.object({
|
|
30916
|
+
/** Its file name, "R3A · Cards.html". */
|
|
30917
|
+
file: CanvasFileName,
|
|
30918
|
+
/** What it is, from its name: "Cards". The file's name without its extension when the name has nothing after the revision. */
|
|
30919
|
+
name: external_exports.string().min(1).max(255),
|
|
30920
|
+
/** The revision its name starts with: 3 for "R3A · Cards.html". Absent for a name without one. */
|
|
30921
|
+
revision: external_exports.number().int().min(0).max(9999).optional(),
|
|
30922
|
+
/** The letters after the revision, as written: "A" for "R3A · Cards.html". Absent when there are none. */
|
|
30923
|
+
letter: external_exports.string().regex(/^[A-Za-z]{1,3}$/).optional(),
|
|
30924
|
+
/** The size it asks for (`<meta name="board" content="1280x800">`), else 1280 × 800. */
|
|
30925
|
+
width: external_exports.number().int().min(1).max(CANVAS_SIZE_MAX),
|
|
30926
|
+
height: external_exports.number().int().min(1).max(CANVAS_SIZE_MAX),
|
|
30927
|
+
/** When it was last saved. */
|
|
30928
|
+
modified: external_exports.string().datetime(),
|
|
30929
|
+
/** Its size on disk. */
|
|
30930
|
+
bytes: external_exports.number().int().min(0)
|
|
30931
|
+
});
|
|
30932
|
+
var CanvasRequestFrame = external_exports.object({ type: external_exports.literal("canvas"), id: RequestId, cwd: CanvasCwd });
|
|
30933
|
+
var CanvasSubscribeFrame = external_exports.object({ type: external_exports.literal("canvas.subscribe"), id: RequestId, cwd: CanvasCwd });
|
|
30934
|
+
var CanvasUnsubscribeFrame = external_exports.object({ type: external_exports.literal("canvas.unsubscribe"), cwd: CanvasCwd });
|
|
30935
|
+
var CanvasBoardRequestFrame = external_exports.object({ type: external_exports.literal("canvas.board"), id: RequestId, cwd: CanvasCwd, file: CanvasFileName });
|
|
30936
|
+
var CanvasFrame = external_exports.object({
|
|
30937
|
+
type: external_exports.literal("canvas"),
|
|
30938
|
+
/** The request's; absent on a frame sent because the canvas changed. */
|
|
30939
|
+
id: RequestId.optional(),
|
|
30940
|
+
/** As the client sent it. */
|
|
30941
|
+
cwd: external_exports.string().min(1),
|
|
30942
|
+
/** The canvas folder, absolute: `<cwd>/.grenade/canvas`. */
|
|
30943
|
+
folder: external_exports.string().min(1),
|
|
30944
|
+
boards: external_exports.array(CanvasBoard).max(CANVAS_BOARDS_MAX),
|
|
30945
|
+
/** The folder is not there (yet): no session has drawn on this canvas. `boards` is empty. */
|
|
30946
|
+
missing: external_exports.literal(true).optional(),
|
|
30947
|
+
truncated: external_exports.literal(true).optional()
|
|
30948
|
+
});
|
|
30949
|
+
var CanvasBoardFrame = external_exports.object({
|
|
30950
|
+
type: external_exports.literal("canvas.board"),
|
|
30951
|
+
id: RequestId,
|
|
30952
|
+
cwd: external_exports.string().min(1),
|
|
30953
|
+
file: CanvasFileName,
|
|
30954
|
+
/** The whole file, at most `CANVAS_BOARD_MAX_BYTES` bytes on disk. */
|
|
30955
|
+
html: external_exports.string().max(CANVAS_BOARD_MAX_BYTES),
|
|
30956
|
+
modified: external_exports.string().datetime(),
|
|
30957
|
+
bytes: external_exports.number().int().min(0).max(CANVAS_BOARD_MAX_BYTES)
|
|
30958
|
+
});
|
|
30959
|
+
function boardNameOf(file2) {
|
|
30960
|
+
const stem = file2.replace(/\.html?$/i, "") || file2;
|
|
30961
|
+
const match = /^[Rr](\d{1,4})([A-Za-z]{0,3})(?![A-Za-z0-9])/.exec(stem);
|
|
30962
|
+
if (!match)
|
|
30963
|
+
return { name: stem };
|
|
30964
|
+
const rest = stem.slice(match[0].length).replace(/^[\s·•\-–—_:|.]+/u, "").trim();
|
|
30965
|
+
return { name: rest || stem, revision: Number(match[1]), ...match[2] ? { letter: match[2] } : {} };
|
|
30966
|
+
}
|
|
30967
|
+
function boardSizeOf(html) {
|
|
30968
|
+
const meta3 = /<meta[^>]*name=["']board["'][^>]*>/i.exec(html);
|
|
30969
|
+
const content = meta3 && /content=["']\s*(\d{2,5})\s*[x×]\s*(\d{2,5})\s*["']/.exec(meta3[0]);
|
|
30970
|
+
if (!content)
|
|
30971
|
+
return { width: CANVAS_DEFAULT_WIDTH, height: CANVAS_DEFAULT_HEIGHT };
|
|
30972
|
+
return { width: Math.min(Number(content[1]), CANVAS_SIZE_MAX), height: Math.min(Number(content[2]), CANVAS_SIZE_MAX) };
|
|
30973
|
+
}
|
|
30974
|
+
|
|
30903
30975
|
// ../grenade-protocol/dist/relay.js
|
|
30904
30976
|
var RELAY_PROTOCOL_VERSION = 1;
|
|
30905
30977
|
var RELAY_DAEMON_PATH = "/v1/daemon";
|
|
@@ -31142,6 +31214,8 @@ var DaemonInfo = external_exports.object({
|
|
|
31142
31214
|
groupNames: external_exports.literal(1).optional(),
|
|
31143
31215
|
/** 1 when the daemon keeps voice providers' keys and answers `voice`, `voice.key` and `voice.token` (PROTOCOL.md "Voice providers"). Absent from older daemons. */
|
|
31144
31216
|
voice: external_exports.literal(1).optional(),
|
|
31217
|
+
/** 1 when the daemon serves a group's design canvas: `canvas`, `canvas.subscribe`, `canvas.board` (PROTOCOL.md "Canvas"). Absent from older daemons. */
|
|
31218
|
+
canvas: external_exports.literal(1).optional(),
|
|
31145
31219
|
/** Retired: 1 when the daemon sends `activity` for Codex sessions too. Still sent for apps that read it; new clients read `agents`. */
|
|
31146
31220
|
codexActivity: external_exports.literal(1).optional(),
|
|
31147
31221
|
/** Every agent the daemon can start and what it does with each (PROTOCOL.md "Agents"). Absent from older daemons. */
|
|
@@ -31282,6 +31356,10 @@ var ClientFrame = external_exports.discriminatedUnion("type", [
|
|
|
31282
31356
|
VoiceRequestFrame,
|
|
31283
31357
|
VoiceKeyFrame,
|
|
31284
31358
|
VoiceTokenRequestFrame,
|
|
31359
|
+
CanvasRequestFrame,
|
|
31360
|
+
CanvasSubscribeFrame,
|
|
31361
|
+
CanvasUnsubscribeFrame,
|
|
31362
|
+
CanvasBoardRequestFrame,
|
|
31285
31363
|
PingFrame
|
|
31286
31364
|
]);
|
|
31287
31365
|
var ErrorCode = external_exports.enum([
|
|
@@ -31307,7 +31385,7 @@ var ErrorFrame = external_exports.object({
|
|
|
31307
31385
|
code: ErrorCode,
|
|
31308
31386
|
message: external_exports.string(),
|
|
31309
31387
|
ref: external_exports.string().optional(),
|
|
31310
|
-
/** The `id` of the frame this error answers, when it had one (an `input`). */
|
|
31388
|
+
/** The `id` of the frame this error answers, when it had one (an `input`, a `voice.token`, a `canvas` request). */
|
|
31311
31389
|
id: external_exports.string().min(1).max(64).optional()
|
|
31312
31390
|
});
|
|
31313
31391
|
var SessionsFrame = external_exports.object({ type: external_exports.literal("sessions"), sessions: external_exports.array(Session) });
|
|
@@ -31378,6 +31456,8 @@ var DaemonFrame = external_exports.discriminatedUnion("type", [
|
|
|
31378
31456
|
FoldersFrame,
|
|
31379
31457
|
VoiceFrame,
|
|
31380
31458
|
VoiceTokenFrame,
|
|
31459
|
+
CanvasFrame,
|
|
31460
|
+
CanvasBoardFrame,
|
|
31381
31461
|
PongFrame
|
|
31382
31462
|
]);
|
|
31383
31463
|
var PairRequest = external_exports.object({ code: external_exports.string().regex(/^\d{6}$/), client: ClientInfo });
|
|
@@ -34478,12 +34558,12 @@ function terminalFromEnv() {
|
|
|
34478
34558
|
const env = process.env["GRENADE_TERMINAL"];
|
|
34479
34559
|
return env !== void 0 && isTerminalKind(env) ? env : void 0;
|
|
34480
34560
|
}
|
|
34481
|
-
function relayoutSteps(ordered,
|
|
34561
|
+
function relayoutSteps(ordered, open6) {
|
|
34482
34562
|
const [head, ...rest] = ordered;
|
|
34483
34563
|
if (head === void 0) return [];
|
|
34484
|
-
const keepHead =
|
|
34564
|
+
const keepHead = open6.has(head);
|
|
34485
34565
|
const steps = [];
|
|
34486
|
-
for (const id of keepHead ? rest : ordered) if (
|
|
34566
|
+
for (const id of keepHead ? rest : ordered) if (open6.has(id)) steps.push({ kind: "close", id });
|
|
34487
34567
|
if (!keepHead) steps.push({ kind: "tab", id: head });
|
|
34488
34568
|
rest.forEach((id, i) => steps.push({ kind: "split", id, nextTo: ordered[i] }));
|
|
34489
34569
|
return steps;
|
|
@@ -34637,9 +34717,9 @@ var TerminalMirror = class {
|
|
|
34637
34717
|
/** The nearest live member before `s` in group order that has a pane in `a`, else the last one that has. */
|
|
34638
34718
|
openMemberOf(s, a) {
|
|
34639
34719
|
if (s.group === void 0) return void 0;
|
|
34640
|
-
const
|
|
34641
|
-
const before =
|
|
34642
|
-
return (before.at(-1) ??
|
|
34720
|
+
const open6 = otherMembers(s.group, this.registry.list(), s.id).filter((m) => this.openIn.get(m.id) === a);
|
|
34721
|
+
const before = open6.filter((m) => byGroupOrder(m, s) < 0);
|
|
34722
|
+
return (before.at(-1) ?? open6.at(-1))?.id;
|
|
34643
34723
|
}
|
|
34644
34724
|
closeTab(id) {
|
|
34645
34725
|
const a = this.openIn.get(id);
|
|
@@ -34738,7 +34818,7 @@ function underService(env = process.env, pid = process.pid) {
|
|
|
34738
34818
|
}
|
|
34739
34819
|
|
|
34740
34820
|
// src/daemon/server.ts
|
|
34741
|
-
import { dirname as
|
|
34821
|
+
import { dirname as dirname16, join as join18 } from "node:path";
|
|
34742
34822
|
import { createServer as createServer2 } from "node:http";
|
|
34743
34823
|
import { networkInterfaces } from "node:os";
|
|
34744
34824
|
|
|
@@ -36729,13 +36809,13 @@ function switchModelDialog(below) {
|
|
|
36729
36809
|
}
|
|
36730
36810
|
function paragraphs(rows) {
|
|
36731
36811
|
const out = [];
|
|
36732
|
-
let
|
|
36812
|
+
let open6 = false;
|
|
36733
36813
|
for (const row of rows.map((r) => r.trim())) {
|
|
36734
|
-
if (!row)
|
|
36735
|
-
else if (
|
|
36814
|
+
if (!row) open6 = false;
|
|
36815
|
+
else if (open6) out[out.length - 1] += ` ${row}`;
|
|
36736
36816
|
else {
|
|
36737
36817
|
out.push(row);
|
|
36738
|
-
|
|
36818
|
+
open6 = true;
|
|
36739
36819
|
}
|
|
36740
36820
|
}
|
|
36741
36821
|
return out.map((p) => /[.!?:]$/.test(p) ? p : `${p}.`).join(" ");
|
|
@@ -36772,25 +36852,25 @@ async function switchClaudeModel(term, model, effort, timing = DEFAULT_TIMING) {
|
|
|
36772
36852
|
throw new ModelSwitchError("Claude Code's prompt is not empty. Send or clear what is typed there, then choose again.");
|
|
36773
36853
|
}
|
|
36774
36854
|
await term.key("enter");
|
|
36775
|
-
let
|
|
36776
|
-
if (!
|
|
36777
|
-
if (!
|
|
36778
|
-
for (let moves = 0; selectedModel(
|
|
36855
|
+
let open6 = await look2(modelPickerIn);
|
|
36856
|
+
if (!open6) throw new ModelSwitchError("Claude Code did not open its model picker.");
|
|
36857
|
+
if (!open6.sessionOnly) await giveUp("This Claude Code cannot switch one session only. Update Claude Code, then choose again.");
|
|
36858
|
+
for (let moves = 0; selectedModel(open6) !== model; moves++) {
|
|
36779
36859
|
if (moves >= MOST_MOVES) await giveUp(`Claude Code's picker has no ${model}.`);
|
|
36780
|
-
const down = rowsDownTo(
|
|
36860
|
+
const down = rowsDownTo(open6, model);
|
|
36781
36861
|
await term.key(down !== null && down > 0 ? "down" : "up");
|
|
36782
36862
|
await timing.wait(LOOK_EVERY_MS);
|
|
36783
|
-
|
|
36863
|
+
open6 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36784
36864
|
}
|
|
36785
36865
|
if (effort !== void 0) {
|
|
36786
|
-
for (let steps = 0;
|
|
36866
|
+
for (let steps = 0; open6.effort !== effort; steps++) {
|
|
36787
36867
|
if (steps >= MOST_EFFORT_STEPS) await giveUp(`${model} has no ${effort} effort in Claude Code.`);
|
|
36788
36868
|
await term.type(RIGHT);
|
|
36789
36869
|
await timing.wait(LOOK_EVERY_MS);
|
|
36790
|
-
|
|
36870
|
+
open6 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36791
36871
|
}
|
|
36792
36872
|
}
|
|
36793
|
-
const chosenEffort =
|
|
36873
|
+
const chosenEffort = open6.effort ?? void 0;
|
|
36794
36874
|
await term.type("s");
|
|
36795
36875
|
const closed = await look2((lines) => modelPickerIn(lines) ? null : lines);
|
|
36796
36876
|
if (!closed) await giveUp("Claude Code did not take the choice.");
|
|
@@ -38603,6 +38683,271 @@ function switchTimingProblem(session) {
|
|
|
38603
38683
|
return null;
|
|
38604
38684
|
}
|
|
38605
38685
|
|
|
38686
|
+
// src/canvas/canvasService.ts
|
|
38687
|
+
import { homedir as homedir9 } from "node:os";
|
|
38688
|
+
|
|
38689
|
+
// src/canvas/canvasAccess.ts
|
|
38690
|
+
import { posix } from "node:path";
|
|
38691
|
+
function normalizeCwd(cwd, home) {
|
|
38692
|
+
const expanded = expandHome(cwd, home);
|
|
38693
|
+
if (!expanded.startsWith("/")) return null;
|
|
38694
|
+
return posix.resolve(expanded);
|
|
38695
|
+
}
|
|
38696
|
+
function sharedFolder(paths2) {
|
|
38697
|
+
if (paths2.length === 0) return null;
|
|
38698
|
+
let shared = paths2[0].split("/").filter(Boolean);
|
|
38699
|
+
for (const path2 of paths2.slice(1)) {
|
|
38700
|
+
const parts = path2.split("/").filter(Boolean);
|
|
38701
|
+
let i = 0;
|
|
38702
|
+
while (i < shared.length && i < parts.length && shared[i] === parts[i]) i++;
|
|
38703
|
+
shared = shared.slice(0, i);
|
|
38704
|
+
}
|
|
38705
|
+
return shared.length === 0 ? null : `/${shared.join("/")}`;
|
|
38706
|
+
}
|
|
38707
|
+
function canvasCwds(sessions, home) {
|
|
38708
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
38709
|
+
const groups = /* @__PURE__ */ new Map();
|
|
38710
|
+
for (const s of sessions) {
|
|
38711
|
+
const cwd = normalizeCwd(s.cwd, home);
|
|
38712
|
+
if (!cwd) continue;
|
|
38713
|
+
allowed.add(cwd);
|
|
38714
|
+
const key = s.group ?? s.id;
|
|
38715
|
+
groups.set(key, [...groups.get(key) ?? [], cwd]);
|
|
38716
|
+
}
|
|
38717
|
+
for (const cwds of groups.values()) {
|
|
38718
|
+
const shared = cwds.length > 1 ? sharedFolder(cwds) : null;
|
|
38719
|
+
if (shared) allowed.add(shared);
|
|
38720
|
+
}
|
|
38721
|
+
return allowed;
|
|
38722
|
+
}
|
|
38723
|
+
function allowedCanvasCwd(cwd, sessions, home) {
|
|
38724
|
+
const normalized = normalizeCwd(cwd, home);
|
|
38725
|
+
return normalized && canvasCwds(sessions, home).has(normalized) ? normalized : null;
|
|
38726
|
+
}
|
|
38727
|
+
function canvasFolderOf(cwd) {
|
|
38728
|
+
return posix.join(cwd, CANVAS_FOLDER);
|
|
38729
|
+
}
|
|
38730
|
+
|
|
38731
|
+
// src/canvas/canvasFolder.ts
|
|
38732
|
+
import { constants as constants3 } from "node:fs";
|
|
38733
|
+
import { lstat, open as open5, readdir as readdir5 } from "node:fs/promises";
|
|
38734
|
+
import { dirname as dirname12, join as join16 } from "node:path";
|
|
38735
|
+
|
|
38736
|
+
// src/canvas/boardListing.ts
|
|
38737
|
+
var finderOrder2 = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
|
|
38738
|
+
function isBoardFile(name) {
|
|
38739
|
+
return CanvasFileName.safeParse(name).success;
|
|
38740
|
+
}
|
|
38741
|
+
function boardsFrom(files, max = CANVAS_BOARDS_MAX) {
|
|
38742
|
+
const sorted = files.filter((f) => isBoardFile(f.file)).sort((a, b) => finderOrder2.compare(a.file, b.file));
|
|
38743
|
+
const boards = sorted.slice(0, max).map((f) => ({
|
|
38744
|
+
file: f.file,
|
|
38745
|
+
...boardNameOf(f.file),
|
|
38746
|
+
...boardSizeOf(f.head),
|
|
38747
|
+
modified: new Date(f.mtimeMs).toISOString(),
|
|
38748
|
+
bytes: f.bytes
|
|
38749
|
+
}));
|
|
38750
|
+
return { boards, truncated: sorted.length > max };
|
|
38751
|
+
}
|
|
38752
|
+
function listingKey(listing) {
|
|
38753
|
+
if (listing.missing) return "missing";
|
|
38754
|
+
return listing.boards.map((b) => `${b.file}
|
|
38755
|
+
${b.modified}
|
|
38756
|
+
${b.bytes}`).join("\n\n");
|
|
38757
|
+
}
|
|
38758
|
+
function megabytes(bytes) {
|
|
38759
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
38760
|
+
}
|
|
38761
|
+
function boardTooLarge(file2, fileBytes, frameBytes, computer = "Mac") {
|
|
38762
|
+
if (fileBytes <= CANVAS_BOARD_MAX_BYTES && frameBytes <= CANVAS_BOARD_FRAME_MAX_BYTES) return null;
|
|
38763
|
+
const name = file2.replace(/\.html?$/i, "");
|
|
38764
|
+
return `"${name}" is too large to show here (${megabytes(Math.max(fileBytes, frameBytes))} MB; at most ${megabytes(CANVAS_BOARD_MAX_BYTES).replace(/\.0$/, "")} MB). Open it on the ${computer}.`;
|
|
38765
|
+
}
|
|
38766
|
+
|
|
38767
|
+
// src/canvas/canvasFolder.ts
|
|
38768
|
+
var HEAD_BYTES = 8192;
|
|
38769
|
+
var CanvasError = class extends Error {
|
|
38770
|
+
};
|
|
38771
|
+
async function realFolder(path2) {
|
|
38772
|
+
const st = await lstat(path2).catch((e) => {
|
|
38773
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
38774
|
+
throw e;
|
|
38775
|
+
});
|
|
38776
|
+
if (!st) return false;
|
|
38777
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new CanvasError(`${path2} is not a folder of its own, so it is not served as a canvas.`);
|
|
38778
|
+
return true;
|
|
38779
|
+
}
|
|
38780
|
+
async function canvasThere(folder) {
|
|
38781
|
+
return await realFolder(dirname12(folder)) && await realFolder(folder);
|
|
38782
|
+
}
|
|
38783
|
+
async function openBoard(path2) {
|
|
38784
|
+
const handle = await open5(path2, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK).catch(() => null);
|
|
38785
|
+
if (!handle) return null;
|
|
38786
|
+
const st = await handle.stat();
|
|
38787
|
+
if (!st.isFile()) {
|
|
38788
|
+
await handle.close();
|
|
38789
|
+
return null;
|
|
38790
|
+
}
|
|
38791
|
+
return {
|
|
38792
|
+
size: st.size,
|
|
38793
|
+
mtimeMs: st.mtimeMs,
|
|
38794
|
+
async read(max) {
|
|
38795
|
+
const buffer = Buffer.alloc(Math.min(max, st.size));
|
|
38796
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
38797
|
+
return buffer.subarray(0, bytesRead);
|
|
38798
|
+
},
|
|
38799
|
+
close: () => handle.close()
|
|
38800
|
+
};
|
|
38801
|
+
}
|
|
38802
|
+
async function listCanvas(folder, heads = /* @__PURE__ */ new Map()) {
|
|
38803
|
+
if (!await canvasThere(folder)) return { boards: [], missing: true };
|
|
38804
|
+
const entries = await readdir5(folder, { withFileTypes: true });
|
|
38805
|
+
const files = [];
|
|
38806
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38807
|
+
for (const e of entries) {
|
|
38808
|
+
if (!e.isFile() || !isBoardFile(e.name)) continue;
|
|
38809
|
+
const st = await lstat(join16(folder, e.name)).catch(() => null);
|
|
38810
|
+
if (!st?.isFile()) continue;
|
|
38811
|
+
const key = `${e.name}
|
|
38812
|
+
${st.mtimeMs}
|
|
38813
|
+
${st.size}`;
|
|
38814
|
+
seen.add(key);
|
|
38815
|
+
let head = heads.get(key);
|
|
38816
|
+
if (head === void 0) {
|
|
38817
|
+
const board = await openBoard(join16(folder, e.name));
|
|
38818
|
+
if (!board) continue;
|
|
38819
|
+
try {
|
|
38820
|
+
head = (await board.read(HEAD_BYTES)).toString("utf8");
|
|
38821
|
+
} finally {
|
|
38822
|
+
await board.close();
|
|
38823
|
+
}
|
|
38824
|
+
heads.set(key, head);
|
|
38825
|
+
}
|
|
38826
|
+
files.push({ file: e.name, mtimeMs: st.mtimeMs, bytes: st.size, head });
|
|
38827
|
+
}
|
|
38828
|
+
for (const key of heads.keys()) if (!seen.has(key)) heads.delete(key);
|
|
38829
|
+
const { boards, truncated } = boardsFrom(files);
|
|
38830
|
+
return { boards, ...truncated ? { truncated: true } : {} };
|
|
38831
|
+
}
|
|
38832
|
+
async function readBoard(folder, file2, computer = "Mac") {
|
|
38833
|
+
const notHere = new CanvasError(`"${file2}" is not on this canvas.`);
|
|
38834
|
+
if (!isBoardFile(file2) || !await canvasThere(folder)) throw notHere;
|
|
38835
|
+
const board = await openBoard(join16(folder, file2));
|
|
38836
|
+
if (!board) throw notHere;
|
|
38837
|
+
try {
|
|
38838
|
+
const tooLarge = boardTooLarge(file2, board.size, 0, computer);
|
|
38839
|
+
if (tooLarge) throw new CanvasError(tooLarge);
|
|
38840
|
+
const data = await board.read(CANVAS_BOARD_MAX_BYTES);
|
|
38841
|
+
return { html: data.toString("utf8"), modified: new Date(board.mtimeMs).toISOString(), bytes: data.length };
|
|
38842
|
+
} finally {
|
|
38843
|
+
await board.close();
|
|
38844
|
+
}
|
|
38845
|
+
}
|
|
38846
|
+
|
|
38847
|
+
// src/canvas/canvasWatcher.ts
|
|
38848
|
+
var CANVAS_POLL_MS = 400;
|
|
38849
|
+
var realEvery = (fn, ms) => {
|
|
38850
|
+
const timer = setInterval(fn, ms);
|
|
38851
|
+
timer.unref?.();
|
|
38852
|
+
return () => clearInterval(timer);
|
|
38853
|
+
};
|
|
38854
|
+
var CanvasWatcher = class {
|
|
38855
|
+
constructor(every = realEvery, list = listCanvas, onError = () => {
|
|
38856
|
+
}) {
|
|
38857
|
+
this.every = every;
|
|
38858
|
+
this.list = list;
|
|
38859
|
+
this.onError = onError;
|
|
38860
|
+
}
|
|
38861
|
+
every;
|
|
38862
|
+
list;
|
|
38863
|
+
onError;
|
|
38864
|
+
folders = /* @__PURE__ */ new Map();
|
|
38865
|
+
/** Starts telling `onChange` of every change to `folder` after `current`, the listing the subscriber has. Returns how to stop. */
|
|
38866
|
+
watch(folder, current, onChange) {
|
|
38867
|
+
let watched = this.folders.get(folder);
|
|
38868
|
+
if (!watched) {
|
|
38869
|
+
const created = { subscribers: /* @__PURE__ */ new Set(), heads: /* @__PURE__ */ new Map(), busy: false, stop: () => {
|
|
38870
|
+
} };
|
|
38871
|
+
created.stop = this.every(() => void this.look(folder, created), CANVAS_POLL_MS);
|
|
38872
|
+
this.folders.set(folder, created);
|
|
38873
|
+
watched = created;
|
|
38874
|
+
}
|
|
38875
|
+
const subscriber = { lastKey: listingKey(current), onChange };
|
|
38876
|
+
watched.subscribers.add(subscriber);
|
|
38877
|
+
const w = watched;
|
|
38878
|
+
return () => {
|
|
38879
|
+
w.subscribers.delete(subscriber);
|
|
38880
|
+
if (w.subscribers.size === 0 && this.folders.get(folder) === w) {
|
|
38881
|
+
w.stop();
|
|
38882
|
+
this.folders.delete(folder);
|
|
38883
|
+
}
|
|
38884
|
+
};
|
|
38885
|
+
}
|
|
38886
|
+
/** How many folders are being polled. */
|
|
38887
|
+
get size() {
|
|
38888
|
+
return this.folders.size;
|
|
38889
|
+
}
|
|
38890
|
+
/** Looks at one folder now; a look still running makes this one a no-op. */
|
|
38891
|
+
async look(folder, watched = this.folders.get(folder)) {
|
|
38892
|
+
if (!watched || watched.busy) return;
|
|
38893
|
+
watched.busy = true;
|
|
38894
|
+
try {
|
|
38895
|
+
const listing = await this.list(folder, watched.heads);
|
|
38896
|
+
const key = listingKey(listing);
|
|
38897
|
+
for (const s of watched.subscribers) {
|
|
38898
|
+
if (s.lastKey === key) continue;
|
|
38899
|
+
s.lastKey = key;
|
|
38900
|
+
s.onChange(listing);
|
|
38901
|
+
}
|
|
38902
|
+
} catch (e) {
|
|
38903
|
+
this.onError(folder, e);
|
|
38904
|
+
} finally {
|
|
38905
|
+
watched.busy = false;
|
|
38906
|
+
}
|
|
38907
|
+
}
|
|
38908
|
+
/** Stops every poll (the daemon is stopping). */
|
|
38909
|
+
stopAll() {
|
|
38910
|
+
for (const w of this.folders.values()) w.stop();
|
|
38911
|
+
this.folders.clear();
|
|
38912
|
+
}
|
|
38913
|
+
};
|
|
38914
|
+
|
|
38915
|
+
// src/canvas/canvasService.ts
|
|
38916
|
+
var CanvasService = class {
|
|
38917
|
+
constructor(sessions, watcher = new CanvasWatcher(), home = homedir9(), computer = computerWord()) {
|
|
38918
|
+
this.sessions = sessions;
|
|
38919
|
+
this.watcher = watcher;
|
|
38920
|
+
this.home = home;
|
|
38921
|
+
this.computer = computer;
|
|
38922
|
+
}
|
|
38923
|
+
sessions;
|
|
38924
|
+
watcher;
|
|
38925
|
+
home;
|
|
38926
|
+
computer;
|
|
38927
|
+
/** The canvas folder of `cwd`, or `CanvasError` when no session works there. */
|
|
38928
|
+
folderOf(cwd) {
|
|
38929
|
+
const allowed = allowedCanvasCwd(cwd, this.sessions(), this.home);
|
|
38930
|
+
if (!allowed) throw new CanvasError(`No session on this ${this.computer} works in ${cwd}, so it has no canvas to show.`);
|
|
38931
|
+
return canvasFolderOf(allowed);
|
|
38932
|
+
}
|
|
38933
|
+
async list(cwd) {
|
|
38934
|
+
const folder = this.folderOf(cwd);
|
|
38935
|
+
return { folder, ...await listCanvas(folder) };
|
|
38936
|
+
}
|
|
38937
|
+
async board(cwd, file2) {
|
|
38938
|
+
return readBoard(this.folderOf(cwd), file2, this.computer);
|
|
38939
|
+
}
|
|
38940
|
+
/** The canvas now, and `onChange` with each change after it until `stop`. */
|
|
38941
|
+
async watch(cwd, onChange) {
|
|
38942
|
+
const reply2 = await this.list(cwd);
|
|
38943
|
+
const stop2 = this.watcher.watch(reply2.folder, reply2, (listing) => onChange({ folder: reply2.folder, ...listing }));
|
|
38944
|
+
return { reply: reply2, stop: stop2 };
|
|
38945
|
+
}
|
|
38946
|
+
stop() {
|
|
38947
|
+
this.watcher.stopAll();
|
|
38948
|
+
}
|
|
38949
|
+
};
|
|
38950
|
+
|
|
38606
38951
|
// src/daemon/wsHandler.ts
|
|
38607
38952
|
var HELLO_TIMEOUT_MS = 5e3;
|
|
38608
38953
|
var NO_PUSH = { type: "push.state", registered: false, delivery: "off", events: [] };
|
|
@@ -38629,6 +38974,8 @@ var Connection = class {
|
|
|
38629
38974
|
/** Live terminals this client has open, by session. */
|
|
38630
38975
|
terms = /* @__PURE__ */ new Map();
|
|
38631
38976
|
stopWatchingPush;
|
|
38977
|
+
/** Canvases this client watches, by `cwd` as it sent it (PROTOCOL.md "Canvas", "Watching"). */
|
|
38978
|
+
canvasWatches = /* @__PURE__ */ new Map();
|
|
38632
38979
|
helloTimer;
|
|
38633
38980
|
onUpdated = (s) => this.send({ type: "session.updated", session: s });
|
|
38634
38981
|
onRemoved = (id) => {
|
|
@@ -38676,7 +39023,7 @@ var Connection = class {
|
|
|
38676
39023
|
try {
|
|
38677
39024
|
await this.dispatch(frame);
|
|
38678
39025
|
} catch (e) {
|
|
38679
|
-
const id = frame.type === "input" ? frame.id : void 0;
|
|
39026
|
+
const id = frame.type === "input" || frame.type === "canvas" || frame.type === "canvas.subscribe" || frame.type === "canvas.board" ? frame.id : void 0;
|
|
38680
39027
|
if (e instanceof UnknownSessionError) return this.fail("unknown_session", e.message, frame.type, false, id);
|
|
38681
39028
|
if (e instanceof SessionExistsError) return this.fail("tmux_failed", e.message, frame.type, false, id);
|
|
38682
39029
|
if (e instanceof BadCwdError || e instanceof UnknownGroupError) return this.fail("bad_frame", e.message, frame.type, false, id);
|
|
@@ -38699,6 +39046,8 @@ var Connection = class {
|
|
|
38699
39046
|
for (const id of this.subscriptions) this.d.registry.unsubscribe(id);
|
|
38700
39047
|
this.subscriptions.clear();
|
|
38701
39048
|
for (const id of [...this.terms.keys()]) this.closeTerm(id);
|
|
39049
|
+
for (const stop2 of this.canvasWatches.values()) stop2();
|
|
39050
|
+
this.canvasWatches.clear();
|
|
38702
39051
|
if (this.authed) {
|
|
38703
39052
|
this.d.registry.off("updated", this.onUpdated);
|
|
38704
39053
|
this.d.registry.off("removed", this.onRemoved);
|
|
@@ -38751,7 +39100,7 @@ var Connection = class {
|
|
|
38751
39100
|
}
|
|
38752
39101
|
this.d.prompts?.on("opened", this.onPrompt);
|
|
38753
39102
|
this.d.prompts?.on("closed", this.onPrompt);
|
|
38754
|
-
for (const
|
|
39103
|
+
for (const open6 of this.d.prompts?.list() ?? []) this.send(open6);
|
|
38755
39104
|
this.d.activity?.on("activity", this.onActivity);
|
|
38756
39105
|
}
|
|
38757
39106
|
async dispatch(frame) {
|
|
@@ -38875,6 +39224,11 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
38875
39224
|
throw e;
|
|
38876
39225
|
}
|
|
38877
39226
|
}
|
|
39227
|
+
case "canvas":
|
|
39228
|
+
case "canvas.subscribe":
|
|
39229
|
+
case "canvas.unsubscribe":
|
|
39230
|
+
case "canvas.board":
|
|
39231
|
+
return this.handleCanvas(frame);
|
|
38878
39232
|
case "session.group": {
|
|
38879
39233
|
const before = r.get(frame.sessionId);
|
|
38880
39234
|
const after = r.setGroup(frame.sessionId, frame.group, frame.index);
|
|
@@ -38930,6 +39284,40 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
38930
39284
|
}
|
|
38931
39285
|
}
|
|
38932
39286
|
}
|
|
39287
|
+
/** The canvas frames (PROTOCOL.md "Canvas"): a refused folder, a missing or too large board is `bad_frame` with the request's `id`. */
|
|
39288
|
+
async handleCanvas(frame) {
|
|
39289
|
+
if (frame.type === "canvas.unsubscribe") {
|
|
39290
|
+
this.canvasWatches.get(frame.cwd)?.();
|
|
39291
|
+
this.canvasWatches.delete(frame.cwd);
|
|
39292
|
+
return;
|
|
39293
|
+
}
|
|
39294
|
+
const canvas = this.d.canvas;
|
|
39295
|
+
if (!canvas) return this.fail("bad_frame", "this daemon serves no canvas", frame.type, false, frame.id);
|
|
39296
|
+
const { cwd, id } = frame;
|
|
39297
|
+
const framed = (reply2, withId) => ({ type: "canvas", ...withId ? { id } : {}, cwd, ...reply2 });
|
|
39298
|
+
try {
|
|
39299
|
+
if (frame.type === "canvas") return this.send(framed(await canvas.list(cwd), true));
|
|
39300
|
+
if (frame.type === "canvas.subscribe") {
|
|
39301
|
+
const again = this.canvasWatches.get(cwd);
|
|
39302
|
+
if (!again && this.canvasWatches.size >= CANVAS_SUBSCRIPTIONS_MAX) {
|
|
39303
|
+
return this.fail("bad_frame", `A connection watches at most ${CANVAS_SUBSCRIPTIONS_MAX} canvases; unsubscribe from one first.`, frame.type, false, id);
|
|
39304
|
+
}
|
|
39305
|
+
const { reply: reply3, stop: stop2 } = await canvas.watch(cwd, (changed) => this.send(framed(changed, false)));
|
|
39306
|
+
if (!this.authed) return stop2();
|
|
39307
|
+
again?.();
|
|
39308
|
+
this.canvasWatches.set(cwd, stop2);
|
|
39309
|
+
return this.send(framed(reply3, true));
|
|
39310
|
+
}
|
|
39311
|
+
const board = await canvas.board(cwd, frame.file);
|
|
39312
|
+
const reply2 = { type: "canvas.board", id, cwd, file: frame.file, ...board };
|
|
39313
|
+
const tooLarge = boardTooLarge(frame.file, board.bytes, Buffer.byteLength(JSON.stringify(reply2)), computerWord());
|
|
39314
|
+
if (tooLarge) return this.fail("bad_frame", tooLarge, frame.type, false, id);
|
|
39315
|
+
return this.send(reply2);
|
|
39316
|
+
} catch (e) {
|
|
39317
|
+
if (e instanceof CanvasError) return this.fail("bad_frame", e.message, frame.type, false, id);
|
|
39318
|
+
throw e;
|
|
39319
|
+
}
|
|
39320
|
+
}
|
|
38933
39321
|
openTerm(sessionId, cols, rows) {
|
|
38934
39322
|
if (!this.d.openTerm) return this.fail("bad_frame", "this daemon streams no terminal", "term.open");
|
|
38935
39323
|
this.requireSession(sessionId);
|
|
@@ -39016,7 +39404,7 @@ var PairingWatch = class {
|
|
|
39016
39404
|
// src/attachments/attachmentStore.ts
|
|
39017
39405
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
39018
39406
|
import { existsSync as existsSync19 } from "node:fs";
|
|
39019
|
-
import { join as
|
|
39407
|
+
import { join as join17 } from "node:path";
|
|
39020
39408
|
|
|
39021
39409
|
// src/attachments/attachmentName.ts
|
|
39022
39410
|
var EXTENSION_FOR_MIME = {
|
|
@@ -39050,7 +39438,7 @@ function stamp(d) {
|
|
|
39050
39438
|
function createAttachmentStore(dir, now = () => /* @__PURE__ */ new Date()) {
|
|
39051
39439
|
return {
|
|
39052
39440
|
async save(sessionId, name, mime, data) {
|
|
39053
|
-
const folder =
|
|
39441
|
+
const folder = join17(dir, sessionId);
|
|
39054
39442
|
await mkdir(folder, { recursive: true });
|
|
39055
39443
|
const path2 = unusedPath(folder, attachmentFileName(name, mime, now()));
|
|
39056
39444
|
await writeFile(path2, data, { flag: "wx" });
|
|
@@ -39062,8 +39450,8 @@ function unusedPath(folder, fileName) {
|
|
|
39062
39450
|
const dot = fileName.lastIndexOf(".");
|
|
39063
39451
|
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
39064
39452
|
const ext = dot > 0 ? fileName.slice(dot) : "";
|
|
39065
|
-
let candidate =
|
|
39066
|
-
for (let n = 2; existsSync19(candidate); n++) candidate =
|
|
39453
|
+
let candidate = join17(folder, fileName);
|
|
39454
|
+
for (let n = 2; existsSync19(candidate); n++) candidate = join17(folder, `${stem}-${n}${ext}`);
|
|
39067
39455
|
return candidate;
|
|
39068
39456
|
}
|
|
39069
39457
|
|
|
@@ -39078,14 +39466,14 @@ function accessHash(token) {
|
|
|
39078
39466
|
|
|
39079
39467
|
// src/relay/e2eKey.ts
|
|
39080
39468
|
import { existsSync as existsSync20, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync15 } from "node:fs";
|
|
39081
|
-
import { dirname as
|
|
39469
|
+
import { dirname as dirname13 } from "node:path";
|
|
39082
39470
|
function loadOrCreateE2EKey(path2) {
|
|
39083
39471
|
if (existsSync20(path2)) {
|
|
39084
39472
|
const raw = Buffer.from(readFileSync14(path2, "utf8").trim(), "base64");
|
|
39085
39473
|
if (raw.length === 32) return x25519FromRaw(raw);
|
|
39086
39474
|
}
|
|
39087
39475
|
const pair2 = generateX25519();
|
|
39088
|
-
mkdirSync13(
|
|
39476
|
+
mkdirSync13(dirname13(path2), { recursive: true });
|
|
39089
39477
|
writeFileSync15(path2, privateRaw(pair2.privateKey).toString("base64") + "\n", { mode: 384 });
|
|
39090
39478
|
return pair2;
|
|
39091
39479
|
}
|
|
@@ -39334,7 +39722,7 @@ var RelayLink = class {
|
|
|
39334
39722
|
|
|
39335
39723
|
// src/push/boardDevices.ts
|
|
39336
39724
|
import { existsSync as existsSync21, mkdirSync as mkdirSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync16 } from "node:fs";
|
|
39337
|
-
import { dirname as
|
|
39725
|
+
import { dirname as dirname14 } from "node:path";
|
|
39338
39726
|
var BoardDevices = class {
|
|
39339
39727
|
constructor(path2) {
|
|
39340
39728
|
this.path = path2;
|
|
@@ -39377,7 +39765,7 @@ var BoardDevices = class {
|
|
|
39377
39765
|
}
|
|
39378
39766
|
save() {
|
|
39379
39767
|
if (!this.path) return;
|
|
39380
|
-
mkdirSync14(
|
|
39768
|
+
mkdirSync14(dirname14(this.path), { recursive: true });
|
|
39381
39769
|
writeFileSync16(this.path, JSON.stringify(this.list(), null, 2) + "\n", { mode: 384 });
|
|
39382
39770
|
}
|
|
39383
39771
|
};
|
|
@@ -39630,7 +40018,7 @@ var BoardPusher = class {
|
|
|
39630
40018
|
|
|
39631
40019
|
// src/push/pushDevices.ts
|
|
39632
40020
|
import { existsSync as existsSync22, mkdirSync as mkdirSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync17 } from "node:fs";
|
|
39633
|
-
import { dirname as
|
|
40021
|
+
import { dirname as dirname15 } from "node:path";
|
|
39634
40022
|
var PushDevices = class {
|
|
39635
40023
|
constructor(path2) {
|
|
39636
40024
|
this.path = path2;
|
|
@@ -39673,7 +40061,7 @@ var PushDevices = class {
|
|
|
39673
40061
|
}
|
|
39674
40062
|
save() {
|
|
39675
40063
|
if (!this.path) return;
|
|
39676
|
-
mkdirSync15(
|
|
40064
|
+
mkdirSync15(dirname15(this.path), { recursive: true });
|
|
39677
40065
|
writeFileSync17(this.path, JSON.stringify(this.list(), null, 2) + "\n", { mode: 384 });
|
|
39678
40066
|
}
|
|
39679
40067
|
};
|
|
@@ -40291,7 +40679,7 @@ async function startDaemon(opts = {}) {
|
|
|
40291
40679
|
const controlPort2 = opts.controlPort ?? CONTROL_PORT;
|
|
40292
40680
|
const log = opts.log ?? createLogger({ file: paths.log, level: process.env["GRENADE_LOG"] ?? "info" });
|
|
40293
40681
|
const e2eKey = loadOrCreateE2EKey(opts.e2eKeyPath ?? paths.e2eKey);
|
|
40294
|
-
const info = { id: loadDaemonId(), name: opts.name ?? defaultName(), version: VERSION, os: daemonOs(), key: e2eKey.publicKey.toString("base64"), e2e: 1, inputSent: 1, conversations: 1, conversationDelete: 1, term: 1, folders: 1, board: 1, groupNames: 1, voice: 1, codexActivity: 1, agents: [...AGENTS] };
|
|
40682
|
+
const info = { id: loadDaemonId(), name: opts.name ?? defaultName(), version: VERSION, os: daemonOs(), key: e2eKey.publicKey.toString("base64"), e2e: 1, inputSent: 1, conversations: 1, conversationDelete: 1, term: 1, folders: 1, board: 1, groupNames: 1, voice: 1, canvas: 1, codexActivity: 1, agents: [...AGENTS] };
|
|
40295
40683
|
const allowPlainLan = opts.allowPlainLan === true;
|
|
40296
40684
|
const tmux = opts.tmux ?? createTmux({ agentFlags: { claude: claudeHookFlags(port), codex: codexHookFlags(port) }, serverScope: underSystemd() });
|
|
40297
40685
|
const tokens = new TokenStore(opts.tokensPath === null ? void 0 : opts.tokensPath ?? paths.tokens);
|
|
@@ -40304,7 +40692,7 @@ async function startDaemon(opts = {}) {
|
|
|
40304
40692
|
});
|
|
40305
40693
|
const summarizer = startSummarizer(registry2, log, opts.summaries ?? process.env["GRENADE_SUMMARIES"] !== "off");
|
|
40306
40694
|
await registry2.adopt();
|
|
40307
|
-
const groupsPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ?
|
|
40695
|
+
const groupsPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ? join18(dirname16(opts.sessionsPath), "groups.json") : paths.groups;
|
|
40308
40696
|
const groupOrder = new GroupOrderStore(registry2, log, groupsPath);
|
|
40309
40697
|
const poller = startPoller({ tmux, registry: registry2, log });
|
|
40310
40698
|
const pinnedTerminal = opts.terminal ?? terminalFromEnv();
|
|
@@ -40376,7 +40764,7 @@ async function startDaemon(opts = {}) {
|
|
|
40376
40764
|
for (const { id, path: path2 } of registry2.transcripts()) {
|
|
40377
40765
|
readActivity(id, path2).catch((e) => log.debug("Could not read the activity from a saved transcript", { session: id, path: path2, error: e }));
|
|
40378
40766
|
}
|
|
40379
|
-
const marksPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ?
|
|
40767
|
+
const marksPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ? join18(dirname16(opts.sessionsPath), "conversations.json") : paths.conversations;
|
|
40380
40768
|
const marks = new ConversationMarks(log, marksPath);
|
|
40381
40769
|
const held = () => heldConversations(registry2.transcripts(), (id) => (registry2.get(id)?.status ?? "gone") !== "gone");
|
|
40382
40770
|
const conversationIndex = new AllConversations([
|
|
@@ -40390,6 +40778,7 @@ async function startDaemon(opts = {}) {
|
|
|
40390
40778
|
log,
|
|
40391
40779
|
...opts.voiceFetch ? { fetch: opts.voiceFetch } : {}
|
|
40392
40780
|
});
|
|
40781
|
+
const canvas = new CanvasService(() => registry2.list(), new CanvasWatcher(void 0, void 0, (folder, error62) => log.debug("Could not look at a canvas", { folder, error: error62 instanceof Error ? error62.message : String(error62) })));
|
|
40393
40782
|
const models = {
|
|
40394
40783
|
async switch(session, model, effort) {
|
|
40395
40784
|
if (switchingModel.has(session.id)) throw new ModelSwitchError("A model switch is already under way in this session.");
|
|
@@ -40531,7 +40920,7 @@ async function startDaemon(opts = {}) {
|
|
|
40531
40920
|
prompts.on("answered", (id) => registry2.applyHook(id, "working", void 0, true));
|
|
40532
40921
|
const screenPrompts = new ScreenPrompts(registry2, prompts, log, (id) => switchingModel.has(id));
|
|
40533
40922
|
watchScreenBackground(registry2);
|
|
40534
|
-
const backgroundWatch = new ClaudeBackgroundWatch(registry2,
|
|
40923
|
+
const backgroundWatch = new ClaudeBackgroundWatch(registry2, join18(opts.claudeDir ?? paths.claudeDir, "sessions"), log);
|
|
40535
40924
|
backgroundWatch.start();
|
|
40536
40925
|
registry2.on("removed", (id) => {
|
|
40537
40926
|
prompts.closeSession(id);
|
|
@@ -40590,12 +40979,13 @@ async function startDaemon(opts = {}) {
|
|
|
40590
40979
|
groups: groupOrder,
|
|
40591
40980
|
models,
|
|
40592
40981
|
voice,
|
|
40982
|
+
canvas,
|
|
40593
40983
|
conversations,
|
|
40594
|
-
openTerm(
|
|
40984
|
+
openTerm(open6) {
|
|
40595
40985
|
const stream = new TermStream({
|
|
40596
|
-
...
|
|
40597
|
-
resize: (cols, rows, by) => registry2.resize(
|
|
40598
|
-
release: (by) => registry2.releaseSize(
|
|
40986
|
+
...open6,
|
|
40987
|
+
resize: (cols, rows, by) => registry2.resize(open6.sessionId, cols, rows, by),
|
|
40988
|
+
release: (by) => registry2.releaseSize(open6.sessionId, by),
|
|
40599
40989
|
log
|
|
40600
40990
|
});
|
|
40601
40991
|
stream.start();
|
|
@@ -40727,6 +41117,7 @@ async function startDaemon(opts = {}) {
|
|
|
40727
41117
|
mirror.stop();
|
|
40728
41118
|
poller.stop();
|
|
40729
41119
|
relayLink?.stop();
|
|
41120
|
+
canvas.stop();
|
|
40730
41121
|
push2.stop();
|
|
40731
41122
|
prompts.closeAll();
|
|
40732
41123
|
summarizer?.stop();
|
|
@@ -40923,7 +41314,7 @@ program2.command("install-hooks").description("also let a claude typed by hand i
|
|
|
40923
41314
|
if (o.print) return console.log(JSON.stringify(settings, null, 2));
|
|
40924
41315
|
if (o.remove) removeOldCodexHooks();
|
|
40925
41316
|
if (!changed) return console.log(`nothing to do: ${paths.claudeSettings} already up to date`);
|
|
40926
|
-
mkdirSync16(
|
|
41317
|
+
mkdirSync16(dirname17(paths.claudeSettings), { recursive: true });
|
|
40927
41318
|
writeFileSync18(paths.claudeSettings, JSON.stringify(settings, null, 2) + "\n");
|
|
40928
41319
|
console.log(`${o.remove ? "removed Grenade hooks from" : "installed Grenade hooks in"} ${paths.claudeSettings}`);
|
|
40929
41320
|
});
|