baychat 0.13.1 → 0.15.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.
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ // Keeping already-connected clients working across a credential renewal.
3
+ //
4
+ // WHY THIS EXISTS. The device credential has a hard 30-day expiry, and every
5
+ // connected client holds a COPY of it — Codex and Cursor in their own config
6
+ // files, Claude Code in its MCP registration. `baychat login` renewed the
7
+ // credential in `~/.baychat/credentials.json` and updated none of them.
8
+ //
9
+ // Observed on 2026-08-30, in the exact shape it will hit every user: a login
10
+ // printed `✓ Logged in as Karrrmex`, and left both Codex configs and Claude
11
+ // Code's registration holding a token with two hours left on it. In two hours
12
+ // every agent on that machine would have stopped working, and the one command a
13
+ // user would reach for to fix it — `baychat login` — is the command that had
14
+ // just claimed success.
15
+ //
16
+ // A renewal is not an edge case. It is the normal, scheduled state of a
17
+ // credential with an expiry, so it has to be the case this code handles best.
18
+ //
19
+ // REFRESH IS NOT CONNECT. This only ever rewrites a file that ALREADY names
20
+ // BayChat. Writing a config for a client the user never connected would
21
+ // configure software they did not ask us to touch, on the occasion of renewing
22
+ // something else.
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.staleClientConfigs = staleClientConfigs;
25
+ exports.refreshExistingClientConfigs = refreshExistingClientConfigs;
26
+ const client_config_writer_1 = require("./client-config-writer");
27
+ const client_paths_1 = require("./client-paths");
28
+ const mcp_dialects_1 = require("./mcp-dialects");
29
+ /**
30
+ * Config files that name BayChat but not with this token.
31
+ *
32
+ * Used by `doctor` to report the fault, and by the refresh below to fix it. A
33
+ * file with no BayChat entry is not stale — it is unconnected, and reporting it
34
+ * would send someone to fix something that is not broken.
35
+ */
36
+ function staleClientConfigs(token, io, env) {
37
+ const stale = [];
38
+ for (const path of everyKnownConfigPath(env)) {
39
+ const contents = io.readFile(path);
40
+ if (contents === null || !contents.includes("baychat"))
41
+ continue;
42
+ if (!contents.includes(token))
43
+ stale.push(path);
44
+ }
45
+ return stale;
46
+ }
47
+ /**
48
+ * Rewrite every already-connected client config with the current credential.
49
+ *
50
+ * @returns the paths actually updated, for the caller to report. Naming them
51
+ * matters: a user whose editor is open needs to know which file changed under
52
+ * it, and a silent rewrite of a config file is exactly the behaviour that makes
53
+ * people stop trusting a tool that edits their configs.
54
+ */
55
+ function refreshExistingClientConfigs(endpoint, io, env) {
56
+ const updated = [];
57
+ for (const client of mcp_dialects_1.MCP_CLIENTS) {
58
+ for (const path of (0, client_paths_1.inspectableConfigPaths)(client, env)) {
59
+ const existing = io.readFile(path);
60
+ // Only files that already name BayChat. See the header: refresh is not
61
+ // connect.
62
+ if (existing === null || !existing.includes("baychat"))
63
+ continue;
64
+ if (existing.includes(endpoint.token))
65
+ continue;
66
+ if (rewrite(client, path, existing, endpoint, io))
67
+ updated.push(path);
68
+ }
69
+ }
70
+ return updated;
71
+ }
72
+ /**
73
+ * One file, rewritten, or `false` when it could not be.
74
+ *
75
+ * A failure here is deliberately not fatal. `mergeJsonConfig` refuses a config
76
+ * it cannot parse — correctly, because overwriting a file that is merely
77
+ * mid-edit would destroy it — and one unparseable Cursor config must not stop
78
+ * Codex being refreshed. A partial renewal is far better than none, and the
79
+ * caller reports what it managed.
80
+ */
81
+ function rewrite(client, path, existing, endpoint, io) {
82
+ try {
83
+ const merged = (0, client_config_writer_1.mergeClientConfig)((0, mcp_dialects_1.buildClientConfig)(client, endpoint), existing);
84
+ io.copyFile(path, (0, client_config_writer_1.backupPathFor)(path));
85
+ io.writeFile(path, merged.content);
86
+ return true;
87
+ }
88
+ catch {
89
+ // Nothing to log here: the caller reports which files it updated, and the
90
+ // absence of one from that list is the report. `doctor` names the rest.
91
+ return false;
92
+ }
93
+ }
94
+ /** Every config location of every client this package knows how to write. */
95
+ function everyKnownConfigPath(env) {
96
+ return mcp_dialects_1.MCP_CLIENTS.flatMap((client) => (0, client_paths_1.inspectableConfigPaths)(client, env));
97
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ // `baychat doctor` — the I/O half of the diagnosis.
3
+ //
4
+ // `doctor.ts` decides what is wrong from a description of a machine. This file
5
+ // builds that description from the real one and prints the result. The split is
6
+ // the reason the whole report is testable against fixtures, including machines
7
+ // nobody here owns — a Mac, a Windows box, a beta user's laptop with a broken
8
+ // Codex install.
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.currentDoctorEnv = currentDoctorEnv;
44
+ exports.cmdDoctor = cmdDoctor;
45
+ const child_process_1 = require("child_process");
46
+ const fs = __importStar(require("fs"));
47
+ const os = __importStar(require("os"));
48
+ const config_1 = require("./config");
49
+ const doctor_1 = require("./doctor");
50
+ const commands_1 = require("./relay/commands");
51
+ const credential_refresh_1 = require("./credential-refresh");
52
+ const client_paths_1 = require("./client-paths");
53
+ const runtime_binary_1 = require("./runtime-binary");
54
+ /** How long `claude mcp list` may take before we treat it as unanswerable. */
55
+ const MCP_LIST_TIMEOUT_MS = 15_000;
56
+ /**
57
+ * An environment variable naming a runtime's executable explicitly, for the
58
+ * machine where resolution genuinely cannot work it out.
59
+ *
60
+ * Documented in the `binary` check's remedy, so a user who hits an unresolvable
61
+ * runtime is told the escape hatch at the moment they need it.
62
+ */
63
+ function overrideFor(command) {
64
+ return process.env[`BAYCHAT_${command.toUpperCase()}_BIN`];
65
+ }
66
+ /** Where Codex is, so a snap install's own config location is checked too. */
67
+ function codexPath() {
68
+ const resolved = (0, runtime_binary_1.resolveRuntimeBinary)("codex", (0, runtime_binary_1.currentBinaryEnv)(overrideFor("codex")));
69
+ return resolved.ok ? resolved.path : undefined;
70
+ }
71
+ async function currentDoctorEnv() {
72
+ return {
73
+ home: os.homedir(),
74
+ platform: process.platform,
75
+ now: Date.now(),
76
+ device: (0, config_1.loadDeviceCredentials)(),
77
+ relay: await (0, commands_1.tryRelayStatus)(),
78
+ readText(filePath) {
79
+ try {
80
+ return fs.readFileSync(filePath, "utf8");
81
+ }
82
+ catch {
83
+ // Missing, unreadable, or a directory — all mean "nothing to inspect".
84
+ // The checks turn that into a finding; there is nothing to log here.
85
+ return null;
86
+ }
87
+ },
88
+ staleConfigs(token) {
89
+ // Uses the same reader the refresh does, so "doctor says stale" and
90
+ // "login fixed it" can never disagree about which files count.
91
+ return (0, credential_refresh_1.staleClientConfigs)(token, {
92
+ readFile: (filePath) => {
93
+ try {
94
+ return fs.readFileSync(filePath, "utf8");
95
+ }
96
+ catch {
97
+ // Absent or unreadable: nothing to compare, so nothing to report.
98
+ return null;
99
+ }
100
+ },
101
+ writeFile: () => undefined,
102
+ copyFile: () => undefined,
103
+ mkdirp: () => undefined,
104
+ }, (0, client_paths_1.currentPathEnv)());
105
+ },
106
+ resolveBinary(command) {
107
+ return (0, runtime_binary_1.resolveRuntimeBinary)(command, (0, runtime_binary_1.currentBinaryEnv)(overrideFor(command)));
108
+ },
109
+ mcpList(runtime) {
110
+ // Ask the runtime itself. Its own answer is correct wherever its config
111
+ // turns out to live — which, for a snap install, is not where we wrote.
112
+ const resolved = (0, runtime_binary_1.resolveRuntimeBinary)(runtime, (0, runtime_binary_1.currentBinaryEnv)(overrideFor(runtime)));
113
+ if (!resolved.ok)
114
+ return null;
115
+ // Same .cmd rule as the probe: an npm-installed CLI on Windows is a shim
116
+ // that cannot be spawned without a shell, and this call would otherwise
117
+ // throw EINVAL and be reported as "could not run `codex mcp list`".
118
+ const plan = (0, runtime_binary_1.spawnPlanFor)(resolved.path, process.platform);
119
+ try {
120
+ return (0, child_process_1.execFileSync)(plan.file, [...plan.prefixArgs, "mcp", "list"], {
121
+ encoding: "utf8",
122
+ timeout: MCP_LIST_TIMEOUT_MS,
123
+ stdio: ["ignore", "pipe", "pipe"],
124
+ });
125
+ }
126
+ catch (err) {
127
+ // A non-zero exit can still carry the listing on stdout, so it is worth
128
+ // reading before giving up. Only a genuinely empty result becomes
129
+ // "could not run", which the check reports as unknown rather than as
130
+ // "not registered".
131
+ const stdout = err.stdout;
132
+ return stdout && stdout.trim() !== "" ? stdout : null;
133
+ }
134
+ },
135
+ };
136
+ }
137
+ /**
138
+ * `baychat doctor [--json]`
139
+ *
140
+ * Exit codes match `relay status`: 0 clear, 1 something is broken, 2 there is
141
+ * mail nobody read.
142
+ */
143
+ async function cmdDoctor(argv) {
144
+ const report = (0, doctor_1.buildReport)(await currentDoctorEnv());
145
+ if (argv.includes("--json")) {
146
+ // The whole report as data, so a beta user can paste one blob into a
147
+ // support thread and it can be read without a round of questions.
148
+ console.log(JSON.stringify(report, null, 2));
149
+ return (0, doctor_1.exitCodeFor)(report);
150
+ }
151
+ for (const line of (0, doctor_1.renderReport)(report))
152
+ console.log(line);
153
+ return (0, doctor_1.exitCodeFor)(report);
154
+ }