claudemesh-cli 1.0.0-alpha.9 → 1.0.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.
@@ -1299,6 +1299,7 @@ class BrokerClient {
1299
1299
  outbound = [];
1300
1300
  pushHandlers = new Set;
1301
1301
  pushBuffer = [];
1302
+ pushChain = Promise.resolve();
1302
1303
  listPeersResolvers = new Map;
1303
1304
  stateResolvers = new Map;
1304
1305
  stateListResolvers = new Map;
@@ -1423,8 +1424,10 @@ class BrokerClient {
1423
1424
  this.startStatsReporting();
1424
1425
  if (msg.restored) {
1425
1426
  const groups = msg.restoredGroups ? msg.restoredGroups.map((g) => g.role ? `@${g.name}:${g.role}` : `@${g.name}`).join(", ") : "none";
1426
- process.stderr.write(`[claudemesh] session restored — last seen ${msg.lastSeenAt ?? "unknown"}, groups: ${groups}
1427
+ if (!this.opts.quiet) {
1428
+ process.stderr.write(`[claudemesh] session restored — last seen ${msg.lastSeenAt ?? "unknown"}, groups: ${groups}
1427
1429
  `);
1430
+ }
1428
1431
  if (msg.restoredStats) {
1429
1432
  const rs = msg.restoredStats;
1430
1433
  this._statsCounters.messagesIn = rs.messagesIn ?? 0;
@@ -2443,6 +2446,22 @@ class BrokerClient {
2443
2446
  return;
2444
2447
  this.ws.send(JSON.stringify(payload));
2445
2448
  }
2449
+ async sendAndWait(payload, timeoutMs = 1e4) {
2450
+ const reqId = `rw-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2451
+ return new Promise((resolve, reject) => {
2452
+ const timer = setTimeout(() => {
2453
+ this.genericResolvers.delete(reqId);
2454
+ reject(new Error("sendAndWait timeout"));
2455
+ }, timeoutMs);
2456
+ this.genericResolvers.set(reqId, (msg) => {
2457
+ clearTimeout(timer);
2458
+ this.genericResolvers.delete(reqId);
2459
+ resolve(msg);
2460
+ });
2461
+ this.sendRaw({ ...payload, _reqId: reqId });
2462
+ });
2463
+ }
2464
+ genericResolvers = new Map;
2446
2465
  close() {
2447
2466
  this.closed = true;
2448
2467
  this.stopStatsReporting();
@@ -2643,13 +2662,17 @@ class BrokerClient {
2643
2662
  }
2644
2663
  handleServerMessage(msg) {
2645
2664
  const msgReqId = msg._reqId;
2665
+ if (msgReqId && this.genericResolvers.has(msgReqId)) {
2666
+ const resolve = this.genericResolvers.get(msgReqId);
2667
+ resolve(msg);
2668
+ return;
2669
+ }
2646
2670
  if (msg.type === "ack") {
2647
2671
  const pending = this.pendingSends.get(String(msg.id ?? ""));
2648
2672
  if (pending) {
2649
- pending.resolve({
2650
- ok: true,
2651
- messageId: String(msg.messageId ?? "")
2652
- });
2673
+ const queued = msg.queued !== false;
2674
+ const errStr = typeof msg.error === "string" ? msg.error : undefined;
2675
+ pending.resolve(queued ? { ok: true, messageId: String(msg.messageId ?? "") } : { ok: false, error: errStr ?? "broker rejected send" });
2653
2676
  this.pendingSends.delete(pending.id);
2654
2677
  }
2655
2678
  return;
@@ -2664,7 +2687,7 @@ class BrokerClient {
2664
2687
  const nonce = String(msg.nonce ?? "");
2665
2688
  const ciphertext = String(msg.ciphertext ?? "");
2666
2689
  const senderPubkey = String(msg.senderPubkey ?? "");
2667
- (async () => {
2690
+ this.pushChain = this.pushChain.then(async () => {
2668
2691
  const isSystem = msg.subtype === "system" || senderPubkey === "system";
2669
2692
  const kind = isSystem ? "broadcast" : senderPubkey ? "direct" : "unknown";
2670
2693
  let plaintext = null;
@@ -2689,7 +2712,13 @@ class BrokerClient {
2689
2712
  plaintext = `[${event}]`;
2690
2713
  }
2691
2714
  } else if (senderPubkey && nonce && ciphertext) {
2692
- plaintext = await decryptDirect({ nonce, ciphertext }, senderPubkey, this.sessionSecretKey ?? this.mesh.secretKey);
2715
+ const envelope = { nonce, ciphertext };
2716
+ if (this.sessionSecretKey) {
2717
+ plaintext = await decryptDirect(envelope, senderPubkey, this.sessionSecretKey);
2718
+ }
2719
+ if (plaintext === null) {
2720
+ plaintext = await decryptDirect(envelope, senderPubkey, this.mesh.secretKey);
2721
+ }
2693
2722
  }
2694
2723
  if (plaintext === null && ciphertext && !senderPubkey) {
2695
2724
  try {
@@ -2735,7 +2764,9 @@ class BrokerClient {
2735
2764
  h(push);
2736
2765
  } catch {}
2737
2766
  }
2738
- })();
2767
+ }).catch((e) => {
2768
+ this.debug(`push handler chain error: ${e instanceof Error ? e.message : e}`);
2769
+ });
2739
2770
  return;
2740
2771
  }
2741
2772
  if (msg.type === "state_result") {
@@ -3181,11 +3212,17 @@ class BrokerClient {
3181
3212
  send();
3182
3213
  }
3183
3214
  scheduleReconnect() {
3215
+ if (this.reconnectTimer) {
3216
+ this.debug("reconnect already scheduled — skipping");
3217
+ return;
3218
+ }
3184
3219
  this.setConnStatus("reconnecting");
3185
- const delay = BACKOFF_CAPS[Math.min(this.reconnectAttempt, BACKOFF_CAPS.length - 1)];
3220
+ const base = BACKOFF_CAPS[Math.min(this.reconnectAttempt, BACKOFF_CAPS.length - 1)];
3221
+ const delay = Math.floor(Math.random() * base);
3186
3222
  this.reconnectAttempt += 1;
3187
- this.debug(`reconnect in ${delay}ms (attempt ${this.reconnectAttempt})`);
3223
+ this.debug(`reconnect in ${delay}ms (attempt ${this.reconnectAttempt}, base ${base}ms)`);
3188
3224
  this.reconnectTimer = setTimeout(() => {
3225
+ this.reconnectTimer = null;
3189
3226
  if (this.closed)
3190
3227
  return;
3191
3228
  this.connect().catch((e) => {
@@ -3286,6 +3323,1072 @@ var init_facade3 = __esm(() => {
3286
3323
  init_errors();
3287
3324
  });
3288
3325
 
3326
+ // src/commands/connect.ts
3327
+ import { hostname } from "node:os";
3328
+ import { createInterface } from "node:readline";
3329
+ async function pickMesh(meshes) {
3330
+ console.log(`
3331
+ Select mesh:`);
3332
+ meshes.forEach((m, i) => {
3333
+ console.log(` ${i + 1}) ${m.slug}`);
3334
+ });
3335
+ console.log("");
3336
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3337
+ return new Promise((resolve) => {
3338
+ rl.question(" Choice [1]: ", (answer) => {
3339
+ rl.close();
3340
+ const idx = parseInt(answer || "1", 10) - 1;
3341
+ if (idx >= 0 && idx < meshes.length) {
3342
+ resolve(meshes[idx]);
3343
+ } else {
3344
+ console.error(" Invalid choice, using first mesh.");
3345
+ resolve(meshes[0]);
3346
+ }
3347
+ });
3348
+ });
3349
+ }
3350
+ async function withMesh(opts, fn) {
3351
+ const config = readConfig();
3352
+ if (config.meshes.length === 0) {
3353
+ console.error("No meshes joined. Run `claudemesh join <url>` first.");
3354
+ process.exit(1);
3355
+ }
3356
+ let mesh;
3357
+ if (opts.meshSlug) {
3358
+ const found = config.meshes.find((m) => m.slug === opts.meshSlug);
3359
+ if (!found) {
3360
+ console.error(`Mesh "${opts.meshSlug}" not found. Joined: ${config.meshes.map((m) => m.slug).join(", ")}`);
3361
+ process.exit(1);
3362
+ }
3363
+ mesh = found;
3364
+ } else if (config.meshes.length === 1) {
3365
+ mesh = config.meshes[0];
3366
+ } else {
3367
+ mesh = await pickMesh(config.meshes);
3368
+ }
3369
+ const displayName = opts.displayName ?? config.displayName ?? `${hostname()}-${process.pid}`;
3370
+ const client = new BrokerClient(mesh, { displayName, quiet: true });
3371
+ try {
3372
+ await client.connect();
3373
+ const result = await fn(client, mesh);
3374
+ return result;
3375
+ } finally {
3376
+ client.close();
3377
+ }
3378
+ }
3379
+ var init_connect = __esm(() => {
3380
+ init_facade3();
3381
+ init_facade();
3382
+ });
3383
+
3384
+ // src/ui/styles.ts
3385
+ function moveTo(row, col) {
3386
+ return isTTY ? `\x1B[${row};${col}H` : "";
3387
+ }
3388
+ function visibleLength(s) {
3389
+ return s.replace(/\x1b\[[^m]*m/g, "").length;
3390
+ }
3391
+ var isTTY, esc = (code) => (s) => isTTY ? `${code}${s}\x1B[0m` : s, orange, clay, amber, bold, dim, green, yellow, red, cyan, boldOrange, HIDE_CURSOR, SHOW_CURSOR, CLEAR_SCREEN, CLEAR_LINE, icons;
3392
+ var init_styles = __esm(() => {
3393
+ isTTY = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
3394
+ orange = esc("\x1B[38;5;208m");
3395
+ clay = esc("\x1B[38;5;173m");
3396
+ amber = esc("\x1B[38;5;214m");
3397
+ bold = esc("\x1B[1m");
3398
+ dim = esc("\x1B[2m");
3399
+ green = esc("\x1B[32m");
3400
+ yellow = esc("\x1B[33m");
3401
+ red = esc("\x1B[31m");
3402
+ cyan = esc("\x1B[36m");
3403
+ boldOrange = esc("\x1B[1m\x1B[38;5;208m");
3404
+ HIDE_CURSOR = isTTY ? "\x1B[?25l" : "";
3405
+ SHOW_CURSOR = isTTY ? "\x1B[?25h" : "";
3406
+ CLEAR_SCREEN = isTTY ? "\x1B[2J\x1B[H" : "";
3407
+ CLEAR_LINE = isTTY ? "\x1B[K" : "";
3408
+ icons = {
3409
+ check: "✔",
3410
+ cross: "✘",
3411
+ warn: "⚠",
3412
+ arrow: "→",
3413
+ bullet: "●",
3414
+ dash: "—",
3415
+ ellipsis: "…"
3416
+ };
3417
+ });
3418
+
3419
+ // src/ui/render.ts
3420
+ var OUT, ERR, INDENT = " ", render;
3421
+ var init_render = __esm(() => {
3422
+ init_styles();
3423
+ OUT = process.stdout;
3424
+ ERR = process.stderr;
3425
+ render = {
3426
+ blank() {
3427
+ OUT.write(`
3428
+ `);
3429
+ },
3430
+ ok(msg, detail) {
3431
+ const d = detail ? ` ${dim("(" + detail + ")")}` : "";
3432
+ OUT.write(`${INDENT}${green(icons.check)} ${msg}${d}
3433
+ `);
3434
+ },
3435
+ warn(msg, hint) {
3436
+ OUT.write(`${INDENT}${yellow(icons.warn)} ${msg}
3437
+ `);
3438
+ if (hint)
3439
+ OUT.write(`${INDENT} ${dim(hint)}
3440
+ `);
3441
+ },
3442
+ err(msg, hint) {
3443
+ ERR.write(`${INDENT}${red(icons.cross)} ${msg}
3444
+ `);
3445
+ if (hint)
3446
+ ERR.write(`${INDENT} ${dim(hint)}
3447
+ `);
3448
+ },
3449
+ info(msg) {
3450
+ OUT.write(`${INDENT}${msg}
3451
+ `);
3452
+ },
3453
+ section(title) {
3454
+ OUT.write(`
3455
+ ${INDENT}${dim("—")} ${clay(title)}
3456
+
3457
+ `);
3458
+ },
3459
+ heading(title) {
3460
+ OUT.write(`${INDENT}${bold(title)}
3461
+ `);
3462
+ },
3463
+ kv(pairs, opts) {
3464
+ const pad = opts?.padTo ?? Math.max(...pairs.map(([k]) => k.length)) + 2;
3465
+ for (const [k, v] of pairs) {
3466
+ OUT.write(`${INDENT}${dim(k.padEnd(pad, " "))}${v}
3467
+ `);
3468
+ }
3469
+ },
3470
+ code(snippet) {
3471
+ for (const line of snippet.split(`
3472
+ `)) {
3473
+ OUT.write(`${INDENT} ${cyan(line)}
3474
+ `);
3475
+ }
3476
+ },
3477
+ link(url) {
3478
+ OUT.write(`${INDENT}${clay(url)}
3479
+ `);
3480
+ },
3481
+ hint(msg) {
3482
+ OUT.write(`${INDENT}${dim(icons.arrow + " " + msg)}
3483
+ `);
3484
+ }
3485
+ };
3486
+ });
3487
+
3488
+ // src/constants/exit-codes.ts
3489
+ var EXIT;
3490
+ var init_exit_codes = __esm(() => {
3491
+ EXIT = {
3492
+ SUCCESS: 0,
3493
+ USER_CANCELLED: 1,
3494
+ AUTH_FAILED: 2,
3495
+ INVALID_ARGS: 3,
3496
+ NETWORK_ERROR: 4,
3497
+ NOT_FOUND: 5,
3498
+ ALREADY_EXISTS: 6,
3499
+ PERMISSION_DENIED: 7,
3500
+ INTERNAL_ERROR: 8,
3501
+ CLAUDE_MISSING: 9
3502
+ };
3503
+ });
3504
+
3505
+ // src/constants/timings.ts
3506
+ var TIMINGS;
3507
+ var init_timings = __esm(() => {
3508
+ TIMINGS = {
3509
+ DEVICE_CODE_POLL_MS: 1500,
3510
+ DEVICE_CODE_TIMEOUT_MS: 5 * 60 * 1000,
3511
+ WS_RECONNECT_BASE_MS: 1000,
3512
+ WS_RECONNECT_MAX_MS: 30000,
3513
+ UPDATE_CHECK_INTERVAL_MS: 24 * 60 * 60 * 1000,
3514
+ TELEGRAM_CONNECT_TIMEOUT_MS: 5 * 60 * 1000,
3515
+ TELEGRAM_POLL_INTERVAL_MS: 2000,
3516
+ API_TIMEOUT_MS: 15000,
3517
+ API_RETRY_COUNT: 2
3518
+ };
3519
+ });
3520
+
3521
+ // src/constants/urls.ts
3522
+ var exports_urls = {};
3523
+ __export(exports_urls, {
3524
+ env: () => env,
3525
+ VERSION: () => VERSION,
3526
+ URLS: () => URLS
3527
+ });
3528
+ var URLS, VERSION = "1.0.0", env;
3529
+ var init_urls = __esm(() => {
3530
+ URLS = {
3531
+ BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
3532
+ API_BASE: process.env.CLAUDEMESH_API_URL ?? "https://claudemesh.com",
3533
+ DASHBOARD: "https://claudemesh.com/dashboard",
3534
+ NPM_REGISTRY: "https://registry.npmjs.org/claudemesh-cli"
3535
+ };
3536
+ env = {
3537
+ CLAUDEMESH_BROKER_URL: URLS.BROKER,
3538
+ CLAUDEMESH_CONFIG_DIR: process.env.CLAUDEMESH_CONFIG_DIR || undefined,
3539
+ CLAUDEMESH_DEBUG: process.env.CLAUDEMESH_DEBUG === "1" || process.env.CLAUDEMESH_DEBUG === "true"
3540
+ };
3541
+ });
3542
+
3543
+ // src/services/logger/logger.ts
3544
+ function timestamp() {
3545
+ return new Date().toISOString();
3546
+ }
3547
+ function log(msg, ...args) {
3548
+ if (!isQuiet)
3549
+ console.log(msg, ...args);
3550
+ }
3551
+ function debug(msg, ...args) {
3552
+ if (isDebug)
3553
+ console.error(`[${timestamp()}] DEBUG ${msg}`, ...args);
3554
+ }
3555
+ function warn(msg, ...args) {
3556
+ console.error(`⚠ ${msg}`, ...args);
3557
+ }
3558
+ var isDebug, isQuiet;
3559
+ var init_logger = __esm(() => {
3560
+ isDebug = process.env.CLAUDEMESH_DEBUG === "1" || process.env.CLAUDEMESH_DEBUG === "true";
3561
+ isQuiet = process.argv.includes("-q") || process.argv.includes("--quiet");
3562
+ });
3563
+
3564
+ // src/services/logger/facade.ts
3565
+ var init_facade4 = __esm(() => {
3566
+ init_logger();
3567
+ });
3568
+
3569
+ // src/services/api/errors.ts
3570
+ var ApiError, NetworkError;
3571
+ var init_errors2 = __esm(() => {
3572
+ ApiError = class ApiError extends Error {
3573
+ status;
3574
+ statusText;
3575
+ body;
3576
+ constructor(status, statusText, body) {
3577
+ super(`API error ${status}: ${statusText}`);
3578
+ this.status = status;
3579
+ this.statusText = statusText;
3580
+ this.body = body;
3581
+ this.name = "ApiError";
3582
+ }
3583
+ get isUnauthorized() {
3584
+ return this.status === 401;
3585
+ }
3586
+ get isNotFound() {
3587
+ return this.status === 404;
3588
+ }
3589
+ get isConflict() {
3590
+ return this.status === 409;
3591
+ }
3592
+ get isRateLimited() {
3593
+ return this.status === 429;
3594
+ }
3595
+ };
3596
+ NetworkError = class NetworkError extends Error {
3597
+ url;
3598
+ constructor(url, cause) {
3599
+ super(`Network error reaching ${url}`);
3600
+ this.url = url;
3601
+ this.name = "NetworkError";
3602
+ this.cause = cause;
3603
+ }
3604
+ };
3605
+ });
3606
+
3607
+ // src/services/api/client.ts
3608
+ async function request(opts) {
3609
+ const base = opts.baseUrl ?? URLS.API_BASE;
3610
+ const url = `${base}${opts.path}`;
3611
+ const method = opts.method ?? "GET";
3612
+ const controller = new AbortController;
3613
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? TIMINGS.API_TIMEOUT_MS);
3614
+ const headers = {
3615
+ "Content-Type": "application/json",
3616
+ Accept: "application/json",
3617
+ "User-Agent": "claudemesh-cli/1.0"
3618
+ };
3619
+ if (opts.token)
3620
+ headers.Authorization = `Bearer ${opts.token}`;
3621
+ debug(`${method} ${url}`);
3622
+ try {
3623
+ const res = await fetch(url, {
3624
+ method,
3625
+ headers,
3626
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
3627
+ signal: controller.signal
3628
+ });
3629
+ if (!res.ok) {
3630
+ let body;
3631
+ try {
3632
+ body = await res.json();
3633
+ } catch {
3634
+ body = await res.text();
3635
+ }
3636
+ throw new ApiError(res.status, res.statusText, body);
3637
+ }
3638
+ const text = await res.text();
3639
+ if (!text)
3640
+ return;
3641
+ return JSON.parse(text);
3642
+ } catch (err) {
3643
+ if (err instanceof ApiError)
3644
+ throw err;
3645
+ throw new NetworkError(url, err);
3646
+ } finally {
3647
+ clearTimeout(timeout);
3648
+ }
3649
+ }
3650
+ async function get(path, token) {
3651
+ return request({ path, token });
3652
+ }
3653
+ async function post(path, body, token) {
3654
+ return request({ path, method: "POST", body, token });
3655
+ }
3656
+ var init_client = __esm(() => {
3657
+ init_urls();
3658
+ init_timings();
3659
+ init_facade4();
3660
+ init_errors2();
3661
+ });
3662
+
3663
+ // src/services/api/my.ts
3664
+ var exports_my = {};
3665
+ __export(exports_my, {
3666
+ revokeSession: () => revokeSession,
3667
+ renameMesh: () => renameMesh,
3668
+ getProfile: () => getProfile,
3669
+ getMeshes: () => getMeshes,
3670
+ createMesh: () => createMesh,
3671
+ createInvite: () => createInvite,
3672
+ cliSync: () => cliSync
3673
+ });
3674
+ async function getProfile(token) {
3675
+ return get("/api/my/profile", token);
3676
+ }
3677
+ async function getMeshes(token) {
3678
+ return get("/api/my/meshes", token);
3679
+ }
3680
+ async function createMesh(token, body) {
3681
+ return post("/api/my/meshes", body, token);
3682
+ }
3683
+ async function renameMesh(token, slug, newName) {
3684
+ return request({
3685
+ path: `/api/my/meshes/${slug}`,
3686
+ method: "PATCH",
3687
+ body: { name: newName },
3688
+ token
3689
+ });
3690
+ }
3691
+ async function createInvite(token, meshSlug, body) {
3692
+ return post(`/api/my/meshes/${meshSlug}/invites`, body, token);
3693
+ }
3694
+ async function revokeSession(token) {
3695
+ const BROKER_HTTP = (await Promise.resolve().then(() => (init_urls(), exports_urls))).URLS.BROKER.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
3696
+ return request({
3697
+ path: "/cli/session/revoke",
3698
+ method: "POST",
3699
+ body: { token },
3700
+ baseUrl: BROKER_HTTP
3701
+ });
3702
+ }
3703
+ async function cliSync(token) {
3704
+ return post("/cli-sync", undefined, token);
3705
+ }
3706
+ var init_my = __esm(() => {
3707
+ init_client();
3708
+ });
3709
+
3710
+ // src/services/api/public.ts
3711
+ var exports_public = {};
3712
+ __export(exports_public, {
3713
+ requestDeviceCode: () => requestDeviceCode,
3714
+ pollDeviceCode: () => pollDeviceCode,
3715
+ claimInvite: () => claimInvite
3716
+ });
3717
+ async function claimInvite(code, body) {
3718
+ return post(`/api/public/invites/${code}/claim`, body);
3719
+ }
3720
+ async function requestDeviceCode(deviceInfo) {
3721
+ return request({
3722
+ path: "/cli/device-code",
3723
+ method: "POST",
3724
+ body: deviceInfo,
3725
+ baseUrl: BROKER_HTTP
3726
+ });
3727
+ }
3728
+ async function pollDeviceCode(deviceCode) {
3729
+ return request({
3730
+ path: `/cli/device-code/${deviceCode}`,
3731
+ baseUrl: BROKER_HTTP
3732
+ });
3733
+ }
3734
+ var BROKER_HTTP;
3735
+ var init_public = __esm(() => {
3736
+ init_client();
3737
+ init_urls();
3738
+ BROKER_HTTP = URLS.BROKER.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
3739
+ });
3740
+
3741
+ // src/services/api/facade.ts
3742
+ var init_facade5 = __esm(() => {
3743
+ init_client();
3744
+ init_errors2();
3745
+ init_my();
3746
+ init_public();
3747
+ });
3748
+
3749
+ // src/services/device/info.ts
3750
+ import { hostname as hostname2, platform as platform2, arch, release } from "node:os";
3751
+ function getDeviceInfo() {
3752
+ return {
3753
+ hostname: hostname2(),
3754
+ platform: platform2(),
3755
+ arch: arch(),
3756
+ osRelease: release(),
3757
+ nodeVersion: process.version
3758
+ };
3759
+ }
3760
+ var init_info = () => {};
3761
+
3762
+ // src/services/device/facade.ts
3763
+ var init_facade6 = __esm(() => {
3764
+ init_info();
3765
+ });
3766
+
3767
+ // src/services/spawn/claude.ts
3768
+ import { spawnSync } from "node:child_process";
3769
+ import { existsSync as existsSync2 } from "node:fs";
3770
+ function findClaudeBinary() {
3771
+ const candidates = [
3772
+ process.env.CLAUDE_BIN,
3773
+ "/usr/local/bin/claude",
3774
+ `${process.env.HOME}/.local/bin/claude`,
3775
+ `${process.env.HOME}/.npm/bin/claude`
3776
+ ].filter(Boolean);
3777
+ for (const bin of candidates) {
3778
+ if (existsSync2(bin))
3779
+ return bin;
3780
+ }
3781
+ const which = spawnSync("which", ["claude"], { encoding: "utf-8" });
3782
+ if (which.status === 0 && which.stdout.trim())
3783
+ return which.stdout.trim();
3784
+ return null;
3785
+ }
3786
+ function spawnClaude(opts) {
3787
+ const bin = findClaudeBinary();
3788
+ if (!bin)
3789
+ throw new Error("Claude binary not found. Install with: npm i -g @anthropic-ai/claude-code");
3790
+ return spawnSync(bin, opts.args, {
3791
+ stdio: "inherit",
3792
+ env: { ...process.env, ...opts.env },
3793
+ cwd: opts.cwd
3794
+ });
3795
+ }
3796
+ var init_claude = () => {};
3797
+
3798
+ // src/services/spawn/browser.ts
3799
+ import { execFile } from "node:child_process";
3800
+ import { platform as platform3 } from "node:os";
3801
+ function openBrowser(url) {
3802
+ return new Promise((resolve, reject) => {
3803
+ const os = platform3();
3804
+ let bin;
3805
+ let args;
3806
+ if (os === "darwin") {
3807
+ bin = "open";
3808
+ args = [url];
3809
+ } else if (os === "win32") {
3810
+ bin = "cmd";
3811
+ args = ["/c", "start", "", url];
3812
+ } else {
3813
+ bin = "xdg-open";
3814
+ args = [url];
3815
+ }
3816
+ execFile(bin, args, (err) => {
3817
+ if (err)
3818
+ reject(err);
3819
+ else
3820
+ resolve();
3821
+ });
3822
+ });
3823
+ }
3824
+ var init_browser = () => {};
3825
+
3826
+ // src/services/spawn/facade.ts
3827
+ var exports_facade3 = {};
3828
+ __export(exports_facade3, {
3829
+ spawnClaude: () => spawnClaude,
3830
+ openBrowser: () => openBrowser,
3831
+ findClaudeBinary: () => findClaudeBinary
3832
+ });
3833
+ var init_facade7 = __esm(() => {
3834
+ init_claude();
3835
+ init_browser();
3836
+ });
3837
+
3838
+ // src/services/auth/token-store.ts
3839
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync3, openSync as openSync2, closeSync as closeSync2 } from "node:fs";
3840
+ function getStoredToken() {
3841
+ if (!existsSync3(PATHS.AUTH_FILE))
3842
+ return null;
3843
+ try {
3844
+ const raw = readFileSync2(PATHS.AUTH_FILE, "utf-8");
3845
+ return JSON.parse(raw);
3846
+ } catch {
3847
+ return null;
3848
+ }
3849
+ }
3850
+ function storeToken(auth) {
3851
+ ensureConfigDir();
3852
+ const data = { ...auth, stored_at: new Date().toISOString() };
3853
+ const content = JSON.stringify(data, null, 2) + `
3854
+ `;
3855
+ const fd = openSync2(PATHS.AUTH_FILE, "w", 384);
3856
+ try {
3857
+ writeFileSync2(fd, content, "utf-8");
3858
+ } finally {
3859
+ closeSync2(fd);
3860
+ }
3861
+ }
3862
+ function clearToken() {
3863
+ try {
3864
+ unlinkSync(PATHS.AUTH_FILE);
3865
+ } catch {}
3866
+ }
3867
+ var init_token_store = __esm(() => {
3868
+ init_paths();
3869
+ init_facade();
3870
+ });
3871
+
3872
+ // src/services/auth/errors.ts
3873
+ var AuthError, DeviceCodeExpired, NotSignedIn;
3874
+ var init_errors3 = __esm(() => {
3875
+ AuthError = class AuthError extends Error {
3876
+ constructor(message) {
3877
+ super(message);
3878
+ this.name = "AuthError";
3879
+ }
3880
+ };
3881
+ DeviceCodeExpired = class DeviceCodeExpired extends AuthError {
3882
+ constructor() {
3883
+ super("Device code expired. Run `claudemesh login` again.");
3884
+ }
3885
+ };
3886
+ NotSignedIn = class NotSignedIn extends AuthError {
3887
+ constructor() {
3888
+ super("Not signed in. Run `claudemesh login` first.");
3889
+ }
3890
+ };
3891
+ });
3892
+
3893
+ // src/services/auth/device-code.ts
3894
+ import { createInterface as createInterface2 } from "node:readline";
3895
+ function parseJwtUser(token) {
3896
+ try {
3897
+ const parts = token.split(".");
3898
+ if (parts[1]) {
3899
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
3900
+ if (payload.exp && payload.exp < Date.now() / 1000)
3901
+ throw new Error("expired");
3902
+ return {
3903
+ id: payload.sub ?? "",
3904
+ display_name: payload.name ?? payload.email ?? "",
3905
+ email: payload.email ?? ""
3906
+ };
3907
+ }
3908
+ } catch {}
3909
+ throw new Error("Invalid token");
3910
+ }
3911
+ async function loginWithDeviceCode() {
3912
+ const device = getDeviceInfo();
3913
+ const { device_code, user_code, session_id, verification_url, token_url } = await exports_public.requestDeviceCode({
3914
+ hostname: device.hostname,
3915
+ platform: device.platform,
3916
+ arch: device.arch
3917
+ });
3918
+ const browserUrl = `${verification_url}?session=${session_id}`;
3919
+ const isTTY2 = process.stdout.isTTY && !process.env.NO_COLOR;
3920
+ const orange2 = (s) => isTTY2 ? `\x1B[38;5;208m${s}\x1B[0m` : s;
3921
+ const bold2 = (s) => isTTY2 ? `\x1B[1m${s}\x1B[0m` : s;
3922
+ const dim2 = (s) => isTTY2 ? `\x1B[2m${s}\x1B[0m` : s;
3923
+ log("");
3924
+ log(" " + orange2("claudemesh") + " — sign in to connect your terminal");
3925
+ log("");
3926
+ log(" ┌──────────────────────────────────┐");
3927
+ log(" │ │");
3928
+ log(" │ Your code: " + bold2(user_code) + " │");
3929
+ log(" │ │");
3930
+ log(" └──────────────────────────────────┘");
3931
+ log("");
3932
+ log(" " + dim2("Confirm this code matches your browser."));
3933
+ log("");
3934
+ log(" " + dim2("If the browser didn't open, visit:"));
3935
+ log(" " + browserUrl);
3936
+ log("");
3937
+ log(" " + dim2("Can't use a browser? Generate a token at:"));
3938
+ log(" " + (token_url || verification_url.replace("/cli-auth", "/token")));
3939
+ log(" " + dim2("Then paste it below."));
3940
+ log("");
3941
+ log(" Waiting… " + dim2("(paste token or Ctrl-C to cancel)"));
3942
+ try {
3943
+ await openBrowser(browserUrl);
3944
+ } catch {
3945
+ warn(" Could not open browser automatically.");
3946
+ }
3947
+ return new Promise((resolve, reject) => {
3948
+ let done = false;
3949
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3950
+ rl.on("line", (line) => {
3951
+ if (done)
3952
+ return;
3953
+ const trimmed = line.trim();
3954
+ if (trimmed.split(".").length === 3 && trimmed.length > 50) {
3955
+ done = true;
3956
+ rl.close();
3957
+ try {
3958
+ const user = parseJwtUser(trimmed);
3959
+ storeToken({ session_token: trimmed, user, token_source: "manual" });
3960
+ resolve({ user, session_token: trimmed });
3961
+ } catch (e) {
3962
+ reject(new Error("Invalid or expired token. Generate a new one."));
3963
+ }
3964
+ }
3965
+ });
3966
+ const startTime = Date.now();
3967
+ const poll = async () => {
3968
+ while (!done && Date.now() - startTime < TIMINGS.DEVICE_CODE_TIMEOUT_MS) {
3969
+ await new Promise((r) => setTimeout(r, TIMINGS.DEVICE_CODE_POLL_MS));
3970
+ if (done)
3971
+ return;
3972
+ try {
3973
+ const result = await exports_public.pollDeviceCode(device_code);
3974
+ if (result.status === "approved" && result.session_token && result.user) {
3975
+ if (done)
3976
+ return;
3977
+ done = true;
3978
+ rl.close();
3979
+ storeToken({ session_token: result.session_token, user: result.user, token_source: "device-code" });
3980
+ resolve({ user: result.user, session_token: result.session_token });
3981
+ return;
3982
+ }
3983
+ if (result.status === "expired") {
3984
+ if (done)
3985
+ return;
3986
+ done = true;
3987
+ rl.close();
3988
+ reject(new DeviceCodeExpired);
3989
+ return;
3990
+ }
3991
+ } catch {}
3992
+ }
3993
+ if (!done) {
3994
+ done = true;
3995
+ rl.close();
3996
+ reject(new DeviceCodeExpired);
3997
+ }
3998
+ };
3999
+ poll();
4000
+ });
4001
+ }
4002
+ var init_device_code = __esm(() => {
4003
+ init_timings();
4004
+ init_facade5();
4005
+ init_facade6();
4006
+ init_facade7();
4007
+ init_facade4();
4008
+ init_token_store();
4009
+ init_errors3();
4010
+ });
4011
+
4012
+ // src/services/auth/client.ts
4013
+ function requireToken() {
4014
+ const auth = getStoredToken();
4015
+ if (!auth)
4016
+ throw new NotSignedIn;
4017
+ return auth.session_token;
4018
+ }
4019
+ async function whoAmI() {
4020
+ const auth = getStoredToken();
4021
+ if (!auth)
4022
+ return { signed_in: false };
4023
+ try {
4024
+ const profile = await exports_my.getProfile(auth.session_token);
4025
+ const meshes = await exports_my.getMeshes(auth.session_token);
4026
+ const owned = meshes.filter((m) => m.role === "owner").length;
4027
+ return {
4028
+ signed_in: true,
4029
+ user: profile,
4030
+ token_source: auth.token_source,
4031
+ meshes: { owned, guest: meshes.length - owned }
4032
+ };
4033
+ } catch (err) {
4034
+ if (err instanceof ApiError && err.isUnauthorized) {
4035
+ clearToken();
4036
+ return { signed_in: false };
4037
+ }
4038
+ throw err;
4039
+ }
4040
+ }
4041
+ async function logout() {
4042
+ const token = requireToken();
4043
+ let revoked = false;
4044
+ try {
4045
+ await exports_my.revokeSession(token);
4046
+ revoked = true;
4047
+ } catch {}
4048
+ clearToken();
4049
+ return { revoked };
4050
+ }
4051
+ async function register(callbackPort) {
4052
+ const { openBrowser: openBrowser2 } = await Promise.resolve().then(() => (init_facade7(), exports_facade3));
4053
+ const url = `https://claudemesh.com/register?source=cli&callback=http://localhost:${callbackPort}`;
4054
+ await openBrowser2(url);
4055
+ }
4056
+ var init_client2 = __esm(() => {
4057
+ init_facade5();
4058
+ init_facade5();
4059
+ init_token_store();
4060
+ init_errors3();
4061
+ });
4062
+
4063
+ // src/services/auth/dashboard-sync.ts
4064
+ async function syncWithBroker(syncToken, peerPubkey, displayName, brokerBaseUrl) {
4065
+ const base = brokerBaseUrl ?? deriveHttpUrl(URLS.BROKER);
4066
+ const res = await fetch(`${base}/cli-sync`, {
4067
+ method: "POST",
4068
+ headers: { "Content-Type": "application/json" },
4069
+ body: JSON.stringify({
4070
+ sync_token: syncToken,
4071
+ peer_pubkey: peerPubkey,
4072
+ display_name: displayName
4073
+ })
4074
+ });
4075
+ if (!res.ok) {
4076
+ const body2 = await res.text();
4077
+ let msg;
4078
+ try {
4079
+ msg = JSON.parse(body2).error ?? body2;
4080
+ } catch {
4081
+ msg = body2;
4082
+ }
4083
+ throw new Error(`Broker sync failed (${res.status}): ${msg}`);
4084
+ }
4085
+ const body = await res.json();
4086
+ if (!body.ok)
4087
+ throw new Error(`Broker sync failed: ${body.error ?? "unknown error"}`);
4088
+ return { account_id: body.account_id, meshes: body.meshes };
4089
+ }
4090
+ function deriveHttpUrl(wssUrl) {
4091
+ const url = new URL(wssUrl);
4092
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
4093
+ url.pathname = url.pathname.replace(/\/ws\/?$/, "");
4094
+ return url.toString().replace(/\/$/, "");
4095
+ }
4096
+ var init_dashboard_sync = __esm(() => {
4097
+ init_urls();
4098
+ });
4099
+
4100
+ // src/services/auth/callback-listener.ts
4101
+ import { createServer } from "node:http";
4102
+ function startCallbackListener() {
4103
+ return new Promise((resolveStart) => {
4104
+ let resolveToken;
4105
+ let resolved = false;
4106
+ const tokenPromise = new Promise((r) => {
4107
+ resolveToken = r;
4108
+ });
4109
+ const server = createServer((req, res) => {
4110
+ const url = new URL(req.url, "http://localhost");
4111
+ if (req.method === "OPTIONS") {
4112
+ res.writeHead(204, {
4113
+ "Access-Control-Allow-Origin": "https://claudemesh.com",
4114
+ "Access-Control-Allow-Methods": "GET",
4115
+ "Access-Control-Allow-Headers": "Content-Type"
4116
+ });
4117
+ res.end();
4118
+ return;
4119
+ }
4120
+ if (url.pathname === "/ping") {
4121
+ res.writeHead(200, {
4122
+ "Content-Type": "text/plain",
4123
+ "Access-Control-Allow-Origin": "https://claudemesh.com"
4124
+ });
4125
+ res.end("ok");
4126
+ return;
4127
+ }
4128
+ if (url.pathname === "/callback") {
4129
+ const token = url.searchParams.get("token");
4130
+ if (token && !resolved) {
4131
+ resolved = true;
4132
+ res.writeHead(200, {
4133
+ "Content-Type": "text/html",
4134
+ "Access-Control-Allow-Origin": "https://claudemesh.com"
4135
+ });
4136
+ res.end("<html><body><h2>Done! You can close this tab.</h2></body></html>");
4137
+ resolveToken(token);
4138
+ setTimeout(() => server.close(), 500);
4139
+ } else {
4140
+ res.writeHead(400, { "Content-Type": "text/plain" });
4141
+ res.end("Missing token");
4142
+ }
4143
+ return;
4144
+ }
4145
+ res.writeHead(404);
4146
+ res.end();
4147
+ });
4148
+ server.listen(0, "127.0.0.1", () => {
4149
+ const addr = server.address();
4150
+ resolveStart({
4151
+ port: addr.port,
4152
+ token: tokenPromise,
4153
+ close: () => server.close()
4154
+ });
4155
+ });
4156
+ });
4157
+ }
4158
+ var init_callback_listener = () => {};
4159
+
4160
+ // src/services/auth/facade.ts
4161
+ var exports_facade4 = {};
4162
+ __export(exports_facade4, {
4163
+ whoAmI: () => whoAmI,
4164
+ syncWithBroker: () => syncWithBroker,
4165
+ storeToken: () => storeToken,
4166
+ startCallbackListener: () => startCallbackListener,
4167
+ register: () => register,
4168
+ logout: () => logout,
4169
+ loginWithDeviceCode: () => loginWithDeviceCode,
4170
+ getStoredToken: () => getStoredToken,
4171
+ generatePairingCode: () => generatePairingCode,
4172
+ clearToken: () => clearToken,
4173
+ NotSignedIn: () => NotSignedIn,
4174
+ DeviceCodeExpired: () => DeviceCodeExpired,
4175
+ AuthError: () => AuthError
4176
+ });
4177
+ import { randomBytes as randomBytes3 } from "node:crypto";
4178
+ function generatePairingCode() {
4179
+ const bytes = randomBytes3(4);
4180
+ return Array.from(bytes, (b) => CHARS[b % CHARS.length]).join("");
4181
+ }
4182
+ var CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
4183
+ var init_facade8 = __esm(() => {
4184
+ init_device_code();
4185
+ init_client2();
4186
+ init_dashboard_sync();
4187
+ init_token_store();
4188
+ init_callback_listener();
4189
+ init_errors3();
4190
+ });
4191
+
4192
+ // src/commands/grants.ts
4193
+ var exports_grants = {};
4194
+ __export(exports_grants, {
4195
+ runRevoke: () => runRevoke,
4196
+ runGrants: () => runGrants,
4197
+ runGrant: () => runGrant,
4198
+ runBlock: () => runBlock,
4199
+ isAllowed: () => isAllowed
4200
+ });
4201
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
4202
+ import { homedir as homedir2 } from "node:os";
4203
+ import { join as join2 } from "node:path";
4204
+ async function syncToBroker(meshSlug, grants) {
4205
+ const auth = getStoredToken();
4206
+ if (!auth)
4207
+ return;
4208
+ try {
4209
+ await request({
4210
+ path: `/cli/mesh/${meshSlug}/grants`,
4211
+ method: "POST",
4212
+ body: { grants },
4213
+ baseUrl: BROKER_HTTP2,
4214
+ token: auth.session_token
4215
+ });
4216
+ } catch (e) {
4217
+ render.warn(`broker grant sync failed — client filter still active: ${e instanceof Error ? e.message : e}`);
4218
+ }
4219
+ }
4220
+ function readGrants() {
4221
+ if (!existsSync4(GRANT_FILE))
4222
+ return {};
4223
+ try {
4224
+ return JSON.parse(readFileSync3(GRANT_FILE, "utf-8"));
4225
+ } catch {
4226
+ return {};
4227
+ }
4228
+ }
4229
+ function writeGrants(g) {
4230
+ const dir = join2(homedir2(), ".claudemesh");
4231
+ if (!existsSync4(dir))
4232
+ mkdirSync2(dir, { recursive: true });
4233
+ writeFileSync3(GRANT_FILE, JSON.stringify(g, null, 2), { mode: 384 });
4234
+ }
4235
+ function resolveCaps(input) {
4236
+ if (input.includes("all"))
4237
+ return [...ALL_CAPS];
4238
+ return input.filter((c) => ALL_CAPS.includes(c));
4239
+ }
4240
+ async function resolvePeer(meshSlug, name) {
4241
+ return await withMesh({ meshSlug }, async (client) => {
4242
+ const peers = await client.listPeers();
4243
+ const match = peers.find((p) => p.displayName === name || p.pubkey === name || p.pubkey.startsWith(name) || p.memberPubkey === name || p.memberPubkey && p.memberPubkey.startsWith(name));
4244
+ if (!match)
4245
+ return null;
4246
+ const key = match.memberPubkey ?? match.pubkey;
4247
+ return { displayName: match.displayName, pubkey: key };
4248
+ });
4249
+ }
4250
+ function pickMesh2(slug) {
4251
+ const cfg = readConfig();
4252
+ if (slug)
4253
+ return cfg.meshes.find((m) => m.slug === slug) ? slug : null;
4254
+ return cfg.meshes[0]?.slug ?? null;
4255
+ }
4256
+ async function runGrant(peer, caps, opts = {}) {
4257
+ if (!peer || caps.length === 0) {
4258
+ render.err("Usage: claudemesh grant <peer> <capability...>");
4259
+ render.hint(`Capabilities: ${ALL_CAPS.join(", ")}, all`);
4260
+ return EXIT.INVALID_ARGS;
4261
+ }
4262
+ const mesh = pickMesh2(opts.mesh);
4263
+ if (!mesh) {
4264
+ render.err("No matching mesh — join one first.");
4265
+ return EXIT.NOT_FOUND;
4266
+ }
4267
+ const resolved = await resolvePeer(mesh, peer);
4268
+ if (!resolved) {
4269
+ render.err(`Peer "${peer}" not found on ${mesh}.`);
4270
+ return EXIT.NOT_FOUND;
4271
+ }
4272
+ const wanted = resolveCaps(caps);
4273
+ if (wanted.length === 0) {
4274
+ render.err(`Unknown capabilities: ${caps.join(", ")}`);
4275
+ return EXIT.INVALID_ARGS;
4276
+ }
4277
+ const store = readGrants();
4278
+ const meshGrants = store[mesh] ?? {};
4279
+ const existing = meshGrants[resolved.pubkey] ?? DEFAULT_CAPS.slice();
4280
+ const merged = Array.from(new Set([...existing, ...wanted]));
4281
+ meshGrants[resolved.pubkey] = merged;
4282
+ store[mesh] = meshGrants;
4283
+ writeGrants(store);
4284
+ await syncToBroker(mesh, { [resolved.pubkey]: merged });
4285
+ render.ok(`Granted ${wanted.join(", ")} to ${resolved.displayName} on ${mesh}.`);
4286
+ render.kv([["now", merged.join(", ")]]);
4287
+ return EXIT.SUCCESS;
4288
+ }
4289
+ async function runRevoke(peer, caps, opts = {}) {
4290
+ if (!peer || caps.length === 0) {
4291
+ render.err("Usage: claudemesh revoke <peer> <capability...>");
4292
+ return EXIT.INVALID_ARGS;
4293
+ }
4294
+ const mesh = pickMesh2(opts.mesh);
4295
+ if (!mesh) {
4296
+ render.err("No matching mesh.");
4297
+ return EXIT.NOT_FOUND;
4298
+ }
4299
+ const resolved = await resolvePeer(mesh, peer);
4300
+ if (!resolved) {
4301
+ render.err(`Peer "${peer}" not found on ${mesh}.`);
4302
+ return EXIT.NOT_FOUND;
4303
+ }
4304
+ const wanted = caps.includes("all") ? ALL_CAPS.slice() : resolveCaps(caps);
4305
+ const store = readGrants();
4306
+ const meshGrants = store[mesh] ?? {};
4307
+ const existing = meshGrants[resolved.pubkey] ?? DEFAULT_CAPS.slice();
4308
+ const after = existing.filter((c) => !wanted.includes(c));
4309
+ meshGrants[resolved.pubkey] = after;
4310
+ store[mesh] = meshGrants;
4311
+ writeGrants(store);
4312
+ await syncToBroker(mesh, { [resolved.pubkey]: after });
4313
+ render.ok(`Revoked ${wanted.join(", ")} from ${resolved.displayName} on ${mesh}.`);
4314
+ render.kv([["now", after.length ? after.join(", ") : "(none)"]]);
4315
+ return EXIT.SUCCESS;
4316
+ }
4317
+ async function runBlock(peer, opts = {}) {
4318
+ if (!peer) {
4319
+ render.err("Usage: claudemesh block <peer>");
4320
+ return EXIT.INVALID_ARGS;
4321
+ }
4322
+ const mesh = pickMesh2(opts.mesh);
4323
+ if (!mesh) {
4324
+ render.err("No matching mesh.");
4325
+ return EXIT.NOT_FOUND;
4326
+ }
4327
+ const resolved = await resolvePeer(mesh, peer);
4328
+ if (!resolved) {
4329
+ render.err(`Peer "${peer}" not found on ${mesh}.`);
4330
+ return EXIT.NOT_FOUND;
4331
+ }
4332
+ const store = readGrants();
4333
+ const meshGrants = store[mesh] ?? {};
4334
+ meshGrants[resolved.pubkey] = [];
4335
+ store[mesh] = meshGrants;
4336
+ writeGrants(store);
4337
+ await syncToBroker(mesh, { [resolved.pubkey]: [] });
4338
+ render.ok(`Blocked ${resolved.displayName} on ${mesh} (all capabilities revoked).`);
4339
+ render.hint(`Undo with: claudemesh grant ${resolved.displayName} all --mesh ${mesh}`);
4340
+ return EXIT.SUCCESS;
4341
+ }
4342
+ async function runGrants(opts = {}) {
4343
+ const mesh = pickMesh2(opts.mesh);
4344
+ if (!mesh) {
4345
+ render.err("No matching mesh.");
4346
+ return EXIT.NOT_FOUND;
4347
+ }
4348
+ const store = readGrants();
4349
+ const meshGrants = store[mesh] ?? {};
4350
+ if (opts.json) {
4351
+ console.log(JSON.stringify({ schema_version: "1.0", mesh, grants: meshGrants }, null, 2));
4352
+ return EXIT.SUCCESS;
4353
+ }
4354
+ render.section(`grants on ${mesh}`);
4355
+ const peerPubkeys = Object.keys(meshGrants);
4356
+ if (peerPubkeys.length === 0) {
4357
+ render.info("(no overrides — all peers use default caps: " + DEFAULT_CAPS.join(", ") + ")");
4358
+ return EXIT.SUCCESS;
4359
+ }
4360
+ await withMesh({ meshSlug: mesh }, async (client) => {
4361
+ const peers = await client.listPeers();
4362
+ const byPk = new Map(peers.map((p) => [p.pubkey, p.displayName]));
4363
+ for (const [pk, caps] of Object.entries(meshGrants)) {
4364
+ const name = byPk.get(pk) ?? `${pk.slice(0, 10)}…`;
4365
+ render.kv([[name, caps.length ? caps.join(", ") : "(blocked)"]]);
4366
+ }
4367
+ });
4368
+ return EXIT.SUCCESS;
4369
+ }
4370
+ function isAllowed(meshSlug, peerPubkey, cap) {
4371
+ const store = readGrants();
4372
+ const entry = store[meshSlug]?.[peerPubkey];
4373
+ if (entry === undefined)
4374
+ return DEFAULT_CAPS.includes(cap);
4375
+ return entry.includes(cap);
4376
+ }
4377
+ var BROKER_HTTP2, ALL_CAPS, DEFAULT_CAPS, GRANT_FILE;
4378
+ var init_grants = __esm(() => {
4379
+ init_facade();
4380
+ init_connect();
4381
+ init_render();
4382
+ init_exit_codes();
4383
+ init_facade8();
4384
+ init_facade5();
4385
+ init_urls();
4386
+ BROKER_HTTP2 = URLS.BROKER.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
4387
+ ALL_CAPS = ["read", "dm", "broadcast", "state-read", "state-write", "file-read"];
4388
+ DEFAULT_CAPS = ["read", "dm", "broadcast", "state-read"];
4389
+ GRANT_FILE = join2(homedir2(), ".claudemesh", "grants.json");
4390
+ });
4391
+
3289
4392
  // src/mcp/server.ts
3290
4393
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3291
4394
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -3339,7 +4442,7 @@ async function resolveClient(to) {
3339
4442
  target = rest;
3340
4443
  }
3341
4444
  }
3342
- if (/^[0-9a-f]{64}$/.test(target) || target.startsWith("#") || target.startsWith("@") || target === "*") {
4445
+ if (target.startsWith("#") || target.startsWith("@") || target === "*") {
3343
4446
  if (targetClients.length === 1) {
3344
4447
  return { client: targetClients[0], targetSpec: target };
3345
4448
  }
@@ -3349,26 +4452,100 @@ async function resolveClient(to) {
3349
4452
  error: `multiple meshes joined; prefix target with "<mesh-slug>:" (joined: ${clients2.map((c) => c.meshSlug).join(", ")})`
3350
4453
  };
3351
4454
  }
4455
+ if (/^[0-9a-f]{8,64}$/.test(target)) {
4456
+ const hits = [];
4457
+ for (const c of targetClients) {
4458
+ const peers = await c.listPeers();
4459
+ for (const p of peers) {
4460
+ if (p.pubkey.startsWith(target)) {
4461
+ hits.push({ mesh: c, pubkey: p.pubkey, displayName: p.displayName });
4462
+ }
4463
+ }
4464
+ }
4465
+ if (hits.length === 1) {
4466
+ return { client: hits[0].mesh, targetSpec: hits[0].pubkey };
4467
+ }
4468
+ if (hits.length > 1) {
4469
+ const lines = hits.map((h) => ` - ${h.displayName} @ ${h.mesh.meshSlug} · pubkey ${h.pubkey.slice(0, 20)}…`).join(`
4470
+ `);
4471
+ return {
4472
+ client: null,
4473
+ targetSpec: target,
4474
+ error: `ambiguous pubkey prefix "${target}" matches ${hits.length} peers:
4475
+ ${lines}
4476
+ Use a longer prefix.`
4477
+ };
4478
+ }
4479
+ if (target.length === 64) {
4480
+ if (targetClients.length === 1) {
4481
+ return { client: targetClients[0], targetSpec: target };
4482
+ }
4483
+ return {
4484
+ client: null,
4485
+ targetSpec: target,
4486
+ error: `multiple meshes joined; prefix target with "<mesh-slug>:" (joined: ${clients2.map((c) => c.meshSlug).join(", ")})`
4487
+ };
4488
+ }
4489
+ return {
4490
+ client: null,
4491
+ targetSpec: target,
4492
+ error: `no online peer's pubkey starts with "${target}".`
4493
+ };
4494
+ }
3352
4495
  const nameLower = target.toLowerCase();
4496
+ const candidates = [];
4497
+ const exactMatches = [];
4498
+ const partialMatches = [];
3353
4499
  for (const c of targetClients) {
4500
+ const ownSession = c.getSessionPubkey();
3354
4501
  const peers = await c.listPeers();
3355
- const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
3356
- if (match)
3357
- return { client: c, targetSpec: match.pubkey };
3358
- const partials = peers.filter((p) => p.displayName.toLowerCase().includes(nameLower));
3359
- if (partials.length === 1) {
3360
- process.stderr.write(`[claudemesh] resolved "${target}" → "${partials[0].displayName}" (partial match)
3361
- `);
3362
- return { client: c, targetSpec: partials[0].pubkey };
4502
+ candidates.push({ mesh: c.meshSlug, peers });
4503
+ for (const p of peers) {
4504
+ if (ownSession && p.pubkey === ownSession)
4505
+ continue;
4506
+ const nameLow = p.displayName.toLowerCase();
4507
+ if (nameLow === nameLower) {
4508
+ exactMatches.push({ mesh: c, pubkey: p.pubkey, displayName: p.displayName, cwd: p.cwd });
4509
+ } else if (nameLow.includes(nameLower)) {
4510
+ partialMatches.push({ mesh: c, pubkey: p.pubkey, displayName: p.displayName, cwd: p.cwd });
4511
+ }
3363
4512
  }
3364
4513
  }
3365
- if (targetClients.length === 1) {
3366
- return { client: targetClients[0], targetSpec: target };
4514
+ if (exactMatches.length === 1) {
4515
+ return { client: exactMatches[0].mesh, targetSpec: exactMatches[0].pubkey };
4516
+ }
4517
+ if (exactMatches.length > 1) {
4518
+ const lines = exactMatches.map((m) => ` - ${m.displayName} · pubkey ${m.pubkey.slice(0, 16)}…${m.cwd ? ` · cwd ${m.cwd}` : ""}`).join(`
4519
+ `);
4520
+ return {
4521
+ client: null,
4522
+ targetSpec: target,
4523
+ error: `"${target}" is ambiguous — ${exactMatches.length} peers share that display name:
4524
+ ${lines}
4525
+ ` + `Disambiguate by pubkey prefix (e.g. send to "${exactMatches[0].pubkey.slice(0, 12)}…").`
4526
+ };
4527
+ }
4528
+ if (partialMatches.length === 1) {
4529
+ process.stderr.write(`[claudemesh] resolved "${target}" → "${partialMatches[0].displayName}" (partial match)
4530
+ `);
4531
+ return { client: partialMatches[0].mesh, targetSpec: partialMatches[0].pubkey };
4532
+ }
4533
+ if (partialMatches.length > 1) {
4534
+ const lines = partialMatches.map((m) => ` - ${m.displayName} · pubkey ${m.pubkey.slice(0, 16)}…`).join(`
4535
+ `);
4536
+ return {
4537
+ client: null,
4538
+ targetSpec: target,
4539
+ error: `"${target}" partially matches ${partialMatches.length} peers:
4540
+ ${lines}
4541
+ Be more specific, or use a pubkey prefix.`
4542
+ };
3367
4543
  }
4544
+ const known = candidates.flatMap((c) => c.peers.map((p) => `${c.mesh}/${p.displayName}`));
3368
4545
  return {
3369
4546
  client: null,
3370
4547
  targetSpec: target,
3371
- error: `peer "${target}" not found in any mesh (joined: ${clients2.map((c) => c.meshSlug).join(", ")})`
4548
+ error: `peer "${target}" not found. ` + (known.length ? `Known peers: ${known.slice(0, 10).join(", ")}${known.length > 10 ? ", …" : ""}` : "No connected peers on your mesh(es). Use pubkey hex, @group, or * for broadcast.")
3372
4549
  };
3373
4550
  }
3374
4551
  async function resolvePeerName(client, pubkey) {
@@ -3686,9 +4863,9 @@ ${manifest.allowed_tools.map((t) => ` - ${t}`).join(`
3686
4863
  const results = [];
3687
4864
  const seen = new Set;
3688
4865
  for (const target of targets) {
3689
- const { client, targetSpec, error } = await resolveClient(target);
4866
+ const { client, targetSpec, error: error2 } = await resolveClient(target);
3690
4867
  if (!client) {
3691
- results.push(`✗ ${target}: ${error ?? "no client resolved"}`);
4868
+ results.push(`✗ ${target}: ${error2 ?? "no client resolved"}`);
3692
4869
  continue;
3693
4870
  }
3694
4871
  if (seen.has(targetSpec))
@@ -3710,13 +4887,23 @@ ${manifest.allowed_tools.map((t) => ` - ${t}`).join(`
3710
4887
  if (clients2.length === 0)
3711
4888
  return text(mesh_slug ? `list_peers: no joined mesh "${mesh_slug}"` : "list_peers: no joined meshes", true);
3712
4889
  const sections = [];
4890
+ const statusCache = {};
3713
4891
  for (const c of clients2) {
3714
4892
  const peers = await c.listPeers();
3715
4893
  const header = `## ${c.meshSlug} (${c.status}, mesh ${c.meshId.slice(0, 8)}…)`;
4894
+ statusCache[c.meshSlug] = {
4895
+ total: peers.length,
4896
+ online: peers.filter((p) => p.status !== "offline").length,
4897
+ updatedAt: new Date().toISOString(),
4898
+ you: process.env.CLAUDEMESH_DISPLAY_NAME ?? undefined
4899
+ };
3716
4900
  if (peers.length === 0) {
3717
4901
  sections.push(`${header}
3718
4902
  No peers connected.`);
3719
4903
  } else {
4904
+ const pubkeyCounts = new Map;
4905
+ for (const p of peers)
4906
+ pubkeyCounts.set(p.pubkey, (pubkeyCounts.get(p.pubkey) ?? 0) + 1);
3720
4907
  const peerLines = peers.map((p) => {
3721
4908
  const summary = p.summary ? ` — "${p.summary}"` : "";
3722
4909
  const groupsStr = p.groups?.length ? ` [${p.groups.map((g) => `@${g.name}${g.role ? ":" + g.role : ""}`).join(", ")}]` : "";
@@ -3734,13 +4921,24 @@ No peers connected.`);
3734
4921
  const profileAvatar = p.profile?.avatar ? `${p.profile.avatar} ` : "";
3735
4922
  const profileTitle = p.profile?.title ? ` (${p.profile.title})` : "";
3736
4923
  const hiddenTag = p.visible === false ? " [hidden]" : "";
3737
- return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${localityTag}${hiddenTag}${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`;
4924
+ const sameKeyCount = pubkeyCounts.get(p.pubkey) ?? 1;
4925
+ const sameKeyTag = sameKeyCount > 1 ? ` [shares key with ${sameKeyCount - 1} other session(s)]` : "";
4926
+ return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${localityTag}${hiddenTag}${sameKeyTag}${groupsStr}${metaStr} (pubkey: ${p.pubkey.slice(0, 16)}…)${cwdStr}${summary}`;
3738
4927
  });
3739
4928
  sections.push(`${header}
3740
4929
  ${peerLines.join(`
3741
4930
  `)}`);
3742
4931
  }
3743
4932
  }
4933
+ try {
4934
+ const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync3, existsSync: existsSync5 } = await import("node:fs");
4935
+ const { join: joinPath } = await import("node:path");
4936
+ const { homedir: homedir3 } = await import("node:os");
4937
+ const dir = joinPath(homedir3(), ".claudemesh");
4938
+ if (!existsSync5(dir))
4939
+ mkdirSync3(dir, { recursive: true });
4940
+ writeFileSync4(joinPath(dir, "peer-cache.json"), JSON.stringify(statusCache));
4941
+ } catch {}
3744
4942
  return text(sections.join(`
3745
4943
 
3746
4944
  `));
@@ -3762,8 +4960,8 @@ ${peerLines.join(`
3762
4960
  return text(`Message ${id} not found or timed out.`);
3763
4961
  const recipientLines = result.recipients.map((r) => ` - ${r.name} (${r.pubkey.slice(0, 12)}…): ${r.status}`);
3764
4962
  return text(`Message ${id.slice(0, 12)}… → ${result.targetSpec}
3765
- ` + `Delivered: ${result.delivered}${result.deliveredAt ? ` at ${result.deliveredAt}` : ""}
3766
- ` + `Recipients:
4963
+ Delivered: ${result.delivered}${result.deliveredAt ? ` at ${result.deliveredAt}` : ""}
4964
+ Recipients:
3767
4965
  ${recipientLines.join(`
3768
4966
  `)}`);
3769
4967
  }
@@ -3981,23 +5179,23 @@ ${lines.join(`
3981
5179
  const { path: filePath, name: fileName, tags, to: fileTo } = args ?? {};
3982
5180
  if (!filePath)
3983
5181
  return text("share_file: `path` required", true);
3984
- const { existsSync: existsSync2 } = await import("node:fs");
3985
- if (!existsSync2(filePath))
5182
+ const { existsSync: existsSync5 } = await import("node:fs");
5183
+ if (!existsSync5(filePath))
3986
5184
  return text(`share_file: file not found: ${filePath}`, true);
3987
5185
  const client = allClients()[0];
3988
5186
  if (!client)
3989
5187
  return text("share_file: not connected", true);
3990
5188
  if (fileTo) {
3991
5189
  const { encryptFile: encryptFile2, sealKeyForPeer: sealKeyForPeer2 } = await Promise.resolve().then(() => (init_file_crypto(), exports_file_crypto));
3992
- const { readFileSync: readFileSync2, writeFileSync: writeFileSync2, mkdtempSync, unlinkSync, rmdirSync } = await import("node:fs");
5190
+ const { readFileSync: readFileSync4, writeFileSync: writeFileSync4, mkdtempSync, unlinkSync: unlinkSync2, rmdirSync } = await import("node:fs");
3993
5191
  const { tmpdir } = await import("node:os");
3994
- const { join: join2, basename } = await import("node:path");
5192
+ const { join: join3, basename } = await import("node:path");
3995
5193
  const peers = await client.listPeers();
3996
5194
  const targetPeer = peers.find((p) => p.pubkey === fileTo || p.displayName === fileTo);
3997
5195
  if (!targetPeer) {
3998
5196
  return text(`share_file: peer not found: ${fileTo}`, true);
3999
5197
  }
4000
- const plaintext = readFileSync2(filePath);
5198
+ const plaintext = readFileSync4(filePath);
4001
5199
  const { ciphertext, nonce, key } = await encryptFile2(new Uint8Array(plaintext));
4002
5200
  const sealedForTarget = await sealKeyForPeer2(key, targetPeer.pubkey);
4003
5201
  const myPubkey = client.getSessionPubkey();
@@ -4014,9 +5212,9 @@ ${lines.join(`
4014
5212
  combined.set(ciphertext, nonceBytes.length);
4015
5213
  const rawName = fileName ?? basename(filePath);
4016
5214
  const baseName = basename(rawName).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 255);
4017
- const tmpDir = mkdtempSync(join2(tmpdir(), "cm-"));
4018
- const tmpPath = join2(tmpDir, baseName);
4019
- writeFileSync2(tmpPath, combined);
5215
+ const tmpDir = mkdtempSync(join3(tmpdir(), "cm-"));
5216
+ const tmpPath = join3(tmpDir, baseName);
5217
+ writeFileSync4(tmpPath, combined);
4020
5218
  try {
4021
5219
  const fileId = await client.uploadFile(tmpPath, client.meshId, client.meshSlug, {
4022
5220
  name: baseName,
@@ -4031,7 +5229,7 @@ ${lines.join(`
4031
5229
  return text(`share_file: upload failed — ${e instanceof Error ? e.message : String(e)}`, true);
4032
5230
  } finally {
4033
5231
  try {
4034
- unlinkSync(tmpPath);
5232
+ unlinkSync2(tmpPath);
4035
5233
  } catch {}
4036
5234
  try {
4037
5235
  rmdirSync(tmpDir);
@@ -4091,10 +5289,10 @@ ${lines.join(`
4091
5289
  const plaintext = await decryptFile2(ciphertext, nonce, kf);
4092
5290
  if (!plaintext)
4093
5291
  return text(genericErr, true);
4094
- const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("node:fs");
5292
+ const { writeFileSync: writeFileSync5, mkdirSync: mkdirSync4 } = await import("node:fs");
4095
5293
  const { dirname: dirname2 } = await import("node:path");
4096
- mkdirSync3(dirname2(save_to), { recursive: true });
4097
- writeFileSync3(save_to, plaintext);
5294
+ mkdirSync4(dirname2(save_to), { recursive: true });
5295
+ writeFileSync5(save_to, plaintext);
4098
5296
  return text(`Downloaded and decrypted: ${result.name} → ${save_to}`);
4099
5297
  }
4100
5298
  let res = await fetch(result.url, { signal: AbortSignal.timeout(1e4) }).catch(() => null);
@@ -4104,10 +5302,10 @@ ${lines.join(`
4104
5302
  }
4105
5303
  if (!res.ok)
4106
5304
  return text(`get_file: download failed (${res.status})`, true);
4107
- const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("node:fs");
5305
+ const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync3 } = await import("node:fs");
4108
5306
  const { dirname } = await import("node:path");
4109
- mkdirSync2(dirname(save_to), { recursive: true });
4110
- writeFileSync2(save_to, Buffer.from(await res.arrayBuffer()));
5307
+ mkdirSync3(dirname(save_to), { recursive: true });
5308
+ writeFileSync4(save_to, Buffer.from(await res.arrayBuffer()));
4111
5309
  return text(`Downloaded: ${result.name} → ${save_to}`);
4112
5310
  }
4113
5311
  case "list_files": {
@@ -4865,10 +6063,10 @@ ${lines.join(`
4865
6063
  const entryType = vType ?? "env";
4866
6064
  let plaintextBytes;
4867
6065
  if (entryType === "file") {
4868
- const { existsSync: existsSync2, readFileSync: readFileSync2 } = await import("node:fs");
4869
- if (!existsSync2(value))
6066
+ const { existsSync: existsSync5, readFileSync: readFileSync4 } = await import("node:fs");
6067
+ if (!existsSync5(value))
4870
6068
  return text(`vault_set: file not found: ${value}`, true);
4871
- plaintextBytes = new Uint8Array(readFileSync2(value));
6069
+ plaintextBytes = new Uint8Array(readFileSync4(value));
4872
6070
  } else {
4873
6071
  plaintextBytes = new TextEncoder().encode(value);
4874
6072
  }
@@ -5196,6 +6394,17 @@ ${lines.join(`
5196
6394
  }
5197
6395
  const fromPubkey = msg.senderPubkey || "";
5198
6396
  const fromName = fromPubkey ? await resolvePeerName(client, fromPubkey) : "unknown";
6397
+ if (fromPubkey) {
6398
+ try {
6399
+ const { isAllowed: isAllowed2 } = await Promise.resolve().then(() => (init_grants(), exports_grants));
6400
+ const kindCap = msg.kind === "broadcast" ? "broadcast" : "dm";
6401
+ if (!isAllowed2(client.meshSlug, fromPubkey, kindCap)) {
6402
+ process.stderr.write(`[claudemesh] dropped ${kindCap} from ${fromName} (not granted)
6403
+ `);
6404
+ return;
6405
+ }
6406
+ } catch {}
6407
+ }
5199
6408
  if (messageMode === "inbox") {
5200
6409
  try {
5201
6410
  await server.notification({
@@ -5208,7 +6417,10 @@ ${lines.join(`
5208
6417
  } catch {}
5209
6418
  return;
5210
6419
  }
5211
- const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
6420
+ const body = msg.plaintext ?? decryptFailedWarning(fromPubkey);
6421
+ const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
6422
+ const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
6423
+ const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
5212
6424
  try {
5213
6425
  await server.notification({
5214
6426
  method: "notifications/claude/channel",
@@ -5227,7 +6439,7 @@ ${lines.join(`
5227
6439
  }
5228
6440
  }
5229
6441
  });
5230
- process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${content.slice(0, 60)}
6442
+ process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${body.slice(0, 60)}
5231
6443
  `);
5232
6444
  } catch (pushErr) {
5233
6445
  process.stderr.write(`[claudemesh] push FAILED: ${pushErr}
@@ -5421,4 +6633,4 @@ startMcpServer().catch((err) => {
5421
6633
  process.exit(1);
5422
6634
  });
5423
6635
 
5424
- //# debugId=F4442514279DCE8564756E2164756E21
6636
+ //# debugId=6DEF79563D9368A464756E2164756E21