rogerrat 0.5.1 → 0.6.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/admin.js CHANGED
@@ -179,6 +179,7 @@ export function adminHtml() {
179
179
  <tr>
180
180
  <th>Channel</th>
181
181
  <th>Retention</th>
182
+ <th>Auth</th>
182
183
  <th>Roster</th>
183
184
  <th>Msgs</th>
184
185
  <th>Opened</th>
@@ -246,7 +247,7 @@ export function adminHtml() {
246
247
  function renderRows(channels) {
247
248
  const rows = $('rows');
248
249
  if (!channels.length) {
249
- rows.innerHTML = '<tr><td colspan="6" class="empty">No active channels yet.</td></tr>';
250
+ rows.innerHTML = '<tr><td colspan="7" class="empty">No active channels yet.</td></tr>';
250
251
  return;
251
252
  }
252
253
  rows.innerHTML = channels.map(c => {
@@ -255,9 +256,12 @@ export function adminHtml() {
255
256
  : '<span style="color:var(--dim)">empty</span>';
256
257
  const opened = c.first_joined_at ? fmtAgo(c.first_joined_at) : '—';
257
258
  const retColor = c.retention === 'full' ? '#d6541f' : c.retention === 'none' ? 'var(--dim)' : 'var(--ink)';
259
+ const authLabel = c.require_identity ? 'identity' : 'token';
260
+ const authColor = c.require_identity ? '#d6541f' : 'var(--dim)';
258
261
  return '<tr>' +
259
262
  '<td class="channel-id">' + esc(c.id) + '</td>' +
260
263
  '<td><span style="color:' + retColor + '">' + esc(c.retention || 'none') + '</span></td>' +
264
+ '<td><span style="color:' + authColor + '">' + authLabel + '</span></td>' +
261
265
  '<td>' + roster + '</td>' +
262
266
  '<td>' + c.message_count + '</td>' +
263
267
  '<td>' + opened + '</td>' +
package/dist/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Hono } from "hono";
3
- import { createAccount, createIdentity, deleteIdentity, getAccount, listIdentities, recoverAccount, verifySession, } from "./accounts.js";
3
+ import { createAccount, createIdentity, deleteIdentity, getAccount, listIdentities, recoverAccount, verifyIdentity, verifySession, } from "./accounts.js";
4
4
  import { adminHtml } from "./admin.js";
5
5
  import { getOrCreateChannel, listActiveChannels } from "./channel.js";
6
6
  import { buildConnectInfo } from "./connect.js";
@@ -8,7 +8,7 @@ import { llmsText, mcpDescriptor, serviceInfo } from "./discovery.js";
8
8
  import { landingHtml } from "./landing.js";
9
9
  import { handleMcpRequest } from "./mcp.js";
10
10
  import { recordJoin as statsRecordJoin, recordMessage as statsRecordMessage, getStats, } from "./stats.js";
11
- import { channelExists, createChannel, getChannelRetention, verifyChannel } from "./store.js";
11
+ import { channelExists, createChannel, getChannelRequireIdentity, getChannelRetention, verifyChannel, } from "./store.js";
12
12
  import { isRetention, readTranscript, recordJoin as transcriptRecordJoin, recordLeave as transcriptRecordLeave, recordMessage as transcriptRecordMessage, } from "./transcripts.js";
13
13
  export function createApp(opts) {
14
14
  const app = new Hono();
@@ -127,8 +127,12 @@ export function createApp(opts) {
127
127
  if (retentionInput !== undefined && !isRetention(retentionInput)) {
128
128
  return c.json({ error: "invalid retention; must be one of none|metadata|prompts|full" }, 400);
129
129
  }
130
- const { id, token, retention } = createChannel({ retention: retentionInput });
131
- return c.json({ ...buildConnectInfo(id, token, opts.publicOrigin), retention });
130
+ const requireIdentity = body.require_identity === true;
131
+ const { id, token, retention, require_identity } = createChannel({
132
+ retention: retentionInput,
133
+ require_identity: requireIdentity,
134
+ });
135
+ return c.json({ ...buildConnectInfo(id, token, opts.publicOrigin), retention, require_identity });
132
136
  });
133
137
  app.get("/api/channels/:channelId/transcript", (c) => {
134
138
  const channelId = c.req.param("channelId");
@@ -172,18 +176,32 @@ export function createApp(opts) {
172
176
  catch {
173
177
  /* empty body ok */
174
178
  }
175
- const callsign = String(body.callsign ?? "");
176
- if (!callsign)
177
- return c.json({ error: "callsign required in body" }, 400);
179
+ const callsignArg = String(body.callsign ?? "");
180
+ const identityKey = typeof body.identity_key === "string" ? body.identity_key : undefined;
181
+ let resolvedCallsign = callsignArg;
182
+ let identitySource = null;
183
+ if (identityKey) {
184
+ const idRec = verifyIdentity(identityKey);
185
+ if (!idRec)
186
+ return c.json({ error: "invalid identity_key" }, 401);
187
+ resolvedCallsign = idRec.callsign;
188
+ identitySource = idRec.account_id;
189
+ }
190
+ else if (getChannelRequireIdentity(channelId)) {
191
+ return c.json({ error: "this channel requires identity_key (require_identity=true)" }, 403);
192
+ }
193
+ if (!resolvedCallsign)
194
+ return c.json({ error: "callsign or identity_key required" }, 400);
178
195
  const sessionId = randomUUID();
179
196
  const channel = getOrCreateChannel(channelId);
180
197
  try {
181
- const { roster, history } = channel.join(sessionId, callsign);
198
+ const { roster, history } = channel.join(sessionId, resolvedCallsign);
182
199
  statsRecordJoin();
183
- transcriptRecordJoin(channelId, getChannelRetention(channelId), callsign);
200
+ transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
184
201
  return c.json({
185
202
  session_id: sessionId,
186
- callsign,
203
+ callsign: resolvedCallsign,
204
+ identity_account: identitySource,
187
205
  roster,
188
206
  history,
189
207
  retention: getChannelRetention(channelId),
@@ -286,7 +304,7 @@ export function createApp(opts) {
286
304
  const denied = requireAdmin(c);
287
305
  if (denied)
288
306
  return denied;
289
- return c.json({ channels: listActiveChannels(getChannelRetention) });
307
+ return c.json({ channels: listActiveChannels(getChannelRetention, getChannelRequireIdentity) });
290
308
  });
291
309
  async function mcpHandler(c, channelId) {
292
310
  if (channelId !== null) {
package/dist/channel.js CHANGED
@@ -168,12 +168,13 @@ export function getOrCreateChannel(id) {
168
168
  }
169
169
  return ch;
170
170
  }
171
- export function listActiveChannels(retentionFor) {
171
+ export function listActiveChannels(retentionFor, requireIdentityFor) {
172
172
  return [...channels.values()]
173
173
  .filter((c) => c.size() > 0 || c.firstJoinedAt !== null)
174
174
  .map((c) => ({
175
175
  id: c.id,
176
176
  retention: retentionFor(c.id),
177
+ require_identity: requireIdentityFor(c.id),
177
178
  roster: c.roster(),
178
179
  agent_count: c.size(),
179
180
  message_count: c.history(100).length,
package/dist/discovery.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "0.5.1";
1
+ const VERSION = "0.6.0";
2
2
  export function llmsText(origin) {
3
3
  return `# RogerRat
4
4
 
package/dist/landing.js CHANGED
@@ -237,7 +237,7 @@ export function landingHtml() {
237
237
 
238
238
  <div class="cta">
239
239
  <p style="margin-top:0"><strong>Create a private channel</strong> — pick your client below and share the snippet with another agent.</p>
240
- <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
240
+ <div style="display:flex;gap:16px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
241
241
  <label style="font-size:13px;color:var(--dim)">retention:
242
242
  <select id="retention" style="padding:6px 8px;border:1px solid var(--line);background:var(--paper);font-family:inherit;font-size:13px;margin-left:6px">
243
243
  <option value="none" selected>none — ephemeral (default)</option>
@@ -246,6 +246,9 @@ export function landingHtml() {
246
246
  <option value="full">full — keep everything</option>
247
247
  </select>
248
248
  </label>
249
+ <label style="font-size:13px;color:var(--dim);display:inline-flex;align-items:center;gap:6px">
250
+ <input type="checkbox" id="require_identity" /> require account-bound identity to join
251
+ </label>
249
252
  </div>
250
253
  <button id="create">Create channel</button>
251
254
 
@@ -352,10 +355,11 @@ export function landingHtml() {
352
355
  btn.textContent = 'Creating…';
353
356
  try {
354
357
  const retention = document.getElementById('retention').value;
358
+ const require_identity = document.getElementById('require_identity').checked;
355
359
  const r = await fetch('/api/channels', {
356
360
  method: 'POST',
357
361
  headers: { 'Content-Type': 'application/json' },
358
- body: JSON.stringify({ retention }),
362
+ body: JSON.stringify({ retention, require_identity }),
359
363
  });
360
364
  if (!r.ok) throw new Error('http ' + r.status);
361
365
  const j = await r.json();
package/dist/mcp.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { verifyIdentity } from "./accounts.js";
2
3
  import { getOrCreateChannel } from "./channel.js";
3
4
  import { buildConnectInfo } from "./connect.js";
4
5
  import { recordJoin as statsRecordJoin, recordMessage as statsRecordMessage } from "./stats.js";
5
- import { channelExists, createChannel, getChannelRetention, verifyChannel } from "./store.js";
6
+ import { channelExists, createChannel, getChannelRequireIdentity, getChannelRetention, verifyChannel, } from "./store.js";
6
7
  import { isRetention, recordJoin as transcriptRecordJoin, recordLeave as transcriptRecordLeave, recordMessage as transcriptRecordMessage, } from "./transcripts.js";
7
8
  const PROTOCOL_VERSION = "2025-03-26";
8
9
  const SERVER_INFO = { name: "rogerrat", version: "0.1.0" };
@@ -85,7 +86,7 @@ const CHANNEL_TOOLS = [
85
86
  const UNIFIED_TOOLS = [
86
87
  {
87
88
  name: "create_channel",
88
- description: "Create a new RogerRat channel. Returns the channel id, join token, MCP URL, and connect snippets. Optional retention: 'none' (default), 'metadata', 'prompts', 'full'.",
89
+ description: "Create a new RogerRat channel. Returns the channel id, join token, MCP URL, and connect snippets. Optional retention: 'none' (default), 'metadata', 'prompts', 'full'. Optional require_identity (default false): when true, joining the channel requires a valid identity_key from an account.",
89
90
  inputSchema: {
90
91
  type: "object",
91
92
  properties: {
@@ -94,23 +95,31 @@ const UNIFIED_TOOLS = [
94
95
  enum: ["none", "metadata", "prompts", "full"],
95
96
  description: "Server-side transcript retention. Default: 'none' (ephemeral).",
96
97
  },
98
+ require_identity: {
99
+ type: "boolean",
100
+ description: "Require an identity_key (from an account) to join. Default: false.",
101
+ },
97
102
  },
98
103
  },
99
104
  },
100
105
  {
101
106
  name: "join",
102
- description: "Join a channel by id + token + callsign. After joining, this session is bound to that channel — subsequent send/listen/roster/history/leave operate on it. Call leave to detach (then you can join another channel in the same session). Returns roster, recent history, and operating instructions.",
107
+ description: "Join a channel by id + token. Provide either a callsign (anonymous) or an identity_key (account-bound; callsign comes from the identity). If the channel has require_identity=true, identity_key is mandatory. After joining, this session is bound to that channel — subsequent send/listen/roster/history/leave operate on it. Call leave to detach.",
103
108
  inputSchema: {
104
109
  type: "object",
105
110
  properties: {
106
111
  channel_id: { type: "string", description: "Channel id like 'quiet-otter-3a8f'." },
107
- token: { type: "string", description: "Bearer token for that channel (received from create_channel)." },
112
+ token: { type: "string", description: "Bearer token for that channel." },
108
113
  callsign: {
109
114
  type: "string",
110
- description: "Your handle on the channel. 1-32 chars, alphanumeric/underscore/dash. Cannot be 'all'.",
115
+ description: "Anonymous handle. Ignored if identity_key is provided. 1-32 chars, alphanumeric/underscore/dash. Cannot be 'all'.",
116
+ },
117
+ identity_key: {
118
+ type: "string",
119
+ description: "Account-bound identity key (from POST /api/account/identities). Required when channel has require_identity=true.",
111
120
  },
112
121
  },
113
- required: ["channel_id", "token", "callsign"],
122
+ required: ["channel_id", "token"],
114
123
  },
115
124
  },
116
125
  {
@@ -275,26 +284,40 @@ async function callUnifiedTool(name, args, state, sessionId, publicOrigin) {
275
284
  if (name === "join") {
276
285
  const channelId = String(args.channel_id ?? "");
277
286
  const token = String(args.token ?? "");
278
- const callsign = String(args.callsign ?? "");
279
- if (!channelId || !token || !callsign) {
280
- throw new Error("join requires channel_id, token, and callsign");
281
- }
287
+ const callsignArg = String(args.callsign ?? "");
288
+ const identityKey = typeof args.identity_key === "string" ? args.identity_key : undefined;
289
+ if (!channelId || !token)
290
+ throw new Error("join requires channel_id and token");
282
291
  if (!channelExists(channelId))
283
292
  throw new Error(`channel not found: ${channelId}`);
284
293
  if (!verifyChannel(channelId, token))
285
294
  throw new Error("invalid token for channel");
295
+ let resolvedCallsign = callsignArg;
296
+ let identitySource = null;
297
+ if (identityKey) {
298
+ const idRec = verifyIdentity(identityKey);
299
+ if (!idRec)
300
+ throw new Error("invalid identity_key");
301
+ resolvedCallsign = idRec.callsign;
302
+ identitySource = idRec.account_id;
303
+ }
304
+ else if (getChannelRequireIdentity(channelId)) {
305
+ throw new Error("this channel requires identity_key (require_identity=true). Create one at POST /api/account/identities.");
306
+ }
307
+ if (!resolvedCallsign)
308
+ throw new Error("either callsign or identity_key is required");
286
309
  if (state.boundChannel && state.boundChannel !== channelId) {
287
310
  const oldChannel = getOrCreateChannel(state.boundChannel);
288
311
  oldChannel.leave(sessionId);
289
312
  state.boundChannel = null;
290
313
  }
291
314
  const channel = getOrCreateChannel(channelId);
292
- const { roster, history } = channel.join(sessionId, callsign);
315
+ const { roster, history } = channel.join(sessionId, resolvedCallsign);
293
316
  statsRecordJoin();
294
- transcriptRecordJoin(channelId, getChannelRetention(channelId), callsign);
317
+ transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
295
318
  state.boundChannel = channelId;
296
319
  const body = [
297
- `Joined channel ${channelId} as ${callsign}.`,
320
+ `Joined channel ${channelId} as ${resolvedCallsign}${identitySource ? ` (identity-bound to account ${identitySource})` : ""}.`,
298
321
  `Roster (${roster.length}): ${roster.join(", ")}`,
299
322
  "",
300
323
  `Recent history (${history.length}):`,
package/dist/store.js CHANGED
@@ -25,6 +25,7 @@ function ensureLoaded() {
25
25
  tokenHash: r.tokenHash,
26
26
  createdAt: r.createdAt,
27
27
  retention: isRetention(r.retention) ? r.retention : "none",
28
+ requireIdentity: r.requireIdentity === true,
28
29
  },
29
30
  ]));
30
31
  }
@@ -44,16 +45,17 @@ function persist() {
44
45
  export function createChannel(opts = {}) {
45
46
  ensureLoaded();
46
47
  const retention = opts.retention ?? "none";
48
+ const requireIdentity = opts.require_identity === true;
47
49
  let id;
48
50
  do {
49
51
  id = generateChannelId();
50
52
  } while (channels.has(id));
51
53
  const token = generateToken();
52
- channels.set(id, { id, tokenHash: hashToken(token), createdAt: Date.now(), retention });
54
+ channels.set(id, { id, tokenHash: hashToken(token), createdAt: Date.now(), retention, requireIdentity });
53
55
  persist();
54
56
  statsRecordChannelCreated();
55
57
  transcriptRecordChannelCreated(id, retention);
56
- return { id, token, retention };
58
+ return { id, token, retention, require_identity: requireIdentity };
57
59
  }
58
60
  export function verifyChannel(id, token) {
59
61
  ensureLoaded();
@@ -74,3 +76,7 @@ export function getChannelRetention(id) {
74
76
  ensureLoaded();
75
77
  return channels.get(id)?.retention ?? "none";
76
78
  }
79
+ export function getChannelRequireIdentity(id) {
80
+ ensureLoaded();
81
+ return channels.get(id)?.requireIdentity ?? false;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rogerrat",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Walkie-talkie MCP server for AI coding agents. Two Claudes (or Cursor, Cline, Claude Desktop) talk to each other over a hosted hub or your own localhost.",
5
5
  "keywords": [
6
6
  "mcp",