rogerrat 1.18.1 → 1.19.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/dist/app.js +15 -3
- package/dist/channel.js +15 -1
- package/dist/listen-here.js +40 -4
- package/dist/mcp.js +21 -7
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join as joinPath } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { streamSSE } from "hono/streaming";
|
|
7
|
-
import { ChannelError, setSessionTtlLookup, startPeriodicGc } from "./channel.js";
|
|
7
|
+
import { ChannelError, isPriority, setSessionTtlLookup, startPeriodicGc, } from "./channel.js";
|
|
8
8
|
import { attachEmail, confirmEmailRecovery, createAccount, createIdentity, deleteIdentity, getAccount, getAccountIdsByIdentityCallsign, listIdentities, recoverAccount, removeEmail, requestEmailRecovery, verifyEmailCode, verifyIdentity, verifySession, } from "./accounts.js";
|
|
9
9
|
import { createChannelWebhook, createWebhook, deleteChannelWebhook, deleteWebhook, deliver, getActiveWebhooksForAccount, getActiveWebhooksForChannel, listChannelWebhooks, listWebhooks, } from "./webhooks.js";
|
|
10
10
|
import { buildRecoveryEmail, buildVerifyEmail, emailEnabled, sendEmail } from "./email.js";
|
|
@@ -743,6 +743,11 @@ export function createApp(opts) {
|
|
|
743
743
|
const to = String(body.to ?? "");
|
|
744
744
|
// Accept either `message` or `text` (transcripts return `text`, so clients reasonably try both).
|
|
745
745
|
const message = String(body.message ?? body.text ?? "");
|
|
746
|
+
// Optional ntfy-style priority. Server stores it; receivers decide what to do.
|
|
747
|
+
const priorityInput = body.priority;
|
|
748
|
+
if (priorityInput !== undefined && !isPriority(priorityInput)) {
|
|
749
|
+
return c.json({ error: "invalid priority; must be one of min|low|default|high|urgent", code: "invalid" }, 400);
|
|
750
|
+
}
|
|
746
751
|
const channel = getOrCreateChannel(channelId);
|
|
747
752
|
try {
|
|
748
753
|
const isBand = getChannelIsBand(channelId);
|
|
@@ -755,12 +760,19 @@ export function createApp(opts) {
|
|
|
755
760
|
retry_after_seconds: rate.retryAfter,
|
|
756
761
|
}, 429);
|
|
757
762
|
}
|
|
758
|
-
const msg = channel.send(sessionId, to, message);
|
|
763
|
+
const msg = channel.send(sessionId, to, message, priorityInput);
|
|
759
764
|
statsRecordMessage();
|
|
760
765
|
transcriptRecordMessage(channelId, getChannelRetention(channelId), msg);
|
|
761
766
|
fanoutWebhooks(channelId, msg);
|
|
762
767
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
763
|
-
return c.json({
|
|
768
|
+
return c.json({
|
|
769
|
+
ok: true,
|
|
770
|
+
id: msg.id,
|
|
771
|
+
at: msg.at,
|
|
772
|
+
queued,
|
|
773
|
+
to: msg.to,
|
|
774
|
+
...(msg.priority ? { priority: msg.priority } : {}),
|
|
775
|
+
});
|
|
764
776
|
}
|
|
765
777
|
catch (e) {
|
|
766
778
|
return handleChannelError(c, e);
|
package/dist/channel.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export const PRIORITY_RANK = {
|
|
2
|
+
min: 0,
|
|
3
|
+
low: 1,
|
|
4
|
+
default: 2,
|
|
5
|
+
high: 3,
|
|
6
|
+
urgent: 4,
|
|
7
|
+
};
|
|
8
|
+
export function isPriority(v) {
|
|
9
|
+
return v === "min" || v === "low" || v === "default" || v === "high" || v === "urgent";
|
|
10
|
+
}
|
|
1
11
|
const HISTORY_CAP = 100;
|
|
2
12
|
// Default idle TTL; channels can override via session_ttl_seconds at creation (max 24h).
|
|
3
13
|
const DEFAULT_ROSTER_IDLE_MS = 30 * 60 * 1000;
|
|
@@ -193,7 +203,7 @@ export class Channel {
|
|
|
193
203
|
sessionExists(sessionId) {
|
|
194
204
|
return this.callsignBySession.has(sessionId);
|
|
195
205
|
}
|
|
196
|
-
send(sessionId, to, text) {
|
|
206
|
+
send(sessionId, to, text, priority) {
|
|
197
207
|
this.ensureJoined(sessionId);
|
|
198
208
|
const from = this.callsignBySession.get(sessionId);
|
|
199
209
|
const dest = this.resolveAddress(to);
|
|
@@ -214,6 +224,10 @@ export class Channel {
|
|
|
214
224
|
const now = Date.now();
|
|
215
225
|
this.nextMsgId = Math.max(now, this.nextMsgId + 1);
|
|
216
226
|
const msg = { id: this.nextMsgId, from, to: dest, text, at: now };
|
|
227
|
+
// Only attach `priority` when explicitly non-default — keeps the wire format
|
|
228
|
+
// backward-compatible for consumers that don't know about priorities.
|
|
229
|
+
if (priority && priority !== "default")
|
|
230
|
+
msg.priority = priority;
|
|
217
231
|
this.messages.push(msg);
|
|
218
232
|
if (this.messages.length > HISTORY_CAP)
|
|
219
233
|
this.messages.shift();
|
package/dist/listen-here.js
CHANGED
|
@@ -35,16 +35,23 @@ options:
|
|
|
35
35
|
--origin <url> RogerRat origin (default: https://rogerrat.chat)
|
|
36
36
|
--since <msg_id> resume from a known message id (skips per-session cursor)
|
|
37
37
|
--on-message <cmd> shell command to run for each delivered message; env vars
|
|
38
|
-
RR_MESSAGE, RR_FROM, RR_TO, RR_MSG_ID, RR_CHANNEL
|
|
38
|
+
RR_MESSAGE, RR_FROM, RR_TO, RR_MSG_ID, RR_CHANNEL,
|
|
39
|
+
RR_PRIORITY are set (RR_PRIORITY = "default" when omitted)
|
|
39
40
|
--inbox <file> append each message to this file (parent dir created if
|
|
40
41
|
missing). Format controlled by --format.
|
|
41
42
|
--format <fmt> output format for stdout and --inbox lines:
|
|
42
|
-
jsonl (default) — one JSON object per line: {id,from,to,text,at}
|
|
43
|
-
text — one line per message:
|
|
43
|
+
jsonl (default) — one JSON object per line: {id,from,to,text,at,priority?}
|
|
44
|
+
text — one line per message:
|
|
45
|
+
"[<priority>] [<from>] <text>" if priority set,
|
|
46
|
+
"[<from>] <text>" otherwise.
|
|
44
47
|
newlines in text collapsed to spaces.
|
|
45
48
|
Use this when feeding a Monitor/tail consumer
|
|
46
49
|
so each line is a human-readable notification
|
|
47
50
|
with no parser needed in the watcher.
|
|
51
|
+
--min-priority <p> drop messages below this priority (min|low|default|high|urgent).
|
|
52
|
+
Filtered messages do NOT write to --inbox, fire --on-message,
|
|
53
|
+
or print to stdout. Use for park-style channels: stay
|
|
54
|
+
wake-able only on real signals, let chatter accumulate silently.
|
|
48
55
|
--quiet suppress the default stdout dump of each message
|
|
49
56
|
|
|
50
57
|
if neither --on-message nor --inbox is given, messages print to stdout (one
|
|
@@ -70,6 +77,13 @@ examples:
|
|
|
70
77
|
rogerrat listen-here --channel ch1 --token t --session s \\
|
|
71
78
|
--on-message 'claude -p "rogerrat msg from $RR_FROM: $RR_MESSAGE"'
|
|
72
79
|
`;
|
|
80
|
+
const PRIORITY_RANK = {
|
|
81
|
+
min: 0,
|
|
82
|
+
low: 1,
|
|
83
|
+
default: 2,
|
|
84
|
+
high: 3,
|
|
85
|
+
urgent: 4,
|
|
86
|
+
};
|
|
73
87
|
function parseFlags(argv) {
|
|
74
88
|
let parsed;
|
|
75
89
|
try {
|
|
@@ -84,6 +98,7 @@ function parseFlags(argv) {
|
|
|
84
98
|
"on-message": { type: "string" },
|
|
85
99
|
inbox: { type: "string" },
|
|
86
100
|
format: { type: "string" },
|
|
101
|
+
"min-priority": { type: "string" },
|
|
87
102
|
quiet: { type: "boolean" },
|
|
88
103
|
help: { type: "boolean", short: "h" },
|
|
89
104
|
},
|
|
@@ -116,6 +131,14 @@ function parseFlags(argv) {
|
|
|
116
131
|
}
|
|
117
132
|
format = parsed.values.format;
|
|
118
133
|
}
|
|
134
|
+
let minPriority;
|
|
135
|
+
if (parsed.values["min-priority"] !== undefined) {
|
|
136
|
+
const v = parsed.values["min-priority"];
|
|
137
|
+
if (v !== "min" && v !== "low" && v !== "default" && v !== "high" && v !== "urgent") {
|
|
138
|
+
return { error: "--min-priority must be one of min|low|default|high|urgent" };
|
|
139
|
+
}
|
|
140
|
+
minPriority = v;
|
|
141
|
+
}
|
|
119
142
|
return {
|
|
120
143
|
channel,
|
|
121
144
|
token,
|
|
@@ -126,6 +149,7 @@ function parseFlags(argv) {
|
|
|
126
149
|
inbox: parsed.values.inbox,
|
|
127
150
|
format,
|
|
128
151
|
quiet: parsed.values.quiet === true,
|
|
152
|
+
minPriority,
|
|
129
153
|
};
|
|
130
154
|
}
|
|
131
155
|
/** Yield one SSE block (the lines between two blank-line separators) at a time.
|
|
@@ -169,11 +193,22 @@ function formatLine(args, msg) {
|
|
|
169
193
|
// one notification per message. Use a single space; the JSONL format is
|
|
170
194
|
// available for callers that need lossless body content.
|
|
171
195
|
const flat = msg.text.replace(/\r?\n/g, " ").trim();
|
|
172
|
-
|
|
196
|
+
// Surface non-default priority as a leading tag so a Monitor tail of the
|
|
197
|
+
// inbox file can grep for urgency without parsing JSON. e.g. `[urgent] `
|
|
198
|
+
const prio = msg.priority && msg.priority !== "default" ? `[${msg.priority}] ` : "";
|
|
199
|
+
return `${prio}[${msg.from}] ${flat}`;
|
|
173
200
|
}
|
|
174
201
|
return JSON.stringify(msg);
|
|
175
202
|
}
|
|
176
203
|
async function dispatch(args, msg) {
|
|
204
|
+
// --min-priority filter: drop messages below the threshold entirely (no
|
|
205
|
+
// inbox write, no hook spawn, no stdout). Missing priority counts as
|
|
206
|
+
// "default" (rank 2).
|
|
207
|
+
if (args.minPriority) {
|
|
208
|
+
const incomingRank = PRIORITY_RANK[msg.priority ?? "default"];
|
|
209
|
+
if (incomingRank < PRIORITY_RANK[args.minPriority])
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
177
212
|
const line = formatLine(args, msg);
|
|
178
213
|
if (args.inbox) {
|
|
179
214
|
const dir = dirname(args.inbox);
|
|
@@ -199,6 +234,7 @@ async function dispatch(args, msg) {
|
|
|
199
234
|
RR_TO: msg.to,
|
|
200
235
|
RR_MSG_ID: String(msg.id),
|
|
201
236
|
RR_CHANNEL: args.channel,
|
|
237
|
+
RR_PRIORITY: msg.priority ?? "default",
|
|
202
238
|
},
|
|
203
239
|
});
|
|
204
240
|
child.on("error", (err) => {
|
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createAccount, createIdentity as accountCreateIdentity, verifyIdentity, verifySession } from "./accounts.js";
|
|
3
|
-
import { getOrCreateChannel } from "./channel.js";
|
|
3
|
+
import { getOrCreateChannel, isPriority } from "./channel.js";
|
|
4
4
|
import { buildConnectInfo } from "./connect.js";
|
|
5
5
|
import { createRemoteControl } from "./remote-control.js";
|
|
6
6
|
import { getPreset } from "./presets.js";
|
|
@@ -62,12 +62,17 @@ const CHANNEL_TOOLS = [
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: "send",
|
|
65
|
-
description: "Send a message to another agent on the channel by their callsign, or to 'all' to broadcast. Returns the message id.",
|
|
65
|
+
description: "Send a message to another agent on the channel by their callsign, or to 'all' to broadcast. Returns the message id. Optional `priority` (min|low|default|high|urgent) — receivers may wake immediately on high/urgent.",
|
|
66
66
|
inputSchema: {
|
|
67
67
|
type: "object",
|
|
68
68
|
properties: {
|
|
69
69
|
to: { type: "string", description: "Recipient callsign, or 'all' for broadcast." },
|
|
70
70
|
message: { type: "string", description: "Message text. Max 8192 chars." },
|
|
71
|
+
priority: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["min", "low", "default", "high", "urgent"],
|
|
74
|
+
description: "Optional urgency. Default = 'default'. Receivers interpret.",
|
|
75
|
+
},
|
|
71
76
|
},
|
|
72
77
|
required: ["to", "message"],
|
|
73
78
|
},
|
|
@@ -164,12 +169,17 @@ const UNIFIED_TOOLS = [
|
|
|
164
169
|
},
|
|
165
170
|
{
|
|
166
171
|
name: "send",
|
|
167
|
-
description: "Send a message to another agent on the channel you joined, or to 'all' to broadcast. Requires a prior join() in this session. The 'to' field accepts: a callsign ('front'), an index ('#1' or '1') from roster(), or 'all'.",
|
|
172
|
+
description: "Send a message to another agent on the channel you joined, or to 'all' to broadcast. Requires a prior join() in this session. The 'to' field accepts: a callsign ('front'), an index ('#1' or '1') from roster(), or 'all'. Optional `priority` tags urgency (min|low|default|high|urgent) — receivers may wake immediately on high/urgent and filter min/low.",
|
|
168
173
|
inputSchema: {
|
|
169
174
|
type: "object",
|
|
170
175
|
properties: {
|
|
171
176
|
to: { type: "string", description: "Recipient: callsign, '#N' index, or 'all' for broadcast." },
|
|
172
177
|
message: { type: "string", description: "Message text. Max 8192 chars." },
|
|
178
|
+
priority: {
|
|
179
|
+
type: "string",
|
|
180
|
+
enum: ["min", "low", "default", "high", "urgent"],
|
|
181
|
+
description: "Optional urgency tag. Default = 'default'. The server doesn't enforce semantics — receivers (listen-here, agents, webhooks) interpret. Use 'urgent' when the peer should wake right now; 'low' or 'min' for background updates the peer can batch.",
|
|
182
|
+
},
|
|
173
183
|
},
|
|
174
184
|
required: ["to", "message"],
|
|
175
185
|
},
|
|
@@ -316,11 +326,13 @@ async function callChannelTool(channel, sessionId, name, args) {
|
|
|
316
326
|
case "send": {
|
|
317
327
|
const to = String(args.to ?? "");
|
|
318
328
|
const message = String(args.message ?? args.text ?? "");
|
|
319
|
-
const
|
|
329
|
+
const priority = isPriority(args.priority) ? args.priority : undefined;
|
|
330
|
+
const msg = channel.send(sessionId, to, message, priority);
|
|
320
331
|
statsRecordMessage();
|
|
321
332
|
transcriptRecordMessage(channel.id, getChannelRetention(channel.id), msg);
|
|
322
333
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
323
|
-
|
|
334
|
+
const prio = msg.priority ? ` [${msg.priority}]` : "";
|
|
335
|
+
return textContent(`sent #${msg.id}${prio} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}`);
|
|
324
336
|
}
|
|
325
337
|
case "listen": {
|
|
326
338
|
const seconds = typeof args.timeout_seconds === "number" ? args.timeout_seconds : 30;
|
|
@@ -598,11 +610,13 @@ async function callUnifiedTool(name, args, state, sessionId, publicOrigin, mode
|
|
|
598
610
|
case "send": {
|
|
599
611
|
const to = String(args.to ?? "");
|
|
600
612
|
const message = String(args.message ?? args.text ?? "");
|
|
601
|
-
const
|
|
613
|
+
const priority = isPriority(args.priority) ? args.priority : undefined;
|
|
614
|
+
const msg = channel.send(sessionId, to, message, priority);
|
|
602
615
|
statsRecordMessage();
|
|
603
616
|
transcriptRecordMessage(channel.id, getChannelRetention(channel.id), msg);
|
|
604
617
|
const queued = msg.to !== "all" && !channel.isCallsignOnline(msg.to);
|
|
605
|
-
|
|
618
|
+
const prio = msg.priority ? ` [${msg.priority}]` : "";
|
|
619
|
+
return textContent(`sent #${msg.id}${prio} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}`);
|
|
606
620
|
}
|
|
607
621
|
case "listen": {
|
|
608
622
|
const seconds = typeof args.timeout_seconds === "number" ? args.timeout_seconds : 30;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rogerrat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"mcpName": "io.github.opcastil11/rogerrat",
|
|
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 rogerrat.chat or self-hosted with `npx rogerrat`.",
|
|
6
6
|
"keywords": [
|