@holdgrenade/cli 1.0.48 → 1.0.50
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/README.md +1 -1
- package/dist/cli.js +674 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ grenade setup
|
|
|
29
29
|
3. It offers the relay, for reaching the computer from any network (`grenade relay off`). Push notifications follow that answer: on with the relay, off without it, and setup says which.
|
|
30
30
|
4. It shows a QR code. Scan it in the Grenade app and the phone is paired, on any network when the relay is on.
|
|
31
31
|
|
|
32
|
-
Setup touches no agent's settings. Grenade starts Claude Code and Codex with its hooks (`claude --settings …`, `codex -c hooks.…`), so the phone knows when they work and wait. The first Codex session asks once to trust them; the phone shows it as a card, so tap Trust there or pick "Trust all and continue" in the terminal.
|
|
32
|
+
Setup touches no agent's settings. Grenade starts Claude Code and Codex with its hooks (`claude --settings …`, `codex -c hooks.…`), so the phone knows when they work and wait. Claude Code also starts with a status line of Grenade's, which tells grenaded how full the conversation is and how much of your Pro or Max plan is used, and then runs your own status line, if you have one, so your line looks as before (a change to it applies to sessions started after). The first Codex session asks once to trust them; the phone shows it as a card, so tap Trust there or pick "Trust all and continue" in the terminal.
|
|
33
33
|
|
|
34
34
|
`--yes` takes the suggested answer to every question; `--no-hooks`, `--no-service`, `--no-relay` and `--no-pair` leave a step out.
|
|
35
35
|
|
package/dist/cli.js
CHANGED
|
@@ -2127,7 +2127,7 @@ var require_extension = __commonJS({
|
|
|
2127
2127
|
if (dest[name] === void 0) dest[name] = [elem];
|
|
2128
2128
|
else dest[name].push(elem);
|
|
2129
2129
|
}
|
|
2130
|
-
function
|
|
2130
|
+
function parse8(header) {
|
|
2131
2131
|
const offers = /* @__PURE__ */ Object.create(null);
|
|
2132
2132
|
let params = /* @__PURE__ */ Object.create(null);
|
|
2133
2133
|
let mustUnescape = false;
|
|
@@ -2267,7 +2267,7 @@ var require_extension = __commonJS({
|
|
|
2267
2267
|
}).join(", ");
|
|
2268
2268
|
}).join(", ");
|
|
2269
2269
|
}
|
|
2270
|
-
module.exports = { format, parse:
|
|
2270
|
+
module.exports = { format, parse: parse8 };
|
|
2271
2271
|
}
|
|
2272
2272
|
});
|
|
2273
2273
|
|
|
@@ -2301,7 +2301,7 @@ var require_websocket = __commonJS({
|
|
|
2301
2301
|
var {
|
|
2302
2302
|
EventTarget: { addEventListener, removeEventListener }
|
|
2303
2303
|
} = require_event_target();
|
|
2304
|
-
var { format, parse:
|
|
2304
|
+
var { format, parse: parse8 } = require_extension();
|
|
2305
2305
|
var { toBuffer } = require_buffer_util();
|
|
2306
2306
|
var kAborted = /* @__PURE__ */ Symbol("kAborted");
|
|
2307
2307
|
var protocolVersions = [8, 13];
|
|
@@ -2991,7 +2991,7 @@ var require_websocket = __commonJS({
|
|
|
2991
2991
|
}
|
|
2992
2992
|
let extensions;
|
|
2993
2993
|
try {
|
|
2994
|
-
extensions =
|
|
2994
|
+
extensions = parse8(secWebSocketExtensions);
|
|
2995
2995
|
} catch (err) {
|
|
2996
2996
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
2997
2997
|
abortHandshake(websocket, socket, message);
|
|
@@ -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 open7() {
|
|
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 open7() {
|
|
3267
3267
|
duplex._write(chunk, encoding, callback);
|
|
3268
3268
|
});
|
|
3269
3269
|
return;
|
|
@@ -3283,7 +3283,7 @@ var require_subprotocol = __commonJS({
|
|
|
3283
3283
|
"node_modules/ws/lib/subprotocol.js"(exports, module) {
|
|
3284
3284
|
"use strict";
|
|
3285
3285
|
var { tokenChars } = require_validation();
|
|
3286
|
-
function
|
|
3286
|
+
function parse8(header) {
|
|
3287
3287
|
const protocols = /* @__PURE__ */ new Set();
|
|
3288
3288
|
let start = -1;
|
|
3289
3289
|
let end = -1;
|
|
@@ -3319,7 +3319,7 @@ var require_subprotocol = __commonJS({
|
|
|
3319
3319
|
protocols.add(protocol);
|
|
3320
3320
|
return protocols;
|
|
3321
3321
|
}
|
|
3322
|
-
module.exports = { parse:
|
|
3322
|
+
module.exports = { parse: parse8 };
|
|
3323
3323
|
}
|
|
3324
3324
|
});
|
|
3325
3325
|
|
|
@@ -7024,8 +7024,8 @@ function isKnownAgent(kind) {
|
|
|
7024
7024
|
}
|
|
7025
7025
|
|
|
7026
7026
|
// src/cli.ts
|
|
7027
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as
|
|
7028
|
-
import { dirname as
|
|
7027
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "node:fs";
|
|
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 {
|
|
@@ -12240,18 +12240,18 @@ var validateAsync = async (schema, value, _ctx) => {
|
|
|
12240
12240
|
return result.issues.length === 0;
|
|
12241
12241
|
};
|
|
12242
12242
|
var _encode = (_Err) => {
|
|
12243
|
-
const
|
|
12243
|
+
const parse8 = _parse(_Err);
|
|
12244
12244
|
const fn = (schema, value, _ctx, _params) => {
|
|
12245
12245
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
12246
|
-
return
|
|
12246
|
+
return parse8(schema, value, ctx, finalizeParams(fn, _params));
|
|
12247
12247
|
};
|
|
12248
12248
|
return fn;
|
|
12249
12249
|
};
|
|
12250
12250
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
12251
12251
|
var _decode = (_Err) => {
|
|
12252
|
-
const
|
|
12252
|
+
const parse8 = _parse(_Err);
|
|
12253
12253
|
const fn = (schema, value, _ctx, _params) => {
|
|
12254
|
-
return
|
|
12254
|
+
return parse8(schema, value, _ctx, finalizeParams(fn, _params));
|
|
12255
12255
|
};
|
|
12256
12256
|
return fn;
|
|
12257
12257
|
};
|
|
@@ -13918,7 +13918,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
13918
13918
|
}
|
|
13919
13919
|
return propValues;
|
|
13920
13920
|
});
|
|
13921
|
-
const
|
|
13921
|
+
const isObject5 = isObject;
|
|
13922
13922
|
const catchall = def.catchall;
|
|
13923
13923
|
let value;
|
|
13924
13924
|
const memo2 = globalConfig.memoizer;
|
|
@@ -13926,7 +13926,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
13926
13926
|
inst._zod.parse = (payload, ctx) => {
|
|
13927
13927
|
value ?? (value = _normalized.value);
|
|
13928
13928
|
const input2 = payload.value;
|
|
13929
|
-
if (!
|
|
13929
|
+
if (!isObject5(input2)) {
|
|
13930
13930
|
payload.issues.push({
|
|
13931
13931
|
expected: "object",
|
|
13932
13932
|
code: "invalid_type",
|
|
@@ -14062,7 +14062,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
14062
14062
|
return doc.compile();
|
|
14063
14063
|
};
|
|
14064
14064
|
let fastpass;
|
|
14065
|
-
const
|
|
14065
|
+
const isObject5 = isObject;
|
|
14066
14066
|
const jit = !globalConfig.jitless;
|
|
14067
14067
|
const allowsEval2 = allowsEval;
|
|
14068
14068
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -14071,7 +14071,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
14071
14071
|
inst._zod.parse = (payload, ctx) => {
|
|
14072
14072
|
value ?? (value = _normalized.value);
|
|
14073
14073
|
const input2 = payload.value;
|
|
14074
|
-
if (!
|
|
14074
|
+
if (!isObject5(input2)) {
|
|
14075
14075
|
payload.issues.push({
|
|
14076
14076
|
expected: "object",
|
|
14077
14077
|
code: "invalid_type",
|
|
@@ -30900,6 +30900,97 @@ var VoiceTokenFrame = external_exports.object({
|
|
|
30900
30900
|
once: external_exports.literal(true).optional()
|
|
30901
30901
|
});
|
|
30902
30902
|
|
|
30903
|
+
// ../grenade-protocol/dist/usage.js
|
|
30904
|
+
var LIMITS_MAX = 16;
|
|
30905
|
+
var SessionContext = external_exports.object({
|
|
30906
|
+
used: external_exports.number().int().min(0),
|
|
30907
|
+
size: external_exports.number().int().min(1)
|
|
30908
|
+
});
|
|
30909
|
+
var PlanLimit = external_exports.object({
|
|
30910
|
+
agent: AgentKind,
|
|
30911
|
+
minutes: external_exports.number().int().min(1),
|
|
30912
|
+
usedPercent: external_exports.number().min(0).max(100),
|
|
30913
|
+
resetsAt: external_exports.string().datetime().optional(),
|
|
30914
|
+
at: external_exports.string().datetime()
|
|
30915
|
+
});
|
|
30916
|
+
var LimitsRequestFrame = external_exports.object({ type: external_exports.literal("limits") });
|
|
30917
|
+
var LimitsFrame = external_exports.object({
|
|
30918
|
+
type: external_exports.literal("limits"),
|
|
30919
|
+
limits: external_exports.array(PlanLimit).max(LIMITS_MAX)
|
|
30920
|
+
});
|
|
30921
|
+
|
|
30922
|
+
// ../grenade-protocol/dist/canvas.js
|
|
30923
|
+
var CANVAS_FOLDER = ".grenade/canvas";
|
|
30924
|
+
var CANVAS_BOARDS_MAX = 500;
|
|
30925
|
+
var CANVAS_BOARD_MAX_BYTES = 2 * 1024 * 1024;
|
|
30926
|
+
var CANVAS_BOARD_FRAME_MAX_BYTES = 3e6;
|
|
30927
|
+
var CANVAS_SUBSCRIPTIONS_MAX = 8;
|
|
30928
|
+
var CANVAS_DEFAULT_WIDTH = 1280;
|
|
30929
|
+
var CANVAS_DEFAULT_HEIGHT = 800;
|
|
30930
|
+
var CANVAS_SIZE_MAX = 8e3;
|
|
30931
|
+
var CanvasCwd = external_exports.string().min(1).max(1024).refine((p) => p === "~" || p.startsWith("~/") || p.startsWith("/"), "absolute or starting with ~");
|
|
30932
|
+
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");
|
|
30933
|
+
var RequestId = external_exports.string().min(1).max(64);
|
|
30934
|
+
var CanvasBoard = external_exports.object({
|
|
30935
|
+
/** Its file name, "R3A · Cards.html". */
|
|
30936
|
+
file: CanvasFileName,
|
|
30937
|
+
/** What it is, from its name: "Cards". The file's name without its extension when the name has nothing after the revision. */
|
|
30938
|
+
name: external_exports.string().min(1).max(255),
|
|
30939
|
+
/** The revision its name starts with: 3 for "R3A · Cards.html". Absent for a name without one. */
|
|
30940
|
+
revision: external_exports.number().int().min(0).max(9999).optional(),
|
|
30941
|
+
/** The letters after the revision, as written: "A" for "R3A · Cards.html". Absent when there are none. */
|
|
30942
|
+
letter: external_exports.string().regex(/^[A-Za-z]{1,3}$/).optional(),
|
|
30943
|
+
/** The size it asks for (`<meta name="board" content="1280x800">`), else 1280 × 800. */
|
|
30944
|
+
width: external_exports.number().int().min(1).max(CANVAS_SIZE_MAX),
|
|
30945
|
+
height: external_exports.number().int().min(1).max(CANVAS_SIZE_MAX),
|
|
30946
|
+
/** When it was last saved. */
|
|
30947
|
+
modified: external_exports.string().datetime(),
|
|
30948
|
+
/** Its size on disk. */
|
|
30949
|
+
bytes: external_exports.number().int().min(0)
|
|
30950
|
+
});
|
|
30951
|
+
var CanvasRequestFrame = external_exports.object({ type: external_exports.literal("canvas"), id: RequestId, cwd: CanvasCwd });
|
|
30952
|
+
var CanvasSubscribeFrame = external_exports.object({ type: external_exports.literal("canvas.subscribe"), id: RequestId, cwd: CanvasCwd });
|
|
30953
|
+
var CanvasUnsubscribeFrame = external_exports.object({ type: external_exports.literal("canvas.unsubscribe"), cwd: CanvasCwd });
|
|
30954
|
+
var CanvasBoardRequestFrame = external_exports.object({ type: external_exports.literal("canvas.board"), id: RequestId, cwd: CanvasCwd, file: CanvasFileName });
|
|
30955
|
+
var CanvasFrame = external_exports.object({
|
|
30956
|
+
type: external_exports.literal("canvas"),
|
|
30957
|
+
/** The request's; absent on a frame sent because the canvas changed. */
|
|
30958
|
+
id: RequestId.optional(),
|
|
30959
|
+
/** As the client sent it. */
|
|
30960
|
+
cwd: external_exports.string().min(1),
|
|
30961
|
+
/** The canvas folder, absolute: `<cwd>/.grenade/canvas`. */
|
|
30962
|
+
folder: external_exports.string().min(1),
|
|
30963
|
+
boards: external_exports.array(CanvasBoard).max(CANVAS_BOARDS_MAX),
|
|
30964
|
+
/** The folder is not there (yet): no session has drawn on this canvas. `boards` is empty. */
|
|
30965
|
+
missing: external_exports.literal(true).optional(),
|
|
30966
|
+
truncated: external_exports.literal(true).optional()
|
|
30967
|
+
});
|
|
30968
|
+
var CanvasBoardFrame = external_exports.object({
|
|
30969
|
+
type: external_exports.literal("canvas.board"),
|
|
30970
|
+
id: RequestId,
|
|
30971
|
+
cwd: external_exports.string().min(1),
|
|
30972
|
+
file: CanvasFileName,
|
|
30973
|
+
/** The whole file, at most `CANVAS_BOARD_MAX_BYTES` bytes on disk. */
|
|
30974
|
+
html: external_exports.string().max(CANVAS_BOARD_MAX_BYTES),
|
|
30975
|
+
modified: external_exports.string().datetime(),
|
|
30976
|
+
bytes: external_exports.number().int().min(0).max(CANVAS_BOARD_MAX_BYTES)
|
|
30977
|
+
});
|
|
30978
|
+
function boardNameOf(file2) {
|
|
30979
|
+
const stem = file2.replace(/\.html?$/i, "") || file2;
|
|
30980
|
+
const match = /^[Rr](\d{1,4})([A-Za-z]{0,3})(?![A-Za-z0-9])/.exec(stem);
|
|
30981
|
+
if (!match)
|
|
30982
|
+
return { name: stem };
|
|
30983
|
+
const rest = stem.slice(match[0].length).replace(/^[\s·•\-–—_:|.]+/u, "").trim();
|
|
30984
|
+
return { name: rest || stem, revision: Number(match[1]), ...match[2] ? { letter: match[2] } : {} };
|
|
30985
|
+
}
|
|
30986
|
+
function boardSizeOf(html) {
|
|
30987
|
+
const meta3 = /<meta[^>]*name=["']board["'][^>]*>/i.exec(html);
|
|
30988
|
+
const content = meta3 && /content=["']\s*(\d{2,5})\s*[x×]\s*(\d{2,5})\s*["']/.exec(meta3[0]);
|
|
30989
|
+
if (!content)
|
|
30990
|
+
return { width: CANVAS_DEFAULT_WIDTH, height: CANVAS_DEFAULT_HEIGHT };
|
|
30991
|
+
return { width: Math.min(Number(content[1]), CANVAS_SIZE_MAX), height: Math.min(Number(content[2]), CANVAS_SIZE_MAX) };
|
|
30992
|
+
}
|
|
30993
|
+
|
|
30903
30994
|
// ../grenade-protocol/dist/relay.js
|
|
30904
30995
|
var RELAY_PROTOCOL_VERSION = 1;
|
|
30905
30996
|
var RELAY_DAEMON_PATH = "/v1/daemon";
|
|
@@ -31089,6 +31180,8 @@ var Session = external_exports.object({
|
|
|
31089
31180
|
model: external_exports.string().min(1).max(MODEL_NAME_MAX).optional(),
|
|
31090
31181
|
/** The effort level the agent last answered with, or was last switched to (PROTOCOL.md "Models"). Absent until the daemon has seen one, and for a model that takes none. */
|
|
31091
31182
|
effort: EffortLevel.optional(),
|
|
31183
|
+
/** How full the session's context is after the agent's last reply, in tokens (PROTOCOL.md "Usage"). Absent until the daemon has read it, and for shell sessions. */
|
|
31184
|
+
context: SessionContext.optional(),
|
|
31092
31185
|
createdAt: external_exports.string().datetime(),
|
|
31093
31186
|
/** Sessions with the same group are shown together. Absent (older daemons) means the session is on its own. */
|
|
31094
31187
|
group: external_exports.string().min(1).optional(),
|
|
@@ -31142,6 +31235,10 @@ var DaemonInfo = external_exports.object({
|
|
|
31142
31235
|
groupNames: external_exports.literal(1).optional(),
|
|
31143
31236
|
/** 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
31237
|
voice: external_exports.literal(1).optional(),
|
|
31238
|
+
/** 1 when the daemon serves a group's design canvas: `canvas`, `canvas.subscribe`, `canvas.board` (PROTOCOL.md "Canvas"). Absent from older daemons. */
|
|
31239
|
+
canvas: external_exports.literal(1).optional(),
|
|
31240
|
+
/** 1 when the daemon answers `limits` and sends `context` on sessions (PROTOCOL.md "Usage"). Absent from older daemons. */
|
|
31241
|
+
limits: external_exports.literal(1).optional(),
|
|
31145
31242
|
/** Retired: 1 when the daemon sends `activity` for Codex sessions too. Still sent for apps that read it; new clients read `agents`. */
|
|
31146
31243
|
codexActivity: external_exports.literal(1).optional(),
|
|
31147
31244
|
/** Every agent the daemon can start and what it does with each (PROTOCOL.md "Agents"). Absent from older daemons. */
|
|
@@ -31282,6 +31379,11 @@ var ClientFrame = external_exports.discriminatedUnion("type", [
|
|
|
31282
31379
|
VoiceRequestFrame,
|
|
31283
31380
|
VoiceKeyFrame,
|
|
31284
31381
|
VoiceTokenRequestFrame,
|
|
31382
|
+
CanvasRequestFrame,
|
|
31383
|
+
CanvasSubscribeFrame,
|
|
31384
|
+
CanvasUnsubscribeFrame,
|
|
31385
|
+
CanvasBoardRequestFrame,
|
|
31386
|
+
LimitsRequestFrame,
|
|
31285
31387
|
PingFrame
|
|
31286
31388
|
]);
|
|
31287
31389
|
var ErrorCode = external_exports.enum([
|
|
@@ -31307,7 +31409,7 @@ var ErrorFrame = external_exports.object({
|
|
|
31307
31409
|
code: ErrorCode,
|
|
31308
31410
|
message: external_exports.string(),
|
|
31309
31411
|
ref: external_exports.string().optional(),
|
|
31310
|
-
/** The `id` of the frame this error answers, when it had one (an `input`). */
|
|
31412
|
+
/** The `id` of the frame this error answers, when it had one (an `input`, a `voice.token`, a `canvas` request). */
|
|
31311
31413
|
id: external_exports.string().min(1).max(64).optional()
|
|
31312
31414
|
});
|
|
31313
31415
|
var SessionsFrame = external_exports.object({ type: external_exports.literal("sessions"), sessions: external_exports.array(Session) });
|
|
@@ -31378,6 +31480,9 @@ var DaemonFrame = external_exports.discriminatedUnion("type", [
|
|
|
31378
31480
|
FoldersFrame,
|
|
31379
31481
|
VoiceFrame,
|
|
31380
31482
|
VoiceTokenFrame,
|
|
31483
|
+
CanvasFrame,
|
|
31484
|
+
CanvasBoardFrame,
|
|
31485
|
+
LimitsFrame,
|
|
31381
31486
|
PongFrame
|
|
31382
31487
|
]);
|
|
31383
31488
|
var PairRequest = external_exports.object({ code: external_exports.string().regex(/^\d{6}$/), client: ClientInfo });
|
|
@@ -33323,6 +33428,53 @@ function shellQuote(s) {
|
|
|
33323
33428
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
33324
33429
|
}
|
|
33325
33430
|
|
|
33431
|
+
// src/usage/claudeStatusLine.ts
|
|
33432
|
+
var STATUS_LINE_PATH = "/hooks/claude/status";
|
|
33433
|
+
function claudeStatusLine(port, user) {
|
|
33434
|
+
const post = `curl -s -m 2 -X POST "http://127.0.0.1:${port}${STATUS_LINE_PATH}?session=$GRENADE_SESSION" -H 'content-type: application/json' --data-binary @-`;
|
|
33435
|
+
const report = `(printf '%s' "$in" | ${post} >/dev/null 2>&1 &)`;
|
|
33436
|
+
const command = user ? `in=$(cat); ${report}; printf '%s' "$in" | sh -c ${shellQuote(user.command)}` : `in=$(cat); ${report}`;
|
|
33437
|
+
return { type: "command", command, ...user?.padding !== void 0 ? { padding: user.padding } : {} };
|
|
33438
|
+
}
|
|
33439
|
+
function userStatusLineIn(settings) {
|
|
33440
|
+
const line = settings?.statusLine;
|
|
33441
|
+
if (!line || line.type !== "command" || typeof line.command !== "string" || !line.command.trim()) return void 0;
|
|
33442
|
+
if (line.command.includes(STATUS_LINE_PATH)) return void 0;
|
|
33443
|
+
return { command: line.command, ...typeof line.padding === "number" ? { padding: line.padding } : {} };
|
|
33444
|
+
}
|
|
33445
|
+
var WINDOWS = { five_hour: 300, seven_day: 10080 };
|
|
33446
|
+
function claudeStatusUsage(payload, at) {
|
|
33447
|
+
const p = payload ?? {};
|
|
33448
|
+
const limits = [];
|
|
33449
|
+
if (isObject3(p.rate_limits)) {
|
|
33450
|
+
for (const [name, minutes] of Object.entries(WINDOWS)) {
|
|
33451
|
+
const w = p.rate_limits[name];
|
|
33452
|
+
if (!isObject3(w) || typeof w.used_percentage !== "number") continue;
|
|
33453
|
+
const resetsAt = typeof w.resets_at === "number" ? new Date(w.resets_at * 1e3).toISOString() : void 0;
|
|
33454
|
+
limits.push({ agent: "claude", minutes, usedPercent: clampPercent(w.used_percentage), ...resetsAt ? { resetsAt } : {}, at });
|
|
33455
|
+
}
|
|
33456
|
+
}
|
|
33457
|
+
const context = contextIn(p.context_window);
|
|
33458
|
+
return { ...context ? { context } : {}, limits };
|
|
33459
|
+
}
|
|
33460
|
+
function contextIn(window) {
|
|
33461
|
+
if (!isObject3(window)) return void 0;
|
|
33462
|
+
const size = window.context_window_size;
|
|
33463
|
+
if (typeof size !== "number" || !(size >= 1)) return void 0;
|
|
33464
|
+
const usage = window.current_usage;
|
|
33465
|
+
if (isObject3(usage)) {
|
|
33466
|
+
const used = tokens(usage.input_tokens) + tokens(usage.cache_creation_input_tokens) + tokens(usage.cache_read_input_tokens);
|
|
33467
|
+
if (used > 0) return { used, size: Math.round(size) };
|
|
33468
|
+
}
|
|
33469
|
+
if (typeof window.used_percentage === "number") return { used: Math.round(clampPercent(window.used_percentage) / 100 * size), size: Math.round(size) };
|
|
33470
|
+
return void 0;
|
|
33471
|
+
}
|
|
33472
|
+
var tokens = (v) => typeof v === "number" && v > 0 ? Math.round(v) : 0;
|
|
33473
|
+
var clampPercent = (v) => Math.max(0, Math.min(100, v));
|
|
33474
|
+
function isObject3(v) {
|
|
33475
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33476
|
+
}
|
|
33477
|
+
|
|
33326
33478
|
// src/hooks/installHooks.ts
|
|
33327
33479
|
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Notification", "Stop", "StopFailure", "SessionEnd"];
|
|
33328
33480
|
var PROMPT_EVENT = "PermissionRequest";
|
|
@@ -33344,15 +33496,16 @@ function mergeHooks(input2, port) {
|
|
|
33344
33496
|
const command = { type: "command", command: hookCommand(port) };
|
|
33345
33497
|
return mergeEntries(input2, [...HOOK_EVENTS.map((e) => [e, command]), [PROMPT_EVENT, promptHook(port)]], isGrenadeHook);
|
|
33346
33498
|
}
|
|
33347
|
-
function claudeHookFlags(port) {
|
|
33348
|
-
|
|
33499
|
+
function claudeHookFlags(port, userStatusLine) {
|
|
33500
|
+
const settings = { ...mergeHooks({}, port).settings, statusLine: claudeStatusLine(port, userStatusLine) };
|
|
33501
|
+
return `--settings ${shellQuote(JSON.stringify(settings))}`;
|
|
33349
33502
|
}
|
|
33350
33503
|
function removeHooks(input2) {
|
|
33351
33504
|
return removeEntries(input2, isGrenadeHook);
|
|
33352
33505
|
}
|
|
33353
33506
|
function mergeEntries(input2, wanted, isOurs) {
|
|
33354
|
-
const settings =
|
|
33355
|
-
const hooks =
|
|
33507
|
+
const settings = isObject4(input2) ? structuredClone(input2) : {};
|
|
33508
|
+
const hooks = isObject4(settings.hooks) ? settings.hooks : {};
|
|
33356
33509
|
settings.hooks = hooks;
|
|
33357
33510
|
let changed = false;
|
|
33358
33511
|
for (const [event, entry] of wanted) {
|
|
@@ -33373,8 +33526,8 @@ function mergeEntries(input2, wanted, isOurs) {
|
|
|
33373
33526
|
return { settings, changed };
|
|
33374
33527
|
}
|
|
33375
33528
|
function removeEntries(input2, isOurs) {
|
|
33376
|
-
const settings =
|
|
33377
|
-
const hooks =
|
|
33529
|
+
const settings = isObject4(input2) ? structuredClone(input2) : {};
|
|
33530
|
+
const hooks = isObject4(settings.hooks) ? settings.hooks : {};
|
|
33378
33531
|
let changed = false;
|
|
33379
33532
|
for (const [event, matchers] of Object.entries(hooks)) {
|
|
33380
33533
|
if (!Array.isArray(matchers)) continue;
|
|
@@ -33384,7 +33537,7 @@ function removeEntries(input2, isOurs) {
|
|
|
33384
33537
|
}
|
|
33385
33538
|
return { settings, changed };
|
|
33386
33539
|
}
|
|
33387
|
-
function
|
|
33540
|
+
function isObject4(v) {
|
|
33388
33541
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33389
33542
|
}
|
|
33390
33543
|
|
|
@@ -34478,12 +34631,12 @@ function terminalFromEnv() {
|
|
|
34478
34631
|
const env = process.env["GRENADE_TERMINAL"];
|
|
34479
34632
|
return env !== void 0 && isTerminalKind(env) ? env : void 0;
|
|
34480
34633
|
}
|
|
34481
|
-
function relayoutSteps(ordered,
|
|
34634
|
+
function relayoutSteps(ordered, open7) {
|
|
34482
34635
|
const [head, ...rest] = ordered;
|
|
34483
34636
|
if (head === void 0) return [];
|
|
34484
|
-
const keepHead =
|
|
34637
|
+
const keepHead = open7.has(head);
|
|
34485
34638
|
const steps = [];
|
|
34486
|
-
for (const id of keepHead ? rest : ordered) if (
|
|
34639
|
+
for (const id of keepHead ? rest : ordered) if (open7.has(id)) steps.push({ kind: "close", id });
|
|
34487
34640
|
if (!keepHead) steps.push({ kind: "tab", id: head });
|
|
34488
34641
|
rest.forEach((id, i) => steps.push({ kind: "split", id, nextTo: ordered[i] }));
|
|
34489
34642
|
return steps;
|
|
@@ -34637,9 +34790,9 @@ var TerminalMirror = class {
|
|
|
34637
34790
|
/** The nearest live member before `s` in group order that has a pane in `a`, else the last one that has. */
|
|
34638
34791
|
openMemberOf(s, a) {
|
|
34639
34792
|
if (s.group === void 0) return void 0;
|
|
34640
|
-
const
|
|
34641
|
-
const before =
|
|
34642
|
-
return (before.at(-1) ??
|
|
34793
|
+
const open7 = otherMembers(s.group, this.registry.list(), s.id).filter((m) => this.openIn.get(m.id) === a);
|
|
34794
|
+
const before = open7.filter((m) => byGroupOrder(m, s) < 0);
|
|
34795
|
+
return (before.at(-1) ?? open7.at(-1))?.id;
|
|
34643
34796
|
}
|
|
34644
34797
|
closeTab(id) {
|
|
34645
34798
|
const a = this.openIn.get(id);
|
|
@@ -34738,7 +34891,7 @@ function underService(env = process.env, pid = process.pid) {
|
|
|
34738
34891
|
}
|
|
34739
34892
|
|
|
34740
34893
|
// src/daemon/server.ts
|
|
34741
|
-
import { dirname as
|
|
34894
|
+
import { dirname as dirname16, join as join18 } from "node:path";
|
|
34742
34895
|
import { createServer as createServer2 } from "node:http";
|
|
34743
34896
|
import { networkInterfaces } from "node:os";
|
|
34744
34897
|
|
|
@@ -35478,6 +35631,18 @@ var SessionRegistry = class extends EventEmitter2 {
|
|
|
35478
35631
|
this.persist();
|
|
35479
35632
|
this.emit("updated", r.session);
|
|
35480
35633
|
}
|
|
35634
|
+
/**
|
|
35635
|
+
* How full the session's context is (PROTOCOL.md "Usage"): Claude Code's status line, Codex's rollout. Not saved:
|
|
35636
|
+
* the next status line or hook after a restart brings it back.
|
|
35637
|
+
*/
|
|
35638
|
+
setContext(id, context) {
|
|
35639
|
+
const r = this.records.get(id);
|
|
35640
|
+
if (!r) return;
|
|
35641
|
+
const now = r.session.context;
|
|
35642
|
+
if (now && now.used === context.used && now.size === context.size) return;
|
|
35643
|
+
r.session = { ...r.session, context: { used: context.used, size: context.size } };
|
|
35644
|
+
this.emit("updated", r.session);
|
|
35645
|
+
}
|
|
35481
35646
|
/**
|
|
35482
35647
|
* A client switched the session to `model` (PROTOCOL.md "Models") and the agent took it: the session says so at
|
|
35483
35648
|
* once, before the agent's next reply. `effort` undefined is a model that takes none.
|
|
@@ -36729,13 +36894,13 @@ function switchModelDialog(below) {
|
|
|
36729
36894
|
}
|
|
36730
36895
|
function paragraphs(rows) {
|
|
36731
36896
|
const out = [];
|
|
36732
|
-
let
|
|
36897
|
+
let open7 = false;
|
|
36733
36898
|
for (const row of rows.map((r) => r.trim())) {
|
|
36734
|
-
if (!row)
|
|
36735
|
-
else if (
|
|
36899
|
+
if (!row) open7 = false;
|
|
36900
|
+
else if (open7) out[out.length - 1] += ` ${row}`;
|
|
36736
36901
|
else {
|
|
36737
36902
|
out.push(row);
|
|
36738
|
-
|
|
36903
|
+
open7 = true;
|
|
36739
36904
|
}
|
|
36740
36905
|
}
|
|
36741
36906
|
return out.map((p) => /[.!?:]$/.test(p) ? p : `${p}.`).join(" ");
|
|
@@ -36772,25 +36937,25 @@ async function switchClaudeModel(term, model, effort, timing = DEFAULT_TIMING) {
|
|
|
36772
36937
|
throw new ModelSwitchError("Claude Code's prompt is not empty. Send or clear what is typed there, then choose again.");
|
|
36773
36938
|
}
|
|
36774
36939
|
await term.key("enter");
|
|
36775
|
-
let
|
|
36776
|
-
if (!
|
|
36777
|
-
if (!
|
|
36778
|
-
for (let moves = 0; selectedModel(
|
|
36940
|
+
let open7 = await look2(modelPickerIn);
|
|
36941
|
+
if (!open7) throw new ModelSwitchError("Claude Code did not open its model picker.");
|
|
36942
|
+
if (!open7.sessionOnly) await giveUp("This Claude Code cannot switch one session only. Update Claude Code, then choose again.");
|
|
36943
|
+
for (let moves = 0; selectedModel(open7) !== model; moves++) {
|
|
36779
36944
|
if (moves >= MOST_MOVES) await giveUp(`Claude Code's picker has no ${model}.`);
|
|
36780
|
-
const down = rowsDownTo(
|
|
36945
|
+
const down = rowsDownTo(open7, model);
|
|
36781
36946
|
await term.key(down !== null && down > 0 ? "down" : "up");
|
|
36782
36947
|
await timing.wait(LOOK_EVERY_MS);
|
|
36783
|
-
|
|
36948
|
+
open7 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36784
36949
|
}
|
|
36785
36950
|
if (effort !== void 0) {
|
|
36786
|
-
for (let steps = 0;
|
|
36951
|
+
for (let steps = 0; open7.effort !== effort; steps++) {
|
|
36787
36952
|
if (steps >= MOST_EFFORT_STEPS) await giveUp(`${model} has no ${effort} effort in Claude Code.`);
|
|
36788
36953
|
await term.type(RIGHT);
|
|
36789
36954
|
await timing.wait(LOOK_EVERY_MS);
|
|
36790
|
-
|
|
36955
|
+
open7 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36791
36956
|
}
|
|
36792
36957
|
}
|
|
36793
|
-
const chosenEffort =
|
|
36958
|
+
const chosenEffort = open7.effort ?? void 0;
|
|
36794
36959
|
await term.type("s");
|
|
36795
36960
|
const closed = await look2((lines) => modelPickerIn(lines) ? null : lines);
|
|
36796
36961
|
if (!closed) await giveUp("Claude Code did not take the choice.");
|
|
@@ -38603,6 +38768,271 @@ function switchTimingProblem(session) {
|
|
|
38603
38768
|
return null;
|
|
38604
38769
|
}
|
|
38605
38770
|
|
|
38771
|
+
// src/canvas/canvasService.ts
|
|
38772
|
+
import { homedir as homedir9 } from "node:os";
|
|
38773
|
+
|
|
38774
|
+
// src/canvas/canvasAccess.ts
|
|
38775
|
+
import { posix } from "node:path";
|
|
38776
|
+
function normalizeCwd(cwd, home) {
|
|
38777
|
+
const expanded = expandHome(cwd, home);
|
|
38778
|
+
if (!expanded.startsWith("/")) return null;
|
|
38779
|
+
return posix.resolve(expanded);
|
|
38780
|
+
}
|
|
38781
|
+
function sharedFolder(paths2) {
|
|
38782
|
+
if (paths2.length === 0) return null;
|
|
38783
|
+
let shared = paths2[0].split("/").filter(Boolean);
|
|
38784
|
+
for (const path2 of paths2.slice(1)) {
|
|
38785
|
+
const parts = path2.split("/").filter(Boolean);
|
|
38786
|
+
let i = 0;
|
|
38787
|
+
while (i < shared.length && i < parts.length && shared[i] === parts[i]) i++;
|
|
38788
|
+
shared = shared.slice(0, i);
|
|
38789
|
+
}
|
|
38790
|
+
return shared.length === 0 ? null : `/${shared.join("/")}`;
|
|
38791
|
+
}
|
|
38792
|
+
function canvasCwds(sessions, home) {
|
|
38793
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
38794
|
+
const groups = /* @__PURE__ */ new Map();
|
|
38795
|
+
for (const s of sessions) {
|
|
38796
|
+
const cwd = normalizeCwd(s.cwd, home);
|
|
38797
|
+
if (!cwd) continue;
|
|
38798
|
+
allowed.add(cwd);
|
|
38799
|
+
const key = s.group ?? s.id;
|
|
38800
|
+
groups.set(key, [...groups.get(key) ?? [], cwd]);
|
|
38801
|
+
}
|
|
38802
|
+
for (const cwds of groups.values()) {
|
|
38803
|
+
const shared = cwds.length > 1 ? sharedFolder(cwds) : null;
|
|
38804
|
+
if (shared) allowed.add(shared);
|
|
38805
|
+
}
|
|
38806
|
+
return allowed;
|
|
38807
|
+
}
|
|
38808
|
+
function allowedCanvasCwd(cwd, sessions, home) {
|
|
38809
|
+
const normalized = normalizeCwd(cwd, home);
|
|
38810
|
+
return normalized && canvasCwds(sessions, home).has(normalized) ? normalized : null;
|
|
38811
|
+
}
|
|
38812
|
+
function canvasFolderOf(cwd) {
|
|
38813
|
+
return posix.join(cwd, CANVAS_FOLDER);
|
|
38814
|
+
}
|
|
38815
|
+
|
|
38816
|
+
// src/canvas/canvasFolder.ts
|
|
38817
|
+
import { constants as constants3 } from "node:fs";
|
|
38818
|
+
import { lstat, open as open5, readdir as readdir5 } from "node:fs/promises";
|
|
38819
|
+
import { dirname as dirname12, join as join16 } from "node:path";
|
|
38820
|
+
|
|
38821
|
+
// src/canvas/boardListing.ts
|
|
38822
|
+
var finderOrder2 = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
|
|
38823
|
+
function isBoardFile(name) {
|
|
38824
|
+
return CanvasFileName.safeParse(name).success;
|
|
38825
|
+
}
|
|
38826
|
+
function boardsFrom(files, max = CANVAS_BOARDS_MAX) {
|
|
38827
|
+
const sorted = files.filter((f) => isBoardFile(f.file)).sort((a, b) => finderOrder2.compare(a.file, b.file));
|
|
38828
|
+
const boards = sorted.slice(0, max).map((f) => ({
|
|
38829
|
+
file: f.file,
|
|
38830
|
+
...boardNameOf(f.file),
|
|
38831
|
+
...boardSizeOf(f.head),
|
|
38832
|
+
modified: new Date(f.mtimeMs).toISOString(),
|
|
38833
|
+
bytes: f.bytes
|
|
38834
|
+
}));
|
|
38835
|
+
return { boards, truncated: sorted.length > max };
|
|
38836
|
+
}
|
|
38837
|
+
function listingKey(listing) {
|
|
38838
|
+
if (listing.missing) return "missing";
|
|
38839
|
+
return listing.boards.map((b) => `${b.file}
|
|
38840
|
+
${b.modified}
|
|
38841
|
+
${b.bytes}`).join("\n\n");
|
|
38842
|
+
}
|
|
38843
|
+
function megabytes(bytes) {
|
|
38844
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
38845
|
+
}
|
|
38846
|
+
function boardTooLarge(file2, fileBytes, frameBytes, computer = "Mac") {
|
|
38847
|
+
if (fileBytes <= CANVAS_BOARD_MAX_BYTES && frameBytes <= CANVAS_BOARD_FRAME_MAX_BYTES) return null;
|
|
38848
|
+
const name = file2.replace(/\.html?$/i, "");
|
|
38849
|
+
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}.`;
|
|
38850
|
+
}
|
|
38851
|
+
|
|
38852
|
+
// src/canvas/canvasFolder.ts
|
|
38853
|
+
var HEAD_BYTES = 8192;
|
|
38854
|
+
var CanvasError = class extends Error {
|
|
38855
|
+
};
|
|
38856
|
+
async function realFolder(path2) {
|
|
38857
|
+
const st = await lstat(path2).catch((e) => {
|
|
38858
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
38859
|
+
throw e;
|
|
38860
|
+
});
|
|
38861
|
+
if (!st) return false;
|
|
38862
|
+
if (st.isSymbolicLink() || !st.isDirectory()) throw new CanvasError(`${path2} is not a folder of its own, so it is not served as a canvas.`);
|
|
38863
|
+
return true;
|
|
38864
|
+
}
|
|
38865
|
+
async function canvasThere(folder) {
|
|
38866
|
+
return await realFolder(dirname12(folder)) && await realFolder(folder);
|
|
38867
|
+
}
|
|
38868
|
+
async function openBoard(path2) {
|
|
38869
|
+
const handle = await open5(path2, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK).catch(() => null);
|
|
38870
|
+
if (!handle) return null;
|
|
38871
|
+
const st = await handle.stat();
|
|
38872
|
+
if (!st.isFile()) {
|
|
38873
|
+
await handle.close();
|
|
38874
|
+
return null;
|
|
38875
|
+
}
|
|
38876
|
+
return {
|
|
38877
|
+
size: st.size,
|
|
38878
|
+
mtimeMs: st.mtimeMs,
|
|
38879
|
+
async read(max) {
|
|
38880
|
+
const buffer = Buffer.alloc(Math.min(max, st.size));
|
|
38881
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
38882
|
+
return buffer.subarray(0, bytesRead);
|
|
38883
|
+
},
|
|
38884
|
+
close: () => handle.close()
|
|
38885
|
+
};
|
|
38886
|
+
}
|
|
38887
|
+
async function listCanvas(folder, heads = /* @__PURE__ */ new Map()) {
|
|
38888
|
+
if (!await canvasThere(folder)) return { boards: [], missing: true };
|
|
38889
|
+
const entries = await readdir5(folder, { withFileTypes: true });
|
|
38890
|
+
const files = [];
|
|
38891
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38892
|
+
for (const e of entries) {
|
|
38893
|
+
if (!e.isFile() || !isBoardFile(e.name)) continue;
|
|
38894
|
+
const st = await lstat(join16(folder, e.name)).catch(() => null);
|
|
38895
|
+
if (!st?.isFile()) continue;
|
|
38896
|
+
const key = `${e.name}
|
|
38897
|
+
${st.mtimeMs}
|
|
38898
|
+
${st.size}`;
|
|
38899
|
+
seen.add(key);
|
|
38900
|
+
let head = heads.get(key);
|
|
38901
|
+
if (head === void 0) {
|
|
38902
|
+
const board = await openBoard(join16(folder, e.name));
|
|
38903
|
+
if (!board) continue;
|
|
38904
|
+
try {
|
|
38905
|
+
head = (await board.read(HEAD_BYTES)).toString("utf8");
|
|
38906
|
+
} finally {
|
|
38907
|
+
await board.close();
|
|
38908
|
+
}
|
|
38909
|
+
heads.set(key, head);
|
|
38910
|
+
}
|
|
38911
|
+
files.push({ file: e.name, mtimeMs: st.mtimeMs, bytes: st.size, head });
|
|
38912
|
+
}
|
|
38913
|
+
for (const key of heads.keys()) if (!seen.has(key)) heads.delete(key);
|
|
38914
|
+
const { boards, truncated } = boardsFrom(files);
|
|
38915
|
+
return { boards, ...truncated ? { truncated: true } : {} };
|
|
38916
|
+
}
|
|
38917
|
+
async function readBoard(folder, file2, computer = "Mac") {
|
|
38918
|
+
const notHere = new CanvasError(`"${file2}" is not on this canvas.`);
|
|
38919
|
+
if (!isBoardFile(file2) || !await canvasThere(folder)) throw notHere;
|
|
38920
|
+
const board = await openBoard(join16(folder, file2));
|
|
38921
|
+
if (!board) throw notHere;
|
|
38922
|
+
try {
|
|
38923
|
+
const tooLarge = boardTooLarge(file2, board.size, 0, computer);
|
|
38924
|
+
if (tooLarge) throw new CanvasError(tooLarge);
|
|
38925
|
+
const data = await board.read(CANVAS_BOARD_MAX_BYTES);
|
|
38926
|
+
return { html: data.toString("utf8"), modified: new Date(board.mtimeMs).toISOString(), bytes: data.length };
|
|
38927
|
+
} finally {
|
|
38928
|
+
await board.close();
|
|
38929
|
+
}
|
|
38930
|
+
}
|
|
38931
|
+
|
|
38932
|
+
// src/canvas/canvasWatcher.ts
|
|
38933
|
+
var CANVAS_POLL_MS = 400;
|
|
38934
|
+
var realEvery = (fn, ms) => {
|
|
38935
|
+
const timer = setInterval(fn, ms);
|
|
38936
|
+
timer.unref?.();
|
|
38937
|
+
return () => clearInterval(timer);
|
|
38938
|
+
};
|
|
38939
|
+
var CanvasWatcher = class {
|
|
38940
|
+
constructor(every = realEvery, list = listCanvas, onError = () => {
|
|
38941
|
+
}) {
|
|
38942
|
+
this.every = every;
|
|
38943
|
+
this.list = list;
|
|
38944
|
+
this.onError = onError;
|
|
38945
|
+
}
|
|
38946
|
+
every;
|
|
38947
|
+
list;
|
|
38948
|
+
onError;
|
|
38949
|
+
folders = /* @__PURE__ */ new Map();
|
|
38950
|
+
/** Starts telling `onChange` of every change to `folder` after `current`, the listing the subscriber has. Returns how to stop. */
|
|
38951
|
+
watch(folder, current, onChange) {
|
|
38952
|
+
let watched = this.folders.get(folder);
|
|
38953
|
+
if (!watched) {
|
|
38954
|
+
const created = { subscribers: /* @__PURE__ */ new Set(), heads: /* @__PURE__ */ new Map(), busy: false, stop: () => {
|
|
38955
|
+
} };
|
|
38956
|
+
created.stop = this.every(() => void this.look(folder, created), CANVAS_POLL_MS);
|
|
38957
|
+
this.folders.set(folder, created);
|
|
38958
|
+
watched = created;
|
|
38959
|
+
}
|
|
38960
|
+
const subscriber = { lastKey: listingKey(current), onChange };
|
|
38961
|
+
watched.subscribers.add(subscriber);
|
|
38962
|
+
const w = watched;
|
|
38963
|
+
return () => {
|
|
38964
|
+
w.subscribers.delete(subscriber);
|
|
38965
|
+
if (w.subscribers.size === 0 && this.folders.get(folder) === w) {
|
|
38966
|
+
w.stop();
|
|
38967
|
+
this.folders.delete(folder);
|
|
38968
|
+
}
|
|
38969
|
+
};
|
|
38970
|
+
}
|
|
38971
|
+
/** How many folders are being polled. */
|
|
38972
|
+
get size() {
|
|
38973
|
+
return this.folders.size;
|
|
38974
|
+
}
|
|
38975
|
+
/** Looks at one folder now; a look still running makes this one a no-op. */
|
|
38976
|
+
async look(folder, watched = this.folders.get(folder)) {
|
|
38977
|
+
if (!watched || watched.busy) return;
|
|
38978
|
+
watched.busy = true;
|
|
38979
|
+
try {
|
|
38980
|
+
const listing = await this.list(folder, watched.heads);
|
|
38981
|
+
const key = listingKey(listing);
|
|
38982
|
+
for (const s of watched.subscribers) {
|
|
38983
|
+
if (s.lastKey === key) continue;
|
|
38984
|
+
s.lastKey = key;
|
|
38985
|
+
s.onChange(listing);
|
|
38986
|
+
}
|
|
38987
|
+
} catch (e) {
|
|
38988
|
+
this.onError(folder, e);
|
|
38989
|
+
} finally {
|
|
38990
|
+
watched.busy = false;
|
|
38991
|
+
}
|
|
38992
|
+
}
|
|
38993
|
+
/** Stops every poll (the daemon is stopping). */
|
|
38994
|
+
stopAll() {
|
|
38995
|
+
for (const w of this.folders.values()) w.stop();
|
|
38996
|
+
this.folders.clear();
|
|
38997
|
+
}
|
|
38998
|
+
};
|
|
38999
|
+
|
|
39000
|
+
// src/canvas/canvasService.ts
|
|
39001
|
+
var CanvasService = class {
|
|
39002
|
+
constructor(sessions, watcher = new CanvasWatcher(), home = homedir9(), computer = computerWord()) {
|
|
39003
|
+
this.sessions = sessions;
|
|
39004
|
+
this.watcher = watcher;
|
|
39005
|
+
this.home = home;
|
|
39006
|
+
this.computer = computer;
|
|
39007
|
+
}
|
|
39008
|
+
sessions;
|
|
39009
|
+
watcher;
|
|
39010
|
+
home;
|
|
39011
|
+
computer;
|
|
39012
|
+
/** The canvas folder of `cwd`, or `CanvasError` when no session works there. */
|
|
39013
|
+
folderOf(cwd) {
|
|
39014
|
+
const allowed = allowedCanvasCwd(cwd, this.sessions(), this.home);
|
|
39015
|
+
if (!allowed) throw new CanvasError(`No session on this ${this.computer} works in ${cwd}, so it has no canvas to show.`);
|
|
39016
|
+
return canvasFolderOf(allowed);
|
|
39017
|
+
}
|
|
39018
|
+
async list(cwd) {
|
|
39019
|
+
const folder = this.folderOf(cwd);
|
|
39020
|
+
return { folder, ...await listCanvas(folder) };
|
|
39021
|
+
}
|
|
39022
|
+
async board(cwd, file2) {
|
|
39023
|
+
return readBoard(this.folderOf(cwd), file2, this.computer);
|
|
39024
|
+
}
|
|
39025
|
+
/** The canvas now, and `onChange` with each change after it until `stop`. */
|
|
39026
|
+
async watch(cwd, onChange) {
|
|
39027
|
+
const reply2 = await this.list(cwd);
|
|
39028
|
+
const stop2 = this.watcher.watch(reply2.folder, reply2, (listing) => onChange({ folder: reply2.folder, ...listing }));
|
|
39029
|
+
return { reply: reply2, stop: stop2 };
|
|
39030
|
+
}
|
|
39031
|
+
stop() {
|
|
39032
|
+
this.watcher.stopAll();
|
|
39033
|
+
}
|
|
39034
|
+
};
|
|
39035
|
+
|
|
38606
39036
|
// src/daemon/wsHandler.ts
|
|
38607
39037
|
var HELLO_TIMEOUT_MS = 5e3;
|
|
38608
39038
|
var NO_PUSH = { type: "push.state", registered: false, delivery: "off", events: [] };
|
|
@@ -38629,6 +39059,8 @@ var Connection = class {
|
|
|
38629
39059
|
/** Live terminals this client has open, by session. */
|
|
38630
39060
|
terms = /* @__PURE__ */ new Map();
|
|
38631
39061
|
stopWatchingPush;
|
|
39062
|
+
/** Canvases this client watches, by `cwd` as it sent it (PROTOCOL.md "Canvas", "Watching"). */
|
|
39063
|
+
canvasWatches = /* @__PURE__ */ new Map();
|
|
38632
39064
|
helloTimer;
|
|
38633
39065
|
onUpdated = (s) => this.send({ type: "session.updated", session: s });
|
|
38634
39066
|
onRemoved = (id) => {
|
|
@@ -38676,7 +39108,7 @@ var Connection = class {
|
|
|
38676
39108
|
try {
|
|
38677
39109
|
await this.dispatch(frame);
|
|
38678
39110
|
} catch (e) {
|
|
38679
|
-
const id = frame.type === "input" ? frame.id : void 0;
|
|
39111
|
+
const id = frame.type === "input" || frame.type === "canvas" || frame.type === "canvas.subscribe" || frame.type === "canvas.board" ? frame.id : void 0;
|
|
38680
39112
|
if (e instanceof UnknownSessionError) return this.fail("unknown_session", e.message, frame.type, false, id);
|
|
38681
39113
|
if (e instanceof SessionExistsError) return this.fail("tmux_failed", e.message, frame.type, false, id);
|
|
38682
39114
|
if (e instanceof BadCwdError || e instanceof UnknownGroupError) return this.fail("bad_frame", e.message, frame.type, false, id);
|
|
@@ -38699,6 +39131,8 @@ var Connection = class {
|
|
|
38699
39131
|
for (const id of this.subscriptions) this.d.registry.unsubscribe(id);
|
|
38700
39132
|
this.subscriptions.clear();
|
|
38701
39133
|
for (const id of [...this.terms.keys()]) this.closeTerm(id);
|
|
39134
|
+
for (const stop2 of this.canvasWatches.values()) stop2();
|
|
39135
|
+
this.canvasWatches.clear();
|
|
38702
39136
|
if (this.authed) {
|
|
38703
39137
|
this.d.registry.off("updated", this.onUpdated);
|
|
38704
39138
|
this.d.registry.off("removed", this.onRemoved);
|
|
@@ -38751,7 +39185,7 @@ var Connection = class {
|
|
|
38751
39185
|
}
|
|
38752
39186
|
this.d.prompts?.on("opened", this.onPrompt);
|
|
38753
39187
|
this.d.prompts?.on("closed", this.onPrompt);
|
|
38754
|
-
for (const
|
|
39188
|
+
for (const open7 of this.d.prompts?.list() ?? []) this.send(open7);
|
|
38755
39189
|
this.d.activity?.on("activity", this.onActivity);
|
|
38756
39190
|
}
|
|
38757
39191
|
async dispatch(frame) {
|
|
@@ -38849,6 +39283,8 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
38849
39283
|
return this.send({ type: "conversations", conversations: await this.d.conversations.list(this.anyAgentConversations) });
|
|
38850
39284
|
case "folders":
|
|
38851
39285
|
return this.send(await listFolders(frame.path));
|
|
39286
|
+
case "limits":
|
|
39287
|
+
return this.send({ type: "limits", limits: this.d.limits?.list() ?? [] });
|
|
38852
39288
|
case "voice":
|
|
38853
39289
|
if (!this.d.voice) return this.fail("bad_frame", "this daemon keeps no voice keys", frame.type);
|
|
38854
39290
|
if (!this.watchesVoice) this.d.voice.on("changed", this.onVoice);
|
|
@@ -38875,6 +39311,11 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
38875
39311
|
throw e;
|
|
38876
39312
|
}
|
|
38877
39313
|
}
|
|
39314
|
+
case "canvas":
|
|
39315
|
+
case "canvas.subscribe":
|
|
39316
|
+
case "canvas.unsubscribe":
|
|
39317
|
+
case "canvas.board":
|
|
39318
|
+
return this.handleCanvas(frame);
|
|
38878
39319
|
case "session.group": {
|
|
38879
39320
|
const before = r.get(frame.sessionId);
|
|
38880
39321
|
const after = r.setGroup(frame.sessionId, frame.group, frame.index);
|
|
@@ -38930,6 +39371,40 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
38930
39371
|
}
|
|
38931
39372
|
}
|
|
38932
39373
|
}
|
|
39374
|
+
/** The canvas frames (PROTOCOL.md "Canvas"): a refused folder, a missing or too large board is `bad_frame` with the request's `id`. */
|
|
39375
|
+
async handleCanvas(frame) {
|
|
39376
|
+
if (frame.type === "canvas.unsubscribe") {
|
|
39377
|
+
this.canvasWatches.get(frame.cwd)?.();
|
|
39378
|
+
this.canvasWatches.delete(frame.cwd);
|
|
39379
|
+
return;
|
|
39380
|
+
}
|
|
39381
|
+
const canvas = this.d.canvas;
|
|
39382
|
+
if (!canvas) return this.fail("bad_frame", "this daemon serves no canvas", frame.type, false, frame.id);
|
|
39383
|
+
const { cwd, id } = frame;
|
|
39384
|
+
const framed = (reply2, withId) => ({ type: "canvas", ...withId ? { id } : {}, cwd, ...reply2 });
|
|
39385
|
+
try {
|
|
39386
|
+
if (frame.type === "canvas") return this.send(framed(await canvas.list(cwd), true));
|
|
39387
|
+
if (frame.type === "canvas.subscribe") {
|
|
39388
|
+
const again = this.canvasWatches.get(cwd);
|
|
39389
|
+
if (!again && this.canvasWatches.size >= CANVAS_SUBSCRIPTIONS_MAX) {
|
|
39390
|
+
return this.fail("bad_frame", `A connection watches at most ${CANVAS_SUBSCRIPTIONS_MAX} canvases; unsubscribe from one first.`, frame.type, false, id);
|
|
39391
|
+
}
|
|
39392
|
+
const { reply: reply3, stop: stop2 } = await canvas.watch(cwd, (changed) => this.send(framed(changed, false)));
|
|
39393
|
+
if (!this.authed) return stop2();
|
|
39394
|
+
again?.();
|
|
39395
|
+
this.canvasWatches.set(cwd, stop2);
|
|
39396
|
+
return this.send(framed(reply3, true));
|
|
39397
|
+
}
|
|
39398
|
+
const board = await canvas.board(cwd, frame.file);
|
|
39399
|
+
const reply2 = { type: "canvas.board", id, cwd, file: frame.file, ...board };
|
|
39400
|
+
const tooLarge = boardTooLarge(frame.file, board.bytes, Buffer.byteLength(JSON.stringify(reply2)), computerWord());
|
|
39401
|
+
if (tooLarge) return this.fail("bad_frame", tooLarge, frame.type, false, id);
|
|
39402
|
+
return this.send(reply2);
|
|
39403
|
+
} catch (e) {
|
|
39404
|
+
if (e instanceof CanvasError) return this.fail("bad_frame", e.message, frame.type, false, id);
|
|
39405
|
+
throw e;
|
|
39406
|
+
}
|
|
39407
|
+
}
|
|
38933
39408
|
openTerm(sessionId, cols, rows) {
|
|
38934
39409
|
if (!this.d.openTerm) return this.fail("bad_frame", "this daemon streams no terminal", "term.open");
|
|
38935
39410
|
this.requireSession(sessionId);
|
|
@@ -39016,7 +39491,7 @@ var PairingWatch = class {
|
|
|
39016
39491
|
// src/attachments/attachmentStore.ts
|
|
39017
39492
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
39018
39493
|
import { existsSync as existsSync19 } from "node:fs";
|
|
39019
|
-
import { join as
|
|
39494
|
+
import { join as join17 } from "node:path";
|
|
39020
39495
|
|
|
39021
39496
|
// src/attachments/attachmentName.ts
|
|
39022
39497
|
var EXTENSION_FOR_MIME = {
|
|
@@ -39050,7 +39525,7 @@ function stamp(d) {
|
|
|
39050
39525
|
function createAttachmentStore(dir, now = () => /* @__PURE__ */ new Date()) {
|
|
39051
39526
|
return {
|
|
39052
39527
|
async save(sessionId, name, mime, data) {
|
|
39053
|
-
const folder =
|
|
39528
|
+
const folder = join17(dir, sessionId);
|
|
39054
39529
|
await mkdir(folder, { recursive: true });
|
|
39055
39530
|
const path2 = unusedPath(folder, attachmentFileName(name, mime, now()));
|
|
39056
39531
|
await writeFile(path2, data, { flag: "wx" });
|
|
@@ -39062,8 +39537,8 @@ function unusedPath(folder, fileName) {
|
|
|
39062
39537
|
const dot = fileName.lastIndexOf(".");
|
|
39063
39538
|
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
39064
39539
|
const ext = dot > 0 ? fileName.slice(dot) : "";
|
|
39065
|
-
let candidate =
|
|
39066
|
-
for (let n = 2; existsSync19(candidate); n++) candidate =
|
|
39540
|
+
let candidate = join17(folder, fileName);
|
|
39541
|
+
for (let n = 2; existsSync19(candidate); n++) candidate = join17(folder, `${stem}-${n}${ext}`);
|
|
39067
39542
|
return candidate;
|
|
39068
39543
|
}
|
|
39069
39544
|
|
|
@@ -39078,14 +39553,14 @@ function accessHash(token) {
|
|
|
39078
39553
|
|
|
39079
39554
|
// src/relay/e2eKey.ts
|
|
39080
39555
|
import { existsSync as existsSync20, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync15 } from "node:fs";
|
|
39081
|
-
import { dirname as
|
|
39556
|
+
import { dirname as dirname13 } from "node:path";
|
|
39082
39557
|
function loadOrCreateE2EKey(path2) {
|
|
39083
39558
|
if (existsSync20(path2)) {
|
|
39084
39559
|
const raw = Buffer.from(readFileSync14(path2, "utf8").trim(), "base64");
|
|
39085
39560
|
if (raw.length === 32) return x25519FromRaw(raw);
|
|
39086
39561
|
}
|
|
39087
39562
|
const pair2 = generateX25519();
|
|
39088
|
-
mkdirSync13(
|
|
39563
|
+
mkdirSync13(dirname13(path2), { recursive: true });
|
|
39089
39564
|
writeFileSync15(path2, privateRaw(pair2.privateKey).toString("base64") + "\n", { mode: 384 });
|
|
39090
39565
|
return pair2;
|
|
39091
39566
|
}
|
|
@@ -39334,7 +39809,7 @@ var RelayLink = class {
|
|
|
39334
39809
|
|
|
39335
39810
|
// src/push/boardDevices.ts
|
|
39336
39811
|
import { existsSync as existsSync21, mkdirSync as mkdirSync14, readFileSync as readFileSync15, writeFileSync as writeFileSync16 } from "node:fs";
|
|
39337
|
-
import { dirname as
|
|
39812
|
+
import { dirname as dirname14 } from "node:path";
|
|
39338
39813
|
var BoardDevices = class {
|
|
39339
39814
|
constructor(path2) {
|
|
39340
39815
|
this.path = path2;
|
|
@@ -39377,7 +39852,7 @@ var BoardDevices = class {
|
|
|
39377
39852
|
}
|
|
39378
39853
|
save() {
|
|
39379
39854
|
if (!this.path) return;
|
|
39380
|
-
mkdirSync14(
|
|
39855
|
+
mkdirSync14(dirname14(this.path), { recursive: true });
|
|
39381
39856
|
writeFileSync16(this.path, JSON.stringify(this.list(), null, 2) + "\n", { mode: 384 });
|
|
39382
39857
|
}
|
|
39383
39858
|
};
|
|
@@ -39630,7 +40105,7 @@ var BoardPusher = class {
|
|
|
39630
40105
|
|
|
39631
40106
|
// src/push/pushDevices.ts
|
|
39632
40107
|
import { existsSync as existsSync22, mkdirSync as mkdirSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync17 } from "node:fs";
|
|
39633
|
-
import { dirname as
|
|
40108
|
+
import { dirname as dirname15 } from "node:path";
|
|
39634
40109
|
var PushDevices = class {
|
|
39635
40110
|
constructor(path2) {
|
|
39636
40111
|
this.path = path2;
|
|
@@ -39673,7 +40148,7 @@ var PushDevices = class {
|
|
|
39673
40148
|
}
|
|
39674
40149
|
save() {
|
|
39675
40150
|
if (!this.path) return;
|
|
39676
|
-
mkdirSync15(
|
|
40151
|
+
mkdirSync15(dirname15(this.path), { recursive: true });
|
|
39677
40152
|
writeFileSync17(this.path, JSON.stringify(this.list(), null, 2) + "\n", { mode: 384 });
|
|
39678
40153
|
}
|
|
39679
40154
|
};
|
|
@@ -40284,6 +40759,87 @@ function promptText2(prompt) {
|
|
|
40284
40759
|
}
|
|
40285
40760
|
}
|
|
40286
40761
|
|
|
40762
|
+
// src/usage/planLimits.ts
|
|
40763
|
+
var PlanLimits = class {
|
|
40764
|
+
byAgent = /* @__PURE__ */ new Map();
|
|
40765
|
+
/** An agent's latest windows replace what it reported before. A report with none changes nothing. */
|
|
40766
|
+
record(agent, limits) {
|
|
40767
|
+
if (limits.length > 0) this.byAgent.set(agent, [...limits]);
|
|
40768
|
+
}
|
|
40769
|
+
/** Every window not yet reset at `now`, shortest first, then by agent. */
|
|
40770
|
+
list(now = Date.now()) {
|
|
40771
|
+
return [...this.byAgent.values()].flat().filter((l) => l.resetsAt === void 0 || Date.parse(l.resetsAt) > now).sort((a, b) => a.minutes - b.minutes || a.agent.localeCompare(b.agent)).slice(0, LIMITS_MAX);
|
|
40772
|
+
}
|
|
40773
|
+
};
|
|
40774
|
+
|
|
40775
|
+
// src/usage/readUsage.ts
|
|
40776
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
40777
|
+
import { open as open6 } from "node:fs/promises";
|
|
40778
|
+
|
|
40779
|
+
// src/usage/codexUsage.ts
|
|
40780
|
+
function codexUsageIn(jsonl) {
|
|
40781
|
+
const lines = jsonl.split("\n");
|
|
40782
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
40783
|
+
const line = lines[i];
|
|
40784
|
+
if (!line.includes('"token_count"')) continue;
|
|
40785
|
+
const entry = parse7(line);
|
|
40786
|
+
if (entry?.payload?.type !== "token_count") continue;
|
|
40787
|
+
const at = typeof entry.timestamp === "string" && !Number.isNaN(Date.parse(entry.timestamp)) ? new Date(entry.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
40788
|
+
const info = entry.payload.info ?? {};
|
|
40789
|
+
const used = info.last_token_usage?.total_tokens;
|
|
40790
|
+
const size = info.model_context_window;
|
|
40791
|
+
const context = typeof used === "number" && typeof size === "number" && size >= 1 ? { used: Math.max(0, Math.round(used)), size: Math.round(size) } : void 0;
|
|
40792
|
+
const limits = [];
|
|
40793
|
+
const rl = entry.payload.rate_limits ?? {};
|
|
40794
|
+
for (const key of ["primary", "secondary"]) {
|
|
40795
|
+
const w = rl[key];
|
|
40796
|
+
if (!w || typeof w.used_percent !== "number" || typeof w.window_minutes !== "number" || w.window_minutes < 1) continue;
|
|
40797
|
+
const resets = typeof w.resets_at === "number" ? w.resets_at * 1e3 : typeof w.resets_in_seconds === "number" ? Date.parse(at) + w.resets_in_seconds * 1e3 : void 0;
|
|
40798
|
+
limits.push({
|
|
40799
|
+
agent: "codex",
|
|
40800
|
+
minutes: Math.round(w.window_minutes),
|
|
40801
|
+
usedPercent: Math.max(0, Math.min(100, w.used_percent)),
|
|
40802
|
+
...resets !== void 0 ? { resetsAt: new Date(resets).toISOString() } : {},
|
|
40803
|
+
at
|
|
40804
|
+
});
|
|
40805
|
+
}
|
|
40806
|
+
return { ...context ? { context } : {}, limits };
|
|
40807
|
+
}
|
|
40808
|
+
return null;
|
|
40809
|
+
}
|
|
40810
|
+
function parse7(line) {
|
|
40811
|
+
try {
|
|
40812
|
+
return JSON.parse(line);
|
|
40813
|
+
} catch {
|
|
40814
|
+
return null;
|
|
40815
|
+
}
|
|
40816
|
+
}
|
|
40817
|
+
|
|
40818
|
+
// src/usage/readUsage.ts
|
|
40819
|
+
var TAIL_BYTES2 = 256 * 1024;
|
|
40820
|
+
async function readTail(path2) {
|
|
40821
|
+
const file2 = await open6(path2, "r");
|
|
40822
|
+
try {
|
|
40823
|
+
const { size } = await file2.stat();
|
|
40824
|
+
const length = Math.min(size, TAIL_BYTES2);
|
|
40825
|
+
const buffer = Buffer.alloc(length);
|
|
40826
|
+
await file2.read(buffer, 0, length, size - length);
|
|
40827
|
+
return buffer.toString("utf8");
|
|
40828
|
+
} finally {
|
|
40829
|
+
await file2.close();
|
|
40830
|
+
}
|
|
40831
|
+
}
|
|
40832
|
+
async function readCodexUsage(rolloutPath) {
|
|
40833
|
+
return codexUsageIn(await readTail(rolloutPath));
|
|
40834
|
+
}
|
|
40835
|
+
function readUserStatusLine(settingsPath) {
|
|
40836
|
+
try {
|
|
40837
|
+
return userStatusLineIn(JSON.parse(readFileSync17(settingsPath, "utf8")));
|
|
40838
|
+
} catch {
|
|
40839
|
+
return void 0;
|
|
40840
|
+
}
|
|
40841
|
+
}
|
|
40842
|
+
|
|
40287
40843
|
// src/daemon/server.ts
|
|
40288
40844
|
async function startDaemon(opts = {}) {
|
|
40289
40845
|
ensureDir();
|
|
@@ -40291,10 +40847,16 @@ async function startDaemon(opts = {}) {
|
|
|
40291
40847
|
const controlPort2 = opts.controlPort ?? CONTROL_PORT;
|
|
40292
40848
|
const log = opts.log ?? createLogger({ file: paths.log, level: process.env["GRENADE_LOG"] ?? "info" });
|
|
40293
40849
|
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] };
|
|
40850
|
+
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, limits: 1, codexActivity: 1, agents: [...AGENTS] };
|
|
40295
40851
|
const allowPlainLan = opts.allowPlainLan === true;
|
|
40296
|
-
const tmux = opts.tmux ?? createTmux({ agentFlags: {
|
|
40297
|
-
|
|
40852
|
+
const tmux = opts.tmux ?? createTmux({ agentFlags: {
|
|
40853
|
+
// Read as each session starts, so a status line the user set up since is the one it runs.
|
|
40854
|
+
get claude() {
|
|
40855
|
+
return claudeHookFlags(port, readUserStatusLine(join18(opts.claudeDir ?? paths.claudeDir, "settings.json")));
|
|
40856
|
+
},
|
|
40857
|
+
codex: codexHookFlags(port)
|
|
40858
|
+
}, serverScope: underSystemd() });
|
|
40859
|
+
const tokens2 = new TokenStore(opts.tokensPath === null ? void 0 : opts.tokensPath ?? paths.tokens);
|
|
40298
40860
|
const codes = new PairingCodes();
|
|
40299
40861
|
const pairing = new PairingWatch();
|
|
40300
40862
|
const registry2 = new SessionRegistry({
|
|
@@ -40304,13 +40866,14 @@ async function startDaemon(opts = {}) {
|
|
|
40304
40866
|
});
|
|
40305
40867
|
const summarizer = startSummarizer(registry2, log, opts.summaries ?? process.env["GRENADE_SUMMARIES"] !== "off");
|
|
40306
40868
|
await registry2.adopt();
|
|
40307
|
-
const groupsPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ?
|
|
40869
|
+
const groupsPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ? join18(dirname16(opts.sessionsPath), "groups.json") : paths.groups;
|
|
40308
40870
|
const groupOrder = new GroupOrderStore(registry2, log, groupsPath);
|
|
40309
40871
|
const poller = startPoller({ tmux, registry: registry2, log });
|
|
40310
40872
|
const pinnedTerminal = opts.terminal ?? terminalFromEnv();
|
|
40311
40873
|
const mirror = new TerminalMirror({ registry: registry2, log, terminal: pinnedTerminal ?? (() => readTerminalSetting()) });
|
|
40312
40874
|
const terminalStatus = () => ({ ...mirror.status(), pinned: pinnedTerminal !== void 0 });
|
|
40313
40875
|
mirror.start().catch((e) => log.warn("Could not mirror sessions into a terminal", { error: e }));
|
|
40876
|
+
const planLimits = new PlanLimits();
|
|
40314
40877
|
const http = createServer2(async (req, res) => {
|
|
40315
40878
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
40316
40879
|
try {
|
|
@@ -40324,6 +40887,11 @@ async function startDaemon(opts = {}) {
|
|
|
40324
40887
|
closePromptsByHook(prompts, session, body);
|
|
40325
40888
|
return sendJson(res, r.status, r.body);
|
|
40326
40889
|
}
|
|
40890
|
+
if (req.method === "POST" && url2.pathname === STATUS_LINE_PATH) {
|
|
40891
|
+
if (!isLoopback(req.socket.remoteAddress)) return sendJson(res, 403, { error: "forbidden" });
|
|
40892
|
+
applyStatusLine(url2.searchParams.get("session"), await readBody(req));
|
|
40893
|
+
return sendJson(res, 200, { ok: true });
|
|
40894
|
+
}
|
|
40327
40895
|
if (req.method === "POST" && url2.pathname === "/hooks/codex") {
|
|
40328
40896
|
if (!isLoopback(req.socket.remoteAddress)) return sendJson(res, 403, { error: "forbidden" });
|
|
40329
40897
|
const r = applyCodexHook(url2.searchParams.get("session"), await readBody(req));
|
|
@@ -40376,7 +40944,7 @@ async function startDaemon(opts = {}) {
|
|
|
40376
40944
|
for (const { id, path: path2 } of registry2.transcripts()) {
|
|
40377
40945
|
readActivity(id, path2).catch((e) => log.debug("Could not read the activity from a saved transcript", { session: id, path: path2, error: e }));
|
|
40378
40946
|
}
|
|
40379
|
-
const marksPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ?
|
|
40947
|
+
const marksPath = opts.sessionsPath === null ? void 0 : opts.sessionsPath ? join18(dirname16(opts.sessionsPath), "conversations.json") : paths.conversations;
|
|
40380
40948
|
const marks = new ConversationMarks(log, marksPath);
|
|
40381
40949
|
const held = () => heldConversations(registry2.transcripts(), (id) => (registry2.get(id)?.status ?? "gone") !== "gone");
|
|
40382
40950
|
const conversationIndex = new AllConversations([
|
|
@@ -40390,6 +40958,7 @@ async function startDaemon(opts = {}) {
|
|
|
40390
40958
|
log,
|
|
40391
40959
|
...opts.voiceFetch ? { fetch: opts.voiceFetch } : {}
|
|
40392
40960
|
});
|
|
40961
|
+
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
40962
|
const models = {
|
|
40394
40963
|
async switch(session, model, effort) {
|
|
40395
40964
|
if (switchingModel.has(session.id)) throw new ModelSwitchError("A model switch is already under way in this session.");
|
|
@@ -40464,6 +41033,10 @@ async function startDaemon(opts = {}) {
|
|
|
40464
41033
|
},
|
|
40465
41034
|
onTranscript: (id, path2, event) => {
|
|
40466
41035
|
registry2.setTranscript(id, path2);
|
|
41036
|
+
readCodexUsage(path2).then((usage) => {
|
|
41037
|
+
if (usage?.context) registry2.setContext(id, usage.context);
|
|
41038
|
+
if (usage) planLimits.record("codex", usage.limits);
|
|
41039
|
+
}).catch((e) => log.debug("Could not read the usage from a Codex rollout", { session: id, path: path2, error: e }));
|
|
40467
41040
|
catchUp.cancel(id);
|
|
40468
41041
|
codexStopCatchUp.cancel(id);
|
|
40469
41042
|
readActivity(id, path2).then((entries) => {
|
|
@@ -40472,6 +41045,17 @@ async function startDaemon(opts = {}) {
|
|
|
40472
41045
|
}).catch((e) => log.debug("Could not read the activity from a Codex rollout", { session: id, path: path2, error: e }));
|
|
40473
41046
|
}
|
|
40474
41047
|
});
|
|
41048
|
+
function applyStatusLine(session, body) {
|
|
41049
|
+
let payload;
|
|
41050
|
+
try {
|
|
41051
|
+
payload = JSON.parse(body || "{}");
|
|
41052
|
+
} catch {
|
|
41053
|
+
return;
|
|
41054
|
+
}
|
|
41055
|
+
const usage = claudeStatusUsage(payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
41056
|
+
planLimits.record("claude", usage.limits);
|
|
41057
|
+
if (session && usage.context) registry2.setContext(session, usage.context);
|
|
41058
|
+
}
|
|
40475
41059
|
function handlePair(raw, res) {
|
|
40476
41060
|
if (!allowPlainLan) return sendJson(res, 426, { error: "encryption_required" });
|
|
40477
41061
|
let json2;
|
|
@@ -40485,7 +41069,7 @@ async function startDaemon(opts = {}) {
|
|
|
40485
41069
|
const verdict = codes.verify(body.data.code);
|
|
40486
41070
|
if (verdict === "too_many_attempts") return sendJson(res, 429, { error: verdict });
|
|
40487
41071
|
if (verdict === "invalid_code") return sendJson(res, 400, { error: verdict });
|
|
40488
|
-
const token =
|
|
41072
|
+
const token = tokens2.issue(body.data.client);
|
|
40489
41073
|
pairing.paired(body.data.client, "lan");
|
|
40490
41074
|
log.info(`Paired a new phone: ${body.data.client.name}`, { platform: body.data.client.platform });
|
|
40491
41075
|
sendJson(res, 200, { token, daemon: info });
|
|
@@ -40494,7 +41078,7 @@ async function startDaemon(opts = {}) {
|
|
|
40494
41078
|
const verdict = codes.verify(credential);
|
|
40495
41079
|
if (verdict === "too_many_attempts") pairing.voided();
|
|
40496
41080
|
if (verdict !== "ok") return { ok: false, code: verdict };
|
|
40497
|
-
const token =
|
|
41081
|
+
const token = tokens2.issue(client, { sealed: true });
|
|
40498
41082
|
pairing.paired(client, route2);
|
|
40499
41083
|
log.info(`Paired a new phone: ${client.name}`, { platform: client.platform, route: route2 });
|
|
40500
41084
|
return { ok: true, token };
|
|
@@ -40515,7 +41099,7 @@ async function startDaemon(opts = {}) {
|
|
|
40515
41099
|
const attachments = createAttachmentStore(opts.attachmentsDir ?? paths.attachments);
|
|
40516
41100
|
const push2 = startPush({
|
|
40517
41101
|
registry: registry2,
|
|
40518
|
-
tokens,
|
|
41102
|
+
tokens: tokens2,
|
|
40519
41103
|
daemon: info,
|
|
40520
41104
|
staticKey: e2eKey,
|
|
40521
41105
|
relay: () => opts.relay === false ? null : loadRelayConfig(opts.relayPath ?? paths.relay),
|
|
@@ -40531,7 +41115,7 @@ async function startDaemon(opts = {}) {
|
|
|
40531
41115
|
prompts.on("answered", (id) => registry2.applyHook(id, "working", void 0, true));
|
|
40532
41116
|
const screenPrompts = new ScreenPrompts(registry2, prompts, log, (id) => switchingModel.has(id));
|
|
40533
41117
|
watchScreenBackground(registry2);
|
|
40534
|
-
const backgroundWatch = new ClaudeBackgroundWatch(registry2,
|
|
41118
|
+
const backgroundWatch = new ClaudeBackgroundWatch(registry2, join18(opts.claudeDir ?? paths.claudeDir, "sessions"), log);
|
|
40535
41119
|
backgroundWatch.start();
|
|
40536
41120
|
registry2.on("removed", (id) => {
|
|
40537
41121
|
prompts.closeSession(id);
|
|
@@ -40552,13 +41136,13 @@ async function startDaemon(opts = {}) {
|
|
|
40552
41136
|
return closed;
|
|
40553
41137
|
}
|
|
40554
41138
|
const devices = {
|
|
40555
|
-
list: () =>
|
|
41139
|
+
list: () => tokens2.list().map((r) => deviceOf(r, live2.routesOf(r.token))),
|
|
40556
41140
|
unpair(id) {
|
|
40557
|
-
const gone =
|
|
41141
|
+
const gone = tokens2.revoke(id);
|
|
40558
41142
|
return gone ? { closed: closeConnectionsOf([gone], `unpaired on the ${computerWord()}`) } : null;
|
|
40559
41143
|
},
|
|
40560
41144
|
unpairAll() {
|
|
40561
|
-
const gone =
|
|
41145
|
+
const gone = tokens2.revokeAll();
|
|
40562
41146
|
return { removed: gone.length, closed: closeConnectionsOf(gone, `unpaired on the ${computerWord()}`) };
|
|
40563
41147
|
}
|
|
40564
41148
|
};
|
|
@@ -40569,19 +41153,19 @@ async function startDaemon(opts = {}) {
|
|
|
40569
41153
|
sentInputs,
|
|
40570
41154
|
activity,
|
|
40571
41155
|
interrupted,
|
|
40572
|
-
isValidToken: (t) =>
|
|
41156
|
+
isValidToken: (t) => tokens2.has(t),
|
|
40573
41157
|
sealed: via.sealed,
|
|
40574
41158
|
route: via.route,
|
|
40575
41159
|
// Plain is for phones that were never encrypted, and only while the daemon allows it.
|
|
40576
|
-
acceptsPlain: (t) => allowPlainLan &&
|
|
41160
|
+
acceptsPlain: (t) => allowPlainLan && tokens2.get(t)?.sealed !== true,
|
|
40577
41161
|
onHello(connection, token, client) {
|
|
40578
41162
|
live2.add(token, connection);
|
|
40579
|
-
|
|
40580
|
-
if (!via.sealed) log.warn(`${client.name} connected without encryption. Update Grenade on that phone.`, { device:
|
|
41163
|
+
tokens2.touch(token, { sealed: via.sealed, client });
|
|
41164
|
+
if (!via.sealed) log.warn(`${client.name} connected without encryption. Update Grenade on that phone.`, { device: tokens2.get(token)?.id });
|
|
40581
41165
|
},
|
|
40582
41166
|
onEnd(connection, token) {
|
|
40583
41167
|
live2.remove(token, connection);
|
|
40584
|
-
|
|
41168
|
+
tokens2.touch(token);
|
|
40585
41169
|
},
|
|
40586
41170
|
pair: pairPhone,
|
|
40587
41171
|
push: push2.pusher,
|
|
@@ -40590,20 +41174,22 @@ async function startDaemon(opts = {}) {
|
|
|
40590
41174
|
groups: groupOrder,
|
|
40591
41175
|
models,
|
|
40592
41176
|
voice,
|
|
41177
|
+
canvas,
|
|
41178
|
+
limits: planLimits,
|
|
40593
41179
|
conversations,
|
|
40594
|
-
openTerm(
|
|
41180
|
+
openTerm(open7) {
|
|
40595
41181
|
const stream = new TermStream({
|
|
40596
|
-
...
|
|
40597
|
-
resize: (cols, rows, by) => registry2.resize(
|
|
40598
|
-
release: (by) => registry2.releaseSize(
|
|
41182
|
+
...open7,
|
|
41183
|
+
resize: (cols, rows, by) => registry2.resize(open7.sessionId, cols, rows, by),
|
|
41184
|
+
release: (by) => registry2.releaseSize(open7.sessionId, by),
|
|
40599
41185
|
log
|
|
40600
41186
|
});
|
|
40601
41187
|
stream.start();
|
|
40602
41188
|
return stream;
|
|
40603
41189
|
},
|
|
40604
41190
|
unpair(token) {
|
|
40605
|
-
const id =
|
|
40606
|
-
const gone = id ?
|
|
41191
|
+
const id = tokens2.get(token)?.id;
|
|
41192
|
+
const gone = id ? tokens2.revoke(id) : void 0;
|
|
40607
41193
|
if (gone) closeConnectionsOf([gone], "unpaired on the phone");
|
|
40608
41194
|
},
|
|
40609
41195
|
daemon: info,
|
|
@@ -40631,7 +41217,7 @@ async function startDaemon(opts = {}) {
|
|
|
40631
41217
|
});
|
|
40632
41218
|
const deviceIdleMs = opts.deviceIdleMs ?? DEVICE_IDLE_MS;
|
|
40633
41219
|
const unpairIdle = () => {
|
|
40634
|
-
if (deviceIdleMs > 0) closeConnectionsOf(
|
|
41220
|
+
if (deviceIdleMs > 0) closeConnectionsOf(tokens2.revokeIdle(deviceIdleMs), "not seen for a long time");
|
|
40635
41221
|
};
|
|
40636
41222
|
unpairIdle();
|
|
40637
41223
|
const idleTimer = setInterval(unpairIdle, 60 * 60 * 1e3);
|
|
@@ -40653,7 +41239,7 @@ async function startDaemon(opts = {}) {
|
|
|
40653
41239
|
config: config2,
|
|
40654
41240
|
name: info.name,
|
|
40655
41241
|
version: VERSION,
|
|
40656
|
-
accessHashes: () => [...
|
|
41242
|
+
accessHashes: () => [...tokens2.list().map((t) => accessHash(t.token)), ...offerAccess()],
|
|
40657
41243
|
localIps: () => localIpv4(networkInterfaces()),
|
|
40658
41244
|
openPipe: (conn, send, onEnd) => new PhonePipe({ conn, staticKey: e2eKey, send, makeConnection: (out, close) => makeConnection(out, close, { route: "relay", sealed: true }), log, onEnd }),
|
|
40659
41245
|
log
|
|
@@ -40673,7 +41259,7 @@ async function startDaemon(opts = {}) {
|
|
|
40673
41259
|
auto: () => opts.updates?.auto ?? readAuto()
|
|
40674
41260
|
});
|
|
40675
41261
|
const relayStatus = () => relayLink?.status() ?? { state: "off", phones: 0, ...opts.relay === false ? { disabled: true } : {} };
|
|
40676
|
-
|
|
41262
|
+
tokens2.onChange(() => relayLink?.tokensChanged());
|
|
40677
41263
|
startRelay();
|
|
40678
41264
|
const control2 = createControlServer({
|
|
40679
41265
|
registry: registry2,
|
|
@@ -40727,6 +41313,7 @@ async function startDaemon(opts = {}) {
|
|
|
40727
41313
|
mirror.stop();
|
|
40728
41314
|
poller.stop();
|
|
40729
41315
|
relayLink?.stop();
|
|
41316
|
+
canvas.stop();
|
|
40730
41317
|
push2.stop();
|
|
40731
41318
|
prompts.closeAll();
|
|
40732
41319
|
summarizer?.stop();
|
|
@@ -40918,18 +41505,18 @@ program2.command("kill <name>").description("end a session").action(async (name)
|
|
|
40918
41505
|
console.log(`killed ${id}`);
|
|
40919
41506
|
});
|
|
40920
41507
|
program2.command("install-hooks").description("also let a claude typed by hand in a Grenade shell report to grenaded (edits ~/.claude/settings.json); sessions Grenade starts have the hooks already").option("--port <port>", "daemon port the hooks post to", parsePort, DEFAULT_PORT).option("--print", "print the resulting settings instead of writing").option("--remove", "remove Grenade hooks").action((o) => {
|
|
40921
|
-
const current = existsSync23(paths.claudeSettings) ? JSON.parse(
|
|
41508
|
+
const current = existsSync23(paths.claudeSettings) ? JSON.parse(readFileSync18(paths.claudeSettings, "utf8")) : {};
|
|
40922
41509
|
const { settings, changed } = o.remove ? removeHooks(current) : mergeHooks(current, o.port);
|
|
40923
41510
|
if (o.print) return console.log(JSON.stringify(settings, null, 2));
|
|
40924
41511
|
if (o.remove) removeOldCodexHooks();
|
|
40925
41512
|
if (!changed) return console.log(`nothing to do: ${paths.claudeSettings} already up to date`);
|
|
40926
|
-
mkdirSync16(
|
|
41513
|
+
mkdirSync16(dirname17(paths.claudeSettings), { recursive: true });
|
|
40927
41514
|
writeFileSync18(paths.claudeSettings, JSON.stringify(settings, null, 2) + "\n");
|
|
40928
41515
|
console.log(`${o.remove ? "removed Grenade hooks from" : "installed Grenade hooks in"} ${paths.claudeSettings}`);
|
|
40929
41516
|
});
|
|
40930
41517
|
function removeOldCodexHooks() {
|
|
40931
41518
|
if (!existsSync23(paths.codexHooks)) return;
|
|
40932
|
-
const { settings, changed } = removeCodexHooks(JSON.parse(
|
|
41519
|
+
const { settings, changed } = removeCodexHooks(JSON.parse(readFileSync18(paths.codexHooks, "utf8")));
|
|
40933
41520
|
if (!changed) return;
|
|
40934
41521
|
writeFileSync18(paths.codexHooks, JSON.stringify(settings, null, 2) + "\n");
|
|
40935
41522
|
console.log(`removed Grenade hooks from ${paths.codexHooks}`);
|