claudemesh-cli 1.7.0 → 1.9.2
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/README.md +7 -2
- package/dist/entrypoints/cli.js +347 -16
- package/dist/entrypoints/cli.js.map +9 -7
- package/dist/entrypoints/mcp.js +25 -4
- package/dist/entrypoints/mcp.js.map +4 -4
- package/package.json +1 -1
- package/skills/claudemesh/SKILL.md +71 -1
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Peer mesh for Claude Code sessions. Connect multiple Claude Code instances into a shared mesh with real-time messaging, shared state, memory, file sharing, vector store, scheduled jobs, and more — all driven from the `claudemesh` CLI. The MCP server is a tool-less push-pipe that delivers inbound peer messages to Claude as `<channel>` interrupts; everything else lives behind CLI verbs that Claude learns from the auto-installed `claudemesh` skill.
|
|
4
4
|
|
|
5
|
-
> **What's new in 1.
|
|
5
|
+
> **What's new in 1.9.x:** topic threading + multi-session reliability fixes. `claudemesh topic post <topic> <msg> --reply-to <id>` threads a reply onto a previous topic message (full id or 8+ char prefix); `topic tail` renders `↳ in reply to <name>: "<snippet>"` above replies and shows a copyable `#xxxxxxxx` short id on every row. `<channel>` MCP attrs now carry `from_member_id`, `from_pubkey` (stable), `from_session_pubkey` (ephemeral), `message_id`, `topic`, `reply_to_id` — everything the recipient needs to reply directly. Broker fixes (v0.3.2): replies to a stale session pubkey now resolve to the owning member's live session instead of bouncing with "not online", and broadcast `*` no longer loopbacks decrypt-fail warnings to the sender's sibling sessions.
|
|
6
|
+
>
|
|
7
|
+
> **What was new in 1.8.0:** per-topic end-to-end encryption (v0.3.0 phase 3, CLI side). `claudemesh topic post <topic> <msg>` encrypts the body with `crypto_secretbox` under the topic's symmetric key — broker stores ciphertext only. `claudemesh topic tail` now decrypts v2 messages on render and runs a background re-seal loop every 30s, so new topic joiners get their sealed keys without manual action. `topic-key` cache is process-only — kill the CLI, the key forgets. Web dashboard reads v1 plaintext for now (phase 3.5 brings browser-side identity).
|
|
8
|
+
>
|
|
9
|
+
> **What was new in 1.7.0:** terminal parity for the v1.6.x server features. New verbs: `claudemesh topic tail` (live SSE message stream — Ctrl-C to exit), `claudemesh notification list` (recent `@you` mentions across topics), `claudemesh member list` (mesh roster with online dots, distinct from `peer list`'s live-session view). Each command auto-mints a 5-minute read-only apikey via the WebSocket and revokes it on exit, so no token plumbing is needed.
|
|
6
10
|
>
|
|
7
11
|
> **What was new in 1.6.0:** topics (channel pub/sub), API keys for human/REST clients, and bridge peers that forward a topic between two meshes. New verbs: `claudemesh topic`, `claudemesh apikey`, `claudemesh bridge`. A REST surface at `https://claudemesh.com/api/v1/*` (messages, topics, peers, history) accepts `Authorization: Bearer cm_...` keys, so any HTTPS client can participate without WebSocket + ed25519 plumbing. **Note**: REST lives on the web host (`claudemesh.com`), not the broker host (`ic.claudemesh.com`) — the broker only speaks WebSocket.
|
|
8
12
|
>
|
|
@@ -45,7 +49,8 @@ USAGE
|
|
|
45
49
|
claudemesh profile view or edit your profile
|
|
46
50
|
|
|
47
51
|
claudemesh topic ... create, list, join, send to topics
|
|
48
|
-
claudemesh topic tail <t> live SSE tail of a topic
|
|
52
|
+
claudemesh topic tail <t> live SSE tail of a topic (decrypts v2)
|
|
53
|
+
claudemesh topic post <t> encrypted REST post (v2 ciphertext)
|
|
49
54
|
claudemesh member list mesh roster with online state
|
|
50
55
|
claudemesh notification list recent @-mentions of you
|
|
51
56
|
claudemesh apikey ... issue, list, revoke API keys (REST clients)
|
package/dist/entrypoints/cli.js
CHANGED
|
@@ -88,7 +88,7 @@ __export(exports_urls, {
|
|
|
88
88
|
VERSION: () => VERSION,
|
|
89
89
|
URLS: () => URLS
|
|
90
90
|
});
|
|
91
|
-
var URLS, VERSION = "1.
|
|
91
|
+
var URLS, VERSION = "1.9.2", env;
|
|
92
92
|
var init_urls = __esm(() => {
|
|
93
93
|
URLS = {
|
|
94
94
|
BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
|
@@ -2779,6 +2779,11 @@ class BrokerClient {
|
|
|
2779
2779
|
messageId: String(msg.messageId ?? ""),
|
|
2780
2780
|
meshId: String(msg.meshId ?? ""),
|
|
2781
2781
|
senderPubkey,
|
|
2782
|
+
...msg.senderMemberPubkey ? { senderMemberPubkey: String(msg.senderMemberPubkey) } : {},
|
|
2783
|
+
...msg.senderMemberId ? { senderMemberId: String(msg.senderMemberId) } : {},
|
|
2784
|
+
...msg.senderName ? { senderName: String(msg.senderName) } : {},
|
|
2785
|
+
...msg.topic ? { topic: String(msg.topic) } : {},
|
|
2786
|
+
...msg.replyToId ? { replyToId: String(msg.replyToId) } : {},
|
|
2782
2787
|
priority: msg.priority ?? "next",
|
|
2783
2788
|
nonce,
|
|
2784
2789
|
ciphertext,
|
|
@@ -11679,18 +11684,151 @@ var init_with_rest_key = __esm(() => {
|
|
|
11679
11684
|
init_connect();
|
|
11680
11685
|
});
|
|
11681
11686
|
|
|
11687
|
+
// src/services/crypto/topic-key.ts
|
|
11688
|
+
function cacheKey(apiKeySecret, topicName) {
|
|
11689
|
+
return `${apiKeySecret.slice(0, 12)}:${topicName}`;
|
|
11690
|
+
}
|
|
11691
|
+
async function getTopicKey(args) {
|
|
11692
|
+
const cacheId = cacheKey(args.apiKeySecret, args.topicName);
|
|
11693
|
+
if (!args.fresh) {
|
|
11694
|
+
const cached = cache.get(cacheId);
|
|
11695
|
+
if (cached)
|
|
11696
|
+
return { ok: true, topicKey: cached.topicKey };
|
|
11697
|
+
}
|
|
11698
|
+
let sealed;
|
|
11699
|
+
try {
|
|
11700
|
+
sealed = await request({
|
|
11701
|
+
path: `/api/v1/topics/${encodeURIComponent(args.topicName)}/key`,
|
|
11702
|
+
token: args.apiKeySecret
|
|
11703
|
+
});
|
|
11704
|
+
} catch (e) {
|
|
11705
|
+
if (e instanceof ApiError) {
|
|
11706
|
+
if (e.status === 404)
|
|
11707
|
+
return { ok: false, error: "not_sealed" };
|
|
11708
|
+
if (e.status === 409)
|
|
11709
|
+
return { ok: false, error: "topic_unencrypted" };
|
|
11710
|
+
}
|
|
11711
|
+
return {
|
|
11712
|
+
ok: false,
|
|
11713
|
+
error: "network",
|
|
11714
|
+
message: e instanceof Error ? e.message : String(e)
|
|
11715
|
+
};
|
|
11716
|
+
}
|
|
11717
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11718
|
+
await sodium4.ready;
|
|
11719
|
+
let recipientX25519Secret;
|
|
11720
|
+
try {
|
|
11721
|
+
const ed = sodium4.from_hex(args.memberSecretKeyHex);
|
|
11722
|
+
recipientX25519Secret = sodium4.crypto_sign_ed25519_sk_to_curve25519(ed);
|
|
11723
|
+
} catch {
|
|
11724
|
+
return { ok: false, error: "bad_member_secret" };
|
|
11725
|
+
}
|
|
11726
|
+
let topicKey;
|
|
11727
|
+
try {
|
|
11728
|
+
const blob = sodium4.from_base64(sealed.encryptedKey, sodium4.base64_variants.ORIGINAL);
|
|
11729
|
+
const nonce = sodium4.from_base64(sealed.nonce, sodium4.base64_variants.ORIGINAL);
|
|
11730
|
+
if (blob.length < 32 + sodium4.crypto_box_MACBYTES) {
|
|
11731
|
+
return {
|
|
11732
|
+
ok: false,
|
|
11733
|
+
error: "decrypt_failed",
|
|
11734
|
+
message: "sealed key blob too short to contain sender pubkey + cipher"
|
|
11735
|
+
};
|
|
11736
|
+
}
|
|
11737
|
+
const senderX25519 = blob.slice(0, 32);
|
|
11738
|
+
const cipher = blob.slice(32);
|
|
11739
|
+
topicKey = sodium4.crypto_box_open_easy(cipher, nonce, senderX25519, recipientX25519Secret);
|
|
11740
|
+
} catch (e) {
|
|
11741
|
+
return {
|
|
11742
|
+
ok: false,
|
|
11743
|
+
error: "decrypt_failed",
|
|
11744
|
+
message: e instanceof Error ? e.message : String(e)
|
|
11745
|
+
};
|
|
11746
|
+
}
|
|
11747
|
+
cache.set(cacheId, { topicKey, fetchedAt: Date.now() });
|
|
11748
|
+
return { ok: true, topicKey };
|
|
11749
|
+
}
|
|
11750
|
+
async function encryptMessage(topicKey, plaintext) {
|
|
11751
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11752
|
+
await sodium4.ready;
|
|
11753
|
+
const nonceBytes = sodium4.randombytes_buf(sodium4.crypto_secretbox_NONCEBYTES);
|
|
11754
|
+
const cipher = sodium4.crypto_secretbox_easy(sodium4.from_string(plaintext), nonceBytes, topicKey);
|
|
11755
|
+
return {
|
|
11756
|
+
ciphertext: sodium4.to_base64(cipher, sodium4.base64_variants.ORIGINAL),
|
|
11757
|
+
nonce: sodium4.to_base64(nonceBytes, sodium4.base64_variants.ORIGINAL)
|
|
11758
|
+
};
|
|
11759
|
+
}
|
|
11760
|
+
async function decryptMessage(topicKey, ciphertextB64, nonceB64) {
|
|
11761
|
+
try {
|
|
11762
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11763
|
+
await sodium4.ready;
|
|
11764
|
+
const cipher = sodium4.from_base64(ciphertextB64, sodium4.base64_variants.ORIGINAL);
|
|
11765
|
+
const nonce = sodium4.from_base64(nonceB64, sodium4.base64_variants.ORIGINAL);
|
|
11766
|
+
const plain = sodium4.crypto_secretbox_open_easy(cipher, nonce, topicKey);
|
|
11767
|
+
return sodium4.to_string(plain);
|
|
11768
|
+
} catch {
|
|
11769
|
+
return null;
|
|
11770
|
+
}
|
|
11771
|
+
}
|
|
11772
|
+
async function sealTopicKeyFor(topicKey, recipientPubkeyHex, ourMemberSecretKeyHex) {
|
|
11773
|
+
try {
|
|
11774
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11775
|
+
await sodium4.ready;
|
|
11776
|
+
const recipientX25519 = sodium4.crypto_sign_ed25519_pk_to_curve25519(sodium4.from_hex(recipientPubkeyHex));
|
|
11777
|
+
const ourEdSecret = sodium4.from_hex(ourMemberSecretKeyHex);
|
|
11778
|
+
const ourX25519Secret = sodium4.crypto_sign_ed25519_sk_to_curve25519(ourEdSecret);
|
|
11779
|
+
const ourEdPublic = ourEdSecret.slice(32, 64);
|
|
11780
|
+
const ourX25519Public = sodium4.crypto_sign_ed25519_pk_to_curve25519(ourEdPublic);
|
|
11781
|
+
const nonceBytes = sodium4.randombytes_buf(sodium4.crypto_box_NONCEBYTES);
|
|
11782
|
+
const cipher = sodium4.crypto_box_easy(topicKey, nonceBytes, recipientX25519, ourX25519Secret);
|
|
11783
|
+
const blob = new Uint8Array(32 + cipher.length);
|
|
11784
|
+
blob.set(ourX25519Public, 0);
|
|
11785
|
+
blob.set(cipher, 32);
|
|
11786
|
+
return {
|
|
11787
|
+
encryptedKey: sodium4.to_base64(blob, sodium4.base64_variants.ORIGINAL),
|
|
11788
|
+
nonce: sodium4.to_base64(nonceBytes, sodium4.base64_variants.ORIGINAL)
|
|
11789
|
+
};
|
|
11790
|
+
} catch {
|
|
11791
|
+
return null;
|
|
11792
|
+
}
|
|
11793
|
+
}
|
|
11794
|
+
var cache;
|
|
11795
|
+
var init_topic_key = __esm(() => {
|
|
11796
|
+
init_client();
|
|
11797
|
+
init_errors();
|
|
11798
|
+
cache = new Map;
|
|
11799
|
+
});
|
|
11800
|
+
|
|
11682
11801
|
// src/commands/topic-tail.ts
|
|
11683
11802
|
var exports_topic_tail = {};
|
|
11684
11803
|
__export(exports_topic_tail, {
|
|
11685
11804
|
runTopicTail: () => runTopicTail
|
|
11686
11805
|
});
|
|
11687
|
-
function
|
|
11806
|
+
function rememberRendered(cache2, m, text) {
|
|
11807
|
+
cache2.set(m.id, {
|
|
11808
|
+
name: m.senderName || m.senderPubkey.slice(0, 8),
|
|
11809
|
+
snippet: text.replace(/\s+/g, " ").slice(0, 60)
|
|
11810
|
+
});
|
|
11811
|
+
if (cache2.size > RECENT_CACHE_MAX) {
|
|
11812
|
+
const firstKey = cache2.keys().next().value;
|
|
11813
|
+
if (firstKey)
|
|
11814
|
+
cache2.delete(firstKey);
|
|
11815
|
+
}
|
|
11816
|
+
}
|
|
11817
|
+
function decodeV1(b64) {
|
|
11688
11818
|
try {
|
|
11689
11819
|
return Buffer.from(b64, "base64").toString("utf-8");
|
|
11690
11820
|
} catch {
|
|
11691
11821
|
return "[decode failed]";
|
|
11692
11822
|
}
|
|
11693
11823
|
}
|
|
11824
|
+
async function decryptForRender(m, topicKey) {
|
|
11825
|
+
if ((m.bodyVersion ?? 1) === 1)
|
|
11826
|
+
return decodeV1(m.ciphertext);
|
|
11827
|
+
if (!topicKey)
|
|
11828
|
+
return "[encrypted — no topic key]";
|
|
11829
|
+
const plain = await decryptMessage(topicKey, m.ciphertext, m.nonce);
|
|
11830
|
+
return plain ?? "[decrypt failed]";
|
|
11831
|
+
}
|
|
11694
11832
|
function fmtTime(iso) {
|
|
11695
11833
|
try {
|
|
11696
11834
|
return new Date(iso).toLocaleTimeString([], {
|
|
@@ -11702,14 +11840,24 @@ function fmtTime(iso) {
|
|
|
11702
11840
|
return iso;
|
|
11703
11841
|
}
|
|
11704
11842
|
}
|
|
11705
|
-
function printMessage(m, json) {
|
|
11706
|
-
const text =
|
|
11843
|
+
async function printMessage(m, topicKey, json, cache2) {
|
|
11844
|
+
const text = await decryptForRender(m, topicKey);
|
|
11707
11845
|
if (json) {
|
|
11708
11846
|
console.log(JSON.stringify({ ...m, message: text }));
|
|
11847
|
+
rememberRendered(cache2, m, text);
|
|
11709
11848
|
return;
|
|
11710
11849
|
}
|
|
11711
|
-
|
|
11850
|
+
const v2Marker = (m.bodyVersion ?? 1) === 2 ? dim("\uD83D\uDD12 ") : "";
|
|
11851
|
+
if (m.replyToId) {
|
|
11852
|
+
const parent = cache2.get(m.replyToId);
|
|
11853
|
+
const ref = parent ? `${parent.name}: "${parent.snippet}${parent.snippet.length === 60 ? "…" : ""}"` : `${m.replyToId.slice(0, 8)}…`;
|
|
11854
|
+
process.stdout.write(` ${dim("↳ in reply to " + ref)}
|
|
11855
|
+
`);
|
|
11856
|
+
}
|
|
11857
|
+
const idTag = dim(`#${m.id.slice(0, 8)}`);
|
|
11858
|
+
process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${idTag} ${v2Marker}${text}
|
|
11712
11859
|
`);
|
|
11860
|
+
rememberRendered(cache2, m, text);
|
|
11713
11861
|
}
|
|
11714
11862
|
async function* readSseStream(reader) {
|
|
11715
11863
|
const decoder = new TextDecoder;
|
|
@@ -11761,7 +11909,56 @@ async function runTopicTail(name, flags) {
|
|
|
11761
11909
|
purpose: `tail-${cleanName}`,
|
|
11762
11910
|
capabilities: ["read"],
|
|
11763
11911
|
topicScopes: [cleanName]
|
|
11764
|
-
}, async ({ secret, meshSlug }) => {
|
|
11912
|
+
}, async ({ secret, meshSlug, mesh }) => {
|
|
11913
|
+
const keyResult = await getTopicKey({
|
|
11914
|
+
apiKeySecret: secret,
|
|
11915
|
+
memberSecretKeyHex: mesh.secretKey,
|
|
11916
|
+
topicName: cleanName
|
|
11917
|
+
});
|
|
11918
|
+
const topicKey = keyResult.ok ? keyResult.topicKey ?? null : null;
|
|
11919
|
+
const snippetCache = new Map;
|
|
11920
|
+
let resealTimer = null;
|
|
11921
|
+
if (topicKey) {
|
|
11922
|
+
const reseal = async () => {
|
|
11923
|
+
try {
|
|
11924
|
+
const pending = await request({
|
|
11925
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/pending-seals`,
|
|
11926
|
+
token: secret
|
|
11927
|
+
});
|
|
11928
|
+
for (const target of pending.pending) {
|
|
11929
|
+
const sealed = await sealTopicKeyFor(topicKey, target.pubkey, mesh.secretKey);
|
|
11930
|
+
if (!sealed)
|
|
11931
|
+
continue;
|
|
11932
|
+
try {
|
|
11933
|
+
await request({
|
|
11934
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/seal`,
|
|
11935
|
+
method: "POST",
|
|
11936
|
+
token: secret,
|
|
11937
|
+
body: {
|
|
11938
|
+
memberId: target.memberId,
|
|
11939
|
+
encryptedKey: sealed.encryptedKey,
|
|
11940
|
+
nonce: sealed.nonce
|
|
11941
|
+
}
|
|
11942
|
+
});
|
|
11943
|
+
if (!flags.json) {
|
|
11944
|
+
render.info(dim(`re-sealed topic key for ${target.displayName}`));
|
|
11945
|
+
}
|
|
11946
|
+
} catch {}
|
|
11947
|
+
}
|
|
11948
|
+
} catch {}
|
|
11949
|
+
};
|
|
11950
|
+
reseal();
|
|
11951
|
+
resealTimer = setInterval(reseal, 30000);
|
|
11952
|
+
}
|
|
11953
|
+
if (!flags.json && !keyResult.ok) {
|
|
11954
|
+
if (keyResult.error === "topic_unencrypted") {
|
|
11955
|
+
render.info(dim("topic is on v1 (plaintext) — encryption will activate after creator-seal"));
|
|
11956
|
+
} else if (keyResult.error === "not_sealed") {
|
|
11957
|
+
render.warn(yellow("no topic key sealed for you yet — wait for a holder to re-seal"));
|
|
11958
|
+
} else if (keyResult.error === "decrypt_failed") {
|
|
11959
|
+
render.warn(yellow(`topic key fetched but decrypt failed: ${keyResult.message ?? ""}`));
|
|
11960
|
+
}
|
|
11961
|
+
}
|
|
11765
11962
|
if (!flags.forwardOnly && limit > 0) {
|
|
11766
11963
|
try {
|
|
11767
11964
|
const history = await request({
|
|
@@ -11772,7 +11969,7 @@ async function runTopicTail(name, flags) {
|
|
|
11772
11969
|
render.section(`${clay("#" + cleanName)} on ${dim(meshSlug)} — backfill ${history.messages.length}, then live`);
|
|
11773
11970
|
}
|
|
11774
11971
|
for (const m of history.messages.slice().reverse()) {
|
|
11775
|
-
printMessage(m, flags.json ?? false);
|
|
11972
|
+
await printMessage(m, topicKey, flags.json ?? false, snippetCache);
|
|
11776
11973
|
}
|
|
11777
11974
|
} catch (err) {
|
|
11778
11975
|
render.warn(`backfill failed: ${err.message}`);
|
|
@@ -11811,7 +12008,7 @@ async function runTopicTail(name, flags) {
|
|
|
11811
12008
|
if (ev.event === "message") {
|
|
11812
12009
|
try {
|
|
11813
12010
|
const m = JSON.parse(ev.data);
|
|
11814
|
-
printMessage(m, flags.json ?? false);
|
|
12011
|
+
await printMessage(m, topicKey, flags.json ?? false, snippetCache);
|
|
11815
12012
|
} catch {}
|
|
11816
12013
|
}
|
|
11817
12014
|
}
|
|
@@ -11824,13 +12021,120 @@ async function runTopicTail(name, flags) {
|
|
|
11824
12021
|
} finally {
|
|
11825
12022
|
process.removeListener("SIGINT", onSig);
|
|
11826
12023
|
process.removeListener("SIGTERM", onSig);
|
|
12024
|
+
if (resealTimer)
|
|
12025
|
+
clearInterval(resealTimer);
|
|
11827
12026
|
}
|
|
11828
12027
|
});
|
|
11829
12028
|
}
|
|
12029
|
+
var RECENT_CACHE_MAX = 256;
|
|
11830
12030
|
var init_topic_tail = __esm(() => {
|
|
11831
12031
|
init_urls();
|
|
11832
12032
|
init_with_rest_key();
|
|
11833
12033
|
init_client();
|
|
12034
|
+
init_topic_key();
|
|
12035
|
+
init_render();
|
|
12036
|
+
init_styles();
|
|
12037
|
+
init_exit_codes();
|
|
12038
|
+
});
|
|
12039
|
+
|
|
12040
|
+
// src/commands/topic-post.ts
|
|
12041
|
+
var exports_topic_post = {};
|
|
12042
|
+
__export(exports_topic_post, {
|
|
12043
|
+
runTopicPost: () => runTopicPost
|
|
12044
|
+
});
|
|
12045
|
+
async function runTopicPost(topicName, message, flags) {
|
|
12046
|
+
if (!topicName || !message) {
|
|
12047
|
+
render.err("Usage: claudemesh topic post <topic> <message>");
|
|
12048
|
+
return EXIT.INVALID_ARGS;
|
|
12049
|
+
}
|
|
12050
|
+
const cleanName = topicName.replace(/^#/, "");
|
|
12051
|
+
const mentions = [];
|
|
12052
|
+
const mentionRe = /(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]{1,64})(?=$|[^A-Za-z0-9_-])/g;
|
|
12053
|
+
let m;
|
|
12054
|
+
while ((m = mentionRe.exec(message)) !== null) {
|
|
12055
|
+
mentions.push(m[2].toLowerCase());
|
|
12056
|
+
if (mentions.length >= 16)
|
|
12057
|
+
break;
|
|
12058
|
+
}
|
|
12059
|
+
return withRestKey({
|
|
12060
|
+
meshSlug: flags.mesh ?? null,
|
|
12061
|
+
purpose: `post-${cleanName}`,
|
|
12062
|
+
capabilities: ["read", "send"],
|
|
12063
|
+
topicScopes: [cleanName]
|
|
12064
|
+
}, async ({ secret, mesh }) => {
|
|
12065
|
+
let bodyVersion = 1;
|
|
12066
|
+
let ciphertext;
|
|
12067
|
+
let nonce;
|
|
12068
|
+
if (flags.plaintext) {
|
|
12069
|
+
ciphertext = Buffer.from(message, "utf-8").toString("base64");
|
|
12070
|
+
nonce = Buffer.from(new Uint8Array(24)).toString("base64");
|
|
12071
|
+
} else {
|
|
12072
|
+
const keyResult = await getTopicKey({
|
|
12073
|
+
apiKeySecret: secret,
|
|
12074
|
+
memberSecretKeyHex: mesh.secretKey,
|
|
12075
|
+
topicName: cleanName
|
|
12076
|
+
});
|
|
12077
|
+
if (keyResult.ok && keyResult.topicKey) {
|
|
12078
|
+
const enc = await encryptMessage(keyResult.topicKey, message);
|
|
12079
|
+
ciphertext = enc.ciphertext;
|
|
12080
|
+
nonce = enc.nonce;
|
|
12081
|
+
bodyVersion = 2;
|
|
12082
|
+
} else if (keyResult.error === "topic_unencrypted") {
|
|
12083
|
+
ciphertext = Buffer.from(message, "utf-8").toString("base64");
|
|
12084
|
+
nonce = Buffer.from(new Uint8Array(24)).toString("base64");
|
|
12085
|
+
} else {
|
|
12086
|
+
render.err(`cannot encrypt for #${cleanName}: ${keyResult.error ?? "unknown"}${keyResult.message ? " — " + keyResult.message : ""}`);
|
|
12087
|
+
return EXIT.INTERNAL_ERROR;
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
12090
|
+
let replyToId;
|
|
12091
|
+
if (flags.replyTo) {
|
|
12092
|
+
if (flags.replyTo.length >= 16) {
|
|
12093
|
+
replyToId = flags.replyTo;
|
|
12094
|
+
} else if (flags.replyTo.length >= 6) {
|
|
12095
|
+
const recent = await request({
|
|
12096
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=200`,
|
|
12097
|
+
method: "GET",
|
|
12098
|
+
token: secret
|
|
12099
|
+
});
|
|
12100
|
+
const hit = recent.messages?.find((r) => r.id.startsWith(flags.replyTo));
|
|
12101
|
+
if (!hit) {
|
|
12102
|
+
render.err(`--reply-to ${flags.replyTo}: no recent message id starts with that prefix`);
|
|
12103
|
+
return EXIT.INVALID_ARGS;
|
|
12104
|
+
}
|
|
12105
|
+
replyToId = hit.id;
|
|
12106
|
+
} else {
|
|
12107
|
+
render.err("--reply-to needs at least 6 characters of the message id");
|
|
12108
|
+
return EXIT.INVALID_ARGS;
|
|
12109
|
+
}
|
|
12110
|
+
}
|
|
12111
|
+
const result = await request({
|
|
12112
|
+
path: "/api/v1/messages",
|
|
12113
|
+
method: "POST",
|
|
12114
|
+
token: secret,
|
|
12115
|
+
body: {
|
|
12116
|
+
topic: cleanName,
|
|
12117
|
+
ciphertext,
|
|
12118
|
+
nonce,
|
|
12119
|
+
bodyVersion,
|
|
12120
|
+
...mentions.length > 0 ? { mentions } : {},
|
|
12121
|
+
...replyToId ? { replyToId } : {}
|
|
12122
|
+
}
|
|
12123
|
+
});
|
|
12124
|
+
if (flags.json) {
|
|
12125
|
+
console.log(JSON.stringify({ ...result, bodyVersion, mentions }));
|
|
12126
|
+
return EXIT.SUCCESS;
|
|
12127
|
+
}
|
|
12128
|
+
const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
|
|
12129
|
+
const replyTag = result.replyToId ? ` ${dim("↳ " + result.replyToId.slice(0, 8))}` : "";
|
|
12130
|
+
render.ok("posted", `${clay("#" + cleanName)} ${versionTag}${replyTag} ${dim(`(${result.notifications} mentions)`)}`);
|
|
12131
|
+
return EXIT.SUCCESS;
|
|
12132
|
+
});
|
|
12133
|
+
}
|
|
12134
|
+
var init_topic_post = __esm(() => {
|
|
12135
|
+
init_with_rest_key();
|
|
12136
|
+
init_client();
|
|
12137
|
+
init_topic_key();
|
|
11834
12138
|
init_render();
|
|
11835
12139
|
init_styles();
|
|
11836
12140
|
init_exit_codes();
|
|
@@ -11841,7 +12145,7 @@ var exports_notification = {};
|
|
|
11841
12145
|
__export(exports_notification, {
|
|
11842
12146
|
runNotificationList: () => runNotificationList
|
|
11843
12147
|
});
|
|
11844
|
-
function
|
|
12148
|
+
function decodeCiphertext(b64) {
|
|
11845
12149
|
try {
|
|
11846
12150
|
return Buffer.from(b64, "base64").toString("utf-8");
|
|
11847
12151
|
} catch {
|
|
@@ -11868,7 +12172,7 @@ async function runNotificationList(flags) {
|
|
|
11868
12172
|
if (flags.json) {
|
|
11869
12173
|
const decoded = result.notifications.map((n) => ({
|
|
11870
12174
|
...n,
|
|
11871
|
-
message:
|
|
12175
|
+
message: decodeCiphertext(n.ciphertext)
|
|
11872
12176
|
}));
|
|
11873
12177
|
console.log(JSON.stringify({ ...result, notifications: decoded }, null, 2));
|
|
11874
12178
|
return EXIT.SUCCESS;
|
|
@@ -11880,7 +12184,7 @@ async function runNotificationList(flags) {
|
|
|
11880
12184
|
render.section(`mentions of @${bold(result.mentionedAs)} (${result.notifications.length})`);
|
|
11881
12185
|
for (const n of result.notifications) {
|
|
11882
12186
|
const when = fmtRelative(n.createdAt);
|
|
11883
|
-
const msg =
|
|
12187
|
+
const msg = decodeCiphertext(n.ciphertext).replace(/\s+/g, " ").trim();
|
|
11884
12188
|
const snippet = msg.length > 100 ? msg.slice(0, 97) + "…" : msg;
|
|
11885
12189
|
process.stdout.write(` ${clay("#" + n.topicName)} ${dim(when)} ${bold(n.senderName)}
|
|
11886
12190
|
`);
|
|
@@ -12191,7 +12495,16 @@ async function startMcpServer() {
|
|
|
12191
12495
|
You are "${myName}"${myRole ? ` (${myRole})` : ""} — a peer in the claudemesh network. Your groups: ${myGroups}. You are one of several Claude Code sessions connected to the same mesh. No orchestrator exists — peers are equals. Your identity comes from your name and group roles, not from a central authority.
|
|
12192
12496
|
|
|
12193
12497
|
## Responding to messages
|
|
12194
|
-
When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Pause your current task, reply via send_message
|
|
12498
|
+
When you receive a <channel source="claudemesh" ...> message, RESPOND IMMEDIATELY. Pause your current task, reply via send_message (or \`claudemesh topic post --reply-to <message_id>\` for topic threads), then resume. Stay in character per your system prompt. Do not ignore low-priority messages — acknowledge them briefly even if you defer action.
|
|
12499
|
+
|
|
12500
|
+
The channel attributes carry everything you need to reply — no extra lookups:
|
|
12501
|
+
- \`from_name\` — sender display name. Use as the \`to\` arg when replying to a DM.
|
|
12502
|
+
- \`from_pubkey\` / \`from_member_id\` — stable ids. Use \`from_member_id\` if the sender's display name might change.
|
|
12503
|
+
- \`mesh_slug\` — pass via \`--mesh\` if your default mesh differs.
|
|
12504
|
+
- \`priority\` — \`now\` / \`next\` / \`low\`.
|
|
12505
|
+
- \`message_id\` — id of THIS message. To thread a reply onto it in a topic, run \`claudemesh topic post <topic> "<text>" --reply-to <message_id>\`.
|
|
12506
|
+
- \`topic\` — set when the message arrived through a topic (vs DM). Reply in the same topic.
|
|
12507
|
+
- \`reply_to_id\` — set when the incoming message is itself a reply. Render thread context if you re-narrate.
|
|
12195
12508
|
|
|
12196
12509
|
If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder you set for yourself — act on it immediately (no reply needed).
|
|
12197
12510
|
|
|
@@ -12551,20 +12864,27 @@ ${manifest.allowed_tools.map((t) => ` - ${t}`).join(`
|
|
|
12551
12864
|
const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
|
|
12552
12865
|
const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
|
|
12553
12866
|
const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
|
|
12867
|
+
const fromMemberPubkey = msg.senderMemberPubkey ?? fromPubkey;
|
|
12554
12868
|
try {
|
|
12555
12869
|
await server.notification({
|
|
12556
12870
|
method: "notifications/claude/channel",
|
|
12557
12871
|
params: {
|
|
12558
12872
|
content,
|
|
12559
12873
|
meta: {
|
|
12560
|
-
from_id:
|
|
12874
|
+
from_id: fromMemberPubkey,
|
|
12875
|
+
from_pubkey: fromMemberPubkey,
|
|
12876
|
+
from_session_pubkey: fromPubkey,
|
|
12561
12877
|
from_name: fromName,
|
|
12878
|
+
...msg.senderMemberId ? { from_member_id: msg.senderMemberId } : {},
|
|
12562
12879
|
mesh_slug: client.meshSlug,
|
|
12563
12880
|
mesh_id: client.meshId,
|
|
12564
12881
|
priority: msg.priority,
|
|
12565
12882
|
sent_at: msg.createdAt,
|
|
12566
12883
|
delivered_at: msg.receivedAt,
|
|
12567
12884
|
kind: msg.kind,
|
|
12885
|
+
message_id: msg.messageId,
|
|
12886
|
+
...msg.topic ? { topic: msg.topic } : {},
|
|
12887
|
+
...msg.replyToId ? { reply_to_id: msg.replyToId } : {},
|
|
12568
12888
|
...msg.subtype ? { subtype: msg.subtype } : {}
|
|
12569
12889
|
}
|
|
12570
12890
|
}
|
|
@@ -13518,7 +13838,8 @@ Topic (conversation scope, v0.2.0)
|
|
|
13518
13838
|
claudemesh topic history <t> fetch message history [--limit --before]
|
|
13519
13839
|
claudemesh topic read <topic> mark all as read
|
|
13520
13840
|
claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
|
|
13521
|
-
claudemesh
|
|
13841
|
+
claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2) [--reply-to <id>]
|
|
13842
|
+
claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
|
|
13522
13843
|
claudemesh member list mesh roster with online state [--online]
|
|
13523
13844
|
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
|
13524
13845
|
|
|
@@ -14308,8 +14629,18 @@ async function main() {
|
|
|
14308
14629
|
};
|
|
14309
14630
|
const { runTopicTail: runTopicTail2 } = await Promise.resolve().then(() => (init_topic_tail(), exports_topic_tail));
|
|
14310
14631
|
process.exit(await runTopicTail2(arg, tailFlags));
|
|
14632
|
+
} else if (sub === "post") {
|
|
14633
|
+
const postFlags = {
|
|
14634
|
+
mesh: flags.mesh,
|
|
14635
|
+
json: !!flags.json,
|
|
14636
|
+
plaintext: !!flags.plaintext,
|
|
14637
|
+
replyTo: flags["reply-to"] || flags.replyTo
|
|
14638
|
+
};
|
|
14639
|
+
const message = positionals.slice(2).join(" ");
|
|
14640
|
+
const { runTopicPost: runTopicPost2 } = await Promise.resolve().then(() => (init_topic_post(), exports_topic_post));
|
|
14641
|
+
process.exit(await runTopicPost2(arg, message, postFlags));
|
|
14311
14642
|
} else {
|
|
14312
|
-
console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail>");
|
|
14643
|
+
console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail|post>");
|
|
14313
14644
|
process.exit(EXIT.INVALID_ARGS);
|
|
14314
14645
|
}
|
|
14315
14646
|
break;
|
|
@@ -14396,4 +14727,4 @@ main().catch((err) => {
|
|
|
14396
14727
|
process.exit(EXIT.INTERNAL_ERROR);
|
|
14397
14728
|
});
|
|
14398
14729
|
|
|
14399
|
-
//# debugId=
|
|
14730
|
+
//# debugId=25CA2A7E4B8DBD3564756E2164756E21
|