feat(cli): claudemesh daemon — peer mesh runtime (v0.9.0)

Long-lived process that holds a persistent WS to the broker and exposes
a local IPC surface (UDS + bearer-auth TCP loopback). Implements the
v0.9.0 spec under .artifacts/specs/.

Core:
- daemon up | status | version | down | accept-host
- daemon outbox list [--failed|--pending|--inflight|--done|--aborted]
- daemon outbox requeue <id> [--new-client-id <id>]
- daemon install-service / uninstall-service (macOS launchd, Linux systemd)

IPC routes:
- /v1/version, /v1/health
- /v1/send  (POST)  — full §4.5.1 idempotency lookup table
- /v1/inbox (GET)   — paged history
- /v1/events        — SSE stream of message/peer_join/peer_leave/broker_status
- /v1/peers         — broker passthrough
- /v1/profile       — summary/status/visible/avatar/title/bio/capabilities
- /v1/outbox + /v1/outbox/requeue — operator recovery

Storage (SQLite via node:sqlite / bun:sqlite):
- outbox.db: pending/inflight/done/dead/aborted with audit columns
- inbox.db: dedupe by client_message_id, decrypts DMs via existing crypto
- BEGIN IMMEDIATE serialization for daemon-local accept races

Identity:
- host_fingerprint.json (machine-id || first-stable-mac)
- refuse-on-mismatch policy with `daemon accept-host` recovery

CLI integration:
- claudemesh send detects the daemon and routes through /v1/send when
  present, falling back to bridge socket / cold path otherwise

Tests: 15-case coverage of the §4.5.1 IPC duplicate lookup table.

Spec arc preserved at .artifacts/specs/2026-05-03-daemon-{v1..v10}.md;
v0.9.0 implementation target locked at 2026-05-03-daemon-spec-v0.9.0.md;
deferred items at 2026-05-03-daemon-spec-broker-hardening-followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-03 20:03:05 +01:00
parent 65e63b0b27
commit abaa4bcf87
34 changed files with 9067 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
// Try forwarding a send through the local daemon's IPC. Returns null if
// the daemon isn't running or the daemon's mesh doesn't match the target
// mesh — the caller falls back to the bridge or cold path.
import { existsSync } from "node:fs";
import { ipc } from "~/daemon/ipc/client.js";
import { DAEMON_PATHS } from "~/daemon/paths.js";
export type DaemonSendOk = {
ok: true;
messageId: string;
duplicate?: boolean;
status?: "queued" | "inflight";
};
export type DaemonSendErr = { ok: false; error: string };
export type DaemonSendResult = DaemonSendOk | DaemonSendErr;
export async function trySendViaDaemon(args: {
to: string;
message: string;
priority: "now" | "next" | "low";
/** Caller-stable id for cross-invocation idempotency. Optional. */
idempotencyKey?: string;
/** When set, only forward to the daemon if it's attached to this mesh.
* We can't query the daemon's mesh today (no IPC route exposes it),
* so for v0.9.0 this is informational; the caller already picked the
* right mesh by either flag or single-mesh-default. */
expectedMesh?: string;
}): Promise<DaemonSendResult | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{
client_message_id?: string;
status?: "queued" | "inflight";
broker_message_id?: string;
duplicate?: boolean;
error?: string;
}>({
method: "POST",
path: "/v1/send",
timeoutMs: 3_000,
body: {
to: args.to,
message: args.message,
priority: args.priority,
...(args.idempotencyKey ? { client_message_id: args.idempotencyKey } : {}),
},
});
if (res.status === 202 || res.status === 200) {
return {
ok: true,
messageId: res.body.broker_message_id ?? res.body.client_message_id ?? "",
duplicate: res.body.duplicate,
status: res.body.status,
};
}
return { ok: false, error: res.body.error ?? `daemon http ${res.status}` };
} catch (err) {
// Connection errors → daemon went away mid-call. Treat as "not present"
// so the caller falls back rather than failing.
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return { ok: false, error: msg };
}
}