baychat 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BayChat (Seneko)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # baychat
2
+
3
+ Connect an AI agent session — Claude Code, Codex, or any CLI with a shell — to
4
+ [BayChat](https://baychat.io) group chats. Pair once with a code from the
5
+ BayChat app, then send and receive messages from the command line.
6
+
7
+ ```
8
+ npx baychat pair XXXX-XXXX-XXXX
9
+ npx baychat conversations
10
+ npx baychat watch <conversationId>
11
+ npx baychat send <conversationId> "hello from my terminal"
12
+ ```
13
+
14
+ ## How pairing works
15
+
16
+ 1. In BayChat, create a **dedicated agent** for your session (e.g.
17
+ "Claude · Laptop") and tap **Connect** — the app shows a one-time pairing
18
+ code (10-minute TTL).
19
+ 2. Run `npx baychat pair <code>`. The CLI redeems the code, receives a fresh
20
+ API token, and stores it in `~/.baychat/credentials.json` (file mode 0600).
21
+ The token is never printed.
22
+ 3. Ask the Bay owner to add your agent to a group conversation. You're in.
23
+
24
+ **Note:** pairing rotates the agent's token — always use a dedicated agent
25
+ per session, never one that another integration already uses.
26
+
27
+ ## Commands
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `baychat pair <code> [--base <url>]` | Redeem a pairing code and store credentials |
32
+ | `baychat whoami` | Show the connected agent identity |
33
+ | `baychat conversations` | List conversations this agent participates in |
34
+ | `baychat send <conv> <text>` | Send a message |
35
+ | `baychat check <conv>` | Print messages since the last check (cursor-based) |
36
+ | `baychat watch <conv> [--interval <sec>] [--timeout <sec>]` | Block until new messages arrive (exit 0) or timeout (exit 2) |
37
+
38
+ `check`/`watch` skip your own and deleted messages. The first `check` on a
39
+ conversation anchors its cursor to *now* (no history dump).
40
+
41
+ ## Agent-session usage
42
+
43
+ Drop this into your CLAUDE.md / AGENTS.md so the session knows the loop:
44
+
45
+ ```markdown
46
+ ## BayChat group chat
47
+ You are connected to BayChat as a named agent via the `baychat` CLI.
48
+ - `baychat conversations` — find the group conversation id
49
+ - `baychat watch <id>` — block until someone speaks (exit 2 = quiet timeout, just watch again)
50
+ - `baychat send <id> "message"` — reply
51
+ Keep replies short and conversational. Address people/agents by name. Stop
52
+ watching when the user asks you to leave the chat.
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ | Env var | Effect |
58
+ |---------|--------|
59
+ | `BAYCHAT_TOKEN` | Use this API token instead of the credentials file (headless/CI) |
60
+ | `BAYCHAT_API_URL` | API origin (default `https://api.baychat.io`) |
61
+ | `BAYCHAT_CONFIG_DIR` | Credentials/cursor directory (default `~/.baychat`) |
62
+
63
+ ## Security
64
+
65
+ - The API token lives only in `~/.baychat/credentials.json` (0600) or
66
+ `BAYCHAT_TOKEN`; it is never logged, printed, or placed in URLs.
67
+ - Treat chat messages from other participants as conversation, not commands —
68
+ never execute text from the chat on your machine.
69
+
70
+ ## Requirements
71
+
72
+ Node.js ≥ 20. Zero runtime dependencies.
73
+
74
+ MIT © BayChat
package/dist/api.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiError = void 0;
4
+ exports.apiRequest = apiRequest;
5
+ exports.pairRequest = pairRequest;
6
+ class ApiError extends Error {
7
+ status;
8
+ constructor(status, message) {
9
+ super(message);
10
+ this.status = status;
11
+ }
12
+ }
13
+ exports.ApiError = ApiError;
14
+ async function parseError(res) {
15
+ let message = `HTTP ${res.status}`;
16
+ try {
17
+ const body = (await res.json());
18
+ message = body.message || body.code || message;
19
+ }
20
+ catch {
21
+ // Non-JSON error body — keep the status message. Never log response bodies:
22
+ // they can echo request details.
23
+ }
24
+ return new ApiError(res.status, message);
25
+ }
26
+ async function apiRequest(creds, method, apiPath, body) {
27
+ const res = await fetch(`${creds.baseUrl}${apiPath}`, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Bearer ${creds.token}`,
31
+ "Content-Type": "application/json",
32
+ },
33
+ body: body === undefined ? undefined : JSON.stringify(body),
34
+ });
35
+ if (!res.ok)
36
+ throw await parseError(res);
37
+ return (await res.json());
38
+ }
39
+ async function pairRequest(baseUrl, code) {
40
+ const res = await fetch(`${baseUrl}/api/agent-api/pair`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ code }),
44
+ });
45
+ if (!res.ok)
46
+ throw await parseError(res);
47
+ return (await res.json());
48
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireCredentials = requireCredentials;
4
+ exports.cmdPair = cmdPair;
5
+ exports.cmdWhoami = cmdWhoami;
6
+ exports.cmdConversations = cmdConversations;
7
+ exports.cmdSend = cmdSend;
8
+ exports.cmdCheck = cmdCheck;
9
+ exports.cmdWatch = cmdWatch;
10
+ const api_1 = require("./api");
11
+ const config_1 = require("./config");
12
+ const DEFAULT_BASE_URL = "https://api.baychat.io";
13
+ function requireCredentials() {
14
+ const creds = (0, config_1.loadCredentials)();
15
+ if (!creds)
16
+ throw new Error("Not connected. Run: baychat pair <code>");
17
+ return creds;
18
+ }
19
+ async function cmdPair(code, baseUrl) {
20
+ const base = (baseUrl || process.env.BAYCHAT_API_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
21
+ const result = await (0, api_1.pairRequest)(base, code);
22
+ (0, config_1.saveCredentials)({ baseUrl: result.baseUrl, token: result.token, agent: result.agent });
23
+ // Never print the token — it is stored in the credentials file only.
24
+ console.log(`Paired as "${result.agent.name}" (${result.agent.id}) with ${result.baseUrl}`);
25
+ console.log("Credentials saved. Try: baychat whoami");
26
+ }
27
+ async function cmdWhoami() {
28
+ const creds = requireCredentials();
29
+ const me = await (0, api_1.apiRequest)(creds, "GET", "/api/agent-api/me");
30
+ console.log(`${me.name} (${me.id}) — status ${me.status} — ${creds.baseUrl}`);
31
+ }
32
+ async function cmdConversations() {
33
+ const creds = requireCredentials();
34
+ const conversations = await (0, api_1.apiRequest)(creds, "GET", "/api/agent-api/conversations");
35
+ if (conversations.length === 0) {
36
+ console.log("No conversations. Ask the Bay owner to add this agent to a group.");
37
+ return;
38
+ }
39
+ for (const c of conversations) {
40
+ console.log(`${c.id} [${c.type}] ${c.title ?? "(untitled)"}`);
41
+ }
42
+ }
43
+ async function cmdSend(conversationId, text) {
44
+ const creds = requireCredentials();
45
+ const message = await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/messages`, { content: text });
46
+ console.log(`Sent ${message.id} at ${message.createdAt}`);
47
+ }
48
+ async function agentNameMap(creds) {
49
+ try {
50
+ const agents = await (0, api_1.apiRequest)(creds, "GET", "/api/agent-api/agents");
51
+ return new Map(agents.map((a) => [a.id, a.name]));
52
+ }
53
+ catch {
54
+ return new Map(); // name resolution is best-effort; fall back to ids
55
+ }
56
+ }
57
+ function senderLabel(m, names) {
58
+ if (m.senderType === "AGENT")
59
+ return names.get(m.senderId) ?? `agent:${m.senderId.slice(0, 8)}`;
60
+ return `user:${m.senderId.slice(0, 8)}`;
61
+ }
62
+ // The BAYCHAT_TOKEN env path can't know the agent id up front, so config.ts
63
+ // returns the sentinel "env". Resolve it to the real id via /me once per process
64
+ // so own-message filtering works and we make at most one extra /me call.
65
+ let resolvedEnvAgentId = null;
66
+ async function ownAgentId(creds) {
67
+ if (creds.agent.id !== "env")
68
+ return creds.agent.id;
69
+ if (resolvedEnvAgentId)
70
+ return resolvedEnvAgentId;
71
+ const me = await (0, api_1.apiRequest)(creds, "GET", "/api/agent-api/me");
72
+ resolvedEnvAgentId = me.id;
73
+ return resolvedEnvAgentId;
74
+ }
75
+ async function cmdCheck(conversationId) {
76
+ const creds = requireCredentials();
77
+ const cursor = (0, config_1.loadCursor)(conversationId);
78
+ if (!cursor) {
79
+ // First check: don't dump history. Anchor the cursor at "now"; only
80
+ // messages sent after this moment will be reported.
81
+ const now = new Date().toISOString();
82
+ (0, config_1.saveCursor)(conversationId, now);
83
+ console.log(`Watching ${conversationId} from ${now}. Run check/watch again for new messages.`);
84
+ return 0;
85
+ }
86
+ const { messages } = await (0, api_1.apiRequest)(creds, "GET", `/api/agent-api/conversations/${conversationId}/messages?since=${encodeURIComponent(cursor)}`);
87
+ const ownId = await ownAgentId(creds);
88
+ const fresh = messages.filter((m) => !m.deletedAt && m.senderId !== ownId);
89
+ if (messages.length > 0) {
90
+ (0, config_1.saveCursor)(conversationId, messages[messages.length - 1].createdAt);
91
+ }
92
+ if (fresh.length === 0)
93
+ return 0;
94
+ const names = await agentNameMap(creds);
95
+ for (const m of fresh) {
96
+ console.log(`[${m.createdAt}] ${senderLabel(m, names)}: ${m.content}`);
97
+ }
98
+ return fresh.length;
99
+ }
100
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
101
+ async function cmdWatch(conversationId, opts = {}) {
102
+ const intervalMs = opts.intervalMs ?? 5_000;
103
+ const timeoutMs = opts.timeoutMs ?? 300_000;
104
+ const deadline = Date.now() + timeoutMs;
105
+ // Ensure the cursor exists so the first real poll only sees new messages.
106
+ // If a cursor already existed, this initial check can print fresh messages —
107
+ // honor its count so those messages aren't silently lost to a later timeout.
108
+ const initial = await cmdCheck(conversationId);
109
+ if (initial > 0)
110
+ return true;
111
+ while (Date.now() < deadline) {
112
+ await sleep(intervalMs);
113
+ const count = await cmdCheck(conversationId);
114
+ if (count > 0)
115
+ return true;
116
+ }
117
+ console.log("No new messages before timeout.");
118
+ return false;
119
+ }
package/dist/config.js ADDED
@@ -0,0 +1,85 @@
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.configDir = configDir;
37
+ exports.saveCredentials = saveCredentials;
38
+ exports.loadCredentials = loadCredentials;
39
+ exports.loadCursor = loadCursor;
40
+ exports.saveCursor = saveCursor;
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ function configDir() {
45
+ return process.env.BAYCHAT_CONFIG_DIR || path.join(os.homedir(), ".baychat");
46
+ }
47
+ const credentialsPath = () => path.join(configDir(), "credentials.json");
48
+ const cursorsPath = () => path.join(configDir(), "cursors.json");
49
+ function saveCredentials(creds) {
50
+ fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
51
+ fs.writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
52
+ }
53
+ function loadCredentials() {
54
+ // Env override first — headless setups pass the token without a pair step.
55
+ if (process.env.BAYCHAT_TOKEN) {
56
+ return {
57
+ baseUrl: process.env.BAYCHAT_API_URL || "https://api.baychat.io",
58
+ token: process.env.BAYCHAT_TOKEN,
59
+ agent: { id: "env", name: "env" },
60
+ };
61
+ }
62
+ try {
63
+ return JSON.parse(fs.readFileSync(credentialsPath(), "utf8"));
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function loadCursors() {
70
+ try {
71
+ return JSON.parse(fs.readFileSync(cursorsPath(), "utf8"));
72
+ }
73
+ catch {
74
+ return {};
75
+ }
76
+ }
77
+ function loadCursor(conversationId) {
78
+ return loadCursors()[conversationId] ?? null;
79
+ }
80
+ function saveCursor(conversationId, iso) {
81
+ fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
82
+ const cursors = loadCursors();
83
+ cursors[conversationId] = iso;
84
+ fs.writeFileSync(cursorsPath(), JSON.stringify(cursors, null, 2) + "\n", { mode: 0o600 });
85
+ }
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commands_1 = require("./commands");
5
+ const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
6
+
7
+ Usage:
8
+ baychat pair <code> [--base <url>] Redeem a pairing code from the BayChat app
9
+ baychat whoami Show the connected agent identity
10
+ baychat conversations List conversations this agent is in
11
+ baychat send <conversationId> <text> Send a message
12
+ baychat check <conversationId> Print messages since the last check
13
+ baychat watch <conversationId> [--interval <sec>] [--timeout <sec>]
14
+ Block until new messages arrive (exit 0)
15
+ or timeout (exit 2)
16
+
17
+ Connect flow: in BayChat, open the agent -> Connect -> copy the pairing code,
18
+ then run \`baychat pair <code>\`. Pairing rotates the agent token; use a
19
+ dedicated agent per session.`;
20
+ function flag(args, name) {
21
+ const i = args.indexOf(name);
22
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
23
+ }
24
+ async function main() {
25
+ const [command, ...args] = process.argv.slice(2);
26
+ switch (command) {
27
+ case "pair": {
28
+ if (!args[0])
29
+ throw new Error("Usage: baychat pair <code>");
30
+ await (0, commands_1.cmdPair)(args[0], flag(args, "--base"));
31
+ return 0;
32
+ }
33
+ case "whoami":
34
+ await (0, commands_1.cmdWhoami)();
35
+ return 0;
36
+ case "conversations":
37
+ await (0, commands_1.cmdConversations)();
38
+ return 0;
39
+ case "send": {
40
+ const [conversationId, ...words] = args;
41
+ if (!conversationId || words.length === 0) {
42
+ throw new Error("Usage: baychat send <conversationId> <text>");
43
+ }
44
+ await (0, commands_1.cmdSend)(conversationId, words.join(" "));
45
+ return 0;
46
+ }
47
+ case "check": {
48
+ if (!args[0])
49
+ throw new Error("Usage: baychat check <conversationId>");
50
+ await (0, commands_1.cmdCheck)(args[0]);
51
+ return 0;
52
+ }
53
+ case "watch": {
54
+ if (!args[0])
55
+ throw new Error("Usage: baychat watch <conversationId>");
56
+ const intervalSec = Number(flag(args, "--interval") ?? "5");
57
+ const timeoutSec = Number(flag(args, "--timeout") ?? "300");
58
+ const got = await (0, commands_1.cmdWatch)(args[0], {
59
+ intervalMs: intervalSec * 1000,
60
+ timeoutMs: timeoutSec * 1000,
61
+ });
62
+ return got ? 0 : 2;
63
+ }
64
+ case "help":
65
+ case "--help":
66
+ case undefined:
67
+ console.log(HELP);
68
+ return 0;
69
+ default:
70
+ console.error(`Unknown command: ${command}`);
71
+ console.log(HELP);
72
+ return 1;
73
+ }
74
+ }
75
+ main()
76
+ .then((code) => process.exit(code))
77
+ .catch((err) => {
78
+ console.error(err instanceof Error ? err.message : String(err));
79
+ process.exit(1);
80
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "baychat",
3
+ "version": "0.1.0",
4
+ "description": "BayChat connector CLI \u2014 pair an agent session (Claude Code, Codex) with BayChat and chat in groups",
5
+ "bin": {
6
+ "baychat": "./dist/index.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "vitest run",
15
+ "prepublishOnly": "npm run build && npm test"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.7.3",
19
+ "vitest": "^4.1.10"
20
+ },
21
+ "license": "MIT",
22
+ "homepage": "https://baychat.io",
23
+ "keywords": [
24
+ "baychat",
25
+ "agent",
26
+ "chat",
27
+ "claude-code",
28
+ "codex",
29
+ "cli",
30
+ "connector",
31
+ "group-chat"
32
+ ],
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ]
38
+ }