claudemesh-cli 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,7 +88,7 @@ __export(exports_urls, {
88
88
  VERSION: () => VERSION,
89
89
  URLS: () => URLS
90
90
  });
91
- var URLS, VERSION = "1.14.0", env;
91
+ var URLS, VERSION = "1.15.0", env;
92
92
  var init_urls = __esm(() => {
93
93
  URLS = {
94
94
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -11766,6 +11766,270 @@ var init_with_rest_key = __esm(() => {
11766
11766
  init_connect();
11767
11767
  });
11768
11768
 
11769
+ // src/commands/me.ts
11770
+ var exports_me = {};
11771
+ __export(exports_me, {
11772
+ runMeTopics: () => runMeTopics,
11773
+ runMeSearch: () => runMeSearch,
11774
+ runMeNotifications: () => runMeNotifications,
11775
+ runMeActivity: () => runMeActivity,
11776
+ runMe: () => runMe
11777
+ });
11778
+ function resolveMeshForMint(explicit) {
11779
+ if (explicit)
11780
+ return explicit;
11781
+ const cfg = readConfig();
11782
+ return cfg.meshes[0]?.slug ?? null;
11783
+ }
11784
+ async function runMe(flags) {
11785
+ return withRestKey({
11786
+ meshSlug: resolveMeshForMint(flags.mesh),
11787
+ purpose: "workspace-overview",
11788
+ capabilities: ["read"]
11789
+ }, async ({ secret }) => {
11790
+ const ws = await request({
11791
+ path: "/api/v1/me/workspace",
11792
+ token: secret
11793
+ });
11794
+ if (flags.json) {
11795
+ console.log(JSON.stringify(ws, null, 2));
11796
+ return EXIT.SUCCESS;
11797
+ }
11798
+ render.section(`${clay("workspace")} — ${bold(ws.userId.slice(0, 8))} ${dim(`· ${ws.totals.meshes} mesh${ws.totals.meshes === 1 ? "" : "es"}`)}`);
11799
+ const totalsLine = [
11800
+ `${green(String(ws.totals.online))}/${ws.totals.peers} online`,
11801
+ `${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}`,
11802
+ ws.totals.unreadMentions > 0 ? yellow(`${ws.totals.unreadMentions} unread @you`) : dim("0 unread @you")
11803
+ ].join(dim(" · "));
11804
+ process.stdout.write(" " + totalsLine + `
11805
+
11806
+ `);
11807
+ if (ws.meshes.length === 0) {
11808
+ process.stdout.write(dim(" no meshes joined — run `claudemesh new` or accept an invite\n"));
11809
+ return EXIT.SUCCESS;
11810
+ }
11811
+ const slugWidth = Math.max(...ws.meshes.map((m) => m.slug.length), 8);
11812
+ for (const m of ws.meshes) {
11813
+ const slug = cyan(m.slug.padEnd(slugWidth));
11814
+ const peers = `${m.online}/${m.peers}`;
11815
+ const role = dim(m.myRole);
11816
+ const unread = m.unreadMentions > 0 ? " " + yellow(`${m.unreadMentions} @you`) : "";
11817
+ process.stdout.write(` ${slug} ${peers.padStart(5)} online ${dim(String(m.topics).padStart(2) + " topics")} ${role}${unread}
11818
+ `);
11819
+ }
11820
+ return EXIT.SUCCESS;
11821
+ });
11822
+ }
11823
+ async function runMeTopics(flags) {
11824
+ return withRestKey({
11825
+ meshSlug: resolveMeshForMint(flags.mesh),
11826
+ purpose: "workspace-topics",
11827
+ capabilities: ["read"]
11828
+ }, async ({ secret }) => {
11829
+ const ws = await request({
11830
+ path: "/api/v1/me/topics",
11831
+ token: secret
11832
+ });
11833
+ const visible = flags.unread ? ws.topics.filter((t) => t.unread > 0) : ws.topics;
11834
+ if (flags.json) {
11835
+ console.log(JSON.stringify({ topics: visible, totals: ws.totals }, null, 2));
11836
+ return EXIT.SUCCESS;
11837
+ }
11838
+ render.section(`${clay("topics")} — ${ws.totals.topics} across all meshes ${dim(ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· all read")}`);
11839
+ if (visible.length === 0) {
11840
+ process.stdout.write(dim(flags.unread ? ` no unread topics
11841
+ ` : " no topics — run `claudemesh topic create #general`\n"));
11842
+ return EXIT.SUCCESS;
11843
+ }
11844
+ const slugWidth = Math.max(...visible.map((t) => t.meshSlug.length), 6);
11845
+ const nameWidth = Math.max(...visible.map((t) => t.name.length), 8);
11846
+ for (const t of visible) {
11847
+ const slug = dim(t.meshSlug.padEnd(slugWidth));
11848
+ const name = cyan(t.name.padEnd(nameWidth));
11849
+ const unread = t.unread > 0 ? yellow(`${t.unread} unread`.padStart(10)) : dim("·".padStart(10));
11850
+ const last = t.lastMessageAt ? dim(formatRelativeTime(t.lastMessageAt)) : dim("never");
11851
+ process.stdout.write(` ${slug} ${name} ${unread} ${last}
11852
+ `);
11853
+ }
11854
+ return EXIT.SUCCESS;
11855
+ });
11856
+ }
11857
+ async function runMeNotifications(flags) {
11858
+ return withRestKey({
11859
+ meshSlug: resolveMeshForMint(flags.mesh),
11860
+ purpose: "workspace-notifications",
11861
+ capabilities: ["read"]
11862
+ }, async ({ secret }) => {
11863
+ const params = new URLSearchParams;
11864
+ if (flags.all)
11865
+ params.set("include", "all");
11866
+ if (flags.since)
11867
+ params.set("since", flags.since);
11868
+ const path = "/api/v1/me/notifications" + (params.toString() ? `?${params.toString()}` : "");
11869
+ const ws = await request({
11870
+ path,
11871
+ token: secret
11872
+ });
11873
+ if (flags.json) {
11874
+ console.log(JSON.stringify(ws, null, 2));
11875
+ return EXIT.SUCCESS;
11876
+ }
11877
+ const headerLabel = flags.all ? "@-mentions (all)" : "@-mentions (unread)";
11878
+ render.section(`${clay(headerLabel)} — ${ws.totals.total} ${dim(ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· nothing pending")}`);
11879
+ if (ws.notifications.length === 0) {
11880
+ process.stdout.write(dim(flags.all ? ` no @-mentions in window
11881
+ ` : ` inbox zero — nothing waiting
11882
+ `));
11883
+ return EXIT.SUCCESS;
11884
+ }
11885
+ const slugWidth = Math.max(...ws.notifications.map((n) => n.meshSlug.length), 6);
11886
+ for (const n of ws.notifications) {
11887
+ const slug = dim(n.meshSlug.padEnd(slugWidth));
11888
+ const topic = cyan(`#${n.topicName}`);
11889
+ const sender = n.senderName ? `from ${n.senderName}` : "from ?";
11890
+ const ago = formatRelativeTime(n.createdAt);
11891
+ const dot = n.read ? dim("·") : yellow("●");
11892
+ const snippet = n.snippet ?? (n.ciphertext ? dim("[encrypted]") : dim("[empty]"));
11893
+ process.stdout.write(` ${dot} ${slug} ${topic} ${dim(sender)} ${dim(ago)}
11894
+ ` + ` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}
11895
+ `);
11896
+ }
11897
+ return EXIT.SUCCESS;
11898
+ });
11899
+ }
11900
+ async function runMeActivity(flags) {
11901
+ return withRestKey({
11902
+ meshSlug: resolveMeshForMint(flags.mesh),
11903
+ purpose: "workspace-activity",
11904
+ capabilities: ["read"]
11905
+ }, async ({ secret }) => {
11906
+ const params = new URLSearchParams;
11907
+ if (flags.since)
11908
+ params.set("since", flags.since);
11909
+ const path = "/api/v1/me/activity" + (params.toString() ? `?${params.toString()}` : "");
11910
+ const ws = await request({
11911
+ path,
11912
+ token: secret
11913
+ });
11914
+ if (flags.json) {
11915
+ console.log(JSON.stringify(ws, null, 2));
11916
+ return EXIT.SUCCESS;
11917
+ }
11918
+ render.section(`${clay("activity")} — ${ws.totals.events} ${dim(flags.since ? `since ${flags.since}` : "in the last 24h")}`);
11919
+ if (ws.activity.length === 0) {
11920
+ process.stdout.write(dim(` quiet — no activity in window
11921
+ `));
11922
+ return EXIT.SUCCESS;
11923
+ }
11924
+ const slugWidth = Math.max(...ws.activity.map((a) => a.meshSlug.length), 6);
11925
+ for (const a of ws.activity) {
11926
+ const slug = dim(a.meshSlug.padEnd(slugWidth));
11927
+ const topic = cyan(`#${a.topicName}`);
11928
+ const sender = a.senderName ?? "?";
11929
+ const ago = formatRelativeTime(a.createdAt);
11930
+ const snippet = a.snippet ?? (a.ciphertext ? dim("[encrypted]") : dim("[empty]"));
11931
+ process.stdout.write(` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}
11932
+ ` + ` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}
11933
+ `);
11934
+ }
11935
+ return EXIT.SUCCESS;
11936
+ });
11937
+ }
11938
+ async function runMeSearch(flags) {
11939
+ if (!flags.query || flags.query.length < 2) {
11940
+ process.stderr.write(`Usage: claudemesh me search <query> (min 2 chars)
11941
+ `);
11942
+ return EXIT.INVALID_ARGS;
11943
+ }
11944
+ return withRestKey({
11945
+ meshSlug: resolveMeshForMint(flags.mesh),
11946
+ purpose: "workspace-search",
11947
+ capabilities: ["read"]
11948
+ }, async ({ secret }) => {
11949
+ const params = new URLSearchParams({ q: flags.query });
11950
+ const ws = await request({
11951
+ path: `/api/v1/me/search?${params.toString()}`,
11952
+ token: secret
11953
+ });
11954
+ if (flags.json) {
11955
+ console.log(JSON.stringify(ws, null, 2));
11956
+ return EXIT.SUCCESS;
11957
+ }
11958
+ render.section(`${clay("search")} — "${flags.query}" ${dim(`${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}, ` + `${ws.totals.messages} message${ws.totals.messages === 1 ? "" : "s"}`)}`);
11959
+ if (ws.topics.length === 0 && ws.messages.length === 0) {
11960
+ process.stdout.write(dim(` no matches
11961
+ `));
11962
+ return EXIT.SUCCESS;
11963
+ }
11964
+ if (ws.topics.length > 0) {
11965
+ process.stdout.write(dim(`
11966
+ topics
11967
+ `));
11968
+ const slugWidth = Math.max(...ws.topics.map((t) => t.meshSlug.length), 6);
11969
+ for (const t of ws.topics) {
11970
+ const slug = dim(t.meshSlug.padEnd(slugWidth));
11971
+ const name = cyan(`#${t.name}`);
11972
+ const desc = t.description ? dim(` — ${t.description}`) : "";
11973
+ process.stdout.write(` ${slug} ${name}${desc}
11974
+ `);
11975
+ }
11976
+ }
11977
+ if (ws.messages.length > 0) {
11978
+ process.stdout.write(dim(`
11979
+ messages
11980
+ `));
11981
+ const slugWidth = Math.max(...ws.messages.map((m) => m.meshSlug.length), 6);
11982
+ for (const m of ws.messages) {
11983
+ const slug = dim(m.meshSlug.padEnd(slugWidth));
11984
+ const topic = cyan(`#${m.topicName}`);
11985
+ const sender = m.senderName;
11986
+ const ago = formatRelativeTime(m.createdAt);
11987
+ const snippet = m.snippet ?? (m.bodyVersion === 2 ? dim("[encrypted — open the topic to decrypt]") : dim("[empty]"));
11988
+ const highlighted = m.snippet ? highlightMatch(snippet, flags.query) : snippet;
11989
+ process.stdout.write(` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}
11990
+ ` + ` ${highlighted}
11991
+ `);
11992
+ }
11993
+ }
11994
+ return EXIT.SUCCESS;
11995
+ });
11996
+ }
11997
+ function highlightMatch(text, query) {
11998
+ if (!query)
11999
+ return text;
12000
+ const idx = text.toLowerCase().indexOf(query.toLowerCase());
12001
+ if (idx === -1)
12002
+ return text;
12003
+ const before = text.slice(0, idx);
12004
+ const match = text.slice(idx, idx + query.length);
12005
+ const after = text.slice(idx + query.length);
12006
+ return `${before}${yellow(match)}${after}`;
12007
+ }
12008
+ function formatRelativeTime(iso) {
12009
+ const then = new Date(iso).getTime();
12010
+ const now = Date.now();
12011
+ const sec = Math.max(0, Math.floor((now - then) / 1000));
12012
+ if (sec < 60)
12013
+ return `${sec}s ago`;
12014
+ if (sec < 3600)
12015
+ return `${Math.floor(sec / 60)}m ago`;
12016
+ if (sec < 86400)
12017
+ return `${Math.floor(sec / 3600)}h ago`;
12018
+ if (sec < 86400 * 30)
12019
+ return `${Math.floor(sec / 86400)}d ago`;
12020
+ if (sec < 86400 * 365)
12021
+ return `${Math.floor(sec / (86400 * 30))}mo ago`;
12022
+ return `${Math.floor(sec / (86400 * 365))}y ago`;
12023
+ }
12024
+ var init_me = __esm(() => {
12025
+ init_with_rest_key();
12026
+ init_client();
12027
+ init_facade();
12028
+ init_render();
12029
+ init_styles();
12030
+ init_exit_codes();
12031
+ });
12032
+
11769
12033
  // src/services/crypto/topic-key.ts
11770
12034
  function cacheKey(apiKeySecret, topicName) {
11771
12035
  return `${apiKeySecret.slice(0, 12)}:${topicName}`;
@@ -12101,439 +12365,182 @@ async function runTopicTail(name, flags) {
12101
12365
  render.err(`tail aborted: ${err.message}`);
12102
12366
  return EXIT.INTERNAL_ERROR;
12103
12367
  } finally {
12104
- process.removeListener("SIGINT", onSig);
12105
- process.removeListener("SIGTERM", onSig);
12106
- if (resealTimer)
12107
- clearInterval(resealTimer);
12108
- }
12109
- });
12110
- }
12111
- var RECENT_CACHE_MAX = 256;
12112
- var init_topic_tail = __esm(() => {
12113
- init_urls();
12114
- init_with_rest_key();
12115
- init_client();
12116
- init_topic_key();
12117
- init_render();
12118
- init_styles();
12119
- init_exit_codes();
12120
- });
12121
-
12122
- // src/commands/topic-post.ts
12123
- var exports_topic_post = {};
12124
- __export(exports_topic_post, {
12125
- runTopicPost: () => runTopicPost
12126
- });
12127
- async function runTopicPost(topicName, message, flags) {
12128
- if (!topicName || !message) {
12129
- render.err("Usage: claudemesh topic post <topic> <message>");
12130
- return EXIT.INVALID_ARGS;
12131
- }
12132
- const cleanName = topicName.replace(/^#/, "");
12133
- const mentions = [];
12134
- const mentionRe = /(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]{1,64})(?=$|[^A-Za-z0-9_-])/g;
12135
- let m;
12136
- while ((m = mentionRe.exec(message)) !== null) {
12137
- mentions.push(m[2].toLowerCase());
12138
- if (mentions.length >= 16)
12139
- break;
12140
- }
12141
- return withRestKey({
12142
- meshSlug: flags.mesh ?? null,
12143
- purpose: `post-${cleanName}`,
12144
- capabilities: ["read", "send"],
12145
- topicScopes: [cleanName]
12146
- }, async ({ secret, mesh }) => {
12147
- let bodyVersion = 1;
12148
- let ciphertext;
12149
- let nonce;
12150
- if (flags.plaintext) {
12151
- ciphertext = Buffer.from(message, "utf-8").toString("base64");
12152
- nonce = Buffer.from(new Uint8Array(24)).toString("base64");
12153
- } else {
12154
- const keyResult = await getTopicKey({
12155
- apiKeySecret: secret,
12156
- memberSecretKeyHex: mesh.secretKey,
12157
- topicName: cleanName
12158
- });
12159
- if (keyResult.ok && keyResult.topicKey) {
12160
- const enc = await encryptMessage(keyResult.topicKey, message);
12161
- ciphertext = enc.ciphertext;
12162
- nonce = enc.nonce;
12163
- bodyVersion = 2;
12164
- } else if (keyResult.error === "topic_unencrypted") {
12165
- ciphertext = Buffer.from(message, "utf-8").toString("base64");
12166
- nonce = Buffer.from(new Uint8Array(24)).toString("base64");
12167
- } else {
12168
- render.err(`cannot encrypt for #${cleanName}: ${keyResult.error ?? "unknown"}${keyResult.message ? " — " + keyResult.message : ""}`);
12169
- return EXIT.INTERNAL_ERROR;
12170
- }
12171
- }
12172
- let replyToId;
12173
- if (flags.replyTo) {
12174
- if (flags.replyTo.length >= 16) {
12175
- replyToId = flags.replyTo;
12176
- } else if (flags.replyTo.length >= 6) {
12177
- const recent = await request({
12178
- path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=200`,
12179
- method: "GET",
12180
- token: secret
12181
- });
12182
- const hit = recent.messages?.find((r) => r.id.startsWith(flags.replyTo));
12183
- if (!hit) {
12184
- render.err(`--reply-to ${flags.replyTo}: no recent message id starts with that prefix`);
12185
- return EXIT.INVALID_ARGS;
12186
- }
12187
- replyToId = hit.id;
12188
- } else {
12189
- render.err("--reply-to needs at least 6 characters of the message id");
12190
- return EXIT.INVALID_ARGS;
12191
- }
12192
- }
12193
- const result = await request({
12194
- path: "/api/v1/messages",
12195
- method: "POST",
12196
- token: secret,
12197
- body: {
12198
- topic: cleanName,
12199
- ciphertext,
12200
- nonce,
12201
- bodyVersion,
12202
- ...mentions.length > 0 ? { mentions } : {},
12203
- ...replyToId ? { replyToId } : {}
12204
- }
12205
- });
12206
- if (flags.json) {
12207
- console.log(JSON.stringify({ ...result, bodyVersion, mentions }));
12208
- return EXIT.SUCCESS;
12209
- }
12210
- const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
12211
- const replyTag = result.replyToId ? ` ${dim("↳ " + result.replyToId.slice(0, 8))}` : "";
12212
- render.ok("posted", `${clay("#" + cleanName)} ${versionTag}${replyTag} ${dim(`(${result.notifications} mentions)`)}`);
12213
- return EXIT.SUCCESS;
12214
- });
12215
- }
12216
- var init_topic_post = __esm(() => {
12217
- init_with_rest_key();
12218
- init_client();
12219
- init_topic_key();
12220
- init_render();
12221
- init_styles();
12222
- init_exit_codes();
12223
- });
12224
-
12225
- // src/commands/notification.ts
12226
- var exports_notification = {};
12227
- __export(exports_notification, {
12228
- runNotificationList: () => runNotificationList
12229
- });
12230
- function decodeCiphertext(b64) {
12231
- try {
12232
- return Buffer.from(b64, "base64").toString("utf-8");
12233
- } catch {
12234
- return "[decode failed]";
12235
- }
12236
- }
12237
- function fmtRelative(iso) {
12238
- const ms = Date.now() - new Date(iso).getTime();
12239
- if (ms < 60000)
12240
- return "now";
12241
- if (ms < 3600000)
12242
- return `${Math.floor(ms / 60000)}m`;
12243
- if (ms < 86400000)
12244
- return `${Math.floor(ms / 3600000)}h`;
12245
- return `${Math.floor(ms / 86400000)}d`;
12246
- }
12247
- async function runNotificationList(flags) {
12248
- return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "notifications" }, async ({ secret }) => {
12249
- const qs = flags.since ? `?since=${encodeURIComponent(flags.since)}` : "";
12250
- const result = await request({
12251
- path: `/api/v1/notifications${qs}`,
12252
- token: secret
12253
- });
12254
- if (flags.json) {
12255
- const decoded = result.notifications.map((n) => ({
12256
- ...n,
12257
- message: decodeCiphertext(n.ciphertext)
12258
- }));
12259
- console.log(JSON.stringify({ ...result, notifications: decoded }, null, 2));
12260
- return EXIT.SUCCESS;
12261
- }
12262
- if (result.notifications.length === 0) {
12263
- render.info(dim(`no mentions of @${result.mentionedAs} since ${result.since}.`));
12264
- return EXIT.SUCCESS;
12265
- }
12266
- render.section(`mentions of @${bold(result.mentionedAs)} (${result.notifications.length})`);
12267
- for (const n of result.notifications) {
12268
- const when = fmtRelative(n.createdAt);
12269
- const msg = decodeCiphertext(n.ciphertext).replace(/\s+/g, " ").trim();
12270
- const snippet = msg.length > 100 ? msg.slice(0, 97) + "…" : msg;
12271
- process.stdout.write(` ${clay("#" + n.topicName)} ${dim(when)} ${bold(n.senderName)}
12272
- `);
12273
- process.stdout.write(` ${snippet}
12274
- `);
12275
- }
12276
- return EXIT.SUCCESS;
12277
- });
12278
- }
12279
- var init_notification = __esm(() => {
12280
- init_with_rest_key();
12281
- init_client();
12282
- init_render();
12283
- init_styles();
12284
- init_exit_codes();
12285
- });
12286
-
12287
- // src/commands/me.ts
12288
- var exports_me = {};
12289
- __export(exports_me, {
12290
- runMeTopics: () => runMeTopics,
12291
- runMeSearch: () => runMeSearch,
12292
- runMeNotifications: () => runMeNotifications,
12293
- runMeActivity: () => runMeActivity,
12294
- runMe: () => runMe
12295
- });
12296
- async function runMe(flags) {
12297
- return withRestKey({
12298
- meshSlug: flags.mesh ?? null,
12299
- purpose: "workspace-overview",
12300
- capabilities: ["read"]
12301
- }, async ({ secret }) => {
12302
- const ws = await request({
12303
- path: "/api/v1/me/workspace",
12304
- token: secret
12305
- });
12306
- if (flags.json) {
12307
- console.log(JSON.stringify(ws, null, 2));
12308
- return EXIT.SUCCESS;
12309
- }
12310
- render.section(`${clay("workspace")} — ${bold(ws.userId.slice(0, 8))} ${dim(`· ${ws.totals.meshes} mesh${ws.totals.meshes === 1 ? "" : "es"}`)}`);
12311
- const totalsLine = [
12312
- `${green(String(ws.totals.online))}/${ws.totals.peers} online`,
12313
- `${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}`,
12314
- ws.totals.unreadMentions > 0 ? yellow(`${ws.totals.unreadMentions} unread @you`) : dim("0 unread @you")
12315
- ].join(dim(" · "));
12316
- process.stdout.write(" " + totalsLine + `
12317
-
12318
- `);
12319
- if (ws.meshes.length === 0) {
12320
- process.stdout.write(dim(" no meshes joined — run `claudemesh new` or accept an invite\n"));
12321
- return EXIT.SUCCESS;
12322
- }
12323
- const slugWidth = Math.max(...ws.meshes.map((m) => m.slug.length), 8);
12324
- for (const m of ws.meshes) {
12325
- const slug = cyan(m.slug.padEnd(slugWidth));
12326
- const peers = `${m.online}/${m.peers}`;
12327
- const role = dim(m.myRole);
12328
- const unread = m.unreadMentions > 0 ? " " + yellow(`${m.unreadMentions} @you`) : "";
12329
- process.stdout.write(` ${slug} ${peers.padStart(5)} online ${dim(String(m.topics).padStart(2) + " topics")} ${role}${unread}
12330
- `);
12331
- }
12332
- return EXIT.SUCCESS;
12333
- });
12334
- }
12335
- async function runMeTopics(flags) {
12336
- return withRestKey({
12337
- meshSlug: flags.mesh ?? null,
12338
- purpose: "workspace-topics",
12339
- capabilities: ["read"]
12340
- }, async ({ secret }) => {
12341
- const ws = await request({
12342
- path: "/api/v1/me/topics",
12343
- token: secret
12344
- });
12345
- const visible = flags.unread ? ws.topics.filter((t) => t.unread > 0) : ws.topics;
12346
- if (flags.json) {
12347
- console.log(JSON.stringify({ topics: visible, totals: ws.totals }, null, 2));
12348
- return EXIT.SUCCESS;
12349
- }
12350
- render.section(`${clay("topics")} — ${ws.totals.topics} across all meshes ${dim(ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· all read")}`);
12351
- if (visible.length === 0) {
12352
- process.stdout.write(dim(flags.unread ? ` no unread topics
12353
- ` : " no topics — run `claudemesh topic create #general`\n"));
12354
- return EXIT.SUCCESS;
12355
- }
12356
- const slugWidth = Math.max(...visible.map((t) => t.meshSlug.length), 6);
12357
- const nameWidth = Math.max(...visible.map((t) => t.name.length), 8);
12358
- for (const t of visible) {
12359
- const slug = dim(t.meshSlug.padEnd(slugWidth));
12360
- const name = cyan(t.name.padEnd(nameWidth));
12361
- const unread = t.unread > 0 ? yellow(`${t.unread} unread`.padStart(10)) : dim("·".padStart(10));
12362
- const last = t.lastMessageAt ? dim(formatRelativeTime(t.lastMessageAt)) : dim("never");
12363
- process.stdout.write(` ${slug} ${name} ${unread} ${last}
12364
- `);
12365
- }
12366
- return EXIT.SUCCESS;
12367
- });
12368
- }
12369
- async function runMeNotifications(flags) {
12370
- return withRestKey({
12371
- meshSlug: flags.mesh ?? null,
12372
- purpose: "workspace-notifications",
12373
- capabilities: ["read"]
12374
- }, async ({ secret }) => {
12375
- const params = new URLSearchParams;
12376
- if (flags.all)
12377
- params.set("include", "all");
12378
- if (flags.since)
12379
- params.set("since", flags.since);
12380
- const path = "/api/v1/me/notifications" + (params.toString() ? `?${params.toString()}` : "");
12381
- const ws = await request({
12382
- path,
12383
- token: secret
12384
- });
12385
- if (flags.json) {
12386
- console.log(JSON.stringify(ws, null, 2));
12387
- return EXIT.SUCCESS;
12388
- }
12389
- const headerLabel = flags.all ? "@-mentions (all)" : "@-mentions (unread)";
12390
- render.section(`${clay(headerLabel)} — ${ws.totals.total} ${dim(ws.totals.unread > 0 ? `· ${ws.totals.unread} unread` : "· nothing pending")}`);
12391
- if (ws.notifications.length === 0) {
12392
- process.stdout.write(dim(flags.all ? ` no @-mentions in window
12393
- ` : ` inbox zero — nothing waiting
12394
- `));
12395
- return EXIT.SUCCESS;
12396
- }
12397
- const slugWidth = Math.max(...ws.notifications.map((n) => n.meshSlug.length), 6);
12398
- for (const n of ws.notifications) {
12399
- const slug = dim(n.meshSlug.padEnd(slugWidth));
12400
- const topic = cyan(`#${n.topicName}`);
12401
- const sender = n.senderName ? `from ${n.senderName}` : "from ?";
12402
- const ago = formatRelativeTime(n.createdAt);
12403
- const dot = n.read ? dim("·") : yellow("●");
12404
- const snippet = n.snippet ?? (n.ciphertext ? dim("[encrypted]") : dim("[empty]"));
12405
- process.stdout.write(` ${dot} ${slug} ${topic} ${dim(sender)} ${dim(ago)}
12406
- ` + ` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}
12407
- `);
12368
+ process.removeListener("SIGINT", onSig);
12369
+ process.removeListener("SIGTERM", onSig);
12370
+ if (resealTimer)
12371
+ clearInterval(resealTimer);
12408
12372
  }
12409
- return EXIT.SUCCESS;
12410
12373
  });
12411
12374
  }
12412
- async function runMeActivity(flags) {
12375
+ var RECENT_CACHE_MAX = 256;
12376
+ var init_topic_tail = __esm(() => {
12377
+ init_urls();
12378
+ init_with_rest_key();
12379
+ init_client();
12380
+ init_topic_key();
12381
+ init_render();
12382
+ init_styles();
12383
+ init_exit_codes();
12384
+ });
12385
+
12386
+ // src/commands/topic-post.ts
12387
+ var exports_topic_post = {};
12388
+ __export(exports_topic_post, {
12389
+ runTopicPost: () => runTopicPost
12390
+ });
12391
+ async function runTopicPost(topicName, message, flags) {
12392
+ if (!topicName || !message) {
12393
+ render.err("Usage: claudemesh topic post <topic> <message>");
12394
+ return EXIT.INVALID_ARGS;
12395
+ }
12396
+ const cleanName = topicName.replace(/^#/, "");
12397
+ const mentions = [];
12398
+ const mentionRe = /(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]{1,64})(?=$|[^A-Za-z0-9_-])/g;
12399
+ let m;
12400
+ while ((m = mentionRe.exec(message)) !== null) {
12401
+ mentions.push(m[2].toLowerCase());
12402
+ if (mentions.length >= 16)
12403
+ break;
12404
+ }
12413
12405
  return withRestKey({
12414
12406
  meshSlug: flags.mesh ?? null,
12415
- purpose: "workspace-activity",
12416
- capabilities: ["read"]
12417
- }, async ({ secret }) => {
12418
- const params = new URLSearchParams;
12419
- if (flags.since)
12420
- params.set("since", flags.since);
12421
- const path = "/api/v1/me/activity" + (params.toString() ? `?${params.toString()}` : "");
12422
- const ws = await request({
12423
- path,
12424
- token: secret
12407
+ purpose: `post-${cleanName}`,
12408
+ capabilities: ["read", "send"],
12409
+ topicScopes: [cleanName]
12410
+ }, async ({ secret, mesh }) => {
12411
+ let bodyVersion = 1;
12412
+ let ciphertext;
12413
+ let nonce;
12414
+ if (flags.plaintext) {
12415
+ ciphertext = Buffer.from(message, "utf-8").toString("base64");
12416
+ nonce = Buffer.from(new Uint8Array(24)).toString("base64");
12417
+ } else {
12418
+ const keyResult = await getTopicKey({
12419
+ apiKeySecret: secret,
12420
+ memberSecretKeyHex: mesh.secretKey,
12421
+ topicName: cleanName
12422
+ });
12423
+ if (keyResult.ok && keyResult.topicKey) {
12424
+ const enc = await encryptMessage(keyResult.topicKey, message);
12425
+ ciphertext = enc.ciphertext;
12426
+ nonce = enc.nonce;
12427
+ bodyVersion = 2;
12428
+ } else if (keyResult.error === "topic_unencrypted") {
12429
+ ciphertext = Buffer.from(message, "utf-8").toString("base64");
12430
+ nonce = Buffer.from(new Uint8Array(24)).toString("base64");
12431
+ } else {
12432
+ render.err(`cannot encrypt for #${cleanName}: ${keyResult.error ?? "unknown"}${keyResult.message ? " — " + keyResult.message : ""}`);
12433
+ return EXIT.INTERNAL_ERROR;
12434
+ }
12435
+ }
12436
+ let replyToId;
12437
+ if (flags.replyTo) {
12438
+ if (flags.replyTo.length >= 16) {
12439
+ replyToId = flags.replyTo;
12440
+ } else if (flags.replyTo.length >= 6) {
12441
+ const recent = await request({
12442
+ path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=200`,
12443
+ method: "GET",
12444
+ token: secret
12445
+ });
12446
+ const hit = recent.messages?.find((r) => r.id.startsWith(flags.replyTo));
12447
+ if (!hit) {
12448
+ render.err(`--reply-to ${flags.replyTo}: no recent message id starts with that prefix`);
12449
+ return EXIT.INVALID_ARGS;
12450
+ }
12451
+ replyToId = hit.id;
12452
+ } else {
12453
+ render.err("--reply-to needs at least 6 characters of the message id");
12454
+ return EXIT.INVALID_ARGS;
12455
+ }
12456
+ }
12457
+ const result = await request({
12458
+ path: "/api/v1/messages",
12459
+ method: "POST",
12460
+ token: secret,
12461
+ body: {
12462
+ topic: cleanName,
12463
+ ciphertext,
12464
+ nonce,
12465
+ bodyVersion,
12466
+ ...mentions.length > 0 ? { mentions } : {},
12467
+ ...replyToId ? { replyToId } : {}
12468
+ }
12425
12469
  });
12426
12470
  if (flags.json) {
12427
- console.log(JSON.stringify(ws, null, 2));
12428
- return EXIT.SUCCESS;
12429
- }
12430
- render.section(`${clay("activity")} — ${ws.totals.events} ${dim(flags.since ? `since ${flags.since}` : "in the last 24h")}`);
12431
- if (ws.activity.length === 0) {
12432
- process.stdout.write(dim(` quiet — no activity in window
12433
- `));
12471
+ console.log(JSON.stringify({ ...result, bodyVersion, mentions }));
12434
12472
  return EXIT.SUCCESS;
12435
12473
  }
12436
- const slugWidth = Math.max(...ws.activity.map((a) => a.meshSlug.length), 6);
12437
- for (const a of ws.activity) {
12438
- const slug = dim(a.meshSlug.padEnd(slugWidth));
12439
- const topic = cyan(`#${a.topicName}`);
12440
- const sender = a.senderName ?? "?";
12441
- const ago = formatRelativeTime(a.createdAt);
12442
- const snippet = a.snippet ?? (a.ciphertext ? dim("[encrypted]") : dim("[empty]"));
12443
- process.stdout.write(` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}
12444
- ` + ` ${snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet}
12445
- `);
12446
- }
12474
+ const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
12475
+ const replyTag = result.replyToId ? ` ${dim("↳ " + result.replyToId.slice(0, 8))}` : "";
12476
+ render.ok("posted", `${clay("#" + cleanName)} ${versionTag}${replyTag} ${dim(`(${result.notifications} mentions)`)}`);
12447
12477
  return EXIT.SUCCESS;
12448
12478
  });
12449
12479
  }
12450
- async function runMeSearch(flags) {
12451
- if (!flags.query || flags.query.length < 2) {
12452
- process.stderr.write(`Usage: claudemesh me search <query> (min 2 chars)
12453
- `);
12454
- return EXIT.INVALID_ARGS;
12480
+ var init_topic_post = __esm(() => {
12481
+ init_with_rest_key();
12482
+ init_client();
12483
+ init_topic_key();
12484
+ init_render();
12485
+ init_styles();
12486
+ init_exit_codes();
12487
+ });
12488
+
12489
+ // src/commands/notification.ts
12490
+ var exports_notification = {};
12491
+ __export(exports_notification, {
12492
+ runNotificationList: () => runNotificationList
12493
+ });
12494
+ function decodeCiphertext(b64) {
12495
+ try {
12496
+ return Buffer.from(b64, "base64").toString("utf-8");
12497
+ } catch {
12498
+ return "[decode failed]";
12455
12499
  }
12456
- return withRestKey({
12457
- meshSlug: flags.mesh ?? null,
12458
- purpose: "workspace-search",
12459
- capabilities: ["read"]
12460
- }, async ({ secret }) => {
12461
- const params = new URLSearchParams({ q: flags.query });
12462
- const ws = await request({
12463
- path: `/api/v1/me/search?${params.toString()}`,
12500
+ }
12501
+ function fmtRelative(iso) {
12502
+ const ms = Date.now() - new Date(iso).getTime();
12503
+ if (ms < 60000)
12504
+ return "now";
12505
+ if (ms < 3600000)
12506
+ return `${Math.floor(ms / 60000)}m`;
12507
+ if (ms < 86400000)
12508
+ return `${Math.floor(ms / 3600000)}h`;
12509
+ return `${Math.floor(ms / 86400000)}d`;
12510
+ }
12511
+ async function runNotificationList(flags) {
12512
+ return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "notifications" }, async ({ secret }) => {
12513
+ const qs = flags.since ? `?since=${encodeURIComponent(flags.since)}` : "";
12514
+ const result = await request({
12515
+ path: `/api/v1/notifications${qs}`,
12464
12516
  token: secret
12465
12517
  });
12466
12518
  if (flags.json) {
12467
- console.log(JSON.stringify(ws, null, 2));
12519
+ const decoded = result.notifications.map((n) => ({
12520
+ ...n,
12521
+ message: decodeCiphertext(n.ciphertext)
12522
+ }));
12523
+ console.log(JSON.stringify({ ...result, notifications: decoded }, null, 2));
12468
12524
  return EXIT.SUCCESS;
12469
12525
  }
12470
- render.section(`${clay("search")} — "${flags.query}" ${dim(`${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}, ` + `${ws.totals.messages} message${ws.totals.messages === 1 ? "" : "s"}`)}`);
12471
- if (ws.topics.length === 0 && ws.messages.length === 0) {
12472
- process.stdout.write(dim(` no matches
12473
- `));
12526
+ if (result.notifications.length === 0) {
12527
+ render.info(dim(`no mentions of @${result.mentionedAs} since ${result.since}.`));
12474
12528
  return EXIT.SUCCESS;
12475
12529
  }
12476
- if (ws.topics.length > 0) {
12477
- process.stdout.write(dim(`
12478
- topics
12479
- `));
12480
- const slugWidth = Math.max(...ws.topics.map((t) => t.meshSlug.length), 6);
12481
- for (const t of ws.topics) {
12482
- const slug = dim(t.meshSlug.padEnd(slugWidth));
12483
- const name = cyan(`#${t.name}`);
12484
- const desc = t.description ? dim(` — ${t.description}`) : "";
12485
- process.stdout.write(` ${slug} ${name}${desc}
12530
+ render.section(`mentions of @${bold(result.mentionedAs)} (${result.notifications.length})`);
12531
+ for (const n of result.notifications) {
12532
+ const when = fmtRelative(n.createdAt);
12533
+ const msg = decodeCiphertext(n.ciphertext).replace(/\s+/g, " ").trim();
12534
+ const snippet = msg.length > 100 ? msg.slice(0, 97) + "…" : msg;
12535
+ process.stdout.write(` ${clay("#" + n.topicName)} ${dim(when)} ${bold(n.senderName)}
12486
12536
  `);
12487
- }
12488
- }
12489
- if (ws.messages.length > 0) {
12490
- process.stdout.write(dim(`
12491
- messages
12492
- `));
12493
- const slugWidth = Math.max(...ws.messages.map((m) => m.meshSlug.length), 6);
12494
- for (const m of ws.messages) {
12495
- const slug = dim(m.meshSlug.padEnd(slugWidth));
12496
- const topic = cyan(`#${m.topicName}`);
12497
- const sender = m.senderName;
12498
- const ago = formatRelativeTime(m.createdAt);
12499
- const snippet = m.snippet ?? (m.bodyVersion === 2 ? dim("[encrypted — open the topic to decrypt]") : dim("[empty]"));
12500
- const highlighted = m.snippet ? highlightMatch(snippet, flags.query) : snippet;
12501
- process.stdout.write(` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}
12502
- ` + ` ${highlighted}
12537
+ process.stdout.write(` ${snippet}
12503
12538
  `);
12504
- }
12505
12539
  }
12506
12540
  return EXIT.SUCCESS;
12507
12541
  });
12508
12542
  }
12509
- function highlightMatch(text, query) {
12510
- if (!query)
12511
- return text;
12512
- const idx = text.toLowerCase().indexOf(query.toLowerCase());
12513
- if (idx === -1)
12514
- return text;
12515
- const before = text.slice(0, idx);
12516
- const match = text.slice(idx, idx + query.length);
12517
- const after = text.slice(idx + query.length);
12518
- return `${before}${yellow(match)}${after}`;
12519
- }
12520
- function formatRelativeTime(iso) {
12521
- const then = new Date(iso).getTime();
12522
- const now = Date.now();
12523
- const sec = Math.max(0, Math.floor((now - then) / 1000));
12524
- if (sec < 60)
12525
- return `${sec}s ago`;
12526
- if (sec < 3600)
12527
- return `${Math.floor(sec / 60)}m ago`;
12528
- if (sec < 86400)
12529
- return `${Math.floor(sec / 3600)}h ago`;
12530
- if (sec < 86400 * 30)
12531
- return `${Math.floor(sec / 86400)}d ago`;
12532
- if (sec < 86400 * 365)
12533
- return `${Math.floor(sec / (86400 * 30))}mo ago`;
12534
- return `${Math.floor(sec / (86400 * 365))}y ago`;
12535
- }
12536
- var init_me = __esm(() => {
12543
+ var init_notification = __esm(() => {
12537
12544
  init_with_rest_key();
12538
12545
  init_client();
12539
12546
  init_render();
@@ -14171,7 +14178,7 @@ Bridge (forward a topic between two meshes, v0.2.0)
14171
14178
 
14172
14179
  Topic (conversation scope, v0.2.0)
14173
14180
  claudemesh topic create <name> create a topic [--description --visibility]
14174
- claudemesh topic list list topics in the mesh
14181
+ claudemesh topic list list topics across all meshes (or --mesh <slug>)
14175
14182
  claudemesh topic join <topic> subscribe (via name or id)
14176
14183
  claudemesh topic leave <topic> unsubscribe
14177
14184
  claudemesh topic members <t> list topic subscribers
@@ -14186,7 +14193,7 @@ Topic (conversation scope, v0.2.0)
14186
14193
  claudemesh me activity cross-mesh recent messages [--since=ISO]
14187
14194
  claudemesh me search <q> cross-mesh search (topics + messages)
14188
14195
  claudemesh member list mesh roster with online state [--online]
14189
- claudemesh notification list recent @-mentions of you [--since <ISO>]
14196
+ claudemesh notification list @-mentions across all meshes (or --mesh <slug>)
14190
14197
 
14191
14198
  Schedule (resource form)
14192
14199
  claudemesh schedule msg <m> one-shot or recurring (alias: remind)
@@ -14983,6 +14990,10 @@ async function main() {
14983
14990
  const { runTopicCreate: runTopicCreate2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
14984
14991
  process.exit(await runTopicCreate2(arg, f));
14985
14992
  } else if (sub === "list") {
14993
+ if (!f.mesh) {
14994
+ const { runMeTopics: runMeTopics2 } = await Promise.resolve().then(() => (init_me(), exports_me));
14995
+ process.exit(await runMeTopics2({ mesh: undefined, json: f.json, unread: !!flags.unread }));
14996
+ }
14986
14997
  const { runTopicList: runTopicList2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
14987
14998
  process.exit(await runTopicList2(f));
14988
14999
  } else if (sub === "join") {
@@ -15034,10 +15045,19 @@ async function main() {
15034
15045
  since: flags.since
15035
15046
  };
15036
15047
  if (sub === "list") {
15048
+ if (!f.mesh) {
15049
+ const { runMeNotifications: runMeNotifications2 } = await Promise.resolve().then(() => (init_me(), exports_me));
15050
+ process.exit(await runMeNotifications2({
15051
+ mesh: undefined,
15052
+ json: f.json,
15053
+ all: !!flags.all,
15054
+ since: f.since
15055
+ }));
15056
+ }
15037
15057
  const { runNotificationList: runNotificationList2 } = await Promise.resolve().then(() => (init_notification(), exports_notification));
15038
15058
  process.exit(await runNotificationList2(f));
15039
15059
  } else {
15040
- console.error("Usage: claudemesh notification list [--since <ISO>]");
15060
+ console.error("Usage: claudemesh notification list [--mesh <slug>] [--since <ISO>] [--all]");
15041
15061
  process.exit(EXIT.INVALID_ARGS);
15042
15062
  }
15043
15063
  break;
@@ -15150,4 +15170,4 @@ main().catch((err) => {
15150
15170
  process.exit(EXIT.INTERNAL_ERROR);
15151
15171
  });
15152
15172
 
15153
- //# debugId=70FF6020E5175BED64756E2164756E21
15173
+ //# debugId=3168504A6207F58264756E2164756E21