baychat 0.39.0 → 0.41.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/README.md +4 -0
- 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/launch/claude-trust.js +3 -1
- package/dist/launch/codex-trust.js +177 -0
- package/dist/launch/command.js +105 -5
- package/dist/launch/policy.js +11 -3
- package/dist/mcp-board-cards.js +115 -0
- package/dist/mcp.js +3 -0
- 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/launcher/capabilities.js +13 -2
- package/dist/relay/launcher/codex-launch.js +12 -1
- package/dist/relay/launcher/loop.js +76 -4
- package/dist/relay/launcher/spot-request.js +70 -0
- 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/runtimes.js +3 -0
- package/dist/tool-defs.js +92 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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/README.md
CHANGED
|
@@ -918,6 +918,10 @@ instruction):
|
|
|
918
918
|
| `get_note` | Read one note in full (and the version to edit from) |
|
|
919
919
|
| `create_note` | Write a note for the project or the chat; it lands on the shelf (people pin) |
|
|
920
920
|
| `update_note` | Edit a note from the version you read; if someone saved since, nothing is overwritten and you get theirs to merge |
|
|
921
|
+
| `list_board_cards` | The to-dos on the boards of the chat's project, by column, with the board's table fields and each to-do's values; `mine` marks the ones you may move or edit |
|
|
922
|
+
| `add_board_card` | Add a to-do: yours by default (claim the work you are doing); one for a person or nobody is only a suggestion until a person accepts it |
|
|
923
|
+
| `move_board_card` | Move one of your to-dos to another column (Doing, Done, Blocked) |
|
|
924
|
+
| `update_board_card` | Edit the title, notes, dates or table values of one of your to-dos (money in integer cents) |
|
|
921
925
|
| `get_conversation_summary` | The rolling catch-up summary (decisions, tasks, questions, facts + source ids) |
|
|
922
926
|
| `get_messages` | Recent messages enriched with sender, mentions, and `shouldRespond` |
|
|
923
927
|
| `send_message` | Send a message into a conversation |
|
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);
|
|
@@ -176,7 +176,9 @@ function trustProblemText(reason, file) {
|
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
178
|
* Set `projects[folder].hasTrustDialogAccepted = true` and nothing else. `folder` should be the
|
|
179
|
-
* realpath (the spot's stored path). Called
|
|
179
|
+
* realpath (the spot's stored path). Called from `baychat launch add` after a person said y, and from
|
|
180
|
+
* the relay for the owner's "Allow… in this folder" on their phone — only for a folder that person
|
|
181
|
+
* opened to it (`phoneMayAllow`, relay/launcher/spot-request.ts).
|
|
180
182
|
*/
|
|
181
183
|
function trustFolder(folder, file = claudeConfigPath()) {
|
|
182
184
|
// Write through a symlinked ~/.claude.json (dotfile managers) instead of replacing the link.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.codexConfigPath = void 0;
|
|
37
|
+
exports.isCodexFolderTrusted = isCodexFolderTrusted;
|
|
38
|
+
exports.codexTrustProblemText = codexTrustProblemText;
|
|
39
|
+
exports.trustCodexFolder = trustCodexFolder;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const codex_launch_1 = require("../relay/launcher/codex-launch");
|
|
43
|
+
Object.defineProperty(exports, "codexConfigPath", { enumerable: true, get: function () { return codex_launch_1.codexConfigPath; } });
|
|
44
|
+
function read(file) {
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, raw: fs.readFileSync(file, "utf8") };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { ok: false, reason: err.code === "ENOENT" ? "CONFIG_MISSING" : "CONFIG_UNREADABLE" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Read-only, by the relay's own rule: the folder's own table says trusted. */
|
|
53
|
+
function isCodexFolderTrusted(folder, file = (0, codex_launch_1.codexConfigPath)()) {
|
|
54
|
+
const r = read(file);
|
|
55
|
+
if (!r.ok)
|
|
56
|
+
return false;
|
|
57
|
+
// codexLaunchProblem also wants the MCP entry; ask it only about trust by handing it a config that has one.
|
|
58
|
+
const withEntry = `[mcp_servers.baychat]\n${r.raw}`;
|
|
59
|
+
return (0, codex_launch_1.codexLaunchProblem)(folder, () => withEntry) === undefined;
|
|
60
|
+
}
|
|
61
|
+
/** A sentence for each problem, for the person at the terminal. */
|
|
62
|
+
function codexTrustProblemText(reason, file) {
|
|
63
|
+
switch (reason) {
|
|
64
|
+
case "CONFIG_MISSING":
|
|
65
|
+
return `${file} does not exist yet (Codex has not run on this computer).`;
|
|
66
|
+
case "CONFIG_UNREADABLE":
|
|
67
|
+
return `${file} could not be read.`;
|
|
68
|
+
case "PATH_UNSAFE":
|
|
69
|
+
return "the folder's name has a line break, or both a quote and a backslash, which BayChat will not write into Codex's config.";
|
|
70
|
+
case "CONFIG_UNKNOWN_FORM":
|
|
71
|
+
return `${file} already names this folder in a form BayChat does not edit, so it was left alone. Set the trust in Codex itself.`;
|
|
72
|
+
case "CONFIG_CHANGING":
|
|
73
|
+
return `${file} kept changing while BayChat tried to write it (is Codex busy?).`;
|
|
74
|
+
case "WRITE_FAILED":
|
|
75
|
+
return `${file} could not be written.`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The folder as a TOML key: a basic string, else a literal string, else null (neither can hold it). */
|
|
79
|
+
function tomlKey(folder) {
|
|
80
|
+
if (/[\n\r]/.test(folder))
|
|
81
|
+
return null;
|
|
82
|
+
if (!/["\\]/.test(folder))
|
|
83
|
+
return `"${folder}"`;
|
|
84
|
+
if (!folder.includes("'"))
|
|
85
|
+
return `'${folder}'`;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The config with `folder` trusted: its table's level set in place, or one table appended — or null when the
|
|
90
|
+
* folder is already a key in some other form, where an appended table would be a duplicate key.
|
|
91
|
+
*/
|
|
92
|
+
function withTrust(raw, folder) {
|
|
93
|
+
const header = new RegExp(`^[ \\t]*\\[projects\\.${(0, codex_launch_1.codexProjectKeyRe)(folder)}\\][ \\t]*$`, "m");
|
|
94
|
+
const found = header.exec(raw);
|
|
95
|
+
if (found) {
|
|
96
|
+
const start = found.index + found[0].length;
|
|
97
|
+
const next = raw.slice(start).search(/^[ \t]*\[/m);
|
|
98
|
+
const end = next === -1 ? raw.length : start + next;
|
|
99
|
+
const body = raw.slice(start, end);
|
|
100
|
+
const level = /^([ \t]*trust_level[ \t]*=[ \t]*)("[^"\n]*"|'[^'\n]*')/m;
|
|
101
|
+
const nextBody = level.test(body) ? body.replace(level, '$1"trusted"') : `\ntrust_level = "trusted"${body}`;
|
|
102
|
+
return raw.slice(0, start) + nextBody + raw.slice(end);
|
|
103
|
+
}
|
|
104
|
+
const otherForm = new RegExp(`${(0, codex_launch_1.codexProjectKeyRe)(folder)}\\s*(?:=|\\.)`);
|
|
105
|
+
if (otherForm.test(raw))
|
|
106
|
+
return null;
|
|
107
|
+
const sep = raw === "" || raw.endsWith("\n") ? "" : "\n";
|
|
108
|
+
return `${raw}${sep}\n[projects.${tomlKey(folder)}]\ntrust_level = "trusted"\n`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Trust `folder` (the spot's stored realpath) for Codex. Called from `baychat launch add` after a person at this
|
|
112
|
+
* computer said y, and from the relay for the owner's "Allow… in this folder" on their phone — only for a folder
|
|
113
|
+
* that person opened to it (`phoneMayAllow`, relay/launcher/spot-request.ts).
|
|
114
|
+
*/
|
|
115
|
+
function trustCodexFolder(folder, file = (0, codex_launch_1.codexConfigPath)()) {
|
|
116
|
+
if (tomlKey(folder) === null)
|
|
117
|
+
return { ok: false, reason: "PATH_UNSAFE" };
|
|
118
|
+
// Write through a symlinked config (dotfile managers) instead of replacing the link.
|
|
119
|
+
let target = file;
|
|
120
|
+
try {
|
|
121
|
+
target = fs.realpathSync(file);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Missing: read() reports it below.
|
|
125
|
+
}
|
|
126
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
127
|
+
const r = read(target);
|
|
128
|
+
// A fresh Codex install has its folder but no config.toml yet (vps, 4 Nov): create it holding only this
|
|
129
|
+
// table, 0600, and only if nothing appeared there meanwhile (`wx`). No folder: Codex has not run here.
|
|
130
|
+
if (!r.ok && r.reason === "CONFIG_MISSING" && fs.existsSync(path.dirname(target))) {
|
|
131
|
+
try {
|
|
132
|
+
fs.writeFileSync(target, (withTrust("", folder) ?? "").replace(/^\n/, ""), { mode: 0o600, flag: "wx" });
|
|
133
|
+
fs.chmodSync(target, 0o600); // writeFile's mode is masked by the umask
|
|
134
|
+
return { ok: true, changed: true };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err.code === "EEXIST")
|
|
138
|
+
continue; // Codex wrote it first: start over.
|
|
139
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!r.ok)
|
|
143
|
+
return r;
|
|
144
|
+
if (isCodexFolderTrusted(folder, target))
|
|
145
|
+
return { ok: true, changed: false };
|
|
146
|
+
const text = withTrust(r.raw, folder);
|
|
147
|
+
if (text === null)
|
|
148
|
+
return { ok: false, reason: "CONFIG_UNKNOWN_FORM" };
|
|
149
|
+
let mode;
|
|
150
|
+
try {
|
|
151
|
+
mode = fs.statSync(target).mode & 0o777;
|
|
152
|
+
const bak = `${target}.bak`;
|
|
153
|
+
fs.copyFileSync(target, bak);
|
|
154
|
+
fs.chmodSync(bak, mode);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
158
|
+
}
|
|
159
|
+
const tmp = path.join(path.dirname(target), `.${path.basename(target)}.baychat-${process.pid}-${Date.now()}.tmp`);
|
|
160
|
+
try {
|
|
161
|
+
fs.writeFileSync(tmp, text, { mode });
|
|
162
|
+
fs.chmodSync(tmp, mode); // writeFile's mode is masked by the umask
|
|
163
|
+
// If Codex rewrote it since we read it, start over rather than rename our older copy over its newer one.
|
|
164
|
+
if (fs.readFileSync(target, "utf8") !== r.raw) {
|
|
165
|
+
fs.rmSync(tmp, { force: true });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
fs.renameSync(tmp, target);
|
|
169
|
+
return { ok: true, changed: true };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
fs.rmSync(tmp, { force: true });
|
|
173
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ok: false, reason: "CONFIG_CHANGING" };
|
|
177
|
+
}
|