claudemesh-cli 1.34.6 → 1.34.8
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/entrypoints/cli.js +411 -22
- package/dist/entrypoints/cli.js.map +14 -12
- package/dist/entrypoints/mcp.js +43 -6
- package/dist/entrypoints/mcp.js.map +3 -3
- package/package.json +1 -1
- package/skills/claudemesh/SKILL.md +15 -1
package/dist/entrypoints/cli.js
CHANGED
|
@@ -104,7 +104,7 @@ __export(exports_urls, {
|
|
|
104
104
|
VERSION: () => VERSION,
|
|
105
105
|
URLS: () => URLS
|
|
106
106
|
});
|
|
107
|
-
var URLS, VERSION = "1.34.
|
|
107
|
+
var URLS, VERSION = "1.34.8", env;
|
|
108
108
|
var init_urls = __esm(() => {
|
|
109
109
|
URLS = {
|
|
110
110
|
BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
|
@@ -4022,7 +4022,9 @@ __export(exports_daemon_route, {
|
|
|
4022
4022
|
tryListInboxViaDaemon: () => tryListInboxViaDaemon,
|
|
4023
4023
|
tryGetStateViaDaemon: () => tryGetStateViaDaemon,
|
|
4024
4024
|
tryGetSkillViaDaemon: () => tryGetSkillViaDaemon,
|
|
4025
|
-
tryForgetViaDaemon: () => tryForgetViaDaemon
|
|
4025
|
+
tryForgetViaDaemon: () => tryForgetViaDaemon,
|
|
4026
|
+
tryFlushInboxViaDaemon: () => tryFlushInboxViaDaemon,
|
|
4027
|
+
tryDeleteInboxRowViaDaemon: () => tryDeleteInboxRowViaDaemon
|
|
4026
4028
|
});
|
|
4027
4029
|
function meshQuery(mesh) {
|
|
4028
4030
|
return mesh ? `?mesh=${encodeURIComponent(mesh)}` : "";
|
|
@@ -4050,11 +4052,18 @@ async function tryListPeersViaDaemon(mesh) {
|
|
|
4050
4052
|
return null;
|
|
4051
4053
|
}
|
|
4052
4054
|
}
|
|
4053
|
-
async function tryListInboxViaDaemon(mesh, limit = 100) {
|
|
4055
|
+
async function tryListInboxViaDaemon(mesh, limit = 100, opts = {}) {
|
|
4054
4056
|
if (!await daemonReachable())
|
|
4055
4057
|
return null;
|
|
4056
4058
|
try {
|
|
4057
|
-
const
|
|
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("&")}`;
|
|
4058
4067
|
const res = await ipc({ path, timeoutMs: 3000 });
|
|
4059
4068
|
if (res.status !== 200)
|
|
4060
4069
|
return null;
|
|
@@ -4066,6 +4075,48 @@ async function tryListInboxViaDaemon(mesh, limit = 100) {
|
|
|
4066
4075
|
return null;
|
|
4067
4076
|
}
|
|
4068
4077
|
}
|
|
4078
|
+
async function tryFlushInboxViaDaemon(args = {}) {
|
|
4079
|
+
if (!await daemonReachable())
|
|
4080
|
+
return null;
|
|
4081
|
+
try {
|
|
4082
|
+
const params = [];
|
|
4083
|
+
if (args.mesh)
|
|
4084
|
+
params.push(`mesh=${encodeURIComponent(args.mesh)}`);
|
|
4085
|
+
if (args.beforeIso)
|
|
4086
|
+
params.push(`before=${encodeURIComponent(args.beforeIso)}`);
|
|
4087
|
+
const path = `/v1/inbox${params.length ? `?${params.join("&")}` : ""}`;
|
|
4088
|
+
const res = await ipc({ path, method: "DELETE", timeoutMs: 3000 });
|
|
4089
|
+
if (res.status !== 200)
|
|
4090
|
+
return null;
|
|
4091
|
+
return typeof res.body.removed === "number" ? res.body.removed : null;
|
|
4092
|
+
} catch (err) {
|
|
4093
|
+
const msg = String(err);
|
|
4094
|
+
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg))
|
|
4095
|
+
return null;
|
|
4096
|
+
return null;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
async function tryDeleteInboxRowViaDaemon(id) {
|
|
4100
|
+
if (!await daemonReachable())
|
|
4101
|
+
return null;
|
|
4102
|
+
try {
|
|
4103
|
+
const res = await ipc({
|
|
4104
|
+
path: `/v1/inbox/${encodeURIComponent(id)}`,
|
|
4105
|
+
method: "DELETE",
|
|
4106
|
+
timeoutMs: 3000
|
|
4107
|
+
});
|
|
4108
|
+
if (res.status === 404)
|
|
4109
|
+
return false;
|
|
4110
|
+
if (res.status !== 200)
|
|
4111
|
+
return null;
|
|
4112
|
+
return (res.body.removed ?? 0) > 0;
|
|
4113
|
+
} catch (err) {
|
|
4114
|
+
const msg = String(err);
|
|
4115
|
+
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg))
|
|
4116
|
+
return null;
|
|
4117
|
+
return null;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4069
4120
|
async function tryListSkillsViaDaemon(mesh) {
|
|
4070
4121
|
if (!await daemonReachable())
|
|
4071
4122
|
return null;
|
|
@@ -8239,6 +8290,87 @@ var init_send = __esm(() => {
|
|
|
8239
8290
|
init_styles();
|
|
8240
8291
|
});
|
|
8241
8292
|
|
|
8293
|
+
// src/commands/inbox-actions.ts
|
|
8294
|
+
var exports_inbox_actions = {};
|
|
8295
|
+
__export(exports_inbox_actions, {
|
|
8296
|
+
runInboxFlush: () => runInboxFlush,
|
|
8297
|
+
runInboxDelete: () => runInboxDelete
|
|
8298
|
+
});
|
|
8299
|
+
async function runInboxFlush(flags) {
|
|
8300
|
+
const hasFilter = !!(flags.mesh || flags.before);
|
|
8301
|
+
if (!hasFilter && !flags.all) {
|
|
8302
|
+
if (flags.json) {
|
|
8303
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "missing_filter" }) + `
|
|
8304
|
+
`);
|
|
8305
|
+
return;
|
|
8306
|
+
}
|
|
8307
|
+
render.info(dim(`Refusing to flush every row on every mesh.
|
|
8308
|
+
` + " Re-run with --mesh <slug>, --before <iso-timestamp>, or --all to confirm."));
|
|
8309
|
+
process.exit(1);
|
|
8310
|
+
}
|
|
8311
|
+
const removed = await tryFlushInboxViaDaemon({
|
|
8312
|
+
...flags.mesh ? { mesh: flags.mesh } : {},
|
|
8313
|
+
...flags.before ? { beforeIso: flags.before } : {}
|
|
8314
|
+
});
|
|
8315
|
+
if (removed === null) {
|
|
8316
|
+
if (flags.json) {
|
|
8317
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "daemon_unreachable" }) + `
|
|
8318
|
+
`);
|
|
8319
|
+
return;
|
|
8320
|
+
}
|
|
8321
|
+
render.info(dim("Daemon not reachable. Run `claudemesh daemon up` and retry."));
|
|
8322
|
+
process.exit(1);
|
|
8323
|
+
}
|
|
8324
|
+
if (flags.json) {
|
|
8325
|
+
process.stdout.write(JSON.stringify({ ok: true, removed }) + `
|
|
8326
|
+
`);
|
|
8327
|
+
return;
|
|
8328
|
+
}
|
|
8329
|
+
const scope = flags.mesh ? `mesh "${flags.mesh}"` : flags.before ? `older than ${flags.before}` : "all meshes";
|
|
8330
|
+
render.info(`✔ Flushed ${removed} message${removed === 1 ? "" : "s"} from ${scope}.`);
|
|
8331
|
+
}
|
|
8332
|
+
async function runInboxDelete(id, flags) {
|
|
8333
|
+
if (!id) {
|
|
8334
|
+
if (flags.json) {
|
|
8335
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "missing_id" }) + `
|
|
8336
|
+
`);
|
|
8337
|
+
return;
|
|
8338
|
+
}
|
|
8339
|
+
render.info(dim("Usage: claudemesh inbox delete <message-id>"));
|
|
8340
|
+
process.exit(1);
|
|
8341
|
+
}
|
|
8342
|
+
const ok = await tryDeleteInboxRowViaDaemon(id);
|
|
8343
|
+
if (ok === null) {
|
|
8344
|
+
if (flags.json) {
|
|
8345
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "daemon_unreachable" }) + `
|
|
8346
|
+
`);
|
|
8347
|
+
return;
|
|
8348
|
+
}
|
|
8349
|
+
render.info(dim("Daemon not reachable. Run `claudemesh daemon up` and retry."));
|
|
8350
|
+
process.exit(1);
|
|
8351
|
+
}
|
|
8352
|
+
if (!ok) {
|
|
8353
|
+
if (flags.json) {
|
|
8354
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "not_found", id }) + `
|
|
8355
|
+
`);
|
|
8356
|
+
return;
|
|
8357
|
+
}
|
|
8358
|
+
render.info(dim(`No inbox row with id "${id}".`));
|
|
8359
|
+
process.exit(1);
|
|
8360
|
+
}
|
|
8361
|
+
if (flags.json) {
|
|
8362
|
+
process.stdout.write(JSON.stringify({ ok: true, id }) + `
|
|
8363
|
+
`);
|
|
8364
|
+
return;
|
|
8365
|
+
}
|
|
8366
|
+
render.info(`✔ Deleted inbox row ${id}.`);
|
|
8367
|
+
}
|
|
8368
|
+
var init_inbox_actions = __esm(() => {
|
|
8369
|
+
init_daemon_route();
|
|
8370
|
+
init_render();
|
|
8371
|
+
init_styles();
|
|
8372
|
+
});
|
|
8373
|
+
|
|
8242
8374
|
// src/commands/inbox.ts
|
|
8243
8375
|
var exports_inbox = {};
|
|
8244
8376
|
__export(exports_inbox, {
|
|
@@ -8255,7 +8387,10 @@ function formatMessage(msg, includeMesh) {
|
|
|
8255
8387
|
}
|
|
8256
8388
|
async function runInbox(flags) {
|
|
8257
8389
|
const meshSlug = flags.mesh;
|
|
8258
|
-
const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100
|
|
8390
|
+
const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100, {
|
|
8391
|
+
unreadOnly: flags.unread === true,
|
|
8392
|
+
markSeen: true
|
|
8393
|
+
});
|
|
8259
8394
|
if (items === null) {
|
|
8260
8395
|
if (flags.json) {
|
|
8261
8396
|
process.stdout.write(`[]
|
|
@@ -8272,10 +8407,12 @@ async function runInbox(flags) {
|
|
|
8272
8407
|
}
|
|
8273
8408
|
if (items.length === 0) {
|
|
8274
8409
|
const scope = meshSlug ? `mesh "${meshSlug}"` : "any mesh";
|
|
8275
|
-
|
|
8410
|
+
const filter = flags.unread ? "unread " : "";
|
|
8411
|
+
render.info(dim(`No ${filter}messages on ${scope}.`));
|
|
8276
8412
|
return;
|
|
8277
8413
|
}
|
|
8278
|
-
const
|
|
8414
|
+
const filterTag = flags.unread ? " unread" : "";
|
|
8415
|
+
const heading = meshSlug ? `inbox — ${meshSlug} (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})` : `inbox (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})`;
|
|
8279
8416
|
render.section(heading);
|
|
8280
8417
|
for (const msg of items) {
|
|
8281
8418
|
process.stdout.write(formatMessage(msg, !meshSlug) + `
|
|
@@ -9457,6 +9594,11 @@ function migrateInbox(db) {
|
|
|
9457
9594
|
CREATE INDEX IF NOT EXISTS inbox_topic ON inbox(topic);
|
|
9458
9595
|
CREATE INDEX IF NOT EXISTS inbox_sender ON inbox(sender_pubkey);
|
|
9459
9596
|
`);
|
|
9597
|
+
const cols = db.prepare(`PRAGMA table_info(inbox)`).all();
|
|
9598
|
+
if (!cols.some((c) => c.name === "seen_at")) {
|
|
9599
|
+
db.exec(`ALTER TABLE inbox ADD COLUMN seen_at INTEGER`);
|
|
9600
|
+
db.exec(`CREATE INDEX IF NOT EXISTS inbox_seen_at ON inbox(seen_at)`);
|
|
9601
|
+
}
|
|
9460
9602
|
}
|
|
9461
9603
|
function insertIfNew(db, row) {
|
|
9462
9604
|
const before = db.prepare(`SELECT id FROM inbox WHERE client_message_id = ?`).get(row.client_message_id);
|
|
@@ -9491,9 +9633,12 @@ function listInbox(db, p) {
|
|
|
9491
9633
|
where.push("mesh = ?");
|
|
9492
9634
|
args.push(p.mesh);
|
|
9493
9635
|
}
|
|
9636
|
+
if (p.unreadOnly === true) {
|
|
9637
|
+
where.push("seen_at IS NULL");
|
|
9638
|
+
}
|
|
9494
9639
|
const sql = `
|
|
9495
9640
|
SELECT id, client_message_id, broker_message_id, mesh, topic,
|
|
9496
|
-
sender_pubkey, sender_name, body, meta, received_at, reply_to_id
|
|
9641
|
+
sender_pubkey, sender_name, body, meta, received_at, reply_to_id, seen_at
|
|
9497
9642
|
FROM inbox
|
|
9498
9643
|
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
9499
9644
|
ORDER BY received_at DESC
|
|
@@ -9502,6 +9647,36 @@ function listInbox(db, p) {
|
|
|
9502
9647
|
args.push(Math.min(Math.max(p.limit ?? 100, 1), 1000));
|
|
9503
9648
|
return db.prepare(sql).all(...args);
|
|
9504
9649
|
}
|
|
9650
|
+
function markInboxSeen(db, ids, now = Date.now()) {
|
|
9651
|
+
if (ids.length === 0)
|
|
9652
|
+
return 0;
|
|
9653
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
9654
|
+
const r = db.prepare(`UPDATE inbox SET seen_at = ? WHERE seen_at IS NULL AND id IN (${placeholders})`).run(now, ...ids);
|
|
9655
|
+
return Number(r.changes);
|
|
9656
|
+
}
|
|
9657
|
+
function pruneInboxBefore(db, cutoffMs) {
|
|
9658
|
+
const r = db.prepare(`DELETE FROM inbox WHERE received_at < ?`).run(cutoffMs);
|
|
9659
|
+
return Number(r.changes);
|
|
9660
|
+
}
|
|
9661
|
+
function deleteInboxRow(db, id) {
|
|
9662
|
+
const r = db.prepare(`DELETE FROM inbox WHERE id = ?`).run(id);
|
|
9663
|
+
return Number(r.changes) > 0;
|
|
9664
|
+
}
|
|
9665
|
+
function flushInbox(db, p) {
|
|
9666
|
+
const where = [];
|
|
9667
|
+
const args = [];
|
|
9668
|
+
if (p.mesh !== undefined) {
|
|
9669
|
+
where.push("mesh = ?");
|
|
9670
|
+
args.push(p.mesh);
|
|
9671
|
+
}
|
|
9672
|
+
if (p.before !== undefined) {
|
|
9673
|
+
where.push("received_at < ?");
|
|
9674
|
+
args.push(p.before);
|
|
9675
|
+
}
|
|
9676
|
+
const sql = `DELETE FROM inbox ${where.length ? "WHERE " + where.join(" AND ") : ""}`;
|
|
9677
|
+
const r = db.prepare(sql).run(...args);
|
|
9678
|
+
return Number(r.changes);
|
|
9679
|
+
}
|
|
9505
9680
|
|
|
9506
9681
|
// src/daemon/events.ts
|
|
9507
9682
|
class EventBus {
|
|
@@ -10243,13 +10418,23 @@ function makeHandler(opts) {
|
|
|
10243
10418
|
const limitRaw = url.searchParams.get("limit");
|
|
10244
10419
|
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
|
|
10245
10420
|
const meshFilter = meshFromCtx(url.searchParams.get("mesh")) ?? undefined;
|
|
10421
|
+
const unreadOnly = url.searchParams.get("unread_only") === "true";
|
|
10422
|
+
const markSeen = url.searchParams.get("mark_seen") !== "false";
|
|
10246
10423
|
const rows = listInbox(opts.inboxDb, {
|
|
10247
10424
|
since: Number.isFinite(since) ? since : undefined,
|
|
10248
10425
|
topic,
|
|
10249
10426
|
fromPubkey,
|
|
10250
10427
|
...meshFilter ? { mesh: meshFilter } : {},
|
|
10428
|
+
unreadOnly,
|
|
10251
10429
|
limit: Number.isFinite(limit ?? NaN) ? limit : undefined
|
|
10252
10430
|
});
|
|
10431
|
+
let flippedCount = 0;
|
|
10432
|
+
if (markSeen) {
|
|
10433
|
+
const unreadIds = rows.filter((r) => r.seen_at == null).map((r) => r.id);
|
|
10434
|
+
if (unreadIds.length > 0) {
|
|
10435
|
+
flippedCount = markInboxSeen(opts.inboxDb, unreadIds);
|
|
10436
|
+
}
|
|
10437
|
+
}
|
|
10253
10438
|
respond(res, 200, {
|
|
10254
10439
|
items: rows.map((r) => ({
|
|
10255
10440
|
id: r.id,
|
|
@@ -10261,11 +10446,65 @@ function makeHandler(opts) {
|
|
|
10261
10446
|
sender_name: r.sender_name,
|
|
10262
10447
|
body: r.body,
|
|
10263
10448
|
received_at: new Date(r.received_at).toISOString(),
|
|
10264
|
-
reply_to_id: r.reply_to_id
|
|
10265
|
-
|
|
10449
|
+
reply_to_id: r.reply_to_id,
|
|
10450
|
+
seen_at: r.seen_at ? new Date(r.seen_at).toISOString() : null
|
|
10451
|
+
})),
|
|
10452
|
+
marked_seen: flippedCount
|
|
10266
10453
|
});
|
|
10267
10454
|
return;
|
|
10268
10455
|
}
|
|
10456
|
+
if (req.method === "POST" && url.pathname === "/v1/inbox/seen") {
|
|
10457
|
+
if (!opts.inboxDb) {
|
|
10458
|
+
respond(res, 503, { error: "inbox not initialised" });
|
|
10459
|
+
return;
|
|
10460
|
+
}
|
|
10461
|
+
try {
|
|
10462
|
+
const body = await readJsonBody(req, 64 * 1024);
|
|
10463
|
+
const ids = Array.isArray(body?.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
10464
|
+
if (ids.length === 0) {
|
|
10465
|
+
respond(res, 400, { error: "missing 'ids' (string[])" });
|
|
10466
|
+
return;
|
|
10467
|
+
}
|
|
10468
|
+
const flipped = markInboxSeen(opts.inboxDb, ids);
|
|
10469
|
+
respond(res, 200, { marked_seen: flipped });
|
|
10470
|
+
} catch (e) {
|
|
10471
|
+
respond(res, 400, { error: String(e) });
|
|
10472
|
+
}
|
|
10473
|
+
return;
|
|
10474
|
+
}
|
|
10475
|
+
if (req.method === "DELETE" && url.pathname === "/v1/inbox") {
|
|
10476
|
+
if (!opts.inboxDb) {
|
|
10477
|
+
respond(res, 503, { error: "inbox not initialised" });
|
|
10478
|
+
return;
|
|
10479
|
+
}
|
|
10480
|
+
const meshFilter = meshFromCtx(url.searchParams.get("mesh")) ?? undefined;
|
|
10481
|
+
const beforeRaw = url.searchParams.get("before");
|
|
10482
|
+
const before = beforeRaw ? Date.parse(beforeRaw) : undefined;
|
|
10483
|
+
const removed = flushInbox(opts.inboxDb, {
|
|
10484
|
+
...meshFilter ? { mesh: meshFilter } : {},
|
|
10485
|
+
...Number.isFinite(before) ? { before } : {}
|
|
10486
|
+
});
|
|
10487
|
+
respond(res, 200, { removed });
|
|
10488
|
+
return;
|
|
10489
|
+
}
|
|
10490
|
+
if (req.method === "DELETE" && url.pathname.startsWith("/v1/inbox/")) {
|
|
10491
|
+
if (!opts.inboxDb) {
|
|
10492
|
+
respond(res, 503, { error: "inbox not initialised" });
|
|
10493
|
+
return;
|
|
10494
|
+
}
|
|
10495
|
+
const id = url.pathname.slice("/v1/inbox/".length);
|
|
10496
|
+
if (!id) {
|
|
10497
|
+
respond(res, 400, { error: "missing id" });
|
|
10498
|
+
return;
|
|
10499
|
+
}
|
|
10500
|
+
const ok = deleteInboxRow(opts.inboxDb, id);
|
|
10501
|
+
if (!ok) {
|
|
10502
|
+
respond(res, 404, { error: "not found", id });
|
|
10503
|
+
return;
|
|
10504
|
+
}
|
|
10505
|
+
respond(res, 200, { removed: 1, id });
|
|
10506
|
+
return;
|
|
10507
|
+
}
|
|
10269
10508
|
if (req.method === "GET" && url.pathname === "/v1/outbox") {
|
|
10270
10509
|
if (!opts.outboxDb) {
|
|
10271
10510
|
respond(res, 503, { error: "outbox not initialised" });
|
|
@@ -11199,6 +11438,11 @@ class SessionBrokerClient {
|
|
|
11199
11438
|
return;
|
|
11200
11439
|
}
|
|
11201
11440
|
if (msg.type === "push" || msg.type === "inbound") {
|
|
11441
|
+
const senderPubkey = String(msg.senderPubkey ?? "").toLowerCase();
|
|
11442
|
+
if (senderPubkey && senderPubkey === this.opts.sessionPubkey.toLowerCase()) {
|
|
11443
|
+
this.log("info", "self_echo_dropped", { sender: senderPubkey.slice(0, 12) });
|
|
11444
|
+
return;
|
|
11445
|
+
}
|
|
11202
11446
|
this.opts.onPush?.(msg);
|
|
11203
11447
|
return;
|
|
11204
11448
|
}
|
|
@@ -11490,6 +11734,46 @@ function defaultLog4(level, msg, meta) {
|
|
|
11490
11734
|
var POLL_INTERVAL_MS = 500, MAX_ATTEMPTS_PER_ROW = 25, BACKOFF_BASE_MS = 500, BACKOFF_CAP_MS = 30000;
|
|
11491
11735
|
var init_drain = () => {};
|
|
11492
11736
|
|
|
11737
|
+
// src/daemon/inbox-pruner.ts
|
|
11738
|
+
function startInboxPruner(opts) {
|
|
11739
|
+
const retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
|
|
11740
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
11741
|
+
const log2 = opts.log ?? defaultLog5;
|
|
11742
|
+
const tick = () => {
|
|
11743
|
+
try {
|
|
11744
|
+
const cutoff = Date.now() - retentionMs;
|
|
11745
|
+
const removed = pruneInboxBefore(opts.db, cutoff);
|
|
11746
|
+
if (removed > 0) {
|
|
11747
|
+
log2("info", "inbox_prune_completed", {
|
|
11748
|
+
removed,
|
|
11749
|
+
retention_days: Math.round(retentionMs / (24 * 60 * 60 * 1000))
|
|
11750
|
+
});
|
|
11751
|
+
}
|
|
11752
|
+
} catch (e) {
|
|
11753
|
+
log2("warn", "inbox_prune_failed", { err: String(e) });
|
|
11754
|
+
}
|
|
11755
|
+
};
|
|
11756
|
+
tick();
|
|
11757
|
+
const handle = setInterval(tick, intervalMs);
|
|
11758
|
+
if (typeof handle.unref === "function")
|
|
11759
|
+
handle.unref();
|
|
11760
|
+
return { stop: () => clearInterval(handle) };
|
|
11761
|
+
}
|
|
11762
|
+
function defaultLog5(level, msg, meta) {
|
|
11763
|
+
const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
|
|
11764
|
+
if (level === "info")
|
|
11765
|
+
process.stdout.write(line + `
|
|
11766
|
+
`);
|
|
11767
|
+
else
|
|
11768
|
+
process.stderr.write(line + `
|
|
11769
|
+
`);
|
|
11770
|
+
}
|
|
11771
|
+
var DEFAULT_RETENTION_MS, DEFAULT_INTERVAL_MS;
|
|
11772
|
+
var init_inbox_pruner = __esm(() => {
|
|
11773
|
+
DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
11774
|
+
DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
|
|
11775
|
+
});
|
|
11776
|
+
|
|
11493
11777
|
// src/daemon/inbound.ts
|
|
11494
11778
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
11495
11779
|
async function handleBrokerPush(msg, ctx) {
|
|
@@ -11806,6 +12090,12 @@ async function runDaemon(opts = {}) {
|
|
|
11806
12090
|
bus.publish("broker_status", { mesh: mesh.slug, status: s });
|
|
11807
12091
|
},
|
|
11808
12092
|
onPush: (m) => {
|
|
12093
|
+
const senderMemberPk = String(m.senderMemberPubkey ?? "").toLowerCase();
|
|
12094
|
+
const senderPubkey = String(m.senderPubkey ?? "").toLowerCase();
|
|
12095
|
+
const ownMember = mesh.pubkey.toLowerCase();
|
|
12096
|
+
if (senderMemberPk && senderMemberPk === ownMember && senderPubkey === ownMember) {
|
|
12097
|
+
return;
|
|
12098
|
+
}
|
|
11809
12099
|
handleBrokerPush(m, {
|
|
11810
12100
|
db: inboxDb,
|
|
11811
12101
|
bus,
|
|
@@ -11827,6 +12117,7 @@ async function runDaemon(opts = {}) {
|
|
|
11827
12117
|
brokers,
|
|
11828
12118
|
getSessionBrokerByPubkey: (pubkey) => sessionBrokersByPubkey.get(pubkey)
|
|
11829
12119
|
});
|
|
12120
|
+
const inboxPruner = startInboxPruner({ db: inboxDb });
|
|
11830
12121
|
setRegistryHooks({
|
|
11831
12122
|
onRegister: (info) => {
|
|
11832
12123
|
if (!info.presence)
|
|
@@ -11930,6 +12221,7 @@ async function runDaemon(opts = {}) {
|
|
|
11930
12221
|
shuttingDown = true;
|
|
11931
12222
|
process.stdout.write(JSON.stringify({ msg: "daemon_shutdown", signal: sig, ts: new Date().toISOString() }) + `
|
|
11932
12223
|
`);
|
|
12224
|
+
inboxPruner.stop();
|
|
11933
12225
|
if (drain)
|
|
11934
12226
|
await drain.close();
|
|
11935
12227
|
for (const b of brokers.values()) {
|
|
@@ -11966,6 +12258,7 @@ var init_run = __esm(() => {
|
|
|
11966
12258
|
init_broker();
|
|
11967
12259
|
init_session_broker();
|
|
11968
12260
|
init_drain();
|
|
12261
|
+
init_inbox_pruner();
|
|
11969
12262
|
init_inbound();
|
|
11970
12263
|
init_identity();
|
|
11971
12264
|
init_facade();
|
|
@@ -16093,7 +16386,15 @@ claudemesh message send <p> "..." --priority low # pull-only
|
|
|
16093
16386
|
claudemesh inbox # all attached meshes, last 100
|
|
16094
16387
|
claudemesh inbox --mesh <slug> # scoped to one mesh
|
|
16095
16388
|
claudemesh inbox --mesh <slug> --limit 20 # custom cap
|
|
16096
|
-
claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, …)
|
|
16389
|
+
claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, seen_at, …)
|
|
16390
|
+
claudemesh inbox --unread # 1.34.8+ only rows whose seen_at IS NULL
|
|
16391
|
+
|
|
16392
|
+
# inbox flush + delete — 1.34.7+
|
|
16393
|
+
claudemesh inbox flush --mesh <slug> # delete all rows on one mesh
|
|
16394
|
+
claudemesh inbox flush --before <iso-timestamp> # delete rows older than timestamp
|
|
16395
|
+
claudemesh inbox flush --all # delete every row on every mesh (required guard)
|
|
16396
|
+
claudemesh inbox delete <id> # delete one inbox row by id (alias: rm)
|
|
16397
|
+
claudemesh inbox flush --mesh <slug> --json # JSON: { ok: true, removed: N }
|
|
16097
16398
|
|
|
16098
16399
|
# delivery status (alias: claudemesh msg-status <id>)
|
|
16099
16400
|
claudemesh message status <message-id>
|
|
@@ -16102,6 +16403,12 @@ claudemesh message status <message-id> --json
|
|
|
16102
16403
|
|
|
16103
16404
|
**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.
|
|
16104
16405
|
|
|
16406
|
+
**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.
|
|
16407
|
+
|
|
16408
|
+
**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.
|
|
16409
|
+
|
|
16410
|
+
**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>\`.
|
|
16411
|
+
|
|
16105
16412
|
\`send\` JSON output: \`{"ok": true, "messageId": "...", "target": "..."}\`. Errors: \`{"ok": false, "error": "..."}\`.
|
|
16106
16413
|
|
|
16107
16414
|
### \`state\` — shared per-mesh key-value store
|
|
@@ -18205,6 +18512,32 @@ function daemonGet(path2, opts = {}) {
|
|
|
18205
18512
|
req.end();
|
|
18206
18513
|
});
|
|
18207
18514
|
}
|
|
18515
|
+
function daemonMarkSeen(ids, sessionToken) {
|
|
18516
|
+
return new Promise((resolve3) => {
|
|
18517
|
+
if (ids.length === 0) {
|
|
18518
|
+
resolve3();
|
|
18519
|
+
return;
|
|
18520
|
+
}
|
|
18521
|
+
const body = JSON.stringify({ ids });
|
|
18522
|
+
const headers = {
|
|
18523
|
+
"Content-Type": "application/json",
|
|
18524
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
18525
|
+
};
|
|
18526
|
+
if (sessionToken)
|
|
18527
|
+
headers.Authorization = `ClaudeMesh-Session ${sessionToken}`;
|
|
18528
|
+
const req = httpRequest2({ socketPath: DAEMON_PATHS.SOCK_FILE, path: "/v1/inbox/seen", method: "POST", timeout: 3000, headers }, (res) => {
|
|
18529
|
+
res.on("data", () => {});
|
|
18530
|
+
res.on("end", () => resolve3());
|
|
18531
|
+
});
|
|
18532
|
+
req.on("error", () => resolve3());
|
|
18533
|
+
req.on("timeout", () => {
|
|
18534
|
+
req.destroy();
|
|
18535
|
+
resolve3();
|
|
18536
|
+
});
|
|
18537
|
+
req.write(body);
|
|
18538
|
+
req.end();
|
|
18539
|
+
});
|
|
18540
|
+
}
|
|
18208
18541
|
function subscribeEvents(onEvent) {
|
|
18209
18542
|
let active = true;
|
|
18210
18543
|
let req = null;
|
|
@@ -18417,6 +18750,8 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
|
|
|
18417
18750
|
} catch {}
|
|
18418
18751
|
};
|
|
18419
18752
|
mcpLog("mcp_started", { version: VERSION });
|
|
18753
|
+
const { readSessionTokenFromEnv: readSessionTokenFromEnv2 } = await Promise.resolve().then(() => (init_token(), exports_token));
|
|
18754
|
+
const sessionTokenForSeen = readSessionTokenFromEnv2();
|
|
18420
18755
|
const sub = subscribeEvents(async (ev) => {
|
|
18421
18756
|
mcpLog("sse_event_received", { kind: ev.kind });
|
|
18422
18757
|
if (ev.kind === "message") {
|
|
@@ -18449,6 +18784,10 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
|
|
|
18449
18784
|
}
|
|
18450
18785
|
});
|
|
18451
18786
|
mcpLog("channel_emitted", { content_preview: content.slice(0, 80), mesh: String(d.mesh ?? "") });
|
|
18787
|
+
const inboxRowId = String(d.id ?? "");
|
|
18788
|
+
if (inboxRowId) {
|
|
18789
|
+
daemonMarkSeen([inboxRowId], sessionTokenForSeen).catch(() => {});
|
|
18790
|
+
}
|
|
18452
18791
|
} catch (err) {
|
|
18453
18792
|
mcpLog("channel_emit_failed", { err: String(err) });
|
|
18454
18793
|
process.stderr.write(`[claudemesh-mcp] channel emit failed: ${err}
|
|
@@ -18548,8 +18887,7 @@ async function emitMeshWelcome(server, mcpLog) {
|
|
|
18548
18887
|
} catch (e) {
|
|
18549
18888
|
mcpLog("welcome_peers_lookup_failed", { err: String(e) });
|
|
18550
18889
|
}
|
|
18551
|
-
const
|
|
18552
|
-
const inboxPath = selfMeshSlug ? `/v1/inbox?mesh=${encodeURIComponent(selfMeshSlug)}&since=${encodeURIComponent(sinceIso)}&limit=20` : `/v1/inbox?since=${encodeURIComponent(sinceIso)}&limit=20`;
|
|
18890
|
+
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`;
|
|
18553
18891
|
let inboxItems = [];
|
|
18554
18892
|
try {
|
|
18555
18893
|
const { status, body } = await daemonGet(inboxPath, { sessionToken });
|
|
@@ -18573,9 +18911,9 @@ async function emitMeshWelcome(server, mcpLog) {
|
|
|
18573
18911
|
lines.push(`\uD83D\uDC65 Peer list unavailable (daemon query failed).`);
|
|
18574
18912
|
}
|
|
18575
18913
|
if (inboxItems.length === 0) {
|
|
18576
|
-
lines.push(`\uD83D\uDCE5
|
|
18914
|
+
lines.push(`\uD83D\uDCE5 No unread messages.`);
|
|
18577
18915
|
} else {
|
|
18578
|
-
lines.push(`\uD83D\uDCE5 ${inboxItems.length} message${inboxItems.length === 1 ? "" : "s"}
|
|
18916
|
+
lines.push(`\uD83D\uDCE5 ${inboxItems.length} unread message${inboxItems.length === 1 ? "" : "s"}:`);
|
|
18579
18917
|
for (const it of inboxItems.slice(0, 3)) {
|
|
18580
18918
|
const sender = String(it.sender_name ?? "unknown");
|
|
18581
18919
|
const senderPub = String(it.sender_pubkey ?? "").slice(0, 8);
|
|
@@ -18614,6 +18952,12 @@ async function emitMeshWelcome(server, mcpLog) {
|
|
|
18614
18952
|
peer_count: peerCount,
|
|
18615
18953
|
unread_count: inboxItems.length
|
|
18616
18954
|
});
|
|
18955
|
+
if (inboxItems.length > 0) {
|
|
18956
|
+
const ids = inboxItems.map((it) => String(it.id ?? "")).filter(Boolean);
|
|
18957
|
+
if (ids.length > 0) {
|
|
18958
|
+
daemonMarkSeen(ids, sessionToken).catch(() => {});
|
|
18959
|
+
}
|
|
18960
|
+
}
|
|
18617
18961
|
} catch (err) {
|
|
18618
18962
|
mcpLog("welcome_emit_failed", { err: String(err) });
|
|
18619
18963
|
}
|
|
@@ -18876,7 +19220,8 @@ var BOOLEAN_FLAGS = new Set([
|
|
|
18876
19220
|
"force",
|
|
18877
19221
|
"dry-run",
|
|
18878
19222
|
"verbose",
|
|
18879
|
-
"skip-service"
|
|
19223
|
+
"skip-service",
|
|
19224
|
+
"unread"
|
|
18880
19225
|
]);
|
|
18881
19226
|
function parseArgv(argv) {
|
|
18882
19227
|
const args = argv.slice(2);
|
|
@@ -19489,8 +19834,14 @@ Message (resource form)
|
|
|
19489
19834
|
fans out to every sibling session of your member)
|
|
19490
19835
|
[--json] (machine-readable result)
|
|
19491
19836
|
claudemesh message inbox read persisted inbox (alias: inbox)
|
|
19492
|
-
flags: [--mesh <slug>] [--limit N] [--json]
|
|
19837
|
+
flags: [--mesh <slug>] [--limit N] [--unread] [--json]
|
|
19493
19838
|
reads ~/.claudemesh/daemon/inbox.db via daemon
|
|
19839
|
+
--unread → only rows never surfaced before (seen_at IS NULL);
|
|
19840
|
+
listing stamps returned rows seen as a side effect
|
|
19841
|
+
claudemesh inbox flush bulk-delete inbox rows
|
|
19842
|
+
flags: [--mesh <slug>] [--before <iso-timestamp>] [--all]
|
|
19843
|
+
--all required when neither --mesh nor --before is set
|
|
19844
|
+
claudemesh inbox delete <id> delete one inbox row by id (alias: rm)
|
|
19494
19845
|
claudemesh message status <id> delivery status (alias: msg-status)
|
|
19495
19846
|
|
|
19496
19847
|
Memory (resource form)
|
|
@@ -19847,8 +20198,27 @@ async function main() {
|
|
|
19847
20198
|
break;
|
|
19848
20199
|
}
|
|
19849
20200
|
case "inbox": {
|
|
19850
|
-
const
|
|
19851
|
-
|
|
20201
|
+
const sub = positionals[0];
|
|
20202
|
+
if (sub === "flush") {
|
|
20203
|
+
const { runInboxFlush: runInboxFlush2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
|
|
20204
|
+
await runInboxFlush2({
|
|
20205
|
+
mesh: flags.mesh,
|
|
20206
|
+
before: flags.before,
|
|
20207
|
+
all: !!flags.all,
|
|
20208
|
+
json: !!flags.json
|
|
20209
|
+
});
|
|
20210
|
+
} else if (sub === "delete" || sub === "rm") {
|
|
20211
|
+
const { runInboxDelete: runInboxDelete2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
|
|
20212
|
+
await runInboxDelete2(positionals[1] ?? "", { json: !!flags.json });
|
|
20213
|
+
} else {
|
|
20214
|
+
const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
|
|
20215
|
+
await runInbox2({
|
|
20216
|
+
mesh: flags.mesh,
|
|
20217
|
+
json: !!flags.json,
|
|
20218
|
+
limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
|
|
20219
|
+
unread: !!flags.unread
|
|
20220
|
+
});
|
|
20221
|
+
}
|
|
19852
20222
|
break;
|
|
19853
20223
|
}
|
|
19854
20224
|
case "state": {
|
|
@@ -20102,8 +20472,27 @@ async function main() {
|
|
|
20102
20472
|
const { runSend: runSend2 } = await Promise.resolve().then(() => (init_send(), exports_send));
|
|
20103
20473
|
await runSend2({ mesh: flags.mesh, priority: flags.priority, json: !!flags.json, self: !!flags.self }, positionals[1] ?? "", positionals.slice(2).join(" "));
|
|
20104
20474
|
} else if (sub === "inbox") {
|
|
20105
|
-
const
|
|
20106
|
-
|
|
20475
|
+
const sub2 = positionals[1];
|
|
20476
|
+
if (sub2 === "flush") {
|
|
20477
|
+
const { runInboxFlush: runInboxFlush2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
|
|
20478
|
+
await runInboxFlush2({
|
|
20479
|
+
mesh: flags.mesh,
|
|
20480
|
+
before: flags.before,
|
|
20481
|
+
all: !!flags.all,
|
|
20482
|
+
json: !!flags.json
|
|
20483
|
+
});
|
|
20484
|
+
} else if (sub2 === "delete" || sub2 === "rm") {
|
|
20485
|
+
const { runInboxDelete: runInboxDelete2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
|
|
20486
|
+
await runInboxDelete2(positionals[2] ?? "", { json: !!flags.json });
|
|
20487
|
+
} else {
|
|
20488
|
+
const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
|
|
20489
|
+
await runInbox2({
|
|
20490
|
+
mesh: flags.mesh,
|
|
20491
|
+
json: !!flags.json,
|
|
20492
|
+
limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
|
|
20493
|
+
unread: !!flags.unread
|
|
20494
|
+
});
|
|
20495
|
+
}
|
|
20107
20496
|
} else if (sub === "status") {
|
|
20108
20497
|
const { runMsgStatus: runMsgStatus2 } = await Promise.resolve().then(() => (init_broker_actions(), exports_broker_actions));
|
|
20109
20498
|
process.exit(await runMsgStatus2(positionals[1], { mesh: flags.mesh, json: !!flags.json }));
|
|
@@ -20656,4 +21045,4 @@ main().catch((err) => {
|
|
|
20656
21045
|
process.exit(EXIT.INTERNAL_ERROR);
|
|
20657
21046
|
});
|
|
20658
21047
|
|
|
20659
|
-
//# debugId=
|
|
21048
|
+
//# debugId=99681AB539B313DF64756E2164756E21
|