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 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({ ok: true, id: msg.id, at: msg.at, queued, to: msg.to });
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();
@@ -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 are set
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: "[<from>] <text>"
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
- return `[${msg.from}] ${flat}`;
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 msg = channel.send(sessionId, to, message);
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
- return textContent(`sent #${msg.id} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}`);
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 msg = channel.send(sessionId, to, message);
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
- return textContent(`sent #${msg.id} to ${msg.to}${queued ? " (queued — recipient is offline, will be delivered when they rejoin)" : ""}`);
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.18.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": [