baychat 0.27.1 → 0.29.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 +32 -0
- package/README.md +42 -4
- package/dist/approve-hook.js +93 -3
- package/dist/channel/claude-link.js +145 -0
- package/dist/channel/detect.js +10 -2
- package/dist/channel/feed.js +27 -13
- package/dist/channel/launch.js +67 -12
- package/dist/channel/permission.js +118 -0
- package/dist/channel/plugin.js +182 -0
- package/dist/channel/relay-link.js +257 -0
- package/dist/channel/server.js +191 -10
- package/dist/claude-onboarding.js +7 -5
- package/dist/cli-help.js +235 -0
- package/dist/commands.js +4 -4
- package/dist/connect-acp.js +3 -2
- package/dist/connect-claude.js +31 -10
- package/dist/device-login.js +2 -2
- package/dist/doctor-command.js +2 -0
- package/dist/doctor.js +97 -3
- package/dist/help-topics.js +4 -3
- package/dist/launch/claude-trust.js +221 -0
- package/dist/launch/command.js +177 -0
- package/dist/launch/policy.js +152 -0
- package/dist/machine-name.js +76 -0
- package/dist/main.js +16 -170
- package/dist/multiplexer-launch.js +271 -0
- package/dist/protocol-content.js +1 -1
- package/dist/relay/acp/agents.js +42 -19
- package/dist/relay/acp/daemon-glue.js +3 -2
- package/dist/relay/acp/runner.js +8 -7
- package/dist/relay/channel-sessions.js +66 -0
- package/dist/relay/commands.js +9 -2
- package/dist/relay/daemon.js +160 -9
- package/dist/relay/launcher/approval-settings.js +140 -0
- package/dist/relay/launcher/argv.js +83 -0
- package/dist/relay/launcher/capabilities.js +76 -0
- package/dist/relay/launcher/dev-channel-screen.fixtures.js +41 -0
- package/dist/relay/launcher/dev-channel-screen.js +47 -0
- package/dist/relay/launcher/handle.js +303 -0
- package/dist/relay/launcher/loop.js +231 -0
- package/dist/relay/launcher/multiplexer.js +168 -0
- package/dist/relay/launcher/started.js +63 -0
- package/dist/relay/profiles.js +1 -1
- package/dist/relay/registry.js +26 -0
- package/dist/relay/terminal-pane.js +31 -2
- package/dist/relay/transport.js +7 -2
- package/dist/relay/ws.js +18 -1
- package/dist/runtime-install.js +19 -3
- package/dist/runtimes.js +3 -0
- package/dist/skill-bootstrap.js +1 -1
- package/dist/start-command.js +12 -56
- package/dist/tool-defs.js +23 -36
- package/package.json +4 -3
package/dist/connect-claude.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.cmdConnectClaude = cmdConnectClaude;
|
|
4
|
+
exports.channelReport = channelReport;
|
|
4
5
|
const commands_1 = require("./commands");
|
|
5
6
|
const config_1 = require("./config");
|
|
6
7
|
const bay_mcp_1 = require("./bay-mcp");
|
|
@@ -9,6 +10,8 @@ const runtime_install_1 = require("./runtime-install");
|
|
|
9
10
|
const bay_mcp_2 = require("./bay-mcp");
|
|
10
11
|
const context_hooks_command_1 = require("./context-hooks-command");
|
|
11
12
|
const launch_1 = require("./channel/launch");
|
|
13
|
+
const plugin_1 = require("./channel/plugin");
|
|
14
|
+
const ws_1 = require("./relay/ws");
|
|
12
15
|
/** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
|
|
13
16
|
async function cmdConnectClaude(opts = {}) {
|
|
14
17
|
try {
|
|
@@ -46,7 +49,8 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
46
49
|
console.log(" " + line);
|
|
47
50
|
if (!registered)
|
|
48
51
|
return 1;
|
|
49
|
-
|
|
52
|
+
for (const line of channelReport(registerChannel(), { version: process.version, webSocket: (0, ws_1.hasBuiltInWebSocket)() }))
|
|
53
|
+
console.log(line);
|
|
50
54
|
console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
|
|
51
55
|
return 0;
|
|
52
56
|
}
|
|
@@ -66,7 +70,12 @@ function registerChannel() {
|
|
|
66
70
|
const problem = (0, context_hooks_command_1.launchProblem)(launch);
|
|
67
71
|
if (problem)
|
|
68
72
|
return { skipped: problem };
|
|
69
|
-
|
|
73
|
+
const run = (0, bay_mcp_2.realClaudeRun)();
|
|
74
|
+
// The plugin first: it updates with the npm package and is the form Anthropic can approve.
|
|
75
|
+
// Where this Claude Code cannot install it, the same server is registered by hand.
|
|
76
|
+
if ((0, plugin_1.installPluginWithClaude)(run))
|
|
77
|
+
return "registered";
|
|
78
|
+
return (0, launch_1.registerChannelWithClaude)(launch, run)
|
|
70
79
|
? "registered"
|
|
71
80
|
: { skipped: "Claude Code did not accept the channel server." };
|
|
72
81
|
}
|
|
@@ -74,13 +83,25 @@ function registerChannel() {
|
|
|
74
83
|
return { skipped: err instanceof Error ? err.message : String(err) };
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
/**
|
|
87
|
+
* What `connect` says about the channel. Installed on a Node without the built-in WebSocket it is
|
|
88
|
+
* installed but cannot start (Claude Code shows it failed, sessions fall back to the Monitor) —
|
|
89
|
+
* said here, at the moment of setup, rather than discovered as a silent session later.
|
|
90
|
+
*/
|
|
91
|
+
function channelReport(result, node) {
|
|
92
|
+
if (result !== "registered") {
|
|
93
|
+
return [
|
|
94
|
+
` BayChat channel not added: ${result.skipped}`,
|
|
95
|
+
" Incoming messages use native WebSocket Monitor; the session will verify its connection after joining.",
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
const lines = [
|
|
99
|
+
"✓ BayChat channel added to Claude Code (wakes a session with no Monitor deadline)",
|
|
100
|
+
` Start Claude Code with \`${launch_1.CHANNEL_LAUNCH_COMMAND}\` (or \`baychat start\`) to use it.`,
|
|
101
|
+
" Without it, incoming messages use the native WebSocket Monitor as before.",
|
|
102
|
+
];
|
|
103
|
+
if (!node.webSocket) {
|
|
104
|
+
lines.push(` ⚠ This Node (${node.version}) has no built-in WebSocket, so the channel cannot start yet: it needs Node ${ws_1.WEBSOCKET_MIN_NODE}+.`, " Until then sessions use the Monitor, and the relay long-polls instead of using its WebSocket.", ` To fix: ${ws_1.NODE_UPGRADE_HINT}.`);
|
|
83
105
|
}
|
|
84
|
-
|
|
85
|
-
console.log(" Incoming messages use native WebSocket Monitor; the session will verify its connection after joining.");
|
|
106
|
+
return lines;
|
|
86
107
|
}
|
package/dist/device-login.js
CHANGED
|
@@ -37,11 +37,11 @@ exports.parseBayLogins = parseBayLogins;
|
|
|
37
37
|
exports.startDeviceLogin = startDeviceLogin;
|
|
38
38
|
exports.finishDeviceLogin = finishDeviceLogin;
|
|
39
39
|
const fs = __importStar(require("node:fs"));
|
|
40
|
-
const os = __importStar(require("node:os"));
|
|
41
40
|
const path = __importStar(require("node:path"));
|
|
42
41
|
const node_crypto_1 = require("node:crypto");
|
|
43
42
|
const zod_1 = require("zod");
|
|
44
43
|
const api_1 = require("./api");
|
|
44
|
+
const machine_name_1 = require("./machine-name");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const bayLoginSchema = zod_1.z.object({
|
|
47
47
|
bayId: zod_1.z.string().min(1),
|
|
@@ -103,7 +103,7 @@ async function startDeviceLogin(baseUrl) {
|
|
|
103
103
|
Date.parse(previous.expiresAt) > Date.now()) {
|
|
104
104
|
return { url: previous.url, expiresAt: previous.expiresAt };
|
|
105
105
|
}
|
|
106
|
-
const request = await (0, api_1.createDeviceLink)(base,
|
|
106
|
+
const request = await (0, api_1.createDeviceLink)(base, (0, machine_name_1.machineName)());
|
|
107
107
|
const parsed = pendingSchema.safeParse({ ...request, baseUrl: base });
|
|
108
108
|
if (!parsed.success)
|
|
109
109
|
throw new Error("BayChat returned an invalid login link. Try baychat login --start again.");
|
package/dist/doctor-command.js
CHANGED
|
@@ -54,6 +54,7 @@ const registry_1 = require("./relay/registry");
|
|
|
54
54
|
const credential_refresh_1 = require("./credential-refresh");
|
|
55
55
|
const client_paths_1 = require("./client-paths");
|
|
56
56
|
const runtime_binary_1 = require("./runtime-binary");
|
|
57
|
+
const ws_1 = require("./relay/ws");
|
|
57
58
|
/** How long `claude mcp list` may take before we treat it as unanswerable. */
|
|
58
59
|
const MCP_LIST_TIMEOUT_MS = 15_000;
|
|
59
60
|
/**
|
|
@@ -95,6 +96,7 @@ async function currentDoctorEnv() {
|
|
|
95
96
|
now: Date.now(),
|
|
96
97
|
device: (0, config_1.loadDeviceCredentials)(),
|
|
97
98
|
relay: await (0, commands_1.tryRelayStatus)(),
|
|
99
|
+
node: { version: process.version, webSocket: (0, ws_1.hasBuiltInWebSocket)() },
|
|
98
100
|
targets: registry.all(),
|
|
99
101
|
readText(filePath) {
|
|
100
102
|
try {
|
package/dist/doctor.js
CHANGED
|
@@ -71,6 +71,11 @@ const agents_1 = require("./relay/acp/agents");
|
|
|
71
71
|
const policy_1 = require("./relay/acp/policy");
|
|
72
72
|
const runtime_binary_1 = require("./runtime-binary");
|
|
73
73
|
const runtimes_1 = require("./runtimes");
|
|
74
|
+
const runtime_install_1 = require("./runtime-install");
|
|
75
|
+
const launch_1 = require("./channel/launch");
|
|
76
|
+
const plugin_1 = require("./channel/plugin");
|
|
77
|
+
const transport_1 = require("./relay/transport");
|
|
78
|
+
const ws_1 = require("./relay/ws");
|
|
74
79
|
/**
|
|
75
80
|
* The runtimes worth reporting on.
|
|
76
81
|
*
|
|
@@ -99,6 +104,14 @@ const SETUP_COMMAND = {
|
|
|
99
104
|
cursor: "baychat connect cursor",
|
|
100
105
|
desktop: "baychat connect desktop",
|
|
101
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* What rewrites a stale skill without signing in again. `baychat update` reinstalls Claude
|
|
109
|
+
* Code's skill as part of re-registering it; `baychat login` would too, but only after an
|
|
110
|
+
* interactive phone approval the person does not need.
|
|
111
|
+
*/
|
|
112
|
+
const SKILL_REFRESH_COMMAND = {
|
|
113
|
+
claude: "baychat update",
|
|
114
|
+
};
|
|
102
115
|
function setupCommand(runtime) {
|
|
103
116
|
return SETUP_COMMAND[runtime] ?? `baychat connect ${runtime}`;
|
|
104
117
|
}
|
|
@@ -217,6 +230,11 @@ function acpChecks(env) {
|
|
|
217
230
|
remedy: row.installHint,
|
|
218
231
|
};
|
|
219
232
|
}
|
|
233
|
+
// The relay refuses this binary before every turn; say so here rather than "pass".
|
|
234
|
+
const binaryProblem = (0, agents_1.binaryRefusal)(row, bin.path, bin.version);
|
|
235
|
+
if (binaryProblem) {
|
|
236
|
+
return { name, status: "fail", detail: `${row.label} on this computer will not be started`, remedy: binaryProblem };
|
|
237
|
+
}
|
|
220
238
|
const folder = (0, policy_1.checkFolder)(policy, row.id, t.acp.cwd);
|
|
221
239
|
if (!folder.ok) {
|
|
222
240
|
return {
|
|
@@ -411,6 +429,18 @@ function relayCheck(env) {
|
|
|
411
429
|
const transport = env.relay.transport
|
|
412
430
|
? `, transport ${env.relay.transport}${env.relay.transportDetail ? ` (${env.relay.transportDetail})` : ""}`
|
|
413
431
|
: "";
|
|
432
|
+
if (env.relay.transport === "long-poll" && env.relay.transportDetail === transport_1.NO_WEBSOCKET_DETAIL) {
|
|
433
|
+
// The relay's OWN Node has no WebSocket, so it long-polls for good — and the server throttles
|
|
434
|
+
// that ("Too many update polls", 15,000 times in three days on one laptop). A failure, not a
|
|
435
|
+
// pass with a note. Which fix depends on the Node this doctor runs on: if it has WebSocket,
|
|
436
|
+
// restarting the relay from it is enough (`update` rewrites the service with this Node).
|
|
437
|
+
return {
|
|
438
|
+
name: "relay",
|
|
439
|
+
status: "fail",
|
|
440
|
+
detail: `running (pid ${env.relay.pid}) but long-polling: its Node has no built-in WebSocket (Node ${ws_1.WEBSOCKET_MIN_NODE}+), and the server throttles the polls`,
|
|
441
|
+
remedy: env.node.webSocket ? `baychat update — restarts the relay on this Node (${env.node.version})` : ws_1.NODE_UPGRADE_HINT,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
414
444
|
return { name: "relay", status: "pass", detail: `running (pid ${env.relay.pid})${transport}` };
|
|
415
445
|
}
|
|
416
446
|
function pendingCheck(env) {
|
|
@@ -459,12 +489,72 @@ function runtimeReport(runtime, env) {
|
|
|
459
489
|
],
|
|
460
490
|
};
|
|
461
491
|
}
|
|
492
|
+
const extra = RUNTIME_EXTRA_CHECKS[runtime];
|
|
462
493
|
return {
|
|
463
494
|
runtime,
|
|
464
495
|
label: spec.label,
|
|
465
|
-
checks: [mcp, skill, binaryCheck(runtime, env), sessionCheck(runtime, env)],
|
|
496
|
+
checks: [mcp, skill, binaryCheck(runtime, env), sessionCheck(runtime, env), ...(extra ? [extra(env)] : [])],
|
|
466
497
|
};
|
|
467
498
|
}
|
|
499
|
+
/** Checks only one runtime has. Claude Code alone has the wake-up channel. */
|
|
500
|
+
const RUNTIME_EXTRA_CHECKS = {
|
|
501
|
+
claude: channelCheck,
|
|
502
|
+
};
|
|
503
|
+
/** `baychat@baychat 0.28.0`; Claude Code records "unknown" for a manifest that names no version. */
|
|
504
|
+
function pluginLabel(version) {
|
|
505
|
+
return version === "unknown" ? "baychat@baychat (version unknown)" : `baychat@baychat ${version}`;
|
|
506
|
+
}
|
|
507
|
+
/** What installs the channel, in either form (docs/features/CLAUDE_CODE_CHANNEL.md). */
|
|
508
|
+
const CHANNEL_SETUP_COMMAND = "baychat connect claude";
|
|
509
|
+
/**
|
|
510
|
+
* Is Claude Code's wake-up channel installed in a form that can deliver?
|
|
511
|
+
*
|
|
512
|
+
* Two forms count: BayChat's plugin, switched on AND declaring the channel in its own manifest,
|
|
513
|
+
* or the hand-registered `baychat_channel` server. Not having either is a `skip` — a session then
|
|
514
|
+
* listens through a Monitor, which works.
|
|
515
|
+
*
|
|
516
|
+
* A `baychat@baychat` that is switched on but carries no channel is a FAILURE, because it is what
|
|
517
|
+
* a silently failed setup leaves behind: computers set up before the channel have a skill-only
|
|
518
|
+
* plugin of that very name, from a GitHub catalog also called `baychat`. On those `connect claude`
|
|
519
|
+
* used to see "already installed", remove the hand-registered server and report success, leaving
|
|
520
|
+
* the channel nowhere (2026-10-03). Its remedy replaces the old catalog (`channel/plugin.ts`).
|
|
521
|
+
*/
|
|
522
|
+
function channelCheck(env) {
|
|
523
|
+
const files = { home: env.home, read: env.readText };
|
|
524
|
+
const installed = (0, plugin_1.installedPlugin)(files);
|
|
525
|
+
const enabled = (0, plugin_1.pluginEnabled)(files);
|
|
526
|
+
if (enabled && installed.state !== "channel") {
|
|
527
|
+
const origin = (0, plugin_1.baychatMarketplace)(files);
|
|
528
|
+
const from = origin.kind === "foreign" ? ` — the old skill-only plugin, from ${origin.source}` : "";
|
|
529
|
+
const what = installed.state === "absent"
|
|
530
|
+
? "baychat@baychat is switched on but not installed"
|
|
531
|
+
: `${pluginLabel(installed.version)} is switched on but carries no channel: ${installed.why}`;
|
|
532
|
+
return { name: "channel", status: "fail", detail: `${what}${from}`, remedy: CHANNEL_SETUP_COMMAND };
|
|
533
|
+
}
|
|
534
|
+
const forms = [
|
|
535
|
+
...(enabled && installed.state === "channel" ? [`plugin ${pluginLabel(installed.version)}`] : []),
|
|
536
|
+
...((0, launch_1.channelRegistered)((0, launch_1.claudeUserConfigPath)(env.home), (p) => env.readText(p) ?? "") ? ["server baychat_channel"] : []),
|
|
537
|
+
];
|
|
538
|
+
if (forms.length === 0) {
|
|
539
|
+
return {
|
|
540
|
+
name: "channel",
|
|
541
|
+
status: "skip",
|
|
542
|
+
detail: "not set up — sessions listen through a Monitor, which ends every 30 minutes",
|
|
543
|
+
remedy: CHANNEL_SETUP_COMMAND,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (!env.node.webSocket) {
|
|
547
|
+
// Set up, and unable to run: the channel needs the built-in WebSocket, so Claude Code would
|
|
548
|
+
// show it failed and every session would quietly fall back to the Monitor.
|
|
549
|
+
return {
|
|
550
|
+
name: "channel",
|
|
551
|
+
status: "fail",
|
|
552
|
+
detail: `${forms.join(" + ")}, but this Node (${env.node.version}) has no built-in WebSocket — the channel needs Node ${ws_1.WEBSOCKET_MIN_NODE}+ and cannot start`,
|
|
553
|
+
remedy: ws_1.NODE_UPGRADE_HINT,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return { name: "channel", status: "pass", detail: `${forms.join(" + ")} — start sessions with \`baychat claude\`` };
|
|
557
|
+
}
|
|
468
558
|
/**
|
|
469
559
|
* Is BayChat actually registered as an MCP server for this runtime?
|
|
470
560
|
*
|
|
@@ -549,13 +639,17 @@ function skillCheck(runtime, env) {
|
|
|
549
639
|
//
|
|
550
640
|
// A doc that is wrong is worse than one that is missing, because a missing one
|
|
551
641
|
// sends the agent looking.
|
|
552
|
-
|
|
642
|
+
//
|
|
643
|
+
// Compared against what the INSTALLER writes, not against the full workflow: Claude
|
|
644
|
+
// Code and Codex get a bootstrap that loads the workflow at run time, so comparing
|
|
645
|
+
// against the workflow failed every machine `baychat update` had just refreshed.
|
|
646
|
+
const current = (0, runtime_install_1.skillFileBody)(runtime);
|
|
553
647
|
if (current !== null && installed.trim() !== current.trim()) {
|
|
554
648
|
return {
|
|
555
649
|
name: "skill",
|
|
556
650
|
status: "fail",
|
|
557
651
|
detail: `${display(file, env)} is out of date — it does not match the skill this version installs`,
|
|
558
|
-
remedy: setupCommand(runtime),
|
|
652
|
+
remedy: SKILL_REFRESH_COMMAND[runtime] ?? setupCommand(runtime),
|
|
559
653
|
};
|
|
560
654
|
}
|
|
561
655
|
return { name: "skill", status: "pass", detail: `${display(file, env)} (current)` };
|
package/dist/help-topics.js
CHANGED
|
@@ -162,9 +162,10 @@ There is no timeout by design. A question worth asking is worth waiting for, and
|
|
|
162
162
|
a decision that expires silently is worse than one that waits.
|
|
163
163
|
|
|
164
164
|
\`baychat approve-hook\` wires Claude Code's own permission prompts to the same
|
|
165
|
-
cards. It is
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
cards. It is on only for Claude Code sessions BayChat starts (from the phone,
|
|
166
|
+
\`baychat claude\` or \`baychat start\`), never for a plain \`claude\`, and it moves
|
|
167
|
+
the last line between an agent and your machine onto a phone — read
|
|
168
|
+
docs/features/REMOTE_APPROVAL_HOOK.md. It fails CLOSED: every error denies.`,
|
|
168
169
|
};
|
|
169
170
|
exports.HELP_TOPICS = [GROUPS, SESSIONS, TOOLS, APPROVALS];
|
|
170
171
|
/** Exact name first, then keyword, then a substring of the body. */
|
|
@@ -0,0 +1,221 @@
|
|
|
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.TRUST_KEY = void 0;
|
|
37
|
+
exports.claudeConfigPath = claudeConfigPath;
|
|
38
|
+
exports.trustKeysFor = trustKeysFor;
|
|
39
|
+
exports.isFolderTrusted = isFolderTrusted;
|
|
40
|
+
exports.trustProblemText = trustProblemText;
|
|
41
|
+
exports.trustFolder = trustFolder;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
/**
|
|
46
|
+
* Claude Code's "Quick safety check" (workspace trust), read — and, only at the computer, written.
|
|
47
|
+
*
|
|
48
|
+
* A `claude` started unattended in a folder Claude Code has not trusted sits at
|
|
49
|
+
* "Is this a project you created or one you trust? ❯ No, exit / Yes, I trust this folder"
|
|
50
|
+
* forever, with nobody there to answer. Claude Code records the answer in `~/.claude.json`
|
|
51
|
+
* (or `$CLAUDE_CONFIG_DIR/.claude.json`) as `projects["<absolute path>"].hasTrustDialogAccepted`.
|
|
52
|
+
* (Key verified against the real file on Claude Code 2.1.289.)
|
|
53
|
+
*
|
|
54
|
+
* ── WHO MAY WRITE TRUST ───────────────────────────────────────────────────────────────
|
|
55
|
+
* Only a person at this computer, through `baychat launch add` answering y. The relay only READS
|
|
56
|
+
* (`isFolderTrusted`) and refuses with FOLDER_NOT_TRUSTED; nothing that arrives over the network
|
|
57
|
+
* ever calls `trustFolder`.
|
|
58
|
+
*
|
|
59
|
+
* The file holds the person's whole Claude Code state (and may hold secrets), so the write is
|
|
60
|
+
* as small as it can be: that one boolean on that one project key, everything else re-serialised
|
|
61
|
+
* unchanged, a `.bak` copy first, then a temp file renamed over the original with its mode.
|
|
62
|
+
* A missing or unparsable file is never written: we report and let the person run `claude`.
|
|
63
|
+
*/
|
|
64
|
+
exports.TRUST_KEY = "hasTrustDialogAccepted";
|
|
65
|
+
/** `$CLAUDE_CONFIG_DIR/.claude.json` when set (as Claude Code does), else `~/.claude.json`. */
|
|
66
|
+
function claudeConfigPath(env = process.env, home = os.homedir()) {
|
|
67
|
+
const dir = env.CLAUDE_CONFIG_DIR?.trim();
|
|
68
|
+
return dir ? path.join(dir, ".claude.json") : path.join(home, ".claude.json");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The keys Claude Code may have recorded for `folder`: the path as given, its realpath, and on
|
|
72
|
+
* macOS the same path with and without `/private` (/tmp, /var and /etc are symlinks there).
|
|
73
|
+
*/
|
|
74
|
+
function trustKeysFor(folder, platform = process.platform) {
|
|
75
|
+
const keys = new Set([folder]);
|
|
76
|
+
try {
|
|
77
|
+
keys.add(fs.realpathSync(folder));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// A folder that does not resolve is checked by the name given only.
|
|
81
|
+
}
|
|
82
|
+
if (platform === "darwin") {
|
|
83
|
+
for (const k of [...keys]) {
|
|
84
|
+
const m = /^\/private(\/(?:tmp|var|etc)(?:\/.*)?)$/.exec(k);
|
|
85
|
+
if (m)
|
|
86
|
+
keys.add(m[1]);
|
|
87
|
+
else if (/^\/(?:tmp|var|etc)(?:\/|$)/.test(k))
|
|
88
|
+
keys.add(`/private${k}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...keys];
|
|
92
|
+
}
|
|
93
|
+
function readConfig(file) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = fs.readFileSync(file, "utf8");
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return { ok: false, reason: err.code === "ENOENT" ? "CONFIG_MISSING" : "CONFIG_UNREADABLE" };
|
|
100
|
+
}
|
|
101
|
+
let json;
|
|
102
|
+
try {
|
|
103
|
+
json = JSON.parse(raw);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { ok: false, reason: "CONFIG_UNPARSABLE" };
|
|
107
|
+
}
|
|
108
|
+
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
109
|
+
return { ok: false, reason: "CONFIG_UNPARSABLE" };
|
|
110
|
+
const projects = json.projects;
|
|
111
|
+
if (projects !== undefined && (!projects || typeof projects !== "object" || Array.isArray(projects))) {
|
|
112
|
+
return { ok: false, reason: "CONFIG_UNPARSABLE" };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, raw, json: json };
|
|
115
|
+
}
|
|
116
|
+
/** Read-only: has Claude Code's trust dialog been accepted for `folder` (under any of its names)? */
|
|
117
|
+
function isFolderTrusted(folder, file = claudeConfigPath()) {
|
|
118
|
+
const parsed = readConfig(file);
|
|
119
|
+
if (!parsed.ok)
|
|
120
|
+
return false;
|
|
121
|
+
const projects = (parsed.json.projects ?? {});
|
|
122
|
+
return trustKeysFor(folder).some((key) => {
|
|
123
|
+
const entry = Object.prototype.hasOwnProperty.call(projects, key) ? projects[key] : undefined;
|
|
124
|
+
return !!entry && typeof entry === "object" && entry[exports.TRUST_KEY] === true;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** A sentence for each problem, for the person at the terminal. */
|
|
128
|
+
function trustProblemText(reason, file) {
|
|
129
|
+
switch (reason) {
|
|
130
|
+
case "CONFIG_MISSING":
|
|
131
|
+
return `${file} does not exist yet (Claude Code has not run on this computer).`;
|
|
132
|
+
case "CONFIG_UNREADABLE":
|
|
133
|
+
return `${file} could not be read.`;
|
|
134
|
+
case "CONFIG_UNPARSABLE":
|
|
135
|
+
return `${file} is not the JSON BayChat expects, so it was left alone.`;
|
|
136
|
+
case "CONFIG_UNSAFE":
|
|
137
|
+
return `${file} holds numbers BayChat could not rewrite exactly, so it was left alone.`;
|
|
138
|
+
case "CONFIG_CHANGING":
|
|
139
|
+
return `${file} kept changing while BayChat tried to write it (is Claude Code busy?).`;
|
|
140
|
+
case "WRITE_FAILED":
|
|
141
|
+
return `${file} could not be written.`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Set `projects[folder].hasTrustDialogAccepted = true` and nothing else. `folder` should be the
|
|
146
|
+
* realpath (the spot's stored path). Called ONLY from `baychat launch add` after a person said y.
|
|
147
|
+
*/
|
|
148
|
+
function trustFolder(folder, file = claudeConfigPath()) {
|
|
149
|
+
// Write through a symlinked ~/.claude.json (dotfile managers) instead of replacing the link.
|
|
150
|
+
let target = file;
|
|
151
|
+
try {
|
|
152
|
+
target = fs.realpathSync(file);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Missing: readConfig reports it below.
|
|
156
|
+
}
|
|
157
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
158
|
+
const parsed = readConfig(target);
|
|
159
|
+
if (!parsed.ok)
|
|
160
|
+
return parsed;
|
|
161
|
+
if (hasLossyNumbers(parsed.raw))
|
|
162
|
+
return { ok: false, reason: "CONFIG_UNSAFE" };
|
|
163
|
+
const projects = (parsed.json.projects ?? {});
|
|
164
|
+
const current = Object.prototype.hasOwnProperty.call(projects, folder) ? projects[folder] : undefined;
|
|
165
|
+
if (current !== undefined && (!current || typeof current !== "object" || Array.isArray(current))) {
|
|
166
|
+
return { ok: false, reason: "CONFIG_UNPARSABLE" };
|
|
167
|
+
}
|
|
168
|
+
if (current?.[exports.TRUST_KEY] === true)
|
|
169
|
+
return { ok: true, changed: false };
|
|
170
|
+
const next = {
|
|
171
|
+
...parsed.json,
|
|
172
|
+
projects: { ...projects, [folder]: { ...(current ?? {}), [exports.TRUST_KEY]: true } },
|
|
173
|
+
};
|
|
174
|
+
const text = serialiseLike(parsed.raw, next);
|
|
175
|
+
let mode;
|
|
176
|
+
try {
|
|
177
|
+
mode = fs.statSync(target).mode & 0o777;
|
|
178
|
+
const bak = `${target}.bak`;
|
|
179
|
+
fs.copyFileSync(target, bak);
|
|
180
|
+
fs.chmodSync(bak, mode);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
184
|
+
}
|
|
185
|
+
const tmp = path.join(path.dirname(target), `.${path.basename(target)}.baychat-${process.pid}-${Date.now()}.tmp`);
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(tmp, text, { mode });
|
|
188
|
+
fs.chmodSync(tmp, mode); // writeFile's mode is masked by the umask
|
|
189
|
+
// Claude Code rewrites this file often. If it changed since we read it, start over
|
|
190
|
+
// rather than rename our older copy over its newer one.
|
|
191
|
+
if (fs.readFileSync(target, "utf8") !== parsed.raw) {
|
|
192
|
+
fs.rmSync(tmp, { force: true });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
fs.renameSync(tmp, target);
|
|
196
|
+
return { ok: true, changed: true };
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
fs.rmSync(tmp, { force: true });
|
|
200
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: false, reason: "CONFIG_CHANGING" };
|
|
204
|
+
}
|
|
205
|
+
/** JSON.stringify in the file's own style: its indent, and its trailing newline or lack of one. */
|
|
206
|
+
function serialiseLike(raw, value) {
|
|
207
|
+
const indent = /^\{\r?\n(\t| +)"/.exec(raw)?.[1] ?? (raw.trimStart().startsWith("{\n") ? 2 : 0);
|
|
208
|
+
const body = JSON.stringify(value, null, indent);
|
|
209
|
+
return raw.endsWith("\n") ? `${body}\n` : body;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* A number JSON.parse cannot hold exactly (more than 15 significant digits) would be silently
|
|
213
|
+
* changed by the rewrite. Strings are blanked first so ids and tokens inside quotes don't count.
|
|
214
|
+
*/
|
|
215
|
+
function hasLossyNumbers(raw) {
|
|
216
|
+
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
|
+
});
|
|
221
|
+
}
|