@holdgrenade/cli 1.0.49 → 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 +265 -69
- 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,7 +7024,7 @@ function isKnownAgent(kind) {
|
|
|
7024
7024
|
}
|
|
7025
7025
|
|
|
7026
7026
|
// src/cli.ts
|
|
7027
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as
|
|
7027
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync16, readFileSync as readFileSync18, writeFileSync as writeFileSync18 } from "node:fs";
|
|
7028
7028
|
import { dirname as dirname17 } from "node:path";
|
|
7029
7029
|
|
|
7030
7030
|
// node_modules/commander/lib/error.js
|
|
@@ -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,25 @@ 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
|
+
|
|
30903
30922
|
// ../grenade-protocol/dist/canvas.js
|
|
30904
30923
|
var CANVAS_FOLDER = ".grenade/canvas";
|
|
30905
30924
|
var CANVAS_BOARDS_MAX = 500;
|
|
@@ -31161,6 +31180,8 @@ var Session = external_exports.object({
|
|
|
31161
31180
|
model: external_exports.string().min(1).max(MODEL_NAME_MAX).optional(),
|
|
31162
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. */
|
|
31163
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(),
|
|
31164
31185
|
createdAt: external_exports.string().datetime(),
|
|
31165
31186
|
/** Sessions with the same group are shown together. Absent (older daemons) means the session is on its own. */
|
|
31166
31187
|
group: external_exports.string().min(1).optional(),
|
|
@@ -31216,6 +31237,8 @@ var DaemonInfo = external_exports.object({
|
|
|
31216
31237
|
voice: external_exports.literal(1).optional(),
|
|
31217
31238
|
/** 1 when the daemon serves a group's design canvas: `canvas`, `canvas.subscribe`, `canvas.board` (PROTOCOL.md "Canvas"). Absent from older daemons. */
|
|
31218
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(),
|
|
31219
31242
|
/** Retired: 1 when the daemon sends `activity` for Codex sessions too. Still sent for apps that read it; new clients read `agents`. */
|
|
31220
31243
|
codexActivity: external_exports.literal(1).optional(),
|
|
31221
31244
|
/** Every agent the daemon can start and what it does with each (PROTOCOL.md "Agents"). Absent from older daemons. */
|
|
@@ -31360,6 +31383,7 @@ var ClientFrame = external_exports.discriminatedUnion("type", [
|
|
|
31360
31383
|
CanvasSubscribeFrame,
|
|
31361
31384
|
CanvasUnsubscribeFrame,
|
|
31362
31385
|
CanvasBoardRequestFrame,
|
|
31386
|
+
LimitsRequestFrame,
|
|
31363
31387
|
PingFrame
|
|
31364
31388
|
]);
|
|
31365
31389
|
var ErrorCode = external_exports.enum([
|
|
@@ -31458,6 +31482,7 @@ var DaemonFrame = external_exports.discriminatedUnion("type", [
|
|
|
31458
31482
|
VoiceTokenFrame,
|
|
31459
31483
|
CanvasFrame,
|
|
31460
31484
|
CanvasBoardFrame,
|
|
31485
|
+
LimitsFrame,
|
|
31461
31486
|
PongFrame
|
|
31462
31487
|
]);
|
|
31463
31488
|
var PairRequest = external_exports.object({ code: external_exports.string().regex(/^\d{6}$/), client: ClientInfo });
|
|
@@ -33403,6 +33428,53 @@ function shellQuote(s) {
|
|
|
33403
33428
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
33404
33429
|
}
|
|
33405
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
|
+
|
|
33406
33478
|
// src/hooks/installHooks.ts
|
|
33407
33479
|
var HOOK_EVENTS = ["UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "Notification", "Stop", "StopFailure", "SessionEnd"];
|
|
33408
33480
|
var PROMPT_EVENT = "PermissionRequest";
|
|
@@ -33424,15 +33496,16 @@ function mergeHooks(input2, port) {
|
|
|
33424
33496
|
const command = { type: "command", command: hookCommand(port) };
|
|
33425
33497
|
return mergeEntries(input2, [...HOOK_EVENTS.map((e) => [e, command]), [PROMPT_EVENT, promptHook(port)]], isGrenadeHook);
|
|
33426
33498
|
}
|
|
33427
|
-
function claudeHookFlags(port) {
|
|
33428
|
-
|
|
33499
|
+
function claudeHookFlags(port, userStatusLine) {
|
|
33500
|
+
const settings = { ...mergeHooks({}, port).settings, statusLine: claudeStatusLine(port, userStatusLine) };
|
|
33501
|
+
return `--settings ${shellQuote(JSON.stringify(settings))}`;
|
|
33429
33502
|
}
|
|
33430
33503
|
function removeHooks(input2) {
|
|
33431
33504
|
return removeEntries(input2, isGrenadeHook);
|
|
33432
33505
|
}
|
|
33433
33506
|
function mergeEntries(input2, wanted, isOurs) {
|
|
33434
|
-
const settings =
|
|
33435
|
-
const hooks =
|
|
33507
|
+
const settings = isObject4(input2) ? structuredClone(input2) : {};
|
|
33508
|
+
const hooks = isObject4(settings.hooks) ? settings.hooks : {};
|
|
33436
33509
|
settings.hooks = hooks;
|
|
33437
33510
|
let changed = false;
|
|
33438
33511
|
for (const [event, entry] of wanted) {
|
|
@@ -33453,8 +33526,8 @@ function mergeEntries(input2, wanted, isOurs) {
|
|
|
33453
33526
|
return { settings, changed };
|
|
33454
33527
|
}
|
|
33455
33528
|
function removeEntries(input2, isOurs) {
|
|
33456
|
-
const settings =
|
|
33457
|
-
const hooks =
|
|
33529
|
+
const settings = isObject4(input2) ? structuredClone(input2) : {};
|
|
33530
|
+
const hooks = isObject4(settings.hooks) ? settings.hooks : {};
|
|
33458
33531
|
let changed = false;
|
|
33459
33532
|
for (const [event, matchers] of Object.entries(hooks)) {
|
|
33460
33533
|
if (!Array.isArray(matchers)) continue;
|
|
@@ -33464,7 +33537,7 @@ function removeEntries(input2, isOurs) {
|
|
|
33464
33537
|
}
|
|
33465
33538
|
return { settings, changed };
|
|
33466
33539
|
}
|
|
33467
|
-
function
|
|
33540
|
+
function isObject4(v) {
|
|
33468
33541
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33469
33542
|
}
|
|
33470
33543
|
|
|
@@ -34558,12 +34631,12 @@ function terminalFromEnv() {
|
|
|
34558
34631
|
const env = process.env["GRENADE_TERMINAL"];
|
|
34559
34632
|
return env !== void 0 && isTerminalKind(env) ? env : void 0;
|
|
34560
34633
|
}
|
|
34561
|
-
function relayoutSteps(ordered,
|
|
34634
|
+
function relayoutSteps(ordered, open7) {
|
|
34562
34635
|
const [head, ...rest] = ordered;
|
|
34563
34636
|
if (head === void 0) return [];
|
|
34564
|
-
const keepHead =
|
|
34637
|
+
const keepHead = open7.has(head);
|
|
34565
34638
|
const steps = [];
|
|
34566
|
-
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 });
|
|
34567
34640
|
if (!keepHead) steps.push({ kind: "tab", id: head });
|
|
34568
34641
|
rest.forEach((id, i) => steps.push({ kind: "split", id, nextTo: ordered[i] }));
|
|
34569
34642
|
return steps;
|
|
@@ -34717,9 +34790,9 @@ var TerminalMirror = class {
|
|
|
34717
34790
|
/** The nearest live member before `s` in group order that has a pane in `a`, else the last one that has. */
|
|
34718
34791
|
openMemberOf(s, a) {
|
|
34719
34792
|
if (s.group === void 0) return void 0;
|
|
34720
|
-
const
|
|
34721
|
-
const before =
|
|
34722
|
-
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;
|
|
34723
34796
|
}
|
|
34724
34797
|
closeTab(id) {
|
|
34725
34798
|
const a = this.openIn.get(id);
|
|
@@ -35558,6 +35631,18 @@ var SessionRegistry = class extends EventEmitter2 {
|
|
|
35558
35631
|
this.persist();
|
|
35559
35632
|
this.emit("updated", r.session);
|
|
35560
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
|
+
}
|
|
35561
35646
|
/**
|
|
35562
35647
|
* A client switched the session to `model` (PROTOCOL.md "Models") and the agent took it: the session says so at
|
|
35563
35648
|
* once, before the agent's next reply. `effort` undefined is a model that takes none.
|
|
@@ -36809,13 +36894,13 @@ function switchModelDialog(below) {
|
|
|
36809
36894
|
}
|
|
36810
36895
|
function paragraphs(rows) {
|
|
36811
36896
|
const out = [];
|
|
36812
|
-
let
|
|
36897
|
+
let open7 = false;
|
|
36813
36898
|
for (const row of rows.map((r) => r.trim())) {
|
|
36814
|
-
if (!row)
|
|
36815
|
-
else if (
|
|
36899
|
+
if (!row) open7 = false;
|
|
36900
|
+
else if (open7) out[out.length - 1] += ` ${row}`;
|
|
36816
36901
|
else {
|
|
36817
36902
|
out.push(row);
|
|
36818
|
-
|
|
36903
|
+
open7 = true;
|
|
36819
36904
|
}
|
|
36820
36905
|
}
|
|
36821
36906
|
return out.map((p) => /[.!?:]$/.test(p) ? p : `${p}.`).join(" ");
|
|
@@ -36852,25 +36937,25 @@ async function switchClaudeModel(term, model, effort, timing = DEFAULT_TIMING) {
|
|
|
36852
36937
|
throw new ModelSwitchError("Claude Code's prompt is not empty. Send or clear what is typed there, then choose again.");
|
|
36853
36938
|
}
|
|
36854
36939
|
await term.key("enter");
|
|
36855
|
-
let
|
|
36856
|
-
if (!
|
|
36857
|
-
if (!
|
|
36858
|
-
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++) {
|
|
36859
36944
|
if (moves >= MOST_MOVES) await giveUp(`Claude Code's picker has no ${model}.`);
|
|
36860
|
-
const down = rowsDownTo(
|
|
36945
|
+
const down = rowsDownTo(open7, model);
|
|
36861
36946
|
await term.key(down !== null && down > 0 ? "down" : "up");
|
|
36862
36947
|
await timing.wait(LOOK_EVERY_MS);
|
|
36863
|
-
|
|
36948
|
+
open7 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36864
36949
|
}
|
|
36865
36950
|
if (effort !== void 0) {
|
|
36866
|
-
for (let steps = 0;
|
|
36951
|
+
for (let steps = 0; open7.effort !== effort; steps++) {
|
|
36867
36952
|
if (steps >= MOST_EFFORT_STEPS) await giveUp(`${model} has no ${effort} effort in Claude Code.`);
|
|
36868
36953
|
await term.type(RIGHT);
|
|
36869
36954
|
await timing.wait(LOOK_EVERY_MS);
|
|
36870
|
-
|
|
36955
|
+
open7 = await picker() ?? await giveUp("Claude Code closed its model picker.");
|
|
36871
36956
|
}
|
|
36872
36957
|
}
|
|
36873
|
-
const chosenEffort =
|
|
36958
|
+
const chosenEffort = open7.effort ?? void 0;
|
|
36874
36959
|
await term.type("s");
|
|
36875
36960
|
const closed = await look2((lines) => modelPickerIn(lines) ? null : lines);
|
|
36876
36961
|
if (!closed) await giveUp("Claude Code did not take the choice.");
|
|
@@ -39100,7 +39185,7 @@ var Connection = class {
|
|
|
39100
39185
|
}
|
|
39101
39186
|
this.d.prompts?.on("opened", this.onPrompt);
|
|
39102
39187
|
this.d.prompts?.on("closed", this.onPrompt);
|
|
39103
|
-
for (const
|
|
39188
|
+
for (const open7 of this.d.prompts?.list() ?? []) this.send(open7);
|
|
39104
39189
|
this.d.activity?.on("activity", this.onActivity);
|
|
39105
39190
|
}
|
|
39106
39191
|
async dispatch(frame) {
|
|
@@ -39198,6 +39283,8 @@ ${frame.id}`, () => r.sendText(frame.sessionId, frame.text, frame.submit));
|
|
|
39198
39283
|
return this.send({ type: "conversations", conversations: await this.d.conversations.list(this.anyAgentConversations) });
|
|
39199
39284
|
case "folders":
|
|
39200
39285
|
return this.send(await listFolders(frame.path));
|
|
39286
|
+
case "limits":
|
|
39287
|
+
return this.send({ type: "limits", limits: this.d.limits?.list() ?? [] });
|
|
39201
39288
|
case "voice":
|
|
39202
39289
|
if (!this.d.voice) return this.fail("bad_frame", "this daemon keeps no voice keys", frame.type);
|
|
39203
39290
|
if (!this.watchesVoice) this.d.voice.on("changed", this.onVoice);
|
|
@@ -40672,6 +40759,87 @@ function promptText2(prompt) {
|
|
|
40672
40759
|
}
|
|
40673
40760
|
}
|
|
40674
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
|
+
|
|
40675
40843
|
// src/daemon/server.ts
|
|
40676
40844
|
async function startDaemon(opts = {}) {
|
|
40677
40845
|
ensureDir();
|
|
@@ -40679,10 +40847,16 @@ async function startDaemon(opts = {}) {
|
|
|
40679
40847
|
const controlPort2 = opts.controlPort ?? CONTROL_PORT;
|
|
40680
40848
|
const log = opts.log ?? createLogger({ file: paths.log, level: process.env["GRENADE_LOG"] ?? "info" });
|
|
40681
40849
|
const e2eKey = loadOrCreateE2EKey(opts.e2eKeyPath ?? paths.e2eKey);
|
|
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] };
|
|
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] };
|
|
40683
40851
|
const allowPlainLan = opts.allowPlainLan === true;
|
|
40684
|
-
const tmux = opts.tmux ?? createTmux({ agentFlags: {
|
|
40685
|
-
|
|
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);
|
|
40686
40860
|
const codes = new PairingCodes();
|
|
40687
40861
|
const pairing = new PairingWatch();
|
|
40688
40862
|
const registry2 = new SessionRegistry({
|
|
@@ -40699,6 +40873,7 @@ async function startDaemon(opts = {}) {
|
|
|
40699
40873
|
const mirror = new TerminalMirror({ registry: registry2, log, terminal: pinnedTerminal ?? (() => readTerminalSetting()) });
|
|
40700
40874
|
const terminalStatus = () => ({ ...mirror.status(), pinned: pinnedTerminal !== void 0 });
|
|
40701
40875
|
mirror.start().catch((e) => log.warn("Could not mirror sessions into a terminal", { error: e }));
|
|
40876
|
+
const planLimits = new PlanLimits();
|
|
40702
40877
|
const http = createServer2(async (req, res) => {
|
|
40703
40878
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
40704
40879
|
try {
|
|
@@ -40712,6 +40887,11 @@ async function startDaemon(opts = {}) {
|
|
|
40712
40887
|
closePromptsByHook(prompts, session, body);
|
|
40713
40888
|
return sendJson(res, r.status, r.body);
|
|
40714
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
|
+
}
|
|
40715
40895
|
if (req.method === "POST" && url2.pathname === "/hooks/codex") {
|
|
40716
40896
|
if (!isLoopback(req.socket.remoteAddress)) return sendJson(res, 403, { error: "forbidden" });
|
|
40717
40897
|
const r = applyCodexHook(url2.searchParams.get("session"), await readBody(req));
|
|
@@ -40853,6 +41033,10 @@ async function startDaemon(opts = {}) {
|
|
|
40853
41033
|
},
|
|
40854
41034
|
onTranscript: (id, path2, event) => {
|
|
40855
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 }));
|
|
40856
41040
|
catchUp.cancel(id);
|
|
40857
41041
|
codexStopCatchUp.cancel(id);
|
|
40858
41042
|
readActivity(id, path2).then((entries) => {
|
|
@@ -40861,6 +41045,17 @@ async function startDaemon(opts = {}) {
|
|
|
40861
41045
|
}).catch((e) => log.debug("Could not read the activity from a Codex rollout", { session: id, path: path2, error: e }));
|
|
40862
41046
|
}
|
|
40863
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
|
+
}
|
|
40864
41059
|
function handlePair(raw, res) {
|
|
40865
41060
|
if (!allowPlainLan) return sendJson(res, 426, { error: "encryption_required" });
|
|
40866
41061
|
let json2;
|
|
@@ -40874,7 +41069,7 @@ async function startDaemon(opts = {}) {
|
|
|
40874
41069
|
const verdict = codes.verify(body.data.code);
|
|
40875
41070
|
if (verdict === "too_many_attempts") return sendJson(res, 429, { error: verdict });
|
|
40876
41071
|
if (verdict === "invalid_code") return sendJson(res, 400, { error: verdict });
|
|
40877
|
-
const token =
|
|
41072
|
+
const token = tokens2.issue(body.data.client);
|
|
40878
41073
|
pairing.paired(body.data.client, "lan");
|
|
40879
41074
|
log.info(`Paired a new phone: ${body.data.client.name}`, { platform: body.data.client.platform });
|
|
40880
41075
|
sendJson(res, 200, { token, daemon: info });
|
|
@@ -40883,7 +41078,7 @@ async function startDaemon(opts = {}) {
|
|
|
40883
41078
|
const verdict = codes.verify(credential);
|
|
40884
41079
|
if (verdict === "too_many_attempts") pairing.voided();
|
|
40885
41080
|
if (verdict !== "ok") return { ok: false, code: verdict };
|
|
40886
|
-
const token =
|
|
41081
|
+
const token = tokens2.issue(client, { sealed: true });
|
|
40887
41082
|
pairing.paired(client, route2);
|
|
40888
41083
|
log.info(`Paired a new phone: ${client.name}`, { platform: client.platform, route: route2 });
|
|
40889
41084
|
return { ok: true, token };
|
|
@@ -40904,7 +41099,7 @@ async function startDaemon(opts = {}) {
|
|
|
40904
41099
|
const attachments = createAttachmentStore(opts.attachmentsDir ?? paths.attachments);
|
|
40905
41100
|
const push2 = startPush({
|
|
40906
41101
|
registry: registry2,
|
|
40907
|
-
tokens,
|
|
41102
|
+
tokens: tokens2,
|
|
40908
41103
|
daemon: info,
|
|
40909
41104
|
staticKey: e2eKey,
|
|
40910
41105
|
relay: () => opts.relay === false ? null : loadRelayConfig(opts.relayPath ?? paths.relay),
|
|
@@ -40941,13 +41136,13 @@ async function startDaemon(opts = {}) {
|
|
|
40941
41136
|
return closed;
|
|
40942
41137
|
}
|
|
40943
41138
|
const devices = {
|
|
40944
|
-
list: () =>
|
|
41139
|
+
list: () => tokens2.list().map((r) => deviceOf(r, live2.routesOf(r.token))),
|
|
40945
41140
|
unpair(id) {
|
|
40946
|
-
const gone =
|
|
41141
|
+
const gone = tokens2.revoke(id);
|
|
40947
41142
|
return gone ? { closed: closeConnectionsOf([gone], `unpaired on the ${computerWord()}`) } : null;
|
|
40948
41143
|
},
|
|
40949
41144
|
unpairAll() {
|
|
40950
|
-
const gone =
|
|
41145
|
+
const gone = tokens2.revokeAll();
|
|
40951
41146
|
return { removed: gone.length, closed: closeConnectionsOf(gone, `unpaired on the ${computerWord()}`) };
|
|
40952
41147
|
}
|
|
40953
41148
|
};
|
|
@@ -40958,19 +41153,19 @@ async function startDaemon(opts = {}) {
|
|
|
40958
41153
|
sentInputs,
|
|
40959
41154
|
activity,
|
|
40960
41155
|
interrupted,
|
|
40961
|
-
isValidToken: (t) =>
|
|
41156
|
+
isValidToken: (t) => tokens2.has(t),
|
|
40962
41157
|
sealed: via.sealed,
|
|
40963
41158
|
route: via.route,
|
|
40964
41159
|
// Plain is for phones that were never encrypted, and only while the daemon allows it.
|
|
40965
|
-
acceptsPlain: (t) => allowPlainLan &&
|
|
41160
|
+
acceptsPlain: (t) => allowPlainLan && tokens2.get(t)?.sealed !== true,
|
|
40966
41161
|
onHello(connection, token, client) {
|
|
40967
41162
|
live2.add(token, connection);
|
|
40968
|
-
|
|
40969
|
-
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 });
|
|
40970
41165
|
},
|
|
40971
41166
|
onEnd(connection, token) {
|
|
40972
41167
|
live2.remove(token, connection);
|
|
40973
|
-
|
|
41168
|
+
tokens2.touch(token);
|
|
40974
41169
|
},
|
|
40975
41170
|
pair: pairPhone,
|
|
40976
41171
|
push: push2.pusher,
|
|
@@ -40980,20 +41175,21 @@ async function startDaemon(opts = {}) {
|
|
|
40980
41175
|
models,
|
|
40981
41176
|
voice,
|
|
40982
41177
|
canvas,
|
|
41178
|
+
limits: planLimits,
|
|
40983
41179
|
conversations,
|
|
40984
|
-
openTerm(
|
|
41180
|
+
openTerm(open7) {
|
|
40985
41181
|
const stream = new TermStream({
|
|
40986
|
-
...
|
|
40987
|
-
resize: (cols, rows, by) => registry2.resize(
|
|
40988
|
-
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),
|
|
40989
41185
|
log
|
|
40990
41186
|
});
|
|
40991
41187
|
stream.start();
|
|
40992
41188
|
return stream;
|
|
40993
41189
|
},
|
|
40994
41190
|
unpair(token) {
|
|
40995
|
-
const id =
|
|
40996
|
-
const gone = id ?
|
|
41191
|
+
const id = tokens2.get(token)?.id;
|
|
41192
|
+
const gone = id ? tokens2.revoke(id) : void 0;
|
|
40997
41193
|
if (gone) closeConnectionsOf([gone], "unpaired on the phone");
|
|
40998
41194
|
},
|
|
40999
41195
|
daemon: info,
|
|
@@ -41021,7 +41217,7 @@ async function startDaemon(opts = {}) {
|
|
|
41021
41217
|
});
|
|
41022
41218
|
const deviceIdleMs = opts.deviceIdleMs ?? DEVICE_IDLE_MS;
|
|
41023
41219
|
const unpairIdle = () => {
|
|
41024
|
-
if (deviceIdleMs > 0) closeConnectionsOf(
|
|
41220
|
+
if (deviceIdleMs > 0) closeConnectionsOf(tokens2.revokeIdle(deviceIdleMs), "not seen for a long time");
|
|
41025
41221
|
};
|
|
41026
41222
|
unpairIdle();
|
|
41027
41223
|
const idleTimer = setInterval(unpairIdle, 60 * 60 * 1e3);
|
|
@@ -41043,7 +41239,7 @@ async function startDaemon(opts = {}) {
|
|
|
41043
41239
|
config: config2,
|
|
41044
41240
|
name: info.name,
|
|
41045
41241
|
version: VERSION,
|
|
41046
|
-
accessHashes: () => [...
|
|
41242
|
+
accessHashes: () => [...tokens2.list().map((t) => accessHash(t.token)), ...offerAccess()],
|
|
41047
41243
|
localIps: () => localIpv4(networkInterfaces()),
|
|
41048
41244
|
openPipe: (conn, send, onEnd) => new PhonePipe({ conn, staticKey: e2eKey, send, makeConnection: (out, close) => makeConnection(out, close, { route: "relay", sealed: true }), log, onEnd }),
|
|
41049
41245
|
log
|
|
@@ -41063,7 +41259,7 @@ async function startDaemon(opts = {}) {
|
|
|
41063
41259
|
auto: () => opts.updates?.auto ?? readAuto()
|
|
41064
41260
|
});
|
|
41065
41261
|
const relayStatus = () => relayLink?.status() ?? { state: "off", phones: 0, ...opts.relay === false ? { disabled: true } : {} };
|
|
41066
|
-
|
|
41262
|
+
tokens2.onChange(() => relayLink?.tokensChanged());
|
|
41067
41263
|
startRelay();
|
|
41068
41264
|
const control2 = createControlServer({
|
|
41069
41265
|
registry: registry2,
|
|
@@ -41309,7 +41505,7 @@ program2.command("kill <name>").description("end a session").action(async (name)
|
|
|
41309
41505
|
console.log(`killed ${id}`);
|
|
41310
41506
|
});
|
|
41311
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) => {
|
|
41312
|
-
const current = existsSync23(paths.claudeSettings) ? JSON.parse(
|
|
41508
|
+
const current = existsSync23(paths.claudeSettings) ? JSON.parse(readFileSync18(paths.claudeSettings, "utf8")) : {};
|
|
41313
41509
|
const { settings, changed } = o.remove ? removeHooks(current) : mergeHooks(current, o.port);
|
|
41314
41510
|
if (o.print) return console.log(JSON.stringify(settings, null, 2));
|
|
41315
41511
|
if (o.remove) removeOldCodexHooks();
|
|
@@ -41320,7 +41516,7 @@ program2.command("install-hooks").description("also let a claude typed by hand i
|
|
|
41320
41516
|
});
|
|
41321
41517
|
function removeOldCodexHooks() {
|
|
41322
41518
|
if (!existsSync23(paths.codexHooks)) return;
|
|
41323
|
-
const { settings, changed } = removeCodexHooks(JSON.parse(
|
|
41519
|
+
const { settings, changed } = removeCodexHooks(JSON.parse(readFileSync18(paths.codexHooks, "utf8")));
|
|
41324
41520
|
if (!changed) return;
|
|
41325
41521
|
writeFileSync18(paths.codexHooks, JSON.stringify(settings, null, 2) + "\n");
|
|
41326
41522
|
console.log(`removed Grenade hooks from ${paths.codexHooks}`);
|