claudemesh-cli 1.34.7 → 1.34.9

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.
@@ -104,7 +104,7 @@ __export(exports_urls, {
104
104
  VERSION: () => VERSION,
105
105
  URLS: () => URLS
106
106
  });
107
- var URLS, VERSION = "1.34.7", env;
107
+ var URLS, VERSION = "1.34.9", env;
108
108
  var init_urls = __esm(() => {
109
109
  URLS = {
110
110
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -4052,11 +4052,18 @@ async function tryListPeersViaDaemon(mesh) {
4052
4052
  return null;
4053
4053
  }
4054
4054
  }
4055
- async function tryListInboxViaDaemon(mesh, limit = 100) {
4055
+ async function tryListInboxViaDaemon(mesh, limit = 100, opts = {}) {
4056
4056
  if (!await daemonReachable())
4057
4057
  return null;
4058
4058
  try {
4059
- const path = `/v1/inbox${meshQuery(mesh)}${meshQuery(mesh) ? "&" : "?"}limit=${limit}`;
4059
+ const params = [`limit=${limit}`];
4060
+ if (mesh)
4061
+ params.push(`mesh=${encodeURIComponent(mesh)}`);
4062
+ if (opts.unreadOnly)
4063
+ params.push("unread_only=true");
4064
+ if (opts.markSeen === false)
4065
+ params.push("mark_seen=false");
4066
+ const path = `/v1/inbox?${params.join("&")}`;
4060
4067
  const res = await ipc({ path, timeoutMs: 3000 });
4061
4068
  if (res.status !== 200)
4062
4069
  return null;
@@ -4324,6 +4331,7 @@ async function ensureDaemonRunning(meshSlug, quiet) {
4324
4331
  if (res.state === "up") {
4325
4332
  if (!quiet)
4326
4333
  render.ok("daemon already running");
4334
+ await warnIfDaemonStale(quiet);
4327
4335
  return;
4328
4336
  }
4329
4337
  if (res.state === "started") {
@@ -4333,6 +4341,21 @@ async function ensureDaemonRunning(meshSlug, quiet) {
4333
4341
  }
4334
4342
  render.warn(`daemon ${res.state}${res.reason ? `: ${res.reason}` : ""}`, "Run `claudemesh daemon up --mesh " + meshSlug + "` manually, then re-launch.");
4335
4343
  }
4344
+ async function warnIfDaemonStale(quiet) {
4345
+ if (quiet)
4346
+ return;
4347
+ try {
4348
+ const { ipc: ipc2 } = await Promise.resolve().then(() => (init_client3(), exports_client));
4349
+ const { VERSION: VERSION2 } = await Promise.resolve().then(() => (init_urls(), exports_urls));
4350
+ const res = await ipc2({ path: "/v1/version", timeoutMs: 1500 });
4351
+ if (res.status !== 200)
4352
+ return;
4353
+ const daemonVersion = res.body.daemon_version ?? "";
4354
+ if (!daemonVersion || daemonVersion === VERSION2)
4355
+ return;
4356
+ render.warn(`daemon is ${daemonVersion}, CLI is ${VERSION2} — restart to pick up new fixes.`, "Run: `claudemesh daemon down && claudemesh daemon up` (or restart the launchd / systemd-user unit).");
4357
+ } catch {}
4358
+ }
4336
4359
  function parseGroupsString(raw) {
4337
4360
  return raw.split(",").map((s) => s.trim()).filter(Boolean).map((token) => {
4338
4361
  const idx = token.indexOf(":");
@@ -8380,7 +8403,10 @@ function formatMessage(msg, includeMesh) {
8380
8403
  }
8381
8404
  async function runInbox(flags) {
8382
8405
  const meshSlug = flags.mesh;
8383
- const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100);
8406
+ const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100, {
8407
+ unreadOnly: flags.unread === true,
8408
+ markSeen: true
8409
+ });
8384
8410
  if (items === null) {
8385
8411
  if (flags.json) {
8386
8412
  process.stdout.write(`[]
@@ -8397,10 +8423,12 @@ async function runInbox(flags) {
8397
8423
  }
8398
8424
  if (items.length === 0) {
8399
8425
  const scope = meshSlug ? `mesh "${meshSlug}"` : "any mesh";
8400
- render.info(dim(`No messages on ${scope}.`));
8426
+ const filter = flags.unread ? "unread " : "";
8427
+ render.info(dim(`No ${filter}messages on ${scope}.`));
8401
8428
  return;
8402
8429
  }
8403
- const heading = meshSlug ? `inbox — ${meshSlug} (${items.length} message${items.length === 1 ? "" : "s"})` : `inbox (${items.length} message${items.length === 1 ? "" : "s"})`;
8430
+ const filterTag = flags.unread ? " unread" : "";
8431
+ const heading = meshSlug ? `inbox — ${meshSlug} (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})` : `inbox (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})`;
8404
8432
  render.section(heading);
8405
8433
  for (const msg of items) {
8406
8434
  process.stdout.write(formatMessage(msg, !meshSlug) + `
@@ -9582,6 +9610,11 @@ function migrateInbox(db) {
9582
9610
  CREATE INDEX IF NOT EXISTS inbox_topic ON inbox(topic);
9583
9611
  CREATE INDEX IF NOT EXISTS inbox_sender ON inbox(sender_pubkey);
9584
9612
  `);
9613
+ const cols = db.prepare(`PRAGMA table_info(inbox)`).all();
9614
+ if (!cols.some((c) => c.name === "seen_at")) {
9615
+ db.exec(`ALTER TABLE inbox ADD COLUMN seen_at INTEGER`);
9616
+ db.exec(`CREATE INDEX IF NOT EXISTS inbox_seen_at ON inbox(seen_at)`);
9617
+ }
9585
9618
  }
9586
9619
  function insertIfNew(db, row) {
9587
9620
  const before = db.prepare(`SELECT id FROM inbox WHERE client_message_id = ?`).get(row.client_message_id);
@@ -9616,9 +9649,12 @@ function listInbox(db, p) {
9616
9649
  where.push("mesh = ?");
9617
9650
  args.push(p.mesh);
9618
9651
  }
9652
+ if (p.unreadOnly === true) {
9653
+ where.push("seen_at IS NULL");
9654
+ }
9619
9655
  const sql = `
9620
9656
  SELECT id, client_message_id, broker_message_id, mesh, topic,
9621
- sender_pubkey, sender_name, body, meta, received_at, reply_to_id
9657
+ sender_pubkey, sender_name, body, meta, received_at, reply_to_id, seen_at
9622
9658
  FROM inbox
9623
9659
  ${where.length ? "WHERE " + where.join(" AND ") : ""}
9624
9660
  ORDER BY received_at DESC
@@ -9627,6 +9663,17 @@ function listInbox(db, p) {
9627
9663
  args.push(Math.min(Math.max(p.limit ?? 100, 1), 1000));
9628
9664
  return db.prepare(sql).all(...args);
9629
9665
  }
9666
+ function markInboxSeen(db, ids, now = Date.now()) {
9667
+ if (ids.length === 0)
9668
+ return 0;
9669
+ const placeholders = ids.map(() => "?").join(",");
9670
+ const r = db.prepare(`UPDATE inbox SET seen_at = ? WHERE seen_at IS NULL AND id IN (${placeholders})`).run(now, ...ids);
9671
+ return Number(r.changes);
9672
+ }
9673
+ function pruneInboxBefore(db, cutoffMs) {
9674
+ const r = db.prepare(`DELETE FROM inbox WHERE received_at < ?`).run(cutoffMs);
9675
+ return Number(r.changes);
9676
+ }
9630
9677
  function deleteInboxRow(db, id) {
9631
9678
  const r = db.prepare(`DELETE FROM inbox WHERE id = ?`).run(id);
9632
9679
  return Number(r.changes) > 0;
@@ -10387,13 +10434,23 @@ function makeHandler(opts) {
10387
10434
  const limitRaw = url.searchParams.get("limit");
10388
10435
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
10389
10436
  const meshFilter = meshFromCtx(url.searchParams.get("mesh")) ?? undefined;
10437
+ const unreadOnly = url.searchParams.get("unread_only") === "true";
10438
+ const markSeen = url.searchParams.get("mark_seen") !== "false";
10390
10439
  const rows = listInbox(opts.inboxDb, {
10391
10440
  since: Number.isFinite(since) ? since : undefined,
10392
10441
  topic,
10393
10442
  fromPubkey,
10394
10443
  ...meshFilter ? { mesh: meshFilter } : {},
10444
+ unreadOnly,
10395
10445
  limit: Number.isFinite(limit ?? NaN) ? limit : undefined
10396
10446
  });
10447
+ let flippedCount = 0;
10448
+ if (markSeen) {
10449
+ const unreadIds = rows.filter((r) => r.seen_at == null).map((r) => r.id);
10450
+ if (unreadIds.length > 0) {
10451
+ flippedCount = markInboxSeen(opts.inboxDb, unreadIds);
10452
+ }
10453
+ }
10397
10454
  respond(res, 200, {
10398
10455
  items: rows.map((r) => ({
10399
10456
  id: r.id,
@@ -10405,11 +10462,32 @@ function makeHandler(opts) {
10405
10462
  sender_name: r.sender_name,
10406
10463
  body: r.body,
10407
10464
  received_at: new Date(r.received_at).toISOString(),
10408
- reply_to_id: r.reply_to_id
10409
- }))
10465
+ reply_to_id: r.reply_to_id,
10466
+ seen_at: r.seen_at ? new Date(r.seen_at).toISOString() : null
10467
+ })),
10468
+ marked_seen: flippedCount
10410
10469
  });
10411
10470
  return;
10412
10471
  }
10472
+ if (req.method === "POST" && url.pathname === "/v1/inbox/seen") {
10473
+ if (!opts.inboxDb) {
10474
+ respond(res, 503, { error: "inbox not initialised" });
10475
+ return;
10476
+ }
10477
+ try {
10478
+ const body = await readJsonBody(req, 64 * 1024);
10479
+ const ids = Array.isArray(body?.ids) ? body.ids.filter((x) => typeof x === "string") : [];
10480
+ if (ids.length === 0) {
10481
+ respond(res, 400, { error: "missing 'ids' (string[])" });
10482
+ return;
10483
+ }
10484
+ const flipped = markInboxSeen(opts.inboxDb, ids);
10485
+ respond(res, 200, { marked_seen: flipped });
10486
+ } catch (e) {
10487
+ respond(res, 400, { error: String(e) });
10488
+ }
10489
+ return;
10490
+ }
10413
10491
  if (req.method === "DELETE" && url.pathname === "/v1/inbox") {
10414
10492
  if (!opts.inboxDb) {
10415
10493
  respond(res, 503, { error: "inbox not initialised" });
@@ -11376,6 +11454,13 @@ class SessionBrokerClient {
11376
11454
  return;
11377
11455
  }
11378
11456
  if (msg.type === "push" || msg.type === "inbound") {
11457
+ if (msg.subtype === "system")
11458
+ return;
11459
+ const senderPubkey = String(msg.senderPubkey ?? "").toLowerCase();
11460
+ if (senderPubkey && senderPubkey === this.opts.sessionPubkey.toLowerCase()) {
11461
+ this.log("info", "self_echo_dropped", { sender: senderPubkey.slice(0, 12) });
11462
+ return;
11463
+ }
11379
11464
  this.opts.onPush?.(msg);
11380
11465
  return;
11381
11466
  }
@@ -11667,14 +11752,58 @@ function defaultLog4(level, msg, meta) {
11667
11752
  var POLL_INTERVAL_MS = 500, MAX_ATTEMPTS_PER_ROW = 25, BACKOFF_BASE_MS = 500, BACKOFF_CAP_MS = 30000;
11668
11753
  var init_drain = () => {};
11669
11754
 
11755
+ // src/daemon/inbox-pruner.ts
11756
+ function startInboxPruner(opts) {
11757
+ const retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
11758
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
11759
+ const log2 = opts.log ?? defaultLog5;
11760
+ const tick = () => {
11761
+ try {
11762
+ const cutoff = Date.now() - retentionMs;
11763
+ const removed = pruneInboxBefore(opts.db, cutoff);
11764
+ if (removed > 0) {
11765
+ log2("info", "inbox_prune_completed", {
11766
+ removed,
11767
+ retention_days: Math.round(retentionMs / (24 * 60 * 60 * 1000))
11768
+ });
11769
+ }
11770
+ } catch (e) {
11771
+ log2("warn", "inbox_prune_failed", { err: String(e) });
11772
+ }
11773
+ };
11774
+ tick();
11775
+ const handle = setInterval(tick, intervalMs);
11776
+ if (typeof handle.unref === "function")
11777
+ handle.unref();
11778
+ return { stop: () => clearInterval(handle) };
11779
+ }
11780
+ function defaultLog5(level, msg, meta) {
11781
+ const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
11782
+ if (level === "info")
11783
+ process.stdout.write(line + `
11784
+ `);
11785
+ else
11786
+ process.stderr.write(line + `
11787
+ `);
11788
+ }
11789
+ var DEFAULT_RETENTION_MS, DEFAULT_INTERVAL_MS;
11790
+ var init_inbox_pruner = __esm(() => {
11791
+ DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
11792
+ DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
11793
+ });
11794
+
11670
11795
  // src/daemon/inbound.ts
11671
11796
  import { randomUUID as randomUUID4 } from "node:crypto";
11672
11797
  async function handleBrokerPush(msg, ctx) {
11673
11798
  if (msg.subtype === "system" && typeof msg.event === "string") {
11799
+ const eventData = msg.eventData ?? {};
11800
+ const eventPubkey = typeof eventData.pubkey === "string" ? eventData.pubkey : "";
11801
+ if (eventPubkey && ctx.isOwnPubkey?.(eventPubkey))
11802
+ return;
11674
11803
  ctx.bus.publish(mapSystemEventKind(msg.event), {
11675
11804
  mesh: ctx.meshSlug,
11676
11805
  event: msg.event,
11677
- ...msg.eventData ?? {}
11806
+ ...eventData
11678
11807
  });
11679
11808
  return;
11680
11809
  }
@@ -11966,6 +12095,8 @@ async function runDaemon(opts = {}) {
11966
12095
  } else {
11967
12096
  meshes = cfg.meshes;
11968
12097
  }
12098
+ const sessionBrokers = new Map;
12099
+ const sessionBrokersByPubkey = new Map;
11969
12100
  const brokers = new Map;
11970
12101
  const meshConfigs = new Map;
11971
12102
  for (const mesh of meshes) {
@@ -11983,12 +12114,23 @@ async function runDaemon(opts = {}) {
11983
12114
  bus.publish("broker_status", { mesh: mesh.slug, status: s });
11984
12115
  },
11985
12116
  onPush: (m) => {
12117
+ const senderMemberPk = String(m.senderMemberPubkey ?? "").toLowerCase();
12118
+ const ownMember = mesh.pubkey.toLowerCase();
12119
+ if (senderMemberPk && senderMemberPk === ownMember) {
12120
+ return;
12121
+ }
11986
12122
  handleBrokerPush(m, {
11987
12123
  db: inboxDb,
11988
12124
  bus,
11989
12125
  meshSlug: mesh.slug,
11990
12126
  recipientSecretKeyHex: mesh.secretKey,
11991
- ackClientMessage: (cmid, bmid) => broker.sendClientAck(cmid, bmid)
12127
+ ackClientMessage: (cmid, bmid) => broker.sendClientAck(cmid, bmid),
12128
+ isOwnPubkey: (pubkey) => {
12129
+ const lower = pubkey.toLowerCase();
12130
+ if (lower === ownMember)
12131
+ return true;
12132
+ return sessionBrokersByPubkey.has(lower);
12133
+ }
11992
12134
  });
11993
12135
  }
11994
12136
  });
@@ -11996,14 +12138,13 @@ async function runDaemon(opts = {}) {
11996
12138
  `));
11997
12139
  brokers.set(mesh.slug, broker);
11998
12140
  }
11999
- const sessionBrokers = new Map;
12000
- const sessionBrokersByPubkey = new Map;
12001
12141
  let drain = null;
12002
12142
  drain = startDrainWorker({
12003
12143
  db: outboxDb,
12004
12144
  brokers,
12005
12145
  getSessionBrokerByPubkey: (pubkey) => sessionBrokersByPubkey.get(pubkey)
12006
12146
  });
12147
+ const inboxPruner = startInboxPruner({ db: inboxDb });
12007
12148
  setRegistryHooks({
12008
12149
  onRegister: (info) => {
12009
12150
  if (!info.presence)
@@ -12107,6 +12248,7 @@ async function runDaemon(opts = {}) {
12107
12248
  shuttingDown = true;
12108
12249
  process.stdout.write(JSON.stringify({ msg: "daemon_shutdown", signal: sig, ts: new Date().toISOString() }) + `
12109
12250
  `);
12251
+ inboxPruner.stop();
12110
12252
  if (drain)
12111
12253
  await drain.close();
12112
12254
  for (const b of brokers.values()) {
@@ -12143,6 +12285,7 @@ var init_run = __esm(() => {
12143
12285
  init_broker();
12144
12286
  init_session_broker();
12145
12287
  init_drain();
12288
+ init_inbox_pruner();
12146
12289
  init_inbound();
12147
12290
  init_identity();
12148
12291
  init_facade();
@@ -16270,7 +16413,8 @@ claudemesh message send <p> "..." --priority low # pull-only
16270
16413
  claudemesh inbox # all attached meshes, last 100
16271
16414
  claudemesh inbox --mesh <slug> # scoped to one mesh
16272
16415
  claudemesh inbox --mesh <slug> --limit 20 # custom cap
16273
- claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, …)
16416
+ claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, seen_at, …)
16417
+ claudemesh inbox --unread # 1.34.8+ only rows whose seen_at IS NULL
16274
16418
 
16275
16419
  # inbox flush + delete — 1.34.7+
16276
16420
  claudemesh inbox flush --mesh <slug> # delete all rows on one mesh
@@ -16286,6 +16430,12 @@ claudemesh message status <message-id> --json
16286
16430
 
16287
16431
  **Inbox source (1.34.0+):** \`claudemesh inbox\` queries the daemon's persistent \`~/.claudemesh/daemon/inbox.db\` over IPC — it is NOT a fresh broker-WS buffer drain. Rows survive daemon restarts. Sender attribution is the actual session pubkey of the launched session that originated the send (NOT the stable member pubkey of the sender's daemon), so two sibling sessions of the same human appear as distinct rows.
16288
16432
 
16433
+ **Read-state (1.34.8+):** every inbox row carries a \`seen_at\` timestamp. \`null\` = never surfaced; an ISO string = first surfaced at that moment. The flag flips automatically when (a) the row is returned by an interactive \`claudemesh inbox\` listing, or (b) the MCP server emits a live \`<channel>\` reminder for it. The launch welcome push uses \`unread_only=true\` to surface only rows the user hasn't seen — so a session relaunched a day later sees what it actually missed, not the same 24h batch every time. Use \`claudemesh inbox --unread\` to get the same filter from the CLI.
16434
+
16435
+ **Self-echo guard (1.34.8+):** broker fan-out paths sometimes mirror an outbound DM back to the originating session-WS. The daemon now drops those at the WS boundary (matching on \`senderPubkey === own.session_pubkey\`), so the sender no longer sees their own \`claudemesh send\` arrive as a \`← claudemesh: <self>: ...\` channel push immediately after dispatching it.
16436
+
16437
+ **Inbox TTL (1.34.8+):** the daemon runs an hourly prune that deletes rows older than 30 days. Without this the inbox grew unbounded; now it self-trims while preserving "I went on holiday and want to see what I missed" recovery for a generous window. No CLI knob — it's a built-in retention policy. To override, manually \`claudemesh inbox flush --before <iso>\`.
16438
+
16289
16439
  \`send\` JSON output: \`{"ok": true, "messageId": "...", "target": "..."}\`. Errors: \`{"ok": false, "error": "..."}\`.
16290
16440
 
16291
16441
  ### \`state\` — shared per-mesh key-value store
@@ -18389,6 +18539,32 @@ function daemonGet(path2, opts = {}) {
18389
18539
  req.end();
18390
18540
  });
18391
18541
  }
18542
+ function daemonMarkSeen(ids, sessionToken) {
18543
+ return new Promise((resolve3) => {
18544
+ if (ids.length === 0) {
18545
+ resolve3();
18546
+ return;
18547
+ }
18548
+ const body = JSON.stringify({ ids });
18549
+ const headers = {
18550
+ "Content-Type": "application/json",
18551
+ "Content-Length": String(Buffer.byteLength(body))
18552
+ };
18553
+ if (sessionToken)
18554
+ headers.Authorization = `ClaudeMesh-Session ${sessionToken}`;
18555
+ const req = httpRequest2({ socketPath: DAEMON_PATHS.SOCK_FILE, path: "/v1/inbox/seen", method: "POST", timeout: 3000, headers }, (res) => {
18556
+ res.on("data", () => {});
18557
+ res.on("end", () => resolve3());
18558
+ });
18559
+ req.on("error", () => resolve3());
18560
+ req.on("timeout", () => {
18561
+ req.destroy();
18562
+ resolve3();
18563
+ });
18564
+ req.write(body);
18565
+ req.end();
18566
+ });
18567
+ }
18392
18568
  function subscribeEvents(onEvent) {
18393
18569
  let active = true;
18394
18570
  let req = null;
@@ -18601,6 +18777,8 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18601
18777
  } catch {}
18602
18778
  };
18603
18779
  mcpLog("mcp_started", { version: VERSION });
18780
+ const { readSessionTokenFromEnv: readSessionTokenFromEnv2 } = await Promise.resolve().then(() => (init_token(), exports_token));
18781
+ const sessionTokenForSeen = readSessionTokenFromEnv2();
18604
18782
  const sub = subscribeEvents(async (ev) => {
18605
18783
  mcpLog("sse_event_received", { kind: ev.kind });
18606
18784
  if (ev.kind === "message") {
@@ -18633,6 +18811,10 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18633
18811
  }
18634
18812
  });
18635
18813
  mcpLog("channel_emitted", { content_preview: content.slice(0, 80), mesh: String(d.mesh ?? "") });
18814
+ const inboxRowId = String(d.id ?? "");
18815
+ if (inboxRowId) {
18816
+ daemonMarkSeen([inboxRowId], sessionTokenForSeen).catch(() => {});
18817
+ }
18636
18818
  } catch (err) {
18637
18819
  mcpLog("channel_emit_failed", { err: String(err) });
18638
18820
  process.stderr.write(`[claudemesh-mcp] channel emit failed: ${err}
@@ -18641,11 +18823,23 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18641
18823
  } else if (ev.kind === "peer_join" || ev.kind === "peer_leave" || ev.kind === "system") {
18642
18824
  const d = ev.data;
18643
18825
  const eventName = String(d.event ?? ev.kind);
18826
+ const renderPeerLine = (verb) => {
18827
+ const name = String(d.name ?? "unknown");
18828
+ const pubkey = String(d.pubkey ?? "");
18829
+ const pubkeyTag = pubkey ? ` (${pubkey.slice(0, 8)})` : "";
18830
+ const groups = Array.isArray(d.groups) ? d.groups : [];
18831
+ const groupNames = groups.map((g) => typeof g === "object" && g !== null && ("name" in g) ? String(g.name) : typeof g === "string" ? g : "").filter(Boolean);
18832
+ const groupsTag = groupNames.length > 0 ? ` [${groupNames.join(", ")}]` : "";
18833
+ const lastSeen = typeof d.lastSeenAt === "string" ? d.lastSeenAt : null;
18834
+ const summary = typeof d.summary === "string" && d.summary.trim() ? d.summary.trim() : null;
18835
+ const returningTail = lastSeen ? ` — last seen ${new Date(lastSeen).toLocaleTimeString()}${summary ? ` · "${summary.slice(0, 80)}"` : ""}` : "";
18836
+ return `[system] Peer "${name}"${pubkeyTag}${groupsTag} ${verb} the mesh${returningTail}`;
18837
+ };
18644
18838
  let content;
18645
18839
  if (ev.kind === "peer_join") {
18646
- content = `[system] Peer "${String(d.name ?? "unknown")}" joined the mesh`;
18840
+ content = renderPeerLine(eventName === "peer_returned" ? "returned to" : "joined");
18647
18841
  } else if (ev.kind === "peer_leave") {
18648
- content = `[system] Peer "${String(d.name ?? "unknown")}" left the mesh`;
18842
+ content = renderPeerLine("left");
18649
18843
  } else {
18650
18844
  content = `[system] ${eventName}: ${JSON.stringify(d).slice(0, 240)}`;
18651
18845
  }
@@ -18657,7 +18851,12 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18657
18851
  meta: {
18658
18852
  kind: "system",
18659
18853
  event: eventName,
18660
- mesh_slug: String(d.mesh ?? "")
18854
+ mesh_slug: String(d.mesh ?? ""),
18855
+ ...typeof d.name === "string" ? { peer_name: d.name } : {},
18856
+ ...typeof d.pubkey === "string" ? { peer_pubkey: d.pubkey } : {},
18857
+ ...Array.isArray(d.groups) ? { peer_groups: JSON.stringify(d.groups) } : {},
18858
+ ...typeof d.lastSeenAt === "string" ? { peer_last_seen_at: d.lastSeenAt } : {},
18859
+ ...typeof d.summary === "string" ? { peer_summary: d.summary } : {}
18661
18860
  }
18662
18861
  }
18663
18862
  });
@@ -18732,8 +18931,7 @@ async function emitMeshWelcome(server, mcpLog) {
18732
18931
  } catch (e) {
18733
18932
  mcpLog("welcome_peers_lookup_failed", { err: String(e) });
18734
18933
  }
18735
- const sinceIso = new Date(Date.now() - 86400000).toISOString();
18736
- const inboxPath = selfMeshSlug ? `/v1/inbox?mesh=${encodeURIComponent(selfMeshSlug)}&since=${encodeURIComponent(sinceIso)}&limit=20` : `/v1/inbox?since=${encodeURIComponent(sinceIso)}&limit=20`;
18934
+ const inboxPath = selfMeshSlug ? `/v1/inbox?mesh=${encodeURIComponent(selfMeshSlug)}&unread_only=true&mark_seen=false&limit=50` : `/v1/inbox?unread_only=true&mark_seen=false&limit=50`;
18737
18935
  let inboxItems = [];
18738
18936
  try {
18739
18937
  const { status, body } = await daemonGet(inboxPath, { sessionToken });
@@ -18757,9 +18955,9 @@ async function emitMeshWelcome(server, mcpLog) {
18757
18955
  lines.push(`\uD83D\uDC65 Peer list unavailable (daemon query failed).`);
18758
18956
  }
18759
18957
  if (inboxItems.length === 0) {
18760
- lines.push(`\uD83D\uDCE5 Inbox is empty (last 24h).`);
18958
+ lines.push(`\uD83D\uDCE5 No unread messages.`);
18761
18959
  } else {
18762
- lines.push(`\uD83D\uDCE5 ${inboxItems.length} message${inboxItems.length === 1 ? "" : "s"} in inbox (last 24h):`);
18960
+ lines.push(`\uD83D\uDCE5 ${inboxItems.length} unread message${inboxItems.length === 1 ? "" : "s"}:`);
18763
18961
  for (const it of inboxItems.slice(0, 3)) {
18764
18962
  const sender = String(it.sender_name ?? "unknown");
18765
18963
  const senderPub = String(it.sender_pubkey ?? "").slice(0, 8);
@@ -18798,6 +18996,12 @@ async function emitMeshWelcome(server, mcpLog) {
18798
18996
  peer_count: peerCount,
18799
18997
  unread_count: inboxItems.length
18800
18998
  });
18999
+ if (inboxItems.length > 0) {
19000
+ const ids = inboxItems.map((it) => String(it.id ?? "")).filter(Boolean);
19001
+ if (ids.length > 0) {
19002
+ daemonMarkSeen(ids, sessionToken).catch(() => {});
19003
+ }
19004
+ }
18801
19005
  } catch (err) {
18802
19006
  mcpLog("welcome_emit_failed", { err: String(err) });
18803
19007
  }
@@ -19060,7 +19264,8 @@ var BOOLEAN_FLAGS = new Set([
19060
19264
  "force",
19061
19265
  "dry-run",
19062
19266
  "verbose",
19063
- "skip-service"
19267
+ "skip-service",
19268
+ "unread"
19064
19269
  ]);
19065
19270
  function parseArgv(argv) {
19066
19271
  const args = argv.slice(2);
@@ -19673,8 +19878,10 @@ Message (resource form)
19673
19878
  fans out to every sibling session of your member)
19674
19879
  [--json] (machine-readable result)
19675
19880
  claudemesh message inbox read persisted inbox (alias: inbox)
19676
- flags: [--mesh <slug>] [--limit N] [--json]
19881
+ flags: [--mesh <slug>] [--limit N] [--unread] [--json]
19677
19882
  reads ~/.claudemesh/daemon/inbox.db via daemon
19883
+ --unread → only rows never surfaced before (seen_at IS NULL);
19884
+ listing stamps returned rows seen as a side effect
19678
19885
  claudemesh inbox flush bulk-delete inbox rows
19679
19886
  flags: [--mesh <slug>] [--before <iso-timestamp>] [--all]
19680
19887
  --all required when neither --mesh nor --before is set
@@ -20052,7 +20259,8 @@ async function main() {
20052
20259
  await runInbox2({
20053
20260
  mesh: flags.mesh,
20054
20261
  json: !!flags.json,
20055
- limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined
20262
+ limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
20263
+ unread: !!flags.unread
20056
20264
  });
20057
20265
  }
20058
20266
  break;
@@ -20325,7 +20533,8 @@ async function main() {
20325
20533
  await runInbox2({
20326
20534
  mesh: flags.mesh,
20327
20535
  json: !!flags.json,
20328
- limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined
20536
+ limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
20537
+ unread: !!flags.unread
20329
20538
  });
20330
20539
  }
20331
20540
  } else if (sub === "status") {
@@ -20880,4 +21089,4 @@ main().catch((err) => {
20880
21089
  process.exit(EXIT.INTERNAL_ERROR);
20881
21090
  });
20882
21091
 
20883
- //# debugId=FB01D4AF897A4CD164756E2164756E21
21092
+ //# debugId=7232363C9624744664756E2164756E21