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:
68
apps/cli/src/services/bridge/daemon-route.ts
Normal file
68
apps/cli/src/services/bridge/daemon-route.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user