baychat 0.11.4 → 0.13.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/README.md +50 -1
- package/dist/api.js +58 -0
- package/dist/approve-hook.js +425 -0
- package/dist/attachments.js +273 -0
- package/dist/commands.js +58 -3
- package/dist/index.js +72 -4
- package/dist/mcp-files.js +442 -0
- package/dist/mcp.js +198 -11
- package/dist/protocol-content.js +1 -1
- package/dist/relay/adapters.js +48 -5
- package/dist/relay/autostart.js +324 -0
- package/dist/relay/commands.js +80 -49
- package/dist/relay/daemon.js +1 -0
- package/dist/relay/socket.js +55 -6
- package/dist/runtimes.js +95 -10
- package/dist/tool-defs.js +212 -45
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure attachment helpers for the CLI — no network, no disk, no MCP.
|
|
3
|
+
//
|
|
4
|
+
// Everything here is a DECISION that the tool handlers around it depend on, kept
|
|
5
|
+
// separate so each one can be tested on its own and reused by the pieces that
|
|
6
|
+
// have nothing else in common:
|
|
7
|
+
//
|
|
8
|
+
// • `mimeForFile` — the stdio upload path, before it opens a socket.
|
|
9
|
+
// • `attachmentsFromMetadata` — the MCP message renderer AND the relay's wake
|
|
10
|
+
// prompt: two very different outputs, one reading of the wire format.
|
|
11
|
+
// • `filenameFromContentDisposition` — the download tool, which turns a header
|
|
12
|
+
// written by a server into a path on the caller's own disk.
|
|
13
|
+
//
|
|
14
|
+
// Zero dependencies on purpose (the package ships no MIME library and adds none).
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.ALLOWED_ATTACHMENT_EXTENSIONS = void 0;
|
|
50
|
+
exports.mimeForFile = mimeForFile;
|
|
51
|
+
exports.attachmentsFromMetadata = attachmentsFromMetadata;
|
|
52
|
+
exports.filenameFromContentDisposition = filenameFromContentDisposition;
|
|
53
|
+
exports.formatBytes = formatBytes;
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
// ─── Extension → MIME ───────────────────────────────────────────────────────
|
|
56
|
+
// The extensions covering the server's SECURE_MIME_ALLOWLIST
|
|
57
|
+
// (apps/api/src/lib/attachments.ts). This map is a fast local NO, never a yes:
|
|
58
|
+
// the server re-checks every upload, so an extension missing here costs a clear
|
|
59
|
+
// error instead of a wasted round trip, and one wrongly present costs a 400.
|
|
60
|
+
//
|
|
61
|
+
// The plain-text family is spelled out rather than left to `.txt` alone: the
|
|
62
|
+
// server accepts `text/plain` bytes whatever the file is called, and an agent's
|
|
63
|
+
// most ordinary attachment is a `.md` note or a `.log` excerpt it just wrote.
|
|
64
|
+
// Refusing those locally would be this map inventing a limit the server has not.
|
|
65
|
+
const EXTENSION_MIME = {
|
|
66
|
+
jpg: "image/jpeg",
|
|
67
|
+
jpeg: "image/jpeg",
|
|
68
|
+
png: "image/png",
|
|
69
|
+
gif: "image/gif",
|
|
70
|
+
webp: "image/webp",
|
|
71
|
+
pdf: "application/pdf",
|
|
72
|
+
doc: "application/msword",
|
|
73
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
74
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
75
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
76
|
+
txt: "text/plain",
|
|
77
|
+
md: "text/plain",
|
|
78
|
+
log: "text/plain",
|
|
79
|
+
json: "text/plain",
|
|
80
|
+
yaml: "text/plain",
|
|
81
|
+
yml: "text/plain",
|
|
82
|
+
csv: "text/csv",
|
|
83
|
+
zip: "application/zip",
|
|
84
|
+
};
|
|
85
|
+
/** Every extension an upload may carry, for the error message that names them. */
|
|
86
|
+
exports.ALLOWED_ATTACHMENT_EXTENSIONS = Object.keys(EXTENSION_MIME);
|
|
87
|
+
/**
|
|
88
|
+
* The MIME type to upload a local file as, from its extension alone — or
|
|
89
|
+
* `undefined` when the server would refuse it, so the caller errors before
|
|
90
|
+
* reading a byte. Never sniffs content: the server decides, this only spares an
|
|
91
|
+
* obviously-doomed request.
|
|
92
|
+
*/
|
|
93
|
+
function mimeForFile(filePath) {
|
|
94
|
+
// `path.extname` returns "" for a dotfile with no second dot (".zip"), which is
|
|
95
|
+
// exactly right — that file is named `.zip`, it is not a zip archive.
|
|
96
|
+
const ext = path.extname(filePath).replace(/^\./, "").toLowerCase();
|
|
97
|
+
return ext ? EXTENSION_MIME[ext] : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A URL we are willing to put in front of a model as "fetch this".
|
|
101
|
+
*
|
|
102
|
+
* Everything this module reads came off a message, and a message body is written
|
|
103
|
+
* by whoever sent it. The server is the layer that guarantees an `attachmentUrl`
|
|
104
|
+
* is one it signed — this is the second one, because the output of
|
|
105
|
+
* `attachmentsFromMetadata` becomes a `curl` line in the relay's wake prompt, and
|
|
106
|
+
* a prompt is a place where a newline or a quote is not a cosmetic problem.
|
|
107
|
+
*
|
|
108
|
+
* Refused: anything unparseable, any scheme but http(s), and any string carrying
|
|
109
|
+
* a control character (a newline would break out of the untrusted-messages block
|
|
110
|
+
* the prompt fences the message in; a NUL or an escape would not survive being
|
|
111
|
+
* printed intact anyway).
|
|
112
|
+
*/
|
|
113
|
+
function fetchableUrl(value) {
|
|
114
|
+
if (typeof value !== "string" || value.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
// eslint-disable-next-line no-control-regex
|
|
117
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
118
|
+
return null;
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = new URL(value);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
127
|
+
return null;
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
function itemFrom(value, fallbackType) {
|
|
131
|
+
if (!value || typeof value !== "object")
|
|
132
|
+
return null;
|
|
133
|
+
const o = value;
|
|
134
|
+
// The id is what makes the URL believable. The server mints an `attachmentUrl`
|
|
135
|
+
// only from an `attachmentId` it resolved to a row, so an entry that carries a
|
|
136
|
+
// URL and no id was not written by the server — it was written by a sender, and
|
|
137
|
+
// honouring it would hand the model an errand chosen by whoever sent the message.
|
|
138
|
+
const id = o.attachmentId;
|
|
139
|
+
if (typeof id !== "string" || id.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
const url = fetchableUrl(o.attachmentUrl);
|
|
142
|
+
// No signed URL means nothing to hand the model: an unsigned read path (the
|
|
143
|
+
// human-facing one) never adds it, and a line pointing nowhere is worse than
|
|
144
|
+
// no line at all.
|
|
145
|
+
if (url === null)
|
|
146
|
+
return null;
|
|
147
|
+
const type = typeof o.type === "string" && o.type ? o.type : undefined;
|
|
148
|
+
const out = {
|
|
149
|
+
attachmentUrl: url,
|
|
150
|
+
type: type ?? (typeof fallbackType === "string" && fallbackType ? fallbackType : "file"),
|
|
151
|
+
};
|
|
152
|
+
if (typeof o.mimeType === "string" && o.mimeType)
|
|
153
|
+
out.mimeType = o.mimeType;
|
|
154
|
+
if (typeof o.sizeBytes === "number" && Number.isFinite(o.sizeBytes)) {
|
|
155
|
+
out.sizeBytes = o.sizeBytes;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The attachments a message carries, in render order — from EITHER wire shape.
|
|
161
|
+
*
|
|
162
|
+
* `metadata.attachments[]` is the current one and wins; the legacy single
|
|
163
|
+
* (`attachmentId`/`attachmentUrl`, or an older `fileUrl` upload) is the fallback
|
|
164
|
+
* for a message written before the array existed, or by a client that still only
|
|
165
|
+
* writes the mirror. Both are read because the server dual-publishes: reading
|
|
166
|
+
* only the array would silently lose old messages, reading only the mirror would
|
|
167
|
+
* lose every file but the first.
|
|
168
|
+
*
|
|
169
|
+
* Pure and total — junk metadata yields `[]`, never a throw. Shared with the
|
|
170
|
+
* relay's wake prompt, so a change here changes what BOTH surfaces show.
|
|
171
|
+
*/
|
|
172
|
+
function attachmentsFromMetadata(metadata) {
|
|
173
|
+
if (!metadata || typeof metadata !== "object")
|
|
174
|
+
return [];
|
|
175
|
+
const m = metadata;
|
|
176
|
+
if (Array.isArray(m.attachments)) {
|
|
177
|
+
const items = m.attachments
|
|
178
|
+
.map((item) => itemFrom(item))
|
|
179
|
+
.filter((item) => item !== null);
|
|
180
|
+
// An array that yielded nothing fetchable falls through to the mirror rather
|
|
181
|
+
// than reporting "no attachments" for a message that plainly has one.
|
|
182
|
+
if (items.length > 0)
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
const legacy = itemFrom(m, m.type);
|
|
186
|
+
if (legacy)
|
|
187
|
+
return [legacy];
|
|
188
|
+
// Older uploads carried the file as a bare `fileUrl` with no attachment row —
|
|
189
|
+
// so there is no id to hold it to, and the URL check is all there is.
|
|
190
|
+
const fileUrl = fetchableUrl(m.fileUrl);
|
|
191
|
+
if (fileUrl !== null) {
|
|
192
|
+
const type = typeof m.type === "string" && m.type ? m.type : "file";
|
|
193
|
+
return [{ attachmentUrl: fileUrl, type }];
|
|
194
|
+
}
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
// ─── Content-Disposition → a filename we are willing to write ───────────────
|
|
198
|
+
/** Long enough for any real name, short enough to stay under every filesystem's
|
|
199
|
+
* limit once a de-duplication suffix is appended. */
|
|
200
|
+
const MAX_FILENAME_LENGTH = 120;
|
|
201
|
+
/**
|
|
202
|
+
* Reduce an arbitrary name to a BASENAME safe to create inside a chosen
|
|
203
|
+
* directory. The header is written by whatever answered the request, and the
|
|
204
|
+
* result becomes a real path on the caller's disk, so this is a security
|
|
205
|
+
* boundary, not tidying: path separators, `..`, control characters and quotes
|
|
206
|
+
* are removed, and anything left empty becomes `download`.
|
|
207
|
+
*/
|
|
208
|
+
function sanitizeDownloadName(raw) {
|
|
209
|
+
// Basename first: `../../etc/passwd` must become `passwd`, never a traversal.
|
|
210
|
+
const base = raw.split(/[\\/]/).pop() ?? "";
|
|
211
|
+
const cleaned = base
|
|
212
|
+
// Control characters and quotes: a name must not carry an unprintable
|
|
213
|
+
// byte onto disk or break out of the line it will be printed in.
|
|
214
|
+
.replace(/[\u0000-\u001f\u007f"]/g, "")
|
|
215
|
+
// Trimmed BEFORE the leading dots are stripped, or a padded name climbs: the
|
|
216
|
+
// RFC 5987 form `%20%2e%2e` decodes to " ..", whose first character is a
|
|
217
|
+
// space, so a strip-then-trim would leave `..` standing.
|
|
218
|
+
.trim()
|
|
219
|
+
.replace(/^\.+/, "") // ".." / ".hidden" — never a name that hides or climbs
|
|
220
|
+
.trim();
|
|
221
|
+
if (!cleaned)
|
|
222
|
+
return "download";
|
|
223
|
+
return cleaned.slice(0, MAX_FILENAME_LENGTH);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The filename a download should be written under, from a `Content-Disposition`
|
|
227
|
+
* header — sanitized. Prefers the RFC 5987 `filename*=UTF-8''…` form (the only
|
|
228
|
+
* one that can carry non-ASCII), then a quoted or bare `filename=`.
|
|
229
|
+
*
|
|
230
|
+
* Always returns a usable name: a missing, empty or nameless header yields
|
|
231
|
+
* `"download"` rather than failing the transfer over a cosmetic detail.
|
|
232
|
+
*/
|
|
233
|
+
function filenameFromContentDisposition(header) {
|
|
234
|
+
if (!header)
|
|
235
|
+
return "download";
|
|
236
|
+
const extended = /filename\*\s*=\s*([^;]+)/i.exec(header);
|
|
237
|
+
if (extended) {
|
|
238
|
+
// `UTF-8''name` / `iso-8859-1'en'name` — the value after the second quote.
|
|
239
|
+
const value = extended[1].trim().replace(/^[^']*'[^']*'/, "");
|
|
240
|
+
try {
|
|
241
|
+
// Only accepted when it survives sanitation as a real name; otherwise the
|
|
242
|
+
// plain `filename=` (often an ASCII fallback of the same file) gets its turn.
|
|
243
|
+
const decoded = sanitizeDownloadName(decodeURIComponent(value));
|
|
244
|
+
if (decoded !== "download")
|
|
245
|
+
return decoded;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// A malformed percent-escape is not fatal: fall through to `filename=`.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const plain = /filename\s*=\s*("([^"]*)"|[^;]+)/i.exec(header);
|
|
252
|
+
if (plain)
|
|
253
|
+
return sanitizeDownloadName((plain[2] ?? plain[1] ?? "").trim());
|
|
254
|
+
return "download";
|
|
255
|
+
}
|
|
256
|
+
// ─── Sizes ──────────────────────────────────────────────────────────────────
|
|
257
|
+
const SIZE_UNITS = ["B", "KB", "MB", "GB"];
|
|
258
|
+
/** A short human size for a render line ("12 KB", "1.5 KB", "5 MB"). Total: a
|
|
259
|
+
* negative or non-finite input renders as `0 B` rather than as nonsense. */
|
|
260
|
+
function formatBytes(bytes) {
|
|
261
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
262
|
+
return "0 B";
|
|
263
|
+
let value = bytes;
|
|
264
|
+
let unit = 0;
|
|
265
|
+
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
|
|
266
|
+
value /= 1024;
|
|
267
|
+
unit += 1;
|
|
268
|
+
}
|
|
269
|
+
// One decimal only where it carries information: "1.5 KB" is useful, "1.5 B"
|
|
270
|
+
// is not, and "12.3 KB" is more precision than a chat line needs.
|
|
271
|
+
const rounded = unit === 0 || value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
272
|
+
return `${rounded} ${SIZE_UNITS[unit]}`;
|
|
273
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -12,6 +12,8 @@ exports.cmdContext = cmdContext;
|
|
|
12
12
|
exports.cmdSummary = cmdSummary;
|
|
13
13
|
exports.cmdOnboard = cmdOnboard;
|
|
14
14
|
exports.cmdSend = cmdSend;
|
|
15
|
+
exports.cmdTyping = cmdTyping;
|
|
16
|
+
exports.cmdReact = cmdReact;
|
|
15
17
|
exports.resetSessionState = resetSessionState;
|
|
16
18
|
exports.cmdCheck = cmdCheck;
|
|
17
19
|
exports.cmdWatch = cmdWatch;
|
|
@@ -243,10 +245,63 @@ async function cmdOnboard(conversationId, opts = {}) {
|
|
|
243
245
|
}
|
|
244
246
|
}
|
|
245
247
|
}
|
|
246
|
-
async function cmdSend(conversationId, text) {
|
|
248
|
+
async function cmdSend(conversationId, text, replyToMessageId) {
|
|
247
249
|
const creds = requireCredentials();
|
|
248
|
-
const message = await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/messages`, {
|
|
249
|
-
|
|
250
|
+
const message = await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/messages`, {
|
|
251
|
+
content: text,
|
|
252
|
+
// Left out entirely when absent — see handleSendMessage for why an explicit null is wrong.
|
|
253
|
+
...(replyToMessageId ? { replyToMessageId } : {}),
|
|
254
|
+
});
|
|
255
|
+
console.log(replyToMessageId
|
|
256
|
+
? `Sent ${message.id} at ${message.createdAt}, replying to ${replyToMessageId}.`
|
|
257
|
+
: `Sent ${message.id} at ${message.createdAt}`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* `baychat typing <conversationId>` — show the typing indicator while this agent
|
|
261
|
+
* works, for CLI-driven agents that have no MCP client.
|
|
262
|
+
*
|
|
263
|
+
* ONE CALL, at the start of a turn. The server entry carries a few-second TTL and
|
|
264
|
+
* a sweep clears it, so there is deliberately no `baychat typing --stop`: an
|
|
265
|
+
* agent that dies mid-turn stops appearing to type without anyone cleaning up
|
|
266
|
+
* after it. The printed line says so, because a caller told only "typing on"
|
|
267
|
+
* would reasonably wrap this in a `while` loop — which is exactly the heartbeat
|
|
268
|
+
* this design exists to avoid.
|
|
269
|
+
*/
|
|
270
|
+
async function cmdTyping(conversationId) {
|
|
271
|
+
const creds = requireCredentials();
|
|
272
|
+
await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/typing`);
|
|
273
|
+
console.log("Typing shown — it lapses on its own in a few seconds. Nothing to stop.");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* `baychat react <conversationId> <messageId> <emoji>` — acknowledge one message,
|
|
277
|
+
* for CLI-driven agents that have no MCP client.
|
|
278
|
+
*
|
|
279
|
+
* The counterpart to `baychat typing`, and deliberately the opposite of it: typing
|
|
280
|
+
* lapses in seconds and says "alive right now", while this STAYS on the message
|
|
281
|
+
* and says "I read this one, and I am on it". A task that runs for minutes needs
|
|
282
|
+
* the second one — by the time it finishes, the first has long since cleared and
|
|
283
|
+
* the person waiting cannot tell work from a crash.
|
|
284
|
+
*
|
|
285
|
+
* One reaction per agent per message: calling again replaces it (👀 while working,
|
|
286
|
+
* ✅ when done), and calling twice with the same emoji changes nothing. `--remove`
|
|
287
|
+
* takes it back and succeeds even if there was nothing to take back.
|
|
288
|
+
*/
|
|
289
|
+
async function cmdReact(conversationId, messageId, emoji, opts = {}) {
|
|
290
|
+
const creds = requireCredentials();
|
|
291
|
+
const path = `/api/agent-api/conversations/${conversationId}/messages/${messageId}/reaction`;
|
|
292
|
+
if (opts.remove) {
|
|
293
|
+
await (0, api_1.apiRequest)(creds, "DELETE", path);
|
|
294
|
+
console.log(`Reaction removed from ${messageId}.`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const trimmed = emoji?.trim();
|
|
298
|
+
if (!trimmed) {
|
|
299
|
+
throw new Error("Usage: baychat react <conversationId> <messageId> <emoji> (👀 for 'seen')");
|
|
300
|
+
}
|
|
301
|
+
const result = await (0, api_1.apiRequest)(creds, "PUT", path, {
|
|
302
|
+
emoji: trimmed,
|
|
303
|
+
});
|
|
304
|
+
console.log(`Reacted ${result.emoji} to ${messageId} — it stays on the message until you change it.`);
|
|
250
305
|
}
|
|
251
306
|
async function agentNameMap(creds) {
|
|
252
307
|
try {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commands_1 = require("./commands");
|
|
5
|
+
const approve_hook_1 = require("./approve-hook");
|
|
5
6
|
const connect_1 = require("./connect");
|
|
6
7
|
const mcp_1 = require("./mcp");
|
|
7
8
|
const mcp_config_1 = require("./mcp-config");
|
|
@@ -29,6 +30,16 @@ Usage:
|
|
|
29
30
|
baychat whoami Show the connected agent identity
|
|
30
31
|
baychat conversations List conversations this agent is in
|
|
31
32
|
baychat send <conversationId> <text> Send a message
|
|
33
|
+
baychat typing <conversationId> Show the typing indicator while you work, so a long
|
|
34
|
+
tool call doesn't look like a crash. ONE call at the
|
|
35
|
+
start of a turn — it lapses on its own after a few
|
|
36
|
+
seconds, so there is nothing to stop and no loop to run
|
|
37
|
+
baychat react <conversationId> <messageId> <emoji> [--remove]
|
|
38
|
+
Acknowledge one message so the person who sent it
|
|
39
|
+
knows you have read it and are on it. Unlike typing,
|
|
40
|
+
the reaction STAYS on the message — use 👀 the moment
|
|
41
|
+
you pick up work that will take more than a few
|
|
42
|
+
seconds. Reacting again replaces it; --remove clears it
|
|
32
43
|
baychat check <conversationId> Print messages since the last check
|
|
33
44
|
baychat context <conversationId> Show the roster + the group's agent instructions
|
|
34
45
|
baychat summary <conversationId> [--refresh]
|
|
@@ -65,6 +76,16 @@ Usage:
|
|
|
65
76
|
came from, so "we can wake this headlessly"
|
|
66
77
|
is a claim you can check
|
|
67
78
|
baychat relay stop Stop the relay and disable it at boot
|
|
79
|
+
baychat approve-hook [--session <name>] [--timeout <sec>]
|
|
80
|
+
Claude Code PermissionRequest hook: send the
|
|
81
|
+
permission prompt to BayChat as a decision card
|
|
82
|
+
and answer it from your phone. Reads the hook
|
|
83
|
+
JSON on stdin, writes ONE decision object to
|
|
84
|
+
stdout, and always exits 0. FAILS CLOSED — every
|
|
85
|
+
error denies. NOT enabled by default; see
|
|
86
|
+
docs/features/REMOTE_APPROVAL_HOOK.md before
|
|
87
|
+
switching it on, because it moves the last line
|
|
88
|
+
between an agent and this machine onto a phone
|
|
68
89
|
baychat relay attach --session <name> [--runtime claude|codex|hermes]
|
|
69
90
|
[--resume-id <id>] [--timeout <sec>]
|
|
70
91
|
Register this session with the relay and block
|
|
@@ -132,11 +153,40 @@ async function main() {
|
|
|
132
153
|
await (0, commands_1.cmdConversations)();
|
|
133
154
|
return 0;
|
|
134
155
|
case "send": {
|
|
135
|
-
|
|
156
|
+
// `--reply-to <id>` is pulled out before the positionals, so the message text can
|
|
157
|
+
// contain anything — including a word that looks like a flag.
|
|
158
|
+
const flagAt = args.indexOf("--reply-to");
|
|
159
|
+
let replyToMessageId;
|
|
160
|
+
let rest = args;
|
|
161
|
+
if (flagAt !== -1) {
|
|
162
|
+
replyToMessageId = args[flagAt + 1];
|
|
163
|
+
if (!replyToMessageId)
|
|
164
|
+
throw new Error("--reply-to needs a message id.");
|
|
165
|
+
rest = [...args.slice(0, flagAt), ...args.slice(flagAt + 2)];
|
|
166
|
+
}
|
|
167
|
+
const [conversationId, ...words] = rest;
|
|
136
168
|
if (!conversationId || words.length === 0) {
|
|
137
|
-
throw new Error("Usage: baychat send <conversationId> <text>");
|
|
169
|
+
throw new Error("Usage: baychat send [--reply-to <messageId>] <conversationId> <text>");
|
|
170
|
+
}
|
|
171
|
+
await (0, commands_1.cmdSend)(conversationId, words.join(" "), replyToMessageId);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
case "typing": {
|
|
175
|
+
if (!args[0])
|
|
176
|
+
throw new Error("Usage: baychat typing <conversationId>");
|
|
177
|
+
await (0, commands_1.cmdTyping)(args[0]);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
case "react": {
|
|
181
|
+
// `--remove` needs no emoji, so the positionals are read from the flag-free
|
|
182
|
+
// list rather than by index: `react c1 m1 --remove` must not read "--remove"
|
|
183
|
+
// as the emoji and try to store it.
|
|
184
|
+
const [conversationId, messageId, emoji] = args.filter((a) => !a.startsWith("--"));
|
|
185
|
+
const remove = args.includes("--remove");
|
|
186
|
+
if (!conversationId || !messageId || (!emoji && !remove)) {
|
|
187
|
+
throw new Error("Usage: baychat react <conversationId> <messageId> <emoji> [--remove]");
|
|
138
188
|
}
|
|
139
|
-
await (0, commands_1.
|
|
189
|
+
await (0, commands_1.cmdReact)(conversationId, messageId, emoji, { remove });
|
|
140
190
|
return 0;
|
|
141
191
|
}
|
|
142
192
|
case "check": {
|
|
@@ -211,6 +261,12 @@ async function main() {
|
|
|
211
261
|
throw new Error("Usage: baychat relay <start|status|stop|attach> [options]");
|
|
212
262
|
}
|
|
213
263
|
}
|
|
264
|
+
case "approve-hook":
|
|
265
|
+
// Never throws and always returns 0 — see the file header in approve-hook.ts. A thrown
|
|
266
|
+
// error here would reach main()'s catch, print to stderr and exit 1, and to Claude Code an
|
|
267
|
+
// exit-1 hook with no JSON on stdout is a NON-BLOCKING error: the tool call proceeds. The
|
|
268
|
+
// guard in the catch below covers that anyway.
|
|
269
|
+
return await (0, approve_hook_1.cmdApproveHook)(args);
|
|
214
270
|
case "connect": {
|
|
215
271
|
// A bare `connect` prints the client menu; positional() skips a leading flag
|
|
216
272
|
// so `connect --base x codex` still finds the client.
|
|
@@ -244,6 +300,18 @@ async function main() {
|
|
|
244
300
|
main()
|
|
245
301
|
.then((code) => process.exit(code))
|
|
246
302
|
.catch((err) => {
|
|
247
|
-
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
console.error(message);
|
|
305
|
+
// The last fail-closed guard. For every other command, exit 1 is the right answer. For
|
|
306
|
+
// `approve-hook` it is the WRONG one twice over: Claude Code treats a non-zero exit with no
|
|
307
|
+
// JSON on stdout as a non-blocking error and lets the tool call through, so a crash in the
|
|
308
|
+
// approval gate would silently grant the permission it exists to withhold. Print a deny and
|
|
309
|
+
// exit 0 instead. Deliberately not sharing main()'s dispatch — this must still hold if the
|
|
310
|
+
// failure happened while parsing argv, or inside an import.
|
|
311
|
+
if (process.argv[2] === "approve-hook") {
|
|
312
|
+
process.stdout.write(`${JSON.stringify((0, approve_hook_1.denyDecision)(`Denied by BayChat remote approval: the approval gate crashed (${message}). ` +
|
|
313
|
+
`This gate fails closed, so the call was refused rather than allowed.`))}\n`);
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
248
316
|
process.exit(1);
|
|
249
317
|
});
|