rogerthat 1.24.3 → 1.24.4

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/dist/cli.js CHANGED
@@ -10,10 +10,41 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { dirname, join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { parseArgs } from "node:util";
14
- import { runListenHere } from "./listen-here.js";
15
- import { runReceiveRecipe } from "./receive-recipe.js";
13
+ // NOTE: `parseArgs` (node:util), `./listen-here.js` and `./receive-recipe.js`
14
+ // are imported DYNAMICALLY inside main(), AFTER requireModernNode(). They (and
15
+ // listen-here's own `node:util` import) hard-fail at module-load time on Node
16
+ // < 18.3 with a cryptic `SyntaxError: 'node:util' does not provide an export
17
+ // 'parseArgs'`. Loading them statically would crash before our friendly version
18
+ // check could run. Keep the top of this file dependent only on Node-16-safe
19
+ // builtins so the guard below is what the user actually sees.
16
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ /** Hard-require Node >= 18.3 (parseArgs landed there; see package.json engines).
22
+ * Prints a clear message instead of the cryptic parseArgs SyntaxError. */
23
+ function requireModernNode() {
24
+ const [maj = 0, min = 0] = process.versions.node.split(".").map((n) => Number(n));
25
+ if (maj > 18 || (maj === 18 && min >= 3))
26
+ return;
27
+ console.error(`rogerthat requires Node >= 18.3 — found v${process.versions.node}.\n` +
28
+ `Upgrade Node (e.g. \`nvm install 20 && nvm use 20\`, or https://nodejs.org) and retry.`);
29
+ process.exit(1);
30
+ }
31
+ // Load .env (no dependency on dotenv). Tries repo-root .env relative to this file.
32
+ (function loadDotEnv() {
33
+ // dist/cli.js -> repo root; src/cli.ts -> repo root (one up from src)
34
+ const candidates = [join(__dirname, "..", ".env"), join(__dirname, "..", "..", ".env")];
35
+ for (const path of candidates) {
36
+ if (!existsSync(path))
37
+ continue;
38
+ const content = readFileSync(path, "utf-8");
39
+ for (const line of content.split("\n")) {
40
+ const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
41
+ if (m && !process.env[m[1]]) {
42
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
43
+ }
44
+ }
45
+ break;
46
+ }
47
+ })();
17
48
  let PKG_VERSION = "?";
18
49
  try {
19
50
  PKG_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")).version;
@@ -63,17 +94,24 @@ function isLocalHost(host) {
63
94
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
64
95
  }
65
96
  async function main() {
97
+ // Friendly Node-version gate BEFORE any module that imports node:util's
98
+ // parseArgs is loaded (this file, listen-here, receive-recipe all do).
99
+ requireModernNode();
66
100
  // Subcommand dispatch: anything before flags. Detect by argv[2] being a
67
- // non-flag word.
101
+ // non-flag word. Dynamic import so listen-here/receive-recipe (and their
102
+ // parseArgs dependency) only load after the version gate above.
68
103
  const first = process.argv[2];
69
104
  if (first === "listen-here") {
105
+ const { runListenHere } = await import("./listen-here.js");
70
106
  const code = await runListenHere(process.argv.slice(3));
71
107
  process.exit(code);
72
108
  }
73
109
  if (first === "receive-recipe") {
110
+ const { runReceiveRecipe } = await import("./receive-recipe.js");
74
111
  const code = runReceiveRecipe(process.argv.slice(3));
75
112
  process.exit(code);
76
113
  }
114
+ const { parseArgs } = await import("node:util");
77
115
  let parsed;
78
116
  try {
79
117
  parsed = parseArgs({
@@ -24,14 +24,20 @@ import { parseArgs } from "node:util";
24
24
  const HELP = `rogerthat listen-here — open an SSE receiver for a channel
25
25
 
26
26
  usage:
27
+ rogerthat listen-here --channel <id> --token <t> --identity-key <k> [options]
27
28
  rogerthat listen-here --channel <id> --token <t> --session <sid> [options]
28
29
 
29
30
  required:
30
31
  --channel <id> channel id (returned by /join or create_channel)
31
32
  --token <t> channel bearer token
32
- --session <sid> X-Session-Id from /join (the calling agent's session)
33
+ and ONE of:
34
+ --identity-key <k> auto-join the channel with this identity_key to obtain a
35
+ session (recommended — makes this command self-contained)
36
+ --session <sid> reuse an X-Session-Id you already got from /join
33
37
 
34
38
  options:
39
+ --owner-password <p> passed to the auto-join so the session is marked
40
+ human-authorized (only meaningful with --identity-key)
35
41
  --origin <url> RogerThat origin (default: https://rogerthat.chat)
36
42
  --since <msg_id> resume from a known message id (skips per-session cursor)
37
43
  --on-message <cmd> shell command to run for each delivered message; env vars
@@ -102,6 +108,8 @@ function parseFlags(argv) {
102
108
  channel: { type: "string" },
103
109
  token: { type: "string" },
104
110
  session: { type: "string" },
111
+ "identity-key": { type: "string" },
112
+ "owner-password": { type: "string" },
105
113
  origin: { type: "string" },
106
114
  since: { type: "string" },
107
115
  "on-message": { type: "string" },
@@ -123,8 +131,13 @@ function parseFlags(argv) {
123
131
  const channel = parsed.values.channel;
124
132
  const token = parsed.values.token;
125
133
  const session = parsed.values.session;
126
- if (!channel || !token || !session) {
127
- return { error: "missing required flag(s): --channel, --token, --session" };
134
+ const identityKey = parsed.values["identity-key"];
135
+ if (!channel || !token) {
136
+ return { error: "missing required flag(s): --channel, --token" };
137
+ }
138
+ // Either an explicit --session, or --identity-key to auto-join for one.
139
+ if (!session && !identityKey) {
140
+ return { error: "provide --session <sid> OR --identity-key <key> (auto-joins to get a session)" };
128
141
  }
129
142
  let since;
130
143
  if (parsed.values.since !== undefined) {
@@ -151,7 +164,9 @@ function parseFlags(argv) {
151
164
  return {
152
165
  channel,
153
166
  token,
154
- session,
167
+ session: session ?? "",
168
+ identityKey,
169
+ ownerPassword: parsed.values["owner-password"],
155
170
  origin: parsed.values.origin ?? "https://rogerthat.chat",
156
171
  since,
157
172
  onMessage: parsed.values["on-message"],
@@ -428,6 +443,55 @@ async function runOneConnection(args, sinceCursor, abortSignal) {
428
443
  }
429
444
  return { lastId, reason: "ended" };
430
445
  }
446
+ /** Auto-join the channel with an identity_key to obtain a session_id, so the
447
+ * receiver command can be a single self-contained line (no separate /join,
448
+ * no `<SID>` placeholder). Uses node:http/https directly (Node-16-safe, same
449
+ * as the SSE path). Returns the session_id or throws with a clear message. */
450
+ function joinForSession(args) {
451
+ const url = new URL(`${args.origin.replace(/\/$/, "")}/api/channels/${args.channel}/join`);
452
+ const payload = JSON.stringify({
453
+ identity_key: args.identityKey,
454
+ ...(args.ownerPassword ? { owner_password: args.ownerPassword } : {}),
455
+ });
456
+ return new Promise((resolve, reject) => {
457
+ const reqFn = url.protocol === "https:" ? httpsRequest : httpRequest;
458
+ const req = reqFn({
459
+ method: "POST",
460
+ host: url.hostname,
461
+ port: url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80,
462
+ path: url.pathname + url.search,
463
+ headers: {
464
+ authorization: `Bearer ${args.token}`,
465
+ "content-type": "application/json",
466
+ "content-length": Buffer.byteLength(payload),
467
+ accept: "application/json",
468
+ },
469
+ }, (res) => {
470
+ let raw = "";
471
+ res.setEncoding("utf8");
472
+ res.on("data", (d) => (raw += d));
473
+ res.on("end", () => {
474
+ const status = res.statusCode ?? 0;
475
+ let body = {};
476
+ try {
477
+ body = JSON.parse(raw);
478
+ }
479
+ catch {
480
+ /* leave empty */
481
+ }
482
+ if (status >= 200 && status < 300 && body.session_id) {
483
+ resolve(body.session_id);
484
+ }
485
+ else {
486
+ reject(new Error(body.error || `join failed (HTTP ${status})`));
487
+ }
488
+ });
489
+ });
490
+ req.on("error", reject);
491
+ req.write(payload);
492
+ req.end();
493
+ });
494
+ }
431
495
  export async function runListenHere(argv) {
432
496
  const parsed = parseFlags(argv);
433
497
  if ("help" in parsed) {
@@ -440,6 +504,19 @@ export async function runListenHere(argv) {
440
504
  return 2;
441
505
  }
442
506
  const args = parsed;
507
+ // No explicit --session? Auto-join with the identity_key to get one. This is
508
+ // what makes the receiver command self-contained (no <SID> to fill in).
509
+ if (!args.session) {
510
+ try {
511
+ args.session = await joinForSession(args);
512
+ if (!args.quiet)
513
+ console.error(`[listen-here] joined as session ${args.session.slice(0, 8)}…`);
514
+ }
515
+ catch (err) {
516
+ console.error(`[listen-here] auto-join failed: ${err.message}`);
517
+ return 1;
518
+ }
519
+ }
443
520
  const ac = new AbortController();
444
521
  const shutdown = (sig) => {
445
522
  if (!args.quiet)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rogerthat",
3
- "version": "1.24.3",
3
+ "version": "1.24.4",
4
4
  "mcpName": "io.github.opcastil11/rogerthat",
5
5
  "description": "Real-time chat for AI agents. A walkie-talkie hub that lets two or more agents — Claude Code, Cursor, Cline, Claude Desktop, Codex — on different machines send messages to each other over MCP or plain REST. Hosted at rogerthat.chat or self-hosted with `npx rogerthat`.",
6
6
  "keywords": [