baychat 0.28.0 → 0.29.1

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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +24 -4
  2. package/README.md +41 -3
  3. package/dist/approve-hook.js +93 -3
  4. package/dist/channel/claude-link.js +145 -0
  5. package/dist/channel/feed.js +26 -12
  6. package/dist/channel/launch.js +59 -45
  7. package/dist/channel/plugin.js +182 -0
  8. package/dist/channel/relay-link.js +257 -0
  9. package/dist/channel/server.js +92 -7
  10. package/dist/cli-help.js +235 -0
  11. package/dist/commands.js +4 -4
  12. package/dist/connect-acp.js +3 -2
  13. package/dist/connect-claude.js +26 -10
  14. package/dist/device-login.js +2 -2
  15. package/dist/doctor-command.js +2 -0
  16. package/dist/doctor.js +97 -3
  17. package/dist/help-topics.js +4 -3
  18. package/dist/launch/claude-trust.js +221 -0
  19. package/dist/launch/command.js +177 -0
  20. package/dist/launch/policy.js +152 -0
  21. package/dist/machine-name.js +76 -0
  22. package/dist/main.js +16 -170
  23. package/dist/multiplexer-launch.js +271 -0
  24. package/dist/protocol-content.js +1 -1
  25. package/dist/relay/acp/agents.js +42 -19
  26. package/dist/relay/acp/daemon-glue.js +3 -2
  27. package/dist/relay/acp/runner.js +8 -7
  28. package/dist/relay/autostart.js +47 -8
  29. package/dist/relay/channel-sessions.js +66 -0
  30. package/dist/relay/commands.js +9 -2
  31. package/dist/relay/daemon.js +160 -9
  32. package/dist/relay/launcher/approval-settings.js +140 -0
  33. package/dist/relay/launcher/argv.js +83 -0
  34. package/dist/relay/launcher/capabilities.js +76 -0
  35. package/dist/relay/launcher/dev-channel-screen.fixtures.js +41 -0
  36. package/dist/relay/launcher/dev-channel-screen.js +47 -0
  37. package/dist/relay/launcher/handle.js +303 -0
  38. package/dist/relay/launcher/loop.js +231 -0
  39. package/dist/relay/launcher/multiplexer.js +168 -0
  40. package/dist/relay/launcher/started.js +63 -0
  41. package/dist/relay/profiles.js +1 -1
  42. package/dist/relay/registry.js +26 -0
  43. package/dist/relay/terminal-pane.js +31 -2
  44. package/dist/relay/transport.js +7 -2
  45. package/dist/relay/ws.js +18 -1
  46. package/dist/runtime-install.js +19 -3
  47. package/dist/runtimes.js +3 -0
  48. package/dist/start-command.js +12 -56
  49. package/dist/tool-defs.js +23 -36
  50. package/package.json +3 -3
