baychat 0.39.0 → 0.40.0
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/.claude-plugin/plugin.json +1 -1
- package/dist/approval-wait.js +15 -3
- package/dist/approve-hook.js +96 -11
- package/dist/card-after.js +47 -0
- package/dist/card-key.js +27 -0
- package/dist/denied/after-hook.js +7 -2
- package/dist/notify-state.js +62 -7
- package/dist/protocol-content.js +1 -1
- package/dist/relay/daemon.js +8 -0
- package/dist/relay/launcher/approval-settings.js +23 -10
- package/dist/relay/pane-interrupt.js +113 -0
- package/dist/relay/session-commands.js +1 -1
- package/dist/relay/terminal-stop.js +41 -0
- package/dist/relay/updates.js +3 -1
- package/dist/tool-defs.js +8 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"displayName": "BayChat",
|
|
5
5
|
"description": "Wakes this Claude Code session the moment a BayChat message arrives: no Monitor, no 30-minute deadline. Uses the baychat CLI already on this computer (npm install -g baychat, then baychat login).",
|
|
6
6
|
"author": {
|
package/dist/approval-wait.js
CHANGED
|
@@ -53,7 +53,11 @@ async function askOwnerAndWait(device, ask, deadlineMs, deps, label) {
|
|
|
53
53
|
options: ask.options,
|
|
54
54
|
...(ask.guardedOptions?.length ? { guardedOptions: ask.guardedOptions } : {}),
|
|
55
55
|
direct: true,
|
|
56
|
-
wait: holdSeconds(deadlineMs - deps.now()),
|
|
56
|
+
wait: deps.onCard ? 1 : holdSeconds(deadlineMs - deps.now()),
|
|
57
|
+
// When THIS process stops waiting (AGENT_STOP_UNDO.md T14): a Bay's undo window holds an
|
|
58
|
+
// answer only if it can still be delivered before then — otherwise the server sends it at
|
|
59
|
+
// once, so a short `--timeout` never turns a phone's "yes" into a deny. Older servers ignore it.
|
|
60
|
+
waitUntil: new Date(deadlineMs).toISOString(),
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
catch (err) {
|
|
@@ -62,6 +66,12 @@ async function askOwnerAndWait(device, ask, deadlineMs, deps, label) {
|
|
|
62
66
|
const requestId = typeof reply.requestId === "string" ? reply.requestId : "";
|
|
63
67
|
if (!requestId)
|
|
64
68
|
return { kind: "failed", reason: "BayChat did not return an approval id" };
|
|
69
|
+
try {
|
|
70
|
+
deps.onCard?.(requestId);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
deps.log(`${label}: could not note the card (${reasonOf(err)})`);
|
|
74
|
+
}
|
|
65
75
|
// Wait. Re-arm across the bounded holds exactly as `await_approval` does. Marked while it
|
|
66
76
|
// waits, so the relay's automatic update never restarts BayChat under an open question.
|
|
67
77
|
const unmark = (0, waiting_cards_1.markWaitingCard)(requestId);
|
|
@@ -133,9 +143,11 @@ async function waitOnOwnerQuestion(device, requestId, deadlineMs, deps, label) {
|
|
|
133
143
|
* effort and NEVER THROWS: a card left pending is harmless — the answer would reach nobody — so
|
|
134
144
|
* a failure here is only logged.
|
|
135
145
|
*/
|
|
136
|
-
async function withdrawOwnerQuestion(device, requestId, log
|
|
146
|
+
async function withdrawOwnerQuestion(device, requestId, log,
|
|
147
|
+
/** "answered_elsewhere": it was answered in the session's own terminal — the card says so. */
|
|
148
|
+
reason) {
|
|
137
149
|
try {
|
|
138
|
-
await (0, api_1.apiRequest)(device, "POST", `/api/device-api/approvals/${encodeURIComponent(requestId)}/withdraw`, {});
|
|
150
|
+
await (0, api_1.apiRequest)(device, "POST", `/api/device-api/approvals/${encodeURIComponent(requestId)}/withdraw`, reason ? { reason } : {});
|
|
139
151
|
}
|
|
140
152
|
catch (err) {
|
|
141
153
|
log(`could not withdraw the card ${requestId} (${reasonOf(err)})`);
|
package/dist/approve-hook.js
CHANGED
|
@@ -38,6 +38,8 @@ exports.allowDecision = allowDecision;
|
|
|
38
38
|
exports.denyDecision = denyDecision;
|
|
39
39
|
exports.describeToolRequest = describeToolRequest;
|
|
40
40
|
exports.composeQuestion = composeQuestion;
|
|
41
|
+
exports.answeredInTerminal = answeredInTerminal;
|
|
42
|
+
exports.setHeldCardForTest = setHeldCardForTest;
|
|
41
43
|
exports.decideApproval = decideApproval;
|
|
42
44
|
exports.sessionFromClaude = sessionFromClaude;
|
|
43
45
|
exports.linkFromClaude = linkFromClaude;
|
|
@@ -62,8 +64,9 @@ exports.cmdApproveHook = cmdApproveHook;
|
|
|
62
64
|
// THE ONE RULE: EVERY PATH OUT OF HERE PRINTS A DECISION.
|
|
63
65
|
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
64
66
|
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
+
// A broken hook decides NOTHING. All of these render no decision, and Claude Code then shows its
|
|
68
|
+
// own terminal permission prompt — the "normal permission flow" (a timeout and an exit 1 with
|
|
69
|
+
// non-JSON output were tested 30 Oct with Claude Code 2.1.314: the prompt came up, nothing ran):
|
|
67
70
|
//
|
|
68
71
|
// • the hook times out • it exits 1 (or any code but 2)
|
|
69
72
|
// • stdout is empty, or is not JSON • stdout is JSON that fails schema validation
|
|
@@ -76,12 +79,14 @@ exports.cmdApproveHook = cmdApproveHook;
|
|
|
76
79
|
// So the fail-closed guarantee is entirely this file's. Every abnormal path — no credential, DNS
|
|
77
80
|
// failure, connection refused, 500, 401, unparseable body, ambiguous session, our own deadline,
|
|
78
81
|
// an exception nobody predicted — is converted into an explicit DENY object and exit 0. There is
|
|
79
|
-
// no `process.exit(1)` anywhere below
|
|
82
|
+
// no `process.exit(1)` anywhere below: exit 1 decides nothing and leaves the terminal prompt, which
|
|
83
|
+
// nobody at a phone can answer.
|
|
80
84
|
//
|
|
81
85
|
// Two consequences worth stating plainly:
|
|
82
86
|
//
|
|
83
|
-
// 1. OUR WAIT MUST END BEFORE THE HANDLER'S `timeout`. A hook that times out
|
|
84
|
-
//
|
|
87
|
+
// 1. OUR WAIT MUST END BEFORE THE HANDLER'S `timeout`. A hook that times out renders no
|
|
88
|
+
// decision (the terminal prompt comes back; tested 30 Oct with Claude Code 2.1.314), so a deadline we control,
|
|
89
|
+
// which denies explicitly and says why, must always be reached first. Hence
|
|
85
90
|
// DEFAULT_WAIT_SECONDS < RECOMMENDED_HANDLER_TIMEOUT_SECONDS, and the docs say to raise
|
|
86
91
|
// both together or neither.
|
|
87
92
|
// 2. STDOUT IS THE DECISION CHANNEL. Nothing else may ever be written to it — "Stdout must
|
|
@@ -98,11 +103,13 @@ exports.cmdApproveHook = cmdApproveHook;
|
|
|
98
103
|
//
|
|
99
104
|
// The card itself never expires: the ApprovalRequest row stays PENDING for as long as it takes,
|
|
100
105
|
// exactly like a terminal prompt. Our deadline bounds only how long THIS PROCESS waits, and it
|
|
101
|
-
// exists
|
|
106
|
+
// exists because the alternative (being killed by the handler timeout) decides nothing: the
|
|
107
|
+
// terminal prompt reappears with no word to the phone about why.
|
|
102
108
|
const child_process_1 = require("child_process");
|
|
103
109
|
const fs = __importStar(require("fs"));
|
|
104
110
|
const os = __importStar(require("os"));
|
|
105
111
|
const approval_wait_1 = require("./approval-wait");
|
|
112
|
+
const card_key_1 = require("./card-key");
|
|
106
113
|
const config_1 = require("./config");
|
|
107
114
|
const hook_stdin_1 = require("./hook-stdin");
|
|
108
115
|
const claude_link_1 = require("./channel/claude-link");
|
|
@@ -151,8 +158,8 @@ function denyClosed(reason) {
|
|
|
151
158
|
/**
|
|
152
159
|
* How long this process waits before denying, in seconds.
|
|
153
160
|
*
|
|
154
|
-
* An hour, not forever: the handler's own `timeout` would otherwise kill us first, and a
|
|
155
|
-
*
|
|
161
|
+
* An hour, not forever: the handler's own `timeout` would otherwise kill us first, and a killed
|
|
162
|
+
* hook decides nothing (the terminal prompt comes back; tested 30 Oct with Claude Code 2.1.314). This number and the `timeout` in settings.json are one setting in two
|
|
156
163
|
* places — see the doc.
|
|
157
164
|
*/
|
|
158
165
|
exports.DEFAULT_WAIT_SECONDS = 3600;
|
|
@@ -271,6 +278,45 @@ exports.APPROVE_OPTIONS = ["Allow this once", "Deny"];
|
|
|
271
278
|
/** The ONLY index that means yes. Anything else — a second option, a third, an index the server
|
|
272
279
|
* invented — is a deny, so a mismatch between these labels and the answer can only fail safe. */
|
|
273
280
|
const ALLOW_INDEX = 0;
|
|
281
|
+
/**
|
|
282
|
+
* The card this process is waiting on right now, for `answeredInTerminal`: the credential, and the
|
|
283
|
+
* card's id once BayChat has made it. One hook run per process, so one card.
|
|
284
|
+
*/
|
|
285
|
+
let heldCard = null;
|
|
286
|
+
/** How long a terminal answer waits for a card id still on its way, and for the withdrawal itself. */
|
|
287
|
+
const SIGNAL_GRACE_MS = 2_000;
|
|
288
|
+
/**
|
|
289
|
+
* Claude Code ended this hook mid-wait — measured on 2.1.316: a "No" at the terminal sends SIGTERM
|
|
290
|
+
* (a "Yes" leaves the hook parked; the after-hook closes that card). Close the card "answered at the
|
|
291
|
+
* computer" so it leaves Needs you, then the deny: EVERY PATH PRINTS A DECISION, even one Claude Code
|
|
292
|
+
* no longer reads. Bounded: a slow BayChat only leaves the card for "Mark done". NEVER THROWS.
|
|
293
|
+
*/
|
|
294
|
+
async function answeredInTerminal(deps = {}) {
|
|
295
|
+
try {
|
|
296
|
+
const held = heldCard;
|
|
297
|
+
if (held) {
|
|
298
|
+
const grace = deps.graceMs ?? SIGNAL_GRACE_MS;
|
|
299
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
300
|
+
// A card made in the hook's first second may not have its id yet.
|
|
301
|
+
for (let waited = 0; !held.cardId && waited < grace; waited += 100)
|
|
302
|
+
await sleep(100);
|
|
303
|
+
if (held.cardId) {
|
|
304
|
+
await Promise.race([
|
|
305
|
+
(deps.withdraw ?? approval_wait_1.withdrawOwnerQuestion)(held.device, held.cardId, held.log, "answered_elsewhere"),
|
|
306
|
+
sleep(grace),
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// The deny below is the guarantee; the withdrawal is a courtesy to the phone.
|
|
313
|
+
}
|
|
314
|
+
return deny("it was answered in the terminal.");
|
|
315
|
+
}
|
|
316
|
+
/** Test seam: what `answeredInTerminal` would close. */
|
|
317
|
+
function setHeldCardForTest(card) {
|
|
318
|
+
heldCard = card;
|
|
319
|
+
}
|
|
274
320
|
function parseArgs(argv) {
|
|
275
321
|
const at = (name) => {
|
|
276
322
|
const i = argv.indexOf(name);
|
|
@@ -328,13 +374,32 @@ async function decideApproval(rawStdin, argv = [], deps = {}) {
|
|
|
328
374
|
// not "answered with index 0" becomes a deny below.
|
|
329
375
|
// While the card is on the phone, notify-hook stays quiet for this session: one prompt, one
|
|
330
376
|
// thing on the phone (notify-state.ts).
|
|
377
|
+
// The mark also names the call, and the card once it exists: answered at the keyboard instead,
|
|
378
|
+
// the after-hook closes it (card-after.ts), and a SIGTERM closes it here (`answeredInTerminal`).
|
|
331
379
|
const claudeSession = str(input.session_id);
|
|
332
|
-
(0,
|
|
380
|
+
const key = (0, card_key_1.cardKeyOf)(toolName, input.tool_input);
|
|
381
|
+
(0, notify_state_1.markCardPending)(claudeSession, {}, key ? { key } : undefined);
|
|
382
|
+
heldCard = { device, cardId: undefined, log };
|
|
333
383
|
let outcome;
|
|
334
384
|
try {
|
|
335
|
-
outcome = await (0, approval_wait_1.askOwnerAndWait)(device, { session: opts.session, question, options: [...exports.APPROVE_OPTIONS] }, deadline, {
|
|
385
|
+
outcome = await (0, approval_wait_1.askOwnerAndWait)(device, { session: opts.session, question, options: [...exports.APPROVE_OPTIONS] }, deadline, {
|
|
386
|
+
now,
|
|
387
|
+
sleep,
|
|
388
|
+
log,
|
|
389
|
+
onCard: (cardId) => {
|
|
390
|
+
if (heldCard)
|
|
391
|
+
heldCard.cardId = cardId;
|
|
392
|
+
(0, notify_state_1.markCardPending)(claudeSession, {}, { cardId, key });
|
|
393
|
+
// Already answered at the keyboard, before the card had an id: close it now. The wait
|
|
394
|
+
// then sees "withdrawn" and this prints its deny.
|
|
395
|
+
if (key && (0, notify_state_1.takeSettled)(claudeSession) === key) {
|
|
396
|
+
void (deps.withdraw ?? approval_wait_1.withdrawOwnerQuestion)(device, cardId, log, "answered_elsewhere");
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
}, "baychat approve-hook");
|
|
336
400
|
}
|
|
337
401
|
finally {
|
|
402
|
+
heldCard = null;
|
|
338
403
|
(0, notify_state_1.clearCardPending)(claudeSession);
|
|
339
404
|
}
|
|
340
405
|
return verdictOf(outcome, opts.waitSeconds, isChained(toolName, input.tool_input));
|
|
@@ -366,7 +431,7 @@ function verdictOf(outcome, waitSeconds, chained = false) {
|
|
|
366
431
|
return deny("the decision was withdrawn in BayChat without an answer.");
|
|
367
432
|
case "deadline":
|
|
368
433
|
return deny(`nobody answered within ${waitSeconds}s.`, "The decision card is still in BayChat; this call was refused because waiting longer " +
|
|
369
|
-
"would have let the hook time out,
|
|
434
|
+
"would have let the hook time out, which hands the decision back to the terminal prompt.");
|
|
370
435
|
case "failed":
|
|
371
436
|
return denyClosed(outcome.reason);
|
|
372
437
|
case "unrecognised":
|
|
@@ -508,6 +573,20 @@ function noDecisionNotice(deps = {}) {
|
|
|
508
573
|
* call PROCEEDS — so exiting 1 on failure would be precisely backwards.
|
|
509
574
|
*/
|
|
510
575
|
async function cmdApproveHook(argv = [], stdin = process.stdin, deps = {}) {
|
|
576
|
+
// Ended mid-wait (a "No" at the terminal sends SIGTERM): close the card, then print the deny — once.
|
|
577
|
+
let printed = false;
|
|
578
|
+
const onSignal = () => {
|
|
579
|
+
if (printed)
|
|
580
|
+
return;
|
|
581
|
+
printed = true;
|
|
582
|
+
void answeredInTerminal().then((denied) => {
|
|
583
|
+
process.stdout.write(`${JSON.stringify(denied)}\n`);
|
|
584
|
+
process.exit(0);
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
const signals = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
588
|
+
for (const sig of signals)
|
|
589
|
+
process.on(sig, onSignal);
|
|
511
590
|
let decision;
|
|
512
591
|
try {
|
|
513
592
|
decision = await decideApprovalOrDefer(await (0, hook_stdin_1.readStdin)(stdin), argv);
|
|
@@ -518,6 +597,12 @@ async function cmdApproveHook(argv = [], stdin = process.stdin, deps = {}) {
|
|
|
518
597
|
process.stderr.write(`baychat approve-hook: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
519
598
|
decision = denyClosed("the approval gate could not run");
|
|
520
599
|
}
|
|
600
|
+
for (const sig of signals)
|
|
601
|
+
process.off(sig, onSignal);
|
|
602
|
+
// A signal already printed its deny; one decision on stdout, never two.
|
|
603
|
+
if (printed)
|
|
604
|
+
return 0;
|
|
605
|
+
printed = true;
|
|
521
606
|
// No decision: `--session-from-claude` with no session found — Claude Code's own dialog, plus a
|
|
522
607
|
// line saying why when this is a channel session that lost its link.
|
|
523
608
|
if (decision === null) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.settleAnsweredCard = settleAnsweredCard;
|
|
4
|
+
// The after-hook's half of "Needs you closes itself" (fix/needs-you-closes-itself, 2 Nov 2026).
|
|
5
|
+
//
|
|
6
|
+
// Measured on Claude Code 2.1.316: while approve-hook waits on its card, the terminal dialog is open
|
|
7
|
+
// too. Answered "Yes" at the keyboard, the tool runs and PostToolUse fires — and approve-hook is NOT
|
|
8
|
+
// stopped, so its card would stay in Needs you for up to an hour. Here the finished call is compared
|
|
9
|
+
// with the session's pending card (notify-state.ts, written by approve-hook): the same call closes the
|
|
10
|
+
// card "answered at the computer"; approve-hook then sees "withdrawn", prints its deny and leaves.
|
|
11
|
+
//
|
|
12
|
+
// It runs on every matching tool call (inside the async `denied-after` hook), so it reads ONE local
|
|
13
|
+
// file first and touches the network only when that file names this very call. Loads no CLI.
|
|
14
|
+
const approval_wait_1 = require("./approval-wait");
|
|
15
|
+
const card_key_1 = require("./card-key");
|
|
16
|
+
const config_1 = require("./config");
|
|
17
|
+
const notify_state_1 = require("./notify-state");
|
|
18
|
+
/** Close the session's card if THIS finished call is the one it asked about. NEVER THROWS. */
|
|
19
|
+
async function settleAnsweredCard(input, deps = {}) {
|
|
20
|
+
const log = deps.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
21
|
+
try {
|
|
22
|
+
const call = (input && typeof input === "object" ? input : {});
|
|
23
|
+
const session = typeof call.session_id === "string" ? call.session_id : "";
|
|
24
|
+
if (!session)
|
|
25
|
+
return "none";
|
|
26
|
+
const pending = (0, notify_state_1.readPendingCard)(session, { dir: deps.dir });
|
|
27
|
+
if (!pending)
|
|
28
|
+
return "none";
|
|
29
|
+
const key = (0, card_key_1.cardKeyOf)(typeof call.tool_name === "string" ? call.tool_name : "", call.tool_input);
|
|
30
|
+
if (!key || pending.key !== key)
|
|
31
|
+
return "other-call";
|
|
32
|
+
if (!pending.cardId) {
|
|
33
|
+
// Answered at the keyboard before BayChat had even made the card: approve-hook closes it.
|
|
34
|
+
(0, notify_state_1.markSettled)(session, key, { dir: deps.dir });
|
|
35
|
+
return "settled-early";
|
|
36
|
+
}
|
|
37
|
+
const device = (deps.loadDevice ?? config_1.loadDeviceCredentials)();
|
|
38
|
+
if (!device)
|
|
39
|
+
return "none";
|
|
40
|
+
await (deps.withdraw ?? ((d, id) => (0, approval_wait_1.withdrawOwnerQuestion)(d, id, log, "answered_elsewhere")))(device, pending.cardId);
|
|
41
|
+
return "withdrawn";
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
log(`baychat after-hook: could not close an answered card (${err instanceof Error ? err.message : String(err)})`);
|
|
45
|
+
return "none";
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/card-key.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Which call a permission card asks about, as a key both of Claude Code's events can produce
|
|
3
|
+
// (fix/needs-you-closes-itself). The PermissionRequest hook gets no `tool_use_id` — only the tool and
|
|
4
|
+
// its input — and PostToolUse gets the same two, so the key is built from those alone. Pure.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cardKeyOf = cardKeyOf;
|
|
7
|
+
/** `Bash:<command>`; any other tool `<name>:<its input, keys sorted>`; undefined when it cannot be named. */
|
|
8
|
+
function cardKeyOf(toolName, toolInput) {
|
|
9
|
+
if (!toolName)
|
|
10
|
+
return undefined;
|
|
11
|
+
const input = toolInput && typeof toolInput === "object" && !Array.isArray(toolInput) ? toolInput : {};
|
|
12
|
+
if (toolName === "Bash") {
|
|
13
|
+
// Only the command: Claude Code may add a description or a timeout on one side and not the other.
|
|
14
|
+
const command = typeof input.command === "string" ? input.command.trim() : "";
|
|
15
|
+
return command ? `Bash:${command}` : undefined;
|
|
16
|
+
}
|
|
17
|
+
return `${toolName}:${stable(input)}`;
|
|
18
|
+
}
|
|
19
|
+
function stable(value) {
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return `[${value.map(stable).join(",")}]`;
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
24
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(",")}}`;
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(value) ?? "null";
|
|
27
|
+
}
|
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.afterCall = afterCall;
|
|
4
4
|
exports.cmdDeniedAfter = cmdDeniedAfter;
|
|
5
5
|
// `baychat denied-after` — the PostToolUse / PostToolUseFailure hook that takes an "Allow once"
|
|
6
|
-
// rule back out as soon as the retried call has run
|
|
6
|
+
// rule back out as soon as the retried call has run, and closes a permission card whose call was
|
|
7
|
+
// answered at the keyboard instead (card-after.ts, fix/needs-you-closes-itself).
|
|
7
8
|
//
|
|
8
9
|
// It is installed beside `denied-hook` (relay/launcher/approval-settings.ts) as an ASYNC hook:
|
|
9
10
|
// Claude Code starts it and moves on, so it adds nothing to any tool call's latency, and it prints
|
|
@@ -14,6 +15,7 @@ exports.cmdDeniedAfter = cmdDeniedAfter;
|
|
|
14
15
|
// and every run here sweeps too — including a grant whose deferred confirmation never finished,
|
|
15
16
|
// which is taken back and the owner told (confirm.ts; it loads only the card client and config,
|
|
16
17
|
// no MCP SDK and none of the CLI's commands).
|
|
18
|
+
const card_after_1 = require("../card-after");
|
|
17
19
|
const hook_stdin_1 = require("../hook-stdin");
|
|
18
20
|
const confirm_1 = require("./confirm");
|
|
19
21
|
const grants_1 = require("./grants");
|
|
@@ -21,9 +23,12 @@ const permission_rule_1 = require("./permission-rule");
|
|
|
21
23
|
/** Handle one finished call. NEVER THROWS; diagnostics go to stderr. */
|
|
22
24
|
async function afterCall(rawStdin, log = defaultLog) {
|
|
23
25
|
try {
|
|
26
|
+
const input = JSON.parse(rawStdin);
|
|
27
|
+
// A permission card answered at the keyboard instead: close it (card-after.ts — one local file
|
|
28
|
+
// read unless this very call has a card on the phone).
|
|
29
|
+
await (0, card_after_1.settleAnsweredCard)(input, { log });
|
|
24
30
|
if (!(0, grants_1.hasGrantsToSweep)())
|
|
25
31
|
return;
|
|
26
|
-
const input = JSON.parse(rawStdin);
|
|
27
32
|
const call = typeof input.tool_name === "string" ? (0, permission_rule_1.callKeyOf)(input.tool_name, input.tool_input) : undefined;
|
|
28
33
|
if (call && typeof input.session_id === "string")
|
|
29
34
|
await (0, grants_1.consumeOnceGrants)(input.session_id, call);
|
package/dist/notify-state.js
CHANGED
|
@@ -36,7 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.notifyStateDir = void 0;
|
|
37
37
|
exports.stateKey = stateKey;
|
|
38
38
|
exports.markCardPending = markCardPending;
|
|
39
|
+
exports.readPendingCard = readPendingCard;
|
|
39
40
|
exports.clearCardPending = clearCardPending;
|
|
41
|
+
exports.markSettled = markSettled;
|
|
42
|
+
exports.takeSettled = takeSettled;
|
|
40
43
|
exports.cardPendingSince = cardPendingSince;
|
|
41
44
|
exports.readLastNotice = readLastNotice;
|
|
42
45
|
exports.writeLastNotice = writeLastNotice;
|
|
@@ -74,18 +77,48 @@ function file(sessionId, ext, dir) {
|
|
|
74
77
|
return undefined;
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
function markCardPending(sessionId, opts = {}) {
|
|
80
|
+
function markCardPending(sessionId, opts = {}, card) {
|
|
78
81
|
const f = file(sessionId, "card", opts.dir);
|
|
79
82
|
if (!f)
|
|
80
83
|
return;
|
|
81
84
|
try {
|
|
82
85
|
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
83
|
-
|
|
86
|
+
const at = (opts.now ?? Date.now)();
|
|
87
|
+
const body = card ? JSON.stringify({ at, ...(card.cardId ? { cardId: card.cardId } : {}), ...(card.key ? { key: card.key } : {}) }) : String(at);
|
|
88
|
+
fs.writeFileSync(f, body, { mode: 0o600 });
|
|
84
89
|
}
|
|
85
90
|
catch {
|
|
86
91
|
// Bookkeeping only.
|
|
87
92
|
}
|
|
88
93
|
}
|
|
94
|
+
/** The session's pending card, or undefined when there is none (or the mark is unreadable). */
|
|
95
|
+
function readPendingCard(sessionId, opts = {}) {
|
|
96
|
+
const f = file(sessionId, "card", opts.dir);
|
|
97
|
+
if (!f)
|
|
98
|
+
return undefined;
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = fs.readFileSync(f, "utf8").trim();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (/^\d+$/.test(raw))
|
|
107
|
+
return { at: Number(raw) };
|
|
108
|
+
try {
|
|
109
|
+
const v = JSON.parse(raw);
|
|
110
|
+
if (typeof v.at !== "number" || !Number.isFinite(v.at))
|
|
111
|
+
return undefined;
|
|
112
|
+
return {
|
|
113
|
+
at: v.at,
|
|
114
|
+
...(typeof v.cardId === "string" && v.cardId ? { cardId: v.cardId } : {}),
|
|
115
|
+
...(typeof v.key === "string" && v.key ? { key: v.key } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
89
122
|
function clearCardPending(sessionId, opts = {}) {
|
|
90
123
|
const f = file(sessionId, "card", opts.dir);
|
|
91
124
|
if (!f)
|
|
@@ -97,19 +130,41 @@ function clearCardPending(sessionId, opts = {}) {
|
|
|
97
130
|
// Bookkeeping only.
|
|
98
131
|
}
|
|
99
132
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `<session_id>.settled` — the after-hook saw the call run while the card had no id yet (answered at
|
|
135
|
+
* the keyboard within the hook's first second). approve-hook takes it once it has the id and closes
|
|
136
|
+
* the card itself. Holds the call's key; read once and removed.
|
|
137
|
+
*/
|
|
138
|
+
function markSettled(sessionId, key, opts = {}) {
|
|
139
|
+
const f = file(sessionId, "settled", opts.dir);
|
|
140
|
+
if (!f)
|
|
141
|
+
return;
|
|
142
|
+
try {
|
|
143
|
+
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
144
|
+
fs.writeFileSync(f, key, { mode: 0o600 });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Bookkeeping only: at worst the card waits for "Mark done".
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** The settled call's key, removed as it is read; undefined when there is none. */
|
|
151
|
+
function takeSettled(sessionId, opts = {}) {
|
|
152
|
+
const f = file(sessionId, "settled", opts.dir);
|
|
103
153
|
if (!f)
|
|
104
154
|
return undefined;
|
|
105
155
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
156
|
+
const key = fs.readFileSync(f, "utf8");
|
|
157
|
+
fs.rmSync(f, { force: true });
|
|
158
|
+
return key || undefined;
|
|
108
159
|
}
|
|
109
160
|
catch {
|
|
110
161
|
return undefined;
|
|
111
162
|
}
|
|
112
163
|
}
|
|
164
|
+
/** When the pending card was marked, or undefined when there is none. */
|
|
165
|
+
function cardPendingSince(sessionId, opts = {}) {
|
|
166
|
+
return readPendingCard(sessionId, opts)?.at;
|
|
167
|
+
}
|
|
113
168
|
function readLastNotice(sessionId, opts = {}) {
|
|
114
169
|
const f = file(sessionId, "json", opts.dir);
|
|
115
170
|
if (!f)
|