From f9ed3fa2867316d5a8817428d8aee493782e3ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sun, 3 May 2026 12:24:45 +0100 Subject: [PATCH] feat(cli): claudemesh skill prints bundled SKILL.md (v1.18.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-install access to the protocol reference: a fresh `npm i -g claudemesh-cli` user (or someone running the prebuilt binary) can now `claudemesh skill | claude --skill-add -` without copying any files into ~/.claude/skills. The skill markdown is embedded into the CLI bundle at build time via Bun's text-import attribute. Also replaces two `<> ALL(...)` raw SQL fragments in the dashboard unread-count queries with drizzle's notInArray() helper — matches the same fix already applied to /v1/me/topics in the API package. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/commands/skill.ts | 21 +++++++++++++++++++ apps/cli/src/entrypoints/cli.ts | 9 ++++++-- .../app/[locale]/dashboard/(user)/page.tsx | 4 ++-- .../[locale]/dashboard/(user)/topics/page.tsx | 4 ++-- docs/roadmap.md | 9 ++++++++ 6 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 apps/cli/src/commands/skill.ts diff --git a/apps/cli/package.json b/apps/cli/package.json index 5ba8caa..2088cc7 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.17.0", + "version": "1.18.0", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/skill.ts b/apps/cli/src/commands/skill.ts new file mode 100644 index 0000000..c04e72e --- /dev/null +++ b/apps/cli/src/commands/skill.ts @@ -0,0 +1,21 @@ +/** + * `claudemesh skill` — print the bundled SKILL.md to stdout. + * + * Zero-install access: the skill is embedded into the binary at build + * time via Bun's text-import attribute, so a fresh `npm i -g` user + * (or someone running the prebuilt binary) can pipe the contents into + * Claude Code (or anywhere else) without copying files into + * ~/.claude/skills. + * + * claudemesh skill | claude --skill-add - + * claudemesh skill > /tmp/cm.md + */ + +import skillContent from "../../skills/claudemesh/SKILL.md" with { type: "text" }; +import { EXIT } from "~/constants/exit-codes.js"; + +export async function runSkill(): Promise { + process.stdout.write(skillContent); + if (!skillContent.endsWith("\n")) process.stdout.write("\n"); + return EXIT.SUCCESS; +} diff --git a/apps/cli/src/entrypoints/cli.ts b/apps/cli/src/entrypoints/cli.ts index 0786941..32a628e 100644 --- a/apps/cli/src/entrypoints/cli.ts +++ b/apps/cli/src/entrypoints/cli.ts @@ -123,6 +123,7 @@ Topic (conversation scope, v0.2.0) claudemesh topic tail live SSE tail [--limit --forward-only] claudemesh topic post encrypted REST post (v0.3.0 v2) [--reply-to ] claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext) + claudemesh skill print the bundled SKILL.md to stdout claudemesh me cross-mesh workspace overview (v0.4.0) claudemesh me topics cross-mesh topic list [--unread] claudemesh me notifications cross-mesh @-mentions [--all] [--since=ISO] @@ -538,10 +539,14 @@ async function main(): Promise { case "skill": { const sub = positionals[0]; const f = { mesh: flags.mesh as string, json: !!flags.json }; - if (sub === "list") { const { runSkillList } = await import("~/commands/platform-actions.js"); process.exit(await runSkillList({ ...f, query: positionals[1] })); } + // No subcommand → print the bundled SKILL.md to stdout. Lets a + // fresh user pipe `claudemesh skill | claude --skill-add -` + // without copying anything into ~/.claude/skills (v1.18.0). + if (!sub) { const { runSkill } = await import("~/commands/skill.js"); process.exit(await runSkill()); } + else if (sub === "list") { const { runSkillList } = await import("~/commands/platform-actions.js"); process.exit(await runSkillList({ ...f, query: positionals[1] })); } else if (sub === "get") { const { runSkillGet } = await import("~/commands/platform-actions.js"); process.exit(await runSkillGet(positionals[1] ?? "", f)); } else if (sub === "remove") { const { runSkillRemove } = await import("~/commands/platform-actions.js"); process.exit(await runSkillRemove(positionals[1] ?? "", f)); } - else { console.error("Usage: claudemesh skill "); process.exit(EXIT.INVALID_ARGS); } + else { console.error("Usage: claudemesh skill (print bundled SKILL.md)\n claudemesh skill "); process.exit(EXIT.INVALID_ARGS); } break; } case "vault": { diff --git a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx index 8cd028b..1024ee5 100644 --- a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx +++ b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx @@ -14,7 +14,7 @@ import { meshTopicMember, meshTopicMessage, } from "@turbostarter/db/schema/mesh"; -import { aliasedTable, and, count, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm"; +import { aliasedTable, and, count, desc, eq, gt, inArray, isNull, notInArray, or, sql } from "drizzle-orm"; import { appConfig } from "~/config/app"; import { pathsConfig } from "~/config/paths"; @@ -110,7 +110,7 @@ export default async function UniversePage() { and( inArray(meshTopic.meshId, meshIds), isNull(meshTopic.archivedAt), - sql`${meshTopicMessage.senderMemberId} <> ALL(${myMemberIds})`, + notInArray(meshTopicMessage.senderMemberId, myMemberIds), or( isNull(meshTopicMember.lastReadAt), sql`${meshTopicMessage.createdAt} > ${meshTopicMember.lastReadAt}`, diff --git a/apps/web/src/app/[locale]/dashboard/(user)/topics/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/topics/page.tsx index 1e523dc..af33a70 100644 --- a/apps/web/src/app/[locale]/dashboard/(user)/topics/page.tsx +++ b/apps/web/src/app/[locale]/dashboard/(user)/topics/page.tsx @@ -8,7 +8,7 @@ import { meshTopicMember, meshTopicMessage, } from "@turbostarter/db/schema/mesh"; -import { and, asc, count, eq, inArray, isNull, or, sql } from "drizzle-orm"; +import { and, asc, count, eq, inArray, isNull, notInArray, or, sql } from "drizzle-orm"; import { pathsConfig } from "~/config/paths"; import { getSession } from "~/lib/auth/server"; @@ -107,7 +107,7 @@ export default async function WorkspaceTopicsPage() { .where( and( inArray(meshTopicMessage.topicId, topicIds), - sql`${meshTopicMessage.senderMemberId} <> ALL(${myMemberIds})`, + notInArray(meshTopicMessage.senderMemberId, myMemberIds), or( isNull(meshTopicMember.lastReadAt), sql`${meshTopicMessage.createdAt} > ${meshTopicMember.lastReadAt}`, diff --git a/docs/roadmap.md b/docs/roadmap.md index 3ed17a8..4ce5345 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -298,6 +298,15 @@ level, or wire claudemesh to messaging surfaces beyond Claude Code. default returns last 30d). CLI: omitting `--mesh` on each verb routes through the matching aggregator. *Shipped 2026-05-03 in CLI v1.16.0.* +- **v0.5.2 — `claudemesh skill` prints the bundled SKILL.md** — + zero-install access for the protocol reference. SKILL.md is + embedded into the CLI bundle at build time via Bun's + text-import attribute, so `claudemesh skill` works on a + fresh `npm i -g` or the prebuilt binary without any + `~/.claude/skills/` setup. Pipe it: `claudemesh skill | + claude --skill-add -`. Existing `claudemesh skill ` subcommands (mesh-shared skills) preserved. *Shipped + 2026-05-03 in CLI v1.18.0.* - **v0.5.1 — peer list self-marking + send self-DM guard** — `peer list` now tags rows from the caller's own member with `(this session)` or `(your other session)`, so a paste from