claudemesh-cli 1.21.1 → 1.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoints/cli.js +2695 -333
- package/dist/entrypoints/cli.js.map +24 -5
- package/dist/entrypoints/mcp.js +2 -2
- package/dist/entrypoints/mcp.js.map +1 -1
- package/package.json +1 -1
- package/skills/claudemesh/SKILL.md +17 -0
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.22.1", env;
|
|
92
92
|
var init_urls = __esm(() => {
|
|
93
93
|
URLS = {
|
|
94
94
|
BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
|
|
@@ -6834,6 +6834,154 @@ var init_peers = __esm(() => {
|
|
|
6834
6834
|
};
|
|
6835
6835
|
});
|
|
6836
6836
|
|
|
6837
|
+
// src/daemon/paths.ts
|
|
6838
|
+
import { join as join6 } from "node:path";
|
|
6839
|
+
var DAEMON_PATHS, DAEMON_TCP_HOST = "127.0.0.1", DAEMON_TCP_DEFAULT_PORT = 47823;
|
|
6840
|
+
var init_paths2 = __esm(() => {
|
|
6841
|
+
init_paths();
|
|
6842
|
+
DAEMON_PATHS = {
|
|
6843
|
+
get DAEMON_DIR() {
|
|
6844
|
+
return join6(PATHS.CONFIG_DIR, "daemon");
|
|
6845
|
+
},
|
|
6846
|
+
get PID_FILE() {
|
|
6847
|
+
return join6(this.DAEMON_DIR, "daemon.pid");
|
|
6848
|
+
},
|
|
6849
|
+
get SOCK_FILE() {
|
|
6850
|
+
return join6(this.DAEMON_DIR, "daemon.sock");
|
|
6851
|
+
},
|
|
6852
|
+
get TOKEN_FILE() {
|
|
6853
|
+
return join6(this.DAEMON_DIR, "local-token");
|
|
6854
|
+
},
|
|
6855
|
+
get OUTBOX_DB() {
|
|
6856
|
+
return join6(this.DAEMON_DIR, "outbox.db");
|
|
6857
|
+
},
|
|
6858
|
+
get INBOX_DB() {
|
|
6859
|
+
return join6(this.DAEMON_DIR, "inbox.db");
|
|
6860
|
+
},
|
|
6861
|
+
get LOG_FILE() {
|
|
6862
|
+
return join6(this.DAEMON_DIR, "daemon.log");
|
|
6863
|
+
}
|
|
6864
|
+
};
|
|
6865
|
+
});
|
|
6866
|
+
|
|
6867
|
+
// src/daemon/local-token.ts
|
|
6868
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6869
|
+
import { dirname as dirname3 } from "node:path";
|
|
6870
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
6871
|
+
function readLocalToken() {
|
|
6872
|
+
try {
|
|
6873
|
+
return readFileSync5(DAEMON_PATHS.TOKEN_FILE, "utf8").trim();
|
|
6874
|
+
} catch {
|
|
6875
|
+
return null;
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
function ensureLocalToken() {
|
|
6879
|
+
const existing = readLocalToken();
|
|
6880
|
+
if (existing)
|
|
6881
|
+
return existing;
|
|
6882
|
+
mkdirSync4(dirname3(DAEMON_PATHS.TOKEN_FILE), { recursive: true, mode: 448 });
|
|
6883
|
+
const tok = randomBytes4(32).toString("base64url");
|
|
6884
|
+
writeFileSync6(DAEMON_PATHS.TOKEN_FILE, tok + `
|
|
6885
|
+
`, { mode: 384 });
|
|
6886
|
+
return tok;
|
|
6887
|
+
}
|
|
6888
|
+
var init_local_token = __esm(() => {
|
|
6889
|
+
init_paths2();
|
|
6890
|
+
});
|
|
6891
|
+
|
|
6892
|
+
// src/daemon/ipc/client.ts
|
|
6893
|
+
import { request as httpRequest } from "node:http";
|
|
6894
|
+
async function ipc(opts) {
|
|
6895
|
+
const useTcp = !!opts.preferTcp;
|
|
6896
|
+
const headers = {
|
|
6897
|
+
accept: "application/json",
|
|
6898
|
+
host: "localhost"
|
|
6899
|
+
};
|
|
6900
|
+
let bodyBuf;
|
|
6901
|
+
if (opts.body !== undefined) {
|
|
6902
|
+
bodyBuf = Buffer.from(JSON.stringify(opts.body), "utf8");
|
|
6903
|
+
headers["content-type"] = "application/json";
|
|
6904
|
+
headers["content-length"] = String(bodyBuf.length);
|
|
6905
|
+
}
|
|
6906
|
+
if (useTcp) {
|
|
6907
|
+
const tok = readLocalToken();
|
|
6908
|
+
if (!tok)
|
|
6909
|
+
throw new IpcError(0, null, "daemon local token not found; is the daemon running?");
|
|
6910
|
+
headers.authorization = `Bearer ${tok}`;
|
|
6911
|
+
}
|
|
6912
|
+
return new Promise((resolve, reject) => {
|
|
6913
|
+
const req = httpRequest(useTcp ? { host: DAEMON_TCP_HOST, port: DAEMON_TCP_DEFAULT_PORT, path: opts.path, method: opts.method ?? "GET", headers } : { socketPath: DAEMON_PATHS.SOCK_FILE, path: opts.path, method: opts.method ?? "GET", headers }, (res) => {
|
|
6914
|
+
const chunks = [];
|
|
6915
|
+
res.on("data", (c) => chunks.push(c));
|
|
6916
|
+
res.on("end", () => {
|
|
6917
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
6918
|
+
let parsed = raw;
|
|
6919
|
+
try {
|
|
6920
|
+
parsed = raw.length > 0 ? JSON.parse(raw) : null;
|
|
6921
|
+
} catch {}
|
|
6922
|
+
resolve({ status: res.statusCode ?? 0, body: parsed });
|
|
6923
|
+
});
|
|
6924
|
+
});
|
|
6925
|
+
req.setTimeout(opts.timeoutMs ?? 5000, () => req.destroy(new Error("ipc_timeout")));
|
|
6926
|
+
req.on("error", (err) => reject(err));
|
|
6927
|
+
if (bodyBuf)
|
|
6928
|
+
req.write(bodyBuf);
|
|
6929
|
+
req.end();
|
|
6930
|
+
});
|
|
6931
|
+
}
|
|
6932
|
+
var IpcError;
|
|
6933
|
+
var init_client4 = __esm(() => {
|
|
6934
|
+
init_paths2();
|
|
6935
|
+
init_local_token();
|
|
6936
|
+
IpcError = class IpcError extends Error {
|
|
6937
|
+
status;
|
|
6938
|
+
payload;
|
|
6939
|
+
constructor(status, payload, msg) {
|
|
6940
|
+
super(msg);
|
|
6941
|
+
this.status = status;
|
|
6942
|
+
this.payload = payload;
|
|
6943
|
+
}
|
|
6944
|
+
};
|
|
6945
|
+
});
|
|
6946
|
+
|
|
6947
|
+
// src/services/bridge/daemon-route.ts
|
|
6948
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
6949
|
+
async function trySendViaDaemon(args) {
|
|
6950
|
+
if (!existsSync7(DAEMON_PATHS.SOCK_FILE))
|
|
6951
|
+
return null;
|
|
6952
|
+
try {
|
|
6953
|
+
const res = await ipc({
|
|
6954
|
+
method: "POST",
|
|
6955
|
+
path: "/v1/send",
|
|
6956
|
+
timeoutMs: 3000,
|
|
6957
|
+
body: {
|
|
6958
|
+
to: args.to,
|
|
6959
|
+
message: args.message,
|
|
6960
|
+
priority: args.priority,
|
|
6961
|
+
...args.idempotencyKey ? { client_message_id: args.idempotencyKey } : {}
|
|
6962
|
+
}
|
|
6963
|
+
});
|
|
6964
|
+
if (res.status === 202 || res.status === 200) {
|
|
6965
|
+
return {
|
|
6966
|
+
ok: true,
|
|
6967
|
+
messageId: res.body.broker_message_id ?? res.body.client_message_id ?? "",
|
|
6968
|
+
duplicate: res.body.duplicate,
|
|
6969
|
+
status: res.body.status
|
|
6970
|
+
};
|
|
6971
|
+
}
|
|
6972
|
+
return { ok: false, error: res.body.error ?? `daemon http ${res.status}` };
|
|
6973
|
+
} catch (err) {
|
|
6974
|
+
const msg = String(err);
|
|
6975
|
+
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg))
|
|
6976
|
+
return null;
|
|
6977
|
+
return { ok: false, error: msg };
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
var init_daemon_route = __esm(() => {
|
|
6981
|
+
init_client4();
|
|
6982
|
+
init_paths2();
|
|
6983
|
+
});
|
|
6984
|
+
|
|
6837
6985
|
// src/commands/send.ts
|
|
6838
6986
|
var exports_send = {};
|
|
6839
6987
|
__export(exports_send, {
|
|
@@ -6855,6 +7003,23 @@ async function runSend(flags, to, message) {
|
|
|
6855
7003
|
process.exit(1);
|
|
6856
7004
|
}
|
|
6857
7005
|
}
|
|
7006
|
+
{
|
|
7007
|
+
const dr = await trySendViaDaemon({ to, message, priority, expectedMesh: meshSlug ?? undefined });
|
|
7008
|
+
if (dr !== null) {
|
|
7009
|
+
if (dr.ok) {
|
|
7010
|
+
if (flags.json)
|
|
7011
|
+
console.log(JSON.stringify({ ok: true, messageId: dr.messageId, target: to, via: "daemon", duplicate: !!dr.duplicate }));
|
|
7012
|
+
else
|
|
7013
|
+
render.ok(`sent to ${to} (daemon)`, dr.messageId ? dim(dr.messageId.slice(0, 8)) : undefined);
|
|
7014
|
+
return;
|
|
7015
|
+
}
|
|
7016
|
+
if (flags.json)
|
|
7017
|
+
console.log(JSON.stringify({ ok: false, error: dr.error, via: "daemon" }));
|
|
7018
|
+
else
|
|
7019
|
+
render.err(`send failed (daemon): ${dr.error}`);
|
|
7020
|
+
process.exit(1);
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
6858
7023
|
if (meshSlug) {
|
|
6859
7024
|
const bridged = await tryBridge(meshSlug, "send", { to, message, priority });
|
|
6860
7025
|
if (bridged !== null) {
|
|
@@ -6918,6 +7083,7 @@ var init_send = __esm(() => {
|
|
|
6918
7083
|
init_connect();
|
|
6919
7084
|
init_facade();
|
|
6920
7085
|
init_client3();
|
|
7086
|
+
init_daemon_route();
|
|
6921
7087
|
init_render();
|
|
6922
7088
|
init_styles();
|
|
6923
7089
|
});
|
|
@@ -8001,180 +8167,2331 @@ async function runRemind(flags, positional) {
|
|
|
8001
8167
|
console.log(JSON.stringify(scheduled, null, 2));
|
|
8002
8168
|
return;
|
|
8003
8169
|
}
|
|
8004
|
-
if (scheduled.length === 0) {
|
|
8005
|
-
render.info(dim("No pending reminders."));
|
|
8006
|
-
return;
|
|
8170
|
+
if (scheduled.length === 0) {
|
|
8171
|
+
render.info(dim("No pending reminders."));
|
|
8172
|
+
return;
|
|
8173
|
+
}
|
|
8174
|
+
render.section(`reminders (${scheduled.length})`);
|
|
8175
|
+
for (const m of scheduled) {
|
|
8176
|
+
const when = new Date(m.deliverAt).toLocaleString();
|
|
8177
|
+
const to = m.to === client.getSessionPubkey() ? dim("(self)") : m.to;
|
|
8178
|
+
process.stdout.write(` ${bold(m.id.slice(0, 8))} ${dim("→")} ${to} ${dim("at")} ${when}
|
|
8179
|
+
`);
|
|
8180
|
+
process.stdout.write(` ${dim(m.message.slice(0, 80))}
|
|
8181
|
+
|
|
8182
|
+
`);
|
|
8183
|
+
}
|
|
8184
|
+
});
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
8187
|
+
if (action === "cancel") {
|
|
8188
|
+
const id = positional[1];
|
|
8189
|
+
if (!id) {
|
|
8190
|
+
render.err("Usage: claudemesh remind cancel <id>");
|
|
8191
|
+
process.exit(1);
|
|
8192
|
+
}
|
|
8193
|
+
await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
|
|
8194
|
+
const ok = await client.cancelScheduled(id);
|
|
8195
|
+
if (ok)
|
|
8196
|
+
render.ok(`cancelled ${bold(id.slice(0, 8))}`);
|
|
8197
|
+
else {
|
|
8198
|
+
render.err(`not found or already fired: ${id}`);
|
|
8199
|
+
process.exit(1);
|
|
8200
|
+
}
|
|
8201
|
+
});
|
|
8202
|
+
return;
|
|
8203
|
+
}
|
|
8204
|
+
const message = action ?? positional.join(" ");
|
|
8205
|
+
if (!message) {
|
|
8206
|
+
render.err("Usage: claudemesh remind <message> --in <duration>");
|
|
8207
|
+
render.info(dim(" claudemesh remind <message> --at <time>"));
|
|
8208
|
+
render.info(dim(' claudemesh remind <message> --cron "0 */2 * * *"'));
|
|
8209
|
+
render.info(dim(" claudemesh remind list"));
|
|
8210
|
+
render.info(dim(" claudemesh remind cancel <id>"));
|
|
8211
|
+
process.exit(1);
|
|
8212
|
+
}
|
|
8213
|
+
const isCron = !!flags.cron;
|
|
8214
|
+
const deliverAt = isCron ? 0 : parseDeliverAt(flags);
|
|
8215
|
+
if (!isCron && deliverAt === null) {
|
|
8216
|
+
render.err("Specify when", 'use --in <duration> (e.g. "2h", "30m"), --at <time> (e.g. "15:00"), or --cron <expression>');
|
|
8217
|
+
process.exit(1);
|
|
8218
|
+
}
|
|
8219
|
+
await withMesh({ meshSlug: flags.mesh ?? null }, async (client) => {
|
|
8220
|
+
let targetSpec;
|
|
8221
|
+
if (flags.to && flags.to !== "self") {
|
|
8222
|
+
if (flags.to.startsWith("@") || flags.to === "*" || /^[0-9a-f]{64}$/i.test(flags.to)) {
|
|
8223
|
+
targetSpec = flags.to;
|
|
8224
|
+
} else {
|
|
8225
|
+
const peers = await client.listPeers();
|
|
8226
|
+
const match = peers.find((p) => p.displayName.toLowerCase() === flags.to.toLowerCase());
|
|
8227
|
+
if (!match) {
|
|
8228
|
+
render.err(`Peer "${flags.to}" not found`, `online: ${peers.map((p) => p.displayName).join(", ") || "(none)"}`);
|
|
8229
|
+
process.exit(1);
|
|
8230
|
+
}
|
|
8231
|
+
targetSpec = match.pubkey;
|
|
8232
|
+
}
|
|
8233
|
+
} else {
|
|
8234
|
+
targetSpec = client.getSessionPubkey() ?? "*";
|
|
8235
|
+
}
|
|
8236
|
+
const result = await client.scheduleMessage(targetSpec, message, deliverAt ?? 0, false, flags.cron);
|
|
8237
|
+
if (!result) {
|
|
8238
|
+
render.err("Broker did not acknowledge — check connection");
|
|
8239
|
+
process.exit(1);
|
|
8240
|
+
}
|
|
8241
|
+
if (flags.json) {
|
|
8242
|
+
console.log(JSON.stringify(result));
|
|
8243
|
+
return;
|
|
8244
|
+
}
|
|
8245
|
+
const toLabel = !flags.to || flags.to === "self" ? "yourself" : flags.to;
|
|
8246
|
+
if (isCron) {
|
|
8247
|
+
const nextFire = new Date(result.deliverAt).toLocaleString();
|
|
8248
|
+
render.ok(`recurring reminder set`, `${result.scheduledId.slice(0, 8)} · ${clay(message)} → ${toLabel} · cron ${flags.cron} · next ${nextFire}`);
|
|
8249
|
+
} else {
|
|
8250
|
+
const when = new Date(result.deliverAt).toLocaleString();
|
|
8251
|
+
render.ok(`reminder set`, `${result.scheduledId.slice(0, 8)} · ${clay(message)} → ${toLabel} at ${when}`);
|
|
8252
|
+
}
|
|
8253
|
+
});
|
|
8254
|
+
}
|
|
8255
|
+
var init_remind = __esm(() => {
|
|
8256
|
+
init_connect();
|
|
8257
|
+
init_render();
|
|
8258
|
+
init_styles();
|
|
8259
|
+
});
|
|
8260
|
+
|
|
8261
|
+
// src/commands/register.ts
|
|
8262
|
+
var exports_register = {};
|
|
8263
|
+
__export(exports_register, {
|
|
8264
|
+
register: () => register2
|
|
8265
|
+
});
|
|
8266
|
+
async function register2() {
|
|
8267
|
+
return login();
|
|
8268
|
+
}
|
|
8269
|
+
var init_register = __esm(() => {
|
|
8270
|
+
init_login();
|
|
8271
|
+
});
|
|
8272
|
+
|
|
8273
|
+
// src/commands/logout.ts
|
|
8274
|
+
var exports_logout = {};
|
|
8275
|
+
__export(exports_logout, {
|
|
8276
|
+
logout: () => logout2
|
|
8277
|
+
});
|
|
8278
|
+
async function logout2() {
|
|
8279
|
+
try {
|
|
8280
|
+
const { revoked } = await logout();
|
|
8281
|
+
if (revoked) {
|
|
8282
|
+
console.log(` ${green(icons.check)} Revoked session on claudemesh.com`);
|
|
8283
|
+
} else {
|
|
8284
|
+
console.log(` ${yellow(icons.warn)} Could not revoke session on claudemesh.com.`);
|
|
8285
|
+
console.log(` Revoke manually at https://claudemesh.com/dashboard/settings/sessions`);
|
|
8286
|
+
}
|
|
8287
|
+
console.log(` ${green(icons.check)} Removed local credentials.`);
|
|
8288
|
+
return EXIT.SUCCESS;
|
|
8289
|
+
} catch (err) {
|
|
8290
|
+
console.error(` ${icons.cross} Logout failed: ${err instanceof Error ? err.message : err}`);
|
|
8291
|
+
return EXIT.AUTH_FAILED;
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
var init_logout = __esm(() => {
|
|
8295
|
+
init_facade6();
|
|
8296
|
+
init_styles();
|
|
8297
|
+
init_exit_codes();
|
|
8298
|
+
});
|
|
8299
|
+
|
|
8300
|
+
// src/commands/whoami.ts
|
|
8301
|
+
var exports_whoami = {};
|
|
8302
|
+
__export(exports_whoami, {
|
|
8303
|
+
whoami: () => whoami
|
|
8304
|
+
});
|
|
8305
|
+
async function whoami(opts) {
|
|
8306
|
+
const result = await whoAmI();
|
|
8307
|
+
if (opts.json) {
|
|
8308
|
+
console.log(JSON.stringify({ schema_version: "1.0", ...result }, null, 2));
|
|
8309
|
+
return result.signed_in || result.local ? EXIT.SUCCESS : EXIT.AUTH_FAILED;
|
|
8310
|
+
}
|
|
8311
|
+
if (!result.signed_in && !result.local) {
|
|
8312
|
+
render.err("Not signed in", "Run `claudemesh login` to sign in or `claudemesh <invite>` to join.");
|
|
8313
|
+
return EXIT.AUTH_FAILED;
|
|
8314
|
+
}
|
|
8315
|
+
render.section("whoami");
|
|
8316
|
+
if (result.signed_in) {
|
|
8317
|
+
render.kv([
|
|
8318
|
+
["user", `${bold(result.user.display_name)} ${dim(`(${result.user.email})`)}`],
|
|
8319
|
+
["token", `${result.token_source} ${dim("(~/.claudemesh/auth.json)")}`],
|
|
8320
|
+
...result.meshes ? [["meshes", `${result.meshes.owned} owned · ${result.meshes.guest} guest`]] : []
|
|
8321
|
+
]);
|
|
8322
|
+
} else {
|
|
8323
|
+
render.kv([
|
|
8324
|
+
["web", dim("not signed in · run `claudemesh login` for account features")]
|
|
8325
|
+
]);
|
|
8326
|
+
}
|
|
8327
|
+
if (result.local) {
|
|
8328
|
+
render.blank();
|
|
8329
|
+
render.kv([
|
|
8330
|
+
["local", `${result.local.meshes.length} mesh${result.local.meshes.length === 1 ? "" : "es"} · ${dim(result.local.config_path)}`]
|
|
8331
|
+
]);
|
|
8332
|
+
for (const m of result.local.meshes) {
|
|
8333
|
+
console.log(` ${clay("●")} ${bold(m.slug)} ${dim(`member ${m.member_id.slice(0, 8)}… pk ${m.pubkey_prefix}…`)}`);
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
render.blank();
|
|
8337
|
+
return EXIT.SUCCESS;
|
|
8338
|
+
}
|
|
8339
|
+
var init_whoami = __esm(() => {
|
|
8340
|
+
init_facade6();
|
|
8341
|
+
init_render();
|
|
8342
|
+
init_styles();
|
|
8343
|
+
init_exit_codes();
|
|
8344
|
+
});
|
|
8345
|
+
|
|
8346
|
+
// src/daemon/lock.ts
|
|
8347
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8348
|
+
import { dirname as dirname4 } from "node:path";
|
|
8349
|
+
function acquireSingletonLock() {
|
|
8350
|
+
mkdirSync5(dirname4(DAEMON_PATHS.PID_FILE), { recursive: true, mode: 448 });
|
|
8351
|
+
if (existsSync8(DAEMON_PATHS.PID_FILE)) {
|
|
8352
|
+
const raw = readFileSync6(DAEMON_PATHS.PID_FILE, "utf8").trim();
|
|
8353
|
+
const oldPid = Number.parseInt(raw, 10);
|
|
8354
|
+
if (Number.isFinite(oldPid) && oldPid > 0 && isProcessAlive(oldPid)) {
|
|
8355
|
+
return { result: "already-running", pid: oldPid };
|
|
8356
|
+
}
|
|
8357
|
+
try {
|
|
8358
|
+
unlinkSync2(DAEMON_PATHS.PID_FILE);
|
|
8359
|
+
} catch {}
|
|
8360
|
+
writeFileSync7(DAEMON_PATHS.PID_FILE, String(process.pid), { mode: 384 });
|
|
8361
|
+
return { result: "stale", pid: process.pid };
|
|
8362
|
+
}
|
|
8363
|
+
writeFileSync7(DAEMON_PATHS.PID_FILE, String(process.pid), { mode: 384 });
|
|
8364
|
+
return { result: "acquired", pid: process.pid };
|
|
8365
|
+
}
|
|
8366
|
+
function releaseSingletonLock() {
|
|
8367
|
+
try {
|
|
8368
|
+
const raw = readFileSync6(DAEMON_PATHS.PID_FILE, "utf8").trim();
|
|
8369
|
+
if (Number.parseInt(raw, 10) === process.pid)
|
|
8370
|
+
unlinkSync2(DAEMON_PATHS.PID_FILE);
|
|
8371
|
+
} catch {}
|
|
8372
|
+
}
|
|
8373
|
+
function readRunningPid() {
|
|
8374
|
+
try {
|
|
8375
|
+
const raw = readFileSync6(DAEMON_PATHS.PID_FILE, "utf8").trim();
|
|
8376
|
+
const pid = Number.parseInt(raw, 10);
|
|
8377
|
+
if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid))
|
|
8378
|
+
return pid;
|
|
8379
|
+
} catch {}
|
|
8380
|
+
return null;
|
|
8381
|
+
}
|
|
8382
|
+
function isProcessAlive(pid) {
|
|
8383
|
+
try {
|
|
8384
|
+
process.kill(pid, 0);
|
|
8385
|
+
return true;
|
|
8386
|
+
} catch (err) {
|
|
8387
|
+
return err.code === "EPERM";
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
var init_lock = __esm(() => {
|
|
8391
|
+
init_paths2();
|
|
8392
|
+
});
|
|
8393
|
+
|
|
8394
|
+
// src/daemon/db/outbox.ts
|
|
8395
|
+
function migrateOutbox(db) {
|
|
8396
|
+
db.exec(`
|
|
8397
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
8398
|
+
id TEXT PRIMARY KEY,
|
|
8399
|
+
client_message_id TEXT NOT NULL UNIQUE,
|
|
8400
|
+
request_fingerprint BLOB NOT NULL,
|
|
8401
|
+
payload BLOB NOT NULL,
|
|
8402
|
+
enqueued_at INTEGER NOT NULL,
|
|
8403
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
8404
|
+
next_attempt_at INTEGER NOT NULL,
|
|
8405
|
+
status TEXT NOT NULL CHECK(status IN
|
|
8406
|
+
('pending','inflight','done','dead','aborted')),
|
|
8407
|
+
last_error TEXT,
|
|
8408
|
+
delivered_at INTEGER,
|
|
8409
|
+
broker_message_id TEXT,
|
|
8410
|
+
aborted_at INTEGER,
|
|
8411
|
+
aborted_by TEXT,
|
|
8412
|
+
superseded_by TEXT
|
|
8413
|
+
);
|
|
8414
|
+
CREATE INDEX IF NOT EXISTS outbox_pending
|
|
8415
|
+
ON outbox(status, next_attempt_at);
|
|
8416
|
+
CREATE INDEX IF NOT EXISTS outbox_aborted
|
|
8417
|
+
ON outbox(status, aborted_at) WHERE status = 'aborted';
|
|
8418
|
+
`);
|
|
8419
|
+
}
|
|
8420
|
+
function findByClientId(db, clientMessageId) {
|
|
8421
|
+
const row = db.prepare(`
|
|
8422
|
+
SELECT id, client_message_id, request_fingerprint, payload, enqueued_at,
|
|
8423
|
+
attempts, next_attempt_at, status, last_error, delivered_at,
|
|
8424
|
+
broker_message_id, aborted_at, aborted_by, superseded_by
|
|
8425
|
+
FROM outbox WHERE client_message_id = ?
|
|
8426
|
+
`).get(clientMessageId);
|
|
8427
|
+
return row ?? null;
|
|
8428
|
+
}
|
|
8429
|
+
function insertPending(db, input) {
|
|
8430
|
+
db.prepare(`
|
|
8431
|
+
INSERT INTO outbox (
|
|
8432
|
+
id, client_message_id, request_fingerprint, payload,
|
|
8433
|
+
enqueued_at, attempts, next_attempt_at, status
|
|
8434
|
+
) VALUES (?, ?, ?, ?, ?, 0, ?, 'pending')
|
|
8435
|
+
`).run(input.id, input.client_message_id, input.request_fingerprint, input.payload, input.now, input.now);
|
|
8436
|
+
}
|
|
8437
|
+
function fingerprintsEqual(a, b) {
|
|
8438
|
+
if (a.length !== b.length)
|
|
8439
|
+
return false;
|
|
8440
|
+
let diff = 0;
|
|
8441
|
+
for (let i = 0;i < a.length; i++)
|
|
8442
|
+
diff |= a[i] ^ b[i];
|
|
8443
|
+
return diff === 0;
|
|
8444
|
+
}
|
|
8445
|
+
function listOutbox(db, p = {}) {
|
|
8446
|
+
const where = [];
|
|
8447
|
+
const args = [];
|
|
8448
|
+
if (p.status) {
|
|
8449
|
+
where.push("status = ?");
|
|
8450
|
+
args.push(p.status);
|
|
8451
|
+
}
|
|
8452
|
+
const sql = `
|
|
8453
|
+
SELECT id, client_message_id, request_fingerprint, payload, enqueued_at,
|
|
8454
|
+
attempts, next_attempt_at, status, last_error, delivered_at,
|
|
8455
|
+
broker_message_id, aborted_at, aborted_by, superseded_by
|
|
8456
|
+
FROM outbox
|
|
8457
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
8458
|
+
ORDER BY enqueued_at DESC
|
|
8459
|
+
LIMIT ?
|
|
8460
|
+
`;
|
|
8461
|
+
args.push(Math.min(Math.max(p.limit ?? 50, 1), 500));
|
|
8462
|
+
return db.prepare(sql).all(...args);
|
|
8463
|
+
}
|
|
8464
|
+
function findById(db, id) {
|
|
8465
|
+
return db.prepare(`
|
|
8466
|
+
SELECT id, client_message_id, request_fingerprint, payload, enqueued_at,
|
|
8467
|
+
attempts, next_attempt_at, status, last_error, delivered_at,
|
|
8468
|
+
broker_message_id, aborted_at, aborted_by, superseded_by
|
|
8469
|
+
FROM outbox WHERE id = ?
|
|
8470
|
+
`).get(id) ?? null;
|
|
8471
|
+
}
|
|
8472
|
+
function requeueDeadOrPending(db, args) {
|
|
8473
|
+
const existing = findById(db, args.id);
|
|
8474
|
+
if (!existing)
|
|
8475
|
+
return null;
|
|
8476
|
+
if (existing.status === "aborted" || existing.status === "done")
|
|
8477
|
+
return null;
|
|
8478
|
+
db.prepare(`
|
|
8479
|
+
UPDATE outbox
|
|
8480
|
+
SET status = 'aborted', aborted_at = ?, aborted_by = ?, superseded_by = ?
|
|
8481
|
+
WHERE id = ? AND status IN ('pending','inflight','dead')
|
|
8482
|
+
`).run(args.now, args.abortedBy, args.newRowId, args.id);
|
|
8483
|
+
db.prepare(`
|
|
8484
|
+
INSERT INTO outbox (
|
|
8485
|
+
id, client_message_id, request_fingerprint, payload,
|
|
8486
|
+
enqueued_at, attempts, next_attempt_at, status
|
|
8487
|
+
) VALUES (?, ?, ?, ?, ?, 0, ?, 'pending')
|
|
8488
|
+
`).run(args.newRowId, args.newClientMessageId, existing.request_fingerprint, existing.payload, args.now, args.now);
|
|
8489
|
+
return {
|
|
8490
|
+
abortedRowId: existing.id,
|
|
8491
|
+
newRowId: args.newRowId,
|
|
8492
|
+
newClientMessageId: args.newClientMessageId
|
|
8493
|
+
};
|
|
8494
|
+
}
|
|
8495
|
+
|
|
8496
|
+
// src/daemon/db/sqlite.ts
|
|
8497
|
+
async function loadSqlite() {
|
|
8498
|
+
if (cached)
|
|
8499
|
+
return cached;
|
|
8500
|
+
try {
|
|
8501
|
+
const mod = await import("node:sqlite");
|
|
8502
|
+
cached = mod.DatabaseSync;
|
|
8503
|
+
return cached;
|
|
8504
|
+
} catch (nodeErr) {
|
|
8505
|
+
try {
|
|
8506
|
+
const bunMod = await import("bun:sqlite");
|
|
8507
|
+
cached = bunMod.Database;
|
|
8508
|
+
return cached;
|
|
8509
|
+
} catch {
|
|
8510
|
+
const msg = `claudemesh daemon requires Node.js 22.5+ for the embedded SQLite store (node:sqlite), or Bun (bun:sqlite) for dev. Current: ${process.version}. Original error: ${String(nodeErr)}`;
|
|
8511
|
+
throw new Error(msg);
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
async function openSqlite(path) {
|
|
8516
|
+
const Database = await loadSqlite();
|
|
8517
|
+
const db = new Database(path);
|
|
8518
|
+
db.exec(`
|
|
8519
|
+
PRAGMA journal_mode = WAL;
|
|
8520
|
+
PRAGMA synchronous = NORMAL;
|
|
8521
|
+
PRAGMA foreign_keys = ON;
|
|
8522
|
+
PRAGMA busy_timeout = 5000;
|
|
8523
|
+
`);
|
|
8524
|
+
return db;
|
|
8525
|
+
}
|
|
8526
|
+
function inImmediateTx(db, fn) {
|
|
8527
|
+
db.exec("BEGIN IMMEDIATE");
|
|
8528
|
+
try {
|
|
8529
|
+
const out = fn();
|
|
8530
|
+
db.exec("COMMIT");
|
|
8531
|
+
return out;
|
|
8532
|
+
} catch (err) {
|
|
8533
|
+
try {
|
|
8534
|
+
db.exec("ROLLBACK");
|
|
8535
|
+
} catch {}
|
|
8536
|
+
throw err;
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8539
|
+
var cached = null;
|
|
8540
|
+
|
|
8541
|
+
// src/daemon/fingerprint.ts
|
|
8542
|
+
import { createHash } from "node:crypto";
|
|
8543
|
+
function computeRequestFingerprint(req) {
|
|
8544
|
+
const h = createHash("sha256");
|
|
8545
|
+
h.update(String(req.envelope_version), "utf8");
|
|
8546
|
+
h.update(NUL);
|
|
8547
|
+
h.update(req.destination_kind, "utf8");
|
|
8548
|
+
h.update(NUL);
|
|
8549
|
+
h.update(req.destination_ref, "utf8");
|
|
8550
|
+
h.update(NUL);
|
|
8551
|
+
h.update(req.reply_to_id ?? "", "utf8");
|
|
8552
|
+
h.update(NUL);
|
|
8553
|
+
h.update(req.priority, "utf8");
|
|
8554
|
+
h.update(NUL);
|
|
8555
|
+
h.update(req.meta ? canonicalJson(req.meta) : "", "utf8");
|
|
8556
|
+
h.update(NUL);
|
|
8557
|
+
h.update(createHash("sha256").update(req.body).digest());
|
|
8558
|
+
return h.digest();
|
|
8559
|
+
}
|
|
8560
|
+
function canonicalJson(value) {
|
|
8561
|
+
return JSON.stringify(sortKeys(value));
|
|
8562
|
+
}
|
|
8563
|
+
function sortKeys(value) {
|
|
8564
|
+
if (Array.isArray(value))
|
|
8565
|
+
return value.map(sortKeys);
|
|
8566
|
+
if (value !== null && typeof value === "object") {
|
|
8567
|
+
const obj = value;
|
|
8568
|
+
const out = {};
|
|
8569
|
+
for (const k of Object.keys(obj).sort())
|
|
8570
|
+
out[k] = sortKeys(obj[k]);
|
|
8571
|
+
return out;
|
|
8572
|
+
}
|
|
8573
|
+
return value;
|
|
8574
|
+
}
|
|
8575
|
+
function fingerprintHexPrefix(fp, bytes = 8) {
|
|
8576
|
+
let s = "";
|
|
8577
|
+
for (let i = 0;i < bytes && i < fp.length; i++) {
|
|
8578
|
+
s += fp[i].toString(16).padStart(2, "0");
|
|
8579
|
+
}
|
|
8580
|
+
return s;
|
|
8581
|
+
}
|
|
8582
|
+
var NUL;
|
|
8583
|
+
var init_fingerprint = __esm(() => {
|
|
8584
|
+
NUL = Buffer.from([0]);
|
|
8585
|
+
});
|
|
8586
|
+
|
|
8587
|
+
// src/daemon/ipc/handlers/send.ts
|
|
8588
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8589
|
+
function acceptSend(req, deps) {
|
|
8590
|
+
const now = (deps.now ?? Date.now)();
|
|
8591
|
+
const newId = deps.newId ?? randomUUID3;
|
|
8592
|
+
const clientId = req.client_message_id?.trim() || ulidLike(newId);
|
|
8593
|
+
const body = Buffer.from(req.message, "utf8");
|
|
8594
|
+
const fingerprint = computeRequestFingerprint({
|
|
8595
|
+
envelope_version: ENVELOPE_VERSION,
|
|
8596
|
+
destination_kind: req.destination_kind,
|
|
8597
|
+
destination_ref: req.destination_ref,
|
|
8598
|
+
reply_to_id: req.reply_to_id ?? null,
|
|
8599
|
+
priority: req.priority ?? "next",
|
|
8600
|
+
meta: req.meta ?? null,
|
|
8601
|
+
body
|
|
8602
|
+
});
|
|
8603
|
+
return inImmediateTx(deps.db, () => {
|
|
8604
|
+
const existing = findByClientId(deps.db, clientId);
|
|
8605
|
+
if (!existing) {
|
|
8606
|
+
insertPending(deps.db, {
|
|
8607
|
+
id: newId(),
|
|
8608
|
+
client_message_id: clientId,
|
|
8609
|
+
request_fingerprint: fingerprint,
|
|
8610
|
+
payload: body,
|
|
8611
|
+
now
|
|
8612
|
+
});
|
|
8613
|
+
return { kind: "accepted_pending", status: 202, client_message_id: clientId };
|
|
8614
|
+
}
|
|
8615
|
+
return decideForExistingRow(existing, fingerprint);
|
|
8616
|
+
});
|
|
8617
|
+
}
|
|
8618
|
+
function decideForExistingRow(row, fp) {
|
|
8619
|
+
const match = fingerprintsEqual(fp, row.request_fingerprint);
|
|
8620
|
+
const fpPrefix = fingerprintHexPrefix(fp);
|
|
8621
|
+
switch (row.status) {
|
|
8622
|
+
case "pending":
|
|
8623
|
+
return match ? { kind: "accepted_pending", status: 202, client_message_id: row.client_message_id } : conflict("outbox_pending_fingerprint_mismatch", fpPrefix);
|
|
8624
|
+
case "inflight":
|
|
8625
|
+
return match ? { kind: "accepted_inflight", status: 202, client_message_id: row.client_message_id } : conflict("outbox_inflight_fingerprint_mismatch", fpPrefix);
|
|
8626
|
+
case "done":
|
|
8627
|
+
return match ? {
|
|
8628
|
+
kind: "accepted_done",
|
|
8629
|
+
status: 200,
|
|
8630
|
+
client_message_id: row.client_message_id,
|
|
8631
|
+
broker_message_id: row.broker_message_id
|
|
8632
|
+
} : conflict("outbox_done_fingerprint_mismatch", fpPrefix, row.broker_message_id);
|
|
8633
|
+
case "dead":
|
|
8634
|
+
return match ? conflict("outbox_dead_fingerprint_match", fpPrefix, row.broker_message_id) : conflict("outbox_dead_fingerprint_mismatch", fpPrefix);
|
|
8635
|
+
case "aborted":
|
|
8636
|
+
return match ? conflict("outbox_aborted_fingerprint_match", fpPrefix) : conflict("outbox_aborted_fingerprint_mismatch", fpPrefix);
|
|
8637
|
+
default: {
|
|
8638
|
+
const _ = row.status;
|
|
8639
|
+
throw new Error(`unknown outbox status: ${String(_)}`);
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
function conflict(reason, fpPrefix, brokerMessageId = null) {
|
|
8644
|
+
return {
|
|
8645
|
+
kind: "conflict",
|
|
8646
|
+
status: 409,
|
|
8647
|
+
reason,
|
|
8648
|
+
daemon_fingerprint_prefix: fpPrefix,
|
|
8649
|
+
broker_message_id: brokerMessageId
|
|
8650
|
+
};
|
|
8651
|
+
}
|
|
8652
|
+
function ulidLike(newId) {
|
|
8653
|
+
return newId();
|
|
8654
|
+
}
|
|
8655
|
+
var ENVELOPE_VERSION = 1;
|
|
8656
|
+
var init_send2 = __esm(() => {
|
|
8657
|
+
init_fingerprint();
|
|
8658
|
+
});
|
|
8659
|
+
|
|
8660
|
+
// src/daemon/db/inbox.ts
|
|
8661
|
+
function migrateInbox(db) {
|
|
8662
|
+
db.exec(`
|
|
8663
|
+
CREATE TABLE IF NOT EXISTS inbox (
|
|
8664
|
+
id TEXT PRIMARY KEY,
|
|
8665
|
+
client_message_id TEXT NOT NULL UNIQUE,
|
|
8666
|
+
broker_message_id TEXT,
|
|
8667
|
+
mesh TEXT NOT NULL,
|
|
8668
|
+
topic TEXT,
|
|
8669
|
+
sender_pubkey TEXT NOT NULL,
|
|
8670
|
+
sender_name TEXT NOT NULL,
|
|
8671
|
+
body TEXT,
|
|
8672
|
+
meta TEXT,
|
|
8673
|
+
received_at INTEGER NOT NULL,
|
|
8674
|
+
reply_to_id TEXT
|
|
8675
|
+
);
|
|
8676
|
+
CREATE INDEX IF NOT EXISTS inbox_received_at ON inbox(received_at);
|
|
8677
|
+
CREATE INDEX IF NOT EXISTS inbox_topic ON inbox(topic);
|
|
8678
|
+
CREATE INDEX IF NOT EXISTS inbox_sender ON inbox(sender_pubkey);
|
|
8679
|
+
`);
|
|
8680
|
+
}
|
|
8681
|
+
function insertIfNew(db, row) {
|
|
8682
|
+
const before = db.prepare(`SELECT id FROM inbox WHERE client_message_id = ?`).get(row.client_message_id);
|
|
8683
|
+
if (before)
|
|
8684
|
+
return null;
|
|
8685
|
+
db.prepare(`
|
|
8686
|
+
INSERT INTO inbox (
|
|
8687
|
+
id, client_message_id, broker_message_id, mesh, topic,
|
|
8688
|
+
sender_pubkey, sender_name, body, meta, received_at, reply_to_id
|
|
8689
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8690
|
+
ON CONFLICT(client_message_id) DO NOTHING
|
|
8691
|
+
`).run(row.id, row.client_message_id, row.broker_message_id, row.mesh, row.topic, row.sender_pubkey, row.sender_name, row.body, row.meta, row.received_at, row.reply_to_id);
|
|
8692
|
+
const after = db.prepare(`SELECT id FROM inbox WHERE client_message_id = ?`).get(row.client_message_id);
|
|
8693
|
+
return after?.id === row.id ? row.id : null;
|
|
8694
|
+
}
|
|
8695
|
+
function listInbox(db, p) {
|
|
8696
|
+
const where = [];
|
|
8697
|
+
const args = [];
|
|
8698
|
+
if (p.since !== undefined) {
|
|
8699
|
+
where.push("received_at >= ?");
|
|
8700
|
+
args.push(p.since);
|
|
8701
|
+
}
|
|
8702
|
+
if (p.topic !== undefined) {
|
|
8703
|
+
where.push("topic = ?");
|
|
8704
|
+
args.push(p.topic);
|
|
8705
|
+
}
|
|
8706
|
+
if (p.fromPubkey !== undefined) {
|
|
8707
|
+
where.push("sender_pubkey = ?");
|
|
8708
|
+
args.push(p.fromPubkey);
|
|
8709
|
+
}
|
|
8710
|
+
const sql = `
|
|
8711
|
+
SELECT id, client_message_id, broker_message_id, mesh, topic,
|
|
8712
|
+
sender_pubkey, sender_name, body, meta, received_at, reply_to_id
|
|
8713
|
+
FROM inbox
|
|
8714
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
8715
|
+
ORDER BY received_at DESC
|
|
8716
|
+
LIMIT ?
|
|
8717
|
+
`;
|
|
8718
|
+
args.push(Math.min(Math.max(p.limit ?? 100, 1), 1000));
|
|
8719
|
+
return db.prepare(sql).all(...args);
|
|
8720
|
+
}
|
|
8721
|
+
|
|
8722
|
+
// src/daemon/events.ts
|
|
8723
|
+
class EventBus {
|
|
8724
|
+
subs = new Set;
|
|
8725
|
+
publish(kind, data) {
|
|
8726
|
+
const e = { kind, ts: new Date().toISOString(), data };
|
|
8727
|
+
for (const s of this.subs) {
|
|
8728
|
+
try {
|
|
8729
|
+
s(e);
|
|
8730
|
+
} catch {}
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
subscribe(fn) {
|
|
8734
|
+
this.subs.add(fn);
|
|
8735
|
+
return () => this.subs.delete(fn);
|
|
8736
|
+
}
|
|
8737
|
+
}
|
|
8738
|
+
function writeSse(res, e, idCounter) {
|
|
8739
|
+
res.write(`id: ${idCounter}
|
|
8740
|
+
`);
|
|
8741
|
+
res.write(`event: ${e.kind}
|
|
8742
|
+
`);
|
|
8743
|
+
res.write(`data: ${JSON.stringify({ ts: e.ts, ...e.data })}
|
|
8744
|
+
|
|
8745
|
+
`);
|
|
8746
|
+
}
|
|
8747
|
+
function bindSseStream(res, bus) {
|
|
8748
|
+
res.statusCode = 200;
|
|
8749
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
8750
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
8751
|
+
res.setHeader("Connection", "keep-alive");
|
|
8752
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
8753
|
+
res.write(`: connected
|
|
8754
|
+
|
|
8755
|
+
`);
|
|
8756
|
+
let counter = 0;
|
|
8757
|
+
const unsubscribe = bus.subscribe((e) => writeSse(res, e, ++counter));
|
|
8758
|
+
const heartbeat = setInterval(() => {
|
|
8759
|
+
try {
|
|
8760
|
+
res.write(`: keepalive
|
|
8761
|
+
|
|
8762
|
+
`);
|
|
8763
|
+
} catch {}
|
|
8764
|
+
}, 15000);
|
|
8765
|
+
const cleanup = () => {
|
|
8766
|
+
clearInterval(heartbeat);
|
|
8767
|
+
unsubscribe();
|
|
8768
|
+
try {
|
|
8769
|
+
res.end();
|
|
8770
|
+
} catch {}
|
|
8771
|
+
};
|
|
8772
|
+
res.on("close", cleanup);
|
|
8773
|
+
res.on("error", cleanup);
|
|
8774
|
+
return cleanup;
|
|
8775
|
+
}
|
|
8776
|
+
|
|
8777
|
+
// src/daemon/ipc/server.ts
|
|
8778
|
+
import { createServer as createServer2 } from "node:http";
|
|
8779
|
+
import { chmodSync as chmodSync3, existsSync as existsSync9, unlinkSync as unlinkSync3 } from "node:fs";
|
|
8780
|
+
import { timingSafeEqual } from "node:crypto";
|
|
8781
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
8782
|
+
function startIpcServer(opts) {
|
|
8783
|
+
const log2 = opts.log ?? defaultLogger;
|
|
8784
|
+
const handler = makeHandler({
|
|
8785
|
+
localToken: opts.localToken,
|
|
8786
|
+
publicHealthCheck: !!opts.publicHealthCheck,
|
|
8787
|
+
log: log2,
|
|
8788
|
+
outboxDb: opts.outboxDb,
|
|
8789
|
+
inboxDb: opts.inboxDb,
|
|
8790
|
+
bus: opts.bus,
|
|
8791
|
+
broker: opts.broker,
|
|
8792
|
+
onPendingInserted: opts.onPendingInserted
|
|
8793
|
+
});
|
|
8794
|
+
if (existsSync9(DAEMON_PATHS.SOCK_FILE)) {
|
|
8795
|
+
try {
|
|
8796
|
+
unlinkSync3(DAEMON_PATHS.SOCK_FILE);
|
|
8797
|
+
} catch {}
|
|
8798
|
+
}
|
|
8799
|
+
const uds = createServer2(handler);
|
|
8800
|
+
const udsReady = new Promise((resolve, reject) => {
|
|
8801
|
+
uds.once("error", reject);
|
|
8802
|
+
uds.listen(DAEMON_PATHS.SOCK_FILE, () => {
|
|
8803
|
+
try {
|
|
8804
|
+
chmodSync3(DAEMON_PATHS.SOCK_FILE, 384);
|
|
8805
|
+
} catch (err) {
|
|
8806
|
+
log2("warn", "uds_chmod_failed", { err: String(err) });
|
|
8807
|
+
}
|
|
8808
|
+
resolve();
|
|
8809
|
+
});
|
|
8810
|
+
});
|
|
8811
|
+
let tcp = null;
|
|
8812
|
+
let tcpReady = Promise.resolve();
|
|
8813
|
+
if (opts.tcpEnabled !== false) {
|
|
8814
|
+
tcp = createServer2(handler);
|
|
8815
|
+
tcpReady = new Promise((resolve, reject) => {
|
|
8816
|
+
tcp.once("error", reject);
|
|
8817
|
+
tcp.listen(opts.tcpPort ?? DAEMON_TCP_DEFAULT_PORT, DAEMON_TCP_HOST, () => resolve());
|
|
8818
|
+
});
|
|
8819
|
+
}
|
|
8820
|
+
return {
|
|
8821
|
+
uds,
|
|
8822
|
+
tcp,
|
|
8823
|
+
ready: Promise.all([udsReady, tcpReady]).then(() => {
|
|
8824
|
+
return;
|
|
8825
|
+
}),
|
|
8826
|
+
close: async () => {
|
|
8827
|
+
await Promise.allSettled([
|
|
8828
|
+
new Promise((res) => uds.close(() => res())),
|
|
8829
|
+
tcp ? new Promise((res) => tcp.close(() => res())) : Promise.resolve()
|
|
8830
|
+
]);
|
|
8831
|
+
try {
|
|
8832
|
+
unlinkSync3(DAEMON_PATHS.SOCK_FILE);
|
|
8833
|
+
} catch {}
|
|
8834
|
+
}
|
|
8835
|
+
};
|
|
8836
|
+
}
|
|
8837
|
+
function defaultLogger(level, msg, meta) {
|
|
8838
|
+
const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
|
|
8839
|
+
if (level === "info")
|
|
8840
|
+
process.stdout.write(line + `
|
|
8841
|
+
`);
|
|
8842
|
+
else
|
|
8843
|
+
process.stderr.write(line + `
|
|
8844
|
+
`);
|
|
8845
|
+
}
|
|
8846
|
+
function makeHandler(opts) {
|
|
8847
|
+
const tokenBytes = Buffer.from(opts.localToken, "utf8");
|
|
8848
|
+
return async (req, res) => {
|
|
8849
|
+
const url = new URL(req.url ?? "/", "http://daemon.local");
|
|
8850
|
+
if (url.searchParams.has("token")) {
|
|
8851
|
+
opts.log("warn", "ipc_token_in_query_string_rejected", { path: url.pathname });
|
|
8852
|
+
respond(res, 400, { error: "token must be in Authorization header, not query string" });
|
|
8853
|
+
return;
|
|
8854
|
+
}
|
|
8855
|
+
const host = (req.headers.host ?? "").toLowerCase().split(":")[0]?.trim() ?? "";
|
|
8856
|
+
if (host && host !== "localhost" && host !== "127.0.0.1" && host !== "[::1]" && host !== "::1") {
|
|
8857
|
+
respond(res, 403, { error: "forbidden host" });
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
if (req.headers.origin) {
|
|
8861
|
+
respond(res, 403, { error: "forbidden origin" });
|
|
8862
|
+
return;
|
|
8863
|
+
}
|
|
8864
|
+
const isUds = req.socket.remoteAddress === undefined;
|
|
8865
|
+
const isPublicHealth = opts.publicHealthCheck && url.pathname === "/v1/health";
|
|
8866
|
+
if (!isUds && !isPublicHealth) {
|
|
8867
|
+
const authz = req.headers.authorization ?? "";
|
|
8868
|
+
const m = /^Bearer\s+(.+)$/.exec(authz.trim());
|
|
8869
|
+
if (!m || !m[1]) {
|
|
8870
|
+
respond(res, 401, { error: "missing bearer token" });
|
|
8871
|
+
return;
|
|
8872
|
+
}
|
|
8873
|
+
const provided = Buffer.from(m[1], "utf8");
|
|
8874
|
+
if (provided.length !== tokenBytes.length || !timingSafeEqual(provided, tokenBytes)) {
|
|
8875
|
+
opts.log("warn", "ipc_bearer_mismatch", { path: url.pathname });
|
|
8876
|
+
respond(res, 401, { error: "invalid bearer token" });
|
|
8877
|
+
return;
|
|
8878
|
+
}
|
|
8879
|
+
}
|
|
8880
|
+
if (req.method === "GET" && url.pathname === "/v1/version") {
|
|
8881
|
+
respond(res, 200, {
|
|
8882
|
+
daemon_version: VERSION,
|
|
8883
|
+
ipc_api: "v1",
|
|
8884
|
+
ipc_features: ["version", "health", "send", "inbox", "events", "peers", "profile"],
|
|
8885
|
+
schema_version: 1
|
|
8886
|
+
});
|
|
8887
|
+
return;
|
|
8888
|
+
}
|
|
8889
|
+
if (req.method === "GET" && url.pathname === "/v1/health") {
|
|
8890
|
+
respond(res, 200, { ok: true, pid: process.pid });
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
if (req.method === "GET" && url.pathname === "/v1/events") {
|
|
8894
|
+
if (!opts.bus) {
|
|
8895
|
+
respond(res, 503, { error: "event bus not initialised" });
|
|
8896
|
+
return;
|
|
8897
|
+
}
|
|
8898
|
+
bindSseStream(res, opts.bus);
|
|
8899
|
+
return;
|
|
8900
|
+
}
|
|
8901
|
+
if (req.method === "GET" && url.pathname === "/v1/peers") {
|
|
8902
|
+
if (!opts.broker) {
|
|
8903
|
+
respond(res, 503, { error: "broker not initialised" });
|
|
8904
|
+
return;
|
|
8905
|
+
}
|
|
8906
|
+
try {
|
|
8907
|
+
const peers = await opts.broker.listPeers();
|
|
8908
|
+
respond(res, 200, { peers });
|
|
8909
|
+
} catch (e) {
|
|
8910
|
+
respond(res, 502, { error: "broker_unreachable", detail: String(e) });
|
|
8911
|
+
}
|
|
8912
|
+
return;
|
|
8913
|
+
}
|
|
8914
|
+
if (req.method === "POST" && url.pathname === "/v1/profile") {
|
|
8915
|
+
if (!opts.broker) {
|
|
8916
|
+
respond(res, 503, { error: "broker not initialised" });
|
|
8917
|
+
return;
|
|
8918
|
+
}
|
|
8919
|
+
try {
|
|
8920
|
+
const body = await readJsonBody(req, 16 * 1024);
|
|
8921
|
+
if (!body) {
|
|
8922
|
+
respond(res, 400, { error: "expected JSON object" });
|
|
8923
|
+
return;
|
|
8924
|
+
}
|
|
8925
|
+
const updates = {};
|
|
8926
|
+
if (typeof body.summary === "string")
|
|
8927
|
+
opts.broker.setSummary(body.summary);
|
|
8928
|
+
if (body.status === "idle" || body.status === "working" || body.status === "dnd")
|
|
8929
|
+
opts.broker.setStatus(body.status);
|
|
8930
|
+
if (typeof body.visible === "boolean")
|
|
8931
|
+
opts.broker.setVisible(body.visible);
|
|
8932
|
+
const profile = {};
|
|
8933
|
+
if (typeof body.avatar === "string")
|
|
8934
|
+
profile.avatar = body.avatar;
|
|
8935
|
+
if (typeof body.title === "string")
|
|
8936
|
+
profile.title = body.title;
|
|
8937
|
+
if (typeof body.bio === "string")
|
|
8938
|
+
profile.bio = body.bio;
|
|
8939
|
+
if (Array.isArray(body.capabilities))
|
|
8940
|
+
profile.capabilities = body.capabilities.filter((c) => typeof c === "string");
|
|
8941
|
+
if (Object.keys(profile).length > 0)
|
|
8942
|
+
opts.broker.setProfile(profile);
|
|
8943
|
+
Object.assign(updates, body);
|
|
8944
|
+
respond(res, 200, { ok: true, applied: Object.keys(updates) });
|
|
8945
|
+
} catch (e) {
|
|
8946
|
+
respond(res, 400, { error: String(e) });
|
|
8947
|
+
}
|
|
8948
|
+
return;
|
|
8949
|
+
}
|
|
8950
|
+
if (req.method === "GET" && url.pathname === "/v1/inbox") {
|
|
8951
|
+
if (!opts.inboxDb) {
|
|
8952
|
+
respond(res, 503, { error: "inbox not initialised" });
|
|
8953
|
+
return;
|
|
8954
|
+
}
|
|
8955
|
+
const sinceRaw = url.searchParams.get("since");
|
|
8956
|
+
const since = sinceRaw ? Date.parse(sinceRaw) : undefined;
|
|
8957
|
+
const topic = url.searchParams.get("topic") ?? undefined;
|
|
8958
|
+
const fromPubkey = url.searchParams.get("from") ?? undefined;
|
|
8959
|
+
const limitRaw = url.searchParams.get("limit");
|
|
8960
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
|
|
8961
|
+
const rows = listInbox(opts.inboxDb, {
|
|
8962
|
+
since: Number.isFinite(since) ? since : undefined,
|
|
8963
|
+
topic,
|
|
8964
|
+
fromPubkey,
|
|
8965
|
+
limit: Number.isFinite(limit ?? NaN) ? limit : undefined
|
|
8966
|
+
});
|
|
8967
|
+
respond(res, 200, {
|
|
8968
|
+
items: rows.map((r) => ({
|
|
8969
|
+
id: r.id,
|
|
8970
|
+
client_message_id: r.client_message_id,
|
|
8971
|
+
broker_message_id: r.broker_message_id,
|
|
8972
|
+
mesh: r.mesh,
|
|
8973
|
+
topic: r.topic,
|
|
8974
|
+
sender_pubkey: r.sender_pubkey,
|
|
8975
|
+
sender_name: r.sender_name,
|
|
8976
|
+
body: r.body,
|
|
8977
|
+
received_at: new Date(r.received_at).toISOString(),
|
|
8978
|
+
reply_to_id: r.reply_to_id
|
|
8979
|
+
}))
|
|
8980
|
+
});
|
|
8981
|
+
return;
|
|
8982
|
+
}
|
|
8983
|
+
if (req.method === "GET" && url.pathname === "/v1/outbox") {
|
|
8984
|
+
if (!opts.outboxDb) {
|
|
8985
|
+
respond(res, 503, { error: "outbox not initialised" });
|
|
8986
|
+
return;
|
|
8987
|
+
}
|
|
8988
|
+
const statusParam = url.searchParams.get("status") ?? undefined;
|
|
8989
|
+
const allowed = ["pending", "inflight", "done", "dead", "aborted"];
|
|
8990
|
+
const status = statusParam && allowed.includes(statusParam) ? statusParam : undefined;
|
|
8991
|
+
const limitRaw = url.searchParams.get("limit");
|
|
8992
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
|
|
8993
|
+
const rows = listOutbox(opts.outboxDb, {
|
|
8994
|
+
status,
|
|
8995
|
+
limit: Number.isFinite(limit ?? NaN) ? limit : undefined
|
|
8996
|
+
});
|
|
8997
|
+
respond(res, 200, {
|
|
8998
|
+
items: rows.map((r) => ({
|
|
8999
|
+
id: r.id,
|
|
9000
|
+
client_message_id: r.client_message_id,
|
|
9001
|
+
status: r.status,
|
|
9002
|
+
attempts: r.attempts,
|
|
9003
|
+
enqueued_at: new Date(r.enqueued_at).toISOString(),
|
|
9004
|
+
next_attempt_at: new Date(r.next_attempt_at).toISOString(),
|
|
9005
|
+
delivered_at: r.delivered_at ? new Date(r.delivered_at).toISOString() : null,
|
|
9006
|
+
broker_message_id: r.broker_message_id,
|
|
9007
|
+
last_error: r.last_error,
|
|
9008
|
+
aborted_at: r.aborted_at ? new Date(r.aborted_at).toISOString() : null,
|
|
9009
|
+
aborted_by: r.aborted_by,
|
|
9010
|
+
superseded_by: r.superseded_by,
|
|
9011
|
+
payload_bytes: r.payload?.byteLength ?? 0
|
|
9012
|
+
}))
|
|
9013
|
+
});
|
|
9014
|
+
return;
|
|
9015
|
+
}
|
|
9016
|
+
if (req.method === "POST" && url.pathname === "/v1/outbox/requeue") {
|
|
9017
|
+
if (!opts.outboxDb) {
|
|
9018
|
+
respond(res, 503, { error: "outbox not initialised" });
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
try {
|
|
9022
|
+
const body = await readJsonBody(req, 4 * 1024);
|
|
9023
|
+
if (!body || typeof body.id !== "string") {
|
|
9024
|
+
respond(res, 400, { error: "missing 'id'" });
|
|
9025
|
+
return;
|
|
9026
|
+
}
|
|
9027
|
+
const newId = typeof body.new_client_message_id === "string" && body.new_client_message_id.trim() ? body.new_client_message_id.trim() : randomUUID4();
|
|
9028
|
+
const result = requeueDeadOrPending(opts.outboxDb, {
|
|
9029
|
+
id: body.id,
|
|
9030
|
+
newClientMessageId: newId,
|
|
9031
|
+
newRowId: randomUUID4(),
|
|
9032
|
+
now: Date.now(),
|
|
9033
|
+
abortedBy: typeof body.aborted_by === "string" ? body.aborted_by : "operator"
|
|
9034
|
+
});
|
|
9035
|
+
if (!result) {
|
|
9036
|
+
respond(res, 409, { error: "row not found, already aborted, or already done" });
|
|
9037
|
+
return;
|
|
9038
|
+
}
|
|
9039
|
+
respond(res, 200, {
|
|
9040
|
+
aborted_row_id: result.abortedRowId,
|
|
9041
|
+
new_row_id: result.newRowId,
|
|
9042
|
+
new_client_message_id: result.newClientMessageId
|
|
9043
|
+
});
|
|
9044
|
+
opts.onPendingInserted?.();
|
|
9045
|
+
} catch (e) {
|
|
9046
|
+
respond(res, 400, { error: String(e) });
|
|
9047
|
+
}
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
9050
|
+
if (req.method === "POST" && url.pathname === "/v1/send") {
|
|
9051
|
+
if (!opts.outboxDb) {
|
|
9052
|
+
respond(res, 503, { error: "outbox not initialised" });
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
try {
|
|
9056
|
+
const body = await readJsonBody(req, 256 * 1024);
|
|
9057
|
+
const parsed = parseSendRequest(body, req.headers["idempotency-key"]);
|
|
9058
|
+
if ("error" in parsed) {
|
|
9059
|
+
respond(res, 400, { error: parsed.error });
|
|
9060
|
+
return;
|
|
9061
|
+
}
|
|
9062
|
+
const outcome = acceptSend(parsed.req, { db: opts.outboxDb });
|
|
9063
|
+
switch (outcome.kind) {
|
|
9064
|
+
case "accepted_pending":
|
|
9065
|
+
respond(res, outcome.status, {
|
|
9066
|
+
client_message_id: outcome.client_message_id,
|
|
9067
|
+
status: "queued"
|
|
9068
|
+
});
|
|
9069
|
+
opts.onPendingInserted?.();
|
|
9070
|
+
return;
|
|
9071
|
+
case "accepted_inflight":
|
|
9072
|
+
respond(res, outcome.status, {
|
|
9073
|
+
client_message_id: outcome.client_message_id,
|
|
9074
|
+
status: "inflight"
|
|
9075
|
+
});
|
|
9076
|
+
return;
|
|
9077
|
+
case "accepted_done":
|
|
9078
|
+
respond(res, outcome.status, {
|
|
9079
|
+
client_message_id: outcome.client_message_id,
|
|
9080
|
+
broker_message_id: outcome.broker_message_id,
|
|
9081
|
+
duplicate: true
|
|
9082
|
+
});
|
|
9083
|
+
return;
|
|
9084
|
+
case "conflict":
|
|
9085
|
+
respond(res, outcome.status, {
|
|
9086
|
+
error: "idempotency_key_reused",
|
|
9087
|
+
conflict: outcome.reason,
|
|
9088
|
+
daemon_fingerprint_prefix: outcome.daemon_fingerprint_prefix,
|
|
9089
|
+
broker_message_id: outcome.broker_message_id ?? null
|
|
9090
|
+
});
|
|
9091
|
+
return;
|
|
9092
|
+
}
|
|
9093
|
+
} catch (err) {
|
|
9094
|
+
opts.log("error", "ipc_send_failed", { err: String(err) });
|
|
9095
|
+
respond(res, 500, { error: "internal" });
|
|
9096
|
+
return;
|
|
9097
|
+
}
|
|
9098
|
+
}
|
|
9099
|
+
respond(res, 404, { error: "not found" });
|
|
9100
|
+
};
|
|
9101
|
+
}
|
|
9102
|
+
async function readJsonBody(req, maxBytes) {
|
|
9103
|
+
const chunks = [];
|
|
9104
|
+
let total = 0;
|
|
9105
|
+
for await (const chunk of req) {
|
|
9106
|
+
const buf = chunk;
|
|
9107
|
+
total += buf.length;
|
|
9108
|
+
if (total > maxBytes)
|
|
9109
|
+
throw new Error("payload_too_large");
|
|
9110
|
+
chunks.push(buf);
|
|
9111
|
+
}
|
|
9112
|
+
if (total === 0)
|
|
9113
|
+
return null;
|
|
9114
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
9115
|
+
try {
|
|
9116
|
+
return JSON.parse(text);
|
|
9117
|
+
} catch {
|
|
9118
|
+
throw new Error("invalid_json");
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
function parseSendRequest(body, idempotencyHeader) {
|
|
9122
|
+
if (!body || typeof body !== "object")
|
|
9123
|
+
return { error: "expected JSON object" };
|
|
9124
|
+
const b = body;
|
|
9125
|
+
const to = typeof b.to === "string" ? b.to.trim() : "";
|
|
9126
|
+
const message = typeof b.message === "string" ? b.message : "";
|
|
9127
|
+
if (!to)
|
|
9128
|
+
return { error: "missing 'to'" };
|
|
9129
|
+
if (!message)
|
|
9130
|
+
return { error: "missing 'message'" };
|
|
9131
|
+
const priority = b.priority;
|
|
9132
|
+
if (priority !== undefined && priority !== "now" && priority !== "next" && priority !== "low") {
|
|
9133
|
+
return { error: "priority must be 'now' | 'next' | 'low'" };
|
|
9134
|
+
}
|
|
9135
|
+
const meta = b.meta;
|
|
9136
|
+
if (meta !== undefined && meta !== null && (typeof meta !== "object" || Array.isArray(meta))) {
|
|
9137
|
+
return { error: "'meta' must be an object" };
|
|
9138
|
+
}
|
|
9139
|
+
let destination_kind;
|
|
9140
|
+
let destination_ref;
|
|
9141
|
+
if (to.startsWith("@")) {
|
|
9142
|
+
destination_kind = "topic";
|
|
9143
|
+
destination_ref = to.slice(1);
|
|
9144
|
+
} else if (to === "*") {
|
|
9145
|
+
destination_kind = "topic";
|
|
9146
|
+
destination_ref = "*";
|
|
9147
|
+
} else {
|
|
9148
|
+
destination_kind = "dm";
|
|
9149
|
+
destination_ref = to;
|
|
9150
|
+
}
|
|
9151
|
+
const headerId = Array.isArray(idempotencyHeader) ? idempotencyHeader[0] : idempotencyHeader;
|
|
9152
|
+
const client_message_id = typeof b.client_message_id === "string" && b.client_message_id.trim() ? b.client_message_id.trim() : typeof headerId === "string" && headerId.trim() ? headerId.trim() : undefined;
|
|
9153
|
+
const reply_to_id = typeof b.reply_to_id === "string" ? b.reply_to_id : undefined;
|
|
9154
|
+
return {
|
|
9155
|
+
req: {
|
|
9156
|
+
to,
|
|
9157
|
+
message,
|
|
9158
|
+
priority,
|
|
9159
|
+
meta: meta ?? undefined,
|
|
9160
|
+
reply_to_id,
|
|
9161
|
+
client_message_id,
|
|
9162
|
+
destination_kind,
|
|
9163
|
+
destination_ref
|
|
9164
|
+
}
|
|
9165
|
+
};
|
|
9166
|
+
}
|
|
9167
|
+
function respond(res, status, body) {
|
|
9168
|
+
const json = JSON.stringify(body);
|
|
9169
|
+
res.statusCode = status;
|
|
9170
|
+
res.setHeader("Content-Type", "application/json");
|
|
9171
|
+
res.setHeader("Content-Length", Buffer.byteLength(json));
|
|
9172
|
+
res.end(json);
|
|
9173
|
+
}
|
|
9174
|
+
var init_server = __esm(() => {
|
|
9175
|
+
init_paths2();
|
|
9176
|
+
init_send2();
|
|
9177
|
+
init_urls();
|
|
9178
|
+
});
|
|
9179
|
+
|
|
9180
|
+
// src/daemon/broker.ts
|
|
9181
|
+
import WebSocket2 from "ws";
|
|
9182
|
+
|
|
9183
|
+
class DaemonBrokerClient {
|
|
9184
|
+
mesh;
|
|
9185
|
+
opts;
|
|
9186
|
+
ws = null;
|
|
9187
|
+
_status = "closed";
|
|
9188
|
+
closed = false;
|
|
9189
|
+
reconnectAttempt = 0;
|
|
9190
|
+
reconnectTimer = null;
|
|
9191
|
+
helloTimer = null;
|
|
9192
|
+
pendingAcks = new Map;
|
|
9193
|
+
peerListResolvers = new Map;
|
|
9194
|
+
sessionPubkey = null;
|
|
9195
|
+
sessionSecretKey = null;
|
|
9196
|
+
opens = [];
|
|
9197
|
+
reqCounter = 0;
|
|
9198
|
+
constructor(mesh, opts = {}) {
|
|
9199
|
+
this.mesh = mesh;
|
|
9200
|
+
this.opts = opts;
|
|
9201
|
+
}
|
|
9202
|
+
get status() {
|
|
9203
|
+
return this._status;
|
|
9204
|
+
}
|
|
9205
|
+
get meshSlug() {
|
|
9206
|
+
return this.mesh.slug;
|
|
9207
|
+
}
|
|
9208
|
+
get meshId() {
|
|
9209
|
+
return this.mesh.meshId;
|
|
9210
|
+
}
|
|
9211
|
+
log = (level, msg, meta) => {
|
|
9212
|
+
(this.opts.log ?? defaultLog)(level, msg, { mesh: this.mesh.slug, ...meta });
|
|
9213
|
+
};
|
|
9214
|
+
setConnStatus(s) {
|
|
9215
|
+
if (this._status === s)
|
|
9216
|
+
return;
|
|
9217
|
+
this._status = s;
|
|
9218
|
+
this.opts.onStatusChange?.(s);
|
|
9219
|
+
}
|
|
9220
|
+
async connect() {
|
|
9221
|
+
if (this.closed)
|
|
9222
|
+
throw new Error("client_closed");
|
|
9223
|
+
if (this._status === "connecting" || this._status === "open")
|
|
9224
|
+
return;
|
|
9225
|
+
this.setConnStatus("connecting");
|
|
9226
|
+
const ws = new WebSocket2(this.mesh.brokerUrl);
|
|
9227
|
+
this.ws = ws;
|
|
9228
|
+
return new Promise((resolve, reject) => {
|
|
9229
|
+
ws.on("open", async () => {
|
|
9230
|
+
try {
|
|
9231
|
+
if (!this.sessionPubkey) {
|
|
9232
|
+
const { generateKeypair: generateKeypair4 } = await Promise.resolve().then(() => (init_facade7(), exports_facade4));
|
|
9233
|
+
const kp = await generateKeypair4();
|
|
9234
|
+
this.sessionPubkey = kp.publicKey;
|
|
9235
|
+
this.sessionSecretKey = kp.secretKey;
|
|
9236
|
+
}
|
|
9237
|
+
const { timestamp: timestamp2, signature } = await signHello(this.mesh.meshId, this.mesh.memberId, this.mesh.pubkey, this.mesh.secretKey);
|
|
9238
|
+
ws.send(JSON.stringify({
|
|
9239
|
+
type: "hello",
|
|
9240
|
+
meshId: this.mesh.meshId,
|
|
9241
|
+
memberId: this.mesh.memberId,
|
|
9242
|
+
pubkey: this.mesh.pubkey,
|
|
9243
|
+
sessionPubkey: this.sessionPubkey,
|
|
9244
|
+
displayName: this.opts.displayName,
|
|
9245
|
+
sessionId: `daemon-${process.pid}`,
|
|
9246
|
+
pid: process.pid,
|
|
9247
|
+
cwd: process.cwd(),
|
|
9248
|
+
hostname: __require("node:os").hostname(),
|
|
9249
|
+
peerType: "ai",
|
|
9250
|
+
channel: "claudemesh-daemon",
|
|
9251
|
+
timestamp: timestamp2,
|
|
9252
|
+
signature
|
|
9253
|
+
}));
|
|
9254
|
+
this.helloTimer = setTimeout(() => {
|
|
9255
|
+
this.log("warn", "broker_hello_ack_timeout");
|
|
9256
|
+
try {
|
|
9257
|
+
ws.close();
|
|
9258
|
+
} catch {}
|
|
9259
|
+
reject(new Error("hello_ack_timeout"));
|
|
9260
|
+
}, HELLO_ACK_TIMEOUT_MS2);
|
|
9261
|
+
} catch (e) {
|
|
9262
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
9263
|
+
}
|
|
9264
|
+
});
|
|
9265
|
+
ws.on("message", (raw) => {
|
|
9266
|
+
let msg;
|
|
9267
|
+
try {
|
|
9268
|
+
msg = JSON.parse(raw.toString());
|
|
9269
|
+
} catch {
|
|
9270
|
+
return;
|
|
9271
|
+
}
|
|
9272
|
+
if (msg.type === "hello_ack") {
|
|
9273
|
+
if (this.helloTimer)
|
|
9274
|
+
clearTimeout(this.helloTimer);
|
|
9275
|
+
this.helloTimer = null;
|
|
9276
|
+
this.setConnStatus("open");
|
|
9277
|
+
this.reconnectAttempt = 0;
|
|
9278
|
+
const queued = this.opens.slice();
|
|
9279
|
+
this.opens.length = 0;
|
|
9280
|
+
for (const fn of queued) {
|
|
9281
|
+
try {
|
|
9282
|
+
fn();
|
|
9283
|
+
} catch (e) {
|
|
9284
|
+
this.log("warn", "open_handler_failed", { err: String(e) });
|
|
9285
|
+
}
|
|
9286
|
+
}
|
|
9287
|
+
resolve();
|
|
9288
|
+
return;
|
|
9289
|
+
}
|
|
9290
|
+
if (msg.type === "ack") {
|
|
9291
|
+
const id = String(msg.id ?? "");
|
|
9292
|
+
const ack = this.pendingAcks.get(id);
|
|
9293
|
+
if (ack) {
|
|
9294
|
+
this.pendingAcks.delete(id);
|
|
9295
|
+
clearTimeout(ack.timer);
|
|
9296
|
+
if (typeof msg.error === "string" && msg.error.length > 0) {
|
|
9297
|
+
ack.resolve({ ok: false, error: msg.error, permanent: classifyPermanent(msg.error) });
|
|
9298
|
+
} else {
|
|
9299
|
+
ack.resolve({ ok: true, messageId: String(msg.messageId ?? id) });
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
return;
|
|
9303
|
+
}
|
|
9304
|
+
if (msg.type === "peers_list") {
|
|
9305
|
+
const reqId = String(msg._reqId ?? "");
|
|
9306
|
+
const pending = this.peerListResolvers.get(reqId);
|
|
9307
|
+
if (pending) {
|
|
9308
|
+
this.peerListResolvers.delete(reqId);
|
|
9309
|
+
clearTimeout(pending.timer);
|
|
9310
|
+
pending.resolve(Array.isArray(msg.peers) ? msg.peers : []);
|
|
9311
|
+
}
|
|
9312
|
+
return;
|
|
9313
|
+
}
|
|
9314
|
+
if (msg.type === "push" || msg.type === "inbound") {
|
|
9315
|
+
this.opts.onPush?.(msg);
|
|
9316
|
+
return;
|
|
9317
|
+
}
|
|
9318
|
+
});
|
|
9319
|
+
ws.on("close", (code, reason) => {
|
|
9320
|
+
if (this.helloTimer) {
|
|
9321
|
+
clearTimeout(this.helloTimer);
|
|
9322
|
+
this.helloTimer = null;
|
|
9323
|
+
}
|
|
9324
|
+
this.failPendingAcks(`broker_disconnected_${code}`);
|
|
9325
|
+
if (this.closed) {
|
|
9326
|
+
this.setConnStatus("closed");
|
|
9327
|
+
return;
|
|
9328
|
+
}
|
|
9329
|
+
this.setConnStatus("reconnecting");
|
|
9330
|
+
const wait = BACKOFF_CAPS_MS[Math.min(this.reconnectAttempt, BACKOFF_CAPS_MS.length - 1)] ?? 30000;
|
|
9331
|
+
this.reconnectAttempt++;
|
|
9332
|
+
this.log("info", "broker_reconnect_scheduled", { wait_ms: wait, code, reason: reason.toString("utf8") });
|
|
9333
|
+
this.reconnectTimer = setTimeout(() => this.connect().catch((err) => this.log("warn", "broker_reconnect_failed", { err: String(err) })), wait);
|
|
9334
|
+
if (this._status === "connecting")
|
|
9335
|
+
reject(new Error(`closed_before_hello_${code}`));
|
|
9336
|
+
});
|
|
9337
|
+
ws.on("error", (err) => this.log("warn", "broker_ws_error", { err: err.message }));
|
|
9338
|
+
});
|
|
9339
|
+
}
|
|
9340
|
+
send(req) {
|
|
9341
|
+
return new Promise((resolve) => {
|
|
9342
|
+
const dispatch = () => {
|
|
9343
|
+
if (!this.ws || this.ws.readyState !== this.ws.OPEN) {
|
|
9344
|
+
resolve({ ok: false, error: "broker_not_open", permanent: false });
|
|
9345
|
+
return;
|
|
9346
|
+
}
|
|
9347
|
+
const id = req.client_message_id;
|
|
9348
|
+
const timer = setTimeout(() => {
|
|
9349
|
+
if (this.pendingAcks.delete(id)) {
|
|
9350
|
+
resolve({ ok: false, error: "ack_timeout", permanent: false });
|
|
9351
|
+
}
|
|
9352
|
+
}, SEND_ACK_TIMEOUT_MS);
|
|
9353
|
+
this.pendingAcks.set(id, { resolve, timer });
|
|
9354
|
+
try {
|
|
9355
|
+
this.ws.send(JSON.stringify({
|
|
9356
|
+
type: "send",
|
|
9357
|
+
id,
|
|
9358
|
+
client_message_id: id,
|
|
9359
|
+
request_fingerprint: req.request_fingerprint_hex,
|
|
9360
|
+
targetSpec: req.targetSpec,
|
|
9361
|
+
priority: req.priority,
|
|
9362
|
+
nonce: req.nonce,
|
|
9363
|
+
ciphertext: req.ciphertext
|
|
9364
|
+
}));
|
|
9365
|
+
} catch (e) {
|
|
9366
|
+
this.pendingAcks.delete(id);
|
|
9367
|
+
clearTimeout(timer);
|
|
9368
|
+
resolve({ ok: false, error: `ws_write_failed: ${String(e)}`, permanent: false });
|
|
9369
|
+
}
|
|
9370
|
+
};
|
|
9371
|
+
if (this._status === "open")
|
|
9372
|
+
dispatch();
|
|
9373
|
+
else
|
|
9374
|
+
this.opens.push(dispatch);
|
|
9375
|
+
});
|
|
9376
|
+
}
|
|
9377
|
+
async listPeers(timeoutMs = 5000) {
|
|
9378
|
+
if (this._status !== "open" || !this.ws)
|
|
9379
|
+
return [];
|
|
9380
|
+
return new Promise((resolve) => {
|
|
9381
|
+
const reqId = `pl-${++this.reqCounter}`;
|
|
9382
|
+
const timer = setTimeout(() => {
|
|
9383
|
+
if (this.peerListResolvers.delete(reqId))
|
|
9384
|
+
resolve([]);
|
|
9385
|
+
}, timeoutMs);
|
|
9386
|
+
this.peerListResolvers.set(reqId, { resolve, timer });
|
|
9387
|
+
try {
|
|
9388
|
+
this.ws.send(JSON.stringify({ type: "list_peers", _reqId: reqId }));
|
|
9389
|
+
} catch {
|
|
9390
|
+
this.peerListResolvers.delete(reqId);
|
|
9391
|
+
clearTimeout(timer);
|
|
9392
|
+
resolve([]);
|
|
9393
|
+
}
|
|
9394
|
+
});
|
|
9395
|
+
}
|
|
9396
|
+
setProfile(profile) {
|
|
9397
|
+
if (this._status !== "open" || !this.ws)
|
|
9398
|
+
return;
|
|
9399
|
+
try {
|
|
9400
|
+
this.ws.send(JSON.stringify({ type: "set_profile", ...profile }));
|
|
9401
|
+
} catch {}
|
|
9402
|
+
}
|
|
9403
|
+
setSummary(summary) {
|
|
9404
|
+
if (this._status !== "open" || !this.ws)
|
|
9405
|
+
return;
|
|
9406
|
+
try {
|
|
9407
|
+
this.ws.send(JSON.stringify({ type: "set_summary", summary }));
|
|
9408
|
+
} catch {}
|
|
9409
|
+
}
|
|
9410
|
+
setStatus(status) {
|
|
9411
|
+
if (this._status !== "open" || !this.ws)
|
|
9412
|
+
return;
|
|
9413
|
+
try {
|
|
9414
|
+
this.ws.send(JSON.stringify({ type: "set_status", status }));
|
|
9415
|
+
} catch {}
|
|
9416
|
+
}
|
|
9417
|
+
setVisible(visible) {
|
|
9418
|
+
if (this._status !== "open" || !this.ws)
|
|
9419
|
+
return;
|
|
9420
|
+
try {
|
|
9421
|
+
this.ws.send(JSON.stringify({ type: "set_visible", visible }));
|
|
9422
|
+
} catch {}
|
|
9423
|
+
}
|
|
9424
|
+
async close() {
|
|
9425
|
+
this.closed = true;
|
|
9426
|
+
if (this.reconnectTimer) {
|
|
9427
|
+
clearTimeout(this.reconnectTimer);
|
|
9428
|
+
this.reconnectTimer = null;
|
|
9429
|
+
}
|
|
9430
|
+
if (this.helloTimer) {
|
|
9431
|
+
clearTimeout(this.helloTimer);
|
|
9432
|
+
this.helloTimer = null;
|
|
9433
|
+
}
|
|
9434
|
+
this.failPendingAcks("daemon_shutdown");
|
|
9435
|
+
try {
|
|
9436
|
+
this.ws?.close();
|
|
9437
|
+
} catch {}
|
|
9438
|
+
this.setConnStatus("closed");
|
|
9439
|
+
}
|
|
9440
|
+
getSessionKeys() {
|
|
9441
|
+
if (!this.sessionPubkey || !this.sessionSecretKey)
|
|
9442
|
+
return null;
|
|
9443
|
+
return { sessionPubkey: this.sessionPubkey, sessionSecretKey: this.sessionSecretKey };
|
|
9444
|
+
}
|
|
9445
|
+
failPendingAcks(reason) {
|
|
9446
|
+
for (const [id, ack] of this.pendingAcks) {
|
|
9447
|
+
clearTimeout(ack.timer);
|
|
9448
|
+
ack.resolve({ ok: false, error: reason, permanent: false });
|
|
9449
|
+
this.pendingAcks.delete(id);
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9453
|
+
function defaultLog(level, msg, meta) {
|
|
9454
|
+
const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
|
|
9455
|
+
if (level === "info")
|
|
9456
|
+
process.stdout.write(line + `
|
|
9457
|
+
`);
|
|
9458
|
+
else
|
|
9459
|
+
process.stderr.write(line + `
|
|
9460
|
+
`);
|
|
9461
|
+
}
|
|
9462
|
+
function classifyPermanent(err) {
|
|
9463
|
+
return /payload_too_large|forbidden|not_found|invalid|schema|auth|signature/i.test(err);
|
|
9464
|
+
}
|
|
9465
|
+
var HELLO_ACK_TIMEOUT_MS2 = 5000, SEND_ACK_TIMEOUT_MS = 15000, BACKOFF_CAPS_MS;
|
|
9466
|
+
var init_broker = __esm(() => {
|
|
9467
|
+
init_hello_sig();
|
|
9468
|
+
BACKOFF_CAPS_MS = [1000, 2000, 4000, 8000, 16000, 30000];
|
|
9469
|
+
});
|
|
9470
|
+
|
|
9471
|
+
// src/daemon/drain.ts
|
|
9472
|
+
function startDrainWorker(opts) {
|
|
9473
|
+
const log2 = opts.log ?? defaultLog2;
|
|
9474
|
+
let stopped = false;
|
|
9475
|
+
let wakeResolve = null;
|
|
9476
|
+
let wakePromise = new Promise((r) => {
|
|
9477
|
+
wakeResolve = r;
|
|
9478
|
+
});
|
|
9479
|
+
const wake = () => {
|
|
9480
|
+
if (wakeResolve) {
|
|
9481
|
+
const r = wakeResolve;
|
|
9482
|
+
wakeResolve = null;
|
|
9483
|
+
r();
|
|
9484
|
+
}
|
|
9485
|
+
};
|
|
9486
|
+
const tick = async () => {
|
|
9487
|
+
while (!stopped) {
|
|
9488
|
+
try {
|
|
9489
|
+
await drainOnce(opts, log2);
|
|
9490
|
+
} catch (e) {
|
|
9491
|
+
log2("warn", "drain_tick_failed", { err: String(e) });
|
|
9492
|
+
}
|
|
9493
|
+
await Promise.race([
|
|
9494
|
+
wakePromise,
|
|
9495
|
+
new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
|
|
9496
|
+
]);
|
|
9497
|
+
wakePromise = new Promise((r) => {
|
|
9498
|
+
wakeResolve = r;
|
|
9499
|
+
});
|
|
9500
|
+
}
|
|
9501
|
+
};
|
|
9502
|
+
tick();
|
|
9503
|
+
return {
|
|
9504
|
+
wake,
|
|
9505
|
+
close: async () => {
|
|
9506
|
+
stopped = true;
|
|
9507
|
+
wake();
|
|
9508
|
+
}
|
|
9509
|
+
};
|
|
9510
|
+
}
|
|
9511
|
+
async function drainOnce(opts, log2) {
|
|
9512
|
+
const now = Date.now();
|
|
9513
|
+
const rows = opts.db.prepare(`
|
|
9514
|
+
SELECT id, client_message_id, request_fingerprint, payload, attempts
|
|
9515
|
+
FROM outbox
|
|
9516
|
+
WHERE status = 'pending' AND next_attempt_at <= ?
|
|
9517
|
+
ORDER BY enqueued_at
|
|
9518
|
+
LIMIT 32
|
|
9519
|
+
`).all(now);
|
|
9520
|
+
if (rows.length === 0)
|
|
9521
|
+
return;
|
|
9522
|
+
for (const row of rows) {
|
|
9523
|
+
if (markInflight(opts.db, row.id, now) === 0)
|
|
9524
|
+
continue;
|
|
9525
|
+
const fpHex = bufferToHex(row.request_fingerprint);
|
|
9526
|
+
const sessionKeys = opts.broker.getSessionKeys();
|
|
9527
|
+
const targetSpec = "*";
|
|
9528
|
+
const nonce = await randomNonce2();
|
|
9529
|
+
const ciphertext = Buffer.from(row.payload).toString("base64");
|
|
9530
|
+
let res;
|
|
9531
|
+
try {
|
|
9532
|
+
res = await opts.broker.send({
|
|
9533
|
+
targetSpec,
|
|
9534
|
+
priority: "next",
|
|
9535
|
+
nonce,
|
|
9536
|
+
ciphertext,
|
|
9537
|
+
client_message_id: row.client_message_id,
|
|
9538
|
+
request_fingerprint_hex: fpHex
|
|
9539
|
+
});
|
|
9540
|
+
} catch (e) {
|
|
9541
|
+
log2("warn", "drain_send_threw", { id: row.id, err: String(e) });
|
|
9542
|
+
backoffPending(opts.db, row.id, row.attempts + 1, "exception", String(e));
|
|
9543
|
+
continue;
|
|
9544
|
+
}
|
|
9545
|
+
if (res.ok) {
|
|
9546
|
+
markDone(opts.db, row.id, res.messageId, Date.now());
|
|
9547
|
+
} else if (res.permanent) {
|
|
9548
|
+
log2("warn", "drain_permanent_failure", { id: row.id, err: res.error });
|
|
9549
|
+
markDead(opts.db, row.id, res.error);
|
|
9550
|
+
} else if (row.attempts + 1 >= MAX_ATTEMPTS_PER_ROW) {
|
|
9551
|
+
log2("warn", "drain_max_attempts", { id: row.id, err: res.error });
|
|
9552
|
+
markDead(opts.db, row.id, `max_attempts: ${res.error}`);
|
|
9553
|
+
} else {
|
|
9554
|
+
backoffPending(opts.db, row.id, row.attempts + 1, "retry", res.error);
|
|
9555
|
+
}
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
function markInflight(db, id, now) {
|
|
9559
|
+
return Number(db.prepare(`
|
|
9560
|
+
UPDATE outbox
|
|
9561
|
+
SET status = 'inflight', attempts = attempts + 1, next_attempt_at = ?
|
|
9562
|
+
WHERE id = ? AND status = 'pending'
|
|
9563
|
+
`).run(now + BACKOFF_CAP_MS, id).changes);
|
|
9564
|
+
}
|
|
9565
|
+
function markDone(db, id, brokerMessageId, now) {
|
|
9566
|
+
db.prepare(`
|
|
9567
|
+
UPDATE outbox
|
|
9568
|
+
SET status = 'done', delivered_at = ?, broker_message_id = ?, last_error = NULL
|
|
9569
|
+
WHERE id = ?
|
|
9570
|
+
`).run(now, brokerMessageId, id);
|
|
9571
|
+
}
|
|
9572
|
+
function markDead(db, id, err) {
|
|
9573
|
+
db.prepare(`UPDATE outbox SET status = 'dead', last_error = ? WHERE id = ?`).run(err, id);
|
|
9574
|
+
}
|
|
9575
|
+
function backoffPending(db, id, attempts, _kind, err) {
|
|
9576
|
+
const wait = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** Math.min(attempts, 12));
|
|
9577
|
+
const next = Date.now() + wait;
|
|
9578
|
+
db.prepare(`
|
|
9579
|
+
UPDATE outbox
|
|
9580
|
+
SET status = 'pending', attempts = ?, next_attempt_at = ?, last_error = ?
|
|
9581
|
+
WHERE id = ?
|
|
9582
|
+
`).run(attempts, next, err, id);
|
|
9583
|
+
}
|
|
9584
|
+
function bufferToHex(b) {
|
|
9585
|
+
let s = "";
|
|
9586
|
+
for (let i = 0;i < b.length; i++)
|
|
9587
|
+
s += b[i].toString(16).padStart(2, "0");
|
|
9588
|
+
return s;
|
|
9589
|
+
}
|
|
9590
|
+
async function randomNonce2() {
|
|
9591
|
+
const { randomBytes: randomBytes5 } = await import("node:crypto");
|
|
9592
|
+
return randomBytes5(24).toString("base64");
|
|
9593
|
+
}
|
|
9594
|
+
function defaultLog2(level, msg, meta) {
|
|
9595
|
+
const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
|
|
9596
|
+
if (level === "info")
|
|
9597
|
+
process.stdout.write(line + `
|
|
9598
|
+
`);
|
|
9599
|
+
else
|
|
9600
|
+
process.stderr.write(line + `
|
|
9601
|
+
`);
|
|
9602
|
+
}
|
|
9603
|
+
var POLL_INTERVAL_MS = 500, MAX_ATTEMPTS_PER_ROW = 25, BACKOFF_BASE_MS = 500, BACKOFF_CAP_MS = 30000;
|
|
9604
|
+
var init_drain = () => {};
|
|
9605
|
+
|
|
9606
|
+
// src/daemon/inbound.ts
|
|
9607
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
9608
|
+
async function handleBrokerPush(msg, ctx) {
|
|
9609
|
+
if (msg.subtype === "system" && typeof msg.event === "string") {
|
|
9610
|
+
ctx.bus.publish(mapSystemEventKind(msg.event), {
|
|
9611
|
+
mesh: ctx.meshSlug,
|
|
9612
|
+
event: msg.event,
|
|
9613
|
+
...msg.eventData ?? {}
|
|
9614
|
+
});
|
|
9615
|
+
return;
|
|
9616
|
+
}
|
|
9617
|
+
if (msg.type !== "push")
|
|
9618
|
+
return;
|
|
9619
|
+
const brokerMessageId = stringOrNull(msg.messageId);
|
|
9620
|
+
const senderPubkey = stringOrNull(msg.senderPubkey) ?? "";
|
|
9621
|
+
const senderName = stringOrNull(msg.senderName) ?? senderPubkey.slice(0, 8);
|
|
9622
|
+
const topic = stringOrNull(msg.topic);
|
|
9623
|
+
const replyToId = stringOrNull(msg.replyToId);
|
|
9624
|
+
const ciphertext = stringOrNull(msg.ciphertext) ?? "";
|
|
9625
|
+
const nonce = stringOrNull(msg.nonce) ?? "";
|
|
9626
|
+
const createdAt = stringOrNull(msg.createdAt);
|
|
9627
|
+
const clientMessageId = stringOrNull(msg.client_message_id) ?? brokerMessageId ?? randomUUID5();
|
|
9628
|
+
const body = await decryptOrFallback({
|
|
9629
|
+
ciphertext,
|
|
9630
|
+
nonce,
|
|
9631
|
+
senderPubkey,
|
|
9632
|
+
ctx
|
|
9633
|
+
});
|
|
9634
|
+
const id = randomUUID5();
|
|
9635
|
+
const inserted = insertIfNew(ctx.db, {
|
|
9636
|
+
id,
|
|
9637
|
+
client_message_id: clientMessageId,
|
|
9638
|
+
broker_message_id: brokerMessageId,
|
|
9639
|
+
mesh: ctx.meshSlug,
|
|
9640
|
+
topic,
|
|
9641
|
+
sender_pubkey: senderPubkey,
|
|
9642
|
+
sender_name: senderName,
|
|
9643
|
+
body,
|
|
9644
|
+
meta: createdAt ? JSON.stringify({ created_at: createdAt }) : null,
|
|
9645
|
+
received_at: Date.now(),
|
|
9646
|
+
reply_to_id: replyToId
|
|
9647
|
+
});
|
|
9648
|
+
if (!inserted)
|
|
9649
|
+
return;
|
|
9650
|
+
ctx.bus.publish("message", {
|
|
9651
|
+
id,
|
|
9652
|
+
mesh: ctx.meshSlug,
|
|
9653
|
+
client_message_id: clientMessageId,
|
|
9654
|
+
broker_message_id: brokerMessageId,
|
|
9655
|
+
sender_pubkey: senderPubkey,
|
|
9656
|
+
sender_name: senderName,
|
|
9657
|
+
topic,
|
|
9658
|
+
reply_to_id: replyToId,
|
|
9659
|
+
body,
|
|
9660
|
+
created_at: createdAt
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
9663
|
+
async function decryptOrFallback(args) {
|
|
9664
|
+
const { ciphertext, nonce, senderPubkey, ctx } = args;
|
|
9665
|
+
if (!ciphertext)
|
|
9666
|
+
return null;
|
|
9667
|
+
if (nonce && senderPubkey) {
|
|
9668
|
+
const envelope = { nonce, ciphertext };
|
|
9669
|
+
if (ctx.sessionSecretKeyHex) {
|
|
9670
|
+
const pt = await decryptDirect(envelope, senderPubkey, ctx.sessionSecretKeyHex);
|
|
9671
|
+
if (pt !== null)
|
|
9672
|
+
return pt;
|
|
9673
|
+
}
|
|
9674
|
+
if (ctx.recipientSecretKeyHex) {
|
|
9675
|
+
const pt = await decryptDirect(envelope, senderPubkey, ctx.recipientSecretKeyHex);
|
|
9676
|
+
if (pt !== null)
|
|
9677
|
+
return pt;
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
try {
|
|
9681
|
+
return Buffer.from(ciphertext, "base64").toString("utf8");
|
|
9682
|
+
} catch (e) {
|
|
9683
|
+
ctx.log?.("warn", "inbound_b64_decode_failed", { err: String(e) });
|
|
9684
|
+
return null;
|
|
9685
|
+
}
|
|
9686
|
+
}
|
|
9687
|
+
function stringOrNull(v) {
|
|
9688
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
9689
|
+
}
|
|
9690
|
+
function mapSystemEventKind(event) {
|
|
9691
|
+
if (event === "peer_joined")
|
|
9692
|
+
return "peer_join";
|
|
9693
|
+
if (event === "peer_left")
|
|
9694
|
+
return "peer_leave";
|
|
9695
|
+
return "system";
|
|
9696
|
+
}
|
|
9697
|
+
var init_inbound = __esm(() => {
|
|
9698
|
+
init_facade7();
|
|
9699
|
+
});
|
|
9700
|
+
|
|
9701
|
+
// src/daemon/identity.ts
|
|
9702
|
+
var exports_identity = {};
|
|
9703
|
+
__export(exports_identity, {
|
|
9704
|
+
computeCurrentFingerprint: () => computeCurrentFingerprint,
|
|
9705
|
+
checkFingerprint: () => checkFingerprint,
|
|
9706
|
+
acceptCurrentHost: () => acceptCurrentHost
|
|
9707
|
+
});
|
|
9708
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
9709
|
+
import { join as join7 } from "node:path";
|
|
9710
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9711
|
+
import { networkInterfaces } from "node:os";
|
|
9712
|
+
function path() {
|
|
9713
|
+
return join7(DAEMON_PATHS.DAEMON_DIR, FILE_NAME);
|
|
9714
|
+
}
|
|
9715
|
+
function computeCurrentFingerprint() {
|
|
9716
|
+
const host_id = readHostId() ?? "";
|
|
9717
|
+
const stable_mac = pickStableMac() ?? "";
|
|
9718
|
+
const fp = createHash2("sha256").update(host_id, "utf8").update("\x00").update(stable_mac, "utf8").digest("hex");
|
|
9719
|
+
return {
|
|
9720
|
+
schema_version: 1,
|
|
9721
|
+
fingerprint: fp,
|
|
9722
|
+
host_id,
|
|
9723
|
+
stable_mac,
|
|
9724
|
+
written_at: new Date().toISOString()
|
|
9725
|
+
};
|
|
9726
|
+
}
|
|
9727
|
+
function checkFingerprint() {
|
|
9728
|
+
const current = computeCurrentFingerprint();
|
|
9729
|
+
if (!existsSync10(path())) {
|
|
9730
|
+
writeFileSync8(path(), JSON.stringify(current, null, 2), { mode: 384 });
|
|
9731
|
+
return { result: "first_run", current };
|
|
9732
|
+
}
|
|
9733
|
+
let stored;
|
|
9734
|
+
try {
|
|
9735
|
+
stored = JSON.parse(readFileSync7(path(), "utf8"));
|
|
9736
|
+
} catch {
|
|
9737
|
+
return { result: "unavailable", current };
|
|
9738
|
+
}
|
|
9739
|
+
if (stored.fingerprint === current.fingerprint)
|
|
9740
|
+
return { result: "match", current, stored };
|
|
9741
|
+
return { result: "mismatch", current, stored };
|
|
9742
|
+
}
|
|
9743
|
+
function acceptCurrentHost() {
|
|
9744
|
+
const current = computeCurrentFingerprint();
|
|
9745
|
+
writeFileSync8(path(), JSON.stringify(current, null, 2), { mode: 384 });
|
|
9746
|
+
return current;
|
|
9747
|
+
}
|
|
9748
|
+
function readHostId() {
|
|
9749
|
+
if (process.platform === "linux") {
|
|
9750
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
9751
|
+
try {
|
|
9752
|
+
const raw = readFileSync7(p, "utf8").trim();
|
|
9753
|
+
if (raw)
|
|
9754
|
+
return `linux:${raw}`;
|
|
9755
|
+
} catch {}
|
|
9756
|
+
}
|
|
9757
|
+
return null;
|
|
9758
|
+
}
|
|
9759
|
+
if (process.platform === "darwin") {
|
|
9760
|
+
return null;
|
|
9761
|
+
}
|
|
9762
|
+
return null;
|
|
9763
|
+
}
|
|
9764
|
+
function pickStableMac() {
|
|
9765
|
+
const ifs = networkInterfaces();
|
|
9766
|
+
const candidates = [];
|
|
9767
|
+
for (const [name, addrs] of Object.entries(ifs)) {
|
|
9768
|
+
if (!addrs)
|
|
9769
|
+
continue;
|
|
9770
|
+
if (isIgnoredInterface(name))
|
|
9771
|
+
continue;
|
|
9772
|
+
for (const a of addrs) {
|
|
9773
|
+
if (a.internal)
|
|
9774
|
+
continue;
|
|
9775
|
+
if (!a.mac || a.mac === "00:00:00:00:00:00")
|
|
9776
|
+
continue;
|
|
9777
|
+
candidates.push(`${name}::${a.mac}`);
|
|
9778
|
+
break;
|
|
9779
|
+
}
|
|
9780
|
+
}
|
|
9781
|
+
if (candidates.length === 0)
|
|
9782
|
+
return null;
|
|
9783
|
+
candidates.sort();
|
|
9784
|
+
const first = candidates[0];
|
|
9785
|
+
const idx = first.indexOf("::");
|
|
9786
|
+
return idx >= 0 ? first.slice(idx + 2) : first;
|
|
9787
|
+
}
|
|
9788
|
+
function isIgnoredInterface(name) {
|
|
9789
|
+
return /^(lo|docker|br-|veth|tap|tun|tailscale|wg|utun|ppp|vboxnet|vmnet|awdl|llw)/i.test(name);
|
|
9790
|
+
}
|
|
9791
|
+
var FILE_NAME = "host_fingerprint.json";
|
|
9792
|
+
var init_identity = __esm(() => {
|
|
9793
|
+
init_paths2();
|
|
9794
|
+
});
|
|
9795
|
+
|
|
9796
|
+
// src/daemon/run.ts
|
|
9797
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
9798
|
+
function detectContainer() {
|
|
9799
|
+
if (process.env.KUBERNETES_SERVICE_HOST)
|
|
9800
|
+
return true;
|
|
9801
|
+
if (process.env.CONTAINER === "1")
|
|
9802
|
+
return true;
|
|
9803
|
+
try {
|
|
9804
|
+
if (existsSync11("/.dockerenv"))
|
|
9805
|
+
return true;
|
|
9806
|
+
const cg = readFileSync8("/proc/1/cgroup", "utf8");
|
|
9807
|
+
if (/(docker|kubepods|containerd)/.test(cg))
|
|
9808
|
+
return true;
|
|
9809
|
+
} catch {}
|
|
9810
|
+
return false;
|
|
9811
|
+
}
|
|
9812
|
+
async function runDaemon(opts = {}) {
|
|
9813
|
+
mkdirSync6(DAEMON_PATHS.DAEMON_DIR, { recursive: true, mode: 448 });
|
|
9814
|
+
const lock = acquireSingletonLock();
|
|
9815
|
+
if (lock.result === "already-running") {
|
|
9816
|
+
process.stderr.write(`daemon already running (pid ${lock.pid})
|
|
9817
|
+
`);
|
|
9818
|
+
return 1;
|
|
9819
|
+
}
|
|
9820
|
+
if (lock.result === "stale") {
|
|
9821
|
+
process.stderr.write(`recovered stale pid file; starting fresh
|
|
9822
|
+
`);
|
|
9823
|
+
}
|
|
9824
|
+
const fpCheck = checkFingerprint();
|
|
9825
|
+
const policy = opts.clonePolicy ?? "refuse";
|
|
9826
|
+
if (fpCheck.result === "mismatch") {
|
|
9827
|
+
const msg = `host_fingerprint mismatch: this daemon dir was started on a different host.`;
|
|
9828
|
+
if (policy === "refuse") {
|
|
9829
|
+
process.stderr.write(`${msg}
|
|
9830
|
+
`);
|
|
9831
|
+
process.stderr.write(` stored host_id: ${fpCheck.stored?.host_id}
|
|
9832
|
+
`);
|
|
9833
|
+
process.stderr.write(` current host_id: ${fpCheck.current.host_id}
|
|
9834
|
+
`);
|
|
9835
|
+
process.stderr.write(`Run \`claudemesh daemon accept-host\` to write a fresh fingerprint, or
|
|
9836
|
+
`);
|
|
9837
|
+
process.stderr.write(`run \`claudemesh daemon remint\` to mint a new keypair (Sprint 7+).
|
|
9838
|
+
`);
|
|
9839
|
+
releaseSingletonLock();
|
|
9840
|
+
return 4;
|
|
9841
|
+
}
|
|
9842
|
+
if (policy === "warn") {
|
|
9843
|
+
process.stderr.write(`WARN: ${msg} (continuing per [clone] policy=warn)
|
|
9844
|
+
`);
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9847
|
+
if (fpCheck.result === "first_run") {
|
|
9848
|
+
process.stdout.write(JSON.stringify({
|
|
9849
|
+
msg: "host_fingerprint_written",
|
|
9850
|
+
fingerprint_prefix: fpCheck.current.fingerprint.slice(0, 16),
|
|
9851
|
+
ts: new Date().toISOString()
|
|
9852
|
+
}) + `
|
|
9853
|
+
`);
|
|
9854
|
+
}
|
|
9855
|
+
const localToken = ensureLocalToken();
|
|
9856
|
+
const tcpEnabled = opts.tcpEnabled ?? !detectContainer();
|
|
9857
|
+
let outboxDb;
|
|
9858
|
+
let inboxDb;
|
|
9859
|
+
try {
|
|
9860
|
+
outboxDb = await openSqlite(DAEMON_PATHS.OUTBOX_DB);
|
|
9861
|
+
migrateOutbox(outboxDb);
|
|
9862
|
+
inboxDb = await openSqlite(DAEMON_PATHS.INBOX_DB);
|
|
9863
|
+
migrateInbox(inboxDb);
|
|
9864
|
+
} catch (err) {
|
|
9865
|
+
process.stderr.write(`db open failed: ${String(err)}
|
|
9866
|
+
`);
|
|
9867
|
+
releaseSingletonLock();
|
|
9868
|
+
return 1;
|
|
9869
|
+
}
|
|
9870
|
+
const bus = new EventBus;
|
|
9871
|
+
const cfg = readConfig();
|
|
9872
|
+
let mesh = null;
|
|
9873
|
+
if (opts.mesh) {
|
|
9874
|
+
mesh = cfg.meshes.find((m) => m.slug === opts.mesh) ?? null;
|
|
9875
|
+
if (!mesh) {
|
|
9876
|
+
process.stderr.write(`mesh not found: ${opts.mesh}
|
|
9877
|
+
`);
|
|
9878
|
+
process.stderr.write(`joined meshes: ${cfg.meshes.map((m) => m.slug).join(", ") || "(none)"}
|
|
9879
|
+
`);
|
|
9880
|
+
releaseSingletonLock();
|
|
9881
|
+
try {
|
|
9882
|
+
outboxDb.close();
|
|
9883
|
+
} catch {}
|
|
9884
|
+
return 2;
|
|
9885
|
+
}
|
|
9886
|
+
} else if (cfg.meshes.length === 1) {
|
|
9887
|
+
mesh = cfg.meshes[0];
|
|
9888
|
+
} else if (cfg.meshes.length === 0) {
|
|
9889
|
+
process.stderr.write(`no mesh joined; run \`claudemesh join <invite-url>\` first
|
|
9890
|
+
`);
|
|
9891
|
+
releaseSingletonLock();
|
|
9892
|
+
try {
|
|
9893
|
+
outboxDb.close();
|
|
9894
|
+
} catch {}
|
|
9895
|
+
return 2;
|
|
9896
|
+
} else {
|
|
9897
|
+
process.stderr.write(`multiple meshes joined; pass --mesh <slug>
|
|
9898
|
+
`);
|
|
9899
|
+
process.stderr.write(`available: ${cfg.meshes.map((m) => m.slug).join(", ")}
|
|
9900
|
+
`);
|
|
9901
|
+
releaseSingletonLock();
|
|
9902
|
+
try {
|
|
9903
|
+
outboxDb.close();
|
|
9904
|
+
} catch {}
|
|
9905
|
+
return 2;
|
|
9906
|
+
}
|
|
9907
|
+
const broker = new DaemonBrokerClient(mesh, {
|
|
9908
|
+
displayName: opts.displayName,
|
|
9909
|
+
onStatusChange: (s) => {
|
|
9910
|
+
process.stdout.write(JSON.stringify({
|
|
9911
|
+
msg: "broker_status",
|
|
9912
|
+
status: s,
|
|
9913
|
+
mesh: mesh.slug,
|
|
9914
|
+
ts: new Date().toISOString()
|
|
9915
|
+
}) + `
|
|
9916
|
+
`);
|
|
9917
|
+
bus.publish("broker_status", { mesh: mesh.slug, status: s });
|
|
9918
|
+
},
|
|
9919
|
+
onPush: (m) => {
|
|
9920
|
+
const sessionKeys = broker.getSessionKeys();
|
|
9921
|
+
handleBrokerPush(m, {
|
|
9922
|
+
db: inboxDb,
|
|
9923
|
+
bus,
|
|
9924
|
+
meshSlug: mesh.slug,
|
|
9925
|
+
recipientSecretKeyHex: mesh.secretKey,
|
|
9926
|
+
sessionSecretKeyHex: sessionKeys?.sessionSecretKey
|
|
9927
|
+
});
|
|
9928
|
+
}
|
|
9929
|
+
});
|
|
9930
|
+
broker.connect().catch((err) => process.stderr.write(`broker connect failed: ${String(err)}
|
|
9931
|
+
`));
|
|
9932
|
+
let drain = null;
|
|
9933
|
+
drain = startDrainWorker({ db: outboxDb, broker });
|
|
9934
|
+
const ipc2 = startIpcServer({
|
|
9935
|
+
localToken,
|
|
9936
|
+
tcpEnabled,
|
|
9937
|
+
publicHealthCheck: opts.publicHealthCheck,
|
|
9938
|
+
outboxDb,
|
|
9939
|
+
inboxDb,
|
|
9940
|
+
bus,
|
|
9941
|
+
broker,
|
|
9942
|
+
onPendingInserted: () => drain?.wake()
|
|
9943
|
+
});
|
|
9944
|
+
try {
|
|
9945
|
+
await ipc2.ready;
|
|
9946
|
+
} catch (err) {
|
|
9947
|
+
process.stderr.write(`ipc listen failed: ${String(err)}
|
|
9948
|
+
`);
|
|
9949
|
+
releaseSingletonLock();
|
|
9950
|
+
return 1;
|
|
9951
|
+
}
|
|
9952
|
+
process.stdout.write(JSON.stringify({
|
|
9953
|
+
msg: "daemon_started",
|
|
9954
|
+
pid: process.pid,
|
|
9955
|
+
sock: DAEMON_PATHS.SOCK_FILE,
|
|
9956
|
+
tcp: tcpEnabled ? `127.0.0.1:47823` : null,
|
|
9957
|
+
mesh: mesh.slug,
|
|
9958
|
+
ts: new Date().toISOString()
|
|
9959
|
+
}) + `
|
|
9960
|
+
`);
|
|
9961
|
+
let shuttingDown = false;
|
|
9962
|
+
const shutdown = async (sig) => {
|
|
9963
|
+
if (shuttingDown)
|
|
9964
|
+
return;
|
|
9965
|
+
shuttingDown = true;
|
|
9966
|
+
process.stdout.write(JSON.stringify({ msg: "daemon_shutdown", signal: sig, ts: new Date().toISOString() }) + `
|
|
9967
|
+
`);
|
|
9968
|
+
if (drain)
|
|
9969
|
+
await drain.close();
|
|
9970
|
+
await broker.close();
|
|
9971
|
+
await ipc2.close();
|
|
9972
|
+
try {
|
|
9973
|
+
outboxDb.close();
|
|
9974
|
+
} catch {}
|
|
9975
|
+
try {
|
|
9976
|
+
inboxDb.close();
|
|
9977
|
+
} catch {}
|
|
9978
|
+
releaseSingletonLock();
|
|
9979
|
+
process.exit(0);
|
|
9980
|
+
};
|
|
9981
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
9982
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
9983
|
+
return new Promise(() => {});
|
|
9984
|
+
}
|
|
9985
|
+
var init_run = __esm(() => {
|
|
9986
|
+
init_paths2();
|
|
9987
|
+
init_lock();
|
|
9988
|
+
init_local_token();
|
|
9989
|
+
init_server();
|
|
9990
|
+
init_broker();
|
|
9991
|
+
init_drain();
|
|
9992
|
+
init_inbound();
|
|
9993
|
+
init_identity();
|
|
9994
|
+
init_facade();
|
|
9995
|
+
});
|
|
9996
|
+
|
|
9997
|
+
// src/daemon/service-install.ts
|
|
9998
|
+
var exports_service_install = {};
|
|
9999
|
+
__export(exports_service_install, {
|
|
10000
|
+
uninstallService: () => uninstallService,
|
|
10001
|
+
readInstalledUnit: () => readInstalledUnit,
|
|
10002
|
+
installService: () => installService,
|
|
10003
|
+
detectPlatform: () => detectPlatform
|
|
10004
|
+
});
|
|
10005
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
10006
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
10007
|
+
import { homedir as homedir6 } from "node:os";
|
|
10008
|
+
import { join as join8, dirname as dirname5 } from "node:path";
|
|
10009
|
+
function detectPlatform() {
|
|
10010
|
+
if (process.platform === "darwin")
|
|
10011
|
+
return "darwin";
|
|
10012
|
+
if (process.platform === "linux")
|
|
10013
|
+
return "linux";
|
|
10014
|
+
return null;
|
|
10015
|
+
}
|
|
10016
|
+
function isCi() {
|
|
10017
|
+
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.BUILDKITE || process.env.CIRCLECI || process.env.JENKINS_URL);
|
|
10018
|
+
}
|
|
10019
|
+
function installService(args) {
|
|
10020
|
+
const platform5 = detectPlatform();
|
|
10021
|
+
if (!platform5)
|
|
10022
|
+
throw new Error(`unsupported platform: ${process.platform}`);
|
|
10023
|
+
if (isCi() && !args.allowCi) {
|
|
10024
|
+
throw new Error("Refusing to install persistent service in CI; pass --allow-ci-persistent to override.");
|
|
10025
|
+
}
|
|
10026
|
+
if (!existsSync12(args.binaryPath)) {
|
|
10027
|
+
throw new Error(`binary not found at ${args.binaryPath}`);
|
|
10028
|
+
}
|
|
10029
|
+
mkdirSync7(DAEMON_PATHS.DAEMON_DIR, { recursive: true, mode: 448 });
|
|
10030
|
+
if (platform5 === "darwin")
|
|
10031
|
+
return installDarwin(args);
|
|
10032
|
+
return installLinux(args);
|
|
10033
|
+
}
|
|
10034
|
+
function uninstallService() {
|
|
10035
|
+
const platform5 = detectPlatform();
|
|
10036
|
+
const removed = [];
|
|
10037
|
+
if (platform5 === "darwin") {
|
|
10038
|
+
const p = darwinPlistPath();
|
|
10039
|
+
try {
|
|
10040
|
+
execSync2(`launchctl bootout gui/$(id -u)/${SERVICE_LABEL}`, { stdio: "ignore" });
|
|
10041
|
+
} catch {}
|
|
10042
|
+
if (existsSync12(p)) {
|
|
10043
|
+
unlinkSync4(p);
|
|
10044
|
+
removed.push(p);
|
|
10045
|
+
}
|
|
10046
|
+
} else if (platform5 === "linux") {
|
|
10047
|
+
const p = linuxUnitPath();
|
|
10048
|
+
try {
|
|
10049
|
+
execSync2(`systemctl --user disable --now ${SYSTEMD_UNIT}`, { stdio: "ignore" });
|
|
10050
|
+
} catch {}
|
|
10051
|
+
if (existsSync12(p)) {
|
|
10052
|
+
unlinkSync4(p);
|
|
10053
|
+
removed.push(p);
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
return { platform: platform5, removed };
|
|
10057
|
+
}
|
|
10058
|
+
function darwinPlistPath() {
|
|
10059
|
+
return join8(homedir6(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
10060
|
+
}
|
|
10061
|
+
function installDarwin(args) {
|
|
10062
|
+
const plist = darwinPlistPath();
|
|
10063
|
+
mkdirSync7(dirname5(plist), { recursive: true });
|
|
10064
|
+
const log2 = DAEMON_PATHS.LOG_FILE;
|
|
10065
|
+
const meshArgs = [
|
|
10066
|
+
"<string>daemon</string>",
|
|
10067
|
+
"<string>up</string>",
|
|
10068
|
+
"<string>--mesh</string>",
|
|
10069
|
+
`<string>${escapeXml(args.meshSlug)}</string>`,
|
|
10070
|
+
...args.displayName ? ["<string>--name</string>", `<string>${escapeXml(args.displayName)}</string>`] : []
|
|
10071
|
+
].join(`
|
|
10072
|
+
`);
|
|
10073
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
10074
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
10075
|
+
<plist version="1.0">
|
|
10076
|
+
<dict>
|
|
10077
|
+
<key>Label</key>
|
|
10078
|
+
<string>${SERVICE_LABEL}</string>
|
|
10079
|
+
<key>ProgramArguments</key>
|
|
10080
|
+
<array>
|
|
10081
|
+
<string>${escapeXml(args.binaryPath)}</string>
|
|
10082
|
+
${meshArgs}
|
|
10083
|
+
</array>
|
|
10084
|
+
<key>RunAtLoad</key>
|
|
10085
|
+
<true/>
|
|
10086
|
+
<key>KeepAlive</key>
|
|
10087
|
+
<true/>
|
|
10088
|
+
<key>StandardOutPath</key>
|
|
10089
|
+
<string>${escapeXml(log2)}</string>
|
|
10090
|
+
<key>StandardErrorPath</key>
|
|
10091
|
+
<string>${escapeXml(log2)}</string>
|
|
10092
|
+
<key>WorkingDirectory</key>
|
|
10093
|
+
<string>${escapeXml(homedir6())}</string>
|
|
10094
|
+
<key>EnvironmentVariables</key>
|
|
10095
|
+
<dict>
|
|
10096
|
+
<key>HOME</key>
|
|
10097
|
+
<string>${escapeXml(homedir6())}</string>
|
|
10098
|
+
<key>PATH</key>
|
|
10099
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
10100
|
+
</dict>
|
|
10101
|
+
</dict>
|
|
10102
|
+
</plist>
|
|
10103
|
+
`;
|
|
10104
|
+
writeFileSync9(plist, xml, { mode: 420 });
|
|
10105
|
+
return {
|
|
10106
|
+
platform: "darwin",
|
|
10107
|
+
unitPath: plist,
|
|
10108
|
+
bootCommand: `launchctl bootstrap gui/$(id -u) ${shellQuote(plist)}`
|
|
10109
|
+
};
|
|
10110
|
+
}
|
|
10111
|
+
function linuxUnitPath() {
|
|
10112
|
+
return join8(homedir6(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
10113
|
+
}
|
|
10114
|
+
function installLinux(args) {
|
|
10115
|
+
const unit = linuxUnitPath();
|
|
10116
|
+
mkdirSync7(dirname5(unit), { recursive: true });
|
|
10117
|
+
const execArgs = [
|
|
10118
|
+
"daemon",
|
|
10119
|
+
"up",
|
|
10120
|
+
"--mesh",
|
|
10121
|
+
args.meshSlug,
|
|
10122
|
+
...args.displayName ? ["--name", args.displayName] : []
|
|
10123
|
+
].map(shellQuote).join(" ");
|
|
10124
|
+
const content = `[Unit]
|
|
10125
|
+
Description=claudemesh daemon (peer mesh runtime)
|
|
10126
|
+
After=network-online.target
|
|
10127
|
+
Wants=network-online.target
|
|
10128
|
+
|
|
10129
|
+
[Service]
|
|
10130
|
+
Type=simple
|
|
10131
|
+
ExecStart=${shellQuote(args.binaryPath)} ${execArgs}
|
|
10132
|
+
Restart=always
|
|
10133
|
+
RestartSec=3
|
|
10134
|
+
StandardOutput=append:${DAEMON_PATHS.LOG_FILE}
|
|
10135
|
+
StandardError=append:${DAEMON_PATHS.LOG_FILE}
|
|
10136
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
10137
|
+
|
|
10138
|
+
[Install]
|
|
10139
|
+
WantedBy=default.target
|
|
10140
|
+
`;
|
|
10141
|
+
writeFileSync9(unit, content, { mode: 420 });
|
|
10142
|
+
return {
|
|
10143
|
+
platform: "linux",
|
|
10144
|
+
unitPath: unit,
|
|
10145
|
+
bootCommand: `systemctl --user daemon-reload && systemctl --user enable --now ${SYSTEMD_UNIT}`
|
|
10146
|
+
};
|
|
10147
|
+
}
|
|
10148
|
+
function escapeXml(s) {
|
|
10149
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10150
|
+
}
|
|
10151
|
+
function shellQuote(s) {
|
|
10152
|
+
if (/^[\w@%+=:,./-]+$/.test(s))
|
|
10153
|
+
return s;
|
|
10154
|
+
return "'" + s.replace(/'/g, `'"'"'`) + "'";
|
|
10155
|
+
}
|
|
10156
|
+
function readInstalledUnit() {
|
|
10157
|
+
const platform5 = detectPlatform();
|
|
10158
|
+
if (!platform5)
|
|
10159
|
+
return { platform: null, path: null, content: null };
|
|
10160
|
+
const path2 = platform5 === "darwin" ? darwinPlistPath() : linuxUnitPath();
|
|
10161
|
+
if (!existsSync12(path2))
|
|
10162
|
+
return { platform: platform5, path: null, content: null };
|
|
10163
|
+
try {
|
|
10164
|
+
return { platform: platform5, path: path2, content: readFileSync9(path2, "utf8") };
|
|
10165
|
+
} catch {
|
|
10166
|
+
return { platform: platform5, path: path2, content: null };
|
|
10167
|
+
}
|
|
10168
|
+
}
|
|
10169
|
+
var SERVICE_LABEL = "com.claudemesh.daemon", SYSTEMD_UNIT = "claudemesh-daemon.service";
|
|
10170
|
+
var init_service_install = __esm(() => {
|
|
10171
|
+
init_paths2();
|
|
10172
|
+
});
|
|
10173
|
+
|
|
10174
|
+
// src/commands/daemon.ts
|
|
10175
|
+
var exports_daemon = {};
|
|
10176
|
+
__export(exports_daemon, {
|
|
10177
|
+
runDaemonCommand: () => runDaemonCommand
|
|
10178
|
+
});
|
|
10179
|
+
async function runDaemonCommand(sub, opts, rest = []) {
|
|
10180
|
+
switch (sub) {
|
|
10181
|
+
case undefined:
|
|
10182
|
+
return printDaemonUsage();
|
|
10183
|
+
case "up":
|
|
10184
|
+
case "start":
|
|
10185
|
+
return runDaemon({
|
|
10186
|
+
tcpEnabled: !opts.noTcp,
|
|
10187
|
+
publicHealthCheck: opts.publicHealth,
|
|
10188
|
+
mesh: opts.mesh,
|
|
10189
|
+
displayName: opts.displayName
|
|
10190
|
+
});
|
|
10191
|
+
case "help":
|
|
10192
|
+
case "--help":
|
|
10193
|
+
case "-h":
|
|
10194
|
+
return printDaemonUsage();
|
|
10195
|
+
case "status":
|
|
10196
|
+
return runStatus(opts);
|
|
10197
|
+
case "version":
|
|
10198
|
+
return runVersion(opts);
|
|
10199
|
+
case "down":
|
|
10200
|
+
case "stop":
|
|
10201
|
+
return runStop(opts);
|
|
10202
|
+
case "accept-host":
|
|
10203
|
+
return runAcceptHost(opts);
|
|
10204
|
+
case "outbox":
|
|
10205
|
+
return runOutbox(rest, opts);
|
|
10206
|
+
case "install-service":
|
|
10207
|
+
return runInstallService(opts);
|
|
10208
|
+
case "uninstall-service":
|
|
10209
|
+
return runUninstallService(opts);
|
|
10210
|
+
default:
|
|
10211
|
+
process.stderr.write(`unknown daemon subcommand: ${sub}
|
|
10212
|
+
|
|
10213
|
+
`);
|
|
10214
|
+
printDaemonUsage(process.stderr);
|
|
10215
|
+
return 2;
|
|
10216
|
+
}
|
|
10217
|
+
}
|
|
10218
|
+
function printDaemonUsage(stream = process.stdout) {
|
|
10219
|
+
stream.write(`claudemesh daemon — long-lived peer mesh runtime (v0.9.0)
|
|
10220
|
+
|
|
10221
|
+
USAGE
|
|
10222
|
+
claudemesh daemon <command> [options]
|
|
10223
|
+
|
|
10224
|
+
COMMANDS
|
|
10225
|
+
up | start start the daemon in the foreground
|
|
10226
|
+
status show running pid + IPC health
|
|
10227
|
+
version ipc + schema version of the running daemon
|
|
10228
|
+
down | stop stop the running daemon (SIGTERM, then wait)
|
|
10229
|
+
accept-host pin the current host fingerprint
|
|
10230
|
+
outbox list list local outbox rows (newest first)
|
|
10231
|
+
outbox requeue <id> re-enqueue an aborted / dead outbox row
|
|
10232
|
+
install-service --mesh <s> write launchd (macOS) / systemd-user (Linux) unit
|
|
10233
|
+
uninstall-service remove the platform service unit
|
|
10234
|
+
|
|
10235
|
+
OPTIONS
|
|
10236
|
+
--mesh <slug> attach to / target this mesh
|
|
10237
|
+
--name <displayName> override CLAUDEMESH_DISPLAY_NAME
|
|
10238
|
+
--no-tcp disable the loopback TCP listener (UDS only)
|
|
10239
|
+
--public-health expose /v1/health unauthenticated on TCP
|
|
10240
|
+
--json machine-readable output where supported
|
|
10241
|
+
|
|
10242
|
+
OUTBOX FLAGS (for 'daemon outbox list')
|
|
10243
|
+
--pending --inflight --done --failed --aborted filter by status
|
|
10244
|
+
|
|
10245
|
+
OUTBOX FLAGS (for 'daemon outbox requeue')
|
|
10246
|
+
--new-client-id <id> mint the new row with this client_message_id
|
|
10247
|
+
|
|
10248
|
+
See ${"https://claudemesh.com/docs"} for the full daemon spec.
|
|
10249
|
+
`);
|
|
10250
|
+
return 0;
|
|
10251
|
+
}
|
|
10252
|
+
async function runOutbox(rest, opts) {
|
|
10253
|
+
const sub = rest[0];
|
|
10254
|
+
switch (sub) {
|
|
10255
|
+
case undefined:
|
|
10256
|
+
case "list": {
|
|
10257
|
+
const status = opts.outboxStatus;
|
|
10258
|
+
const path2 = `/v1/outbox${status ? `?status=${status}` : ""}`;
|
|
10259
|
+
try {
|
|
10260
|
+
const res = await ipc({ path: path2 });
|
|
10261
|
+
if (opts.json) {
|
|
10262
|
+
process.stdout.write(JSON.stringify(res.body) + `
|
|
10263
|
+
`);
|
|
10264
|
+
return 0;
|
|
10265
|
+
}
|
|
10266
|
+
if (!res.body.items?.length) {
|
|
10267
|
+
process.stdout.write(`(empty)
|
|
10268
|
+
`);
|
|
10269
|
+
return 0;
|
|
10270
|
+
}
|
|
10271
|
+
for (const r of res.body.items) {
|
|
10272
|
+
const tag = r.status.padEnd(8);
|
|
10273
|
+
const bm = r.broker_message_id ? ` → ${r.broker_message_id}` : "";
|
|
10274
|
+
const err = r.last_error ? ` last_error="${r.last_error.slice(0, 60)}"` : "";
|
|
10275
|
+
process.stdout.write(`${tag} ${r.id} cid=${r.client_message_id} attempts=${r.attempts}${bm}${err}
|
|
10276
|
+
`);
|
|
10277
|
+
}
|
|
10278
|
+
return 0;
|
|
10279
|
+
} catch (err) {
|
|
10280
|
+
process.stderr.write(`daemon unreachable: ${String(err)}
|
|
10281
|
+
`);
|
|
10282
|
+
return 1;
|
|
10283
|
+
}
|
|
10284
|
+
}
|
|
10285
|
+
case "requeue": {
|
|
10286
|
+
const id = rest[1];
|
|
10287
|
+
if (!id) {
|
|
10288
|
+
process.stderr.write(`usage: claudemesh daemon outbox requeue <id> [--new-client-id <id>]
|
|
10289
|
+
`);
|
|
10290
|
+
return 2;
|
|
8007
10291
|
}
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
const
|
|
8011
|
-
|
|
8012
|
-
|
|
10292
|
+
const newClientMessageId = opts.newClientId;
|
|
10293
|
+
try {
|
|
10294
|
+
const res = await ipc({
|
|
10295
|
+
method: "POST",
|
|
10296
|
+
path: "/v1/outbox/requeue",
|
|
10297
|
+
body: { id, new_client_message_id: newClientMessageId }
|
|
10298
|
+
});
|
|
10299
|
+
if (res.status === 200) {
|
|
10300
|
+
if (opts.json)
|
|
10301
|
+
process.stdout.write(JSON.stringify(res.body) + `
|
|
8013
10302
|
`);
|
|
8014
|
-
|
|
8015
|
-
|
|
10303
|
+
else
|
|
10304
|
+
process.stdout.write(`requeued: aborted ${res.body.aborted_row_id} → new ${res.body.new_row_id} ` + `(client_message_id=${res.body.new_client_message_id})
|
|
10305
|
+
`);
|
|
10306
|
+
return 0;
|
|
10307
|
+
}
|
|
10308
|
+
process.stderr.write(`requeue failed (${res.status}): ${res.body.error ?? "unknown"}
|
|
10309
|
+
`);
|
|
10310
|
+
return 1;
|
|
10311
|
+
} catch (err) {
|
|
10312
|
+
process.stderr.write(`daemon unreachable: ${String(err)}
|
|
8016
10313
|
`);
|
|
10314
|
+
return 1;
|
|
8017
10315
|
}
|
|
8018
|
-
});
|
|
8019
|
-
return;
|
|
8020
|
-
}
|
|
8021
|
-
if (action === "cancel") {
|
|
8022
|
-
const id = positional[1];
|
|
8023
|
-
if (!id) {
|
|
8024
|
-
render.err("Usage: claudemesh remind cancel <id>");
|
|
8025
|
-
process.exit(1);
|
|
8026
10316
|
}
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
process.exit(1);
|
|
8034
|
-
}
|
|
8035
|
-
});
|
|
8036
|
-
return;
|
|
10317
|
+
default:
|
|
10318
|
+
process.stderr.write(`unknown outbox subcommand: ${sub}
|
|
10319
|
+
`);
|
|
10320
|
+
process.stderr.write(`usage: claudemesh daemon outbox [list|requeue <id>]
|
|
10321
|
+
`);
|
|
10322
|
+
return 2;
|
|
8037
10323
|
}
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
10324
|
+
}
|
|
10325
|
+
async function runInstallService(opts) {
|
|
10326
|
+
const { installService: installService2, detectPlatform: detectPlatform2 } = await Promise.resolve().then(() => (init_service_install(), exports_service_install));
|
|
10327
|
+
const platform5 = detectPlatform2();
|
|
10328
|
+
if (!platform5) {
|
|
10329
|
+
process.stderr.write(`unsupported platform: ${process.platform}
|
|
10330
|
+
`);
|
|
10331
|
+
return 2;
|
|
8046
10332
|
}
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
process.exit(1);
|
|
10333
|
+
if (!opts.mesh) {
|
|
10334
|
+
process.stderr.write(`pass --mesh <slug> so the service knows which mesh to attach to
|
|
10335
|
+
`);
|
|
10336
|
+
return 2;
|
|
8052
10337
|
}
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
render.err(`Peer "${flags.to}" not found`, `online: ${peers.map((p) => p.displayName).join(", ") || "(none)"}`);
|
|
8063
|
-
process.exit(1);
|
|
8064
|
-
}
|
|
8065
|
-
targetSpec = match.pubkey;
|
|
8066
|
-
}
|
|
8067
|
-
} else {
|
|
8068
|
-
targetSpec = client.getSessionPubkey() ?? "*";
|
|
8069
|
-
}
|
|
8070
|
-
const result = await client.scheduleMessage(targetSpec, message, deliverAt ?? 0, false, flags.cron);
|
|
8071
|
-
if (!result) {
|
|
8072
|
-
render.err("Broker did not acknowledge — check connection");
|
|
8073
|
-
process.exit(1);
|
|
8074
|
-
}
|
|
8075
|
-
if (flags.json) {
|
|
8076
|
-
console.log(JSON.stringify(result));
|
|
8077
|
-
return;
|
|
10338
|
+
let binary = process.argv[1] ?? "";
|
|
10339
|
+
if (!binary || /\.ts$/.test(binary) || /node_modules|src\/entrypoints/.test(binary)) {
|
|
10340
|
+
try {
|
|
10341
|
+
const { execSync: execSync3 } = await import("node:child_process");
|
|
10342
|
+
binary = execSync3("which claudemesh", { encoding: "utf8" }).trim();
|
|
10343
|
+
} catch {
|
|
10344
|
+
process.stderr.write(`couldn't resolve a 'claudemesh' binary on PATH; install via npm/homebrew first
|
|
10345
|
+
`);
|
|
10346
|
+
return 1;
|
|
8078
10347
|
}
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
10348
|
+
}
|
|
10349
|
+
try {
|
|
10350
|
+
const r = installService2({
|
|
10351
|
+
binaryPath: binary,
|
|
10352
|
+
meshSlug: opts.mesh,
|
|
10353
|
+
displayName: opts.displayName
|
|
10354
|
+
});
|
|
10355
|
+
if (opts.json) {
|
|
10356
|
+
process.stdout.write(JSON.stringify({ ok: true, ...r }) + `
|
|
10357
|
+
`);
|
|
8083
10358
|
} else {
|
|
8084
|
-
|
|
8085
|
-
|
|
10359
|
+
process.stdout.write(`installed ${r.platform} service unit: ${r.unitPath}
|
|
10360
|
+
`);
|
|
10361
|
+
process.stdout.write(`bring it up now: ${r.bootCommand}
|
|
10362
|
+
`);
|
|
8086
10363
|
}
|
|
8087
|
-
|
|
10364
|
+
return 0;
|
|
10365
|
+
} catch (err) {
|
|
10366
|
+
process.stderr.write(`install-service failed: ${String(err)}
|
|
10367
|
+
`);
|
|
10368
|
+
return 1;
|
|
10369
|
+
}
|
|
8088
10370
|
}
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
}
|
|
8100
|
-
|
|
8101
|
-
return
|
|
10371
|
+
async function runUninstallService(opts) {
|
|
10372
|
+
const { uninstallService: uninstallService2 } = await Promise.resolve().then(() => (init_service_install(), exports_service_install));
|
|
10373
|
+
const r = uninstallService2();
|
|
10374
|
+
if (opts.json)
|
|
10375
|
+
process.stdout.write(JSON.stringify(r) + `
|
|
10376
|
+
`);
|
|
10377
|
+
else if (r.removed.length === 0)
|
|
10378
|
+
process.stdout.write(`no service unit installed
|
|
10379
|
+
`);
|
|
10380
|
+
else
|
|
10381
|
+
process.stdout.write(`removed: ${r.removed.join(", ")}
|
|
10382
|
+
`);
|
|
10383
|
+
return 0;
|
|
8102
10384
|
}
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
10385
|
+
async function runAcceptHost(opts) {
|
|
10386
|
+
const { acceptCurrentHost: acceptCurrentHost2 } = await Promise.resolve().then(() => (init_identity(), exports_identity));
|
|
10387
|
+
const fp = acceptCurrentHost2();
|
|
10388
|
+
if (opts.json)
|
|
10389
|
+
process.stdout.write(JSON.stringify({ ok: true, fingerprint_prefix: fp.fingerprint.slice(0, 16) }) + `
|
|
10390
|
+
`);
|
|
10391
|
+
else
|
|
10392
|
+
process.stdout.write(`host fingerprint accepted: ${fp.fingerprint.slice(0, 16)}…
|
|
10393
|
+
`);
|
|
10394
|
+
return 0;
|
|
10395
|
+
}
|
|
10396
|
+
async function runStatus(opts) {
|
|
10397
|
+
const pid = readRunningPid();
|
|
10398
|
+
if (!pid) {
|
|
10399
|
+
if (opts.json)
|
|
10400
|
+
process.stdout.write(JSON.stringify({ running: false }) + `
|
|
10401
|
+
`);
|
|
10402
|
+
else
|
|
10403
|
+
process.stdout.write(`daemon: not running
|
|
10404
|
+
`);
|
|
10405
|
+
return 1;
|
|
10406
|
+
}
|
|
8113
10407
|
try {
|
|
8114
|
-
const
|
|
8115
|
-
if (
|
|
8116
|
-
|
|
10408
|
+
const res = await ipc({ path: "/v1/health" });
|
|
10409
|
+
if (opts.json) {
|
|
10410
|
+
process.stdout.write(JSON.stringify({ running: true, pid, health: res.body }) + `
|
|
10411
|
+
`);
|
|
8117
10412
|
} else {
|
|
8118
|
-
|
|
8119
|
-
|
|
10413
|
+
process.stdout.write(`daemon: running (pid ${pid})
|
|
10414
|
+
`);
|
|
10415
|
+
process.stdout.write(`socket: ${DAEMON_PATHS.SOCK_FILE}
|
|
10416
|
+
`);
|
|
8120
10417
|
}
|
|
8121
|
-
|
|
8122
|
-
return EXIT.SUCCESS;
|
|
10418
|
+
return 0;
|
|
8123
10419
|
} catch (err) {
|
|
8124
|
-
|
|
8125
|
-
|
|
10420
|
+
if (opts.json)
|
|
10421
|
+
process.stdout.write(JSON.stringify({ running: true, pid, ipc_error: String(err) }) + `
|
|
10422
|
+
`);
|
|
10423
|
+
else
|
|
10424
|
+
process.stdout.write(`daemon: pid ${pid} alive but IPC unreachable (${String(err)})
|
|
10425
|
+
`);
|
|
10426
|
+
return 1;
|
|
8126
10427
|
}
|
|
8127
10428
|
}
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
}
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
10429
|
+
async function runVersion(opts) {
|
|
10430
|
+
try {
|
|
10431
|
+
const res = await ipc({ path: "/v1/version" });
|
|
10432
|
+
if (opts.json)
|
|
10433
|
+
process.stdout.write(JSON.stringify(res.body) + `
|
|
10434
|
+
`);
|
|
10435
|
+
else {
|
|
10436
|
+
const v = res.body;
|
|
10437
|
+
process.stdout.write(`daemon ${v.daemon_version ?? "unknown"} (ipc ${v.ipc_api ?? "?"}, schema ${v.schema_version ?? "?"})
|
|
10438
|
+
`);
|
|
10439
|
+
}
|
|
10440
|
+
return 0;
|
|
10441
|
+
} catch (err) {
|
|
10442
|
+
if (err instanceof IpcError) {
|
|
10443
|
+
process.stderr.write(`${err.message}
|
|
10444
|
+
`);
|
|
10445
|
+
return err.status === 401 ? 3 : 1;
|
|
10446
|
+
}
|
|
10447
|
+
process.stderr.write(`daemon unreachable: ${String(err)}
|
|
10448
|
+
`);
|
|
10449
|
+
return 1;
|
|
8144
10450
|
}
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
10451
|
+
}
|
|
10452
|
+
async function runStop(opts) {
|
|
10453
|
+
const pid = readRunningPid();
|
|
10454
|
+
if (!pid) {
|
|
10455
|
+
if (opts.json)
|
|
10456
|
+
process.stdout.write(JSON.stringify({ stopped: false, reason: "not_running" }) + `
|
|
10457
|
+
`);
|
|
10458
|
+
else
|
|
10459
|
+
process.stdout.write(`daemon: not running
|
|
10460
|
+
`);
|
|
10461
|
+
return 0;
|
|
8148
10462
|
}
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
]);
|
|
8156
|
-
} else {
|
|
8157
|
-
render.kv([
|
|
8158
|
-
["web", dim("not signed in · run `claudemesh login` for account features")]
|
|
8159
|
-
]);
|
|
10463
|
+
try {
|
|
10464
|
+
process.kill(pid, "SIGTERM");
|
|
10465
|
+
} catch (err) {
|
|
10466
|
+
process.stderr.write(`failed to signal pid ${pid}: ${String(err)}
|
|
10467
|
+
`);
|
|
10468
|
+
return 1;
|
|
8160
10469
|
}
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
10470
|
+
for (let i = 0;i < 50; i++) {
|
|
10471
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
10472
|
+
if (!readRunningPid()) {
|
|
10473
|
+
if (opts.json)
|
|
10474
|
+
process.stdout.write(JSON.stringify({ stopped: true, pid }) + `
|
|
10475
|
+
`);
|
|
10476
|
+
else
|
|
10477
|
+
process.stdout.write(`daemon: stopped (was pid ${pid})
|
|
10478
|
+
`);
|
|
10479
|
+
return 0;
|
|
8168
10480
|
}
|
|
8169
10481
|
}
|
|
8170
|
-
|
|
8171
|
-
|
|
10482
|
+
if (opts.json)
|
|
10483
|
+
process.stdout.write(JSON.stringify({ stopped: false, pid, reason: "shutdown_timeout" }) + `
|
|
10484
|
+
`);
|
|
10485
|
+
else
|
|
10486
|
+
process.stdout.write(`daemon: signaled but did not exit within 5s (pid ${pid})
|
|
10487
|
+
`);
|
|
10488
|
+
return 1;
|
|
8172
10489
|
}
|
|
8173
|
-
var
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
10490
|
+
var init_daemon = __esm(() => {
|
|
10491
|
+
init_run();
|
|
10492
|
+
init_client4();
|
|
10493
|
+
init_lock();
|
|
10494
|
+
init_paths2();
|
|
8178
10495
|
});
|
|
8179
10496
|
|
|
8180
10497
|
// src/commands/install.ts
|
|
@@ -8184,21 +10501,21 @@ __export(exports_install, {
|
|
|
8184
10501
|
runInstall: () => runInstall
|
|
8185
10502
|
});
|
|
8186
10503
|
import {
|
|
8187
|
-
chmodSync as
|
|
10504
|
+
chmodSync as chmodSync4,
|
|
8188
10505
|
copyFileSync,
|
|
8189
|
-
existsSync as
|
|
8190
|
-
mkdirSync as
|
|
8191
|
-
readFileSync as
|
|
8192
|
-
writeFileSync as
|
|
10506
|
+
existsSync as existsSync13,
|
|
10507
|
+
mkdirSync as mkdirSync8,
|
|
10508
|
+
readFileSync as readFileSync10,
|
|
10509
|
+
writeFileSync as writeFileSync10
|
|
8193
10510
|
} from "node:fs";
|
|
8194
|
-
import { homedir as
|
|
8195
|
-
import { dirname as
|
|
10511
|
+
import { homedir as homedir7, platform as platform5 } from "node:os";
|
|
10512
|
+
import { dirname as dirname6, join as join9, resolve } from "node:path";
|
|
8196
10513
|
import { fileURLToPath } from "node:url";
|
|
8197
10514
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
8198
10515
|
function readClaudeConfig() {
|
|
8199
|
-
if (!
|
|
10516
|
+
if (!existsSync13(CLAUDE_CONFIG))
|
|
8200
10517
|
return {};
|
|
8201
|
-
const text =
|
|
10518
|
+
const text = readFileSync10(CLAUDE_CONFIG, "utf-8").trim();
|
|
8202
10519
|
if (!text)
|
|
8203
10520
|
return {};
|
|
8204
10521
|
try {
|
|
@@ -8208,12 +10525,12 @@ function readClaudeConfig() {
|
|
|
8208
10525
|
}
|
|
8209
10526
|
}
|
|
8210
10527
|
function backupClaudeConfig() {
|
|
8211
|
-
if (!
|
|
10528
|
+
if (!existsSync13(CLAUDE_CONFIG))
|
|
8212
10529
|
return;
|
|
8213
|
-
const backupDir =
|
|
8214
|
-
|
|
10530
|
+
const backupDir = join9(dirname6(CLAUDE_CONFIG), ".claude", "backups");
|
|
10531
|
+
mkdirSync8(backupDir, { recursive: true });
|
|
8215
10532
|
const ts = Date.now();
|
|
8216
|
-
const dest =
|
|
10533
|
+
const dest = join9(backupDir, `.claude.json.pre-claudemesh.${ts}`);
|
|
8217
10534
|
copyFileSync(CLAUDE_CONFIG, dest);
|
|
8218
10535
|
}
|
|
8219
10536
|
function patchMcpServer(entry) {
|
|
@@ -8237,7 +10554,7 @@ function patchMcpServer(entry) {
|
|
|
8237
10554
|
return action;
|
|
8238
10555
|
}
|
|
8239
10556
|
function removeMcpServer() {
|
|
8240
|
-
if (!
|
|
10557
|
+
if (!existsSync13(CLAUDE_CONFIG))
|
|
8241
10558
|
return false;
|
|
8242
10559
|
backupClaudeConfig();
|
|
8243
10560
|
const cfg = readClaudeConfig();
|
|
@@ -8250,11 +10567,11 @@ function removeMcpServer() {
|
|
|
8250
10567
|
return true;
|
|
8251
10568
|
}
|
|
8252
10569
|
function flushClaudeConfig(obj) {
|
|
8253
|
-
|
|
8254
|
-
|
|
10570
|
+
mkdirSync8(dirname6(CLAUDE_CONFIG), { recursive: true });
|
|
10571
|
+
writeFileSync10(CLAUDE_CONFIG, JSON.stringify(obj, null, 2) + `
|
|
8255
10572
|
`, "utf-8");
|
|
8256
10573
|
try {
|
|
8257
|
-
|
|
10574
|
+
chmodSync4(CLAUDE_CONFIG, 384);
|
|
8258
10575
|
} catch {}
|
|
8259
10576
|
}
|
|
8260
10577
|
function bunAvailable() {
|
|
@@ -8268,13 +10585,13 @@ function resolveEntry() {
|
|
|
8268
10585
|
const here = fileURLToPath(import.meta.url);
|
|
8269
10586
|
if (isBundledFile(here))
|
|
8270
10587
|
return here;
|
|
8271
|
-
return resolve(
|
|
10588
|
+
return resolve(dirname6(here), "..", "index.ts");
|
|
8272
10589
|
}
|
|
8273
10590
|
function resolveBundledSkillsDir() {
|
|
8274
10591
|
const here = fileURLToPath(import.meta.url);
|
|
8275
|
-
const pkgRoot = resolve(
|
|
8276
|
-
const skillsDir =
|
|
8277
|
-
if (
|
|
10592
|
+
const pkgRoot = resolve(dirname6(here), "..", "..");
|
|
10593
|
+
const skillsDir = join9(pkgRoot, "skills");
|
|
10594
|
+
if (existsSync13(skillsDir))
|
|
8278
10595
|
return skillsDir;
|
|
8279
10596
|
return null;
|
|
8280
10597
|
}
|
|
@@ -8287,13 +10604,13 @@ function installSkills() {
|
|
|
8287
10604
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
8288
10605
|
if (!entry.isDirectory())
|
|
8289
10606
|
continue;
|
|
8290
|
-
const srcDir =
|
|
8291
|
-
const dstDir =
|
|
8292
|
-
|
|
10607
|
+
const srcDir = join9(src, entry.name);
|
|
10608
|
+
const dstDir = join9(CLAUDE_SKILLS_ROOT, entry.name);
|
|
10609
|
+
mkdirSync8(dstDir, { recursive: true });
|
|
8293
10610
|
for (const file of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
8294
10611
|
if (!file.isFile())
|
|
8295
10612
|
continue;
|
|
8296
|
-
copyFileSync(
|
|
10613
|
+
copyFileSync(join9(srcDir, file.name), join9(dstDir, file.name));
|
|
8297
10614
|
}
|
|
8298
10615
|
installed.push(entry.name);
|
|
8299
10616
|
}
|
|
@@ -8308,8 +10625,8 @@ function uninstallSkills() {
|
|
|
8308
10625
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
8309
10626
|
if (!entry.isDirectory())
|
|
8310
10627
|
continue;
|
|
8311
|
-
const dstDir =
|
|
8312
|
-
if (
|
|
10628
|
+
const dstDir = join9(CLAUDE_SKILLS_ROOT, entry.name);
|
|
10629
|
+
if (existsSync13(dstDir)) {
|
|
8313
10630
|
try {
|
|
8314
10631
|
fs.rmSync(dstDir, { recursive: true, force: true });
|
|
8315
10632
|
removed.push(entry.name);
|
|
@@ -8334,9 +10651,9 @@ function entriesEqual(a, b) {
|
|
|
8334
10651
|
return a.command === b.command && JSON.stringify(a.args ?? []) === JSON.stringify(b.args ?? []);
|
|
8335
10652
|
}
|
|
8336
10653
|
function readClaudeSettings() {
|
|
8337
|
-
if (!
|
|
10654
|
+
if (!existsSync13(CLAUDE_SETTINGS))
|
|
8338
10655
|
return {};
|
|
8339
|
-
const text =
|
|
10656
|
+
const text = readFileSync10(CLAUDE_SETTINGS, "utf-8").trim();
|
|
8340
10657
|
if (!text)
|
|
8341
10658
|
return {};
|
|
8342
10659
|
try {
|
|
@@ -8346,8 +10663,8 @@ function readClaudeSettings() {
|
|
|
8346
10663
|
}
|
|
8347
10664
|
}
|
|
8348
10665
|
function writeClaudeSettings(obj) {
|
|
8349
|
-
|
|
8350
|
-
|
|
10666
|
+
mkdirSync8(dirname6(CLAUDE_SETTINGS), { recursive: true });
|
|
10667
|
+
writeFileSync10(CLAUDE_SETTINGS, JSON.stringify(obj, null, 2) + `
|
|
8351
10668
|
`, "utf-8");
|
|
8352
10669
|
}
|
|
8353
10670
|
function installAllowedTools() {
|
|
@@ -8361,7 +10678,7 @@ function installAllowedTools() {
|
|
|
8361
10678
|
return { added: toAdd, unchanged: CLAUDEMESH_TOOLS.length - toAdd.length };
|
|
8362
10679
|
}
|
|
8363
10680
|
function uninstallAllowedTools() {
|
|
8364
|
-
if (!
|
|
10681
|
+
if (!existsSync13(CLAUDE_SETTINGS))
|
|
8365
10682
|
return 0;
|
|
8366
10683
|
const settings = readClaudeSettings();
|
|
8367
10684
|
const existing = settings.allowedTools ?? [];
|
|
@@ -8396,7 +10713,7 @@ function installHooks() {
|
|
|
8396
10713
|
return { added, unchanged };
|
|
8397
10714
|
}
|
|
8398
10715
|
function uninstallHooks() {
|
|
8399
|
-
if (!
|
|
10716
|
+
if (!existsSync13(CLAUDE_SETTINGS))
|
|
8400
10717
|
return 0;
|
|
8401
10718
|
const settings = readClaudeSettings();
|
|
8402
10719
|
const hooks = settings.hooks;
|
|
@@ -8445,7 +10762,7 @@ function runInstall(args = []) {
|
|
|
8445
10762
|
render.err("`bun` is not on PATH.", "Install Bun first: https://bun.com");
|
|
8446
10763
|
process.exit(1);
|
|
8447
10764
|
}
|
|
8448
|
-
if (!
|
|
10765
|
+
if (!existsSync13(entry)) {
|
|
8449
10766
|
render.err(`MCP entry not found at ${entry}`);
|
|
8450
10767
|
process.exit(1);
|
|
8451
10768
|
}
|
|
@@ -8496,7 +10813,7 @@ function runInstall(args = []) {
|
|
|
8496
10813
|
const installed = installSkills();
|
|
8497
10814
|
if (installed.length > 0) {
|
|
8498
10815
|
render.ok(`Claude skill${installed.length === 1 ? "" : "s"} installed`, installed.join(", "));
|
|
8499
|
-
render.info(dim(` ${
|
|
10816
|
+
render.info(dim(` ${join9(CLAUDE_SKILLS_ROOT, installed[0])}/SKILL.md`));
|
|
8500
10817
|
}
|
|
8501
10818
|
} catch (e) {
|
|
8502
10819
|
render.warn(`skill install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -8584,9 +10901,9 @@ var init_install = __esm(() => {
|
|
|
8584
10901
|
init_facade();
|
|
8585
10902
|
init_render();
|
|
8586
10903
|
init_styles();
|
|
8587
|
-
CLAUDE_CONFIG =
|
|
8588
|
-
CLAUDE_SETTINGS =
|
|
8589
|
-
CLAUDE_SKILLS_ROOT =
|
|
10904
|
+
CLAUDE_CONFIG = join9(homedir7(), ".claude.json");
|
|
10905
|
+
CLAUDE_SETTINGS = join9(homedir7(), ".claude", "settings.json");
|
|
10906
|
+
CLAUDE_SKILLS_ROOT = join9(homedir7(), ".claude", "skills");
|
|
8590
10907
|
CLAUDEMESH_TOOLS = [
|
|
8591
10908
|
"mcp__claudemesh__cancel_scheduled",
|
|
8592
10909
|
"mcp__claudemesh__check_messages",
|
|
@@ -8641,35 +10958,35 @@ var exports_uninstall = {};
|
|
|
8641
10958
|
__export(exports_uninstall, {
|
|
8642
10959
|
uninstall: () => uninstall
|
|
8643
10960
|
});
|
|
8644
|
-
import { readFileSync as
|
|
8645
|
-
import { join as
|
|
8646
|
-
import { homedir as
|
|
10961
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync11, existsSync as existsSync14, rmSync as rmSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
10962
|
+
import { join as join10, dirname as dirname7 } from "node:path";
|
|
10963
|
+
import { homedir as homedir8 } from "node:os";
|
|
8647
10964
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8648
10965
|
function bundledSkillsDir() {
|
|
8649
10966
|
const here = fileURLToPath2(import.meta.url);
|
|
8650
|
-
const pkgRoot =
|
|
8651
|
-
const skillsDir =
|
|
8652
|
-
return
|
|
10967
|
+
const pkgRoot = join10(dirname7(here), "..", "..");
|
|
10968
|
+
const skillsDir = join10(pkgRoot, "skills");
|
|
10969
|
+
return existsSync14(skillsDir) ? skillsDir : null;
|
|
8653
10970
|
}
|
|
8654
10971
|
async function uninstall() {
|
|
8655
10972
|
let removed = 0;
|
|
8656
|
-
if (
|
|
10973
|
+
if (existsSync14(PATHS.CLAUDE_JSON)) {
|
|
8657
10974
|
try {
|
|
8658
|
-
const raw =
|
|
10975
|
+
const raw = readFileSync11(PATHS.CLAUDE_JSON, "utf-8");
|
|
8659
10976
|
const config = JSON.parse(raw);
|
|
8660
10977
|
const servers = config.mcpServers;
|
|
8661
10978
|
if (servers && "claudemesh" in servers) {
|
|
8662
10979
|
delete servers.claudemesh;
|
|
8663
|
-
|
|
10980
|
+
writeFileSync11(PATHS.CLAUDE_JSON, JSON.stringify(config, null, 2) + `
|
|
8664
10981
|
`, "utf-8");
|
|
8665
10982
|
render.ok("removed MCP server", dim("~/.claude.json"));
|
|
8666
10983
|
removed++;
|
|
8667
10984
|
}
|
|
8668
10985
|
} catch {}
|
|
8669
10986
|
}
|
|
8670
|
-
if (
|
|
10987
|
+
if (existsSync14(PATHS.CLAUDE_SETTINGS)) {
|
|
8671
10988
|
try {
|
|
8672
|
-
const raw =
|
|
10989
|
+
const raw = readFileSync11(PATHS.CLAUDE_SETTINGS, "utf-8");
|
|
8673
10990
|
const config = JSON.parse(raw);
|
|
8674
10991
|
const hooks = config.hooks;
|
|
8675
10992
|
if (hooks) {
|
|
@@ -8690,7 +11007,7 @@ async function uninstall() {
|
|
|
8690
11007
|
}
|
|
8691
11008
|
}
|
|
8692
11009
|
if (removedHooks > 0) {
|
|
8693
|
-
|
|
11010
|
+
writeFileSync11(PATHS.CLAUDE_SETTINGS, JSON.stringify(config, null, 2) + `
|
|
8694
11011
|
`, "utf-8");
|
|
8695
11012
|
render.ok(`removed ${removedHooks} claudemesh hook${removedHooks === 1 ? "" : "s"}`, dim("settings.json"));
|
|
8696
11013
|
removed++;
|
|
@@ -8705,8 +11022,8 @@ async function uninstall() {
|
|
|
8705
11022
|
for (const entry of readdirSync2(src, { withFileTypes: true })) {
|
|
8706
11023
|
if (!entry.isDirectory())
|
|
8707
11024
|
continue;
|
|
8708
|
-
const dst =
|
|
8709
|
-
if (
|
|
11025
|
+
const dst = join10(CLAUDE_SKILLS_ROOT2, entry.name);
|
|
11026
|
+
if (existsSync14(dst)) {
|
|
8710
11027
|
try {
|
|
8711
11028
|
rmSync2(dst, { recursive: true, force: true });
|
|
8712
11029
|
removedSkills.push(entry.name);
|
|
@@ -8730,7 +11047,7 @@ var init_uninstall = __esm(() => {
|
|
|
8730
11047
|
init_render();
|
|
8731
11048
|
init_styles();
|
|
8732
11049
|
init_exit_codes();
|
|
8733
|
-
CLAUDE_SKILLS_ROOT2 =
|
|
11050
|
+
CLAUDE_SKILLS_ROOT2 = join10(homedir8(), ".claude", "skills");
|
|
8734
11051
|
});
|
|
8735
11052
|
|
|
8736
11053
|
// src/commands/doctor.ts
|
|
@@ -8738,9 +11055,9 @@ var exports_doctor = {};
|
|
|
8738
11055
|
__export(exports_doctor, {
|
|
8739
11056
|
runDoctor: () => runDoctor
|
|
8740
11057
|
});
|
|
8741
|
-
import { existsSync as
|
|
8742
|
-
import { homedir as
|
|
8743
|
-
import { join as
|
|
11058
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, statSync as statSync2 } from "node:fs";
|
|
11059
|
+
import { homedir as homedir9, platform as platform6 } from "node:os";
|
|
11060
|
+
import { join as join11 } from "node:path";
|
|
8744
11061
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
8745
11062
|
function checkNode() {
|
|
8746
11063
|
const major = Number(process.versions.node.split(".")[0]);
|
|
@@ -8764,8 +11081,8 @@ function checkClaudeOnPath() {
|
|
|
8764
11081
|
};
|
|
8765
11082
|
}
|
|
8766
11083
|
function checkMcpRegistered() {
|
|
8767
|
-
const claudeConfig =
|
|
8768
|
-
if (!
|
|
11084
|
+
const claudeConfig = join11(homedir9(), ".claude.json");
|
|
11085
|
+
if (!existsSync15(claudeConfig)) {
|
|
8769
11086
|
return {
|
|
8770
11087
|
name: "claudemesh MCP registered in ~/.claude.json",
|
|
8771
11088
|
pass: false,
|
|
@@ -8773,7 +11090,7 @@ function checkMcpRegistered() {
|
|
|
8773
11090
|
};
|
|
8774
11091
|
}
|
|
8775
11092
|
try {
|
|
8776
|
-
const cfg = JSON.parse(
|
|
11093
|
+
const cfg = JSON.parse(readFileSync12(claudeConfig, "utf-8"));
|
|
8777
11094
|
const registered = Boolean(cfg.mcpServers?.["claudemesh"]);
|
|
8778
11095
|
return {
|
|
8779
11096
|
name: "claudemesh MCP registered in ~/.claude.json",
|
|
@@ -8790,8 +11107,8 @@ function checkMcpRegistered() {
|
|
|
8790
11107
|
}
|
|
8791
11108
|
}
|
|
8792
11109
|
function checkHooksRegistered() {
|
|
8793
|
-
const settings =
|
|
8794
|
-
if (!
|
|
11110
|
+
const settings = join11(homedir9(), ".claude", "settings.json");
|
|
11111
|
+
if (!existsSync15(settings)) {
|
|
8795
11112
|
return {
|
|
8796
11113
|
name: "Status hooks registered in ~/.claude/settings.json",
|
|
8797
11114
|
pass: false,
|
|
@@ -8799,7 +11116,7 @@ function checkHooksRegistered() {
|
|
|
8799
11116
|
};
|
|
8800
11117
|
}
|
|
8801
11118
|
try {
|
|
8802
|
-
const raw =
|
|
11119
|
+
const raw = readFileSync12(settings, "utf-8");
|
|
8803
11120
|
const has = raw.includes("claudemesh hook ");
|
|
8804
11121
|
return {
|
|
8805
11122
|
name: "Status hooks registered in ~/.claude/settings.json",
|
|
@@ -8815,8 +11132,8 @@ function checkHooksRegistered() {
|
|
|
8815
11132
|
}
|
|
8816
11133
|
}
|
|
8817
11134
|
function checkConfigFile() {
|
|
8818
|
-
const
|
|
8819
|
-
if (!
|
|
11135
|
+
const path2 = getConfigPath();
|
|
11136
|
+
if (!existsSync15(path2)) {
|
|
8820
11137
|
return {
|
|
8821
11138
|
name: "~/.claudemesh/config.json exists and parses",
|
|
8822
11139
|
pass: true,
|
|
@@ -8825,14 +11142,14 @@ function checkConfigFile() {
|
|
|
8825
11142
|
}
|
|
8826
11143
|
try {
|
|
8827
11144
|
readConfig();
|
|
8828
|
-
const st = statSync2(
|
|
11145
|
+
const st = statSync2(path2);
|
|
8829
11146
|
const mode = (st.mode & 511).toString(8);
|
|
8830
11147
|
const secure = platform6() === "win32" || mode === "600";
|
|
8831
11148
|
return {
|
|
8832
11149
|
name: "~/.claudemesh/config.json parses + chmod 0600",
|
|
8833
11150
|
pass: secure,
|
|
8834
11151
|
detail: platform6() === "win32" ? "chmod skipped on Windows" : `0${mode}`,
|
|
8835
|
-
fix: secure ? undefined : `chmod 600 ${
|
|
11152
|
+
fix: secure ? undefined : `chmod 600 ${path2}`
|
|
8836
11153
|
};
|
|
8837
11154
|
} catch (e) {
|
|
8838
11155
|
return {
|
|
@@ -8888,8 +11205,8 @@ async function checkBrokerWs() {
|
|
|
8888
11205
|
const wsUrl = URLS.BROKER;
|
|
8889
11206
|
const start = Date.now();
|
|
8890
11207
|
try {
|
|
8891
|
-
const
|
|
8892
|
-
const ws = new
|
|
11208
|
+
const WebSocket3 = (await import("ws")).default;
|
|
11209
|
+
const ws = new WebSocket3(wsUrl);
|
|
8893
11210
|
const result = await new Promise((resolve2) => {
|
|
8894
11211
|
const timer = setTimeout(() => {
|
|
8895
11212
|
try {
|
|
@@ -8997,14 +11314,14 @@ var init_doctor = __esm(() => {
|
|
|
8997
11314
|
// src/commands/status.ts
|
|
8998
11315
|
var exports_status = {};
|
|
8999
11316
|
__export(exports_status, {
|
|
9000
|
-
runStatus: () =>
|
|
11317
|
+
runStatus: () => runStatus2
|
|
9001
11318
|
});
|
|
9002
|
-
import { statSync as statSync3, existsSync as
|
|
9003
|
-
import
|
|
11319
|
+
import { statSync as statSync3, existsSync as existsSync16 } from "node:fs";
|
|
11320
|
+
import WebSocket3 from "ws";
|
|
9004
11321
|
async function probeBroker(url, timeoutMs = 4000) {
|
|
9005
11322
|
return new Promise((resolve2) => {
|
|
9006
11323
|
const started = Date.now();
|
|
9007
|
-
const ws = new
|
|
11324
|
+
const ws = new WebSocket3(url);
|
|
9008
11325
|
const timer = setTimeout(() => {
|
|
9009
11326
|
try {
|
|
9010
11327
|
ws.terminate();
|
|
@@ -9025,11 +11342,11 @@ async function probeBroker(url, timeoutMs = 4000) {
|
|
|
9025
11342
|
});
|
|
9026
11343
|
});
|
|
9027
11344
|
}
|
|
9028
|
-
async function
|
|
11345
|
+
async function runStatus2() {
|
|
9029
11346
|
render.section(`status (v${VERSION})`);
|
|
9030
11347
|
const configPath = getConfigPath();
|
|
9031
11348
|
let configPermsNote = "missing";
|
|
9032
|
-
if (
|
|
11349
|
+
if (existsSync16(configPath)) {
|
|
9033
11350
|
const mode = (statSync3(configPath).mode & 511).toString(8).padStart(4, "0");
|
|
9034
11351
|
configPermsNote = mode === "0600" ? `${mode}` : `${mode} — expected 0600`;
|
|
9035
11352
|
}
|
|
@@ -9175,13 +11492,13 @@ var init_check_claude_binary = __esm(() => {
|
|
|
9175
11492
|
});
|
|
9176
11493
|
|
|
9177
11494
|
// src/services/health/check-mcp-registered.ts
|
|
9178
|
-
import { existsSync as
|
|
11495
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
9179
11496
|
function checkMcpRegistered2() {
|
|
9180
11497
|
try {
|
|
9181
|
-
if (!
|
|
11498
|
+
if (!existsSync17(PATHS.CLAUDE_JSON)) {
|
|
9182
11499
|
return { name: "mcp-registered", ok: false, message: "~/.claude.json not found" };
|
|
9183
11500
|
}
|
|
9184
|
-
const raw =
|
|
11501
|
+
const raw = readFileSync13(PATHS.CLAUDE_JSON, "utf-8");
|
|
9185
11502
|
const config = JSON.parse(raw);
|
|
9186
11503
|
if (config.mcpServers && "claudemesh" in config.mcpServers) {
|
|
9187
11504
|
return { name: "mcp-registered", ok: true, message: "MCP server registered" };
|
|
@@ -9196,13 +11513,13 @@ var init_check_mcp_registered = __esm(() => {
|
|
|
9196
11513
|
});
|
|
9197
11514
|
|
|
9198
11515
|
// src/services/health/check-hooks-registered.ts
|
|
9199
|
-
import { existsSync as
|
|
11516
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
|
|
9200
11517
|
function checkHooksRegistered2() {
|
|
9201
11518
|
try {
|
|
9202
|
-
if (!
|
|
11519
|
+
if (!existsSync18(PATHS.CLAUDE_SETTINGS)) {
|
|
9203
11520
|
return { name: "hooks-registered", ok: false, message: "~/.claude/settings.json not found" };
|
|
9204
11521
|
}
|
|
9205
|
-
const raw =
|
|
11522
|
+
const raw = readFileSync14(PATHS.CLAUDE_SETTINGS, "utf-8");
|
|
9206
11523
|
const config = JSON.parse(raw);
|
|
9207
11524
|
if (config.hooks) {
|
|
9208
11525
|
return { name: "hooks-registered", ok: true, message: "Hooks configured" };
|
|
@@ -9217,10 +11534,10 @@ var init_check_hooks_registered = __esm(() => {
|
|
|
9217
11534
|
});
|
|
9218
11535
|
|
|
9219
11536
|
// src/services/health/check-config-perms.ts
|
|
9220
|
-
import { existsSync as
|
|
11537
|
+
import { existsSync as existsSync19, statSync as statSync4 } from "node:fs";
|
|
9221
11538
|
function checkConfigPerms() {
|
|
9222
11539
|
const configFile = PATHS.CONFIG_FILE;
|
|
9223
|
-
if (!
|
|
11540
|
+
if (!existsSync19(configFile)) {
|
|
9224
11541
|
return { name: "config-perms", ok: true, message: "No config file yet (first run)" };
|
|
9225
11542
|
}
|
|
9226
11543
|
try {
|
|
@@ -9238,13 +11555,13 @@ var init_check_config_perms = __esm(() => {
|
|
|
9238
11555
|
});
|
|
9239
11556
|
|
|
9240
11557
|
// src/services/health/check-keypairs-valid.ts
|
|
9241
|
-
import { existsSync as
|
|
11558
|
+
import { existsSync as existsSync20, readFileSync as readFileSync15 } from "node:fs";
|
|
9242
11559
|
function checkKeypairsValid() {
|
|
9243
|
-
if (!
|
|
11560
|
+
if (!existsSync20(PATHS.CONFIG_FILE)) {
|
|
9244
11561
|
return { name: "keypairs-valid", ok: true, message: "No config (first run)" };
|
|
9245
11562
|
}
|
|
9246
11563
|
try {
|
|
9247
|
-
const raw =
|
|
11564
|
+
const raw = readFileSync15(PATHS.CONFIG_FILE, "utf-8");
|
|
9248
11565
|
const config = JSON.parse(raw);
|
|
9249
11566
|
const meshes = config.meshes ?? [];
|
|
9250
11567
|
if (meshes.length === 0) {
|
|
@@ -9645,12 +11962,12 @@ var exports_verify = {};
|
|
|
9645
11962
|
__export(exports_verify, {
|
|
9646
11963
|
runVerify: () => runVerify
|
|
9647
11964
|
});
|
|
9648
|
-
import { createHash } from "node:crypto";
|
|
11965
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9649
11966
|
function safetyNumber(myPubkey, peerPubkey) {
|
|
9650
11967
|
const a = Buffer.from(myPubkey, "hex");
|
|
9651
11968
|
const b = Buffer.from(peerPubkey, "hex");
|
|
9652
11969
|
const [lo, hi] = Buffer.compare(a, b) < 0 ? [a, b] : [b, a];
|
|
9653
|
-
const hash =
|
|
11970
|
+
const hash = createHash3("sha256").update(lo).update(hi).digest();
|
|
9654
11971
|
const bits = [];
|
|
9655
11972
|
for (let i = 0;i < 15; i++) {
|
|
9656
11973
|
for (let b2 = 7;b2 >= 0; b2--) {
|
|
@@ -9724,19 +12041,19 @@ var exports_url_handler = {};
|
|
|
9724
12041
|
__export(exports_url_handler, {
|
|
9725
12042
|
runUrlHandler: () => runUrlHandler
|
|
9726
12043
|
});
|
|
9727
|
-
import { platform as platform7, homedir as
|
|
9728
|
-
import { existsSync as
|
|
9729
|
-
import { join as
|
|
12044
|
+
import { platform as platform7, homedir as homedir10 } from "node:os";
|
|
12045
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12, rmSync as rmSync3, chmodSync as chmodSync5 } from "node:fs";
|
|
12046
|
+
import { join as join12 } from "node:path";
|
|
9730
12047
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9731
12048
|
function resolveClaudemeshBin() {
|
|
9732
12049
|
return process.argv[1] ?? "claudemesh";
|
|
9733
12050
|
}
|
|
9734
|
-
function
|
|
12051
|
+
function installDarwin2() {
|
|
9735
12052
|
const binPath = resolveClaudemeshBin();
|
|
9736
|
-
const appDir =
|
|
9737
|
-
const contents =
|
|
9738
|
-
const macOS =
|
|
9739
|
-
|
|
12053
|
+
const appDir = join12(homedir10(), "Library", "Application Support", "claudemesh", "ClaudemeshHandler.app");
|
|
12054
|
+
const contents = join12(appDir, "Contents");
|
|
12055
|
+
const macOS = join12(contents, "MacOS");
|
|
12056
|
+
mkdirSync9(macOS, { recursive: true });
|
|
9740
12057
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
9741
12058
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9742
12059
|
<plist version="1.0">
|
|
@@ -9759,7 +12076,7 @@ function installDarwin() {
|
|
|
9759
12076
|
</array>
|
|
9760
12077
|
</dict>
|
|
9761
12078
|
</plist>`;
|
|
9762
|
-
|
|
12079
|
+
writeFileSync12(join12(contents, "Info.plist"), plist);
|
|
9763
12080
|
const shim = `#!/bin/sh
|
|
9764
12081
|
URL="$1"
|
|
9765
12082
|
CODE=\${URL#claudemesh://}
|
|
@@ -9773,9 +12090,9 @@ tell application "Terminal"
|
|
|
9773
12090
|
end tell
|
|
9774
12091
|
EOF
|
|
9775
12092
|
`;
|
|
9776
|
-
const shimPath =
|
|
9777
|
-
|
|
9778
|
-
|
|
12093
|
+
const shimPath = join12(macOS, "open-url");
|
|
12094
|
+
writeFileSync12(shimPath, shim);
|
|
12095
|
+
chmodSync5(shimPath, 493);
|
|
9779
12096
|
const lsreg = spawnSync5("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", ["-f", appDir], { encoding: "utf-8" });
|
|
9780
12097
|
if (lsreg.status !== 0) {
|
|
9781
12098
|
render.warn("lsregister returned non-zero", "scheme may not activate until Finder rescans.");
|
|
@@ -9783,10 +12100,10 @@ EOF
|
|
|
9783
12100
|
render.ok("registered claudemesh:// scheme on macOS", dim(appDir));
|
|
9784
12101
|
return EXIT.SUCCESS;
|
|
9785
12102
|
}
|
|
9786
|
-
function
|
|
12103
|
+
function installLinux2() {
|
|
9787
12104
|
const binPath = resolveClaudemeshBin();
|
|
9788
|
-
const appsDir =
|
|
9789
|
-
|
|
12105
|
+
const appsDir = join12(homedir10(), ".local", "share", "applications");
|
|
12106
|
+
mkdirSync9(appsDir, { recursive: true });
|
|
9790
12107
|
const desktop = `[Desktop Entry]
|
|
9791
12108
|
Type=Application
|
|
9792
12109
|
Name=Claudemesh
|
|
@@ -9797,8 +12114,8 @@ Terminal=true
|
|
|
9797
12114
|
MimeType=x-scheme-handler/claudemesh;
|
|
9798
12115
|
NoDisplay=true
|
|
9799
12116
|
`;
|
|
9800
|
-
const desktopPath =
|
|
9801
|
-
|
|
12117
|
+
const desktopPath = join12(appsDir, "claudemesh.desktop");
|
|
12118
|
+
writeFileSync12(desktopPath, desktop);
|
|
9802
12119
|
const xdg1 = spawnSync5("xdg-mime", ["default", "claudemesh.desktop", "x-scheme-handler/claudemesh"], { encoding: "utf-8" });
|
|
9803
12120
|
if (xdg1.status !== 0) {
|
|
9804
12121
|
render.warn("xdg-mime not available — skipped mime default registration");
|
|
@@ -9820,8 +12137,8 @@ function installWindows() {
|
|
|
9820
12137
|
`[HKEY_CURRENT_USER\\Software\\Classes\\claudemesh\\shell\\open\\command]`,
|
|
9821
12138
|
`@="\\"${binPath.replace(/\\/g, "\\\\")}\\" \\"%1\\""`
|
|
9822
12139
|
];
|
|
9823
|
-
const regPath =
|
|
9824
|
-
|
|
12140
|
+
const regPath = join12(homedir10(), "claudemesh-handler.reg");
|
|
12141
|
+
writeFileSync12(regPath, lines.join(`\r
|
|
9825
12142
|
`));
|
|
9826
12143
|
const res = spawnSync5("reg.exe", ["import", regPath], { encoding: "utf-8" });
|
|
9827
12144
|
if (res.status !== 0) {
|
|
@@ -9832,15 +12149,15 @@ function installWindows() {
|
|
|
9832
12149
|
return EXIT.SUCCESS;
|
|
9833
12150
|
}
|
|
9834
12151
|
function uninstallDarwin() {
|
|
9835
|
-
const appDir =
|
|
9836
|
-
if (
|
|
12152
|
+
const appDir = join12(homedir10(), "Library", "Application Support", "claudemesh", "ClaudemeshHandler.app");
|
|
12153
|
+
if (existsSync21(appDir))
|
|
9837
12154
|
rmSync3(appDir, { recursive: true, force: true });
|
|
9838
12155
|
render.ok("removed claudemesh:// handler on macOS");
|
|
9839
12156
|
return EXIT.SUCCESS;
|
|
9840
12157
|
}
|
|
9841
12158
|
function uninstallLinux() {
|
|
9842
|
-
const desktopPath =
|
|
9843
|
-
if (
|
|
12159
|
+
const desktopPath = join12(homedir10(), ".local", "share", "applications", "claudemesh.desktop");
|
|
12160
|
+
if (existsSync21(desktopPath))
|
|
9844
12161
|
rmSync3(desktopPath, { force: true });
|
|
9845
12162
|
render.ok("removed claudemesh:// handler on Linux");
|
|
9846
12163
|
return EXIT.SUCCESS;
|
|
@@ -9855,9 +12172,9 @@ async function runUrlHandler(action) {
|
|
|
9855
12172
|
const p = platform7();
|
|
9856
12173
|
if (act === "install") {
|
|
9857
12174
|
if (p === "darwin")
|
|
9858
|
-
return
|
|
12175
|
+
return installDarwin2();
|
|
9859
12176
|
if (p === "linux")
|
|
9860
|
-
return
|
|
12177
|
+
return installLinux2();
|
|
9861
12178
|
if (p === "win32")
|
|
9862
12179
|
return installWindows();
|
|
9863
12180
|
} else if (act === "uninstall" || act === "remove") {
|
|
@@ -9885,9 +12202,9 @@ var exports_status_line = {};
|
|
|
9885
12202
|
__export(exports_status_line, {
|
|
9886
12203
|
runStatusLine: () => runStatusLine
|
|
9887
12204
|
});
|
|
9888
|
-
import { existsSync as
|
|
9889
|
-
import { join as
|
|
9890
|
-
import { homedir as
|
|
12205
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16 } from "node:fs";
|
|
12206
|
+
import { join as join13 } from "node:path";
|
|
12207
|
+
import { homedir as homedir11 } from "node:os";
|
|
9891
12208
|
async function runStatusLine() {
|
|
9892
12209
|
try {
|
|
9893
12210
|
const config = readConfig();
|
|
@@ -9895,11 +12212,11 @@ async function runStatusLine() {
|
|
|
9895
12212
|
process.stdout.write("◇ claudemesh (not joined)");
|
|
9896
12213
|
return EXIT.SUCCESS;
|
|
9897
12214
|
}
|
|
9898
|
-
const cachePath =
|
|
12215
|
+
const cachePath = join13(homedir11(), ".claudemesh", "peer-cache.json");
|
|
9899
12216
|
let cache = {};
|
|
9900
|
-
if (
|
|
12217
|
+
if (existsSync22(cachePath)) {
|
|
9901
12218
|
try {
|
|
9902
|
-
cache = JSON.parse(
|
|
12219
|
+
cache = JSON.parse(readFileSync16(cachePath, "utf-8"));
|
|
9903
12220
|
} catch {}
|
|
9904
12221
|
}
|
|
9905
12222
|
const pick = config.meshes[0];
|
|
@@ -9930,7 +12247,7 @@ __export(exports_backup, {
|
|
|
9930
12247
|
runRestore: () => runRestore,
|
|
9931
12248
|
runBackup: () => runBackup
|
|
9932
12249
|
});
|
|
9933
|
-
import { readFileSync as
|
|
12250
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync23 } from "node:fs";
|
|
9934
12251
|
import { createInterface as createInterface11 } from "node:readline";
|
|
9935
12252
|
function readHidden(prompt5) {
|
|
9936
12253
|
return new Promise((resolve2) => {
|
|
@@ -9972,11 +12289,11 @@ async function deriveKey(pass, salt, s) {
|
|
|
9972
12289
|
}
|
|
9973
12290
|
async function runBackup(outPath) {
|
|
9974
12291
|
const configPath = getConfigPath();
|
|
9975
|
-
if (!
|
|
12292
|
+
if (!existsSync23(configPath)) {
|
|
9976
12293
|
console.error(" No config found — nothing to back up. Join a mesh first.");
|
|
9977
12294
|
return EXIT.NOT_FOUND;
|
|
9978
12295
|
}
|
|
9979
|
-
const plaintext =
|
|
12296
|
+
const plaintext = readFileSync17(configPath);
|
|
9980
12297
|
const pass = await readHidden(" Passphrase (min 12 chars): ");
|
|
9981
12298
|
if (pass.length < 12) {
|
|
9982
12299
|
console.error(" ✗ Passphrase too short.");
|
|
@@ -9994,7 +12311,7 @@ async function runBackup(outPath) {
|
|
|
9994
12311
|
const ciphertext = Buffer.from(s.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, null, null, nonce, key));
|
|
9995
12312
|
const blob = Buffer.concat([MAGIC, salt, nonce, ciphertext]);
|
|
9996
12313
|
const file = outPath ?? `claudemesh-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.cmb`;
|
|
9997
|
-
|
|
12314
|
+
writeFileSync13(file, blob, { mode: 384 });
|
|
9998
12315
|
console.log(`
|
|
9999
12316
|
✓ Backup saved: ${file}`);
|
|
10000
12317
|
console.log(` Size: ${blob.length} bytes. Guard the passphrase — there is no recovery.
|
|
@@ -10006,11 +12323,11 @@ async function runRestore(inPath) {
|
|
|
10006
12323
|
console.error(" Usage: claudemesh restore <backup-file>");
|
|
10007
12324
|
return EXIT.INVALID_ARGS;
|
|
10008
12325
|
}
|
|
10009
|
-
if (!
|
|
12326
|
+
if (!existsSync23(inPath)) {
|
|
10010
12327
|
console.error(` ✗ File not found: ${inPath}`);
|
|
10011
12328
|
return EXIT.NOT_FOUND;
|
|
10012
12329
|
}
|
|
10013
|
-
const blob =
|
|
12330
|
+
const blob = readFileSync17(inPath);
|
|
10014
12331
|
if (blob.length < 4 + 16 + 24 + 17 || !blob.subarray(0, 4).equals(MAGIC)) {
|
|
10015
12332
|
console.error(" ✗ Not a claudemesh backup file (bad magic).");
|
|
10016
12333
|
return EXIT.INVALID_ARGS;
|
|
@@ -10029,12 +12346,12 @@ async function runRestore(inPath) {
|
|
|
10029
12346
|
return EXIT.INTERNAL_ERROR;
|
|
10030
12347
|
}
|
|
10031
12348
|
const configPath = getConfigPath();
|
|
10032
|
-
if (
|
|
12349
|
+
if (existsSync23(configPath)) {
|
|
10033
12350
|
const backupOld = `${configPath}.before-restore.${Date.now()}`;
|
|
10034
|
-
|
|
12351
|
+
writeFileSync13(backupOld, readFileSync17(configPath), { mode: 384 });
|
|
10035
12352
|
console.log(` ↻ Existing config saved to ${backupOld}`);
|
|
10036
12353
|
}
|
|
10037
|
-
|
|
12354
|
+
writeFileSync13(configPath, Buffer.from(plaintext), { mode: 384 });
|
|
10038
12355
|
console.log(`
|
|
10039
12356
|
✓ Config restored to ${configPath}`);
|
|
10040
12357
|
console.log(" Run `claudemesh list` to verify your meshes.\n");
|
|
@@ -10054,8 +12371,8 @@ __export(exports_upgrade, {
|
|
|
10054
12371
|
runUpgrade: () => runUpgrade
|
|
10055
12372
|
});
|
|
10056
12373
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
10057
|
-
import { existsSync as
|
|
10058
|
-
import { dirname as
|
|
12374
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
12375
|
+
import { dirname as dirname8, join as join14, resolve as resolve2 } from "node:path";
|
|
10059
12376
|
async function latestVersion() {
|
|
10060
12377
|
try {
|
|
10061
12378
|
const res = await fetch(URLS.NPM_REGISTRY, { signal: AbortSignal.timeout(8000) });
|
|
@@ -10068,15 +12385,15 @@ async function latestVersion() {
|
|
|
10068
12385
|
}
|
|
10069
12386
|
}
|
|
10070
12387
|
function findNpm() {
|
|
10071
|
-
const portable =
|
|
10072
|
-
if (
|
|
10073
|
-
return { npm: portable, prefix:
|
|
12388
|
+
const portable = join14(process.env.HOME ?? "", ".claudemesh", "node", "bin", "npm");
|
|
12389
|
+
if (existsSync24(portable)) {
|
|
12390
|
+
return { npm: portable, prefix: join14(process.env.HOME ?? "", ".claudemesh") };
|
|
10074
12391
|
}
|
|
10075
12392
|
let cur = resolve2(process.argv[1] ?? ".");
|
|
10076
12393
|
for (let i = 0;i < 6; i++) {
|
|
10077
|
-
cur =
|
|
10078
|
-
const candidate =
|
|
10079
|
-
if (
|
|
12394
|
+
cur = dirname8(cur);
|
|
12395
|
+
const candidate = join14(cur, "bin", "npm");
|
|
12396
|
+
if (existsSync24(candidate))
|
|
10080
12397
|
return { npm: candidate };
|
|
10081
12398
|
}
|
|
10082
12399
|
return { npm: "npm" };
|
|
@@ -10138,9 +12455,9 @@ __export(exports_grants, {
|
|
|
10138
12455
|
runBlock: () => runBlock,
|
|
10139
12456
|
isAllowed: () => isAllowed
|
|
10140
12457
|
});
|
|
10141
|
-
import { existsSync as
|
|
10142
|
-
import { homedir as
|
|
10143
|
-
import { join as
|
|
12458
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync10, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "node:fs";
|
|
12459
|
+
import { homedir as homedir12 } from "node:os";
|
|
12460
|
+
import { join as join15 } from "node:path";
|
|
10144
12461
|
async function syncToBroker(meshSlug, grants) {
|
|
10145
12462
|
const auth = getStoredToken();
|
|
10146
12463
|
if (!auth)
|
|
@@ -10158,19 +12475,19 @@ async function syncToBroker(meshSlug, grants) {
|
|
|
10158
12475
|
}
|
|
10159
12476
|
}
|
|
10160
12477
|
function readGrants() {
|
|
10161
|
-
if (!
|
|
12478
|
+
if (!existsSync25(GRANT_FILE))
|
|
10162
12479
|
return {};
|
|
10163
12480
|
try {
|
|
10164
|
-
return JSON.parse(
|
|
12481
|
+
return JSON.parse(readFileSync18(GRANT_FILE, "utf-8"));
|
|
10165
12482
|
} catch {
|
|
10166
12483
|
return {};
|
|
10167
12484
|
}
|
|
10168
12485
|
}
|
|
10169
12486
|
function writeGrants(g) {
|
|
10170
|
-
const dir =
|
|
10171
|
-
if (!
|
|
10172
|
-
|
|
10173
|
-
|
|
12487
|
+
const dir = join15(homedir12(), ".claudemesh");
|
|
12488
|
+
if (!existsSync25(dir))
|
|
12489
|
+
mkdirSync10(dir, { recursive: true });
|
|
12490
|
+
writeFileSync14(GRANT_FILE, JSON.stringify(g, null, 2), { mode: 384 });
|
|
10174
12491
|
}
|
|
10175
12492
|
function resolveCaps(input) {
|
|
10176
12493
|
if (input.includes("all"))
|
|
@@ -10326,7 +12643,7 @@ var init_grants = __esm(() => {
|
|
|
10326
12643
|
BROKER_HTTP7 = URLS.BROKER.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
|
|
10327
12644
|
ALL_CAPS = ["read", "dm", "broadcast", "state-read", "state-write", "file-read"];
|
|
10328
12645
|
DEFAULT_CAPS = ["read", "dm", "broadcast", "state-read"];
|
|
10329
|
-
GRANT_FILE =
|
|
12646
|
+
GRANT_FILE = join15(homedir12(), ".claudemesh", "grants.json");
|
|
10330
12647
|
});
|
|
10331
12648
|
|
|
10332
12649
|
// src/commands/profile.ts
|
|
@@ -11223,6 +13540,23 @@ claudemesh send "<from_name>" "..." --mesh "<mesh_slug>"
|
|
|
11223
13540
|
|
|
11224
13541
|
If the parent Claude session was launched via \`claudemesh launch\`, an MCP push-pipe is running and holds the per-mesh WS connection. CLI invocations dial \`~/.claudemesh/sockets/<mesh-slug>.sock\` and reuse that warm connection (~200ms total round-trip including Node.js startup). If no push-pipe is running (cron, scripts, hooks fired outside a session), the CLI opens its own WS, which takes ~500-700ms cold. **You don't manage this** — every verb auto-detects and falls through.
|
|
11225
13542
|
|
|
13543
|
+
### Daemon path (v0.9.0, opt-in, fastest)
|
|
13544
|
+
|
|
13545
|
+
\`claudemesh daemon up [--mesh <slug>]\` starts a persistent per-user runtime that holds the broker WS, a durable SQLite outbox/inbox, and listens on \`~/.claudemesh/daemon/daemon.sock\` (UDS) plus an optional loopback TCP. When the daemon socket is present, every verb routes through it first (~1ms IPC) before falling back to bridge / cold paths. The send envelope carries a caller-stable \`client_message_id\`, so a \`claudemesh send\` that started before a daemon crash survives the restart via the on-disk outbox.
|
|
13546
|
+
|
|
13547
|
+
Lifecycle:
|
|
13548
|
+
|
|
13549
|
+
\`\`\`bash
|
|
13550
|
+
claudemesh daemon up --mesh <slug> # foreground
|
|
13551
|
+
claudemesh daemon install-service --mesh <slug> # macOS launchd / Linux systemd-user
|
|
13552
|
+
claudemesh daemon status [--json] # health + pid
|
|
13553
|
+
claudemesh daemon outbox list [--failed|--pending|...] # local queue inspection
|
|
13554
|
+
claudemesh daemon outbox requeue <id> # re-enqueue an aborted/dead row
|
|
13555
|
+
claudemesh daemon down # SIGTERM + wait
|
|
13556
|
+
\`\`\`
|
|
13557
|
+
|
|
13558
|
+
\`claudemesh install\` (MCP + hooks registration) and the daemon are independent — install does not start the daemon, and the daemon does not require install. Run both for the warmest path: install gives you the in-session push-pipe, daemon gives you cross-invocation persistence and a survivable outbox.
|
|
13559
|
+
|
|
11226
13560
|
## Spawning new sessions (no wizard)
|
|
11227
13561
|
|
|
11228
13562
|
\`claudemesh launch\` is the canonical way to start a new Claude Code session connected to claudemesh. Pass every required flag up front so no interactive prompt fires — that's what makes the verb scriptable from tmux send-keys, AppleScript/iTerm spawn helpers, hooks, cron, and the \`claudemesh launch\` you call from inside another session. **Always use this verb, never \`claude\` directly with hand-rolled flags** — it sets up the per-session ed25519 keypair, exports \`CLAUDEMESH_DISPLAY_NAME\`, isolates the mesh config in a tmpdir, and passes the \`--dangerously-load-development-channels server:claudemesh\` plumbing that the MCP push-pipe needs.
|
|
@@ -11805,8 +14139,8 @@ __export(exports_file, {
|
|
|
11805
14139
|
runFileGet: () => runFileGet
|
|
11806
14140
|
});
|
|
11807
14141
|
import { hostname as osHostname } from "node:os";
|
|
11808
|
-
import { resolve as resolvePath, basename, dirname as
|
|
11809
|
-
import { statSync as statSync6, existsSync as
|
|
14142
|
+
import { resolve as resolvePath, basename, dirname as dirname9 } from "node:path";
|
|
14143
|
+
import { statSync as statSync6, existsSync as existsSync26, writeFileSync as writeFileSync15, mkdirSync as mkdirSync11 } from "node:fs";
|
|
11810
14144
|
function emitJson2(data) {
|
|
11811
14145
|
console.log(JSON.stringify(data, null, 2));
|
|
11812
14146
|
}
|
|
@@ -11823,7 +14157,7 @@ async function runFileShare(filePath, opts) {
|
|
|
11823
14157
|
return EXIT.INVALID_ARGS;
|
|
11824
14158
|
}
|
|
11825
14159
|
const absPath = resolvePath(filePath);
|
|
11826
|
-
if (!
|
|
14160
|
+
if (!existsSync26(absPath)) {
|
|
11827
14161
|
render.err(`File not found: ${absPath}`);
|
|
11828
14162
|
return EXIT.INVALID_ARGS;
|
|
11829
14163
|
}
|
|
@@ -11902,8 +14236,8 @@ async function runFileGet(fileId, opts) {
|
|
|
11902
14236
|
}
|
|
11903
14237
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
11904
14238
|
const outPath = opts.out ? resolvePath(opts.out) : resolvePath(process.cwd(), meta.name);
|
|
11905
|
-
|
|
11906
|
-
|
|
14239
|
+
mkdirSync11(dirname9(outPath), { recursive: true });
|
|
14240
|
+
writeFileSync15(outPath, buf);
|
|
11907
14241
|
if (opts.json) {
|
|
11908
14242
|
emitJson2({ fileId, name: meta.name, savedTo: outPath, sizeBytes: buf.length });
|
|
11909
14243
|
} else {
|
|
@@ -11998,7 +14332,7 @@ var require_client = __commonJS((exports) => {
|
|
|
11998
14332
|
var ws_1 = __importDefault(__require("ws"));
|
|
11999
14333
|
var crypto_js_1 = require_crypto();
|
|
12000
14334
|
var MAX_QUEUED2 = 100;
|
|
12001
|
-
var
|
|
14335
|
+
var HELLO_ACK_TIMEOUT_MS3 = 5000;
|
|
12002
14336
|
var BACKOFF_CAPS2 = [1000, 2000, 4000, 8000, 16000, 30000];
|
|
12003
14337
|
|
|
12004
14338
|
class MeshClient extends node_events_1.EventEmitter {
|
|
@@ -12063,7 +14397,7 @@ var require_client = __commonJS((exports) => {
|
|
|
12063
14397
|
this.debug("hello_ack timeout");
|
|
12064
14398
|
ws.close();
|
|
12065
14399
|
reject(new Error("hello_ack timeout"));
|
|
12066
|
-
},
|
|
14400
|
+
}, HELLO_ACK_TIMEOUT_MS3);
|
|
12067
14401
|
};
|
|
12068
14402
|
const onMessage = (raw) => {
|
|
12069
14403
|
let msg;
|
|
@@ -12513,7 +14847,7 @@ __export(exports_bridge, {
|
|
|
12513
14847
|
runBridge: () => runBridge,
|
|
12514
14848
|
bridgeConfigTemplate: () => bridgeConfigTemplate
|
|
12515
14849
|
});
|
|
12516
|
-
import { readFileSync as
|
|
14850
|
+
import { readFileSync as readFileSync19, existsSync as existsSync27 } from "node:fs";
|
|
12517
14851
|
function parseConfig(text) {
|
|
12518
14852
|
const trimmed = text.trim();
|
|
12519
14853
|
if (trimmed.startsWith("{"))
|
|
@@ -12557,13 +14891,13 @@ async function runBridge(configPath) {
|
|
|
12557
14891
|
render.err("Usage: claudemesh bridge run <config.yaml>");
|
|
12558
14892
|
return EXIT.INVALID_ARGS;
|
|
12559
14893
|
}
|
|
12560
|
-
if (!
|
|
14894
|
+
if (!existsSync27(configPath)) {
|
|
12561
14895
|
render.err(`config file not found: ${configPath}`);
|
|
12562
14896
|
return EXIT.NOT_FOUND;
|
|
12563
14897
|
}
|
|
12564
14898
|
let cfg;
|
|
12565
14899
|
try {
|
|
12566
|
-
cfg = parseConfig(
|
|
14900
|
+
cfg = parseConfig(readFileSync19(configPath, "utf-8"));
|
|
12567
14901
|
} catch (e) {
|
|
12568
14902
|
render.err(`failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
12569
14903
|
return EXIT.INVALID_ARGS;
|
|
@@ -12967,9 +15301,9 @@ function cacheKey(apiKeySecret, topicName) {
|
|
|
12967
15301
|
async function getTopicKey(args) {
|
|
12968
15302
|
const cacheId = cacheKey(args.apiKeySecret, args.topicName);
|
|
12969
15303
|
if (!args.fresh) {
|
|
12970
|
-
const
|
|
12971
|
-
if (
|
|
12972
|
-
return { ok: true, topicKey:
|
|
15304
|
+
const cached2 = cache.get(cacheId);
|
|
15305
|
+
if (cached2)
|
|
15306
|
+
return { ok: true, topicKey: cached2.topicKey };
|
|
12973
15307
|
}
|
|
12974
15308
|
let sealed;
|
|
12975
15309
|
try {
|
|
@@ -13533,8 +15867,8 @@ var init_definitions = __esm(() => {
|
|
|
13533
15867
|
});
|
|
13534
15868
|
|
|
13535
15869
|
// src/services/bridge/server.ts
|
|
13536
|
-
import { createServer as
|
|
13537
|
-
import { mkdirSync as
|
|
15870
|
+
import { createServer as createServer3 } from "node:net";
|
|
15871
|
+
import { mkdirSync as mkdirSync12, unlinkSync as unlinkSync5, existsSync as existsSync28, chmodSync as chmodSync6 } from "node:fs";
|
|
13538
15872
|
async function resolveTarget2(client, to) {
|
|
13539
15873
|
if (to.startsWith("@") || to === "*" || /^[0-9a-f]{64}$/i.test(to)) {
|
|
13540
15874
|
return { ok: true, spec: to };
|
|
@@ -13646,21 +15980,21 @@ function handleConnection(socket, client) {
|
|
|
13646
15980
|
socket.on("error", () => {});
|
|
13647
15981
|
}
|
|
13648
15982
|
function startBridgeServer(client) {
|
|
13649
|
-
const
|
|
15983
|
+
const path2 = socketPath(client.meshSlug);
|
|
13650
15984
|
const dir = socketDir();
|
|
13651
|
-
if (!
|
|
13652
|
-
|
|
15985
|
+
if (!existsSync28(dir)) {
|
|
15986
|
+
mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
13653
15987
|
}
|
|
13654
|
-
if (
|
|
15988
|
+
if (existsSync28(path2)) {
|
|
13655
15989
|
try {
|
|
13656
|
-
|
|
15990
|
+
unlinkSync5(path2);
|
|
13657
15991
|
} catch {}
|
|
13658
15992
|
}
|
|
13659
|
-
const server =
|
|
15993
|
+
const server = createServer3((socket) => handleConnection(socket, client));
|
|
13660
15994
|
try {
|
|
13661
|
-
server.listen(
|
|
15995
|
+
server.listen(path2);
|
|
13662
15996
|
} catch (err) {
|
|
13663
|
-
process.stderr.write(`[claudemesh] bridge: failed to bind ${
|
|
15997
|
+
process.stderr.write(`[claudemesh] bridge: failed to bind ${path2}: ${String(err)}
|
|
13664
15998
|
`);
|
|
13665
15999
|
return null;
|
|
13666
16000
|
}
|
|
@@ -13669,11 +16003,11 @@ function startBridgeServer(client) {
|
|
|
13669
16003
|
`);
|
|
13670
16004
|
});
|
|
13671
16005
|
try {
|
|
13672
|
-
|
|
16006
|
+
chmodSync6(path2, 384);
|
|
13673
16007
|
} catch {}
|
|
13674
16008
|
let stopped = false;
|
|
13675
16009
|
return {
|
|
13676
|
-
path,
|
|
16010
|
+
path: path2,
|
|
13677
16011
|
stop() {
|
|
13678
16012
|
if (stopped)
|
|
13679
16013
|
return;
|
|
@@ -13682,12 +16016,12 @@ function startBridgeServer(client) {
|
|
|
13682
16016
|
server.close();
|
|
13683
16017
|
} catch {}
|
|
13684
16018
|
try {
|
|
13685
|
-
|
|
16019
|
+
unlinkSync5(path2);
|
|
13686
16020
|
} catch {}
|
|
13687
16021
|
}
|
|
13688
16022
|
};
|
|
13689
16023
|
}
|
|
13690
|
-
var
|
|
16024
|
+
var init_server2 = __esm(() => {
|
|
13691
16025
|
init_protocol();
|
|
13692
16026
|
});
|
|
13693
16027
|
|
|
@@ -14258,9 +16592,9 @@ async function startServiceProxy(serviceName) {
|
|
|
14258
16592
|
const fetched = await client.getServiceTools(serviceName);
|
|
14259
16593
|
tools = fetched;
|
|
14260
16594
|
} catch {
|
|
14261
|
-
const
|
|
14262
|
-
if (
|
|
14263
|
-
tools =
|
|
16595
|
+
const cached2 = client.serviceCatalog.find((s) => s.name === serviceName);
|
|
16596
|
+
if (cached2) {
|
|
16597
|
+
tools = cached2.tools;
|
|
14264
16598
|
}
|
|
14265
16599
|
}
|
|
14266
16600
|
if (tools.length === 0) {
|
|
@@ -14349,11 +16683,11 @@ async function startServiceProxy(serviceName) {
|
|
|
14349
16683
|
process.on("SIGINT", shutdown);
|
|
14350
16684
|
}
|
|
14351
16685
|
var peerNameCache, peerNameCacheAge = 0, CACHE_TTL_MS = 30000;
|
|
14352
|
-
var
|
|
16686
|
+
var init_server3 = __esm(() => {
|
|
14353
16687
|
init_definitions();
|
|
14354
16688
|
init_facade();
|
|
14355
16689
|
init_facade8();
|
|
14356
|
-
|
|
16690
|
+
init_server2();
|
|
14357
16691
|
peerNameCache = new Map;
|
|
14358
16692
|
});
|
|
14359
16693
|
|
|
@@ -14369,7 +16703,7 @@ async function runMcp() {
|
|
|
14369
16703
|
process.exit(0);
|
|
14370
16704
|
}
|
|
14371
16705
|
var init_mcp = __esm(() => {
|
|
14372
|
-
|
|
16706
|
+
init_server3();
|
|
14373
16707
|
});
|
|
14374
16708
|
|
|
14375
16709
|
// src/commands/hook.ts
|
|
@@ -15174,6 +17508,17 @@ Security
|
|
|
15174
17508
|
claudemesh backup [file] encrypt config → portable recovery file
|
|
15175
17509
|
claudemesh restore <file> restore config from a backup file
|
|
15176
17510
|
|
|
17511
|
+
Daemon (long-lived peer mesh runtime, v0.9.0)
|
|
17512
|
+
claudemesh daemon up start daemon (alias: start) [--mesh <slug>] [--no-tcp]
|
|
17513
|
+
claudemesh daemon status show running pid + IPC health [--json]
|
|
17514
|
+
claudemesh daemon down stop daemon (alias: stop)
|
|
17515
|
+
claudemesh daemon version ipc + schema version of running daemon
|
|
17516
|
+
claudemesh daemon outbox list list local outbox rows [--failed|--pending|--inflight|--done]
|
|
17517
|
+
claudemesh daemon outbox requeue <id> re-enqueue an aborted/dead row [--new-client-id <id>]
|
|
17518
|
+
claudemesh daemon accept-host pin current host fingerprint
|
|
17519
|
+
claudemesh daemon install-service --mesh <slug> write launchd / systemd-user unit
|
|
17520
|
+
claudemesh daemon uninstall-service remove the unit
|
|
17521
|
+
|
|
15177
17522
|
Setup
|
|
15178
17523
|
claudemesh install register MCP server + hooks
|
|
15179
17524
|
claudemesh uninstall remove MCP server + hooks
|
|
@@ -15470,6 +17815,23 @@ async function main() {
|
|
|
15470
17815
|
process.exit(await whoami2({ json: !!flags.json }));
|
|
15471
17816
|
break;
|
|
15472
17817
|
}
|
|
17818
|
+
case "daemon": {
|
|
17819
|
+
const { runDaemonCommand: runDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon(), exports_daemon));
|
|
17820
|
+
const sub = positionals[0];
|
|
17821
|
+
const rest = positionals.slice(1);
|
|
17822
|
+
const outboxStatus = flags.failed ? "dead" : flags.pending ? "pending" : flags.inflight ? "inflight" : flags.done ? "done" : flags.aborted ? "aborted" : undefined;
|
|
17823
|
+
const code = await runDaemonCommand2(sub, {
|
|
17824
|
+
json: !!flags.json,
|
|
17825
|
+
noTcp: !!flags["no-tcp"],
|
|
17826
|
+
publicHealth: !!flags["public-health"],
|
|
17827
|
+
mesh: flags.mesh,
|
|
17828
|
+
displayName: flags.name,
|
|
17829
|
+
outboxStatus,
|
|
17830
|
+
newClientId: flags["new-client-id"]
|
|
17831
|
+
}, rest);
|
|
17832
|
+
process.exit(code);
|
|
17833
|
+
break;
|
|
17834
|
+
}
|
|
15473
17835
|
case "install": {
|
|
15474
17836
|
const { runInstall: runInstall2 } = await Promise.resolve().then(() => (init_install(), exports_install));
|
|
15475
17837
|
runInstall2(positionals);
|
|
@@ -15490,8 +17852,8 @@ async function main() {
|
|
|
15490
17852
|
const { runStatusSet: runStatusSet2 } = await Promise.resolve().then(() => (init_broker_actions(), exports_broker_actions));
|
|
15491
17853
|
process.exit(await runStatusSet2(positionals[1] ?? "", { mesh: flags.mesh, json: !!flags.json }));
|
|
15492
17854
|
} else {
|
|
15493
|
-
const { runStatus:
|
|
15494
|
-
await
|
|
17855
|
+
const { runStatus: runStatus3 } = await Promise.resolve().then(() => (init_status(), exports_status));
|
|
17856
|
+
await runStatus3();
|
|
15495
17857
|
}
|
|
15496
17858
|
break;
|
|
15497
17859
|
}
|
|
@@ -15643,8 +18005,8 @@ async function main() {
|
|
|
15643
18005
|
const { runStatusSet: runStatusSet2 } = await Promise.resolve().then(() => (init_broker_actions(), exports_broker_actions));
|
|
15644
18006
|
process.exit(await runStatusSet2(positionals[2] ?? "", { mesh: flags.mesh, json: !!flags.json }));
|
|
15645
18007
|
} else {
|
|
15646
|
-
const { runStatus:
|
|
15647
|
-
await
|
|
18008
|
+
const { runStatus: runStatus3 } = await Promise.resolve().then(() => (init_status(), exports_status));
|
|
18009
|
+
await runStatus3();
|
|
15648
18010
|
}
|
|
15649
18011
|
} else {
|
|
15650
18012
|
console.error("Usage: claudemesh profile [summary|visible|status]");
|
|
@@ -16131,4 +18493,4 @@ main().catch((err) => {
|
|
|
16131
18493
|
process.exit(EXIT.INTERNAL_ERROR);
|
|
16132
18494
|
});
|
|
16133
18495
|
|
|
16134
|
-
//# debugId=
|
|
18496
|
+
//# debugId=7D9AD4B3EA7DF68364756E2164756E21
|