baychat 0.29.9 → 0.31.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 +43 -10
- package/dist/approval-wait.js +143 -0
- package/dist/approve-hook.js +68 -133
- package/dist/auto-update/describe.js +66 -0
- package/dist/auto-update/notice.js +40 -0
- package/dist/auto-update/policy.js +110 -0
- package/dist/auto-update/relay-glue.js +143 -0
- package/dist/auto-update/store.js +140 -0
- package/dist/auto-update/updater.js +324 -0
- package/dist/channel/permission.js +6 -0
- package/dist/cli-help.js +30 -3
- package/dist/denied/after-hook.js +46 -0
- package/dist/denied/confirm.js +131 -0
- package/dist/denied/grants.js +292 -0
- package/dist/denied/hook-log.js +75 -0
- package/dist/denied/live-session.js +154 -0
- package/dist/denied/notice.js +29 -0
- package/dist/denied/opt-in.js +68 -0
- package/dist/denied/permission-rule.js +186 -0
- package/dist/denied/settings-rules.js +221 -0
- package/dist/denied/transcript.js +173 -0
- package/dist/denied-hook.js +431 -0
- package/dist/doctor-command.js +7 -1
- package/dist/doctor.js +28 -0
- package/dist/help-topics.js +12 -4
- package/dist/hook-stdin.js +35 -0
- package/dist/index.js +13 -1
- package/dist/main.js +20 -2
- package/dist/permissions-command.js +104 -0
- package/dist/protocol-content.js +1 -1
- package/dist/refresh-clients.js +36 -0
- package/dist/relay/acp/runner.js +4 -0
- package/dist/relay/commands.js +19 -3
- package/dist/relay/daemon.js +37 -1
- package/dist/relay/launcher/approval-settings.js +56 -2
- package/dist/relay/launcher/loop.js +4 -1
- package/dist/relay/self-restart.js +149 -0
- package/dist/relay/terminal-pane.js +74 -2
- package/dist/runtimes.js +2 -2
- package/dist/self-upgrade.js +92 -27
- package/dist/start-command.js +15 -1
- package/dist/update-command.js +51 -31
- package/dist/waiting-cards.js +107 -0
- package/package.json +4 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutoUpdater = exports.TICK_MS = exports.UPDATE_NOW_INDEX = exports.UPDATE_OPTIONS = void 0;
|
|
4
|
+
const self_upgrade_1 = require("../self-upgrade");
|
|
5
|
+
const policy_1 = require("./policy");
|
|
6
|
+
const self_restart_1 = require("../relay/self-restart");
|
|
7
|
+
exports.UPDATE_OPTIONS = ["Update now", "Later"];
|
|
8
|
+
exports.UPDATE_NOW_INDEX = 0;
|
|
9
|
+
/** How often the state is looked at. Cheap: one small file and the clock. */
|
|
10
|
+
exports.TICK_MS = 60_000;
|
|
11
|
+
class AutoUpdater {
|
|
12
|
+
deps;
|
|
13
|
+
startedAt;
|
|
14
|
+
/** Spreads this relay's checks; fixed for its lifetime so the schedule does not wander. */
|
|
15
|
+
spread;
|
|
16
|
+
timer;
|
|
17
|
+
running = false;
|
|
18
|
+
stopped = false;
|
|
19
|
+
lastCardPoll = 0;
|
|
20
|
+
constructor(deps) {
|
|
21
|
+
this.deps = deps;
|
|
22
|
+
this.startedAt = deps.now();
|
|
23
|
+
this.spread = deps.random();
|
|
24
|
+
}
|
|
25
|
+
/** Finish an update the previous relay installed, then look at the state once a minute. */
|
|
26
|
+
start() {
|
|
27
|
+
void this.runExclusive(() => this.finishPendingUpdate());
|
|
28
|
+
this.timer = setInterval(() => void this.tick(), exports.TICK_MS);
|
|
29
|
+
this.timer.unref();
|
|
30
|
+
}
|
|
31
|
+
stop() {
|
|
32
|
+
this.stopped = true;
|
|
33
|
+
clearInterval(this.timer);
|
|
34
|
+
}
|
|
35
|
+
status() {
|
|
36
|
+
const state = this.deps.loadState();
|
|
37
|
+
const { mode, reason } = this.mode();
|
|
38
|
+
return {
|
|
39
|
+
mode,
|
|
40
|
+
reason,
|
|
41
|
+
current: this.deps.current,
|
|
42
|
+
latest: state.latest,
|
|
43
|
+
lastCheckAt: state.lastCheckAt,
|
|
44
|
+
waitingFor: state.waitingFor,
|
|
45
|
+
cardVersion: state.card?.requestId ? state.card.version : undefined,
|
|
46
|
+
failure: state.failure?.reason,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** One look at the state. Never throws; never runs twice at once. */
|
|
50
|
+
async tick() {
|
|
51
|
+
await this.runExclusive(async () => {
|
|
52
|
+
if (this.stopped)
|
|
53
|
+
return;
|
|
54
|
+
const state = this.deps.loadState();
|
|
55
|
+
const { mode, reason } = this.mode();
|
|
56
|
+
if (state.mode !== mode || state.modeReason !== reason || state.current !== this.deps.current) {
|
|
57
|
+
Object.assign(state, { mode, modeReason: reason, current: this.deps.current });
|
|
58
|
+
this.deps.saveState(state);
|
|
59
|
+
}
|
|
60
|
+
// New files are on disk under old code: restarting onto them comes before anything else,
|
|
61
|
+
// whatever the setting says now — running half old, half new is the one state to avoid.
|
|
62
|
+
if (state.finish)
|
|
63
|
+
return this.finishPendingUpdate();
|
|
64
|
+
if (mode === "off" || mode === "none")
|
|
65
|
+
return;
|
|
66
|
+
const now = this.deps.now();
|
|
67
|
+
if (now >= (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread))
|
|
68
|
+
await this.check(state, mode);
|
|
69
|
+
if (mode !== "auto")
|
|
70
|
+
return;
|
|
71
|
+
if (state.card?.requestId && now - this.lastCardPoll >= policy_1.CARD_POLL_MS)
|
|
72
|
+
await this.pollCard(state);
|
|
73
|
+
if (state.target)
|
|
74
|
+
await this.installTarget(state);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
mode() {
|
|
78
|
+
const off = (0, policy_1.autoUpdateOffReason)(this.deps.env, this.deps.setting());
|
|
79
|
+
if (off)
|
|
80
|
+
return { mode: "off", reason: off };
|
|
81
|
+
const { install, supervised, root } = this.deps.ability;
|
|
82
|
+
if (install.kind === "source")
|
|
83
|
+
return { mode: "none", reason: "running from a checkout" };
|
|
84
|
+
if (install.kind === "npx")
|
|
85
|
+
return { mode: "notify", reason: "for an npx copy (use `npx baychat@latest`)" };
|
|
86
|
+
if (install.kind === "local")
|
|
87
|
+
return { mode: "notify", reason: "for a project install (update it there with `npm i baychat@latest`)" };
|
|
88
|
+
if (root)
|
|
89
|
+
return { mode: "notify", reason: "as root (install with `baychat update`)" };
|
|
90
|
+
if (!(0, self_restart_1.selfUpdateAllowedOn)(this.deps.ability.platform))
|
|
91
|
+
return { mode: "notify", reason: "on Windows (install with `baychat update`)" };
|
|
92
|
+
if (!supervised)
|
|
93
|
+
return { mode: "notify", reason: "for a relay started by hand (install with `baychat update`)" };
|
|
94
|
+
return { mode: "auto" };
|
|
95
|
+
}
|
|
96
|
+
/** Ask npm, and decide. */
|
|
97
|
+
async check(state, mode) {
|
|
98
|
+
const looked = await this.deps.lookupLatest();
|
|
99
|
+
state.lastCheckAt = this.deps.now();
|
|
100
|
+
if (!looked.ok) {
|
|
101
|
+
// Offline, npm down: quiet. Tried again within the hour, then less often.
|
|
102
|
+
state.checkFailures = (state.checkFailures ?? 0) + 1;
|
|
103
|
+
state.lastCheckError = looked.reason;
|
|
104
|
+
this.deps.log(`auto-update: could not ask npm (${looked.reason})`);
|
|
105
|
+
return this.deps.saveState(state);
|
|
106
|
+
}
|
|
107
|
+
state.checkFailures = 0;
|
|
108
|
+
state.lastCheckError = undefined;
|
|
109
|
+
state.latest = looked.release.version;
|
|
110
|
+
const plan = (0, policy_1.planUpdate)(this.deps.current, looked.release);
|
|
111
|
+
if (plan.kind === "current") {
|
|
112
|
+
// Updated some other way (`baychat update`), or nothing new: nothing may stay on the phone.
|
|
113
|
+
await this.dropCard(state);
|
|
114
|
+
state.target = undefined;
|
|
115
|
+
return this.deps.saveState(state);
|
|
116
|
+
}
|
|
117
|
+
if (mode === "notify") {
|
|
118
|
+
this.notifyOnce(state, `available:${plan.version}`, this.availableLine(plan.version));
|
|
119
|
+
return this.deps.saveState(state);
|
|
120
|
+
}
|
|
121
|
+
if (plan.kind === "auto") {
|
|
122
|
+
if (state.target?.version !== plan.version)
|
|
123
|
+
state.target = { version: plan.version, via: "auto" };
|
|
124
|
+
return this.deps.saveState(state);
|
|
125
|
+
}
|
|
126
|
+
await this.askOwner(state, plan.version, plan.reason);
|
|
127
|
+
this.deps.saveState(state);
|
|
128
|
+
}
|
|
129
|
+
/** The card for a version that needs the person. At most one per version, never while one waits. */
|
|
130
|
+
async askOwner(state, version, reason) {
|
|
131
|
+
if (state.target?.version === version)
|
|
132
|
+
return; // already said "Update now"
|
|
133
|
+
const card = state.card;
|
|
134
|
+
if (card?.version === version && (card.requestId || (card.laterUntil ?? 0) > this.deps.now()))
|
|
135
|
+
return;
|
|
136
|
+
if (card && card.version !== version)
|
|
137
|
+
await this.dropCard(state); // a newer version supersedes it
|
|
138
|
+
this.notifyOnce(state, `ask:${version}`, `baychat ${version} needs you: ${reason}. Run \`baychat update\` when you are ready.`);
|
|
139
|
+
const asked = await this.deps.cards.ask(`New BayChat for your computer (${version}): ${reason}`);
|
|
140
|
+
if (!asked) {
|
|
141
|
+
this.deps.log(`auto-update: ${version} needs the owner, and no card could be put up yet`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
state.card = { version, reason, askedAt: this.deps.now(), ...asked };
|
|
145
|
+
this.deps.log(`auto-update: asked the owner about ${version} (${reason})`);
|
|
146
|
+
}
|
|
147
|
+
async pollCard(state) {
|
|
148
|
+
this.lastCardPoll = this.deps.now();
|
|
149
|
+
const card = state.card;
|
|
150
|
+
const answer = await this.deps.cards.answer(card.requestId);
|
|
151
|
+
if (answer.kind === "waiting")
|
|
152
|
+
return;
|
|
153
|
+
if (answer.kind === "update") {
|
|
154
|
+
this.deps.log(`auto-update: the owner chose to update to ${card.version}`);
|
|
155
|
+
state.target = { version: card.version, via: "card", conversationId: card.conversationId, session: card.session };
|
|
156
|
+
state.card = undefined;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// "Later", or the card is gone (withdrawn, deleted): ask again in a few days, not before.
|
|
160
|
+
state.card = { ...card, requestId: undefined, laterUntil: this.deps.now() + policy_1.ASK_AGAIN_MS };
|
|
161
|
+
}
|
|
162
|
+
this.deps.saveState(state);
|
|
163
|
+
}
|
|
164
|
+
/** Install the decided version in a quiet moment, then restart onto it in another. */
|
|
165
|
+
async installTarget(state) {
|
|
166
|
+
const target = state.target;
|
|
167
|
+
if ((0, self_upgrade_1.compareVersions)(this.deps.current, target.version) !== -1) {
|
|
168
|
+
state.target = undefined;
|
|
169
|
+
return this.deps.saveState(state);
|
|
170
|
+
}
|
|
171
|
+
if (this.deps.now() < (0, policy_1.installAllowedAt)(state.failure, target.version))
|
|
172
|
+
return;
|
|
173
|
+
const busy = this.deps.busyReason();
|
|
174
|
+
if (busy)
|
|
175
|
+
return this.wait(state, busy);
|
|
176
|
+
this.deps.log(`auto-update: installing baychat ${target.version} (from ${this.deps.current})`);
|
|
177
|
+
const installed = await this.deps.installVersion(target.version);
|
|
178
|
+
if (!installed.ok) {
|
|
179
|
+
const count = state.failure?.version === target.version ? state.failure.count + 1 : 1;
|
|
180
|
+
state.failure = { version: target.version, count, at: this.deps.now(), reason: installed.reason };
|
|
181
|
+
state.waitingFor = undefined;
|
|
182
|
+
this.deps.log(`auto-update: install of ${target.version} failed (${installed.reason}); ${this.deps.current} keeps running`);
|
|
183
|
+
this.notifyOnce(state, `failed:${target.version}`, `BayChat could not update itself to ${target.version}: ${installed.reason}`);
|
|
184
|
+
return this.deps.saveState(state);
|
|
185
|
+
}
|
|
186
|
+
// The new code is on disk. From here the NEW relay finishes, whoever restarts it.
|
|
187
|
+
state.finish = { from: this.deps.current, to: installed.to, via: target.via, conversationId: target.conversationId, session: target.session };
|
|
188
|
+
state.target = undefined;
|
|
189
|
+
state.failure = undefined;
|
|
190
|
+
state.waitingFor = undefined;
|
|
191
|
+
this.deps.saveState(state);
|
|
192
|
+
await this.restartWhenQuiet();
|
|
193
|
+
}
|
|
194
|
+
/** Restart now if nothing is going on, else on a later tick (the `finish` step marks it owed). */
|
|
195
|
+
async restartWhenQuiet() {
|
|
196
|
+
const state = this.deps.loadState();
|
|
197
|
+
if (!state.finish)
|
|
198
|
+
return;
|
|
199
|
+
const busy = this.deps.busyReason();
|
|
200
|
+
if (busy)
|
|
201
|
+
return this.wait(state, busy);
|
|
202
|
+
state.finish.restarts = (state.finish.restarts ?? 0) + 1;
|
|
203
|
+
state.waitingFor = undefined;
|
|
204
|
+
this.deps.saveState(state);
|
|
205
|
+
this.deps.log(`auto-update: restarting onto baychat ${state.finish.to}`);
|
|
206
|
+
await this.deps.restart();
|
|
207
|
+
}
|
|
208
|
+
wait(state, busy) {
|
|
209
|
+
if (state.waitingFor === busy)
|
|
210
|
+
return;
|
|
211
|
+
state.waitingFor = busy;
|
|
212
|
+
this.deps.saveState(state);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Run at start. If the previous relay installed a version and this IS that version, finish it.
|
|
216
|
+
* If this is still the old code (its restart came up on the old files somehow), the install
|
|
217
|
+
* evidently did not land: forget it, and the next check decides again.
|
|
218
|
+
*/
|
|
219
|
+
async finishPendingUpdate() {
|
|
220
|
+
const state = this.deps.loadState();
|
|
221
|
+
const finish = state.finish;
|
|
222
|
+
if (!finish)
|
|
223
|
+
return this.dropStaleCard(state);
|
|
224
|
+
if ((0, self_upgrade_1.compareVersions)(this.deps.current, finish.to) === -1) {
|
|
225
|
+
// Still the old code. Never restarted yet (the old relay died first): restart onto the new
|
|
226
|
+
// files. Restarted already and STILL old: the init system runs a different copy than the one
|
|
227
|
+
// updated. Give up on it rather than restart forever; the failure backs the next try off.
|
|
228
|
+
if (!finish.restarts)
|
|
229
|
+
return this.restartWhenQuiet();
|
|
230
|
+
const reason = `the relay restarted but still runs ${this.deps.current}`;
|
|
231
|
+
state.finish = undefined;
|
|
232
|
+
state.failure = { version: finish.to, count: 1, at: this.deps.now(), reason };
|
|
233
|
+
this.deps.log(`auto-update: ${reason}, not ${finish.to}`);
|
|
234
|
+
this.notifyOnce(state, `failed:${finish.to}`, `BayChat could not update itself to ${finish.to}: ${reason}. Run \`baychat update\`.`);
|
|
235
|
+
return this.deps.saveState(state);
|
|
236
|
+
}
|
|
237
|
+
state.finish = undefined;
|
|
238
|
+
state.lastUpdate = { from: finish.from, to: finish.to, at: this.deps.now() };
|
|
239
|
+
this.deps.saveState(state);
|
|
240
|
+
await this.refreshClientsQuietly();
|
|
241
|
+
this.notifyOnce(state, `updated:${finish.to}`, `BayChat updated itself to ${finish.to} (from ${finish.from}).`);
|
|
242
|
+
this.deps.saveState(state);
|
|
243
|
+
this.deps.log(`auto-update: now running baychat ${finish.to} (from ${finish.from})`);
|
|
244
|
+
await this.sayDone(finish);
|
|
245
|
+
await this.dropStaleCard(this.deps.loadState());
|
|
246
|
+
}
|
|
247
|
+
async refreshClientsQuietly() {
|
|
248
|
+
try {
|
|
249
|
+
await this.deps.refreshClients();
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
this.deps.log(`auto-update: could not refresh client configs (${errText(err)})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** The owner asked for this one: tell them, once, where they asked. A routine update stays quiet. */
|
|
256
|
+
async sayDone(finish) {
|
|
257
|
+
if (finish.via !== "card" || !finish.conversationId || !finish.session)
|
|
258
|
+
return;
|
|
259
|
+
try {
|
|
260
|
+
await this.deps.cards.say(finish.conversationId, finish.session, `Updated BayChat on this computer to ${finish.to}.`);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
this.deps.log(`auto-update: could not say the update was done (${errText(err)})`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** A card about a version this relay already runs (updated by hand meanwhile) comes off the phone. */
|
|
267
|
+
async dropStaleCard(state) {
|
|
268
|
+
if (!state.card || (0, self_upgrade_1.compareVersions)(this.deps.current, state.card.version) === -1)
|
|
269
|
+
return;
|
|
270
|
+
await this.dropCard(state);
|
|
271
|
+
this.deps.saveState(state);
|
|
272
|
+
}
|
|
273
|
+
async dropCard(state) {
|
|
274
|
+
const requestId = state.card?.requestId;
|
|
275
|
+
state.card = undefined;
|
|
276
|
+
if (!requestId)
|
|
277
|
+
return;
|
|
278
|
+
try {
|
|
279
|
+
await this.deps.cards.withdraw(requestId);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.deps.log(`auto-update: could not withdraw the update card (${errText(err)})`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** One line for the terminal, and never the same line twice. */
|
|
286
|
+
notifyOnce(state, key, text) {
|
|
287
|
+
if (state.noticeKey === key)
|
|
288
|
+
return;
|
|
289
|
+
state.noticeKey = key;
|
|
290
|
+
try {
|
|
291
|
+
this.deps.notify(text);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
this.deps.log(`auto-update: could not leave the terminal notice (${errText(err)})`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
availableLine(version) {
|
|
298
|
+
const { install } = this.deps.ability;
|
|
299
|
+
if (install.kind === "npx")
|
|
300
|
+
return `baychat ${version} is available. Use \`npx baychat@latest\`, or install it once with \`npm i -g baychat\` so it updates itself.`;
|
|
301
|
+
if (install.kind === "local")
|
|
302
|
+
return `baychat ${version} is available. Update it in ${install.packageRoot} with \`npm i baychat@latest\`.`;
|
|
303
|
+
return `baychat ${version} is available. Run \`baychat update\`.`;
|
|
304
|
+
}
|
|
305
|
+
async runExclusive(work) {
|
|
306
|
+
if (this.running)
|
|
307
|
+
return;
|
|
308
|
+
this.running = true;
|
|
309
|
+
try {
|
|
310
|
+
await work();
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
// A bug in one look must not end the loop: the next tick starts from the state on disk.
|
|
314
|
+
this.deps.log(`auto-update: ${errText(err)}`);
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
this.running = false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.AutoUpdater = AutoUpdater;
|
|
322
|
+
function errText(err) {
|
|
323
|
+
return err instanceof Error ? err.message : String(err);
|
|
324
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PermissionRelay = exports.RELAY_OPTIONS = void 0;
|
|
4
4
|
exports.composeRelayQuestion = composeRelayQuestion;
|
|
5
|
+
const waiting_cards_1 = require("../waiting-cards");
|
|
5
6
|
/** Button labels, and the ONLY index that means yes. Same as `baychat approve-hook`. */
|
|
6
7
|
exports.RELAY_OPTIONS = ["Allow this once", "Deny"];
|
|
7
8
|
const ALLOW_INDEX = 0;
|
|
@@ -40,6 +41,7 @@ class PermissionRelay {
|
|
|
40
41
|
return; // Nobody is listening through this channel: the terminal alone decides.
|
|
41
42
|
const pending = { claudeId: req.request_id, login: target.login, retired: false };
|
|
42
43
|
this.current = pending;
|
|
44
|
+
let unmark = () => undefined;
|
|
43
45
|
try {
|
|
44
46
|
let reply = await this.deps.api(target.login, "POST", "/api/device-api/approvals", {
|
|
45
47
|
session: target.session,
|
|
@@ -55,6 +57,9 @@ class PermissionRelay {
|
|
|
55
57
|
this.deps.log("permission relay: BayChat returned no card id");
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
60
|
+
// Marked while the card waits, so the relay's automatic update never restarts BayChat
|
|
61
|
+
// under an open question (waiting-cards.ts).
|
|
62
|
+
unmark = (0, waiting_cards_1.markWaitingCard)(pending.cardId);
|
|
58
63
|
let failures = 0;
|
|
59
64
|
for (;;) {
|
|
60
65
|
if (pending.retired) {
|
|
@@ -87,6 +92,7 @@ class PermissionRelay {
|
|
|
87
92
|
this.deps.log(`permission relay: could not ask BayChat (${err instanceof Error ? err.message : String(err)}); the terminal still decides`);
|
|
88
93
|
}
|
|
89
94
|
finally {
|
|
95
|
+
unmark();
|
|
90
96
|
if (this.current === pending)
|
|
91
97
|
this.current = undefined;
|
|
92
98
|
}
|
package/dist/cli-help.js
CHANGED
|
@@ -29,7 +29,12 @@ Usage:
|
|
|
29
29
|
baychat update Bring this machine onto the current version —
|
|
30
30
|
the package, every connected client's config, and
|
|
31
31
|
the relay daemon (which otherwise keeps running
|
|
32
|
-
old code).
|
|
32
|
+
old code). The relay does this by itself about
|
|
33
|
+
once a day, in a quiet moment, and asks on your
|
|
34
|
+
phone first when a version needs you
|
|
35
|
+
baychat update --auto on|off Switch those automatic updates on or off for this
|
|
36
|
+
computer (default on; BAYCHAT_AUTO_UPDATE=0 also
|
|
37
|
+
turns them off)
|
|
33
38
|
baychat join [name] [group] [--sessions | --private | --group <title>] [--runtime <runtime>] [--bay <Bay>]
|
|
34
39
|
baychat join --session <name> --group <title> --runtime codex
|
|
35
40
|
Join and connect incoming messages in this terminal
|
|
@@ -159,6 +164,17 @@ Usage:
|
|
|
159
164
|
docs/features/REMOTE_APPROVAL_HOOK.md before
|
|
160
165
|
switching it on, because it moves the last line
|
|
161
166
|
between an agent and this machine onto a phone
|
|
167
|
+
baychat denied-hook [--session <name> | --session-from-claude] [--timeout <sec>]
|
|
168
|
+
Claude Code PermissionDenied hook: when auto
|
|
169
|
+
mode blocks a call, put a card in BayChat; on
|
|
170
|
+
your yes, write that exact allow rule to the
|
|
171
|
+
project's .claude/settings.local.json and let
|
|
172
|
+
the session retry. Prints the retry or nothing;
|
|
173
|
+
every error leaves the denial standing
|
|
174
|
+
baychat permissions [list | remove <id> | on | off]
|
|
175
|
+
The denied-command card: on/off for sessions
|
|
176
|
+
BayChat starts (OFF by default), and the rules
|
|
177
|
+
it kept. See docs/features/DENIED_COMMAND_CARD.md
|
|
162
178
|
baychat relay attach --session <name> [--runtime claude|codex|hermes]
|
|
163
179
|
[--resume-id <id>] [--timeout <sec>]
|
|
164
180
|
Register this session with the relay and block
|
|
@@ -206,10 +222,21 @@ function helpRequest(command, args) {
|
|
|
206
222
|
* Commands whose arguments belong to someone else, so `--help` among them is data:
|
|
207
223
|
* - `claude` passes every argument to Claude Code — `baychat claude --help` is Claude Code's help;
|
|
208
224
|
* - `send` and `search` take free text, which may say "--help";
|
|
209
|
-
* - `approve-hook` must write exactly one decision object to stdout, whatever it is given
|
|
225
|
+
* - `approve-hook` must write exactly one decision object to stdout, whatever it is given, and
|
|
226
|
+
* `denied-hook` / `denied-after` must never print help where Claude Code reads a hook's output;
|
|
210
227
|
* - `help` (and its spellings) already prints help, and its arguments are a question.
|
|
211
228
|
*/
|
|
212
|
-
const ARGS_ARE_NOT_OURS = new Set([
|
|
229
|
+
const ARGS_ARE_NOT_OURS = new Set([
|
|
230
|
+
"claude",
|
|
231
|
+
"send",
|
|
232
|
+
"search",
|
|
233
|
+
"approve-hook",
|
|
234
|
+
"denied-hook",
|
|
235
|
+
"denied-after",
|
|
236
|
+
"help",
|
|
237
|
+
"--help",
|
|
238
|
+
"-h",
|
|
239
|
+
]);
|
|
213
240
|
/** Commands with a usage of their own, fuller than their lines in `HELP`. */
|
|
214
241
|
const OWN_USAGE = { login: exports.LOGIN_HELP, bays: bays_command_1.BAYS_USAGE, launch: command_1.LAUNCH_USAGE };
|
|
215
242
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.afterCall = afterCall;
|
|
4
|
+
exports.cmdDeniedAfter = cmdDeniedAfter;
|
|
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.
|
|
7
|
+
//
|
|
8
|
+
// It is installed beside `denied-hook` (relay/launcher/approval-settings.ts) as an ASYNC hook:
|
|
9
|
+
// Claude Code starts it and moves on, so it adds nothing to any tool call's latency, and it prints
|
|
10
|
+
// nothing. It runs on every matching tool call, so it is routed by index.ts without loading the
|
|
11
|
+
// CLI, and it returns at once when no once-grant exists — the common case by far.
|
|
12
|
+
//
|
|
13
|
+
// The TTL sweep (grants.ts, ONCE_TTL_MS) is the backstop for a retried call that never finishes,
|
|
14
|
+
// and every run here sweeps too — including a grant whose deferred confirmation never finished,
|
|
15
|
+
// which is taken back and the owner told (confirm.ts; it loads only the card client and config,
|
|
16
|
+
// no MCP SDK and none of the CLI's commands).
|
|
17
|
+
const hook_stdin_1 = require("../hook-stdin");
|
|
18
|
+
const confirm_1 = require("./confirm");
|
|
19
|
+
const grants_1 = require("./grants");
|
|
20
|
+
const permission_rule_1 = require("./permission-rule");
|
|
21
|
+
/** Handle one finished call. NEVER THROWS; diagnostics go to stderr. */
|
|
22
|
+
async function afterCall(rawStdin, log = defaultLog) {
|
|
23
|
+
try {
|
|
24
|
+
if (!(0, grants_1.hasGrantsToSweep)())
|
|
25
|
+
return;
|
|
26
|
+
const input = JSON.parse(rawStdin);
|
|
27
|
+
const call = typeof input.tool_name === "string" ? (0, permission_rule_1.callKeyOf)(input.tool_name, input.tool_input) : undefined;
|
|
28
|
+
if (call && typeof input.session_id === "string")
|
|
29
|
+
await (0, grants_1.consumeOnceGrants)(input.session_id, call);
|
|
30
|
+
await (0, grants_1.sweepExpiredGrants)();
|
|
31
|
+
if ((0, grants_1.hasStaleUnconfirmed)()) {
|
|
32
|
+
await (0, confirm_1.takeBackStaleUnconfirmed)({ log });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
log(`baychat denied-after: ${err instanceof Error ? err.message : String(err)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function defaultLog(line) {
|
|
40
|
+
process.stderr.write(`${line}\n`);
|
|
41
|
+
}
|
|
42
|
+
/** The command. Always 0: nothing this hook could say would change the call that already ran. */
|
|
43
|
+
async function cmdDeniedAfter(stdin = process.stdin) {
|
|
44
|
+
await afterCall(await (0, hook_stdin_1.readStdin)(stdin, 5_000));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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.confirmGrant = confirmGrant;
|
|
37
|
+
exports.takeBackStaleUnconfirmed = takeBackStaleUnconfirmed;
|
|
38
|
+
// `baychat permissions confirm --grant <id>` — the deferred half of the denied-command card's proof.
|
|
39
|
+
//
|
|
40
|
+
// Claude Code 2.1.299 writes a denied call to the transcript only after the PermissionDenied hook
|
|
41
|
+
// returns (transcript.ts), so the hook accepts such a call on the live-session proof
|
|
42
|
+
// (live-session.ts) and records the owner's grant as UNCONFIRMED. Just before printing `retry` it
|
|
43
|
+
// starts this command detached (like `permissions sweep`), which polls the transcript for the
|
|
44
|
+
// original `tool_use_id` for up to CONFIRM_WAIT_MS:
|
|
45
|
+
//
|
|
46
|
+
// • found, and the same call → the grant is marked confirmed and stands;
|
|
47
|
+
// • not found, or another call under that id → the grant's rules are taken back out of the file
|
|
48
|
+
// they went into, and the owner gets an info card saying so.
|
|
49
|
+
//
|
|
50
|
+
// A confirmation that never finishes (the child killed, the computer asleep) is caught by the next
|
|
51
|
+
// run of either hook or of a sweep: an unconfirmed grant older than UNCONFIRMED_STALE_MS is taken
|
|
52
|
+
// back the same way (`takeBackStaleUnconfirmed`).
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const approval_wait_1 = require("../approval-wait");
|
|
55
|
+
const config_1 = require("../config");
|
|
56
|
+
const grants_1 = require("./grants");
|
|
57
|
+
const notice_1 = require("./notice");
|
|
58
|
+
const transcript_1 = require("./transcript");
|
|
59
|
+
const LOG_LABEL = "baychat permissions confirm";
|
|
60
|
+
/** Confirm grant `id` against the transcript, or take it back. NEVER THROWS. */
|
|
61
|
+
async function confirmGrant(id, deps = {}) {
|
|
62
|
+
const log = deps.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
63
|
+
try {
|
|
64
|
+
const grant = (0, grants_1.findGrant)(id);
|
|
65
|
+
if (!grant) {
|
|
66
|
+
log(`${LOG_LABEL}: grant ${id} is already gone; nothing to confirm.`);
|
|
67
|
+
return "missing";
|
|
68
|
+
}
|
|
69
|
+
const pending = grant.unconfirmed;
|
|
70
|
+
if (!pending) {
|
|
71
|
+
log(`${LOG_LABEL}: grant ${id} was proven up front; nothing to confirm.`);
|
|
72
|
+
return "not-pending";
|
|
73
|
+
}
|
|
74
|
+
const verify = deps.verify ?? ((ref) => (0, transcript_1.verifyDeniedCall)(ref, { waitMs: transcript_1.CONFIRM_WAIT_MS }));
|
|
75
|
+
const verdict = await verify({
|
|
76
|
+
transcriptPath: pending.transcriptPath,
|
|
77
|
+
agentId: pending.agentId,
|
|
78
|
+
toolUseId: pending.toolUseId,
|
|
79
|
+
toolName: pending.toolName,
|
|
80
|
+
call: grant.call,
|
|
81
|
+
});
|
|
82
|
+
if (verdict.ok) {
|
|
83
|
+
const still = await (0, grants_1.markConfirmed)(id);
|
|
84
|
+
log(`${LOG_LABEL}: grant ${id} confirmed — the call is in the session's transcript${still ? "" : " (its once-rule was already used and removed)"}.`);
|
|
85
|
+
return "confirmed";
|
|
86
|
+
}
|
|
87
|
+
const revoked = await (0, grants_1.revokeGrant)(id);
|
|
88
|
+
log(`${LOG_LABEL}: grant ${id} NOT confirmed (${verdict.why}); ${revoked ? "its rules were removed" : "it was already gone"}.`);
|
|
89
|
+
// A once-grant gone by now was used by the retried call: the owner still has to hear of it.
|
|
90
|
+
if (revoked || grant.kind === "once")
|
|
91
|
+
await notifyOwner([{ grant, why: verdict.why, used: !revoked }], deps, log);
|
|
92
|
+
return "revoked";
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
log(`${LOG_LABEL}: grant ${id} could not be confirmed (${(0, approval_wait_1.reasonOf)(err)}); a later hook run takes it back when it goes stale.`);
|
|
96
|
+
return "missing";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Take back every unconfirmed grant whose confirmation should have finished, and tell the owner.
|
|
100
|
+
* Returns how many. NEVER THROWS. */
|
|
101
|
+
async function takeBackStaleUnconfirmed(deps = {}) {
|
|
102
|
+
const log = deps.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
103
|
+
try {
|
|
104
|
+
const doomed = await (0, grants_1.sweepStaleUnconfirmed)((deps.now ?? Date.now)());
|
|
105
|
+
for (const grant of doomed)
|
|
106
|
+
log(`${LOG_LABEL}: grant ${grant.id} was never confirmed in time; its rules were removed.`);
|
|
107
|
+
if (doomed.length)
|
|
108
|
+
await notifyOwner(doomed.map((grant) => ({ grant, why: "its confirmation never finished", used: false })), deps, log);
|
|
109
|
+
return doomed.length;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
log(`${LOG_LABEL}: stale unconfirmed grants could not be swept (${(0, approval_wait_1.reasonOf)(err)}).`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function notifyOwner(items, deps, log) {
|
|
117
|
+
const device = (deps.loadDevice ?? config_1.loadDeviceCredentials)();
|
|
118
|
+
if (!device) {
|
|
119
|
+
log(`${LOG_LABEL}: not logged in to BayChat, so the owner could not be told.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const noticeDeps = {
|
|
123
|
+
now: deps.now ?? Date.now,
|
|
124
|
+
sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
|
|
125
|
+
log,
|
|
126
|
+
};
|
|
127
|
+
const host = (deps.hostname ?? os.hostname)();
|
|
128
|
+
for (const { grant, why, used } of items) {
|
|
129
|
+
await (0, notice_1.postNotice)(device, grant.session, (0, notice_1.unconfirmedNotice)(grant, host, why, used), noticeDeps, LOG_LABEL);
|
|
130
|
+
}
|
|
131
|
+
}
|