claudemesh-cli 1.6.1 → 1.8.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.
- package/README.md +9 -1
- package/dist/entrypoints/cli.js +612 -4
- package/dist/entrypoints/cli.js.map +10 -4
- package/dist/entrypoints/mcp.js +2 -2
- package/dist/entrypoints/mcp.js.map +1 -1
- package/package.json +1 -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.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).
|
|
6
|
+
>
|
|
7
|
+
> **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.
|
|
8
|
+
>
|
|
9
|
+
> **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.
|
|
6
10
|
>
|
|
7
11
|
> **Migration note (1.5.0):** the previous 79 MCP tools (`send_message`, `list_peers`, `remember`, …) are removed. Use the matching CLI verbs (`claudemesh send`, `claudemesh peers`, `claudemesh remember`). Run `claudemesh install` and the bundled skill teaches Claude the full surface.
|
|
8
12
|
|
|
@@ -43,6 +47,10 @@ USAGE
|
|
|
43
47
|
claudemesh profile view or edit your profile
|
|
44
48
|
|
|
45
49
|
claudemesh topic ... create, list, join, send to topics
|
|
50
|
+
claudemesh topic tail <t> live SSE tail of a topic (decrypts v2)
|
|
51
|
+
claudemesh topic post <t> encrypted REST post (v2 ciphertext)
|
|
52
|
+
claudemesh member list mesh roster with online state
|
|
53
|
+
claudemesh notification list recent @-mentions of you
|
|
46
54
|
claudemesh apikey ... issue, list, revoke API keys (REST clients)
|
|
47
55
|
claudemesh bridge ... forward a topic between two meshes
|
|
48
56
|
|
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.8.0", env;
|
|
92
92
|
var init_urls = __esm(() => {
|
|
93
93
|
URLS = {
|
|
94
94
|
BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
|
@@ -11648,6 +11648,558 @@ var init_topic = __esm(() => {
|
|
|
11648
11648
|
init_exit_codes();
|
|
11649
11649
|
});
|
|
11650
11650
|
|
|
11651
|
+
// src/services/api/with-rest-key.ts
|
|
11652
|
+
async function withRestKey(opts, fn) {
|
|
11653
|
+
return withMesh({ meshSlug: opts.meshSlug ?? null }, async (client, mesh) => {
|
|
11654
|
+
const result = await client.apiKeyCreate({
|
|
11655
|
+
label: `cli-${opts.purpose ?? "rest"}-${process.pid}`,
|
|
11656
|
+
capabilities: opts.capabilities ?? ["read"],
|
|
11657
|
+
topicScopes: opts.topicScopes ?? undefined,
|
|
11658
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
|
|
11659
|
+
});
|
|
11660
|
+
if (!result || !result.secret) {
|
|
11661
|
+
throw new Error("apikey mint failed — broker did not return a secret");
|
|
11662
|
+
}
|
|
11663
|
+
try {
|
|
11664
|
+
return await fn({
|
|
11665
|
+
secret: result.secret,
|
|
11666
|
+
meshId: mesh.meshId,
|
|
11667
|
+
meshSlug: mesh.slug,
|
|
11668
|
+
client,
|
|
11669
|
+
mesh
|
|
11670
|
+
});
|
|
11671
|
+
} finally {
|
|
11672
|
+
try {
|
|
11673
|
+
await client.apiKeyRevoke(result.id);
|
|
11674
|
+
} catch {}
|
|
11675
|
+
}
|
|
11676
|
+
});
|
|
11677
|
+
}
|
|
11678
|
+
var init_with_rest_key = __esm(() => {
|
|
11679
|
+
init_connect();
|
|
11680
|
+
});
|
|
11681
|
+
|
|
11682
|
+
// src/services/crypto/topic-key.ts
|
|
11683
|
+
function cacheKey(apiKeySecret, topicName) {
|
|
11684
|
+
return `${apiKeySecret.slice(0, 12)}:${topicName}`;
|
|
11685
|
+
}
|
|
11686
|
+
async function getTopicKey(args) {
|
|
11687
|
+
const cacheId = cacheKey(args.apiKeySecret, args.topicName);
|
|
11688
|
+
if (!args.fresh) {
|
|
11689
|
+
const cached = cache.get(cacheId);
|
|
11690
|
+
if (cached)
|
|
11691
|
+
return { ok: true, topicKey: cached.topicKey };
|
|
11692
|
+
}
|
|
11693
|
+
let sealed;
|
|
11694
|
+
try {
|
|
11695
|
+
sealed = await request({
|
|
11696
|
+
path: `/api/v1/topics/${encodeURIComponent(args.topicName)}/key`,
|
|
11697
|
+
token: args.apiKeySecret
|
|
11698
|
+
});
|
|
11699
|
+
} catch (e) {
|
|
11700
|
+
if (e instanceof ApiError) {
|
|
11701
|
+
if (e.status === 404)
|
|
11702
|
+
return { ok: false, error: "not_sealed" };
|
|
11703
|
+
if (e.status === 409)
|
|
11704
|
+
return { ok: false, error: "topic_unencrypted" };
|
|
11705
|
+
}
|
|
11706
|
+
return {
|
|
11707
|
+
ok: false,
|
|
11708
|
+
error: "network",
|
|
11709
|
+
message: e instanceof Error ? e.message : String(e)
|
|
11710
|
+
};
|
|
11711
|
+
}
|
|
11712
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11713
|
+
await sodium4.ready;
|
|
11714
|
+
let recipientX25519Secret;
|
|
11715
|
+
try {
|
|
11716
|
+
const ed = sodium4.from_hex(args.memberSecretKeyHex);
|
|
11717
|
+
recipientX25519Secret = sodium4.crypto_sign_ed25519_sk_to_curve25519(ed);
|
|
11718
|
+
} catch {
|
|
11719
|
+
return { ok: false, error: "bad_member_secret" };
|
|
11720
|
+
}
|
|
11721
|
+
let topicKey;
|
|
11722
|
+
try {
|
|
11723
|
+
const blob = sodium4.from_base64(sealed.encryptedKey, sodium4.base64_variants.ORIGINAL);
|
|
11724
|
+
const nonce = sodium4.from_base64(sealed.nonce, sodium4.base64_variants.ORIGINAL);
|
|
11725
|
+
if (blob.length < 32 + sodium4.crypto_box_MACBYTES) {
|
|
11726
|
+
return {
|
|
11727
|
+
ok: false,
|
|
11728
|
+
error: "decrypt_failed",
|
|
11729
|
+
message: "sealed key blob too short to contain sender pubkey + cipher"
|
|
11730
|
+
};
|
|
11731
|
+
}
|
|
11732
|
+
const senderX25519 = blob.slice(0, 32);
|
|
11733
|
+
const cipher = blob.slice(32);
|
|
11734
|
+
topicKey = sodium4.crypto_box_open_easy(cipher, nonce, senderX25519, recipientX25519Secret);
|
|
11735
|
+
} catch (e) {
|
|
11736
|
+
return {
|
|
11737
|
+
ok: false,
|
|
11738
|
+
error: "decrypt_failed",
|
|
11739
|
+
message: e instanceof Error ? e.message : String(e)
|
|
11740
|
+
};
|
|
11741
|
+
}
|
|
11742
|
+
cache.set(cacheId, { topicKey, fetchedAt: Date.now() });
|
|
11743
|
+
return { ok: true, topicKey };
|
|
11744
|
+
}
|
|
11745
|
+
async function encryptMessage(topicKey, plaintext) {
|
|
11746
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11747
|
+
await sodium4.ready;
|
|
11748
|
+
const nonceBytes = sodium4.randombytes_buf(sodium4.crypto_secretbox_NONCEBYTES);
|
|
11749
|
+
const cipher = sodium4.crypto_secretbox_easy(sodium4.from_string(plaintext), nonceBytes, topicKey);
|
|
11750
|
+
return {
|
|
11751
|
+
ciphertext: sodium4.to_base64(cipher, sodium4.base64_variants.ORIGINAL),
|
|
11752
|
+
nonce: sodium4.to_base64(nonceBytes, sodium4.base64_variants.ORIGINAL)
|
|
11753
|
+
};
|
|
11754
|
+
}
|
|
11755
|
+
async function decryptMessage(topicKey, ciphertextB64, nonceB64) {
|
|
11756
|
+
try {
|
|
11757
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11758
|
+
await sodium4.ready;
|
|
11759
|
+
const cipher = sodium4.from_base64(ciphertextB64, sodium4.base64_variants.ORIGINAL);
|
|
11760
|
+
const nonce = sodium4.from_base64(nonceB64, sodium4.base64_variants.ORIGINAL);
|
|
11761
|
+
const plain = sodium4.crypto_secretbox_open_easy(cipher, nonce, topicKey);
|
|
11762
|
+
return sodium4.to_string(plain);
|
|
11763
|
+
} catch {
|
|
11764
|
+
return null;
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
async function sealTopicKeyFor(topicKey, recipientPubkeyHex, ourMemberSecretKeyHex) {
|
|
11768
|
+
try {
|
|
11769
|
+
const sodium4 = (await import("libsodium-wrappers")).default;
|
|
11770
|
+
await sodium4.ready;
|
|
11771
|
+
const recipientX25519 = sodium4.crypto_sign_ed25519_pk_to_curve25519(sodium4.from_hex(recipientPubkeyHex));
|
|
11772
|
+
const ourEdSecret = sodium4.from_hex(ourMemberSecretKeyHex);
|
|
11773
|
+
const ourX25519Secret = sodium4.crypto_sign_ed25519_sk_to_curve25519(ourEdSecret);
|
|
11774
|
+
const ourEdPublic = ourEdSecret.slice(32, 64);
|
|
11775
|
+
const ourX25519Public = sodium4.crypto_sign_ed25519_pk_to_curve25519(ourEdPublic);
|
|
11776
|
+
const nonceBytes = sodium4.randombytes_buf(sodium4.crypto_box_NONCEBYTES);
|
|
11777
|
+
const cipher = sodium4.crypto_box_easy(topicKey, nonceBytes, recipientX25519, ourX25519Secret);
|
|
11778
|
+
const blob = new Uint8Array(32 + cipher.length);
|
|
11779
|
+
blob.set(ourX25519Public, 0);
|
|
11780
|
+
blob.set(cipher, 32);
|
|
11781
|
+
return {
|
|
11782
|
+
encryptedKey: sodium4.to_base64(blob, sodium4.base64_variants.ORIGINAL),
|
|
11783
|
+
nonce: sodium4.to_base64(nonceBytes, sodium4.base64_variants.ORIGINAL)
|
|
11784
|
+
};
|
|
11785
|
+
} catch {
|
|
11786
|
+
return null;
|
|
11787
|
+
}
|
|
11788
|
+
}
|
|
11789
|
+
var cache;
|
|
11790
|
+
var init_topic_key = __esm(() => {
|
|
11791
|
+
init_client();
|
|
11792
|
+
init_errors();
|
|
11793
|
+
cache = new Map;
|
|
11794
|
+
});
|
|
11795
|
+
|
|
11796
|
+
// src/commands/topic-tail.ts
|
|
11797
|
+
var exports_topic_tail = {};
|
|
11798
|
+
__export(exports_topic_tail, {
|
|
11799
|
+
runTopicTail: () => runTopicTail
|
|
11800
|
+
});
|
|
11801
|
+
function decodeV1(b64) {
|
|
11802
|
+
try {
|
|
11803
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
11804
|
+
} catch {
|
|
11805
|
+
return "[decode failed]";
|
|
11806
|
+
}
|
|
11807
|
+
}
|
|
11808
|
+
async function decryptForRender(m, topicKey) {
|
|
11809
|
+
if ((m.bodyVersion ?? 1) === 1)
|
|
11810
|
+
return decodeV1(m.ciphertext);
|
|
11811
|
+
if (!topicKey)
|
|
11812
|
+
return "[encrypted — no topic key]";
|
|
11813
|
+
const plain = await decryptMessage(topicKey, m.ciphertext, m.nonce);
|
|
11814
|
+
return plain ?? "[decrypt failed]";
|
|
11815
|
+
}
|
|
11816
|
+
function fmtTime(iso) {
|
|
11817
|
+
try {
|
|
11818
|
+
return new Date(iso).toLocaleTimeString([], {
|
|
11819
|
+
hour: "2-digit",
|
|
11820
|
+
minute: "2-digit",
|
|
11821
|
+
second: "2-digit"
|
|
11822
|
+
});
|
|
11823
|
+
} catch {
|
|
11824
|
+
return iso;
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
11827
|
+
async function printMessage(m, topicKey, json) {
|
|
11828
|
+
const text = await decryptForRender(m, topicKey);
|
|
11829
|
+
if (json) {
|
|
11830
|
+
console.log(JSON.stringify({ ...m, message: text }));
|
|
11831
|
+
return;
|
|
11832
|
+
}
|
|
11833
|
+
const v2Marker = (m.bodyVersion ?? 1) === 2 ? dim("\uD83D\uDD12 ") : "";
|
|
11834
|
+
process.stdout.write(` ${dim(fmtTime(m.createdAt))} ${bold(m.senderName || m.senderPubkey.slice(0, 8))} ${v2Marker}${text}
|
|
11835
|
+
`);
|
|
11836
|
+
}
|
|
11837
|
+
async function* readSseStream(reader) {
|
|
11838
|
+
const decoder = new TextDecoder;
|
|
11839
|
+
let buffer = "";
|
|
11840
|
+
while (true) {
|
|
11841
|
+
const { value, done } = await reader.read();
|
|
11842
|
+
if (done)
|
|
11843
|
+
break;
|
|
11844
|
+
buffer += decoder.decode(value, { stream: true });
|
|
11845
|
+
let idx;
|
|
11846
|
+
while ((idx = buffer.indexOf(`
|
|
11847
|
+
|
|
11848
|
+
`)) !== -1) {
|
|
11849
|
+
const block = buffer.slice(0, idx);
|
|
11850
|
+
buffer = buffer.slice(idx + 2);
|
|
11851
|
+
const ev = { event: "message", data: "" };
|
|
11852
|
+
const dataLines = [];
|
|
11853
|
+
for (const line of block.split(`
|
|
11854
|
+
`)) {
|
|
11855
|
+
if (!line || line.startsWith(":"))
|
|
11856
|
+
continue;
|
|
11857
|
+
const colon = line.indexOf(":");
|
|
11858
|
+
if (colon < 0)
|
|
11859
|
+
continue;
|
|
11860
|
+
const field = line.slice(0, colon);
|
|
11861
|
+
const val = line.slice(colon + 1).replace(/^ /, "");
|
|
11862
|
+
if (field === "event")
|
|
11863
|
+
ev.event = val;
|
|
11864
|
+
else if (field === "id")
|
|
11865
|
+
ev.id = val;
|
|
11866
|
+
else if (field === "data")
|
|
11867
|
+
dataLines.push(val);
|
|
11868
|
+
}
|
|
11869
|
+
ev.data = dataLines.join(`
|
|
11870
|
+
`);
|
|
11871
|
+
yield ev;
|
|
11872
|
+
}
|
|
11873
|
+
}
|
|
11874
|
+
}
|
|
11875
|
+
async function runTopicTail(name, flags) {
|
|
11876
|
+
if (!name) {
|
|
11877
|
+
render.err("Usage: claudemesh topic tail <topic> [--limit N]");
|
|
11878
|
+
return EXIT.INVALID_ARGS;
|
|
11879
|
+
}
|
|
11880
|
+
const cleanName = name.replace(/^#/, "");
|
|
11881
|
+
const limit = flags.limit ? Number(flags.limit) : 20;
|
|
11882
|
+
return withRestKey({
|
|
11883
|
+
meshSlug: flags.mesh ?? null,
|
|
11884
|
+
purpose: `tail-${cleanName}`,
|
|
11885
|
+
capabilities: ["read"],
|
|
11886
|
+
topicScopes: [cleanName]
|
|
11887
|
+
}, async ({ secret, meshSlug, mesh }) => {
|
|
11888
|
+
const keyResult = await getTopicKey({
|
|
11889
|
+
apiKeySecret: secret,
|
|
11890
|
+
memberSecretKeyHex: mesh.secretKey,
|
|
11891
|
+
topicName: cleanName
|
|
11892
|
+
});
|
|
11893
|
+
const topicKey = keyResult.ok ? keyResult.topicKey ?? null : null;
|
|
11894
|
+
let resealTimer = null;
|
|
11895
|
+
if (topicKey) {
|
|
11896
|
+
const reseal = async () => {
|
|
11897
|
+
try {
|
|
11898
|
+
const pending = await request({
|
|
11899
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/pending-seals`,
|
|
11900
|
+
token: secret
|
|
11901
|
+
});
|
|
11902
|
+
for (const target of pending.pending) {
|
|
11903
|
+
const sealed = await sealTopicKeyFor(topicKey, target.pubkey, mesh.secretKey);
|
|
11904
|
+
if (!sealed)
|
|
11905
|
+
continue;
|
|
11906
|
+
try {
|
|
11907
|
+
await request({
|
|
11908
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/seal`,
|
|
11909
|
+
method: "POST",
|
|
11910
|
+
token: secret,
|
|
11911
|
+
body: {
|
|
11912
|
+
memberId: target.memberId,
|
|
11913
|
+
encryptedKey: sealed.encryptedKey,
|
|
11914
|
+
nonce: sealed.nonce
|
|
11915
|
+
}
|
|
11916
|
+
});
|
|
11917
|
+
if (!flags.json) {
|
|
11918
|
+
render.info(dim(`re-sealed topic key for ${target.displayName}`));
|
|
11919
|
+
}
|
|
11920
|
+
} catch {}
|
|
11921
|
+
}
|
|
11922
|
+
} catch {}
|
|
11923
|
+
};
|
|
11924
|
+
reseal();
|
|
11925
|
+
resealTimer = setInterval(reseal, 30000);
|
|
11926
|
+
}
|
|
11927
|
+
if (!flags.json && !keyResult.ok) {
|
|
11928
|
+
if (keyResult.error === "topic_unencrypted") {
|
|
11929
|
+
render.info(dim("topic is on v1 (plaintext) — encryption will activate after creator-seal"));
|
|
11930
|
+
} else if (keyResult.error === "not_sealed") {
|
|
11931
|
+
render.warn(yellow("no topic key sealed for you yet — wait for a holder to re-seal"));
|
|
11932
|
+
} else if (keyResult.error === "decrypt_failed") {
|
|
11933
|
+
render.warn(yellow(`topic key fetched but decrypt failed: ${keyResult.message ?? ""}`));
|
|
11934
|
+
}
|
|
11935
|
+
}
|
|
11936
|
+
if (!flags.forwardOnly && limit > 0) {
|
|
11937
|
+
try {
|
|
11938
|
+
const history = await request({
|
|
11939
|
+
path: `/api/v1/topics/${encodeURIComponent(cleanName)}/messages?limit=${limit}`,
|
|
11940
|
+
token: secret
|
|
11941
|
+
});
|
|
11942
|
+
if (!flags.json) {
|
|
11943
|
+
render.section(`${clay("#" + cleanName)} on ${dim(meshSlug)} — backfill ${history.messages.length}, then live`);
|
|
11944
|
+
}
|
|
11945
|
+
for (const m of history.messages.slice().reverse()) {
|
|
11946
|
+
await printMessage(m, topicKey, flags.json ?? false);
|
|
11947
|
+
}
|
|
11948
|
+
} catch (err) {
|
|
11949
|
+
render.warn(`backfill failed: ${err.message}`);
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
11952
|
+
const url = `${URLS.API_BASE}/api/v1/topics/${encodeURIComponent(cleanName)}/stream`;
|
|
11953
|
+
const ctl = new AbortController;
|
|
11954
|
+
const onSig = () => ctl.abort();
|
|
11955
|
+
process.once("SIGINT", onSig);
|
|
11956
|
+
process.once("SIGTERM", onSig);
|
|
11957
|
+
try {
|
|
11958
|
+
const res = await fetch(url, {
|
|
11959
|
+
headers: { Authorization: `Bearer ${secret}` },
|
|
11960
|
+
signal: ctl.signal
|
|
11961
|
+
});
|
|
11962
|
+
if (!res.ok || !res.body) {
|
|
11963
|
+
render.err(`stream open failed: ${res.status}`);
|
|
11964
|
+
return EXIT.INTERNAL_ERROR;
|
|
11965
|
+
}
|
|
11966
|
+
if (!flags.json) {
|
|
11967
|
+
render.info(dim("tailing — Ctrl-C to exit"));
|
|
11968
|
+
}
|
|
11969
|
+
const reader = res.body.getReader();
|
|
11970
|
+
for await (const ev of readSseStream(reader)) {
|
|
11971
|
+
if (ev.event === "ready" || ev.event === "heartbeat")
|
|
11972
|
+
continue;
|
|
11973
|
+
if (ev.event === "error") {
|
|
11974
|
+
try {
|
|
11975
|
+
const parsed = JSON.parse(ev.data);
|
|
11976
|
+
render.err(`stream error: ${parsed.error ?? "unknown"}`);
|
|
11977
|
+
} catch {
|
|
11978
|
+
render.err("stream error");
|
|
11979
|
+
}
|
|
11980
|
+
continue;
|
|
11981
|
+
}
|
|
11982
|
+
if (ev.event === "message") {
|
|
11983
|
+
try {
|
|
11984
|
+
const m = JSON.parse(ev.data);
|
|
11985
|
+
await printMessage(m, topicKey, flags.json ?? false);
|
|
11986
|
+
} catch {}
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
return EXIT.SUCCESS;
|
|
11990
|
+
} catch (err) {
|
|
11991
|
+
if (ctl.signal.aborted)
|
|
11992
|
+
return EXIT.SUCCESS;
|
|
11993
|
+
render.err(`tail aborted: ${err.message}`);
|
|
11994
|
+
return EXIT.INTERNAL_ERROR;
|
|
11995
|
+
} finally {
|
|
11996
|
+
process.removeListener("SIGINT", onSig);
|
|
11997
|
+
process.removeListener("SIGTERM", onSig);
|
|
11998
|
+
if (resealTimer)
|
|
11999
|
+
clearInterval(resealTimer);
|
|
12000
|
+
}
|
|
12001
|
+
});
|
|
12002
|
+
}
|
|
12003
|
+
var init_topic_tail = __esm(() => {
|
|
12004
|
+
init_urls();
|
|
12005
|
+
init_with_rest_key();
|
|
12006
|
+
init_client();
|
|
12007
|
+
init_topic_key();
|
|
12008
|
+
init_render();
|
|
12009
|
+
init_styles();
|
|
12010
|
+
init_exit_codes();
|
|
12011
|
+
});
|
|
12012
|
+
|
|
12013
|
+
// src/commands/topic-post.ts
|
|
12014
|
+
var exports_topic_post = {};
|
|
12015
|
+
__export(exports_topic_post, {
|
|
12016
|
+
runTopicPost: () => runTopicPost
|
|
12017
|
+
});
|
|
12018
|
+
async function runTopicPost(topicName, message, flags) {
|
|
12019
|
+
if (!topicName || !message) {
|
|
12020
|
+
render.err("Usage: claudemesh topic post <topic> <message>");
|
|
12021
|
+
return EXIT.INVALID_ARGS;
|
|
12022
|
+
}
|
|
12023
|
+
const cleanName = topicName.replace(/^#/, "");
|
|
12024
|
+
const mentions = [];
|
|
12025
|
+
const mentionRe = /(^|[^A-Za-z0-9_-])@([A-Za-z0-9_-]{1,64})(?=$|[^A-Za-z0-9_-])/g;
|
|
12026
|
+
let m;
|
|
12027
|
+
while ((m = mentionRe.exec(message)) !== null) {
|
|
12028
|
+
mentions.push(m[2].toLowerCase());
|
|
12029
|
+
if (mentions.length >= 16)
|
|
12030
|
+
break;
|
|
12031
|
+
}
|
|
12032
|
+
return withRestKey({
|
|
12033
|
+
meshSlug: flags.mesh ?? null,
|
|
12034
|
+
purpose: `post-${cleanName}`,
|
|
12035
|
+
capabilities: ["read", "send"],
|
|
12036
|
+
topicScopes: [cleanName]
|
|
12037
|
+
}, async ({ secret, mesh }) => {
|
|
12038
|
+
let bodyVersion = 1;
|
|
12039
|
+
let ciphertext;
|
|
12040
|
+
let nonce;
|
|
12041
|
+
if (flags.plaintext) {
|
|
12042
|
+
ciphertext = Buffer.from(message, "utf-8").toString("base64");
|
|
12043
|
+
nonce = Buffer.from(new Uint8Array(24)).toString("base64");
|
|
12044
|
+
} else {
|
|
12045
|
+
const keyResult = await getTopicKey({
|
|
12046
|
+
apiKeySecret: secret,
|
|
12047
|
+
memberSecretKeyHex: mesh.secretKey,
|
|
12048
|
+
topicName: cleanName
|
|
12049
|
+
});
|
|
12050
|
+
if (keyResult.ok && keyResult.topicKey) {
|
|
12051
|
+
const enc = await encryptMessage(keyResult.topicKey, message);
|
|
12052
|
+
ciphertext = enc.ciphertext;
|
|
12053
|
+
nonce = enc.nonce;
|
|
12054
|
+
bodyVersion = 2;
|
|
12055
|
+
} else if (keyResult.error === "topic_unencrypted") {
|
|
12056
|
+
ciphertext = Buffer.from(message, "utf-8").toString("base64");
|
|
12057
|
+
nonce = Buffer.from(new Uint8Array(24)).toString("base64");
|
|
12058
|
+
} else {
|
|
12059
|
+
render.err(`cannot encrypt for #${cleanName}: ${keyResult.error ?? "unknown"}${keyResult.message ? " — " + keyResult.message : ""}`);
|
|
12060
|
+
return EXIT.INTERNAL_ERROR;
|
|
12061
|
+
}
|
|
12062
|
+
}
|
|
12063
|
+
const result = await request({
|
|
12064
|
+
path: "/api/v1/messages",
|
|
12065
|
+
method: "POST",
|
|
12066
|
+
token: secret,
|
|
12067
|
+
body: {
|
|
12068
|
+
topic: cleanName,
|
|
12069
|
+
ciphertext,
|
|
12070
|
+
nonce,
|
|
12071
|
+
bodyVersion,
|
|
12072
|
+
...mentions.length > 0 ? { mentions } : {}
|
|
12073
|
+
}
|
|
12074
|
+
});
|
|
12075
|
+
if (flags.json) {
|
|
12076
|
+
console.log(JSON.stringify({ ...result, bodyVersion, mentions }));
|
|
12077
|
+
return EXIT.SUCCESS;
|
|
12078
|
+
}
|
|
12079
|
+
const versionTag = bodyVersion === 2 ? green("\uD83D\uDD12 v2") : dim("v1");
|
|
12080
|
+
render.ok("posted", `${clay("#" + cleanName)} ${versionTag} ${dim(`(${result.notifications} mentions)`)}`);
|
|
12081
|
+
return EXIT.SUCCESS;
|
|
12082
|
+
});
|
|
12083
|
+
}
|
|
12084
|
+
var init_topic_post = __esm(() => {
|
|
12085
|
+
init_with_rest_key();
|
|
12086
|
+
init_client();
|
|
12087
|
+
init_topic_key();
|
|
12088
|
+
init_render();
|
|
12089
|
+
init_styles();
|
|
12090
|
+
init_exit_codes();
|
|
12091
|
+
});
|
|
12092
|
+
|
|
12093
|
+
// src/commands/notification.ts
|
|
12094
|
+
var exports_notification = {};
|
|
12095
|
+
__export(exports_notification, {
|
|
12096
|
+
runNotificationList: () => runNotificationList
|
|
12097
|
+
});
|
|
12098
|
+
function decodeCiphertext(b64) {
|
|
12099
|
+
try {
|
|
12100
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
12101
|
+
} catch {
|
|
12102
|
+
return "[decode failed]";
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
function fmtRelative(iso) {
|
|
12106
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
12107
|
+
if (ms < 60000)
|
|
12108
|
+
return "now";
|
|
12109
|
+
if (ms < 3600000)
|
|
12110
|
+
return `${Math.floor(ms / 60000)}m`;
|
|
12111
|
+
if (ms < 86400000)
|
|
12112
|
+
return `${Math.floor(ms / 3600000)}h`;
|
|
12113
|
+
return `${Math.floor(ms / 86400000)}d`;
|
|
12114
|
+
}
|
|
12115
|
+
async function runNotificationList(flags) {
|
|
12116
|
+
return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "notifications" }, async ({ secret }) => {
|
|
12117
|
+
const qs = flags.since ? `?since=${encodeURIComponent(flags.since)}` : "";
|
|
12118
|
+
const result = await request({
|
|
12119
|
+
path: `/api/v1/notifications${qs}`,
|
|
12120
|
+
token: secret
|
|
12121
|
+
});
|
|
12122
|
+
if (flags.json) {
|
|
12123
|
+
const decoded = result.notifications.map((n) => ({
|
|
12124
|
+
...n,
|
|
12125
|
+
message: decodeCiphertext(n.ciphertext)
|
|
12126
|
+
}));
|
|
12127
|
+
console.log(JSON.stringify({ ...result, notifications: decoded }, null, 2));
|
|
12128
|
+
return EXIT.SUCCESS;
|
|
12129
|
+
}
|
|
12130
|
+
if (result.notifications.length === 0) {
|
|
12131
|
+
render.info(dim(`no mentions of @${result.mentionedAs} since ${result.since}.`));
|
|
12132
|
+
return EXIT.SUCCESS;
|
|
12133
|
+
}
|
|
12134
|
+
render.section(`mentions of @${bold(result.mentionedAs)} (${result.notifications.length})`);
|
|
12135
|
+
for (const n of result.notifications) {
|
|
12136
|
+
const when = fmtRelative(n.createdAt);
|
|
12137
|
+
const msg = decodeCiphertext(n.ciphertext).replace(/\s+/g, " ").trim();
|
|
12138
|
+
const snippet = msg.length > 100 ? msg.slice(0, 97) + "…" : msg;
|
|
12139
|
+
process.stdout.write(` ${clay("#" + n.topicName)} ${dim(when)} ${bold(n.senderName)}
|
|
12140
|
+
`);
|
|
12141
|
+
process.stdout.write(` ${snippet}
|
|
12142
|
+
`);
|
|
12143
|
+
}
|
|
12144
|
+
return EXIT.SUCCESS;
|
|
12145
|
+
});
|
|
12146
|
+
}
|
|
12147
|
+
var init_notification = __esm(() => {
|
|
12148
|
+
init_with_rest_key();
|
|
12149
|
+
init_client();
|
|
12150
|
+
init_render();
|
|
12151
|
+
init_styles();
|
|
12152
|
+
init_exit_codes();
|
|
12153
|
+
});
|
|
12154
|
+
|
|
12155
|
+
// src/commands/member.ts
|
|
12156
|
+
var exports_member = {};
|
|
12157
|
+
__export(exports_member, {
|
|
12158
|
+
runMemberList: () => runMemberList
|
|
12159
|
+
});
|
|
12160
|
+
function statusGlyph(m) {
|
|
12161
|
+
if (!m.online)
|
|
12162
|
+
return dim("○");
|
|
12163
|
+
if (m.status === "dnd")
|
|
12164
|
+
return red("●");
|
|
12165
|
+
if (m.status === "working")
|
|
12166
|
+
return yellow("●");
|
|
12167
|
+
return green("●");
|
|
12168
|
+
}
|
|
12169
|
+
async function runMemberList(flags) {
|
|
12170
|
+
return withRestKey({ meshSlug: flags.mesh ?? null, purpose: "members" }, async ({ secret, meshSlug }) => {
|
|
12171
|
+
const result = await request({
|
|
12172
|
+
path: "/api/v1/members",
|
|
12173
|
+
token: secret
|
|
12174
|
+
});
|
|
12175
|
+
const filtered = flags.online ? result.members.filter((m) => m.online) : result.members;
|
|
12176
|
+
if (flags.json) {
|
|
12177
|
+
console.log(JSON.stringify({ members: filtered }, null, 2));
|
|
12178
|
+
return EXIT.SUCCESS;
|
|
12179
|
+
}
|
|
12180
|
+
if (filtered.length === 0) {
|
|
12181
|
+
render.info(dim(flags.online ? `no online members in ${meshSlug}.` : `no members in ${meshSlug}.`));
|
|
12182
|
+
return EXIT.SUCCESS;
|
|
12183
|
+
}
|
|
12184
|
+
const onlineCount = result.members.filter((m) => m.online).length;
|
|
12185
|
+
render.section(`${clay(meshSlug)} members (${onlineCount}/${result.members.length} online)`);
|
|
12186
|
+
for (const m of filtered) {
|
|
12187
|
+
const tag = m.isHuman ? dim("human") : dim("bot");
|
|
12188
|
+
const summary = m.summary ? ` — ${dim(m.summary)}` : "";
|
|
12189
|
+
process.stdout.write(` ${statusGlyph(m)} ${bold(m.displayName)} ${tag} ${dim(m.role)} ${dim(m.pubkey.slice(0, 8))}${summary}
|
|
12190
|
+
`);
|
|
12191
|
+
}
|
|
12192
|
+
return EXIT.SUCCESS;
|
|
12193
|
+
});
|
|
12194
|
+
}
|
|
12195
|
+
var init_member = __esm(() => {
|
|
12196
|
+
init_with_rest_key();
|
|
12197
|
+
init_client();
|
|
12198
|
+
init_render();
|
|
12199
|
+
init_styles();
|
|
12200
|
+
init_exit_codes();
|
|
12201
|
+
});
|
|
12202
|
+
|
|
11651
12203
|
// src/mcp/tools/definitions.ts
|
|
11652
12204
|
var TOOLS;
|
|
11653
12205
|
var init_definitions = __esm(() => {
|
|
@@ -13219,7 +13771,11 @@ Topic (conversation scope, v0.2.0)
|
|
|
13219
13771
|
claudemesh topic members <t> list topic subscribers
|
|
13220
13772
|
claudemesh topic history <t> fetch message history [--limit --before]
|
|
13221
13773
|
claudemesh topic read <topic> mark all as read
|
|
13222
|
-
claudemesh
|
|
13774
|
+
claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
|
|
13775
|
+
claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2)
|
|
13776
|
+
claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
|
|
13777
|
+
claudemesh member list mesh roster with online state [--online]
|
|
13778
|
+
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
|
13223
13779
|
|
|
13224
13780
|
Schedule (resource form)
|
|
13225
13781
|
claudemesh schedule msg <m> one-shot or recurring (alias: remind)
|
|
@@ -13998,8 +14554,60 @@ async function main() {
|
|
|
13998
14554
|
} else if (sub === "read") {
|
|
13999
14555
|
const { runTopicMarkRead: runTopicMarkRead2 } = await Promise.resolve().then(() => (init_topic(), exports_topic));
|
|
14000
14556
|
process.exit(await runTopicMarkRead2(arg, f));
|
|
14557
|
+
} else if (sub === "tail") {
|
|
14558
|
+
const tailFlags = {
|
|
14559
|
+
mesh: flags.mesh,
|
|
14560
|
+
json: !!flags.json,
|
|
14561
|
+
limit: flags.limit,
|
|
14562
|
+
forwardOnly: !!flags["forward-only"]
|
|
14563
|
+
};
|
|
14564
|
+
const { runTopicTail: runTopicTail2 } = await Promise.resolve().then(() => (init_topic_tail(), exports_topic_tail));
|
|
14565
|
+
process.exit(await runTopicTail2(arg, tailFlags));
|
|
14566
|
+
} else if (sub === "post") {
|
|
14567
|
+
const postFlags = {
|
|
14568
|
+
mesh: flags.mesh,
|
|
14569
|
+
json: !!flags.json,
|
|
14570
|
+
plaintext: !!flags.plaintext
|
|
14571
|
+
};
|
|
14572
|
+
const message = positionals.slice(2).join(" ");
|
|
14573
|
+
const { runTopicPost: runTopicPost2 } = await Promise.resolve().then(() => (init_topic_post(), exports_topic_post));
|
|
14574
|
+
process.exit(await runTopicPost2(arg, message, postFlags));
|
|
14575
|
+
} else {
|
|
14576
|
+
console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail|post>");
|
|
14577
|
+
process.exit(EXIT.INVALID_ARGS);
|
|
14578
|
+
}
|
|
14579
|
+
break;
|
|
14580
|
+
}
|
|
14581
|
+
case "notification":
|
|
14582
|
+
case "notifications": {
|
|
14583
|
+
const sub = positionals[0] ?? "list";
|
|
14584
|
+
const f = {
|
|
14585
|
+
mesh: flags.mesh,
|
|
14586
|
+
json: !!flags.json,
|
|
14587
|
+
since: flags.since
|
|
14588
|
+
};
|
|
14589
|
+
if (sub === "list") {
|
|
14590
|
+
const { runNotificationList: runNotificationList2 } = await Promise.resolve().then(() => (init_notification(), exports_notification));
|
|
14591
|
+
process.exit(await runNotificationList2(f));
|
|
14592
|
+
} else {
|
|
14593
|
+
console.error("Usage: claudemesh notification list [--since <ISO>]");
|
|
14594
|
+
process.exit(EXIT.INVALID_ARGS);
|
|
14595
|
+
}
|
|
14596
|
+
break;
|
|
14597
|
+
}
|
|
14598
|
+
case "member":
|
|
14599
|
+
case "members": {
|
|
14600
|
+
const sub = positionals[0] ?? "list";
|
|
14601
|
+
const f = {
|
|
14602
|
+
mesh: flags.mesh,
|
|
14603
|
+
json: !!flags.json,
|
|
14604
|
+
online: !!flags.online
|
|
14605
|
+
};
|
|
14606
|
+
if (sub === "list") {
|
|
14607
|
+
const { runMemberList: runMemberList2 } = await Promise.resolve().then(() => (init_member(), exports_member));
|
|
14608
|
+
process.exit(await runMemberList2(f));
|
|
14001
14609
|
} else {
|
|
14002
|
-
console.error("Usage: claudemesh
|
|
14610
|
+
console.error("Usage: claudemesh member list [--online]");
|
|
14003
14611
|
process.exit(EXIT.INVALID_ARGS);
|
|
14004
14612
|
}
|
|
14005
14613
|
break;
|
|
@@ -14052,4 +14660,4 @@ main().catch((err) => {
|
|
|
14052
14660
|
process.exit(EXIT.INTERNAL_ERROR);
|
|
14053
14661
|
});
|
|
14054
14662
|
|
|
14055
|
-
//# debugId=
|
|
14663
|
+
//# debugId=25484051F3F24E9464756E2164756E21
|