baychat 0.29.8 → 0.30.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 +18 -8
- package/dist/approval-wait.js +130 -0
- package/dist/approve-hook.js +33 -132
- package/dist/cli-help.js +24 -2
- package/dist/client-paths.js +104 -13
- package/dist/connect.js +27 -13
- package/dist/credential-refresh.js +31 -14
- package/dist/denied/after-hook.js +40 -0
- package/dist/denied/grants.js +234 -0
- package/dist/denied/opt-in.js +68 -0
- package/dist/denied/permission-rule.js +183 -0
- package/dist/denied/settings-rules.js +160 -0
- package/dist/denied/transcript.js +150 -0
- package/dist/denied-hook.js +329 -0
- package/dist/doctor-command.js +3 -0
- package/dist/doctor.js +52 -2
- package/dist/help-topics.js +12 -4
- package/dist/hook-stdin.js +35 -0
- package/dist/index.js +13 -1
- package/dist/main.js +9 -0
- package/dist/permissions-command.js +87 -0
- package/dist/protocol-content.js +1 -1
- package/dist/relay/launcher/approval-settings.js +56 -2
- package/dist/runtimes.js +2 -2
- package/dist/tool-defs.js +3 -2
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -475,7 +475,11 @@ function runtimeReport(runtime, env) {
|
|
|
475
475
|
const spec = runtimes_1.RUNTIME_SPECS[runtime];
|
|
476
476
|
const mcp = mcpCheck(runtime, env);
|
|
477
477
|
const skill = skillCheck(runtime, env);
|
|
478
|
-
|
|
478
|
+
// A Claude Desktop that names BayChat in one of its files is connected, even when the
|
|
479
|
+
// file it reads does not — that is the half-finished install `mcpCheck` fails on, and it
|
|
480
|
+
// must be shown, not folded into "not configured".
|
|
481
|
+
const halfConnected = runtime === "desktop" && desktopConfigState(env).named.length > 0;
|
|
482
|
+
if (mcp.status === "fail" && skill.status !== "pass" && !halfConnected) {
|
|
479
483
|
return {
|
|
480
484
|
runtime,
|
|
481
485
|
label: spec.label,
|
|
@@ -587,10 +591,12 @@ function mcpCheck(runtime, env) {
|
|
|
587
591
|
? { name: "mcp", status: "pass", detail: `${command} lists baychat` }
|
|
588
592
|
: { name: "mcp", status: "fail", detail: `\`${command} mcp list\` does not list baychat`, remedy };
|
|
589
593
|
}
|
|
594
|
+
if (runtime === "desktop")
|
|
595
|
+
return desktopMcpCheck(env, remedy);
|
|
590
596
|
// No CLI to ask — a GUI client. Every location this client might read is
|
|
591
597
|
// checked, because "the file we would write" and "the file it opens" are not
|
|
592
598
|
// reliably the same question.
|
|
593
|
-
const paths = (0, client_paths_1.configPathsFor)(runtime,
|
|
599
|
+
const paths = (0, client_paths_1.configPathsFor)(runtime, pathEnvOf(env));
|
|
594
600
|
if (paths.length === 0) {
|
|
595
601
|
return { name: "mcp", status: "skip", detail: `no known config location on ${env.platform}` };
|
|
596
602
|
}
|
|
@@ -607,6 +613,50 @@ function mcpCheck(runtime, env) {
|
|
|
607
613
|
remedy,
|
|
608
614
|
};
|
|
609
615
|
}
|
|
616
|
+
function pathEnvOf(env) {
|
|
617
|
+
return env.pathEnv ?? { home: env.home, platform: env.platform };
|
|
618
|
+
}
|
|
619
|
+
/** Claude Desktop's config files here: all of them, the ones naming BayChat, and the Store copies. */
|
|
620
|
+
function desktopConfigState(env) {
|
|
621
|
+
const pathEnv = pathEnvOf(env);
|
|
622
|
+
const paths = (0, client_paths_1.inspectableConfigPaths)("desktop", pathEnv);
|
|
623
|
+
const named = paths.filter((p) => env.readText(p)?.includes("baychat"));
|
|
624
|
+
return { paths, named, store: (0, client_paths_1.claudeStoreConfigPaths)(pathEnv) };
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Claude Desktop's `mcp` check — a GUI app, so its config files are the only evidence.
|
|
628
|
+
*
|
|
629
|
+
* THE MICROSOFT STORE CASE. Claude from the Store reads its own virtualised copy
|
|
630
|
+
* (`%LOCALAPPDATA%\Packages\Claude_…\LocalCache\Roaming\Claude\`), so a BayChat entry
|
|
631
|
+
* in the classic `%APPDATA%\Claude` file — which is what `connect desktop` wrote before
|
|
632
|
+
* 0.29.9 — is a config that looks right and that the app never opens. That is a FAIL that
|
|
633
|
+
* names both files, not a pass on the one we happened to find.
|
|
634
|
+
*/
|
|
635
|
+
function desktopMcpCheck(env, remedy) {
|
|
636
|
+
const { paths, named, store } = desktopConfigState(env);
|
|
637
|
+
if (paths.length === 0) {
|
|
638
|
+
return { name: "mcp", status: "skip", detail: `no known config location on ${env.platform}` };
|
|
639
|
+
}
|
|
640
|
+
const label = (p) => `${display(p, env)}${(0, client_paths_1.isClaudeStoreConfigPath)(p) ? " (Microsoft Store)" : ""}`;
|
|
641
|
+
const missingStore = store.filter((p) => !named.includes(p));
|
|
642
|
+
if (named.length > 0 && missingStore.length > 0) {
|
|
643
|
+
return {
|
|
644
|
+
name: "mcp",
|
|
645
|
+
status: "fail",
|
|
646
|
+
detail: `baychat is in ${named.map(label).join(" and ")}, but not in ${missingStore.map(label).join(" or ")}` +
|
|
647
|
+
" — the copy Claude from the Microsoft Store reads",
|
|
648
|
+
remedy,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (named.length > 0)
|
|
652
|
+
return { name: "mcp", status: "pass", detail: named.map(label).join(" + ") };
|
|
653
|
+
return {
|
|
654
|
+
name: "mcp",
|
|
655
|
+
status: "fail",
|
|
656
|
+
detail: `no baychat entry in ${paths.map(label).join(" or ")}`,
|
|
657
|
+
remedy,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
610
660
|
/**
|
|
611
661
|
* Is the runtime's BayChat skill on disk?
|
|
612
662
|
*
|
package/dist/help-topics.js
CHANGED
|
@@ -136,10 +136,11 @@ A session is one terminal, named by you. It appears in the app as an agent your
|
|
|
136
136
|
messages can reach, and it survives being parked.
|
|
137
137
|
|
|
138
138
|
join_session({ session: "Session-A" }) → a 1:1 with you
|
|
139
|
-
join_session({ session: "Session-A", group: "Ad Review" }) → that group
|
|
139
|
+
join_session({ session: "Session-A", group: "Ad Review" }) → that group only
|
|
140
140
|
|
|
141
|
-
**
|
|
142
|
-
session name
|
|
141
|
+
**A group join creates no private chat.** If you already have one with this
|
|
142
|
+
session (same agent, same name), the result names it as directConversationId;
|
|
143
|
+
otherwise there is none until you start one.
|
|
143
144
|
|
|
144
145
|
list_sessions() → name, live or idle, last seen
|
|
145
146
|
end_session(...) → park it; the chat and its history survive, and rejoining
|
|
@@ -167,7 +168,14 @@ a decision that expires silently is worse than one that waits.
|
|
|
167
168
|
cards. It is on only for Claude Code sessions BayChat starts (from the phone,
|
|
168
169
|
\`baychat claude\` or \`baychat start\`), never for a plain \`claude\`, and it moves
|
|
169
170
|
the last line between an agent and your machine onto a phone — read
|
|
170
|
-
docs/features/REMOTE_APPROVAL_HOOK.md. It fails CLOSED: every error denies
|
|
171
|
+
docs/features/REMOTE_APPROVAL_HOOK.md. It fails CLOSED: every error denies.
|
|
172
|
+
|
|
173
|
+
When auto mode blocks a call outright there is no prompt to answer. With
|
|
174
|
+
\`baychat permissions on\` (off by default), BayChat-started sessions then put a
|
|
175
|
+
card in BayChat — Allow once / Always allow this exact command / No — and a yes
|
|
176
|
+
writes that one exact rule to the project's .claude/settings.local.json and lets
|
|
177
|
+
the session retry. \`baychat permissions\` lists and removes what it kept. See
|
|
178
|
+
docs/features/DENIED_COMMAND_CARD.md.`,
|
|
171
179
|
};
|
|
172
180
|
exports.HELP_TOPICS = [GROUPS, SESSIONS, TOOLS, APPROVALS];
|
|
173
181
|
/** Exact name first, then keyword, then a substring of the body. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A Claude Code hook's JSON input, read from stdin with a deadline. Imports nothing but Node, so
|
|
3
|
+
// the fast-path hooks (denied/after-hook.ts) can use it without loading the CLI.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.readStdin = readStdin;
|
|
6
|
+
/**
|
|
7
|
+
* The hook's JSON input.
|
|
8
|
+
*
|
|
9
|
+
* Bounded, because a stdin that never closes would stall us until the handler timeout kills the
|
|
10
|
+
* process — and for approve-hook being killed ALLOWS. On timeout we return what arrived (usually
|
|
11
|
+
* nothing), which fails to parse, which each hook turns into its own fail-closed answer.
|
|
12
|
+
*/
|
|
13
|
+
function readStdin(stream = process.stdin, timeoutMs = 10_000) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let text = "";
|
|
16
|
+
let done = false;
|
|
17
|
+
const finish = () => {
|
|
18
|
+
if (done)
|
|
19
|
+
return;
|
|
20
|
+
done = true;
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
resolve(text);
|
|
23
|
+
};
|
|
24
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
25
|
+
// Do not hold the process open on the timer alone.
|
|
26
|
+
if (typeof timer.unref === "function")
|
|
27
|
+
timer.unref();
|
|
28
|
+
stream.setEncoding?.("utf8");
|
|
29
|
+
stream.on("data", (chunk) => {
|
|
30
|
+
text += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
31
|
+
});
|
|
32
|
+
stream.on("end", finish);
|
|
33
|
+
stream.on("error", finish);
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
/** Kept in step with `FAST_CONTEXT_COMMANDS` in claude-context.ts (a test pins the two). */
|
|
13
13
|
const FAST_COMMANDS = new Set(["context-statusline", "context-hook", "context-flush"]);
|
|
14
14
|
const command = process.argv[2];
|
|
15
|
-
if (command
|
|
15
|
+
if (command === "denied-after") {
|
|
16
|
+
// The denied-command card's after-hook runs on every matching tool call (as an async hook), so
|
|
17
|
+
// it too must not load the CLI. It imports only its own files and Node.
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const after = require("./denied/after-hook");
|
|
20
|
+
after.cmdDeniedAfter().then((code) => {
|
|
21
|
+
process.exitCode = code;
|
|
22
|
+
}, (err) => {
|
|
23
|
+
process.stderr.write(`baychat denied-after: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
24
|
+
process.exitCode = 0;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (command !== undefined && FAST_COMMANDS.has(command)) {
|
|
16
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
29
|
const fast = require("./claude-context");
|
|
18
30
|
fast.runFastContextCommand(command, process.argv.slice(3)).then(
|
package/dist/main.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commands_1 = require("./commands");
|
|
5
5
|
const approve_hook_1 = require("./approve-hook");
|
|
6
|
+
const denied_hook_1 = require("./denied-hook");
|
|
7
|
+
const permissions_command_1 = require("./permissions-command");
|
|
6
8
|
const context_hooks_command_1 = require("./context-hooks-command");
|
|
7
9
|
const session_name_1 = require("./session-name");
|
|
8
10
|
const start_command_1 = require("./start-command");
|
|
@@ -301,6 +303,13 @@ async function main() {
|
|
|
301
303
|
// exit-1 hook with no JSON on stdout is a NON-BLOCKING error: the tool call proceeds. The
|
|
302
304
|
// guard in the catch below covers that anyway.
|
|
303
305
|
return await (0, approve_hook_1.cmdApproveHook)(args);
|
|
306
|
+
case "denied-hook":
|
|
307
|
+
// Never throws and always returns 0; prints `retry` or nothing (see denied-hook.ts). For
|
|
308
|
+
// this event Claude Code ignores the exit code, and printing nothing leaves the denial
|
|
309
|
+
// standing — so a crash here is already the safe outcome.
|
|
310
|
+
return await (0, denied_hook_1.cmdDeniedHook)(args);
|
|
311
|
+
case "permissions":
|
|
312
|
+
return await (0, permissions_command_1.cmdPermissions)(args);
|
|
304
313
|
case "context-hooks":
|
|
305
314
|
return await (0, context_hooks_command_1.cmdContextHooks)(args);
|
|
306
315
|
case "doctor":
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdPermissions = cmdPermissions;
|
|
4
|
+
// `baychat permissions` — the denied-command card's switch, and the rules it left on this computer.
|
|
5
|
+
//
|
|
6
|
+
// baychat permissions [list] what BayChat keeps (and any once-rule still waiting)
|
|
7
|
+
// baychat permissions remove <id> take one back out of its settings file
|
|
8
|
+
// baychat permissions on | off the card itself, for sessions BayChat starts from now on
|
|
9
|
+
// baychat permissions sweep (internal) take back once-rules whose time is up
|
|
10
|
+
//
|
|
11
|
+
// Only rules BayChat wrote are listed or removed. A rule the person wrote themselves never
|
|
12
|
+
// appears here, because a rule that was already in the file was never recorded (denied/grants.ts).
|
|
13
|
+
const grants_1 = require("./denied/grants");
|
|
14
|
+
const opt_in_1 = require("./denied/opt-in");
|
|
15
|
+
/** The longest a scheduled sweep may sleep: a once-rule's TTL is minutes, never hours. */
|
|
16
|
+
const MAX_SWEEP_DELAY_MS = 60 * 60_000;
|
|
17
|
+
const USAGE = "Usage: baychat permissions [list | remove <id> | on | off]";
|
|
18
|
+
async function cmdPermissions(args, deps = {}) {
|
|
19
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
20
|
+
const err = deps.err ?? ((line) => console.error(line));
|
|
21
|
+
const [sub = "list", ...rest] = args;
|
|
22
|
+
switch (sub) {
|
|
23
|
+
case "list": {
|
|
24
|
+
out((0, opt_in_1.deniedCardEnabled)()
|
|
25
|
+
? "The denied-command card is ON for sessions BayChat starts (`baychat permissions off` turns it off)."
|
|
26
|
+
: "The denied-command card is OFF (`baychat permissions on` turns it on).");
|
|
27
|
+
const grants = (0, grants_1.listGrants)();
|
|
28
|
+
if (grants.length === 0) {
|
|
29
|
+
out("BayChat keeps no permission rules on this computer.");
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
out("");
|
|
33
|
+
for (const grant of grants)
|
|
34
|
+
out(describeGrant(grant));
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
case "remove": {
|
|
38
|
+
const id = rest[0];
|
|
39
|
+
if (!id) {
|
|
40
|
+
err(USAGE);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (!(await (0, grants_1.revokeGrant)(id))) {
|
|
44
|
+
err(`No rule with id ${id}. \`baychat permissions list\` shows them.`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
out(`Removed ${id}. Claude Code reloads its settings, so running sessions stop allowing it too.`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
case "on": {
|
|
51
|
+
// A deliberate act by a person: the card lets a phone add rules on this computer, so a
|
|
52
|
+
// session's own shell (no terminal) cannot switch it on for itself.
|
|
53
|
+
if (!(deps.isInteractive ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()) {
|
|
54
|
+
err("`baychat permissions on` must be run by you, in a terminal.");
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
(0, opt_in_1.enableDeniedCard)();
|
|
58
|
+
out("The denied-command card is ON. Sessions BayChat starts from now on will put a card in BayChat when");
|
|
59
|
+
out("Claude Code's auto mode blocks a call. Read docs/features/DENIED_COMMAND_CARD.md for what it can and can't allow.");
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
case "off":
|
|
63
|
+
(0, opt_in_1.disableDeniedCard)();
|
|
64
|
+
out("The denied-command card is OFF for sessions BayChat starts from now on. Rules it kept stay until removed.");
|
|
65
|
+
return 0;
|
|
66
|
+
case "sweep": {
|
|
67
|
+
const now = deps.now ?? Date.now;
|
|
68
|
+
const at = Number(rest[rest.indexOf("--at") + 1]);
|
|
69
|
+
if (rest.includes("--at") && Number.isFinite(at)) {
|
|
70
|
+
const delay = Math.min(Math.max(at - now(), 0), MAX_SWEEP_DELAY_MS);
|
|
71
|
+
await (deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(delay + 1000);
|
|
72
|
+
}
|
|
73
|
+
await (0, grants_1.sweepExpiredGrants)(now());
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
err(USAGE);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function describeGrant(grant) {
|
|
82
|
+
const when = grant.kind === "once" ? `once, until ${grant.expiresAt ?? "?"}` : `always, since ${grant.addedAt}`;
|
|
83
|
+
// A once-rule can carry a secret from the command (it is never offered as "always" then), so it
|
|
84
|
+
// is listed as the card showed it, masked; an always-rule never holds one and is listed as written.
|
|
85
|
+
const rules = grant.kind === "once" ? grant.shown : grant.rules.length ? grant.rules.join(", ") : "(the rule was already there)";
|
|
86
|
+
return `${grant.id} ${rules}\n ${when} · ${grant.settingsFile}${grant.session ? ` · ${grant.session}` : ""}`;
|
|
87
|
+
}
|