baychat 0.29.0 → 0.29.2
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/launch/claude-trust.js +44 -10
- package/dist/relay/autostart.js +47 -8
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
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": {
|
|
@@ -37,6 +37,7 @@ exports.TRUST_KEY = void 0;
|
|
|
37
37
|
exports.claudeConfigPath = claudeConfigPath;
|
|
38
38
|
exports.trustKeysFor = trustKeysFor;
|
|
39
39
|
exports.isFolderTrusted = isFolderTrusted;
|
|
40
|
+
exports.trustWalk = trustWalk;
|
|
40
41
|
exports.trustProblemText = trustProblemText;
|
|
41
42
|
exports.trustFolder = trustFolder;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
@@ -113,16 +114,48 @@ function readConfig(file) {
|
|
|
113
114
|
}
|
|
114
115
|
return { ok: true, raw, json: json };
|
|
115
116
|
}
|
|
116
|
-
/**
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Read-only: would Claude Code skip its trust dialog in `folder` (under any of its names)?
|
|
119
|
+
*
|
|
120
|
+
* Trust is INHERITED, as Claude Code checks it (verified in 2.1.289): a folder counts as trusted
|
|
121
|
+
* when it or any folder above it was accepted — but inside a git repository the walk stops at the
|
|
122
|
+
* repository's root, so a trusted home folder does not reach into a repo below it. Checking only
|
|
123
|
+
* the folder's own key refused every spot under an already-trusted home, where Claude Code never
|
|
124
|
+
* asks and so the person had no way to record it.
|
|
125
|
+
*/
|
|
126
|
+
function isFolderTrusted(folder, file = claudeConfigPath(), gitRootOf = findGitRoot) {
|
|
118
127
|
const parsed = readConfig(file);
|
|
119
128
|
if (!parsed.ok)
|
|
120
129
|
return false;
|
|
121
130
|
const projects = (parsed.json.projects ?? {});
|
|
122
|
-
|
|
131
|
+
const accepted = (key) => {
|
|
123
132
|
const entry = Object.prototype.hasOwnProperty.call(projects, key) ? projects[key] : undefined;
|
|
124
133
|
return !!entry && typeof entry === "object" && entry[exports.TRUST_KEY] === true;
|
|
125
|
-
}
|
|
134
|
+
};
|
|
135
|
+
return trustKeysFor(folder).some((key) => trustWalk(key, gitRootOf).some(accepted));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `start` and the folders above it that Claude Code consults: up to the enclosing git root
|
|
139
|
+
* (inclusive) when there is one, else up to the filesystem root.
|
|
140
|
+
*/
|
|
141
|
+
function trustWalk(start, gitRootOf = findGitRoot) {
|
|
142
|
+
const from = path.resolve(start);
|
|
143
|
+
const stop = gitRootOf(from);
|
|
144
|
+
const out = [];
|
|
145
|
+
for (let dir = from;; dir = path.dirname(dir)) {
|
|
146
|
+
out.push(dir);
|
|
147
|
+
if (dir === stop || path.dirname(dir) === dir)
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The nearest folder at or above `dir` holding a `.git` (a directory, or a file for worktrees). */
|
|
152
|
+
function findGitRoot(dir) {
|
|
153
|
+
for (let d = dir;; d = path.dirname(d)) {
|
|
154
|
+
if (fs.existsSync(path.join(d, ".git")))
|
|
155
|
+
return d;
|
|
156
|
+
if (path.dirname(d) === d)
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
126
159
|
}
|
|
127
160
|
/** A sentence for each problem, for the person at the terminal. */
|
|
128
161
|
function trustProblemText(reason, file) {
|
|
@@ -209,13 +242,14 @@ function serialiseLike(raw, value) {
|
|
|
209
242
|
return raw.endsWith("\n") ? `${body}\n` : body;
|
|
210
243
|
}
|
|
211
244
|
/**
|
|
212
|
-
* A number
|
|
213
|
-
*
|
|
245
|
+
* A number the rewrite would not write back character for character: a 20-digit integer
|
|
246
|
+
* JSON.parse rounds, or a `1.0` / `1E5` JSON.stringify respells. Strings are blanked first so ids
|
|
247
|
+
* and tokens inside quotes don't count.
|
|
248
|
+
*
|
|
249
|
+
* Not a digit count: Claude Code itself writes costs like `41.32949860000002` (16+ significant
|
|
250
|
+
* digits) that round-trip exactly, so counting digits refused nearly every real file.
|
|
214
251
|
*/
|
|
215
252
|
function hasLossyNumbers(raw) {
|
|
216
253
|
const outsideStrings = raw.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
217
|
-
return (outsideStrings.match(/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g) ?? []).some((n) =>
|
|
218
|
-
const digits = n.replace(/^-/, "").split(/[eE]/)[0].replace(".", "").replace(/^0+/, "");
|
|
219
|
-
return digits.length > 15;
|
|
220
|
-
});
|
|
254
|
+
return (outsideStrings.match(/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g) ?? []).some((n) => JSON.stringify(Number(n)) !== n);
|
|
221
255
|
}
|
package/dist/relay/autostart.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AUTOSTART_IMPLEMENTATIONS = exports.WINDOWS_RELAY_LAUNCHER = exports.MAC_SEARCH_PATH = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
|
|
37
|
+
exports.replaceLaunchAgent = replaceLaunchAgent;
|
|
37
38
|
exports.launchdLogPath = launchdLogPath;
|
|
38
39
|
exports.autostartForPlatform = autostartForPlatform;
|
|
39
40
|
exports.relayCommand = relayCommand;
|
|
@@ -194,14 +195,7 @@ const launchd = {
|
|
|
194
195
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
195
196
|
fs.writeFileSync(target, launchd.unit(cmd), { mode: 0o644 });
|
|
196
197
|
prepareLaunchdLog();
|
|
197
|
-
|
|
198
|
-
// Replacing an agent means removing the old one first; bootout on a label
|
|
199
|
-
// that was never loaded exits non-zero, which is not a failure here.
|
|
200
|
-
await execFileAsync("launchctl", [
|
|
201
|
-
"bootout",
|
|
202
|
-
`${domain}/${exports.SERVICE_LABEL}`,
|
|
203
|
-
]).catch(() => undefined);
|
|
204
|
-
await execFileAsync("launchctl", ["bootstrap", domain, target]);
|
|
198
|
+
await replaceLaunchAgent(`gui/${os.userInfo().uid}`, exports.SERVICE_LABEL, target);
|
|
205
199
|
},
|
|
206
200
|
async uninstall() {
|
|
207
201
|
const domain = `gui/${os.userInfo().uid}`;
|
|
@@ -212,6 +206,51 @@ const launchd = {
|
|
|
212
206
|
fs.rmSync(launchd.unitPath(), { force: true });
|
|
213
207
|
},
|
|
214
208
|
};
|
|
209
|
+
const realLaunchctl = {
|
|
210
|
+
launchctl: async (args) => {
|
|
211
|
+
await execFileAsync("launchctl", args);
|
|
212
|
+
},
|
|
213
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
214
|
+
};
|
|
215
|
+
/** How long to wait for launchd to finish removing the old agent, and how often to look. */
|
|
216
|
+
const UNLOAD_WAIT_MS = 10_000;
|
|
217
|
+
const UNLOAD_POLL_MS = 250;
|
|
218
|
+
/** Bootstrap attempts, and the pause before each retry. */
|
|
219
|
+
const BOOTSTRAP_RETRY_DELAYS_MS = [1_000, 2_000];
|
|
220
|
+
/**
|
|
221
|
+
* Remove the loaded agent `label` (if any) and bootstrap `plist` in its place.
|
|
222
|
+
*
|
|
223
|
+
* ── WHY IT WAITS AND RETRIES ──────────────────────────────────────────────────────────
|
|
224
|
+
* `baychat update` on a Mac mini (0.28.0 → 0.29.0, 2026-10-05) failed the restart with
|
|
225
|
+
* "bootstrap failed: 5: Input/output error"; `baychat relay start` a moment later worked.
|
|
226
|
+
* launchd answers a bootstrap with EIO while the same label is still being torn down, and
|
|
227
|
+
* `bootout` can return before that teardown has finished. So: bootout, then wait until
|
|
228
|
+
* `launchctl print` no longer finds the label (bounded), then bootstrap — retrying twice,
|
|
229
|
+
* because "no longer printed" and "accepts a new bootstrap" are not quite the same instant.
|
|
230
|
+
* The last failure is rethrown unchanged, so a real refusal (a bad plist) still surfaces.
|
|
231
|
+
*/
|
|
232
|
+
async function replaceLaunchAgent(domain, label, plist, deps = realLaunchctl) {
|
|
233
|
+
// Bootout on a label that was never loaded exits non-zero, which is not a failure here.
|
|
234
|
+
await deps.launchctl(["bootout", `${domain}/${label}`]).catch(() => undefined);
|
|
235
|
+
for (let waited = 0; waited < UNLOAD_WAIT_MS; waited += UNLOAD_POLL_MS) {
|
|
236
|
+
const stillLoaded = await deps.launchctl(["print", `${domain}/${label}`]).then(() => true, () => false);
|
|
237
|
+
if (!stillLoaded)
|
|
238
|
+
break;
|
|
239
|
+
await deps.sleep(UNLOAD_POLL_MS);
|
|
240
|
+
}
|
|
241
|
+
for (let attempt = 0;; attempt++) {
|
|
242
|
+
try {
|
|
243
|
+
await deps.launchctl(["bootstrap", domain, plist]);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const delay = BOOTSTRAP_RETRY_DELAYS_MS[attempt];
|
|
248
|
+
if (delay === undefined)
|
|
249
|
+
throw err;
|
|
250
|
+
await deps.sleep(delay);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
215
254
|
/**
|
|
216
255
|
* The PATH the relay — and every session it wakes — runs with on macOS.
|
|
217
256
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|