feat(cli): 1.27.0 — state/memory through daemon + workspace alias

extend the daemon thin-client surface to two more verb families: state
get/set/list now routes through `/v1/state`, and remember/recall/forget
through `/v1/memory`. same warm-path pattern as 1.25.0 — try the unix
socket first, fall back to the cold ws path when the daemon is absent.
multi-mesh aware (aggregates on read, requires `--mesh` for writes
when ambiguous).

also ships an early `claudemesh workspace <verb>` alias surface — bare
teaser for the 1.28.0 mesh→workspace public rename. no-arg falls
through to launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 09:41:18 +01:00
parent cb90f1ca60
commit 3753a6e137
10 changed files with 523 additions and 9 deletions

View File

@@ -7,13 +7,17 @@ import { existsSync } from "node:fs";
import { ipc } from "~/daemon/ipc/client.js";
import { DAEMON_PATHS } from "~/daemon/paths.js";
function meshQuery(mesh?: string): string {
return mesh ? `?mesh=${encodeURIComponent(mesh)}` : "";
}
/** 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> {
export async function tryListPeersViaDaemon(mesh?: string): Promise<unknown[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ peers?: unknown[] }>({ path: "/v1/peers", timeoutMs: 3_000 });
const res = await ipc<{ peers?: unknown[] }>({ path: `/v1/peers${meshQuery(mesh)}`, timeoutMs: 3_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.peers) ? res.body.peers : [];
} catch (err) {
@@ -24,10 +28,10 @@ export async function tryListPeersViaDaemon(): Promise<unknown[] | null> {
}
/** Try fetching mesh-published skills through the daemon. */
export async function tryListSkillsViaDaemon(): Promise<unknown[] | null> {
export async function tryListSkillsViaDaemon(mesh?: string): Promise<unknown[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ skills?: unknown[] }>({ path: "/v1/skills", timeoutMs: 3_000 });
const res = await ipc<{ skills?: unknown[] }>({ path: `/v1/skills${meshQuery(mesh)}`, timeoutMs: 3_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.skills) ? res.body.skills : [];
} catch (err) {
@@ -38,11 +42,11 @@ export async function tryListSkillsViaDaemon(): Promise<unknown[] | null> {
}
/** Try fetching one skill body through the daemon. */
export async function tryGetSkillViaDaemon(name: string): Promise<unknown | null> {
export async function tryGetSkillViaDaemon(name: string, mesh?: string): Promise<unknown | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ skill?: unknown }>({
path: `/v1/skills/${encodeURIComponent(name)}`,
path: `/v1/skills/${encodeURIComponent(name)}${meshQuery(mesh)}`,
timeoutMs: 3_000,
});
if (res.status === 404) return null;
@@ -51,6 +55,109 @@ export async function tryGetSkillViaDaemon(name: string): Promise<unknown | null
} catch { return null; }
}
// --- state ---
export type StateEntry = {
key: string;
value: unknown;
updatedBy: string;
updatedAt: string;
mesh?: string;
};
/** Try reading a single state key through the daemon. Returns:
* - the entry when the daemon found it
* - undefined when the daemon ran but the key is unset (404)
* - null when the daemon socket isn't present (caller falls back) */
export async function tryGetStateViaDaemon(key: string, mesh?: string): Promise<StateEntry | undefined | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const path = `/v1/state?key=${encodeURIComponent(key)}${mesh ? `&mesh=${encodeURIComponent(mesh)}` : ""}`;
const res = await ipc<{ state?: StateEntry; error?: string }>({ path, timeoutMs: 3_000 });
if (res.status === 404) return undefined;
if (res.status !== 200) return null;
return res.body.state ?? undefined;
} catch (err) {
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return null;
}
}
export async function tryListStateViaDaemon(mesh?: string): Promise<StateEntry[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ entries?: StateEntry[] }>({ path: `/v1/state${meshQuery(mesh)}`, timeoutMs: 3_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.entries) ? res.body.entries : [];
} catch (err) {
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return null;
}
}
export async function trySetStateViaDaemon(key: string, value: unknown, mesh?: string): Promise<boolean> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return false;
try {
const res = await ipc<{ ok?: boolean; error?: string }>({
method: "POST",
path: "/v1/state",
timeoutMs: 3_000,
body: { key, value, ...(mesh ? { mesh } : {}) },
});
return res.status === 200 && res.body.ok === true;
} catch { return false; }
}
// --- memory ---
export type MemoryEntry = {
id: string;
content: string;
tags: string[];
rememberedBy: string;
rememberedAt: string;
mesh?: string;
};
export async function tryRememberViaDaemon(content: string, tags?: string[], mesh?: string): Promise<{ id: string; mesh?: string } | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const res = await ipc<{ id?: string; mesh?: string; error?: string }>({
method: "POST",
path: "/v1/memory",
timeoutMs: 5_000,
body: { content, ...(tags?.length ? { tags } : {}), ...(mesh ? { mesh } : {}) },
});
if (res.status !== 200 || !res.body.id) return null;
return { id: res.body.id, mesh: res.body.mesh };
} catch { return null; }
}
export async function tryRecallViaDaemon(query: string, mesh?: string): Promise<MemoryEntry[] | null> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return null;
try {
const path = `/v1/memory?q=${encodeURIComponent(query)}${mesh ? `&mesh=${encodeURIComponent(mesh)}` : ""}`;
const res = await ipc<{ matches?: MemoryEntry[] }>({ path, timeoutMs: 5_000 });
if (res.status !== 200) return null;
return Array.isArray(res.body.matches) ? res.body.matches : [];
} catch (err) {
const msg = String(err);
if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg)) return null;
return null;
}
}
export async function tryForgetViaDaemon(id: string, mesh?: string): Promise<boolean> {
if (!existsSync(DAEMON_PATHS.SOCK_FILE)) return false;
try {
const path = `/v1/memory/${encodeURIComponent(id)}${meshQuery(mesh)}`;
const res = await ipc<{ ok?: boolean }>({ method: "DELETE", path, timeoutMs: 3_000 });
return res.status === 200 && res.body.ok === true;
} catch { return false; }
}
export type DaemonSendOk = {
ok: true;
messageId: string;