feat(cli): default-aggregation for topic list + notification list
ships v0.5.0 phase 1. omitting --mesh on these read verbs now routes through /v1/me/topics and /v1/me/notifications instead of prompting the user to pick a mesh. behavior preserved for explicit --mesh foo. implementation: resolveMeshForMint helper in commands/me.ts silently picks the first joined mesh for apikey-mint when flags.mesh is null. /v1/me/* endpoints resolve the user from the apikey issuer regardless of which mesh issued the key, so mint location is irrelevant — only the user identity matters. help text updated to reflect the new default. phase 2 (task list, state list, memory recall) needs /v1/me/* aggregator endpoints first; deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"name": "claudemesh-cli",
|
||||||
"version": "1.14.0",
|
"version": "1.15.0",
|
||||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -16,10 +16,23 @@
|
|||||||
|
|
||||||
import { withRestKey } from "~/services/api/with-rest-key.js";
|
import { withRestKey } from "~/services/api/with-rest-key.js";
|
||||||
import { request } from "~/services/api/client.js";
|
import { request } from "~/services/api/client.js";
|
||||||
|
import { readConfig } from "~/services/config/facade.js";
|
||||||
import { render } from "~/ui/render.js";
|
import { render } from "~/ui/render.js";
|
||||||
import { bold, clay, cyan, dim, green, yellow } from "~/ui/styles.js";
|
import { bold, clay, cyan, dim, green, yellow } from "~/ui/styles.js";
|
||||||
import { EXIT } from "~/constants/exit-codes.js";
|
import { EXIT } from "~/constants/exit-codes.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /v1/me/* endpoints resolve the caller's user from the apikey issuer
|
||||||
|
* regardless of which mesh issued the key — every mesh works. When the
|
||||||
|
* user didn't pass --mesh, silently pick the first joined mesh for
|
||||||
|
* apikey-mint instead of prompting; the endpoint sees the same user.
|
||||||
|
*/
|
||||||
|
function resolveMeshForMint(explicit: string | null | undefined): string | null {
|
||||||
|
if (explicit) return explicit;
|
||||||
|
const cfg = readConfig();
|
||||||
|
return cfg.meshes[0]?.slug ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkspaceMesh {
|
interface WorkspaceMesh {
|
||||||
meshId: string;
|
meshId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -53,7 +66,7 @@ export interface MeFlags {
|
|||||||
export async function runMe(flags: MeFlags): Promise<number> {
|
export async function runMe(flags: MeFlags): Promise<number> {
|
||||||
return withRestKey(
|
return withRestKey(
|
||||||
{
|
{
|
||||||
meshSlug: flags.mesh ?? null,
|
meshSlug: resolveMeshForMint(flags.mesh),
|
||||||
purpose: "workspace-overview",
|
purpose: "workspace-overview",
|
||||||
capabilities: ["read"],
|
capabilities: ["read"],
|
||||||
},
|
},
|
||||||
@@ -136,7 +149,7 @@ export interface MeTopicsFlags extends MeFlags {
|
|||||||
export async function runMeTopics(flags: MeTopicsFlags): Promise<number> {
|
export async function runMeTopics(flags: MeTopicsFlags): Promise<number> {
|
||||||
return withRestKey(
|
return withRestKey(
|
||||||
{
|
{
|
||||||
meshSlug: flags.mesh ?? null,
|
meshSlug: resolveMeshForMint(flags.mesh),
|
||||||
purpose: "workspace-topics",
|
purpose: "workspace-topics",
|
||||||
capabilities: ["read"],
|
capabilities: ["read"],
|
||||||
},
|
},
|
||||||
@@ -232,7 +245,7 @@ export async function runMeNotifications(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
return withRestKey(
|
return withRestKey(
|
||||||
{
|
{
|
||||||
meshSlug: flags.mesh ?? null,
|
meshSlug: resolveMeshForMint(flags.mesh),
|
||||||
purpose: "workspace-notifications",
|
purpose: "workspace-notifications",
|
||||||
capabilities: ["read"],
|
capabilities: ["read"],
|
||||||
},
|
},
|
||||||
@@ -321,7 +334,7 @@ export interface MeActivityFlags extends MeFlags {
|
|||||||
export async function runMeActivity(flags: MeActivityFlags): Promise<number> {
|
export async function runMeActivity(flags: MeActivityFlags): Promise<number> {
|
||||||
return withRestKey(
|
return withRestKey(
|
||||||
{
|
{
|
||||||
meshSlug: flags.mesh ?? null,
|
meshSlug: resolveMeshForMint(flags.mesh),
|
||||||
purpose: "workspace-activity",
|
purpose: "workspace-activity",
|
||||||
capabilities: ["read"],
|
capabilities: ["read"],
|
||||||
},
|
},
|
||||||
@@ -416,7 +429,7 @@ export async function runMeSearch(flags: MeSearchFlags): Promise<number> {
|
|||||||
|
|
||||||
return withRestKey(
|
return withRestKey(
|
||||||
{
|
{
|
||||||
meshSlug: flags.mesh ?? null,
|
meshSlug: resolveMeshForMint(flags.mesh),
|
||||||
purpose: "workspace-search",
|
purpose: "workspace-search",
|
||||||
capabilities: ["read"],
|
capabilities: ["read"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ Bridge (forward a topic between two meshes, v0.2.0)
|
|||||||
|
|
||||||
Topic (conversation scope, v0.2.0)
|
Topic (conversation scope, v0.2.0)
|
||||||
claudemesh topic create <name> create a topic [--description --visibility]
|
claudemesh topic create <name> create a topic [--description --visibility]
|
||||||
claudemesh topic list list topics in the mesh
|
claudemesh topic list list topics across all meshes (or --mesh <slug>)
|
||||||
claudemesh topic join <topic> subscribe (via name or id)
|
claudemesh topic join <topic> subscribe (via name or id)
|
||||||
claudemesh topic leave <topic> unsubscribe
|
claudemesh topic leave <topic> unsubscribe
|
||||||
claudemesh topic members <t> list topic subscribers
|
claudemesh topic members <t> list topic subscribers
|
||||||
@@ -129,7 +129,7 @@ Topic (conversation scope, v0.2.0)
|
|||||||
claudemesh me activity cross-mesh recent messages [--since=ISO]
|
claudemesh me activity cross-mesh recent messages [--since=ISO]
|
||||||
claudemesh me search <q> cross-mesh search (topics + messages)
|
claudemesh me search <q> cross-mesh search (topics + messages)
|
||||||
claudemesh member list mesh roster with online state [--online]
|
claudemesh member list mesh roster with online state [--online]
|
||||||
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
claudemesh notification list @-mentions across all meshes (or --mesh <slug>)
|
||||||
|
|
||||||
Schedule (resource form)
|
Schedule (resource form)
|
||||||
claudemesh schedule msg <m> one-shot or recurring (alias: remind)
|
claudemesh schedule msg <m> one-shot or recurring (alias: remind)
|
||||||
@@ -628,7 +628,17 @@ async function main(): Promise<void> {
|
|||||||
};
|
};
|
||||||
const arg = positionals[1] ?? "";
|
const arg = positionals[1] ?? "";
|
||||||
if (sub === "create") { const { runTopicCreate } = await import("~/commands/topic.js"); process.exit(await runTopicCreate(arg, f)); }
|
if (sub === "create") { const { runTopicCreate } = await import("~/commands/topic.js"); process.exit(await runTopicCreate(arg, f)); }
|
||||||
else if (sub === "list") { const { runTopicList } = await import("~/commands/topic.js"); process.exit(await runTopicList(f)); }
|
else if (sub === "list") {
|
||||||
|
// v0.5.0: aggregate across every joined mesh when --mesh is omitted.
|
||||||
|
// The per-mesh runTopicList prompts for a mesh; me topics doesn't
|
||||||
|
// need one and surfaces last-activity + unread per topic.
|
||||||
|
if (!f.mesh) {
|
||||||
|
const { runMeTopics } = await import("~/commands/me.js");
|
||||||
|
process.exit(await runMeTopics({ mesh: undefined, json: f.json, unread: !!flags.unread }));
|
||||||
|
}
|
||||||
|
const { runTopicList } = await import("~/commands/topic.js");
|
||||||
|
process.exit(await runTopicList(f));
|
||||||
|
}
|
||||||
else if (sub === "join") { const { runTopicJoin } = await import("~/commands/topic.js"); process.exit(await runTopicJoin(arg, f)); }
|
else if (sub === "join") { const { runTopicJoin } = await import("~/commands/topic.js"); process.exit(await runTopicJoin(arg, f)); }
|
||||||
else if (sub === "leave") { const { runTopicLeave } = await import("~/commands/topic.js"); process.exit(await runTopicLeave(arg, f)); }
|
else if (sub === "leave") { const { runTopicLeave } = await import("~/commands/topic.js"); process.exit(await runTopicLeave(arg, f)); }
|
||||||
else if (sub === "members") { const { runTopicMembers } = await import("~/commands/topic.js"); process.exit(await runTopicMembers(arg, f)); }
|
else if (sub === "members") { const { runTopicMembers } = await import("~/commands/topic.js"); process.exit(await runTopicMembers(arg, f)); }
|
||||||
@@ -668,10 +678,22 @@ async function main(): Promise<void> {
|
|||||||
since: flags.since as string,
|
since: flags.since as string,
|
||||||
};
|
};
|
||||||
if (sub === "list") {
|
if (sub === "list") {
|
||||||
|
// v0.5.0: aggregate across every joined mesh when --mesh is omitted.
|
||||||
|
if (!f.mesh) {
|
||||||
|
const { runMeNotifications } = await import("~/commands/me.js");
|
||||||
|
process.exit(
|
||||||
|
await runMeNotifications({
|
||||||
|
mesh: undefined,
|
||||||
|
json: f.json,
|
||||||
|
all: !!flags.all,
|
||||||
|
since: f.since,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
const { runNotificationList } = await import("~/commands/notification.js");
|
const { runNotificationList } = await import("~/commands/notification.js");
|
||||||
process.exit(await runNotificationList(f));
|
process.exit(await runNotificationList(f));
|
||||||
} else {
|
} else {
|
||||||
console.error("Usage: claudemesh notification list [--since <ISO>]");
|
console.error("Usage: claudemesh notification list [--mesh <slug>] [--since <ISO>] [--all]");
|
||||||
process.exit(EXIT.INVALID_ARGS);
|
process.exit(EXIT.INVALID_ARGS);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -284,12 +284,18 @@ level, or wire claudemesh to messaging surfaces beyond Claude Code.
|
|||||||
highlighting + 30-day scan note. *Shipped 2026-05-03 in CLI
|
highlighting + 30-day scan note. *Shipped 2026-05-03 in CLI
|
||||||
v1.14.0.* v0.4.0 substrate is complete — every aggregating
|
v1.14.0.* v0.4.0 substrate is complete — every aggregating
|
||||||
read verb now has CLI + web parity.
|
read verb now has CLI + web parity.
|
||||||
- **v0.5.0 — default-aggregation rule** for existing per-mesh
|
- **v0.5.0 phase 1 — default-aggregation for `topic list` +
|
||||||
read verbs (`task list`, `state list`, `memory recall`,
|
`notification list`** — when no `--mesh` is passed these verbs
|
||||||
`notification list`, `topic list`) — when no `--mesh` is
|
now route through `/v1/me/topics` and `/v1/me/notifications`
|
||||||
passed, route through the `/v1/me/*` aggregator instead of
|
instead of prompting. `--mesh foo` keeps the per-mesh
|
||||||
prompting for a mesh. Backward-compatible: `--mesh foo` still
|
behavior. *Shipped 2026-05-03 in CLI v1.15.0.*
|
||||||
scopes to one mesh.
|
- **v0.5.0 phase 2+ — default-aggregation for `task list`,
|
||||||
|
`state list`, `memory recall`** — needs `/v1/me/tasks`,
|
||||||
|
`/v1/me/state`, `/v1/me/memory` aggregator endpoints first.
|
||||||
|
Each subsystem's per-mesh keying scheme decides whether
|
||||||
|
aggregation is straight union (state) or needs ranking
|
||||||
|
(memory recall — vector similarity across meshes is non-
|
||||||
|
trivial).
|
||||||
- **v0.3.2 — multi-session DM routing + broadcast self-loopback** —
|
- **v0.3.2 — multi-session DM routing + broadcast self-loopback** —
|
||||||
fixes two production bugs: (1) replies via `claudemesh send
|
fixes two production bugs: (1) replies via `claudemesh send
|
||||||
<from_id>` rejected with "no connected peer" when the sender's
|
<from_id>` rejected with "no connected peer" when the sender's
|
||||||
|
|||||||
Reference in New Issue
Block a user