@@ -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
+ }
@@ -0,0 +1,177 @@
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.trustByHand = exports.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
37
+ exports.cmdLaunch = cmdLaunch;
38
+ const path = __importStar(require("path"));
39
+ const args_1 = require("../args");
40
+ const multiplexer_launch_1 = require("../multiplexer-launch");
41
+ const policy_1 = require("../relay/acp/policy");
42
+ const claude_trust_1 = require("./claude-trust");
43
+ const policy_2 = require("./policy");
44
+ exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>]
45
+ Let BayChat start sessions in <folder> from the app
46
+ baychat launch list The folders BayChat may start sessions in
47
+ baychat launch remove <id> Stop offering one of them`;
48
+ /** Printed by every `add`: the one thing a person must know before saying yes. */
49
+ exports.APPROVALS_NOTICE = "Sessions started here from BayChat ask for approvals in BayChat.";
50
+ const realDeps = () => {
51
+ const file = (0, claude_trust_1.claudeConfigPath)();
52
+ return {
53
+ print: (line) => console.log(line),
54
+ policyFile: (0, policy_2.launchPolicyPath)(),
55
+ trust: { isTrusted: (folder) => (0, claude_trust_1.isFolderTrusted)(folder, file), trust: (folder) => (0, claude_trust_1.trustFolder)(folder, file), file },
56
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY,
57
+ ask: multiplexer_launch_1.askLine,
58
+ };
59
+ };
60
+ /**
61
+ * `baychat launch add|list|remove` — the only writer of the launch policy.
62
+ *
63
+ * LOCAL ONLY, like `connect dsh`: nothing over the network reaches this, so the folders a phone
64
+ * may start sessions in are always chosen by someone sitting at this computer. Everything that
65
+ * can refuse does so before the file is written.
66
+ */
67
+ async function cmdLaunch(args, deps = realDeps()) {
68
+ const [sub, ...rest] = args;
69
+ switch (sub) {
70
+ case "add":
71
+ return addSpot(rest, deps);
72
+ case "list":
73
+ return listSpots(deps);
74
+ case "remove":
75
+ return removeSpotById(rest, deps);
76
+ default:
77
+ throw new Error(exports.LAUNCH_USAGE);
78
+ }
79
+ }
80
+ async function addSpot(args, deps) {
81
+ const label = (0, args_1.flag)(args, "--label");
82
+ const runtimeList = (0, args_1.flag)(args, "--runtime");
83
+ const maxRaw = (0, args_1.flag)(args, "--max");
84
+ const valued = new Set([label, runtimeList, maxRaw].filter((v) => v !== undefined));
85
+ const folderArg = args.find((a) => !a.startsWith("--") && !valued.has(a));
86
+ if (!folderArg)
87
+ throw new Error(exports.LAUNCH_USAGE);
88
+ const folder = (0, policy_1.validateNewRoot)(folderArg);
89
+ if (!folder.ok) {
90
+ deps.print(folder.reason);
91
+ return 1;
92
+ }
93
+ const spotLabel = (label ?? path.basename(folder.path)).trim();
94
+ if (!spotLabel || spotLabel.length > policy_2.SPOT_LABEL_MAX) {
95
+ deps.print(`A label is 1-${policy_2.SPOT_LABEL_MAX} characters.`);
96
+ return 1;
97
+ }
98
+ const requested = (runtimeList ?? "claude").split(",").map((r) => r.trim()).filter(Boolean);
99
+ const unknown = requested.filter((r) => !(0, policy_2.isLaunchRuntime)(r));
100
+ if (requested.length === 0 || unknown.length > 0) {
101
+ deps.print(`Unknown runtime ${unknown.join(", ") || "(none given)"}. Choose from: ${policy_2.LAUNCH_RUNTIMES.join(", ")}.`);
102
+ return 1;
103
+ }
104
+ const maxSessions = maxRaw === undefined ? policy_2.DEFAULT_MAX_SESSIONS : Number(maxRaw);
105
+ if (!Number.isInteger(maxSessions) || maxSessions < 1 || maxSessions > policy_2.MAX_SESSIONS_PER_COMPUTER) {
106
+ deps.print(`--max is a whole number from 1 to ${policy_2.MAX_SESSIONS_PER_COMPUTER} (the most sessions one computer runs).`);
107
+ return 1;
108
+ }
109
+ const policy = (0, policy_2.loadLaunchPolicy)(deps.policyFile);
110
+ const existing = policy?.spots.find((s) => s.path === folder.path);
111
+ const spot = {
112
+ id: existing?.id ?? (0, policy_2.suggestSpotId)(policy, folder.path),
113
+ label: spotLabel,
114
+ path: folder.path,
115
+ runtimes: [...new Set(requested)],
116
+ maxSessions,
117
+ };
118
+ (0, policy_2.saveLaunchPolicy)((0, policy_2.upsertSpot)(policy, spot), deps.policyFile);
119
+ deps.print(`${existing ? "Updated" : "Added"} "${spot.label}" (${spot.id}): ${spot.path}`);
120
+ deps.print(` Agents: ${spot.runtimes.join(", ")} · at most ${spot.maxSessions} at once`);
121
+ deps.print(` ${exports.APPROVALS_NOTICE}`);
122
+ if (spot.runtimes.includes("claude"))
123
+ await offerClaudeTrust(spot.path, deps);
124
+ return 0;
125
+ }
126
+ /** How to trust a folder by hand: the one answer Claude Code itself accepts. */
127
+ const trustByHand = (folder) => `run \`claude\` in ${folder} once and choose "Yes, I trust this folder".`;
128
+ exports.trustByHand = trustByHand;
129
+ /**
130
+ * A `claude` started from the phone in a folder Claude Code has not trusted waits at its
131
+ * "Quick safety check" forever, so the relay refuses those (FOLDER_NOT_TRUSTED). This is the one
132
+ * place trust may be written: here, by the person at this computer, after they typed y.
133
+ * The spot is kept either way.
134
+ */
135
+ async function offerClaudeTrust(folder, deps) {
136
+ if (deps.trust.isTrusted(folder))
137
+ return;
138
+ deps.print(`Claude Code hasn't trusted this folder yet, so a session started here from BayChat would wait at its safety question with nobody to answer.`);
139
+ if (!deps.interactive) {
140
+ deps.print(`To fix that, ${(0, exports.trustByHand)(folder)}`);
141
+ return;
142
+ }
143
+ const answer = await deps.ask("Trust this folder for Claude Code now? (y/N) ");
144
+ if (!/^\s*y(es)?\s*$/i.test(answer)) {
145
+ deps.print(`Not trusted. Sessions from BayChat can't start here until you ${(0, exports.trustByHand)(folder)}`);
146
+ return;
147
+ }
148
+ const result = deps.trust.trust(folder);
149
+ if (result.ok) {
150
+ deps.print(`Trusted. Claude Code will start in ${folder} without asking (a copy of the old settings is in ${deps.trust.file}.bak).`);
151
+ return;
152
+ }
153
+ deps.print(`Couldn't record it: ${(0, claude_trust_1.trustProblemText)(result.reason, deps.trust.file)} Instead, ${(0, exports.trustByHand)(folder)}`);
154
+ }
155
+ function listSpots(deps) {
156
+ const policy = (0, policy_2.loadLaunchPolicy)(deps.policyFile);
157
+ if (!policy || policy.spots.length === 0) {
158
+ deps.print("BayChat may not start sessions on this computer. Add a folder with `baychat launch add <folder>`.");
159
+ return 0;
160
+ }
161
+ for (const s of policy.spots)
162
+ deps.print(`${s.id} "${s.label}" ${s.path} [${s.runtimes.join(", ")}] max ${s.maxSessions}`);
163
+ return 0;
164
+ }
165
+ function removeSpotById(args, deps) {
166
+ const id = args.find((a) => !a.startsWith("--"));
167
+ if (!id)
168
+ throw new Error(exports.LAUNCH_USAGE);
169
+ const policy = (0, policy_2.loadLaunchPolicy)(deps.policyFile);
170
+ if (!policy?.spots.some((s) => s.id === id)) {
171
+ deps.print(`No folder with id "${id}". \`baychat launch list\` shows them.`);
172
+ return 1;
173
+ }
174
+ (0, policy_2.saveLaunchPolicy)((0, policy_2.removeSpot)(policy, id), deps.policyFile);
175
+ deps.print(`Removed "${id}". Sessions already running there keep running.`);
176
+ return 0;
177
+ }
@@ -0,0 +1,152 @@
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.MAX_SESSIONS_PER_COMPUTER = exports.DEFAULT_MAX_SESSIONS = exports.MAX_SPOTS = exports.SPOT_LABEL_MAX = exports.SPOT_ID_RE = exports.LAUNCH_RUNTIMES = void 0;
37
+ exports.launchPolicyPath = launchPolicyPath;
38
+ exports.isLaunchRuntime = isLaunchRuntime;
39
+ exports.loadLaunchPolicy = loadLaunchPolicy;
40
+ exports.saveLaunchPolicy = saveLaunchPolicy;
41
+ exports.upsertSpot = upsertSpot;
42
+ exports.removeSpot = removeSpot;
43
+ exports.suggestSpotId = suggestSpotId;
44
+ exports.resolveSpotFolder = resolveSpotFolder;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const config_1 = require("../config");
48
+ const policy_1 = require("../relay/acp/policy");
49
+ /**
50
+ * Where BayChat may START sessions on this computer: `~/.baychat/launch-policy.json` (0600).
51
+ *
52
+ * ── WHAT MAKES THIS A BOUNDARY ────────────────────────────────────────────────────────
53
+ * Nothing that arrives over the network writes this file. Only `baychat launch add|remove`, run
54
+ * by a person at this machine, changes it. The relay advertises its spots by LABEL only and
55
+ * re-reads this file for every launch, so a deleted spot stops launches at once. Invalid is
56
+ * treated as absent: a file we cannot read offers nothing — it never means "allow everything".
57
+ *
58
+ * Folder rules are `connect`'s (relay/acp/policy.ts `validateNewRoot`): never `/`, never the home
59
+ * folder itself, only a real directory. The path is stored as its realpath when the spot is added
60
+ * and resolved AGAIN at launch (`resolveSpotFolder`), so a symlink swapped in afterwards is refused.
61
+ */
62
+ /** Every runtime a spot may name. Phase 1 launches only `claude`; the relay refuses the rest. */
63
+ exports.LAUNCH_RUNTIMES = ["claude", "codex", "dsh", "cursor-cli"];
64
+ exports.SPOT_ID_RE = /^[a-z0-9-]{1,32}$/;
65
+ exports.SPOT_LABEL_MAX = 40;
66
+ exports.MAX_SPOTS = 20;
67
+ exports.DEFAULT_MAX_SESSIONS = 3;
68
+ /** The hard cap per computer. A spot's own cap can never exceed it. */
69
+ exports.MAX_SESSIONS_PER_COMPUTER = 6;
70
+ function launchPolicyPath() {
71
+ return path.join((0, config_1.configDir)(), "launch-policy.json");
72
+ }
73
+ function isLaunchRuntime(value) {
74
+ return typeof value === "string" && exports.LAUNCH_RUNTIMES.includes(value);
75
+ }
76
+ /** The policy, or null when it is missing, unreadable or invalid in ANY part (offers nothing). */
77
+ function loadLaunchPolicy(file = launchPolicyPath()) {
78
+ let raw;
79
+ try {
80
+ raw = JSON.parse(fs.readFileSync(file, "utf8"));
81
+ }
82
+ catch {
83
+ // Missing is the ordinary not-set-up state, read on every poll; logging it would be noise.
84
+ return null;
85
+ }
86
+ const spots = raw?.spots;
87
+ if (!Array.isArray(spots) || spots.length > exports.MAX_SPOTS)
88
+ return null;
89
+ const clean = [];
90
+ for (const entry of spots) {
91
+ const spot = parseSpot(entry);
92
+ if (!spot || clean.some((s) => s.id === spot.id))
93
+ return null;
94
+ clean.push(spot);
95
+ }
96
+ return { spots: clean };
97
+ }
98
+ function saveLaunchPolicy(policy, file = launchPolicyPath()) {
99
+ fs.mkdirSync(path.dirname(file), { recursive: true });
100
+ fs.writeFileSync(file, `${JSON.stringify(policy, null, 2)}\n`, { mode: 0o600 });
101
+ fs.chmodSync(file, 0o600); // writeFile's mode applies only when it CREATES the file
102
+ }
103
+ /** Add `spot`, replacing any spot with the same id or the same folder. Pure. */
104
+ function upsertSpot(policy, spot) {
105
+ const others = (policy?.spots ?? []).filter((s) => s.id !== spot.id && s.path !== spot.path);
106
+ return { spots: [...others, spot] };
107
+ }
108
+ /** `policy` without the spot `id`. Removing a spot that is not there changes nothing. Pure. */
109
+ function removeSpot(policy, id) {
110
+ return { spots: (policy?.spots ?? []).filter((s) => s.id !== id) };
111
+ }
112
+ /** A spot id that is free in `policy`, derived from the folder's name. Pure. */
113
+ function suggestSpotId(policy, folder) {
114
+ const base = path.basename(folder).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 28) || "spot";
115
+ const taken = new Set((policy?.spots ?? []).map((s) => s.id));
116
+ if (!taken.has(base))
117
+ return base;
118
+ for (let n = 2;; n++)
119
+ if (!taken.has(`${base}-${n}`))
120
+ return `${base}-${n}`;
121
+ }
122
+ /**
123
+ * The spot's folder, checked NOW: still a real directory, still not `/` or home, and its realpath
124
+ * still the one recorded at `add`. Anything else (a component swapped for a symlink, a deleted
125
+ * folder, a hand-edited path with `..`) is refused rather than followed.
126
+ */
127
+ function resolveSpotFolder(spot) {
128
+ const now = (0, policy_1.validateNewRoot)(spot.path);
129
+ if (!now.ok)
130
+ return { ok: false, reason: now.reason };
131
+ if (now.path !== spot.path) {
132
+ return { ok: false, reason: `The folder for "${spot.label}" now resolves to somewhere else. Run \`baychat launch add\` again if that is intended.` };
133
+ }
134
+ return { ok: true, path: now.path };
135
+ }
136
+ function parseSpot(entry) {
137
+ if (!entry || typeof entry !== "object")
138
+ return null;
139
+ const { id, label, path: p, runtimes, maxSessions } = entry;
140
+ if (typeof id !== "string" || !exports.SPOT_ID_RE.test(id))
141
+ return null;
142
+ if (typeof label !== "string" || label.trim().length === 0 || label.length > exports.SPOT_LABEL_MAX)
143
+ return null;
144
+ // `add` stores a realpath, which is always absolute and normalised; anything else was hand-edited.
145
+ if (typeof p !== "string" || !path.isAbsolute(p) || path.normalize(p) !== p)
146
+ return null;
147
+ if (!Array.isArray(runtimes) || runtimes.length === 0 || !runtimes.every(isLaunchRuntime))
148
+ return null;
149
+ if (typeof maxSessions !== "number" || !Number.isInteger(maxSessions) || maxSessions < 1 || maxSessions > exports.MAX_SESSIONS_PER_COMPUTER)
150
+ return null;
151
+ return { id, label, path: p, runtimes: [...new Set(runtimes)], maxSessions };
152
+ }
@@ -0,0 +1,76 @@
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.isWsl = isWsl;
37
+ exports.machineName = machineName;
38
+ const fs = __importStar(require("node:fs"));
39
+ const os = __importStar(require("node:os"));
40
+ /** The server caps a device name at 60 characters (`createDeviceLink`). */
41
+ const MAX_NAME_LENGTH = 60;
42
+ const WSL_MARK = " (WSL)";
43
+ /**
44
+ * Is this process running inside WSL?
45
+ *
46
+ * `WSL_DISTRO_NAME` is set in every shell WSL opens; a process WSL started some other way (a
47
+ * systemd unit) may not have it, so the kernel's own name — "microsoft" in `/proc/version`
48
+ * on WSL 1 and 2 alike — is asked as well. Only on Linux: nothing else can be WSL.
49
+ */
50
+ function isWsl(platform = process.platform, env = process.env, procVersion = readProcVersion) {
51
+ if (platform !== "linux")
52
+ return false;
53
+ if (env.WSL_DISTRO_NAME)
54
+ return true;
55
+ return /microsoft/i.test(procVersion());
56
+ }
57
+ /**
58
+ * The name a login gives this computer, which is its identity on the server: a new login
59
+ * REPLACES the live login of the same name (one machine, one row). WSL reports the hostname
60
+ * Windows does, so a login from inside WSL is "<hostname> (WSL)" — otherwise Windows and WSL
61
+ * on one PC were one computer to BayChat, and each login signed the other out.
62
+ */
63
+ function machineName(hostname = os.hostname(), wsl = isWsl()) {
64
+ if (!wsl)
65
+ return hostname.slice(0, MAX_NAME_LENGTH);
66
+ return hostname.slice(0, MAX_NAME_LENGTH - WSL_MARK.length) + WSL_MARK;
67
+ }
68
+ /** `/proc/version`, or "" where there is none — not an error: only Linux has it. */
69
+ function readProcVersion() {
70
+ try {
71
+ return fs.readFileSync("/proc/version", "utf8");
72
+ }
73
+ catch {
74
+ return "";
75
+ }
76
+ }