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 +42 -4
- package/dist/listen-here.js +81 -4
- package/package.json +1 -1
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
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
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({
|
package/dist/listen-here.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
+
"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": [
|