baychat 0.37.0 → 0.39.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 +9 -4
- package/dist/approval-wait.js +14 -0
- package/dist/approve-hook.js +65 -7
- package/dist/cli-help.js +17 -5
- package/dist/main.js +20 -5
- package/dist/notify-answer.js +196 -0
- package/dist/notify-hook.js +227 -0
- package/dist/notify-state.js +136 -0
- package/dist/protocol-content.js +1 -1
- package/dist/relay/daemon.js +7 -0
- package/dist/relay/launcher/loop.js +12 -1
- package/dist/relay/launcher/session-spots.js +17 -2
- package/dist/setup-command.js +126 -0
- package/dist/setup-facts.js +44 -0
- package/dist/setup-plan.js +94 -0
- package/dist/start-command.js +1 -1
- package/hooks/hooks.json +14 -2
- package/package.json +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
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.NOTICE_MAX = exports.CARD_PENDING_MAX_MS = exports.NOTIFY_MIN_GAP_MS = exports.NOTIFY_TYPES = void 0;
|
|
37
|
+
exports.lastToolUse = lastToolUse;
|
|
38
|
+
exports.decideNotify = decideNotify;
|
|
39
|
+
exports.cmdNotifyHook = cmdNotifyHook;
|
|
40
|
+
// `baychat notify-hook` — the stuck-question safety net (29 Oct).
|
|
41
|
+
//
|
|
42
|
+
// WHY THIS EXISTS. Branding's Claude Code sat on a terminal dialog ("Jelly Fish" was made by
|
|
43
|
+
// someone else. Let Claude read it?) and nothing reached Karmen's phone: its approve-hook could not
|
|
44
|
+
// name its session (the claude-sessions link was gone), so it left the prompt to the terminal in
|
|
45
|
+
// silence. Messages piled up unread, because Claude Code reads nothing while a question is open.
|
|
46
|
+
//
|
|
47
|
+
// WHAT CLAUDE CODE GIVES US: the `Notification` event, in every session, joined or not, about 6 s
|
|
48
|
+
// after a permission dialog or an MCP form appears (`permission_prompt`, `elicitation_dialog`,
|
|
49
|
+
// `elicitation_url_dialog`; reproduced 29 Oct). Watch-only: its output answers nothing, and its
|
|
50
|
+
// `message` does not hold the question ("Claude needs your permission"), so the question is read
|
|
51
|
+
// from the model's last tool call in the transcript. `idle_prompt` is not taken: it fires a minute
|
|
52
|
+
// after every normal answer.
|
|
53
|
+
//
|
|
54
|
+
// WHAT IT DOES: one line to the owner's phone — "<session> (<folder>) is waiting on a question in its
|
|
55
|
+
// terminal: <what>" — through `POST /api/device-api/notify` (the owner's private chat with BayBrain,
|
|
56
|
+
// a system line and a push). Once per prompt, at most one a minute per session, and nothing while
|
|
57
|
+
// approve-hook's own card for this session is on the phone (notify-state.ts). Prints nothing and
|
|
58
|
+
// exits 0 on every path: a Notification hook can block nothing, and must never stand in the way.
|
|
59
|
+
const child_process_1 = require("child_process");
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const os = __importStar(require("os"));
|
|
62
|
+
const api_1 = require("./api");
|
|
63
|
+
const approve_hook_1 = require("./approve-hook");
|
|
64
|
+
const config_1 = require("./config");
|
|
65
|
+
const hook_stdin_1 = require("./hook-stdin");
|
|
66
|
+
const notify_answer_1 = require("./notify-answer");
|
|
67
|
+
const notify_state_1 = require("./notify-state");
|
|
68
|
+
const terminal_pane_1 = require("./relay/terminal-pane");
|
|
69
|
+
/** The notifications that mean "a question is open in the terminal". */
|
|
70
|
+
exports.NOTIFY_TYPES = ["permission_prompt", "elicitation_dialog", "elicitation_url_dialog"];
|
|
71
|
+
/** At most one notice a minute per Claude Code session. */
|
|
72
|
+
exports.NOTIFY_MIN_GAP_MS = 60_000;
|
|
73
|
+
/** A card marker older than this is a hook that died mid-wait, not a card on the phone. */
|
|
74
|
+
exports.CARD_PENDING_MAX_MS = 60 * 60_000;
|
|
75
|
+
/** The server's cap (terminal-notice.service.ts). */
|
|
76
|
+
exports.NOTICE_MAX = 300;
|
|
77
|
+
/** How much of the transcript's end is read for the last tool call. */
|
|
78
|
+
const TAIL_BYTES = 256 * 1024;
|
|
79
|
+
/** The model's last tool call in transcript JSONL text, or undefined. */
|
|
80
|
+
function lastToolUse(text) {
|
|
81
|
+
const lines = text.split("\n");
|
|
82
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
if (!line.includes('"tool_use"'))
|
|
85
|
+
continue;
|
|
86
|
+
let entry;
|
|
87
|
+
try {
|
|
88
|
+
entry = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue; // the cut-off first line of a tail, or a line being written
|
|
92
|
+
}
|
|
93
|
+
if (entry.type !== "assistant" || !Array.isArray(entry.message?.content))
|
|
94
|
+
continue;
|
|
95
|
+
const blocks = entry.message.content;
|
|
96
|
+
for (let j = blocks.length - 1; j >= 0; j--) {
|
|
97
|
+
const b = blocks[j];
|
|
98
|
+
if (b?.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string")
|
|
99
|
+
return { id: b.id, name: b.name, input: b.input };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function readTail(file) {
|
|
105
|
+
let fd;
|
|
106
|
+
try {
|
|
107
|
+
fd = fs.openSync(file, "r");
|
|
108
|
+
const size = fs.fstatSync(fd).size;
|
|
109
|
+
const length = Math.min(size, TAIL_BYTES);
|
|
110
|
+
const buf = Buffer.alloc(length);
|
|
111
|
+
fs.readSync(fd, buf, 0, length, size - length);
|
|
112
|
+
return buf.toString("utf8");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (fd !== undefined)
|
|
119
|
+
fs.closeSync(fd);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
123
|
+
/** `/home/k/projects/branding` → `~/projects/branding`. */
|
|
124
|
+
function shortPath(cwd) {
|
|
125
|
+
const home = os.homedir();
|
|
126
|
+
return cwd && home && (cwd === home || cwd.startsWith(`${home}/`)) ? `~${cwd.slice(home.length)}` : cwd;
|
|
127
|
+
}
|
|
128
|
+
function clip(text, max) {
|
|
129
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
130
|
+
}
|
|
131
|
+
async function postNotice(device, text) {
|
|
132
|
+
await (0, api_1.apiRequest)(device, "POST", "/api/device-api/notify", { text });
|
|
133
|
+
}
|
|
134
|
+
/** This process's own tmux pane (the hook runs inside the session), as text; undefined elsewhere. */
|
|
135
|
+
function readOwnPane() {
|
|
136
|
+
const handle = (0, terminal_pane_1.detectPane)();
|
|
137
|
+
if (handle?.kind !== "tmux")
|
|
138
|
+
return Promise.resolve(undefined);
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
(0, child_process_1.execFile)("tmux", ["-S", handle.socket, "capture-pane", "-p", "-t", handle.pane], { timeout: 5_000, encoding: "utf8" }, (err, out) => resolve(err ? undefined : String(out)));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** `baychat notify-answer <session> <dialog>`, detached, in this session's environment (its pane). */
|
|
144
|
+
function startAnswerProcess(session, dialog) {
|
|
145
|
+
const entry = process.argv[1];
|
|
146
|
+
if (!entry)
|
|
147
|
+
return false;
|
|
148
|
+
try {
|
|
149
|
+
const encoded = Buffer.from(JSON.stringify(dialog), "utf8").toString("base64");
|
|
150
|
+
(0, child_process_1.spawn)(process.execPath, [entry, "notify-answer", session, encoded], { detached: true, stdio: "ignore", env: process.env }).unref();
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** One hook run. NEVER THROWS: every failure is a skip with its reason. */
|
|
158
|
+
async function decideNotify(rawStdin, deps = {}) {
|
|
159
|
+
const now = deps.now ?? Date.now;
|
|
160
|
+
const opts = { dir: deps.stateDir };
|
|
161
|
+
try {
|
|
162
|
+
let input;
|
|
163
|
+
try {
|
|
164
|
+
input = JSON.parse(rawStdin);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { skipped: "not a question" };
|
|
168
|
+
}
|
|
169
|
+
const type = str(input.notification_type);
|
|
170
|
+
if (input.hook_event_name !== "Notification" || !exports.NOTIFY_TYPES.includes(type))
|
|
171
|
+
return { skipped: "not a question" };
|
|
172
|
+
const sessionId = str(input.session_id);
|
|
173
|
+
const card = (0, notify_state_1.cardPendingSince)(sessionId, opts);
|
|
174
|
+
if (card !== undefined && now() - card < exports.CARD_PENDING_MAX_MS)
|
|
175
|
+
return { skipped: "a card is already on the phone" };
|
|
176
|
+
// What is being asked: the model's last tool call for a permission dialog, else Claude Code's own words.
|
|
177
|
+
const tool = type === "permission_prompt" ? lastToolUse(readTail(str(input.transcript_path)) ?? "") : undefined;
|
|
178
|
+
const what = tool ? (0, approve_hook_1.describeToolRequest)(tool.name, tool.input) : str(input.message) || "a question";
|
|
179
|
+
const key = tool ? `tool:${tool.id}` : `${type}:${str(input.message)}`;
|
|
180
|
+
const last = (0, notify_state_1.readLastNotice)(sessionId, opts);
|
|
181
|
+
if (last?.lastKey === key)
|
|
182
|
+
return { skipped: "already sent for this prompt" };
|
|
183
|
+
if (last && now() - last.lastAt < exports.NOTIFY_MIN_GAP_MS)
|
|
184
|
+
return { skipped: "one a minute per session" };
|
|
185
|
+
const device = (deps.loadDevice ?? config_1.loadDeviceCredentials)();
|
|
186
|
+
if (!device)
|
|
187
|
+
return { skipped: "not logged in" };
|
|
188
|
+
const session = (deps.link ?? approve_hook_1.linkFromClaude)()?.session;
|
|
189
|
+
// A session BayChat knows, in a tmux pane showing a numbered dialog: a card the owner can
|
|
190
|
+
// ANSWER, by a detached `notify-answer` that presses the chosen number (notify-answer.ts).
|
|
191
|
+
if (session) {
|
|
192
|
+
const screen = await (deps.readScreen ?? readOwnPane)();
|
|
193
|
+
const dialog = screen ? (0, notify_answer_1.readDialog)(screen) : undefined;
|
|
194
|
+
if (dialog && (deps.startAnswer ?? startAnswerProcess)(session, dialog)) {
|
|
195
|
+
(0, notify_state_1.writeLastNotice)(sessionId, { lastAt: now(), lastKey: key }, opts);
|
|
196
|
+
return { sent: true, answerable: true };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const who = session || "A Claude Code session";
|
|
200
|
+
const where = shortPath(str(input.cwd));
|
|
201
|
+
const head = `${who}${where ? ` (${where})` : ""} is waiting on a question in its terminal: `;
|
|
202
|
+
const text = clip(`${head}${what.replace(/\s+/g, " ").trim()}`, exports.NOTICE_MAX);
|
|
203
|
+
await (deps.post ?? postNotice)(device, text);
|
|
204
|
+
(0, notify_state_1.writeLastNotice)(sessionId, { lastAt: now(), lastKey: key }, opts);
|
|
205
|
+
return { sent: true };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return { skipped: err instanceof Error ? err.message : String(err) };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* `baychat notify-hook [--dry-run]` — always exits 0 and prints nothing; why it skipped goes to
|
|
213
|
+
* stderr. `--dry-run` writes the line it would send to stderr instead of sending it (and still
|
|
214
|
+
* needs a login, so it shows the real path up to the request).
|
|
215
|
+
*/
|
|
216
|
+
async function cmdNotifyHook(argv = [], stdin = process.stdin) {
|
|
217
|
+
try {
|
|
218
|
+
const dryRun = argv.includes("--dry-run");
|
|
219
|
+
const outcome = await decideNotify(await (0, hook_stdin_1.readStdin)(stdin), dryRun ? { post: async (_d, text) => void process.stderr.write(`baychat notify-hook (dry run): ${text}\n`) } : {});
|
|
220
|
+
if ("skipped" in outcome && outcome.skipped !== "not a question")
|
|
221
|
+
process.stderr.write(`baychat notify-hook: ${outcome.skipped}\n`);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Nothing: a Notification hook must never stand in the way of the session.
|
|
225
|
+
}
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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.notifyStateDir = void 0;
|
|
37
|
+
exports.stateKey = stateKey;
|
|
38
|
+
exports.markCardPending = markCardPending;
|
|
39
|
+
exports.clearCardPending = clearCardPending;
|
|
40
|
+
exports.cardPendingSince = cardPendingSince;
|
|
41
|
+
exports.readLastNotice = readLastNotice;
|
|
42
|
+
exports.writeLastNotice = writeLastNotice;
|
|
43
|
+
/**
|
|
44
|
+
* What notify-hook remembers per Claude Code session, under `<config>/notify-state/`:
|
|
45
|
+
*
|
|
46
|
+
* - `<session_id>.json` — the last notice sent: `{ lastAt, lastKey }`, for "once per prompt, at
|
|
47
|
+
* most once a minute" (notify-hook.ts).
|
|
48
|
+
* - `<session_id>.card` — approve-hook has a card on the owner's phone for this session right now.
|
|
49
|
+
* Written when the card goes out and removed when the hook is done, so the same prompt never
|
|
50
|
+
* reaches the phone twice (a card AND a "waiting" line). Its age bounds it: a hook killed
|
|
51
|
+
* mid-wait leaves a file that stops counting after an hour.
|
|
52
|
+
*
|
|
53
|
+
* Small files, private to the user, never read by anything else. Every function swallows its own
|
|
54
|
+
* errors: a hook's bookkeeping must never stand in the way of the session.
|
|
55
|
+
*/
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
const config_1 = require("./config");
|
|
59
|
+
const notifyStateDir = () => path.join((0, config_1.configDir)(), "notify-state");
|
|
60
|
+
exports.notifyStateDir = notifyStateDir;
|
|
61
|
+
/** A Claude Code session id as a file name: its own characters only, or nothing. */
|
|
62
|
+
function stateKey(sessionId) {
|
|
63
|
+
return /^[A-Za-z0-9_-]{1,128}$/.test(sessionId) ? sessionId : undefined;
|
|
64
|
+
}
|
|
65
|
+
function file(sessionId, ext, dir) {
|
|
66
|
+
const key = stateKey(sessionId);
|
|
67
|
+
if (!key)
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
return path.join(dir ?? (0, exports.notifyStateDir)(), `${key}.${ext}`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// No config directory to keep it in: no bookkeeping, never an error on approve-hook's path.
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function markCardPending(sessionId, opts = {}) {
|
|
78
|
+
const f = file(sessionId, "card", opts.dir);
|
|
79
|
+
if (!f)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
83
|
+
fs.writeFileSync(f, String((opts.now ?? Date.now)()), { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Bookkeeping only.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function clearCardPending(sessionId, opts = {}) {
|
|
90
|
+
const f = file(sessionId, "card", opts.dir);
|
|
91
|
+
if (!f)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
fs.rmSync(f, { force: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Bookkeeping only.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** When the pending card was marked, or undefined when there is none. */
|
|
101
|
+
function cardPendingSince(sessionId, opts = {}) {
|
|
102
|
+
const f = file(sessionId, "card", opts.dir);
|
|
103
|
+
if (!f)
|
|
104
|
+
return undefined;
|
|
105
|
+
try {
|
|
106
|
+
const at = Number(fs.readFileSync(f, "utf8").trim());
|
|
107
|
+
return Number.isFinite(at) ? at : undefined;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function readLastNotice(sessionId, opts = {}) {
|
|
114
|
+
const f = file(sessionId, "json", opts.dir);
|
|
115
|
+
if (!f)
|
|
116
|
+
return undefined;
|
|
117
|
+
try {
|
|
118
|
+
const v = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
119
|
+
return typeof v.lastAt === "number" && typeof v.lastKey === "string" ? { lastAt: v.lastAt, lastKey: v.lastKey } : undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function writeLastNotice(sessionId, notice, opts = {}) {
|
|
126
|
+
const f = file(sessionId, "json", opts.dir);
|
|
127
|
+
if (!f)
|
|
128
|
+
return;
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(path.dirname(f), { recursive: true, mode: 0o700 });
|
|
131
|
+
fs.writeFileSync(f, JSON.stringify(notice), { mode: 0o600 });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Bookkeeping only: at worst the next prompt's notice is not rate-limited.
|
|
135
|
+
}
|
|
136
|
+
}
|