feat(daemon): sprint 4 outbound routing + CLI thin-client + ambient mode
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Daemon outbox now stores resolved target_spec + crypto_box ciphertext
+ nonce per row. Drain worker is a forwarder; no per-row resolution at
drain time. Outbound routing is no longer a placeholder.

Schema additions (additive, NULL allowed for legacy rows): outbox.mesh,
target_spec, nonce, ciphertext, priority. v0.9.0 rows keep draining via
the broadcast fallback so existing in-flight rows finish cleanly.

IPC /v1/send resolves the user-friendly to (display name, hex prefix,
full pubkey, @group, *, #topicId) into a broker-format target_spec at
accept time. DMs encrypt via crypto_box; broadcast/topic/group base64
the plaintext. Hex prefixes (16+ chars) match against connected peers.

CLI thin-client routing extends trySendViaDaemon pattern to peer list
and skill list/get. Three new helpers in services/bridge/daemon-route.ts.

SKILL.md gains ambient mode section: after claudemesh install, raw
claude works for the daemon's attached mesh. Launch stays as the
override path.

Spec at .artifacts/specs/2026-05-04-v2-roadmap-completion.md orders
the remaining v2.0.0 work: multi-mesh daemon (1.26), CLI-to-thin-client
(1.27), mesh-to-workspace rename (1.28), HKDF identity (2.0).

Released as 1.25.0 on npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 01:36:16 +01:00
parent 6794aa8512
commit 0e3a5babd9
13 changed files with 482 additions and 23 deletions

View File

@@ -7,6 +7,50 @@ import { existsSync } from "node:fs";
import { ipc } from "~/daemon/ipc/client.js";
import { DAEMON_PATHS } from "~/daemon/paths.js";
/** Try fetching the peer list through the daemon (~1ms warm IPC).
* Returns null when the daemon socket isn't present so the caller can
* fall back to bridge / cold paths. */
export async function tryListPeersViaDaemon(): Promise<unknown[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ peers?: unknown[] }>({ path: "/v1/peers", timeoutMs: 3_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.peers) ? res.body.peers : [];
} catch (err) {
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return null;
}
}
/** Try fetching mesh-published skills through the daemon. */
export async function tryListSkillsViaDaemon(): Promise<unknown[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ skills?: unknown[] }>({ path: "/v1/skills", timeoutMs: 3_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.skills) ? res.body.skills : [];
} catch (err) {
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return null;
}
}
/** Try fetching one skill body through the daemon. */
export async function tryGetSkillViaDaemon(name: string): Promise<unknown | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ skill?: unknown }>({
path: `/v1/skills/${encodeURIComponent(name)}`,
timeoutMs: 3_000,
});
if (res.status === 404) return null;
if (res.status !== 200) return null;
return res.body.skill ?? null;
} catch { return null; }
}
export type DaemonSendOk = {
ok: true;
messageId: string;