mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
Implement skills store API foundation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
69a368ed51
commit
db599edadf
23 files changed
+2145
-53
No files matched your search
@@ -133,12 +133,19 @@ export interface PaperclipSkillEntry {
|
||||
key: string;
|
||||
runtimeName: string;
|
||||
source: string;
|
||||
versionId?: string | null;
|
||||
currentVersionId?: string | null;
|
||||
sourceStatus?: "available" | "missing";
|
||||
missingDetail?: string | null;
|
||||
required?: boolean;
|
||||
requiredReason?: string | null;
|
||||
}
|
||||
|
||||
export interface PaperclipDesiredSkillEntry {
|
||||
key: string;
|
||||
versionId: string | null;
|
||||
}
|
||||
|
||||
export interface InstalledSkillTarget {
|
||||
targetPath: string | null;
|
||||
kind: "symlink" | "directory" | "file";
|
||||
@@ -1446,6 +1453,8 @@ export function buildRuntimeMountedSkillSnapshot(
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
versionId: available.versionId ?? null,
|
||||
currentVersionId: available.currentVersionId ?? null,
|
||||
desired,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
@@ -1463,6 +1472,8 @@ export function buildRuntimeMountedSkillSnapshot(
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
versionId: available.versionId ?? null,
|
||||
currentVersionId: available.currentVersionId ?? null,
|
||||
desired,
|
||||
managed: true,
|
||||
state: configured ? "configured" : "available",
|
||||
@@ -1528,6 +1539,10 @@ export function buildRuntimeMountedSkillSnapshot(
|
||||
supported,
|
||||
mode,
|
||||
desiredSkills,
|
||||
desiredSkillEntries: desiredSkills.map((key) => ({
|
||||
key,
|
||||
versionId: availableByKey.get(key)?.versionId ?? null,
|
||||
})),
|
||||
entries,
|
||||
warnings,
|
||||
};
|
||||
@@ -1560,6 +1575,8 @@ export function buildPersistentSkillSnapshot(
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
versionId: available.versionId ?? null,
|
||||
currentVersionId: available.currentVersionId ?? null,
|
||||
desired,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
@@ -1595,6 +1612,8 @@ export function buildPersistentSkillSnapshot(
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
versionId: available.versionId ?? null,
|
||||
currentVersionId: available.currentVersionId ?? null,
|
||||
desired,
|
||||
managed,
|
||||
state,
|
||||
@@ -1650,6 +1669,10 @@ export function buildPersistentSkillSnapshot(
|
||||
supported: true,
|
||||
mode: "persistent",
|
||||
desiredSkills,
|
||||
desiredSkillEntries: desiredSkills.map((key) => ({
|
||||
key,
|
||||
versionId: availableByKey.get(key)?.versionId ?? null,
|
||||
})),
|
||||
entries,
|
||||
warnings,
|
||||
};
|
||||
@@ -1668,6 +1691,14 @@ function normalizeConfiguredPaperclipRuntimeSkills(value: unknown): PaperclipSki
|
||||
key,
|
||||
runtimeName,
|
||||
source,
|
||||
versionId:
|
||||
typeof entry.versionId === "string" && entry.versionId.trim().length > 0
|
||||
? entry.versionId.trim()
|
||||
: null,
|
||||
currentVersionId:
|
||||
typeof entry.currentVersionId === "string" && entry.currentVersionId.trim().length > 0
|
||||
? entry.currentVersionId.trim()
|
||||
: null,
|
||||
sourceStatus: entry.sourceStatus === "missing" ? "missing" : "available",
|
||||
missingDetail:
|
||||
typeof entry.missingDetail === "string" && entry.missingDetail.trim().length > 0
|
||||
@@ -1714,22 +1745,41 @@ export async function readPaperclipSkillMarkdown(
|
||||
export function readPaperclipSkillSyncPreference(config: Record<string, unknown>): {
|
||||
explicit: boolean;
|
||||
desiredSkills: string[];
|
||||
desiredSkillEntries: PaperclipDesiredSkillEntry[];
|
||||
} {
|
||||
const raw = config.paperclipSkillSync;
|
||||
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
||||
return { explicit: false, desiredSkills: [] };
|
||||
return { explicit: false, desiredSkills: [], desiredSkillEntries: [] };
|
||||
}
|
||||
const syncConfig = raw as Record<string, unknown>;
|
||||
const desiredValues = syncConfig.desiredSkills;
|
||||
const desired = Array.isArray(desiredValues)
|
||||
? desiredValues
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
? desiredValues.flatMap((value): PaperclipDesiredSkillEntry[] => {
|
||||
if (typeof value === "string") {
|
||||
const key = value.trim();
|
||||
return key ? [{ key, versionId: null }] : [];
|
||||
}
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
const record = value as Record<string, unknown>;
|
||||
const key = typeof record.key === "string" ? record.key.trim() : "";
|
||||
if (!key) return [];
|
||||
const versionId = typeof record.versionId === "string" && record.versionId.trim()
|
||||
? record.versionId.trim()
|
||||
: null;
|
||||
return [{ key, versionId }];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
const byKey = new Map<string, PaperclipDesiredSkillEntry>();
|
||||
for (const entry of desired) {
|
||||
if (!byKey.has(entry.key)) byKey.set(entry.key, entry);
|
||||
}
|
||||
const desiredSkillEntries = Array.from(byKey.values());
|
||||
return {
|
||||
explicit: Object.prototype.hasOwnProperty.call(raw, "desiredSkills"),
|
||||
desiredSkills: Array.from(new Set(desired)),
|
||||
desiredSkills: desiredSkillEntries.map((entry) => entry.key),
|
||||
desiredSkillEntries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1775,7 +1825,7 @@ export function resolvePaperclipDesiredSkillNames(
|
||||
|
||||
export function writePaperclipSkillSyncPreference(
|
||||
config: Record<string, unknown>,
|
||||
desiredSkills: string[],
|
||||
desiredSkills: Array<string | PaperclipDesiredSkillEntry>,
|
||||
): Record<string, unknown> {
|
||||
const next = { ...config };
|
||||
const raw = next.paperclipSkillSync;
|
||||
@@ -1783,13 +1833,23 @@ export function writePaperclipSkillSyncPreference(
|
||||
typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
||||
? { ...(raw as Record<string, unknown>) }
|
||||
: {};
|
||||
current.desiredSkills = Array.from(
|
||||
new Set(
|
||||
desiredSkills
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
const entries = desiredSkills.flatMap((value): PaperclipDesiredSkillEntry[] => {
|
||||
if (typeof value === "string") {
|
||||
const key = value.trim();
|
||||
return key ? [{ key, versionId: null }] : [];
|
||||
}
|
||||
const key = value.key.trim();
|
||||
if (!key) return [];
|
||||
return [{ key, versionId: value.versionId ?? null }];
|
||||
});
|
||||
const byKey = new Map<string, PaperclipDesiredSkillEntry>();
|
||||
for (const entry of entries) {
|
||||
if (!byKey.has(entry.key)) byKey.set(entry.key, entry);
|
||||
}
|
||||
const normalized = Array.from(byKey.values());
|
||||
current.desiredSkills = normalized.some((entry) => entry.versionId)
|
||||
? normalized
|
||||
: normalized.map((entry) => entry.key);
|
||||
next.paperclipSkillSync = current;
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -193,6 +193,8 @@ export type AdapterSkillOrigin =
|
||||
export interface AdapterSkillEntry {
|
||||
key: string;
|
||||
runtimeName: string | null;
|
||||
versionId?: string | null;
|
||||
currentVersionId?: string | null;
|
||||
desired: boolean;
|
||||
managed: boolean;
|
||||
required?: boolean;
|
||||
@@ -212,6 +214,7 @@ export interface AdapterSkillSnapshot {
|
||||
supported: boolean;
|
||||
mode: AdapterSkillSyncMode;
|
||||
desiredSkills: string[];
|
||||
desiredSkillEntries?: Array<{ key: string; versionId: string | null }>;
|
||||
entries: AdapterSkillEntry[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "icon_url" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "color" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "tagline" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "author_name" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "homepage_url" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "categories" text[] NOT NULL DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "sharing_scope" text NOT NULL DEFAULT 'company';--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "public_share_token" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "forked_from_skill_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "forked_from_company_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "star_count" integer NOT NULL DEFAULT 0;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "install_count" integer NOT NULL DEFAULT 0;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "fork_count" integer NOT NULL DEFAULT 0;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "current_version_id" uuid;--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "company_skill_versions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"company_skill_id" uuid NOT NULL,
|
||||
"revision_number" integer NOT NULL,
|
||||
"label" text,
|
||||
"file_inventory" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"author_agent_id" uuid,
|
||||
"author_user_id" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "company_skill_stars" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"company_skill_id" uuid NOT NULL,
|
||||
"agent_id" uuid,
|
||||
"user_id" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "company_skill_comments" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"company_skill_id" uuid NOT NULL,
|
||||
"parent_comment_id" uuid,
|
||||
"author_agent_id" uuid,
|
||||
"author_user_id" text,
|
||||
"body" text NOT NULL,
|
||||
"deleted_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skills" ADD CONSTRAINT "company_skills_forked_from_skill_id_company_skills_id_fk" FOREIGN KEY ("forked_from_skill_id") REFERENCES "public"."company_skills"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skills" ADD CONSTRAINT "company_skills_forked_from_company_id_companies_id_fk" FOREIGN KEY ("forked_from_company_id") REFERENCES "public"."companies"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_versions" ADD CONSTRAINT "company_skill_versions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_versions" ADD CONSTRAINT "company_skill_versions_company_skill_id_company_skills_id_fk" FOREIGN KEY ("company_skill_id") REFERENCES "public"."company_skills"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_versions" ADD CONSTRAINT "company_skill_versions_author_agent_id_agents_id_fk" FOREIGN KEY ("author_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skills" ADD CONSTRAINT "company_skills_current_version_id_company_skill_versions_id_fk" FOREIGN KEY ("current_version_id") REFERENCES "public"."company_skill_versions"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_stars" ADD CONSTRAINT "company_skill_stars_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_stars" ADD CONSTRAINT "company_skill_stars_company_skill_id_company_skills_id_fk" FOREIGN KEY ("company_skill_id") REFERENCES "public"."company_skills"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_stars" ADD CONSTRAINT "company_skill_stars_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_comments" ADD CONSTRAINT "company_skill_comments_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_comments" ADD CONSTRAINT "company_skill_comments_company_skill_id_company_skills_id_fk" FOREIGN KEY ("company_skill_id") REFERENCES "public"."company_skills"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_comments" ADD CONSTRAINT "company_skill_comments_parent_comment_id_company_skill_comments_id_fk" FOREIGN KEY ("parent_comment_id") REFERENCES "public"."company_skill_comments"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "company_skill_comments" ADD CONSTRAINT "company_skill_comments_author_agent_id_agents_id_fk" FOREIGN KEY ("author_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skills_company_categories_idx" ON "company_skills" USING gin ("categories");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skills_company_sharing_scope_idx" ON "company_skills" USING btree ("company_id","sharing_scope");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skills_company_current_version_idx" ON "company_skills" USING btree ("company_id","current_version_id");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skills_company_forked_from_idx" ON "company_skills" USING btree ("company_id","forked_from_skill_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_versions_skill_revision_idx" ON "company_skill_versions" USING btree ("company_skill_id","revision_number");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skill_versions_company_skill_created_idx" ON "company_skill_versions" USING btree ("company_id","company_skill_id","created_at");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_stars_skill_agent_idx" ON "company_skill_stars" USING btree ("company_skill_id","agent_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_stars_skill_user_idx" ON "company_skill_stars" USING btree ("company_skill_id","user_id");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skill_stars_company_skill_created_idx" ON "company_skill_stars" USING btree ("company_id","company_skill_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skill_comments_company_skill_created_idx" ON "company_skill_comments" USING btree ("company_id","company_skill_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skill_comments_parent_idx" ON "company_skill_comments" USING btree ("parent_comment_id");
|
||||
@@ -694,6 +694,13 @@
|
||||
"when": 1780534200000,
|
||||
"tag": "0098_project_icon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 99,
|
||||
"version": "7",
|
||||
"when": 1781130000000,
|
||||
"tag": "0099_skills_store_foundation",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
jsonb,
|
||||
index,
|
||||
integer,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { CompanySkillFileInventoryEntry, CompanySkillSharingScope } from "@paperclipai/shared";
|
||||
import { agents } from "./agents.js";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const companySkills = pgTable(
|
||||
@@ -25,6 +29,20 @@ export const companySkills = pgTable(
|
||||
trustLevel: text("trust_level").notNull().default("markdown_only"),
|
||||
compatibility: text("compatibility").notNull().default("compatible"),
|
||||
fileInventory: jsonb("file_inventory").$type<Array<Record<string, unknown>>>().notNull().default([]),
|
||||
iconUrl: text("icon_url"),
|
||||
color: text("color"),
|
||||
tagline: text("tagline"),
|
||||
authorName: text("author_name"),
|
||||
homepageUrl: text("homepage_url"),
|
||||
categories: text("categories").array().notNull().default([]),
|
||||
sharingScope: text("sharing_scope").$type<CompanySkillSharingScope>().notNull().default("company"),
|
||||
publicShareToken: text("public_share_token"),
|
||||
forkedFromSkillId: uuid("forked_from_skill_id").references((): AnyPgColumn => companySkills.id, { onDelete: "set null" }),
|
||||
forkedFromCompanyId: uuid("forked_from_company_id").references(() => companies.id, { onDelete: "set null" }),
|
||||
starCount: integer("star_count").notNull().default(0),
|
||||
installCount: integer("install_count").notNull().default(0),
|
||||
forkCount: integer("fork_count").notNull().default(0),
|
||||
currentVersionId: uuid("current_version_id").references((): AnyPgColumn => companySkillVersions.id, { onDelete: "set null" }),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
@@ -32,5 +50,84 @@ export const companySkills = pgTable(
|
||||
(table) => ({
|
||||
companyKeyUniqueIdx: uniqueIndex("company_skills_company_key_idx").on(table.companyId, table.key),
|
||||
companyNameIdx: index("company_skills_company_name_idx").on(table.companyId, table.name),
|
||||
companyCategoriesIdx: index("company_skills_company_categories_idx").using("gin", table.categories),
|
||||
companySharingScopeIdx: index("company_skills_company_sharing_scope_idx").on(table.companyId, table.sharingScope),
|
||||
companyCurrentVersionIdx: index("company_skills_company_current_version_idx").on(table.companyId, table.currentVersionId),
|
||||
companyForkedFromIdx: index("company_skills_company_forked_from_idx").on(table.companyId, table.forkedFromSkillId),
|
||||
}),
|
||||
);
|
||||
|
||||
export type CompanySkillVersionFileInventoryEntry = CompanySkillFileInventoryEntry & {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const companySkillVersions = pgTable(
|
||||
"company_skill_versions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
companySkillId: uuid("company_skill_id").notNull().references(() => companySkills.id, { onDelete: "cascade" }),
|
||||
revisionNumber: integer("revision_number").notNull(),
|
||||
label: text("label"),
|
||||
fileInventory: jsonb("file_inventory").$type<CompanySkillVersionFileInventoryEntry[]>().notNull().default([]),
|
||||
authorAgentId: uuid("author_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
authorUserId: text("author_user_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companySkillRevisionUniqueIdx: uniqueIndex("company_skill_versions_skill_revision_idx").on(
|
||||
table.companySkillId,
|
||||
table.revisionNumber,
|
||||
),
|
||||
companySkillCreatedIdx: index("company_skill_versions_company_skill_created_idx").on(
|
||||
table.companyId,
|
||||
table.companySkillId,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const companySkillStars = pgTable(
|
||||
"company_skill_stars",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
companySkillId: uuid("company_skill_id").notNull().references(() => companySkills.id, { onDelete: "cascade" }),
|
||||
agentId: uuid("agent_id").references(() => agents.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companySkillAgentUniqueIdx: uniqueIndex("company_skill_stars_skill_agent_idx").on(table.companySkillId, table.agentId),
|
||||
companySkillUserUniqueIdx: uniqueIndex("company_skill_stars_skill_user_idx").on(table.companySkillId, table.userId),
|
||||
companySkillCreatedIdx: index("company_skill_stars_company_skill_created_idx").on(
|
||||
table.companyId,
|
||||
table.companySkillId,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const companySkillComments = pgTable(
|
||||
"company_skill_comments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
companySkillId: uuid("company_skill_id").notNull().references(() => companySkills.id, { onDelete: "cascade" }),
|
||||
parentCommentId: uuid("parent_comment_id").references((): AnyPgColumn => companySkillComments.id, { onDelete: "set null" }),
|
||||
authorAgentId: uuid("author_agent_id").references(() => agents.id, { onDelete: "set null" }),
|
||||
authorUserId: text("author_user_id"),
|
||||
body: text("body").notNull(),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companySkillCreatedIdx: index("company_skill_comments_company_skill_created_idx").on(
|
||||
table.companyId,
|
||||
table.companySkillId,
|
||||
table.createdAt,
|
||||
),
|
||||
parentIdx: index("company_skill_comments_parent_idx").on(table.parentCommentId),
|
||||
}),
|
||||
);
|
||||
@@ -72,7 +72,7 @@ export { companySecrets } from "./company_secrets.js";
|
||||
export { companySecretVersions } from "./company_secret_versions.js";
|
||||
export { companySecretBindings } from "./company_secret_bindings.js";
|
||||
export { secretAccessEvents } from "./secret_access_events.js";
|
||||
export { companySkills } from "./company_skills.js";
|
||||
export { companySkills, companySkillVersions, companySkillStars, companySkillComments } from "./company_skills.js";
|
||||
export { plugins } from "./plugins.js";
|
||||
export { pluginConfig } from "./plugin_config.js";
|
||||
export { pluginCompanySettings } from "./plugin_company_settings.js";
|
||||
|
||||
@@ -1347,6 +1347,20 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
iconUrl: null,
|
||||
color: null,
|
||||
tagline: declaration.description?.slice(0, 120) ?? null,
|
||||
authorName: null,
|
||||
homepageUrl: null,
|
||||
categories: [],
|
||||
sharingScope: "company",
|
||||
publicShareToken: null,
|
||||
forkedFromSkillId: null,
|
||||
forkedFromCompanyId: null,
|
||||
starCount: 0,
|
||||
installCount: 1,
|
||||
forkCount: 0,
|
||||
currentVersionId: null,
|
||||
metadata: {
|
||||
sourceKind: "catalog",
|
||||
pluginManagedResource: {
|
||||
@@ -1403,6 +1417,20 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
iconUrl: null,
|
||||
color: null,
|
||||
tagline: declaration.description?.slice(0, 120) ?? null,
|
||||
authorName: null,
|
||||
homepageUrl: null,
|
||||
categories: [],
|
||||
sharingScope: "company",
|
||||
publicShareToken: null,
|
||||
forkedFromSkillId: null,
|
||||
forkedFromCompanyId: null,
|
||||
starCount: existing.skill?.starCount ?? 0,
|
||||
installCount: existing.skill?.installCount ?? 1,
|
||||
forkCount: existing.skill?.forkCount ?? 0,
|
||||
currentVersionId: existing.skill?.currentVersionId ?? null,
|
||||
metadata: {
|
||||
sourceKind: "catalog",
|
||||
pluginManagedResource: {
|
||||
|
||||
@@ -334,11 +334,23 @@ export type {
|
||||
CompanySkillTrustLevel,
|
||||
CompanySkillCompatibility,
|
||||
CompanySkillSourceBadge,
|
||||
CompanySkillSharingScope,
|
||||
CompanySkillListSort,
|
||||
CompanySkillFileInventoryEntry,
|
||||
CompanySkillVersionFileInventoryEntry,
|
||||
CompanySkill,
|
||||
CompanySkillListItem,
|
||||
CompanySkillUsageAgent,
|
||||
CompanySkillDetail,
|
||||
CompanySkillListQuery,
|
||||
CompanySkillCategoryCount,
|
||||
CompanySkillVersion,
|
||||
CompanySkillVersionCreateRequest,
|
||||
CompanySkillStarResult,
|
||||
CompanySkillComment,
|
||||
CompanySkillCommentCreateRequest,
|
||||
CompanySkillCommentUpdateRequest,
|
||||
CompanySkillForkRequest,
|
||||
CompanySkillUpdateStatus,
|
||||
CompanySkillAuditSeverity,
|
||||
CompanySkillAuditVerdict,
|
||||
@@ -390,6 +402,7 @@ export type {
|
||||
AgentSkillSyncMode,
|
||||
AgentSkillState,
|
||||
AgentSkillOrigin,
|
||||
AgentDesiredSkillEntry,
|
||||
AgentSkillEntry,
|
||||
AgentSkillSnapshot,
|
||||
AgentSkillSyncRequest,
|
||||
@@ -918,6 +931,8 @@ export {
|
||||
type ProbeEnvironmentConfig,
|
||||
agentSkillStateSchema,
|
||||
agentSkillSyncModeSchema,
|
||||
agentDesiredSkillEntrySchema,
|
||||
agentDesiredSkillSelectionSchema,
|
||||
agentSkillEntrySchema,
|
||||
agentSkillSnapshotSchema,
|
||||
agentSkillSyncSchema,
|
||||
@@ -1212,11 +1227,23 @@ export {
|
||||
companySkillTrustLevelSchema,
|
||||
companySkillCompatibilitySchema,
|
||||
companySkillSourceBadgeSchema,
|
||||
companySkillSharingScopeSchema,
|
||||
companySkillListSortSchema,
|
||||
companySkillFileInventoryEntrySchema,
|
||||
companySkillVersionFileInventoryEntrySchema,
|
||||
companySkillSchema,
|
||||
companySkillListItemSchema,
|
||||
companySkillUsageAgentSchema,
|
||||
companySkillListQuerySchema,
|
||||
companySkillCategoryCountSchema,
|
||||
companySkillVersionSchema,
|
||||
companySkillDetailSchema,
|
||||
companySkillVersionCreateSchema,
|
||||
companySkillStarResultSchema,
|
||||
companySkillCommentSchema,
|
||||
companySkillCommentCreateSchema,
|
||||
companySkillCommentUpdateSchema,
|
||||
companySkillForkSchema,
|
||||
companySkillUpdateStatusSchema,
|
||||
companySkillAuditFindingSchema,
|
||||
companySkillAuditResultSchema,
|
||||
|
||||
@@ -14,9 +14,16 @@ export type AgentSkillOrigin =
|
||||
| "user_installed"
|
||||
| "external_unknown";
|
||||
|
||||
export interface AgentDesiredSkillEntry {
|
||||
key: string;
|
||||
versionId: string | null;
|
||||
}
|
||||
|
||||
export interface AgentSkillEntry {
|
||||
key: string;
|
||||
runtimeName: string | null;
|
||||
versionId?: string | null;
|
||||
currentVersionId?: string | null;
|
||||
desired: boolean;
|
||||
managed: boolean;
|
||||
required?: boolean;
|
||||
@@ -36,10 +43,11 @@ export interface AgentSkillSnapshot {
|
||||
supported: boolean;
|
||||
mode: AgentSkillSyncMode;
|
||||
desiredSkills: string[];
|
||||
desiredSkillEntries?: AgentDesiredSkillEntry[];
|
||||
entries: AgentSkillEntry[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface AgentSkillSyncRequest {
|
||||
desiredSkills: string[];
|
||||
desiredSkills: Array<string | AgentDesiredSkillEntry>;
|
||||
}
|
||||
@@ -6,11 +6,19 @@ export type CompanySkillCompatibility = "compatible" | "unknown" | "invalid";
|
||||
|
||||
export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog" | "skills_sh";
|
||||
|
||||
export type CompanySkillSharingScope = "private" | "company" | "public_link";
|
||||
|
||||
export type CompanySkillListSort = "alphabetical" | "recent" | "installs" | "stars" | "agents" | "forks";
|
||||
|
||||
export interface CompanySkillFileInventoryEntry {
|
||||
path: string;
|
||||
kind: "skill" | "markdown" | "reference" | "script" | "asset" | "other";
|
||||
}
|
||||
|
||||
export interface CompanySkillVersionFileInventoryEntry extends CompanySkillFileInventoryEntry {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface CompanySkill {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -25,6 +33,20 @@ export interface CompanySkill {
|
||||
trustLevel: CompanySkillTrustLevel;
|
||||
compatibility: CompanySkillCompatibility;
|
||||
fileInventory: CompanySkillFileInventoryEntry[];
|
||||
iconUrl: string | null;
|
||||
color: string | null;
|
||||
tagline: string | null;
|
||||
authorName: string | null;
|
||||
homepageUrl: string | null;
|
||||
categories: string[];
|
||||
sharingScope: CompanySkillSharingScope;
|
||||
publicShareToken: string | null;
|
||||
forkedFromSkillId: string | null;
|
||||
forkedFromCompanyId: string | null;
|
||||
starCount: number;
|
||||
installCount: number;
|
||||
forkCount: number;
|
||||
currentVersionId: string | null;
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -43,6 +65,20 @@ export interface CompanySkillListItem {
|
||||
trustLevel: CompanySkillTrustLevel;
|
||||
compatibility: CompanySkillCompatibility;
|
||||
fileInventory: CompanySkillFileInventoryEntry[];
|
||||
iconUrl: string | null;
|
||||
color: string | null;
|
||||
tagline: string | null;
|
||||
authorName: string | null;
|
||||
homepageUrl: string | null;
|
||||
categories: string[];
|
||||
sharingScope: CompanySkillSharingScope;
|
||||
publicShareToken: string | null;
|
||||
forkedFromSkillId: string | null;
|
||||
forkedFromCompanyId: string | null;
|
||||
starCount: number;
|
||||
installCount: number;
|
||||
forkCount: number;
|
||||
currentVersionId: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
attachedAgentCount: number;
|
||||
@@ -69,6 +105,7 @@ export interface CompanySkillUsageAgent {
|
||||
* agent runtimes while loading operator-facing skill metadata.
|
||||
*/
|
||||
actualState: string | null;
|
||||
versionId: string | null;
|
||||
}
|
||||
|
||||
export interface CompanySkillDetail extends CompanySkill {
|
||||
@@ -79,6 +116,70 @@ export interface CompanySkillDetail extends CompanySkill {
|
||||
sourceLabel: string | null;
|
||||
sourceBadge: CompanySkillSourceBadge;
|
||||
sourcePath: string | null;
|
||||
currentVersion: CompanySkillVersion | null;
|
||||
starredByCurrentActor: boolean;
|
||||
}
|
||||
|
||||
export interface CompanySkillListQuery {
|
||||
q?: string;
|
||||
sort?: CompanySkillListSort;
|
||||
categories?: string[];
|
||||
scope?: CompanySkillSharingScope;
|
||||
}
|
||||
|
||||
export interface CompanySkillCategoryCount {
|
||||
slug: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CompanySkillVersion {
|
||||
id: string;
|
||||
companyId: string;
|
||||
companySkillId: string;
|
||||
revisionNumber: number;
|
||||
label: string | null;
|
||||
fileInventory: CompanySkillVersionFileInventoryEntry[];
|
||||
authorAgentId: string | null;
|
||||
authorUserId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface CompanySkillVersionCreateRequest {
|
||||
label?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanySkillStarResult {
|
||||
skillId: string;
|
||||
starred: boolean;
|
||||
starCount: number;
|
||||
}
|
||||
|
||||
export interface CompanySkillComment {
|
||||
id: string;
|
||||
companyId: string;
|
||||
companySkillId: string;
|
||||
parentCommentId: string | null;
|
||||
authorAgentId: string | null;
|
||||
authorUserId: string | null;
|
||||
body: string;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CompanySkillCommentCreateRequest {
|
||||
body: string;
|
||||
parentCommentId?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanySkillCommentUpdateRequest {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface CompanySkillForkRequest {
|
||||
name?: string | null;
|
||||
slug?: string | null;
|
||||
sharingScope?: CompanySkillSharingScope;
|
||||
}
|
||||
|
||||
export interface CompanySkillUpdateStatus {
|
||||
@@ -186,6 +287,13 @@ export interface CompanySkillCreateRequest {
|
||||
slug?: string | null;
|
||||
description?: string | null;
|
||||
markdown?: string | null;
|
||||
iconUrl?: string | null;
|
||||
color?: string | null;
|
||||
tagline?: string | null;
|
||||
authorName?: string | null;
|
||||
homepageUrl?: string | null;
|
||||
categories?: string[];
|
||||
sharingScope?: CompanySkillSharingScope;
|
||||
}
|
||||
|
||||
export interface CompanySkillFileDetail {
|
||||
|
||||
@@ -60,11 +60,23 @@ export type {
|
||||
CompanySkillTrustLevel,
|
||||
CompanySkillCompatibility,
|
||||
CompanySkillSourceBadge,
|
||||
CompanySkillSharingScope,
|
||||
CompanySkillListSort,
|
||||
CompanySkillFileInventoryEntry,
|
||||
CompanySkillVersionFileInventoryEntry,
|
||||
CompanySkill,
|
||||
CompanySkillListItem,
|
||||
CompanySkillUsageAgent,
|
||||
CompanySkillDetail,
|
||||
CompanySkillListQuery,
|
||||
CompanySkillCategoryCount,
|
||||
CompanySkillVersion,
|
||||
CompanySkillVersionCreateRequest,
|
||||
CompanySkillStarResult,
|
||||
CompanySkillComment,
|
||||
CompanySkillCommentCreateRequest,
|
||||
CompanySkillCommentUpdateRequest,
|
||||
CompanySkillForkRequest,
|
||||
CompanySkillUpdateStatus,
|
||||
CompanySkillAuditSeverity,
|
||||
CompanySkillAuditVerdict,
|
||||
@@ -120,6 +132,7 @@ export type {
|
||||
AgentSkillSyncMode,
|
||||
AgentSkillState,
|
||||
AgentSkillOrigin,
|
||||
AgentDesiredSkillEntry,
|
||||
AgentSkillEntry,
|
||||
AgentSkillSnapshot,
|
||||
AgentSkillSyncRequest,
|
||||
|
||||
@@ -22,9 +22,21 @@ export const agentSkillSyncModeSchema = z.enum([
|
||||
"ephemeral",
|
||||
]);
|
||||
|
||||
export const agentDesiredSkillEntrySchema = z.object({
|
||||
key: z.string().min(1),
|
||||
versionId: z.string().uuid().nullable(),
|
||||
});
|
||||
|
||||
export const agentDesiredSkillSelectionSchema = z.union([
|
||||
z.string().min(1),
|
||||
agentDesiredSkillEntrySchema,
|
||||
]);
|
||||
|
||||
export const agentSkillEntrySchema = z.object({
|
||||
key: z.string().min(1),
|
||||
runtimeName: z.string().min(1).nullable(),
|
||||
versionId: z.string().uuid().nullable().optional(),
|
||||
currentVersionId: z.string().uuid().nullable().optional(),
|
||||
desired: z.boolean(),
|
||||
managed: z.boolean(),
|
||||
required: z.boolean().optional(),
|
||||
@@ -44,12 +56,13 @@ export const agentSkillSnapshotSchema = z.object({
|
||||
supported: z.boolean(),
|
||||
mode: agentSkillSyncModeSchema,
|
||||
desiredSkills: z.array(z.string().min(1)),
|
||||
desiredSkillEntries: z.array(agentDesiredSkillEntrySchema).optional(),
|
||||
entries: z.array(agentSkillEntrySchema),
|
||||
warnings: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const agentSkillSyncSchema = z.object({
|
||||
desiredSkills: z.array(z.string().min(1)),
|
||||
desiredSkills: z.array(agentDesiredSkillSelectionSchema),
|
||||
});
|
||||
|
||||
export type AgentSkillSync = z.infer<typeof agentSkillSyncSchema>;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { agentAdapterTypeSchema } from "../adapter-type.js";
|
||||
import { envConfigSchema } from "./secret.js";
|
||||
import { trustAuthorizationPolicySchema, trustPresetSchema } from "./trust-policy.js";
|
||||
import { agentDesiredSkillSelectionSchema } from "./adapter-skills.js";
|
||||
|
||||
export const agentPermissionsSchema = z.object({
|
||||
canCreateAgents: z.boolean().optional().default(false),
|
||||
@@ -73,7 +74,7 @@ export const createAgentSchema = z.object({
|
||||
icon: z.enum(AGENT_ICON_NAMES).optional().nullable(),
|
||||
reportsTo: z.string().uuid().optional().nullable(),
|
||||
capabilities: z.string().optional().nullable(),
|
||||
desiredSkills: z.array(z.string().min(1)).optional(),
|
||||
desiredSkills: z.array(agentDesiredSkillSelectionSchema).optional(),
|
||||
adapterType: agentAdapterTypeSchema,
|
||||
adapterConfig: adapterConfigSchema.optional().default({}),
|
||||
instructionsBundle: createAgentInstructionsBundleSchema.optional(),
|
||||
|
||||
@@ -4,12 +4,18 @@ export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url
|
||||
export const companySkillTrustLevelSchema = z.enum(["markdown_only", "assets", "scripts_executables"]);
|
||||
export const companySkillCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]);
|
||||
export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog", "skills_sh"]);
|
||||
export const companySkillSharingScopeSchema = z.enum(["private", "company", "public_link"]);
|
||||
export const companySkillListSortSchema = z.enum(["alphabetical", "recent", "installs", "stars", "agents", "forks"]);
|
||||
|
||||
export const companySkillFileInventoryEntrySchema = z.object({
|
||||
path: z.string().min(1),
|
||||
kind: z.enum(["skill", "markdown", "reference", "script", "asset", "other"]),
|
||||
});
|
||||
|
||||
export const companySkillVersionFileInventoryEntrySchema = companySkillFileInventoryEntrySchema.extend({
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export const companySkillSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
companyId: z.string().uuid(),
|
||||
@@ -24,6 +30,20 @@ export const companySkillSchema = z.object({
|
||||
trustLevel: companySkillTrustLevelSchema,
|
||||
compatibility: companySkillCompatibilitySchema,
|
||||
fileInventory: z.array(companySkillFileInventoryEntrySchema).default([]),
|
||||
iconUrl: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
tagline: z.string().nullable(),
|
||||
authorName: z.string().nullable(),
|
||||
homepageUrl: z.string().nullable(),
|
||||
categories: z.array(z.string().min(1)).default([]),
|
||||
sharingScope: companySkillSharingScopeSchema,
|
||||
publicShareToken: z.string().nullable(),
|
||||
forkedFromSkillId: z.string().uuid().nullable(),
|
||||
forkedFromCompanyId: z.string().uuid().nullable(),
|
||||
starCount: z.number().int().nonnegative(),
|
||||
installCount: z.number().int().nonnegative(),
|
||||
forkCount: z.number().int().nonnegative(),
|
||||
currentVersionId: z.string().uuid().nullable(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
@@ -50,6 +70,31 @@ export const companySkillUsageAgentSchema = z.object({
|
||||
actualState: z.string().nullable().describe(
|
||||
"Runtime adapter skill state when explicitly fetched; company skill detail reads return null without probing agent runtimes.",
|
||||
),
|
||||
versionId: z.string().uuid().nullable(),
|
||||
});
|
||||
|
||||
export const companySkillListQuerySchema = z.object({
|
||||
q: z.string().min(1).optional(),
|
||||
sort: companySkillListSortSchema.optional(),
|
||||
categories: z.array(z.string().min(1)).optional(),
|
||||
scope: companySkillSharingScopeSchema.optional(),
|
||||
});
|
||||
|
||||
export const companySkillCategoryCountSchema = z.object({
|
||||
slug: z.string().min(1),
|
||||
count: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const companySkillVersionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
companyId: z.string().uuid(),
|
||||
companySkillId: z.string().uuid(),
|
||||
revisionNumber: z.number().int().positive(),
|
||||
label: z.string().nullable(),
|
||||
fileInventory: z.array(companySkillVersionFileInventoryEntrySchema).default([]),
|
||||
authorAgentId: z.string().uuid().nullable(),
|
||||
authorUserId: z.string().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const companySkillDetailSchema = companySkillSchema.extend({
|
||||
@@ -59,8 +104,48 @@ export const companySkillDetailSchema = companySkillSchema.extend({
|
||||
editableReason: z.string().nullable(),
|
||||
sourceLabel: z.string().nullable(),
|
||||
sourceBadge: companySkillSourceBadgeSchema,
|
||||
currentVersion: companySkillVersionSchema.nullable(),
|
||||
starredByCurrentActor: z.boolean(),
|
||||
});
|
||||
|
||||
export const companySkillVersionCreateSchema = z.object({
|
||||
label: z.string().trim().min(1).nullable().optional(),
|
||||
}).default({});
|
||||
|
||||
export const companySkillStarResultSchema = z.object({
|
||||
skillId: z.string().uuid(),
|
||||
starred: z.boolean(),
|
||||
starCount: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const companySkillCommentSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
companyId: z.string().uuid(),
|
||||
companySkillId: z.string().uuid(),
|
||||
parentCommentId: z.string().uuid().nullable(),
|
||||
authorAgentId: z.string().uuid().nullable(),
|
||||
authorUserId: z.string().nullable(),
|
||||
body: z.string(),
|
||||
deletedAt: z.coerce.date().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const companySkillCommentCreateSchema = z.object({
|
||||
body: z.string().min(1),
|
||||
parentCommentId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export const companySkillCommentUpdateSchema = z.object({
|
||||
body: z.string().min(1),
|
||||
});
|
||||
|
||||
export const companySkillForkSchema = z.object({
|
||||
name: z.string().min(1).nullable().optional(),
|
||||
slug: z.string().min(1).nullable().optional(),
|
||||
sharingScope: companySkillSharingScopeSchema.optional(),
|
||||
}).default({});
|
||||
|
||||
export const companySkillUpdateStatusSchema = z.object({
|
||||
supported: z.boolean(),
|
||||
reason: z.string().nullable(),
|
||||
@@ -156,6 +241,13 @@ export const companySkillCreateSchema = z.object({
|
||||
slug: z.string().min(1).nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
markdown: z.string().nullable().optional(),
|
||||
iconUrl: z.string().nullable().optional(),
|
||||
color: z.string().nullable().optional(),
|
||||
tagline: z.string().max(120).nullable().optional(),
|
||||
authorName: z.string().nullable().optional(),
|
||||
homepageUrl: z.string().nullable().optional(),
|
||||
categories: z.array(z.string().min(1)).optional(),
|
||||
sharingScope: companySkillSharingScopeSchema.optional(),
|
||||
});
|
||||
|
||||
export const companySkillFileDetailSchema = z.object({
|
||||
@@ -247,9 +339,14 @@ export const companySkillInstallCatalogResultSchema = z.object({
|
||||
});
|
||||
|
||||
export type CompanySkillImport = z.infer<typeof companySkillImportSchema>;
|
||||
export type CompanySkillListQuery = z.infer<typeof companySkillListQuerySchema>;
|
||||
export type CompanySkillProjectScan = z.infer<typeof companySkillProjectScanRequestSchema>;
|
||||
export type CompanySkillCreate = z.infer<typeof companySkillCreateSchema>;
|
||||
export type CompanySkillFileUpdate = z.infer<typeof companySkillFileUpdateSchema>;
|
||||
export type CompanySkillVersionCreate = z.infer<typeof companySkillVersionCreateSchema>;
|
||||
export type CompanySkillCommentCreate = z.infer<typeof companySkillCommentCreateSchema>;
|
||||
export type CompanySkillCommentUpdate = z.infer<typeof companySkillCommentUpdateSchema>;
|
||||
export type CompanySkillFork = z.infer<typeof companySkillForkSchema>;
|
||||
export type CatalogSkillListQuery = z.infer<typeof catalogSkillListQuerySchema>;
|
||||
export type CompanySkillInstallCatalog = z.infer<typeof companySkillInstallCatalogSchema>;
|
||||
export type CompanySkillInstallUpdate = z.infer<typeof companySkillInstallUpdateSchema>;
|
||||
|
||||
@@ -61,11 +61,23 @@ export {
|
||||
companySkillTrustLevelSchema,
|
||||
companySkillCompatibilitySchema,
|
||||
companySkillSourceBadgeSchema,
|
||||
companySkillSharingScopeSchema,
|
||||
companySkillListSortSchema,
|
||||
companySkillFileInventoryEntrySchema,
|
||||
companySkillVersionFileInventoryEntrySchema,
|
||||
companySkillSchema,
|
||||
companySkillListItemSchema,
|
||||
companySkillUsageAgentSchema,
|
||||
companySkillListQuerySchema,
|
||||
companySkillCategoryCountSchema,
|
||||
companySkillVersionSchema,
|
||||
companySkillDetailSchema,
|
||||
companySkillVersionCreateSchema,
|
||||
companySkillStarResultSchema,
|
||||
companySkillCommentSchema,
|
||||
companySkillCommentCreateSchema,
|
||||
companySkillCommentUpdateSchema,
|
||||
companySkillForkSchema,
|
||||
companySkillUpdateStatusSchema,
|
||||
companySkillAuditFindingSchema,
|
||||
companySkillAuditResultSchema,
|
||||
@@ -89,9 +101,14 @@ export {
|
||||
companySkillInstallUpdateSchema,
|
||||
companySkillResetSchema,
|
||||
type CompanySkillImport,
|
||||
type CompanySkillListQuery,
|
||||
type CompanySkillProjectScan,
|
||||
type CompanySkillCreate,
|
||||
type CompanySkillFileUpdate,
|
||||
type CompanySkillVersionCreate,
|
||||
type CompanySkillCommentCreate,
|
||||
type CompanySkillCommentUpdate,
|
||||
type CompanySkillFork,
|
||||
type CatalogSkillListQuery,
|
||||
type CompanySkillInstallCatalog,
|
||||
type CompanySkillInstallUpdate,
|
||||
@@ -121,6 +138,8 @@ export {
|
||||
export {
|
||||
agentSkillStateSchema,
|
||||
agentSkillSyncModeSchema,
|
||||
agentDesiredSkillEntrySchema,
|
||||
agentDesiredSkillSelectionSchema,
|
||||
agentSkillEntrySchema,
|
||||
agentSkillSnapshotSchema,
|
||||
agentSkillSyncSchema,
|
||||
|
||||
@@ -12,6 +12,19 @@ const mockAccessService = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
const mockCompanySkillService = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
categoryCounts: vi.fn(),
|
||||
detail: vi.fn(),
|
||||
listVersions: vi.fn(),
|
||||
getVersion: vi.fn(),
|
||||
createVersion: vi.fn(),
|
||||
starSkill: vi.fn(),
|
||||
unstarSkill: vi.fn(),
|
||||
forkSkill: vi.fn(),
|
||||
listComments: vi.fn(),
|
||||
createComment: vi.fn(),
|
||||
updateComment: vi.fn(),
|
||||
deleteComment: vi.fn(),
|
||||
importFromSource: vi.fn(),
|
||||
installFromCatalog: vi.fn(),
|
||||
deleteSkill: vi.fn(),
|
||||
@@ -102,6 +115,101 @@ describe("company skill mutation permissions", () => {
|
||||
imported: [],
|
||||
warnings: [],
|
||||
});
|
||||
mockCompanySkillService.list.mockResolvedValue([]);
|
||||
mockCompanySkillService.categoryCounts.mockResolvedValue([]);
|
||||
mockCompanySkillService.detail.mockResolvedValue(null);
|
||||
mockCompanySkillService.listVersions.mockResolvedValue([]);
|
||||
mockCompanySkillService.getVersion.mockResolvedValue(null);
|
||||
mockCompanySkillService.createVersion.mockResolvedValue({
|
||||
id: "version-1",
|
||||
companyId: "company-1",
|
||||
companySkillId: "skill-1",
|
||||
revisionNumber: 1,
|
||||
label: "v1",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill", content: "# Skill" }],
|
||||
authorAgentId: null,
|
||||
authorUserId: "board",
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
});
|
||||
mockCompanySkillService.starSkill.mockResolvedValue({
|
||||
skillId: "skill-1",
|
||||
starred: true,
|
||||
starCount: 1,
|
||||
});
|
||||
mockCompanySkillService.unstarSkill.mockResolvedValue({
|
||||
skillId: "skill-1",
|
||||
starred: false,
|
||||
starCount: 0,
|
||||
});
|
||||
mockCompanySkillService.forkSkill.mockResolvedValue({
|
||||
id: "skill-fork",
|
||||
companyId: "company-1",
|
||||
key: "company/company-1/review-fork",
|
||||
slug: "review-fork",
|
||||
name: "Review Fork",
|
||||
description: null,
|
||||
markdown: "# Review",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: "/tmp/review-fork",
|
||||
sourceRef: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
iconUrl: null,
|
||||
color: null,
|
||||
tagline: null,
|
||||
authorName: null,
|
||||
homepageUrl: null,
|
||||
categories: [],
|
||||
sharingScope: "company",
|
||||
publicShareToken: null,
|
||||
forkedFromSkillId: "skill-1",
|
||||
forkedFromCompanyId: "company-1",
|
||||
starCount: 0,
|
||||
installCount: 1,
|
||||
forkCount: 0,
|
||||
currentVersionId: null,
|
||||
metadata: null,
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
});
|
||||
mockCompanySkillService.listComments.mockResolvedValue([]);
|
||||
mockCompanySkillService.createComment.mockResolvedValue({
|
||||
id: "comment-1",
|
||||
companyId: "company-1",
|
||||
companySkillId: "skill-1",
|
||||
parentCommentId: null,
|
||||
authorAgentId: null,
|
||||
authorUserId: "board",
|
||||
body: "Looks good",
|
||||
deletedAt: null,
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
});
|
||||
mockCompanySkillService.updateComment.mockResolvedValue({
|
||||
id: "comment-1",
|
||||
companyId: "company-1",
|
||||
companySkillId: "skill-1",
|
||||
parentCommentId: null,
|
||||
authorAgentId: null,
|
||||
authorUserId: "board",
|
||||
body: "Updated",
|
||||
deletedAt: null,
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
});
|
||||
mockCompanySkillService.deleteComment.mockResolvedValue({
|
||||
id: "comment-1",
|
||||
companyId: "company-1",
|
||||
companySkillId: "skill-1",
|
||||
parentCommentId: null,
|
||||
authorAgentId: null,
|
||||
authorUserId: "board",
|
||||
body: "Updated",
|
||||
deletedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
createdAt: new Date("2026-05-26T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-05-26T00:01:00.000Z"),
|
||||
});
|
||||
mockCompanySkillService.installFromCatalog.mockResolvedValue({
|
||||
action: "created",
|
||||
skill: {
|
||||
@@ -484,6 +592,77 @@ describe("company skill mutation permissions", () => {
|
||||
expect(mockCompanySkillService.installFromCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes store list filters and category count requests to the service", async () => {
|
||||
const app = await createApp({ type: "board", source: "local_implicit" });
|
||||
|
||||
await request(app)
|
||||
.get("/api/companies/company-1/skills?sort=stars&categories[]=memory&category=git&scope=company&q=review")
|
||||
.expect(200);
|
||||
expect(mockCompanySkillService.list).toHaveBeenCalledWith("company-1", {
|
||||
q: "review",
|
||||
sort: "stars",
|
||||
categories: ["git", "memory"],
|
||||
scope: "company",
|
||||
});
|
||||
|
||||
await request(app).get("/api/companies/company-1/skills/categories").expect(200);
|
||||
expect(mockCompanySkillService.categoryCounts).toHaveBeenCalledWith("company-1");
|
||||
});
|
||||
|
||||
it("creates skill versions and logs the mutation", async () => {
|
||||
const app = await createApp({ type: "board", source: "local_implicit", userId: "user-1" });
|
||||
|
||||
await request(app)
|
||||
.post("/api/companies/company-1/skills/skill-1/versions")
|
||||
.send({ label: "v1" })
|
||||
.expect(201);
|
||||
|
||||
expect(mockCompanySkillService.createVersion).toHaveBeenCalledWith("company-1", "skill-1", { label: "v1" }, {
|
||||
type: "user",
|
||||
userId: "user-1",
|
||||
});
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
||||
action: "company.skill_version_created",
|
||||
entityType: "company_skill_version",
|
||||
entityId: "version-1",
|
||||
}));
|
||||
});
|
||||
|
||||
it("stars, forks, and comments on skills through company-scoped endpoints", async () => {
|
||||
const app = await createApp({ type: "board", source: "local_implicit", userId: "user-1" });
|
||||
|
||||
await request(app).post("/api/companies/company-1/skills/skill-1/star").send({}).expect(200);
|
||||
expect(mockCompanySkillService.starSkill).toHaveBeenCalledWith("company-1", "skill-1", {
|
||||
type: "user",
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await request(app).post("/api/companies/company-1/skills/skill-1/fork").send({ slug: "review-fork" }).expect(201);
|
||||
expect(mockCompanySkillService.forkSkill).toHaveBeenCalledWith("company-1", "skill-1", { slug: "review-fork" }, {
|
||||
type: "user",
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await request(app).post("/api/companies/company-1/skills/skill-1/comments").send({ body: "Looks good" }).expect(201);
|
||||
expect(mockCompanySkillService.createComment).toHaveBeenCalledWith("company-1", "skill-1", { body: "Looks good" }, {
|
||||
type: "user",
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
||||
action: "company.skill_starred",
|
||||
entityId: "skill-1",
|
||||
}));
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
||||
action: "company.skill_forked",
|
||||
entityId: "skill-fork",
|
||||
}));
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
||||
action: "company.skill_comment_created",
|
||||
entityId: "comment-1",
|
||||
}));
|
||||
});
|
||||
|
||||
it("allows agents with canCreateAgents to mutate company skills", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
|
||||
@@ -143,6 +143,169 @@ describeEmbeddedPostgres("companySkillService.list", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("filters store list results by category and creates version snapshots", async () => {
|
||||
const companyId = randomUUID();
|
||||
const skillId = randomUUID();
|
||||
const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-versioned-skill-"));
|
||||
cleanupDirs.add(skillDir);
|
||||
await fs.writeFile(path.join(skillDir, "SKILL.md"), "---\nname: Versioned Skill\ncategories:\n - Memory\n---\n\n# Versioned Skill\n", "utf8");
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companySkills).values({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: `company/${companyId}/versioned-skill`,
|
||||
slug: "versioned-skill",
|
||||
name: "Versioned Skill",
|
||||
description: "Tracks revisions.",
|
||||
markdown: "# Versioned Skill",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: skillDir,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
categories: ["memory"],
|
||||
tagline: "Tracks revisions",
|
||||
});
|
||||
|
||||
const filtered = await svc.list(companyId, { categories: ["memory"], sort: "recent" });
|
||||
expect(filtered.some((skill) => skill.id === skillId)).toBe(true);
|
||||
expect(filtered.find((skill) => skill.id === skillId)).toMatchObject({
|
||||
categories: ["memory"],
|
||||
tagline: "Tracks revisions",
|
||||
});
|
||||
|
||||
const version = await svc.createVersion(companyId, skillId, { label: "v1" }, { type: "user", userId: "board" });
|
||||
expect(version).toMatchObject({
|
||||
companySkillId: skillId,
|
||||
revisionNumber: 1,
|
||||
label: "v1",
|
||||
authorUserId: "board",
|
||||
});
|
||||
expect(version.fileInventory).toEqual([
|
||||
expect.objectContaining({
|
||||
path: "SKILL.md",
|
||||
kind: "skill",
|
||||
content: expect.stringContaining("# Versioned Skill"),
|
||||
}),
|
||||
]);
|
||||
await expect(svc.getVersion(companyId, skillId, version.id)).resolves.toMatchObject({ id: version.id });
|
||||
});
|
||||
|
||||
it("tracks stars and skill comments with actor ownership", async () => {
|
||||
const companyId = randomUUID();
|
||||
const skillId = randomUUID();
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companySkills).values({
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: `company/${companyId}/discussion-skill`,
|
||||
slug: "discussion-skill",
|
||||
name: "Discussion Skill",
|
||||
description: null,
|
||||
markdown: "# Discussion Skill",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
});
|
||||
|
||||
await expect(svc.starSkill(companyId, skillId, { type: "user", userId: "board" })).resolves.toMatchObject({
|
||||
starred: true,
|
||||
starCount: 1,
|
||||
});
|
||||
await expect(svc.starSkill(companyId, skillId, { type: "user", userId: "board" })).resolves.toMatchObject({
|
||||
starred: true,
|
||||
starCount: 1,
|
||||
});
|
||||
const comment = await svc.createComment(
|
||||
companyId,
|
||||
skillId,
|
||||
{ body: "Looks useful." },
|
||||
{ type: "user", userId: "board" },
|
||||
);
|
||||
expect(comment).toMatchObject({ body: "Looks useful.", authorUserId: "board" });
|
||||
await expect(svc.updateComment(
|
||||
companyId,
|
||||
skillId,
|
||||
comment.id,
|
||||
{ body: "Looks very useful." },
|
||||
{ type: "agent", agentId: randomUUID() },
|
||||
)).rejects.toMatchObject({ status: 422 });
|
||||
await expect(svc.deleteComment(companyId, skillId, comment.id, { type: "user", userId: "board" }))
|
||||
.resolves.toMatchObject({ id: comment.id, deletedAt: expect.any(Date) });
|
||||
await expect(svc.unstarSkill(companyId, skillId, { type: "user", userId: "board" })).resolves.toMatchObject({
|
||||
starred: false,
|
||||
starCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("validates version-aware desired skill selections", async () => {
|
||||
const companyId = randomUUID();
|
||||
const skillId = randomUUID();
|
||||
const otherSkillId = randomUUID();
|
||||
const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-pinned-skill-"));
|
||||
cleanupDirs.add(skillDir);
|
||||
await fs.writeFile(path.join(skillDir, "SKILL.md"), "# Pinned Skill\n", "utf8");
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
await db.insert(companySkills).values([
|
||||
{
|
||||
id: skillId,
|
||||
companyId,
|
||||
key: `company/${companyId}/pinned-skill`,
|
||||
slug: "pinned-skill",
|
||||
name: "Pinned Skill",
|
||||
description: null,
|
||||
markdown: "# Pinned Skill",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: skillDir,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
},
|
||||
{
|
||||
id: otherSkillId,
|
||||
companyId,
|
||||
key: `company/${companyId}/other-skill`,
|
||||
slug: "other-skill",
|
||||
name: "Other Skill",
|
||||
description: null,
|
||||
markdown: "# Other Skill",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
},
|
||||
]);
|
||||
const version = await svc.createVersion(companyId, skillId, {}, { type: "user", userId: "board" });
|
||||
|
||||
await expect(svc.resolveRequestedSkillEntries(companyId, [
|
||||
{ key: "pinned-skill", versionId: version.id },
|
||||
])).resolves.toEqual([
|
||||
{ key: `company/${companyId}/pinned-skill`, versionId: version.id },
|
||||
]);
|
||||
await expect(svc.resolveRequestedSkillEntries(companyId, [
|
||||
{ key: "other-skill", versionId: version.id },
|
||||
])).rejects.toMatchObject({ status: 422 });
|
||||
});
|
||||
|
||||
it("preserves missing local-path skills that active agents still desire", async () => {
|
||||
const companyId = randomUUID();
|
||||
const skillId = randomUUID();
|
||||
|
||||
+50
-17
@@ -16,6 +16,7 @@ import {
|
||||
normalizeIssueIdentifier,
|
||||
resetAgentSessionSchema,
|
||||
testAdapterEnvironmentSchema,
|
||||
type AgentDesiredSkillEntry,
|
||||
type AgentSkillSnapshot,
|
||||
type InstanceSchedulerHeartbeatAgent,
|
||||
upsertAgentInstructionsFileSchema,
|
||||
@@ -1282,18 +1283,39 @@ export function agentRoutes(
|
||||
|
||||
function buildUnsupportedSkillSnapshot(
|
||||
adapterType: string,
|
||||
desiredSkills: string[] = [],
|
||||
desiredSkillEntries: AgentDesiredSkillEntry[] = [],
|
||||
): AgentSkillSnapshot {
|
||||
const desiredSkills = desiredSkillEntries.map((entry) => entry.key);
|
||||
return {
|
||||
adapterType,
|
||||
supported: false,
|
||||
mode: "unsupported",
|
||||
desiredSkills,
|
||||
desiredSkillEntries,
|
||||
entries: [],
|
||||
warnings: ["This adapter does not implement skill sync yet."],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDesiredSkillSelections(
|
||||
requestedDesiredSkills: Array<string | AgentDesiredSkillEntry> | undefined,
|
||||
): AgentDesiredSkillEntry[] | undefined {
|
||||
if (!requestedDesiredSkills) return undefined;
|
||||
const out = new Map<string, AgentDesiredSkillEntry>();
|
||||
for (const value of requestedDesiredSkills) {
|
||||
const entry = typeof value === "string"
|
||||
? { key: value.trim(), versionId: null }
|
||||
: { key: value.key.trim(), versionId: value.versionId ?? null };
|
||||
if (!entry.key || out.has(entry.key)) continue;
|
||||
out.set(entry.key, entry);
|
||||
}
|
||||
return Array.from(out.values());
|
||||
}
|
||||
|
||||
function skillVersionSelectionMap(entries: Array<{ key: string; versionId: string | null }>) {
|
||||
return new Map(entries.map((entry) => [entry.key, entry.versionId] as const));
|
||||
}
|
||||
|
||||
// Legacy hardcoded set — used as fallback when adapter module does not
|
||||
// declare requiresMaterializedRuntimeSkills explicitly.
|
||||
const LEGACY_MATERIALIZED_SKILLS_SET = new Set([
|
||||
@@ -1319,9 +1341,11 @@ export function agentRoutes(
|
||||
materializeMissing?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const preference = readPaperclipSkillSyncPreference(config);
|
||||
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, {
|
||||
materializeMissing: options.materializeMissing
|
||||
?? shouldMaterializeRuntimeSkillsForAdapter(adapterType),
|
||||
versionSelections: skillVersionSelectionMap(preference.desiredSkillEntries),
|
||||
});
|
||||
return {
|
||||
...config,
|
||||
@@ -1333,31 +1357,39 @@ export function agentRoutes(
|
||||
companyId: string,
|
||||
adapterType: string,
|
||||
adapterConfig: Record<string, unknown>,
|
||||
requestedDesiredSkills: string[] | undefined,
|
||||
requestedDesiredSkills: AgentDesiredSkillEntry[] | undefined,
|
||||
) {
|
||||
if (!requestedDesiredSkills) {
|
||||
return {
|
||||
adapterConfig,
|
||||
desiredSkills: null as string[] | null,
|
||||
desiredSkillEntries: null as AgentDesiredSkillEntry[] | null,
|
||||
runtimeSkillEntries: null as Awaited<ReturnType<typeof companySkills.listRuntimeSkillEntries>> | null,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedRequestedSkills = await companySkills.resolveRequestedSkillKeys(
|
||||
const resolvedRequestedSkillEntries = await companySkills.resolveRequestedSkillEntries(
|
||||
companyId,
|
||||
requestedDesiredSkills,
|
||||
);
|
||||
const resolvedRequestedSkills = resolvedRequestedSkillEntries.map((entry) => entry.key);
|
||||
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(companyId, {
|
||||
materializeMissing: shouldMaterializeRuntimeSkillsForAdapter(adapterType),
|
||||
versionSelections: skillVersionSelectionMap(resolvedRequestedSkillEntries),
|
||||
});
|
||||
const requiredSkills = runtimeSkillEntries
|
||||
.filter((entry) => entry.required)
|
||||
.map((entry) => entry.key);
|
||||
const desiredSkills = Array.from(new Set([...requiredSkills, ...resolvedRequestedSkills]));
|
||||
const desiredSkillEntries = [
|
||||
...requiredSkills.map((key) => ({ key, versionId: null })),
|
||||
...resolvedRequestedSkillEntries,
|
||||
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.key === entry.key) === index);
|
||||
const desiredSkills = desiredSkillEntries.map((entry) => entry.key);
|
||||
|
||||
return {
|
||||
adapterConfig: writePaperclipSkillSyncPreference(adapterConfig, desiredSkills),
|
||||
adapterConfig: writePaperclipSkillSyncPreference(adapterConfig, desiredSkillEntries),
|
||||
desiredSkills,
|
||||
desiredSkillEntries,
|
||||
runtimeSkillEntries,
|
||||
};
|
||||
}
|
||||
@@ -1574,9 +1606,14 @@ export function agentRoutes(
|
||||
);
|
||||
const runtimeSkillEntries = await companySkills.listRuntimeSkillEntries(agent.companyId, {
|
||||
materializeMissing: false,
|
||||
versionSelections: skillVersionSelectionMap(preference.desiredSkillEntries),
|
||||
});
|
||||
const requiredSkills = runtimeSkillEntries.filter((entry) => entry.required).map((entry) => entry.key);
|
||||
res.json(buildUnsupportedSkillSnapshot(agent.adapterType, Array.from(new Set([...requiredSkills, ...preference.desiredSkills]))));
|
||||
const desiredSkillEntries = [
|
||||
...requiredSkills.map((key) => ({ key, versionId: null })),
|
||||
...preference.desiredSkillEntries,
|
||||
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.key === entry.key) === index);
|
||||
res.json(buildUnsupportedSkillSnapshot(agent.adapterType, desiredSkillEntries));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1611,16 +1648,11 @@ export function agentRoutes(
|
||||
}
|
||||
await assertCanUpdateAgent(req, agent);
|
||||
|
||||
const requestedSkills = Array.from(
|
||||
new Set(
|
||||
(req.body.desiredSkills as string[])
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
const requestedSkills = normalizeDesiredSkillSelections(req.body.desiredSkills);
|
||||
const {
|
||||
adapterConfig: nextAdapterConfig,
|
||||
desiredSkills,
|
||||
desiredSkillEntries,
|
||||
runtimeSkillEntries,
|
||||
} = await resolveDesiredSkillAssignment(
|
||||
agent.companyId,
|
||||
@@ -1628,7 +1660,7 @@ export function agentRoutes(
|
||||
agent.adapterConfig as Record<string, unknown>,
|
||||
requestedSkills,
|
||||
);
|
||||
if (!desiredSkills || !runtimeSkillEntries) {
|
||||
if (!desiredSkills || !desiredSkillEntries || !runtimeSkillEntries) {
|
||||
throw unprocessable("Skill sync requires desiredSkills.");
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
@@ -1669,7 +1701,7 @@ export function agentRoutes(
|
||||
adapterType: updated.adapterType,
|
||||
config: runtimeSkillConfig,
|
||||
})
|
||||
: buildUnsupportedSkillSnapshot(updated.adapterType, desiredSkills);
|
||||
: buildUnsupportedSkillSnapshot(updated.adapterType, desiredSkillEntries);
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: updated.companyId,
|
||||
@@ -1683,6 +1715,7 @@ export function agentRoutes(
|
||||
details: {
|
||||
adapterType: updated.adapterType,
|
||||
desiredSkills,
|
||||
desiredSkillEntries,
|
||||
mode: snapshot.mode,
|
||||
supported: snapshot.supported,
|
||||
entryCount: snapshot.entries.length,
|
||||
@@ -2093,7 +2126,7 @@ export function agentRoutes(
|
||||
companyId,
|
||||
hireInput.adapterType,
|
||||
requestedAdapterConfig,
|
||||
Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
|
||||
normalizeDesiredSkillSelections(Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined),
|
||||
);
|
||||
const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId,
|
||||
@@ -2279,7 +2312,7 @@ export function agentRoutes(
|
||||
companyId,
|
||||
createInput.adapterType,
|
||||
requestedAdapterConfig,
|
||||
Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
|
||||
normalizeDesiredSkillSelections(Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined),
|
||||
);
|
||||
const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId,
|
||||
|
||||
@@ -2,13 +2,18 @@ import { Router, type Request } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
catalogSkillListQuerySchema,
|
||||
companySkillCommentCreateSchema,
|
||||
companySkillCommentUpdateSchema,
|
||||
companySkillCreateSchema,
|
||||
companySkillFileUpdateSchema,
|
||||
companySkillForkSchema,
|
||||
companySkillImportSchema,
|
||||
companySkillInstallCatalogSchema,
|
||||
companySkillInstallUpdateSchema,
|
||||
companySkillListQuerySchema,
|
||||
companySkillProjectScanRequestSchema,
|
||||
companySkillResetSchema,
|
||||
companySkillVersionCreateSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { trackSkillImported } from "@paperclipai/shared/telemetry";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
@@ -63,6 +68,22 @@ export function companySkillRoutes(db: Db) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function queryStringArray(value: unknown): string[] {
|
||||
if (typeof value === "string") return [value];
|
||||
if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string");
|
||||
return [];
|
||||
}
|
||||
|
||||
function skillActor(req: Request) {
|
||||
if (req.actor.type === "agent") {
|
||||
return { type: "agent" as const, agentId: req.actor.agentId ?? null };
|
||||
}
|
||||
if (req.actor.type === "board") {
|
||||
return { type: "user" as const, userId: req.actor.userId ?? "board" };
|
||||
}
|
||||
return { type: "system" as const };
|
||||
}
|
||||
|
||||
async function assertCanMutateCompanySkills(req: Request, companyId: string) {
|
||||
assertCompanyAccess(req, companyId);
|
||||
|
||||
@@ -118,15 +139,30 @@ export function companySkillRoutes(db: Db) {
|
||||
router.get("/companies/:companyId/skills", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.list(companyId);
|
||||
const result = await svc.list(companyId, companySkillListQuerySchema.parse({
|
||||
q: firstQueryString(req.query.q),
|
||||
sort: firstQueryString(req.query.sort),
|
||||
categories: [
|
||||
...queryStringArray(req.query.category),
|
||||
...queryStringArray(req.query.categories),
|
||||
...queryStringArray(req.query["categories[]"]),
|
||||
],
|
||||
scope: firstQueryString(req.query.scope),
|
||||
}));
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/skills/categories", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await svc.categoryCounts(companyId));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/skills/:skillId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.detail(companyId, skillId);
|
||||
const result = await svc.detail(companyId, skillId, skillActor(req));
|
||||
if (!result) {
|
||||
res.status(404).json({ error: "Skill not found" });
|
||||
return;
|
||||
@@ -134,6 +170,199 @@ export function companySkillRoutes(db: Db) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/skills/:skillId/versions", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await svc.listVersions(companyId, skillId));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/skills/:skillId/versions/:versionId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
const versionId = req.params.versionId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.getVersion(companyId, skillId, versionId);
|
||||
if (!result) {
|
||||
res.status(404).json({ error: "Skill version not found" });
|
||||
return;
|
||||
}
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/skills/:skillId/versions",
|
||||
validate(companySkillVersionCreateSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
await assertCanMutateCompanySkills(req, companyId);
|
||||
const result = await svc.createVersion(companyId, skillId, req.body, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_version_created",
|
||||
entityType: "company_skill_version",
|
||||
entityId: result.id,
|
||||
details: {
|
||||
skillId,
|
||||
revisionNumber: result.revisionNumber,
|
||||
label: result.label,
|
||||
},
|
||||
});
|
||||
res.status(201).json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/companies/:companyId/skills/:skillId/star", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.starSkill(companyId, skillId, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_starred",
|
||||
entityType: "company_skill",
|
||||
entityId: skillId,
|
||||
details: { starCount: result.starCount },
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.delete("/companies/:companyId/skills/:skillId/star", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.unstarSkill(companyId, skillId, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_unstarred",
|
||||
entityType: "company_skill",
|
||||
entityId: skillId,
|
||||
details: { starCount: result.starCount },
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/skills/:skillId/fork",
|
||||
validate(companySkillForkSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
await assertCanMutateCompanySkills(req, companyId);
|
||||
const result = await svc.forkSkill(companyId, skillId, req.body, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_forked",
|
||||
entityType: "company_skill",
|
||||
entityId: result.id,
|
||||
details: {
|
||||
sourceSkillId: skillId,
|
||||
slug: result.slug,
|
||||
name: result.name,
|
||||
},
|
||||
});
|
||||
res.status(201).json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/companies/:companyId/skills/:skillId/comments", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await svc.listComments(companyId, skillId));
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/companies/:companyId/skills/:skillId/comments",
|
||||
validate(companySkillCommentCreateSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.createComment(companyId, skillId, req.body, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_comment_created",
|
||||
entityType: "company_skill_comment",
|
||||
entityId: result.id,
|
||||
details: { skillId, parentCommentId: result.parentCommentId },
|
||||
});
|
||||
res.status(201).json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/companies/:companyId/skills/:skillId/comments/:commentId",
|
||||
validate(companySkillCommentUpdateSchema),
|
||||
async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
const commentId = req.params.commentId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.updateComment(companyId, skillId, commentId, req.body, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_comment_updated",
|
||||
entityType: "company_skill_comment",
|
||||
entityId: result.id,
|
||||
details: { skillId },
|
||||
});
|
||||
res.json(result);
|
||||
},
|
||||
);
|
||||
|
||||
router.delete("/companies/:companyId/skills/:skillId/comments/:commentId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
const commentId = req.params.commentId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const result = await svc.deleteComment(companyId, skillId, commentId, skillActor(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "company.skill_comment_deleted",
|
||||
entityType: "company_skill_comment",
|
||||
entityId: result.id,
|
||||
details: { skillId },
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/skills/:skillId/update-status", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const skillId = req.params.skillId as string;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
Agent,
|
||||
AgentDesiredSkillEntry,
|
||||
AgentPermissions,
|
||||
AgentDetail,
|
||||
AgentInstructionsBundle,
|
||||
@@ -174,7 +175,7 @@ export const agentsApi = {
|
||||
listKeys: (id: string, companyId?: string) => api.get<AgentKey[]>(agentPath(id, companyId, "/keys")),
|
||||
skills: (id: string, companyId?: string) =>
|
||||
api.get<AgentSkillSnapshot>(agentPath(id, companyId, "/skills")),
|
||||
syncSkills: (id: string, desiredSkills: string[], companyId?: string) =>
|
||||
syncSkills: (id: string, desiredSkills: Array<string | AgentDesiredSkillEntry>, companyId?: string) =>
|
||||
api.post<AgentSkillSnapshot>(agentPath(id, companyId, "/skills/sync"), { desiredSkills }),
|
||||
createKey: (id: string, name: string, companyId?: string) =>
|
||||
api.post<AgentKeyCreated>(agentPath(id, companyId, "/keys"), { name }),
|
||||
|
||||
@@ -3,16 +3,25 @@ import type {
|
||||
CatalogSkillFileDetail,
|
||||
CatalogSkillKind,
|
||||
CompanySkill,
|
||||
CompanySkillCategoryCount,
|
||||
CompanySkillComment,
|
||||
CompanySkillCommentCreateRequest,
|
||||
CompanySkillCommentUpdateRequest,
|
||||
CompanySkillCreateRequest,
|
||||
CompanySkillDetail,
|
||||
CompanySkillFileDetail,
|
||||
CompanySkillForkRequest,
|
||||
CompanySkillImportResult,
|
||||
CompanySkillInstallCatalogRequest,
|
||||
CompanySkillInstallCatalogResult,
|
||||
CompanySkillListQuery,
|
||||
CompanySkillListItem,
|
||||
CompanySkillProjectScanRequest,
|
||||
CompanySkillProjectScanResult,
|
||||
CompanySkillStarResult,
|
||||
CompanySkillUpdateStatus,
|
||||
CompanySkillVersion,
|
||||
CompanySkillVersionCreateRequest,
|
||||
} from "@paperclipai/shared";
|
||||
import { api } from "./client";
|
||||
|
||||
@@ -23,12 +32,66 @@ export interface CatalogListQuery {
|
||||
}
|
||||
|
||||
export const companySkillsApi = {
|
||||
list: (companyId: string) =>
|
||||
api.get<CompanySkillListItem[]>(`/companies/${encodeURIComponent(companyId)}/skills`),
|
||||
list: (companyId: string, query: CompanySkillListQuery = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (query.q) params.set("q", query.q);
|
||||
if (query.sort) params.set("sort", query.sort);
|
||||
if (query.scope) params.set("scope", query.scope);
|
||||
for (const category of query.categories ?? []) params.append("categories[]", category);
|
||||
const search = params.toString();
|
||||
return api.get<CompanySkillListItem[]>(`/companies/${encodeURIComponent(companyId)}/skills${search ? `?${search}` : ""}`);
|
||||
},
|
||||
categories: (companyId: string) =>
|
||||
api.get<CompanySkillCategoryCount[]>(`/companies/${encodeURIComponent(companyId)}/skills/categories`),
|
||||
detail: (companyId: string, skillId: string) =>
|
||||
api.get<CompanySkillDetail>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}`,
|
||||
),
|
||||
versions: (companyId: string, skillId: string) =>
|
||||
api.get<CompanySkillVersion[]>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/versions`,
|
||||
),
|
||||
version: (companyId: string, skillId: string, versionId: string) =>
|
||||
api.get<CompanySkillVersion>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/versions/${encodeURIComponent(versionId)}`,
|
||||
),
|
||||
createVersion: (companyId: string, skillId: string, payload: CompanySkillVersionCreateRequest = {}) =>
|
||||
api.post<CompanySkillVersion>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/versions`,
|
||||
payload,
|
||||
),
|
||||
star: (companyId: string, skillId: string) =>
|
||||
api.post<CompanySkillStarResult>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/star`,
|
||||
{},
|
||||
),
|
||||
unstar: (companyId: string, skillId: string) =>
|
||||
api.delete<CompanySkillStarResult>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/star`,
|
||||
),
|
||||
fork: (companyId: string, skillId: string, payload: CompanySkillForkRequest = {}) =>
|
||||
api.post<CompanySkill>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/fork`,
|
||||
payload,
|
||||
),
|
||||
comments: (companyId: string, skillId: string) =>
|
||||
api.get<CompanySkillComment[]>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/comments`,
|
||||
),
|
||||
createComment: (companyId: string, skillId: string, payload: CompanySkillCommentCreateRequest) =>
|
||||
api.post<CompanySkillComment>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/comments`,
|
||||
payload,
|
||||
),
|
||||
updateComment: (companyId: string, skillId: string, commentId: string, payload: CompanySkillCommentUpdateRequest) =>
|
||||
api.patch<CompanySkillComment>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/comments/${encodeURIComponent(commentId)}`,
|
||||
payload,
|
||||
),
|
||||
deleteComment: (companyId: string, skillId: string, commentId: string) =>
|
||||
api.delete<CompanySkillComment>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/comments/${encodeURIComponent(commentId)}`,
|
||||
),
|
||||
updateStatus: (companyId: string, skillId: string) =>
|
||||
api.get<CompanySkillUpdateStatus>(
|
||||
`/companies/${encodeURIComponent(companyId)}/skills/${encodeURIComponent(skillId)}/update-status`,
|
||||
|
||||
@@ -424,6 +424,23 @@ function AcpxLocalTranscriptStory() {
|
||||
|
||||
const SKILLS_COMPANY_ID = "company-storybook";
|
||||
|
||||
const defaultStoreSkillFields = {
|
||||
iconUrl: null,
|
||||
color: null,
|
||||
tagline: null,
|
||||
authorName: null,
|
||||
homepageUrl: null,
|
||||
categories: [],
|
||||
sharingScope: "company" as const,
|
||||
publicShareToken: null,
|
||||
forkedFromSkillId: null,
|
||||
forkedFromCompanyId: null,
|
||||
starCount: 0,
|
||||
installCount: 1,
|
||||
forkCount: 0,
|
||||
currentVersionId: null,
|
||||
};
|
||||
|
||||
const acpxSkillsCompanyLibrary: CompanySkillListItem[] = [
|
||||
{
|
||||
id: "skill-paperclip",
|
||||
@@ -439,6 +456,7 @@ const acpxSkillsCompanyLibrary: CompanySkillListItem[] = [
|
||||
trustLevel: "scripts_executables",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
...defaultStoreSkillFields,
|
||||
createdAt: new Date("2026-04-12T09:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-22T15:30:00.000Z"),
|
||||
attachedAgentCount: 4,
|
||||
@@ -466,6 +484,7 @@ const acpxSkillsCompanyLibrary: CompanySkillListItem[] = [
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
...defaultStoreSkillFields,
|
||||
createdAt: new Date("2026-04-15T10:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-25T12:00:00.000Z"),
|
||||
attachedAgentCount: 2,
|
||||
@@ -493,6 +512,7 @@ const acpxSkillsCompanyLibrary: CompanySkillListItem[] = [
|
||||
trustLevel: "assets",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [{ path: "SKILL.md", kind: "skill" }],
|
||||
...defaultStoreSkillFields,
|
||||
createdAt: new Date("2026-04-18T11:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-26T09:30:00.000Z"),
|
||||
attachedAgentCount: 1,
|
||||
|
||||
Reference in new issue
Block a user