@hasna/conversations 0.2.45 → 0.2.47

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/bin/index.js CHANGED
@@ -13571,6 +13571,18 @@ function getDb() {
13571
13571
  PRIMARY KEY (space, agent)
13572
13572
  )
13573
13573
  `);
13574
+ db.exec(`
13575
+ CREATE TABLE IF NOT EXISTS space_subscriptions (
13576
+ space TEXT NOT NULL REFERENCES spaces(name),
13577
+ agent TEXT NOT NULL,
13578
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13579
+ preview_chars INTEGER NOT NULL DEFAULT 140,
13580
+ since_message_id INTEGER NOT NULL DEFAULT 0,
13581
+ PRIMARY KEY (space, agent)
13582
+ )
13583
+ `);
13584
+ db.exec("CREATE INDEX IF NOT EXISTS idx_space_subscriptions_agent ON space_subscriptions(agent)");
13585
+ db.exec("CREATE INDEX IF NOT EXISTS idx_space_subscriptions_space ON space_subscriptions(space)");
13574
13586
  db.exec(`
13575
13587
  CREATE TABLE IF NOT EXISTS agent_presence (
13576
13588
  id TEXT NOT NULL,
@@ -13611,6 +13623,16 @@ function getDb() {
13611
13623
  )
13612
13624
  `);
13613
13625
  db.exec("CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id)");
13626
+ db.exec(`
13627
+ CREATE TABLE IF NOT EXISTS space_notification_reads (
13628
+ agent TEXT NOT NULL,
13629
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
13630
+ read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
13631
+ PRIMARY KEY (agent, message_id)
13632
+ )
13633
+ `);
13634
+ db.exec("CREATE INDEX IF NOT EXISTS idx_space_notification_reads_agent ON space_notification_reads(agent)");
13635
+ db.exec("CREATE INDEX IF NOT EXISTS idx_space_notification_reads_message ON space_notification_reads(message_id)");
13614
13636
  const existingTables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
13615
13637
  const tableNames = existingTables.map((t) => t.name);
13616
13638
  if (tableNames.includes("channels") && tableNames.includes("spaces")) {
@@ -13658,6 +13680,19 @@ function getDb() {
13658
13680
  if (!spaceColNames.includes("topic")) {
13659
13681
  db.exec("ALTER TABLE spaces ADD COLUMN topic TEXT");
13660
13682
  }
13683
+ const spaceSubscriptionCols = db.prepare("PRAGMA table_info(space_subscriptions)").all();
13684
+ const spaceSubscriptionColNames = spaceSubscriptionCols.map((c) => c.name);
13685
+ if (!spaceSubscriptionColNames.includes("since_message_id")) {
13686
+ db.exec("ALTER TABLE space_subscriptions ADD COLUMN since_message_id INTEGER NOT NULL DEFAULT 0");
13687
+ db.exec(`
13688
+ UPDATE space_subscriptions
13689
+ SET since_message_id = COALESCE(
13690
+ (SELECT MAX(m.id) FROM messages m WHERE m.space = space_subscriptions.space),
13691
+ 0
13692
+ )
13693
+ WHERE since_message_id = 0
13694
+ `);
13695
+ }
13661
13696
  const msgCols2 = db.prepare("PRAGMA table_info(messages)").all();
13662
13697
  const colNames2 = msgCols2.map((c) => c.name);
13663
13698
  if (!colNames2.includes("edited_at")) {
@@ -15261,11 +15296,127 @@ var init_presence = __esm(() => {
15261
15296
  CONFLICT_THRESHOLD_SECONDS = 30 * 60;
15262
15297
  });
15263
15298
 
15299
+ // src/lib/space-notifications.ts
15300
+ function buildMessagePreview(content, maxChars = DEFAULT_PREVIEW_CHARS) {
15301
+ const normalized = content.replace(/[*#`~_>\-]/g, " ").replace(/\s+/g, " ").trim();
15302
+ if (normalized.length <= maxChars)
15303
+ return normalized;
15304
+ return normalized.slice(0, Math.max(1, maxChars)).trimEnd() + "\u2026";
15305
+ }
15306
+ function subscribeToSpaceNotifications(space, agent, opts) {
15307
+ const db2 = getDb();
15308
+ const existingSpace = db2.prepare("SELECT name FROM spaces WHERE name = ?").get(space);
15309
+ if (!existingSpace) {
15310
+ throw new Error(`Space not found: ${space}`);
15311
+ }
15312
+ const previewChars = Number.isFinite(opts?.preview_chars) && opts?.preview_chars > 0 ? Math.floor(opts.preview_chars) : DEFAULT_PREVIEW_CHARS;
15313
+ const currentMaxMessageId = db2.prepare("SELECT COALESCE(MAX(id), 0) AS max_id FROM messages WHERE space = ?").get(space).max_id;
15314
+ db2.prepare(`
15315
+ INSERT INTO space_subscriptions (space, agent, preview_chars, since_message_id)
15316
+ VALUES (?, ?, ?, ?)
15317
+ ON CONFLICT(space, agent) DO UPDATE SET preview_chars = excluded.preview_chars
15318
+ `).run(space, agent, previewChars, currentMaxMessageId);
15319
+ return db2.prepare("SELECT space, agent, created_at, preview_chars, since_message_id FROM space_subscriptions WHERE space = ? AND agent = ?").get(space, agent);
15320
+ }
15321
+ function unsubscribeFromSpaceNotifications(space, agent) {
15322
+ const db2 = getDb();
15323
+ const result = db2.prepare("DELETE FROM space_subscriptions WHERE space = ? AND agent = ?").run(space, agent);
15324
+ return result.changes > 0;
15325
+ }
15326
+ function listSpaceNotificationSubscriptions(agent) {
15327
+ const db2 = getDb();
15328
+ if (agent) {
15329
+ return db2.prepare("SELECT space, agent, created_at, preview_chars, since_message_id FROM space_subscriptions WHERE agent = ? ORDER BY created_at ASC, space ASC").all(agent);
15330
+ }
15331
+ return db2.prepare("SELECT space, agent, created_at, preview_chars, since_message_id FROM space_subscriptions ORDER BY agent ASC, space ASC").all();
15332
+ }
15333
+ function readSpaceNotifications(opts) {
15334
+ const db2 = getDb();
15335
+ const conditions = [
15336
+ "s.agent = ?",
15337
+ "m.space IS NOT NULL",
15338
+ "m.from_agent != ?",
15339
+ "m.id > s.since_message_id"
15340
+ ];
15341
+ const params = [opts.agent, opts.agent];
15342
+ if (opts.space) {
15343
+ conditions.push("m.space = ?");
15344
+ params.push(opts.space);
15345
+ }
15346
+ if (opts.since) {
15347
+ conditions.push("m.created_at > ?");
15348
+ params.push(opts.since);
15349
+ }
15350
+ if (opts.unread_only !== false) {
15351
+ conditions.push("snr.message_id IS NULL");
15352
+ }
15353
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
15354
+ const rows = db2.prepare(`
15355
+ SELECT
15356
+ m.id AS message_id,
15357
+ m.space,
15358
+ m.from_agent,
15359
+ m.created_at,
15360
+ m.priority,
15361
+ m.content,
15362
+ m.attachments,
15363
+ s.preview_chars,
15364
+ snr.message_id AS read_message_id
15365
+ FROM messages m
15366
+ INNER JOIN space_subscriptions s
15367
+ ON s.space = m.space
15368
+ LEFT JOIN space_notification_reads snr
15369
+ ON snr.message_id = m.id AND snr.agent = s.agent
15370
+ WHERE ${conditions.join(" AND ")}
15371
+ ORDER BY m.created_at DESC, m.id DESC
15372
+ LIMIT ${Math.max(1, Math.min(limit, 500))}
15373
+ `).all(...params);
15374
+ const notifications = rows.map((row) => ({
15375
+ message_id: row.message_id,
15376
+ space: row.space,
15377
+ from_agent: row.from_agent,
15378
+ created_at: row.created_at,
15379
+ priority: row.priority,
15380
+ preview: buildMessagePreview(row.content, row.preview_chars),
15381
+ unread: row.read_message_id == null,
15382
+ has_attachments: !!row.attachments && row.attachments !== "[]"
15383
+ }));
15384
+ if (opts.mark_read && notifications.length > 0) {
15385
+ markSpaceNotificationsRead(opts.agent, notifications.map((row) => row.message_id));
15386
+ for (const row of notifications)
15387
+ row.unread = false;
15388
+ }
15389
+ return notifications;
15390
+ }
15391
+ function markSpaceNotificationsRead(agent, messageIds) {
15392
+ if (messageIds.length === 0)
15393
+ return 0;
15394
+ const db2 = getDb();
15395
+ const insert = db2.prepare(`
15396
+ INSERT OR IGNORE INTO space_notification_reads (agent, message_id)
15397
+ VALUES (?, ?)
15398
+ `);
15399
+ let count = 0;
15400
+ for (const id of messageIds) {
15401
+ const result = insert.run(agent, id);
15402
+ count += result.changes;
15403
+ }
15404
+ return count;
15405
+ }
15406
+ function markAllSpaceNotificationsRead(agent, space) {
15407
+ const unread = readSpaceNotifications({ agent, space, unread_only: true, limit: 1e4 });
15408
+ return markSpaceNotificationsRead(agent, unread.map((row) => row.message_id));
15409
+ }
15410
+ var DEFAULT_PREVIEW_CHARS = 140;
15411
+ var init_space_notifications = __esm(() => {
15412
+ init_db();
15413
+ });
15414
+
15264
15415
  // package.json
