feat(workspace): default-aggregation for task/state/memory
ships v0.5.0 phase 2. api: three new aggregator endpoints for the per-mesh subsystems that didn't have one yet. - GET /v1/me/tasks — open + claimed by default; ?status=all surfaces completed (30d window). sorted open > claimed > done. - GET /v1/me/state — every (key, value) row across the user's meshes, sorted by recency. ?key=foo filters to one key. - GET /v1/me/memory?q=... — ilike on content + tags, no q returns the last 30 days. excludes forgotten rows. cli (1.16.0): task list, state list, recall now route through the matching aggregator when --mesh is omitted. --mesh foo still scopes to one mesh (existing behavior preserved). with this, every per-mesh read verb in the cli either has a cross-mesh aggregator or doesn't need one. v0.5.0 substrate is complete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -510,6 +510,237 @@ function highlightMatch(text: string, query: string): string {
|
||||
return `${before}${yellow(match)}${after}`;
|
||||
}
|
||||
|
||||
interface WorkspaceTask {
|
||||
id: string;
|
||||
meshId: string;
|
||||
meshSlug: string;
|
||||
title: string;
|
||||
assignee: string | null;
|
||||
claimedByName: string | null;
|
||||
priority: string;
|
||||
status: string;
|
||||
tags: string[];
|
||||
result: string | null;
|
||||
createdByName: string | null;
|
||||
createdAt: string;
|
||||
claimedAt: string | null;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
interface WorkspaceTasksResponse {
|
||||
tasks: WorkspaceTask[];
|
||||
totals: { open: number; claimed: number; completed: number };
|
||||
}
|
||||
|
||||
export interface MeTasksFlags extends MeFlags {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export async function runMeTasks(flags: MeTasksFlags): Promise<number> {
|
||||
return withRestKey(
|
||||
{
|
||||
meshSlug: resolveMeshForMint(flags.mesh),
|
||||
purpose: "workspace-tasks",
|
||||
capabilities: ["read"],
|
||||
},
|
||||
async ({ secret }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (flags.status) params.set("status", flags.status);
|
||||
const path =
|
||||
"/api/v1/me/tasks" +
|
||||
(params.toString() ? `?${params.toString()}` : "");
|
||||
const ws = await request<WorkspaceTasksResponse>({
|
||||
path,
|
||||
token: secret,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(ws, null, 2));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
render.section(
|
||||
`${clay("tasks")} — ${dim(
|
||||
`${ws.totals.open} open · ${ws.totals.claimed} in-flight · ${ws.totals.completed} done`,
|
||||
)}`,
|
||||
);
|
||||
|
||||
if (ws.tasks.length === 0) {
|
||||
process.stdout.write(dim(" no tasks in window\n"));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
const slugWidth = Math.max(...ws.tasks.map((t) => t.meshSlug.length), 6);
|
||||
for (const t of ws.tasks) {
|
||||
const slug = dim(t.meshSlug.padEnd(slugWidth));
|
||||
const status =
|
||||
t.status === "open"
|
||||
? yellow("open ")
|
||||
: t.status === "claimed"
|
||||
? cyan("working ")
|
||||
: green("done ");
|
||||
const prio =
|
||||
t.priority === "urgent"
|
||||
? yellow("!")
|
||||
: t.priority === "low"
|
||||
? dim("·")
|
||||
: " ";
|
||||
const claimer = t.claimedByName ? dim(` ← ${t.claimedByName}`) : "";
|
||||
process.stdout.write(
|
||||
` ${slug} ${prio} ${status} ${t.title}${claimer}\n`,
|
||||
);
|
||||
}
|
||||
return EXIT.SUCCESS;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceStateEntry {
|
||||
meshId: string;
|
||||
meshSlug: string;
|
||||
key: string;
|
||||
value: unknown;
|
||||
updatedByName: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface WorkspaceStateResponse {
|
||||
entries: WorkspaceStateEntry[];
|
||||
totals: { entries: number; meshes: number };
|
||||
}
|
||||
|
||||
export interface MeStateFlags extends MeFlags {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export async function runMeState(flags: MeStateFlags): Promise<number> {
|
||||
return withRestKey(
|
||||
{
|
||||
meshSlug: resolveMeshForMint(flags.mesh),
|
||||
purpose: "workspace-state",
|
||||
capabilities: ["read"],
|
||||
},
|
||||
async ({ secret }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (flags.key) params.set("key", flags.key);
|
||||
const path =
|
||||
"/api/v1/me/state" +
|
||||
(params.toString() ? `?${params.toString()}` : "");
|
||||
const ws = await request<WorkspaceStateResponse>({
|
||||
path,
|
||||
token: secret,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(ws, null, 2));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
render.section(
|
||||
`${clay("state")} — ${ws.totals.entries} entr${ws.totals.entries === 1 ? "y" : "ies"} ${dim(
|
||||
`across ${ws.totals.meshes} mesh${ws.totals.meshes === 1 ? "" : "es"}`,
|
||||
)}`,
|
||||
);
|
||||
|
||||
if (ws.entries.length === 0) {
|
||||
process.stdout.write(dim(" no state entries\n"));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
const slugWidth = Math.max(...ws.entries.map((e) => e.meshSlug.length), 6);
|
||||
const keyWidth = Math.max(...ws.entries.map((e) => e.key.length), 8);
|
||||
for (const e of ws.entries) {
|
||||
const slug = dim(e.meshSlug.padEnd(slugWidth));
|
||||
const key = cyan(e.key.padEnd(keyWidth));
|
||||
const valueStr =
|
||||
typeof e.value === "string"
|
||||
? e.value
|
||||
: JSON.stringify(e.value);
|
||||
const trimmed =
|
||||
valueStr.length > 80 ? valueStr.slice(0, 80) + "…" : valueStr;
|
||||
const ago = dim(formatRelativeTime(e.updatedAt));
|
||||
process.stdout.write(` ${slug} ${key} ${trimmed} ${ago}\n`);
|
||||
}
|
||||
return EXIT.SUCCESS;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceMemory {
|
||||
id: string;
|
||||
meshId: string;
|
||||
meshSlug: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
rememberedByName: string | null;
|
||||
rememberedAt: string;
|
||||
}
|
||||
|
||||
interface WorkspaceMemoryResponse {
|
||||
query: string;
|
||||
memories: WorkspaceMemory[];
|
||||
totals: { entries: number };
|
||||
}
|
||||
|
||||
export interface MeMemoryFlags extends MeFlags {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export async function runMeMemory(flags: MeMemoryFlags): Promise<number> {
|
||||
return withRestKey(
|
||||
{
|
||||
meshSlug: resolveMeshForMint(flags.mesh),
|
||||
purpose: "workspace-memory",
|
||||
capabilities: ["read"],
|
||||
},
|
||||
async ({ secret }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (flags.query) params.set("q", flags.query);
|
||||
const path =
|
||||
"/api/v1/me/memory" +
|
||||
(params.toString() ? `?${params.toString()}` : "");
|
||||
const ws = await request<WorkspaceMemoryResponse>({
|
||||
path,
|
||||
token: secret,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(ws, null, 2));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
const headerLabel = flags.query
|
||||
? `recall — "${flags.query}"`
|
||||
: "recall — last 30 days";
|
||||
render.section(
|
||||
`${clay(headerLabel)} ${dim(`${ws.totals.entries} match${ws.totals.entries === 1 ? "" : "es"}`)}`,
|
||||
);
|
||||
|
||||
if (ws.memories.length === 0) {
|
||||
process.stdout.write(dim(" no memories\n"));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
const slugWidth = Math.max(
|
||||
...ws.memories.map((m) => m.meshSlug.length),
|
||||
6,
|
||||
);
|
||||
for (const m of ws.memories) {
|
||||
const slug = dim(m.meshSlug.padEnd(slugWidth));
|
||||
const ago = dim(formatRelativeTime(m.rememberedAt));
|
||||
const tags =
|
||||
m.tags.length > 0
|
||||
? " " + dim("[" + m.tags.join(", ") + "]")
|
||||
: "";
|
||||
const content =
|
||||
m.content.length > 240 ? m.content.slice(0, 240) + "…" : m.content;
|
||||
process.stdout.write(` ${slug} ${ago}${tags}\n ${content}\n`);
|
||||
}
|
||||
return EXIT.SUCCESS;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function formatRelativeTime(iso: string): string {
|
||||
const then = new Date(iso).getTime();
|
||||
const now = Date.now();
|
||||
|
||||
@@ -323,13 +323,30 @@ async function main(): Promise<void> {
|
||||
case "state": {
|
||||
const sub = positionals[0];
|
||||
if (sub === "set") { const { runStateSet } = await import("~/commands/state.js"); await runStateSet({}, positionals[1] ?? "", positionals[2] ?? ""); }
|
||||
else if (sub === "list") { const { runStateList } = await import("~/commands/state.js"); await runStateList({}); }
|
||||
else if (sub === "list") {
|
||||
// v0.5.0 phase 2: aggregate across every mesh when --mesh is omitted.
|
||||
if (!flags.mesh) {
|
||||
const { runMeState } = await import("~/commands/me.js");
|
||||
process.exit(await runMeState({ json: !!flags.json, key: flags.key as string | undefined }));
|
||||
}
|
||||
const { runStateList } = await import("~/commands/state.js");
|
||||
await runStateList({});
|
||||
}
|
||||
else { const { runStateGet } = await import("~/commands/state.js"); await runStateGet({}, positionals[0] ?? ""); }
|
||||
break;
|
||||
}
|
||||
case "info": { const { runInfo } = await import("~/commands/info.js"); await runInfo({}); break; }
|
||||
case "remember": { const { remember } = await import("~/commands/remember.js"); process.exit(await remember(positionals.join(" "), { mesh: flags.mesh as string, tags: flags.tags as string, json: !!flags.json })); break; }
|
||||
case "recall": { const { recall } = await import("~/commands/recall.js"); process.exit(await recall(positionals.join(" "), { mesh: flags.mesh as string, json: !!flags.json })); break; }
|
||||
case "recall": {
|
||||
// v0.5.0 phase 2: aggregate across every mesh when --mesh is omitted.
|
||||
if (!flags.mesh) {
|
||||
const { runMeMemory } = await import("~/commands/me.js");
|
||||
process.exit(await runMeMemory({ json: !!flags.json, query: positionals.join(" ") }));
|
||||
}
|
||||
const { recall } = await import("~/commands/recall.js");
|
||||
process.exit(await recall(positionals.join(" "), { mesh: flags.mesh as string, json: !!flags.json }));
|
||||
break;
|
||||
}
|
||||
case "forget": { const { runForget } = await import("~/commands/broker-actions.js"); process.exit(await runForget(positionals[0], { mesh: flags.mesh as string, json: !!flags.json })); break; }
|
||||
case "remind": { const { runRemind } = await import("~/commands/remind.js"); await runRemind({ mesh: flags.mesh as string }, positionals); break; }
|
||||
// (profile case moved to resource-aliases block below for sub-command extensibility)
|
||||
@@ -774,7 +791,15 @@ async function main(): Promise<void> {
|
||||
const f = { mesh: flags.mesh as string, json: !!flags.json };
|
||||
if (sub === "claim") { const { runTaskClaim } = await import("~/commands/broker-actions.js"); process.exit(await runTaskClaim(positionals[1], f)); }
|
||||
else if (sub === "complete") { const { runTaskComplete } = await import("~/commands/broker-actions.js"); process.exit(await runTaskComplete(positionals[1], positionals.slice(2).join(" ") || undefined, f)); }
|
||||
else if (sub === "list") { const { runTaskList } = await import("~/commands/platform-actions.js"); process.exit(await runTaskList({ ...f, status: flags.status as string, assignee: flags.assignee as string })); }
|
||||
else if (sub === "list") {
|
||||
// v0.5.0 phase 2: aggregate across every mesh when --mesh is omitted.
|
||||
if (!f.mesh) {
|
||||
const { runMeTasks } = await import("~/commands/me.js");
|
||||
process.exit(await runMeTasks({ json: f.json, status: flags.status as string | undefined }));
|
||||
}
|
||||
const { runTaskList } = await import("~/commands/platform-actions.js");
|
||||
process.exit(await runTaskList({ ...f, status: flags.status as string, assignee: flags.assignee as string }));
|
||||
}
|
||||
else if (sub === "create") { const { runTaskCreate } = await import("~/commands/platform-actions.js"); process.exit(await runTaskCreate(positionals.slice(1).join(" "), { ...f, assignee: flags.assignee as string, priority: flags.priority as string, tags: flags.tags as string })); }
|
||||
else { console.error("Usage: claudemesh task <create|list|claim|complete>"); process.exit(EXIT.INVALID_ARGS); }
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user