feat: implement mesh skills catalog — peers publish and discover reusable instructions

Adds share_skill, get_skill, list_skills, and remove_skill across the full
stack (Drizzle schema, broker CRUD + WS handlers, CLI client methods, MCP
tools). Skills are mesh-scoped, unique by name, and searchable via ILIKE
on name/description/tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-07 23:55:03 +01:00
parent 86a258301f
commit c8cb1e3ea5
4 changed files with 730 additions and 2 deletions

View File

@@ -39,6 +39,7 @@ import {
meshMember as memberTable,
meshMemory,
meshState,
meshSkill,
meshStream,
meshTask,
messageQueue,
@@ -704,6 +705,176 @@ export async function forgetMemory(
);
}
// --- Skills ---
/**
* Upsert a skill in a mesh. If a skill with the same name exists, it is updated.
*/
export async function shareSkill(
meshId: string,
name: string,
description: string,
instructions: string,
tags: string[],
memberId?: string,
memberName?: string,
): Promise<string> {
const existing = await db
.select({ id: meshSkill.id })
.from(meshSkill)
.where(and(eq(meshSkill.meshId, meshId), eq(meshSkill.name, name)))
.limit(1);
if (existing.length > 0) {
await db
.update(meshSkill)
.set({
description,
instructions,
tags,
authorMemberId: memberId ?? null,
authorName: memberName ?? null,
updatedAt: new Date(),
})
.where(eq(meshSkill.id, existing[0]!.id));
return existing[0]!.id;
}
const [row] = await db
.insert(meshSkill)
.values({
meshId,
name,
description,
instructions,
tags,
authorMemberId: memberId ?? null,
authorName: memberName ?? null,
})
.returning({ id: meshSkill.id });
if (!row) throw new Error("failed to insert skill");
return row.id;
}
/**
* Get a skill by name in a mesh.
*/
export async function getSkill(
meshId: string,
name: string,
): Promise<{
name: string;
description: string;
instructions: string;
tags: string[];
author: string;
createdAt: Date;
} | null> {
const rows = await db
.select({
name: meshSkill.name,
description: meshSkill.description,
instructions: meshSkill.instructions,
tags: meshSkill.tags,
authorName: meshSkill.authorName,
createdAt: meshSkill.createdAt,
})
.from(meshSkill)
.where(and(eq(meshSkill.meshId, meshId), eq(meshSkill.name, name)))
.limit(1);
if (rows.length === 0) return null;
const r = rows[0]!;
return {
name: r.name,
description: r.description,
instructions: r.instructions,
tags: r.tags ?? [],
author: r.authorName ?? "unknown",
createdAt: r.createdAt,
};
}
/**
* List skills in a mesh, optionally filtering by keyword across name, description, and tags.
*/
export async function listSkills(
meshId: string,
query?: string,
): Promise<
Array<{
name: string;
description: string;
tags: string[];
author: string;
createdAt: Date;
}>
> {
if (query) {
const pattern = `%${query}%`;
const rows = await db
.select({
name: meshSkill.name,
description: meshSkill.description,
tags: meshSkill.tags,
authorName: meshSkill.authorName,
createdAt: meshSkill.createdAt,
})
.from(meshSkill)
.where(
and(
eq(meshSkill.meshId, meshId),
or(
sql`${meshSkill.name} ILIKE ${pattern}`,
sql`${meshSkill.description} ILIKE ${pattern}`,
sql`EXISTS (SELECT 1 FROM unnest(${meshSkill.tags}) AS t WHERE t ILIKE ${pattern})`,
),
),
)
.orderBy(asc(meshSkill.name));
return rows.map((r) => ({
name: r.name,
description: r.description,
tags: r.tags ?? [],
author: r.authorName ?? "unknown",
createdAt: r.createdAt,
}));
}
const rows = await db
.select({
name: meshSkill.name,
description: meshSkill.description,
tags: meshSkill.tags,
authorName: meshSkill.authorName,
createdAt: meshSkill.createdAt,
})
.from(meshSkill)
.where(eq(meshSkill.meshId, meshId))
.orderBy(asc(meshSkill.name));
return rows.map((r) => ({
name: r.name,
description: r.description,
tags: r.tags ?? [],
author: r.authorName ?? "unknown",
createdAt: r.createdAt,
}));
}
/**
* Remove a skill by name in a mesh. Returns true if a row was deleted.
*/
export async function removeSkill(
meshId: string,
name: string,
): Promise<boolean> {
const result = await db
.delete(meshSkill)
.where(and(eq(meshSkill.meshId, meshId), eq(meshSkill.name, name)))
.returning({ id: meshSkill.id });
return result.length > 0;
}
// --- File sharing ---
/**