15265
15416
  var require_package = __commonJS((exports, module) => {
15266
15417
  module.exports = {
15267
15418
  name: "@hasna/conversations",
15268
- version: "0.2.45",
15419
+ version: "0.2.47",
15269
15420
  description: "Real-time CLI messaging for AI agents",
15270
15421
  type: "module",
15271
15422
  bin: {
@@ -45160,6 +45311,23 @@ function registerMessagingTools(server, resolveProjectId) {
45160
45311
  content: [{ type: "text", text: JSON.stringify({ messages, count: messages.length, offset: args.offset ?? 0 }) }]
45161
45312
  };
45162
45313
  });
45314
+ server.registerTool("get_message", {
45315
+ description: "Get the full content of a message by numeric ID. Use this to inspect a full space message after receiving a preview-only notification blurb.",
45316
+ inputSchema: {
45317
+ id: exports_external2.coerce.number().describe("Numeric message ID to fetch")
45318
+ }
45319
+ }, async (args) => {
45320
+ const message = getMessageById(args.id);
45321
+ if (!message) {
45322
+ return {
45323
+ content: [{ type: "text", text: `Message #${args.id} not found` }],
45324
+ isError: true
45325
+ };
45326
+ }
45327
+ return {
45328
+ content: [{ type: "text", text: JSON.stringify(message) }]
45329
+ };
45330
+ });
45163
45331
  server.registerTool("list_sessions", {
45164
45332
  description: "List all sessions by agent.",
45165
45333
  inputSchema: {
@@ -45554,6 +45722,7 @@ function registerSpaceTools(server) {
45554
45722
  if (fromParam && messages.length > 0) {
45555
45723
  const agent = resolveIdentity(fromParam);
45556
45724
  recordReadReceiptsBatch(messages.map((m) => m.id), agent);
45725
+ markSpaceNotificationsRead(agent, messages.map((m) => m.id));
45557
45726
  }
45558
45727
  return {
45559
45728
  content: [{ type: "text", text: JSON.stringify(messages) }]
@@ -45593,6 +45762,86 @@ function registerSpaceTools(server) {
45593
45762
  content: [{ type: "text", text: JSON.stringify({ space, agent, left }) }]
45594
45763
  };
45595
45764
  });
45765
+ server.registerTool("subscribe_space_notifications", {
45766
+ description: "Subscribe an agent to preview-only notifications for a space.",
45767
+ inputSchema: {
45768
+ space: exports_external2.string(),
45769
+ from: exports_external2.string().optional(),
45770
+ preview_chars: exports_external2.coerce.number().optional()
45771
+ }
45772
+ }, async (args) => {
45773
+ const agent = resolveIdentity(args.from);
45774
+ try {
45775
+ const subscription = subscribeToSpaceNotifications(args.space, agent, { preview_chars: args.preview_chars });
45776
+ return { content: [{ type: "text", text: JSON.stringify(subscription) }] };
45777
+ } catch (e) {
45778
+ return { content: [{ type: "text", text: e.message }], isError: true };
45779
+ }
45780
+ });
45781
+ server.registerTool("unsubscribe_space_notifications", {
45782
+ description: "Stop preview-only notifications for a space.",
45783
+ inputSchema: {
45784
+ space: exports_external2.string(),
45785
+ from: exports_external2.string().optional()
45786
+ }
45787
+ }, async (args) => {
45788
+ const agent = resolveIdentity(args.from);
45789
+ const unsubscribed = unsubscribeFromSpaceNotifications(args.space, agent);
45790
+ return { content: [{ type: "text", text: JSON.stringify({ space: args.space, agent, unsubscribed }) }] };
45791
+ });
45792
+ server.registerTool("list_space_subscriptions", {
45793
+ description: "List an agent's preview-only space notification subscriptions.",
45794
+ inputSchema: {
45795
+ from: exports_external2.string().optional(),
45796
+ space: exports_external2.string().optional()
45797
+ }
45798
+ }, async (args) => {
45799
+ const agent = resolveIdentity(args.from);
45800
+ const subscriptions = listSpaceNotificationSubscriptions(agent).filter((row) => !args.space || row.space === args.space);
45801
+ return { content: [{ type: "text", text: JSON.stringify(subscriptions) }] };
45802
+ });
45803
+ server.registerTool("read_space_notifications", {
45804
+ description: "Read preview-only notifications for an agent's subscribed spaces. Returns blurbs, not full message bodies.",
45805
+ inputSchema: {
45806
+ from: exports_external2.string().optional(),
45807
+ space: exports_external2.string().optional(),
45808
+ unread_only: exports_external2.coerce.boolean().optional(),
45809
+ limit: exports_external2.coerce.number().optional(),
45810
+ since: exports_external2.string().optional(),
45811
+ mark_read: exports_external2.coerce.boolean().optional()
45812
+ }
45813
+ }, async (args) => {
45814
+ const agent = resolveIdentity(args.from);
45815
+ const notifications = readSpaceNotifications({
45816
+ agent,
45817
+ space: args.space,
45818
+ unread_only: args.unread_only,
45819
+ limit: args.limit,
45820
+ since: args.since,
45821
+ mark_read: args.mark_read
45822
+ });
45823
+ return { content: [{ type: "text", text: JSON.stringify({ notifications, count: notifications.length }) }] };
45824
+ });
45825
+ server.registerTool("mark_space_notifications_read", {
45826
+ description: "Mark preview-only space notifications as read for an agent.",
45827
+ inputSchema: {
45828
+ from: exports_external2.string().optional(),
45829
+ ids: exports_external2.array(exports_external2.coerce.number()).optional(),
45830
+ space: exports_external2.string().optional(),
45831
+ all: exports_external2.coerce.boolean().optional()
45832
+ }
45833
+ }, async (args) => {
45834
+ const agent = resolveIdentity(args.from);
45835
+ let marked = 0;
45836
+ if (args.all) {
45837
+ marked = markAllSpaceNotificationsRead(agent, args.space);
45838
+ } else if (Array.isArray(args.ids) && args.ids.length > 0) {
45839
+ marked = markSpaceNotificationsRead(agent, args.ids);
45840
+ } else {
45841
+ return { content: [{ type: "text", text: "Provide ids or all=true" }], isError: true };
45842
+ }
45843
+ return { content: [{ type: "text", text: JSON.stringify({ marked_read: marked }) }] };
45844
+ });
45596
45845
  server.registerTool("update_space", {
45597
45846
  description: "Update space description or parent.",
45598
45847
  inputSchema: {
@@ -45750,6 +45999,7 @@ var init_spaces2 = __esm(() => {
45750
45999
  init_zod2();
45751
46000
  init_messages();
45752
46001
  init_spaces();
46002
+ init_space_notifications();
45753
46003
  init_identity();
45754
46004
  init_messages();
45755
46005
  });
@@ -45981,87 +46231,131 @@ function getSessionAgent() {
45981
46231
  function getClaudeSessionId() {
45982
46232
  return sessionClaudeId || process.env.CONVERSATIONS_SESSION_ID || null;
45983
46233
  }
45984
- function registerChannelBridge(server) {
46234
+ function registerChannelBridge(server, opts) {
45985
46235
  server.server.registerCapabilities({
45986
46236
  experimental: { "claude/channel": {} }
45987
46237
  });
46238
+ const pollIntervalMs = opts?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
46239
+ const startDelayMs = opts?.startDelayMs ?? DEFAULT_START_DELAY_MS;
45988
46240
  let lastAgentMsgId = 0;
45989
46241
  let lastSessionMsgId = 0;
45990
46242
  let pollTimer = null;
46243
+ let startTimer = null;
46244
+ let polling = false;
45991
46245
  function getSessionId() {
45992
46246
  return getClaudeSessionId();
45993
46247
  }
45994
- function seedLastSeen() {
45995
- const agent = getSessionAgent();
45996
- const sid = getSessionId();
45997
- if (agent) {
45998
- const latest = readMessages({ to: agent, order: "desc", limit: 1 });
45999
- if (latest.length > 0)
46000
- lastAgentMsgId = latest[0].id;
46001
- }
46002
- if (sid) {
46003
- const latest = readMessages({ to: `session:${sid}`, order: "desc", limit: 1 });
46004
- if (latest.length > 0)
46005
- lastSessionMsgId = latest[0].id;
46006
- }
46007
- }
46008
- function pushNotification(msg, isDirect) {
46009
- if (isDirect) {
46010
- try {
46011
- markReadByIds([msg.id]);
46012
- } catch {}
46013
- }
46014
- const senderSession = msg.session_id;
46015
- const replyHint = `To reply, use conversations send_message with to="${msg.from_agent}". For direct session injection, use send_to_session with target_session_id from the sender's session.`;
46016
- const enrichedContent = `${msg.content}
46248
+ async function pushNotification(msg, mode) {
46249
+ const enrichedContent = mode === "space_blurb" ? `${msg.from_agent} posted in #${msg.space}: ${msg.content}
46017
46250
 
46018
46251
  ---
46019
- [Via Conversations from ${msg.from_agent} (msg #${msg.id}). ${replyHint}]`;
46020
- server.server.notification({
46021
- method: "notifications/claude/channel",
46022
- params: {
46023
- content: enrichedContent,
46024
- meta: {
46025
- from: msg.from_agent,
46026
- message_id: String(msg.id),
46027
- session_id: msg.session_id,
46028
- mode: isDirect ? "direct" : "dm",
46029
- ...msg.space ? { space: msg.space } : {},
46030
- ...msg.priority && msg.priority !== "normal" ? { priority: msg.priority } : {}
46252
+ [Preview only for space message #${msg.id}. To inspect the full message later, call get_message with id=${msg.id} or run conversations show ${msg.id}.]` : `${msg.content}
46253
+
46254
+ ---
46255
+ [Via Conversations from ${msg.from_agent} (msg #${msg.id}). To reply, use conversations send_message with to="${msg.from_agent}". For direct session injection, use send_to_session with target_session_id from the sender's session.]`;
46256
+ try {
46257
+ await server.server.notification({
46258
+ method: "notifications/claude/channel",
46259
+ params: {
46260
+ content: enrichedContent,
46261
+ meta: {
46262
+ from: msg.from_agent,
46263
+ message_id: String(msg.id),
46264
+ session_id: msg.session_id,
46265
+ mode,
46266
+ ...msg.space ? { space: msg.space } : {},
46267
+ ...msg.priority && msg.priority !== "normal" ? { priority: msg.priority } : {}
46268
+ }
46269
+ }
46270
+ });
46271
+ if (mode === "direct") {
46272
+ try {
46273
+ markReadByIds([msg.id]);
46274
+ } catch {}
46275
+ } else if (mode === "space_blurb") {
46276
+ const agent = getSessionAgent();
46277
+ if (agent) {
46278
+ try {
46279
+ markSpaceNotificationsRead(agent, [msg.id]);
46280
+ } catch {}
46031
46281
  }
46032
46282
  }
46033
- }).catch(() => {});
46283
+ return true;
46284
+ } catch {
46285
+ return false;
46286
+ }
46034
46287
  }
46035
- function startPolling2() {
46036
- if (pollTimer)
46288
+ async function pollOnce() {
46289
+ if (polling)
46037
46290
  return;
46038
- seedLastSeen();
46039
- pollTimer = setInterval(() => {
46040
- try {
46041
- const agent = getSessionAgent();
46042
- const sid = getSessionId();
46043
- if (agent) {
46044
- const msgs = readMessages({ to: agent, order: "asc", limit: 20 }).filter((m) => m.id > lastAgentMsgId && m.from_agent !== agent);
46045
- for (const msg of msgs) {
46046
- lastAgentMsgId = msg.id;
46047
- pushNotification(msg, false);
46048
- }
46291
+ polling = true;
46292
+ try {
46293
+ const agent = getSessionAgent();
46294
+ const sid = getSessionId();
46295
+ if (agent) {
46296
+ const msgs = readMessages({ to: agent, unread_only: true, order: "asc", limit: 20 }).filter((m) => m.id > lastAgentMsgId && m.from_agent !== agent);
46297
+ for (const msg of msgs) {
46298
+ const delivered = await pushNotification(msg, "dm");
46299
+ if (!delivered)
46300
+ break;
46301
+ lastAgentMsgId = msg.id;
46049
46302
  }
46050
- if (sid) {
46051
- const msgs = readMessages({ to: `session:${sid}`, order: "asc", limit: 20 }).filter((m) => m.id > lastSessionMsgId && m.from_agent !== agent);
46052
- for (const msg of msgs) {
46053
- lastSessionMsgId = msg.id;
46054
- pushNotification(msg, true);
46055
- }
46303
+ }
46304
+ if (sid) {
46305
+ const msgs = readMessages({ to: `session:${sid}`, unread_only: true, order: "asc", limit: 20 }).filter((m) => m.id > lastSessionMsgId && m.from_agent !== agent);
46306
+ for (const msg of msgs) {
46307
+ const delivered = await pushNotification(msg, "direct");
46308
+ if (!delivered)
46309
+ break;
46310
+ lastSessionMsgId = msg.id;
46056
46311
  }
46057
- } catch {}
46058
- }, POLL_INTERVAL_MS);
46312
+ }
46313
+ if (agent) {
46314
+ const notifications = readSpaceNotifications({
46315
+ agent,
46316
+ unread_only: true,
46317
+ limit: 20,
46318
+ mark_read: false
46319
+ }).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.message_id - right.message_id);
46320
+ for (const notification of notifications) {
46321
+ const delivered = await pushNotification({
46322
+ id: notification.message_id,
46323
+ content: notification.preview,
46324
+ from_agent: notification.from_agent,
46325
+ session_id: `space:${notification.space}`,
46326
+ space: notification.space,
46327
+ priority: notification.priority
46328
+ }, "space_blurb");
46329
+ if (!delivered)
46330
+ break;
46331
+ }
46332
+ }
46333
+ } finally {
46334
+ polling = false;
46335
+ }
46059
46336
  }
46060
- setTimeout(() => startPolling2(), 2000);
46337
+ function startPolling2() {
46338
+ if (pollTimer)
46339
+ return;
46340
+ pollTimer = setInterval(() => {
46341
+ pollOnce().catch(() => {});
46342
+ }, pollIntervalMs);
46343
+ pollOnce().catch(() => {});
46344
+ }
46345
+ startTimer = setTimeout(() => startPolling2(), startDelayMs);
46346
+ return () => {
46347
+ if (startTimer)
46348
+ clearTimeout(startTimer);
46349
+ if (pollTimer)
46350
+ clearInterval(pollTimer);
46351
+ startTimer = null;
46352
+ pollTimer = null;
46353
+ };
46061
46354
  }
46062
- var POLL_INTERVAL_MS = 1000, sessionAgentId = null, sessionClaudeId = null;
46355
+ var DEFAULT_POLL_INTERVAL_MS = 1000, DEFAULT_START_DELAY_MS = 2000, sessionAgentId = null, sessionClaudeId = null;
46063
46356
  var init_channel = __esm(() => {
46064
46357
  init_messages();
46358
+ init_space_notifications();
46065
46359
  });
46066
46360
 
46067
46361
  // src/mcp/tools/agents.ts
@@ -46784,6 +47078,7 @@ function registerAdvancedTools(server, pkgVersion) {
46784
47078
  const all = [
46785
47079
  "send_message",
46786
47080
  "read_messages",
47081
+ "get_message",
46787
47082
  "read_digest",
46788
47083
  "list_sessions",
46789
47084
  "reply",
@@ -46799,6 +47094,11 @@ function registerAdvancedTools(server, pkgVersion) {
46799
47094
  "update_space",
46800
47095
  "archive_space",
46801
47096
  "unarchive_space",
47097
+ "subscribe_space_notifications",
47098
+ "unsubscribe_space_notifications",
47099
+ "list_space_subscriptions",
47100
+ "read_space_notifications",
47101
+ "mark_space_notifications_read",
46802
47102
  "create_project",
46803
47103
  "list_projects",
46804
47104
  "get_project",
@@ -46854,6 +47154,7 @@ function registerAdvancedTools(server, pkgVersion) {
46854
47154
  const descriptions = {
46855
47155
  send_message: "Send DM to agent. Required: to, content. Optional: from?, priority?(low|normal|high|urgent), blocking?",
46856
47156
  read_messages: "Read messages with filters. Optional: session_id?, from?, to?, space?, since?(ISO), limit?, unread_only?, mark_read?(default true \u2014 auto-marks returned messages as read, pass false to peek without consuming)",
47157
+ get_message: "Get the full content of a specific message by id. Required: id",
46857
47158
  read_digest: "Lightweight unread digest \u2014 preview only (no full bodies), auto-marks read, never overflows tokens. Returns { messages, total_unread, shown }. Optional: space?, session_id?, to?, since?(ISO), limit?, project_id?",
46858
47159
  list_sessions: "List all DM sessions. Optional: agent?(filter by participant)",
46859
47160
  reply: "Reply to a specific message, creating a thread (sets reply_to). Use read_thread to retrieve. Required: message_id, content. Optional: from?",
@@ -46871,6 +47172,11 @@ function registerAdvancedTools(server, pkgVersion) {
46871
47172
  update_space: "Update space fields. Required: name. Optional: description?, parent_id?(use 'null' to remove), project_id?(use 'null' to remove)",
46872
47173
  archive_space: "Archive a space (hidden from default list). Required: name",
46873
47174
  unarchive_space: "Restore archived space. Required: name",
47175
+ subscribe_space_notifications: "Subscribe to preview-only notifications for a space. Required: space. Optional: from?, preview_chars?",
47176
+ unsubscribe_space_notifications: "Stop preview-only notifications for a space. Required: space. Optional: from?",
47177
+ list_space_subscriptions: "List preview-only space notification subscriptions for the current agent. Optional: from?, space?",
47178
+ read_space_notifications: "Read preview-only notifications from subscribed spaces. Returns blurbs instead of full message bodies. Optional: from?, space?, unread_only?, since?, limit?, mark_read?",
47179
+ mark_space_notifications_read: "Mark preview-only space notifications as read. Optional: from?, ids?(array), space?, all?(bool)",
46874
47180
  create_project: "Create a project. Required: name. Optional: from?, description?, path?, repository?, tags?(JSON array), metadata?(JSON), settings?(JSON)",
46875
47181
  list_projects: "List projects. Optional: status?(active|archived)",
46876
47182
  get_project: "Get project by UUID or name. Required: id",
@@ -46987,6 +47293,24 @@ var init_pg_migrations = __esm(() => {
46987
47293
  PRIMARY KEY (space, agent)
46988
47294
  );
46989
47295
 
47296
+ CREATE TABLE IF NOT EXISTS space_subscriptions (
47297
+ space TEXT NOT NULL REFERENCES spaces(name),
47298
+ agent TEXT NOT NULL,
47299
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
47300
+ preview_chars INTEGER NOT NULL DEFAULT 140,
47301
+ since_message_id BIGINT NOT NULL DEFAULT 0,
47302
+ PRIMARY KEY (space, agent)
47303
+ );
47304
+ CREATE INDEX IF NOT EXISTS idx_space_subscriptions_agent ON space_subscriptions(agent);
47305
+ CREATE INDEX IF NOT EXISTS idx_space_subscriptions_space ON space_subscriptions(space);
47306
+ ALTER TABLE space_subscriptions ADD COLUMN IF NOT EXISTS since_message_id BIGINT NOT NULL DEFAULT 0;
47307
+ UPDATE space_subscriptions ss
47308
+ SET since_message_id = COALESCE(
47309
+ (SELECT MAX(m.id) FROM messages m WHERE m.space = ss.space),
47310
+ 0
47311
+ )
47312
+ WHERE ss.since_message_id = 0;
47313
+
46990
47314
  CREATE TABLE IF NOT EXISTS messages (
46991
47315
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
46992
47316
  uuid TEXT NOT NULL DEFAULT gen_random_uuid()::text UNIQUE,
@@ -47064,6 +47388,15 @@ var init_pg_migrations = __esm(() => {
47064
47388
  CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id);
47065
47389
  CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent);
47066
47390
 
47391
+ CREATE TABLE IF NOT EXISTS space_notification_reads (
47392
+ agent TEXT NOT NULL,
47393
+ message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
47394
+ read_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
47395
+ PRIMARY KEY (agent, message_id)
47396
+ );
47397
+ CREATE INDEX IF NOT EXISTS idx_space_notification_reads_agent ON space_notification_reads(agent);
47398
+ CREATE INDEX IF NOT EXISTS idx_space_notification_reads_message ON space_notification_reads(message_id);
47399
+
47067
47400
  CREATE TABLE IF NOT EXISTS message_mentions (
47068
47401
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
47069
47402
  message_id BIGINT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
@@ -47497,11 +47830,11 @@ ${msg.text}`;
47497
47830
  } catch {}
47498
47831
  }
47499
47832
  setTimeout(() => {
47500
- pollTimer = setInterval(() => poll(), POLL_INTERVAL_MS2);
47833
+ pollTimer = setInterval(() => poll(), POLL_INTERVAL_MS);
47501
47834
  console.error("[telegram-channel] polling started");
47502
47835
  }, 2000);
47503
47836
  }
47504
- var POLL_INTERVAL_MS2 = 2000;
47837
+ var POLL_INTERVAL_MS = 2000;
47505
47838
  var init_telegram_channel = __esm(() => {
47506
47839
  init_zod2();
47507
47840
  });
@@ -49362,7 +49695,7 @@ init_db();
49362
49695
  init_identity();
49363
49696
  init_terminal_markdown();
49364
49697
  init_presence();
49365
- init_db();
49698
+ init_space_notifications();
49366
49699
  import chalk4 from "chalk";
49367
49700
  function registerMessagingCommands(program2) {
49368
49701
  program2.command("send").description("Send a message to an agent").argument("<message>", "Message content").option("--to <agent>", "Recipient agent ID (required unless --space is used)").option("--from <agent>", "Sender agent ID").option("--session <id>", "Session ID (auto-generated if omitted)").option("--priority <level>", "Priority: low, normal, high, urgent", "normal").option("--working-dir <path>", "Working directory context").option("--repository <repo>", "Repository context").option("--branch <branch>", "Branch context").option("--metadata <json>", "JSON metadata string").option("--space <name>", "Send to a space instead of a specific agent").option("--blocking", "Send as a blocking message (recipient must acknowledge)").option("-j, --json", "Output as JSON").action((message, opts) => {
@@ -49459,6 +49792,32 @@ function registerMessagingCommands(program2) {
49459
49792
  }
49460
49793
  closeDb();
49461
49794
  });
49795
+ program2.command("show").description("Show a full message by ID").argument("<id>", "Numeric message ID").option("-j, --json", "Output as JSON").action((idArg, opts) => {
49796
+ const id = Number.parseInt(String(idArg), 10);
49797
+ if (!Number.isFinite(id) || id <= 0) {
49798
+ console.error(chalk4.red("Message ID must be a positive integer."));
49799
+ process.exit(1);
49800
+ }
49801
+ const msg = getMessageById(id);
49802
+ if (!msg) {
49803
+ console.error(chalk4.red(`Message #${id} not found.`));
49804
+ process.exit(1);
49805
+ }
49806
+ if (opts.json) {
49807
+ console.log(JSON.stringify(msg, null, 2));
49808
+ } else {
49809
+ const time = chalk4.dim(msg.created_at.slice(0, 19).replace("T", " "));
49810
+ const destination = msg.space ? chalk4.magenta(`#${msg.space}`) : chalk4.yellow(msg.to_agent);
49811
+ const priority = msg.priority !== "normal" ? chalk4.red(` [${msg.priority}]`) : "";
49812
+ const unread = !msg.read_at ? chalk4.green(" [unread]") : "";
49813
+ console.log(`${chalk4.cyan(msg.from_agent)} \u2192 ${destination}${priority}${unread} ${chalk4.dim(`[#${msg.id}] ${time}`)}`);
49814
+ if (msg.attachments?.length) {
49815
+ console.log(chalk4.dim(`Attachments: ${msg.attachments.map((att) => att.name).join(", ")}`));
49816
+ }
49817
+ console.log(renderContent(msg.content));
49818
+ }
49819
+ closeDb();
49820
+ });
49462
49821
  program2.command("digest").description("Show unread message digest (preview only, auto-marks read)").argument("[space]", "Space name to digest (omit for DMs)").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to show", parseInt).option("--to <agent>", "Filter by recipient (for DMs)").option("-j, --json", "Output as JSON").action((spaceArg, opts) => {
49463
49822
  const result = readDigest({
49464
49823
  space: spaceArg || undefined,
@@ -49734,6 +50093,44 @@ Acknowledge with: conversations mark-read ${blockers.map((b) => b.id).join(" ")}
49734
50093
  }
49735
50094
  closeDb();
49736
50095
  });
50096
+ program2.command("notifications").description("List preview-only notifications from subscribed spaces").option("--from <agent>", "Your agent identity").option("--space <name>", "Filter to a single space").option("--since <timestamp>", "Notifications after this ISO timestamp").option("--limit <n>", "Max notifications to return", parseInt).option("--all", "Include already-read notifications").option("--mark-read", "Mark returned notifications as read").option("--clear", "Mark all matching unread notifications as read without listing").option("-j, --json", "Output as JSON").action((opts) => {
50097
+ const agent = resolveIdentity(opts.from);
50098
+ heartbeat(agent);
50099
+ if (opts.clear) {
50100
+ const cleared = markAllSpaceNotificationsRead(agent, opts.space);
50101
+ if (opts.json) {
50102
+ console.log(JSON.stringify({ cleared, agent, space: opts.space || null }, null, 2));
50103
+ } else {
50104
+ console.log(chalk4.green(`Cleared ${cleared} notification(s).`));
50105
+ }
50106
+ closeDb();
50107
+ return;
50108
+ }
50109
+ const notifications = readSpaceNotifications({
50110
+ agent,
50111
+ space: opts.space,
50112
+ since: opts.since,
50113
+ unread_only: !opts.all,
50114
+ limit: opts.limit,
50115
+ mark_read: opts.markRead
50116
+ });
50117
+ if (opts.json) {
50118
+ console.log(JSON.stringify(notifications, null, 2));
50119
+ } else if (notifications.length === 0) {
50120
+ console.log(chalk4.dim("No space notifications."));
50121
+ } else {
50122
+ for (const item of notifications) {
50123
+ const time = chalk4.dim(item.created_at.slice(11, 19));
50124
+ const priority = item.priority !== "normal" ? chalk4.red(` [${item.priority}]`) : "";
50125
+ const unread = item.unread ? chalk4.yellow(" [unread]") : "";
50126
+ console.log(`${time} ${chalk4.cyan(item.from_agent)} ${chalk4.magenta(`#${item.space}`)}${priority}${unread} ${chalk4.dim(`msg #${item.message_id}`)}`);
50127
+ console.log(` ${item.preview}`);
50128
+ }
50129
+ console.log(chalk4.dim(`
50130
+ Inspect the full message later with: conversations show <message-id>`));
50131
+ }
50132
+ closeDb();
50133
+ });
49737
50134
  program2.command("watch").description("Watch for new messages with desktop notifications").option("--from <agent>", "Your agent identity").option("--space <name>", "Watch a specific space").option("--all", "Watch DMs and all subscribed spaces").option("--interval <ms>", "Poll interval in milliseconds", parseInt).action((opts) => {
49738
50135
  const agent = resolveIdentity(opts.from);
49739
50136
  heartbeat(agent);
@@ -49741,9 +50138,7 @@ Acknowledge with: conversations mark-read ${blockers.map((b) => b.id).join(" ")}
49741
50138
  const cols = Math.min(process.stdout.columns || 80, 100);
49742
50139
  let agentSpaces = [];
49743
50140
  if (opts.all) {
49744
- const db2 = getDb();
49745
- const rows = db2.prepare("SELECT space FROM space_members WHERE agent = ?").all(agent);
49746
- agentSpaces = rows.map((r) => r.space);
50141
+ agentSpaces = listSpaceNotificationSubscriptions(agent).map((row) => row.space);
49747
50142
  }
49748
50143
  const modeLabel = opts.all ? `DMs + ${agentSpaces.length} space(s)` : opts.space ? `Space: #${opts.space}` : "All DMs";
49749
50144
  console.log("");
@@ -49778,19 +50173,39 @@ Acknowledge with: conversations mark-read ${blockers.map((b) => b.id).join(" ")}
49778
50173
  console.log(chalk4.dim(" " + "\xB7".repeat(Math.min(cols - 8, 60))));
49779
50174
  console.log("");
49780
50175
  };
50176
+ const renderNotification = (notification) => {
50177
+ const time = chalk4.dim(notification.created_at.slice(11, 19));
50178
+ const priority = notification.priority !== "normal" ? notification.priority === "urgent" ? chalk4.red.bold(` [${notification.priority}]`) : notification.priority === "high" ? chalk4.yellow(` [${notification.priority}]`) : chalk4.dim(` [${notification.priority}]`) : "";
50179
+ const sender = chalk4.cyan.bold(notification.from_agent);
50180
+ console.log(` ${sender} ${chalk4.magenta(`#${notification.space}`)} ${time}${priority} ${chalk4.dim(`[#${notification.message_id}]`)}`);
50181
+ console.log(` ${notification.preview}`);
50182
+ console.log(chalk4.dim(` Preview only. Inspect with: conversations show ${notification.message_id}`));
50183
+ console.log(chalk4.dim(" " + "\xB7".repeat(Math.min(cols - 8, 60))));
50184
+ console.log("");
50185
+ };
49781
50186
  if (opts.all) {
49782
50187
  const dmRecent = readMessages({ to: agent, limit: 20, order: "asc" });
49783
- const spaceRecent = [];
49784
- for (const sp of agentSpaces) {
49785
- spaceRecent.push(...readMessages({ space: sp, limit: 10, order: "asc" }));
49786
- }
49787
- const recent = [...dmRecent, ...spaceRecent].sort((a, b) => a.created_at.localeCompare(b.created_at)).slice(-20);
49788
- if (recent.length > 0) {
49789
- console.log(chalk4.dim(` \u2500\u2500 Recent messages (${recent.length}) \u2500\u2500
50188
+ const pendingNotifications = readSpaceNotifications({
50189
+ agent,
50190
+ unread_only: true,
50191
+ limit: 20,
50192
+ mark_read: true
50193
+ }).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.message_id - right.message_id);
50194
+ if (dmRecent.length > 0) {
50195
+ console.log(chalk4.dim(` \u2500\u2500 Recent DMs (${dmRecent.length}) \u2500\u2500
49790
50196
  `));
49791
- for (const msg of recent) {
50197
+ for (const msg of dmRecent) {
49792
50198
  renderMessage(msg);
49793
50199
  }
50200
+ }
50201
+ if (pendingNotifications.length > 0) {
50202
+ console.log(chalk4.dim(` \u2500\u2500 Pending space notifications (${pendingNotifications.length}) \u2500\u2500
50203
+ `));
50204
+ for (const notification of pendingNotifications) {
50205
+ renderNotification(notification);
50206
+ }
50207
+ }
50208
+ if (dmRecent.length > 0 || pendingNotifications.length > 0) {
49794
50209
  console.log(chalk4.dim(` \u2500\u2500 Live \u2500\u2500
49795
50210
  `));
49796
50211
  }
@@ -49817,24 +50232,50 @@ Acknowledge with: conversations mark-read ${blockers.map((b) => b.id).join(" ")}
49817
50232
  continue;
49818
50233
  renderMessage(msg);
49819
50234
  const where = msg.space ? `#${msg.space}` : "DM";
49820
- const preview = msg.content.replace(/[*#`~_>\-]/g, "").slice(0, 150);
50235
+ const preview = buildMessagePreview(msg.content, 150);
49821
50236
  desktopNotify(`${msg.from_agent} (${where})`, preview);
49822
50237
  }
49823
50238
  };
49824
- if (opts.all) {
49825
- startPolling2({ to_agent: agent, interval_ms: interval, on_messages: onNewMessages });
49826
- for (const sp of agentSpaces) {
49827
- startPolling2({ space: sp, interval_ms: interval, on_messages: onNewMessages });
50239
+ const onNewNotifications = (notifications) => {
50240
+ for (const notification of notifications) {
50241
+ renderNotification(notification);
50242
+ desktopNotify(`${notification.from_agent} (#${notification.space})`, notification.preview);
49828
50243
  }
50244
+ };
50245
+ const stops = [];
50246
+ if (opts.all) {
50247
+ stops.push(startPolling2({ to_agent: agent, interval_ms: interval, on_messages: onNewMessages }));
50248
+ let inFlightNotifications = false;
50249
+ const timer = setInterval(() => {
50250
+ if (inFlightNotifications)
50251
+ return;
50252
+ inFlightNotifications = true;
50253
+ try {
50254
+ const notifications = readSpaceNotifications({
50255
+ agent,
50256
+ unread_only: true,
50257
+ limit: 200,
50258
+ mark_read: true
50259
+ }).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.message_id - right.message_id);
50260
+ if (notifications.length > 0) {
50261
+ onNewNotifications(notifications);
50262
+ }
50263
+ } finally {
50264
+ inFlightNotifications = false;
50265
+ }
50266
+ }, interval);
50267
+ stops.push({ stop: () => clearInterval(timer) });
49829
50268
  } else {
49830
- startPolling2({
50269
+ stops.push(startPolling2({
49831
50270
  to_agent: opts.space ? undefined : agent,
49832
50271
  space: opts.space,
49833
50272
  interval_ms: interval,
49834
50273
  on_messages: onNewMessages
49835
- });
50274
+ }));
49836
50275
  }
49837
50276
  process.on("SIGINT", () => {
50277
+ for (const stop of stops)
50278
+ stop.stop();
49838
50279
  console.log(chalk4.dim(`
49839
50280
  Stopped watching.`));
49840
50281
  closeDb();
@@ -49900,6 +50341,7 @@ Update failed (exit code ${exitCode})`));
49900
50341
  // src/cli/commands/spaces.ts
49901
50342
  init_messages();
49902
50343
  init_spaces();
50344
+ init_space_notifications();
49903
50345
  init_db();
49904
50346
  init_identity();
49905
50347
  init_terminal_markdown();
@@ -50067,7 +50509,7 @@ function registerSpaceCommands(program2) {
50067
50509
  }
50068
50510
  closeDb();
50069
50511
  });
50070
- space.command("read").description("Read messages from a space").argument("<space>", "Space name").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("-j, --json", "Output as JSON").action((spaceName, opts) => {
50512
+ space.command("read").description("Read messages from a space").argument("<space>", "Space name").option("--from <agent>", "Agent reading the space").option("--since <timestamp>", "Messages after this ISO timestamp").option("--limit <n>", "Max messages to return", parseInt).option("-j, --json", "Output as JSON").action((spaceName, opts) => {
50071
50513
  const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
50072
50514
  if (!spaceArg) {
50073
50515
  console.error(chalk5.red("Space name cannot be empty."));
@@ -50078,6 +50520,15 @@ function registerSpaceCommands(program2) {
50078
50520
  since: opts.since,
50079
50521
  limit: opts.limit
50080
50522
  });
50523
+ if (opts.from && messages.length > 0) {
50524
+ const agent = resolveIdentity(opts.from).trim();
50525
+ if (!agent) {
50526
+ console.error(chalk5.red("Agent identity is required."));
50527
+ process.exit(1);
50528
+ }
50529
+ recordReadReceiptsBatch(messages.map((m) => m.id), agent);
50530
+ markSpaceNotificationsRead(agent, messages.map((m) => m.id));
50531
+ }
50081
50532
  if (opts.json) {
50082
50533
  console.log(JSON.stringify(messages, null, 2));
50083
50534
  } else {
@@ -50145,6 +50596,73 @@ function registerSpaceCommands(program2) {
50145
50596
  }
50146
50597
  closeDb();
50147
50598
  });
50599
+ space.command("subscribe").description("Subscribe to preview-only notifications for a space").argument("<space>", "Space name").option("--from <agent>", "Agent ID").option("--preview-chars <n>", "Preview length", parseInt).option("-j, --json", "Output as JSON").action((spaceName, opts) => {
50600
+ const agent = resolveIdentity(opts.from).trim();
50601
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
50602
+ if (!agent) {
50603
+ console.error(chalk5.red("Agent identity is required."));
50604
+ process.exit(1);
50605
+ }
50606
+ if (!spaceArg) {
50607
+ console.error(chalk5.red("Space name cannot be empty."));
50608
+ process.exit(1);
50609
+ }
50610
+ try {
50611
+ const subscription = subscribeToSpaceNotifications(spaceArg, agent, { preview_chars: opts.previewChars });
50612
+ if (opts.json) {
50613
+ console.log(JSON.stringify(subscription, null, 2));
50614
+ } else {
50615
+ console.log(chalk5.green(`${agent} subscribed to #${spaceArg} notifications`) + chalk5.dim(` (${subscription.preview_chars} chars)`));
50616
+ }
50617
+ } catch (e) {
50618
+ console.error(chalk5.red(e.message));
50619
+ process.exit(1);
50620
+ }
50621
+ closeDb();
50622
+ });
50623
+ space.command("unsubscribe").description("Stop preview-only notifications for a space").argument("<space>", "Space name").option("--from <agent>", "Agent ID").option("-j, --json", "Output as JSON").action((spaceName, opts) => {
50624
+ const agent = resolveIdentity(opts.from).trim();
50625
+ const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
50626
+ if (!agent) {
50627
+ console.error(chalk5.red("Agent identity is required."));
50628
+ process.exit(1);
50629
+ }
50630
+ if (!spaceArg) {
50631
+ console.error(chalk5.red("Space name cannot be empty."));
50632
+ process.exit(1);
50633
+ }
50634
+ const unsubscribed = unsubscribeFromSpaceNotifications(spaceArg, agent);
50635
+ if (opts.json) {
50636
+ console.log(JSON.stringify({ space: spaceArg, agent, unsubscribed }));
50637
+ } else if (unsubscribed) {
50638
+ console.log(chalk5.green(`${agent} unsubscribed from #${spaceArg} notifications`));
50639
+ } else {
50640
+ console.log(chalk5.dim(`${agent} had no notification subscription for #${spaceArg}`));
50641
+ }
50642
+ closeDb();
50643
+ });
50644
+ space.command("subscriptions").description("List preview-only space notification subscriptions").option("--from <agent>", "Agent ID").option("--space <name>", "Filter by space").option("-j, --json", "Output as JSON").action((opts) => {
50645
+ const agent = resolveIdentity(opts.from).trim();
50646
+ if (!agent) {
50647
+ console.error(chalk5.red("Agent identity is required."));
50648
+ process.exit(1);
50649
+ }
50650
+ let subscriptions = listSpaceNotificationSubscriptions(agent);
50651
+ if (opts.space) {
50652
+ subscriptions = subscriptions.filter((row) => row.space === opts.space);
50653
+ }
50654
+ if (opts.json) {
50655
+ console.log(JSON.stringify(subscriptions, null, 2));
50656
+ } else if (subscriptions.length === 0) {
50657
+ console.log(chalk5.dim(`No notification subscriptions for ${agent}.`));
50658
+ } else {
50659
+ console.log(chalk5.bold(`${agent} notification subscriptions:`));
50660
+ for (const row of subscriptions) {
50661
+ console.log(` ${chalk5.magenta(`#${row.space}`)} ${chalk5.dim(`preview ${row.preview_chars} chars`)}`);
50662
+ }
50663
+ }
50664
+ closeDb();
50665
+ });
50148
50666
  space.command("members").description("List space members").argument("<space>", "Space name").option("-j, --json", "Output as JSON").action((spaceName, opts) => {
50149
50667
  const spaceArg = typeof spaceName === "string" ? spaceName.trim() : "";
50150
50668
  if (!spaceArg) {
@@ -50573,6 +51091,7 @@ init_hot();
50573
51091
  init_topics();
50574
51092
  init_summary();
50575
51093
  init_graph();
51094
+ init_space_notifications();
50576
51095
  var import__package = __toESM(require_package(), 1);
50577
51096
  import chalk8 from "chalk";
50578
51097
  function registerAnalyticsCommands(program2) {
@@ -50726,8 +51245,22 @@ function registerAnalyticsCommands(program2) {
50726
51245
  WHERE sm.agent = ?
50727
51246
  ORDER BY s.name
50728
51247
  `).all(agent);
51248
+ const subscriptions = listSpaceNotificationSubscriptions(agent);
51249
+ const spaceNotifications = readSpaceNotifications({
51250
+ agent,
51251
+ unread_only: true,
51252
+ limit: 5
51253
+ });
50729
51254
  const recentDMs = readMessages({ to: agent, limit: 3 });
50730
- const context = { agent, online_agents: onlineAgents, unread_dms: unreadDMs, spaces: mySpaces, recent_dms: recentDMs };
51255
+ const context = {
51256
+ agent,
51257
+ online_agents: onlineAgents,
51258
+ unread_dms: unreadDMs,
51259
+ spaces: mySpaces,
51260
+ space_subscriptions: subscriptions,
51261
+ space_notifications: spaceNotifications,
51262
+ recent_dms: recentDMs
51263
+ };
50731
51264
  if (opts.json) {
50732
51265
  console.log(JSON.stringify(context, null, 2));
50733
51266
  } else {
@@ -50756,6 +51289,24 @@ function registerAnalyticsCommands(program2) {
50756
51289
  } else {
50757
51290
  console.log(`${chalk8.bold("My spaces:")} ${chalk8.dim("none")}`);
50758
51291
  }
51292
+ if (subscriptions.length > 0) {
51293
+ console.log(`${chalk8.bold("Subscribed spaces:")}`);
51294
+ for (const row of subscriptions) {
51295
+ console.log(` ${chalk8.magenta("#" + row.space)} ${chalk8.dim(`preview ${row.preview_chars} chars`)}`);
51296
+ }
51297
+ } else {
51298
+ console.log(`${chalk8.bold("Subscribed spaces:")} ${chalk8.dim("none")}`);
51299
+ }
51300
+ if (spaceNotifications.length > 0) {
51301
+ console.log(`${chalk8.bold("Space notifications:")}`);
51302
+ for (const notification of spaceNotifications) {
51303
+ console.log(` ${chalk8.dim(notification.created_at.slice(11, 16))} ${chalk8.cyan(notification.from_agent)} ${chalk8.magenta("#" + notification.space)} ${chalk8.dim(`msg #${notification.message_id}`)}`);
51304
+ console.log(` ${chalk8.dim(notification.preview)}`);
51305
+ }
51306
+ console.log(chalk8.dim(" Inspect with: conversations show <message-id>"));
51307
+ } else {
51308
+ console.log(`${chalk8.bold("Space notifications:")} ${chalk8.dim("none")}`);
51309
+ }
50759
51310
  }
50760
51311
  closeDb();
50761
51312
  });