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:
@@ -67,7 +67,17 @@ async function listPeersForMesh(slug: string): Promise<PeerRecord[]> {
|
||||
const joined = config.meshes.find((m) => m.slug === slug);
|
||||
const selfMemberPubkey = joined?.pubkey ?? null;
|
||||
|
||||
// Try warm path first.
|
||||
// Daemon path — preferred when running. Same routing pattern as send.ts:
|
||||
// ~1 ms IPC round-trip; broker WS already warm in the daemon.
|
||||
try {
|
||||
const { tryListPeersViaDaemon } = await import("~/services/bridge/daemon-route.js");
|
||||
const dr = await tryListPeersViaDaemon();
|
||||
if (dr !== null) {
|
||||
return dr.map((p) => annotateSelf(p as PeerRecord, selfMemberPubkey, null));
|
||||
}
|
||||
} catch { /* daemon route helper not available; fall through */ }
|
||||
|
||||
// Try warm bridge path next.
|
||||
const bridged = await tryBridge(slug, "peers");
|
||||
if (bridged && bridged.ok) {
|
||||
const peers = bridged.result as PeerRecord[];
|
||||
|
||||
@@ -273,6 +273,24 @@ export async function runSqlSchema(opts: Flags): Promise<number> {
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export async function runSkillList(opts: Flags & { query?: string }): Promise<number> {
|
||||
// Daemon path — preferred when running. Mirror trySendViaDaemon shape.
|
||||
try {
|
||||
const { tryListSkillsViaDaemon } = await import("~/services/bridge/daemon-route.js");
|
||||
const dr = await tryListSkillsViaDaemon();
|
||||
if (dr !== null) {
|
||||
const skills = dr as Array<{ name: string; description: string; author: string; tags: string[] }>;
|
||||
if (opts.json) { emitJson(skills); return EXIT.SUCCESS; }
|
||||
if (skills.length === 0) { render.info(dim("(no skills)")); return EXIT.SUCCESS; }
|
||||
render.section(`mesh skills (${skills.length})`);
|
||||
for (const s of skills) {
|
||||
process.stdout.write(` ${bold(s.name)} ${dim("· by " + s.author)}\n`);
|
||||
process.stdout.write(` ${s.description}\n`);
|
||||
if (s.tags?.length) process.stdout.write(` ${dim("tags: " + s.tags.join(", "))}\n`);
|
||||
}
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
} catch { /* fall through to cold path */ }
|
||||
|
||||
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
const skills = await client.listSkills(opts.query);
|
||||
if (opts.json) { emitJson(skills); return EXIT.SUCCESS; }
|
||||
@@ -289,6 +307,27 @@ export async function runSkillList(opts: Flags & { query?: string }): Promise<nu
|
||||
|
||||
export async function runSkillGet(name: string, opts: Flags): Promise<number> {
|
||||
if (!name) { render.err("Usage: claudemesh skill get <name>"); return EXIT.INVALID_ARGS; }
|
||||
// Daemon path first.
|
||||
try {
|
||||
const { tryGetSkillViaDaemon } = await import("~/services/bridge/daemon-route.js");
|
||||
const dr = await tryGetSkillViaDaemon(name);
|
||||
if (dr !== null) {
|
||||
const skill = dr as { name: string; description: string; instructions: string; tags: string[]; author: string; createdAt: string };
|
||||
if (opts.json) { emitJson(skill); return EXIT.SUCCESS; }
|
||||
render.section(skill.name);
|
||||
render.kv([
|
||||
["author", skill.author],
|
||||
["created", skill.createdAt],
|
||||
["tags", skill.tags?.join(", ") || dim("(none)")],
|
||||
]);
|
||||
render.blank();
|
||||
render.info(skill.description);
|
||||
render.blank();
|
||||
process.stdout.write(skill.instructions + "\n");
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
|
||||
const skill = await client.getSkill(name);
|
||||
if (!skill) { render.err(`skill "${name}" not found`); return EXIT.NOT_FOUND; }
|
||||
|
||||
Reference in New Issue
Block a user