feat(workspace): claudemesh me search + dashboard parity
ships v0.4.0 phase 5 — final aggregating verb. v0.4.0 substrate is complete after this. api: GET /v1/me/search?q=... matches against topic names + sender display names + v1 message snippets (base64 decode then ilike). v2 ciphertext matches only on topic/sender — server has no topic keys. 30-day window on messages, capped at 50 hits per category. cli (1.14.0): claudemesh me search <query> renders topic + msg sections with inline yellow highlighting. min 2 chars; --json returns the raw response. web: /dashboard/search adds an autofocused input + mark highlighting on every match site (topic name, sender, snippet). sidebar gets a search entry between activity and invites. roadmap: phase 5 marked shipped, v0.5.0 default-aggregation behavior added as the natural next track. 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.13.0",
|
||||
"version": "1.14.0",
|
||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -374,6 +374,129 @@ export async function runMeActivity(flags: MeActivityFlags): Promise<number> {
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceSearchTopicHit {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
meshId: string;
|
||||
meshSlug: string;
|
||||
meshName: string;
|
||||
}
|
||||
|
||||
interface WorkspaceSearchMessageHit {
|
||||
messageId: string;
|
||||
topicId: string;
|
||||
topicName: string;
|
||||
meshId: string;
|
||||
meshSlug: string;
|
||||
senderName: string;
|
||||
snippet: string | null;
|
||||
bodyVersion: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface WorkspaceSearchResponse {
|
||||
query: string;
|
||||
topics: WorkspaceSearchTopicHit[];
|
||||
messages: WorkspaceSearchMessageHit[];
|
||||
totals: { topics: number; messages: number };
|
||||
}
|
||||
|
||||
export interface MeSearchFlags extends MeFlags {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export async function runMeSearch(flags: MeSearchFlags): Promise<number> {
|
||||
if (!flags.query || flags.query.length < 2) {
|
||||
process.stderr.write(
|
||||
"Usage: claudemesh me search <query> (min 2 chars)\n",
|
||||
);
|
||||
return EXIT.INVALID_ARGS;
|
||||
}
|
||||
|
||||
return withRestKey(
|
||||
{
|
||||
meshSlug: flags.mesh ?? null,
|
||||
purpose: "workspace-search",
|
||||
capabilities: ["read"],
|
||||
},
|
||||
async ({ secret }) => {
|
||||
const params = new URLSearchParams({ q: flags.query });
|
||||
const ws = await request<WorkspaceSearchResponse>({
|
||||
path: `/api/v1/me/search?${params.toString()}`,
|
||||
token: secret,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(ws, null, 2));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
render.section(
|
||||
`${clay("search")} — "${flags.query}" ${dim(
|
||||
`${ws.totals.topics} topic${ws.totals.topics === 1 ? "" : "s"}, ` +
|
||||
`${ws.totals.messages} message${ws.totals.messages === 1 ? "" : "s"}`,
|
||||
)}`,
|
||||
);
|
||||
|
||||
if (ws.topics.length === 0 && ws.messages.length === 0) {
|
||||
process.stdout.write(dim(" no matches\n"));
|
||||
return EXIT.SUCCESS;
|
||||
}
|
||||
|
||||
if (ws.topics.length > 0) {
|
||||
process.stdout.write(dim("\n topics\n"));
|
||||
const slugWidth = Math.max(
|
||||
...ws.topics.map((t) => t.meshSlug.length),
|
||||
6,
|
||||
);
|
||||
for (const t of ws.topics) {
|
||||
const slug = dim(t.meshSlug.padEnd(slugWidth));
|
||||
const name = cyan(`#${t.name}`);
|
||||
const desc = t.description ? dim(` — ${t.description}`) : "";
|
||||
process.stdout.write(` ${slug} ${name}${desc}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (ws.messages.length > 0) {
|
||||
process.stdout.write(dim("\n messages\n"));
|
||||
const slugWidth = Math.max(
|
||||
...ws.messages.map((m) => m.meshSlug.length),
|
||||
6,
|
||||
);
|
||||
for (const m of ws.messages) {
|
||||
const slug = dim(m.meshSlug.padEnd(slugWidth));
|
||||
const topic = cyan(`#${m.topicName}`);
|
||||
const sender = m.senderName;
|
||||
const ago = formatRelativeTime(m.createdAt);
|
||||
const snippet =
|
||||
m.snippet ??
|
||||
(m.bodyVersion === 2 ? dim("[encrypted — open the topic to decrypt]") : dim("[empty]"));
|
||||
const highlighted =
|
||||
m.snippet
|
||||
? highlightMatch(snippet, flags.query)
|
||||
: snippet;
|
||||
process.stdout.write(
|
||||
` ${slug} ${topic} ${dim(sender + " ·")} ${dim(ago)}\n` +
|
||||
` ${highlighted}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return EXIT.SUCCESS;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function highlightMatch(text: string, query: string): string {
|
||||
if (!query) return text;
|
||||
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
||||
if (idx === -1) return text;
|
||||
const before = text.slice(0, idx);
|
||||
const match = text.slice(idx, idx + query.length);
|
||||
const after = text.slice(idx + query.length);
|
||||
return `${before}${yellow(match)}${after}`;
|
||||
}
|
||||
|
||||
function formatRelativeTime(iso: string): string {
|
||||
const then = new Date(iso).getTime();
|
||||
const now = Date.now();
|
||||
|
||||
@@ -127,6 +127,7 @@ Topic (conversation scope, v0.2.0)
|
||||
claudemesh me topics cross-mesh topic list [--unread]
|
||||
claudemesh me notifications cross-mesh @-mentions [--all] [--since=ISO]
|
||||
claudemesh me activity cross-mesh recent messages [--since=ISO]
|
||||
claudemesh me search <q> cross-mesh search (topics + messages)
|
||||
claudemesh member list mesh roster with online state [--online]
|
||||
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
||||
|
||||
@@ -706,6 +707,10 @@ async function main(): Promise<void> {
|
||||
since: flags.since as string | undefined,
|
||||
}),
|
||||
);
|
||||
} else if (sub === "search") {
|
||||
const { runMeSearch } = await import("~/commands/me.js");
|
||||
const query = positionals.slice(1).join(" ").trim();
|
||||
process.exit(await runMeSearch({ ...f, query }));
|
||||
} else {
|
||||
console.error(
|
||||
"Usage: claudemesh me (cross-mesh overview)\n" +
|
||||
@@ -715,7 +720,8 @@ async function main(): Promise<void> {
|
||||
" claudemesh me notifications --all (include already-read)\n" +
|
||||
" claudemesh me notifications --since=ISO (custom window)\n" +
|
||||
" claudemesh me activity (recent messages, last 24h)\n" +
|
||||
" claudemesh me activity --since=ISO (custom window)",
|
||||
" claudemesh me activity --since=ISO (custom window)\n" +
|
||||
" claudemesh me search <query> (cross-mesh search)",
|
||||
);
|
||||
process.exit(EXIT.INVALID_ARGS);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user