feat(daemon): sprint 4 outbound routing + CLI thin-client + ambient mode
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user