/** * `@ai-matrx/agents/skills` — types.ts (moved from chat `ui/skills-types`, agent core A2) * * Wire shapes and view models for the Agent Skills feature. * * The Python backend wire shape uses snake_case (per Pydantic) — see * `aidream/api/routers/skills.py` and `docs/AGENT_SKILLS_HANDOFF.md`. We * mirror it once here in camelCase and convert at the slice boundary * (`skillsConverters.ts`) so consumer components never touch snake_case. * * When the backend OpenAPI regenerates and `/api/skills/{id}/projects/...` * is in `@ai-matrx/api/types`, swap the hand-mirrored * `SkillRowWire` for the generated `components["schemas"]["SkillRow"]` * and drop the `as never` casts in the thunks. */ interface SkillRowWire { id: string; skill_id: string; label: string; description: string; skill_type: string; body: string | null; icon_name: string | null; model_preference: string | null; allowed_tools: string[]; trigger_patterns: string[]; disable_auto_invocation: boolean; platform_targets: string[]; version: string | null; config: Record; category_id: string | null; parent_skill_id: string | null; is_active: boolean; is_system: boolean; is_public: boolean; sort_order: number; user_id: string | null; organization_id: string | null; project_id: string | null; /** Many-to-many associations from skl_skill_projects (read-only on the list endpoint). */ project_ids: string[]; } interface CategoryRowWire { id: string; category_key: string; label: string; description: string | null; icon_name: string | null; color: string | null; parent_category_id: string | null; sort_order: number; is_active: boolean; /** `user_id IS NULL` → system category. Populated when reading direct * from Supabase; the Python wire shape currently strips this on its * CategoryRow response model (it's still on the row server-side). */ user_id?: string | null; } interface SkillsListWire { count: number; skills: SkillRowWire[]; } interface CategoryListWire { count: number; categories: CategoryRowWire[]; } /** `skill.resource` row as it comes off Supabase. The Python backend * doesn't yet expose a CRUD surface for resources; reads + writes go * direct via the Supabase client (RLS gates on parent-skill ownership). */ interface ResourceRowWire { id: string; skill_id: string; resource_type: string; filename: string; content: string | null; storage_path: string | null; mime_type: string | null; sort_order: number; is_active: boolean; } interface IngestRequestWire { roots: string[]; dry_run: boolean; } interface IngestReportWire { parsed: number; created: number; updated: number; unchanged: number; errors: string[]; skills: Array<{ skill_id: string; source_path: string; /** Slug (`platform.categories.slug`, dimension="skill"); "" if none. */ category?: string; /** "created" | "updated" | "unchanged" | "error" | "pending" (dry run). */ status?: string; /** Resulting `skill.definition.id` (UUID); absent on dry run or error. */ id?: string; }>; roots: string[]; } interface SkillCreateWire { skill_id: string; label: string; description: string; skill_type: string; body: string; disable_auto_invocation: boolean; is_public: boolean; icon_name?: string | null; model_preference?: string | null; allowed_tools?: string[]; trigger_patterns?: string[]; platform_targets?: string[]; version?: string | null; config?: Record; category_id?: string | null; parent_skill_id?: string | null; } interface SkillPatchWire { label?: string; description?: string; skill_type?: string; body?: string; icon_name?: string | null; model_preference?: string | null; allowed_tools?: string[]; trigger_patterns?: string[]; disable_auto_invocation?: boolean; platform_targets?: string[]; version?: string | null; config?: Record; category_id?: string | null; parent_skill_id?: string | null; is_public?: boolean; is_active?: boolean; } interface ProjectAssociationWire { skill_id: string; project_id: string; } /** The full skill row as it flows through the FE. Matches `SkillRowWire` * field-for-field with snake_case → camelCase conversion. */ interface SkillRow { id: string; skillId: string; label: string; description: string; skillType: SkillType; body: string | null; iconName: string | null; modelPreference: string | null; allowedTools: string[]; triggerPatterns: string[]; disableAutoInvocation: boolean; platformTargets: string[]; version: string | null; config: Record; categoryId: string | null; parentSkillId: string | null; isActive: boolean; isSystem: boolean; isPublic: boolean; sortOrder: number; userId: string | null; organizationId: string | null; projectId: string | null; /** Multi-project membership via skl_skill_projects join. */ projectIds: string[]; } /** Resource row tied to a skill — markdown / text attachment, or a * storage-path pointer (the storage-bucket integration is deferred; * inline `content` covers the cases we use today). */ interface ResourceRow { id: string; skillId: string; resourceType: string; filename: string; content: string | null; storagePath: string | null; mimeType: string | null; sortOrder: number; isActive: boolean; } /** Local form draft for create / edit. */ interface ResourceDraft { id?: string; skillId: string; resourceType: string; filename: string; content: string; mimeType: string | null; sortOrder: number; } interface CategoryRow { id: string; categoryKey: string; label: string; description: string | null; iconName: string | null; color: string | null; parentCategoryId: string | null; sortOrder: number; isActive: boolean; /** `null` → system category (visible to every user). Populated when * the row was fetched via Supabase direct; may be undefined when read * via the Python `/api/skills/categories` GET (which strips it * today). The editor uses this to gate write-paths. */ userId?: string | null; /** Full `platform.categories.metadata` blob. Kept so `updateCategoryThunk` * can merge-patch it without wiping unrelated fields (description, is_active, * legacy_table, etc. all live here). Absent on wire-path rows (Python API). */ metadata?: Record | null; } type IngestSkillStatus = "created" | "updated" | "unchanged" | "error" /** Dry run — the write hasn't happened, so the real outcome is unknown. */ | "pending"; interface IngestReport { parsed: number; created: number; updated: number; unchanged: number; errors: string[]; skills: Array<{ skillId: string; sourcePath: string; /** Category slug from frontmatter; "" if none was set. */ category: string; status: IngestSkillStatus; /** Resulting `skill.definition.id`; null on dry run or error. */ id: string | null; }>; roots: string[]; } /** Per-agent skill tiering (included / listed / forbidden). Stored on `agx_agent.skill_config` JSONB. */ interface SkillConfig { included: string[]; listed: string[]; forbidden: string[]; disabled: boolean; } /** SkillConfig with an "undecided" tier — used in the picker UI to surface * skills that the agent neither includes nor lists nor forbids (the * implicit DEFAULT bucket). */ type SkillTier = "included" | "listed" | "forbidden" | "default"; /** Free-string per the wire contract, but these are the recognized values * for filtering / labeling. */ type SkillType = "render_block" | "convention" | "workflow" | "task" | "reference" | "mode" | "agent_behavior" | string; /** Local form draft used while a skill is being edited / created. Mirrors * SkillRow but the immutable identity fields collapse to single optional * id / skill_id slots. */ interface SkillDraft { id?: string; skillId: string; label: string; description: string; skillType: SkillType; body: string; iconName: string | null; modelPreference: string | null; allowedTools: string[]; triggerPatterns: string[]; disableAutoInvocation: boolean; platformTargets: string[]; version: string | null; config: Record; categoryId: string | null; parentSkillId: string | null; isPublic: boolean; /** Admin-only: when the form is in admin mode, this toggle promotes * the row to `is_system=true`. Plumbing for the FE; rejected by the * backend if the caller isn't an admin. */ isSystem: boolean; } type AsyncStatus = "idle" | "loading" | "ready" | "error"; /** * `@ai-matrx/agents/skills` — inbound converters (wire / Supabase rows → view models). * Moved verbatim from matrx-frontend `features/skills/redux/skillsConverters.ts` (agent core A2); * only the generated-`Database` row types became structural, so the core needs no app types. * Outbound (draft → wire) converters stay with the editors in the app. */ /** * Aliased row shape returned by the `platform.categories` select (dimension='skill'): * `category_key:slug, label:name, icon_name:icon, parent_category_id:parent_id, * sort_order:position, description/is_active/user_id` out of the `metadata` blob. `is_active` is * JSONB text extraction ("true" | "false" | null), never a real boolean. */ type SklCategoryRow = { id: string; category_key: string | null; label: string; description: string | null; icon_name: string | null; color: string | null; parent_category_id: string | null; sort_order: number | null; is_active: string | null; user_id: string | null; /** Full metadata blob — kept so a category edit can merge-patch without wiping other fields. */ metadata: unknown; }; /** The columns of `skill.definition` the reader converts (structural — a generated Row satisfies it). */ interface SklDefinitionRow { id: string; skill_id: string; label: string; description: string; skill_type: string | null; body: string | null; icon_name: string | null; model_preference: string | null; allowed_tools: unknown; trigger_patterns: unknown; disable_auto_invocation: boolean; platform_targets: unknown; semver: string | null; config: unknown; category_id: string | null; parent_skill_id: string | null; is_active: boolean; is_system: boolean; published_to_web: boolean; sort_order: number | null; created_by: string | null; organization_id: string | null; project_id: string | null; } declare function wireToSkillRow(wire: SkillRowWire): SkillRow; /** Adapter for rows read straight off `skill.definition` via Supabase — the canonical read path. */ declare function supabaseRowToSkillRow(row: SklDefinitionRow): SkillRow; declare function wireToCategoryRow(wire: CategoryRowWire): CategoryRow; declare const PLATFORM_SKILL_CATEGORY_SELECT: "id, slug, name, icon, color, parent_id, position, metadata"; /** The `platform.categories` columns the converter reads (what `PLATFORM_SKILL_CATEGORY_SELECT` selects). */ interface PlatformCategorySelectRow { id: string; slug: string | null; name: string; icon: string | null; color: string | null; parent_id: string | null; position: number | null; metadata: unknown; } /** Maps a `platform.categories` row (dimension='skill') to the aliased select shape. */ declare function platformCategoryToSklRow(row: PlatformCategorySelectRow): SklCategoryRow; /** Adapter for aliased `platform.categories` rows. `is_active` / `user_id` are text (JSONB `->>`), so * compare strictly (`=== "true"`) — `Boolean("false")` is `true`. */ declare function supabaseRowToCategoryRow(row: SklCategoryRow): CategoryRow; declare function wireToIngestReport(wire: IngestReportWire): IngestReport; /** * `@ai-matrx/agents/skills` — `createSkillCatalog`: the headless skill catalog store. * * The ONE holder of the skill list rows, the skill categories and the "a skill changed on the * server" signal. Built like `createModelCatalog` (framework-free; `getState` / `subscribe`; React * binding in `./react`). It READS only: `fetchSkills`, `fetchSkillById`, `fetchSkillCategories`, * plus `applyStreamEvent` for the stream's `resource_changed` skill events. The skill WRITES (create, * patch, delete, categories, resources, project links) stay with the skill editors in the host, * which tell this store what landed through the `apply*` mirror methods — local state only. * * Reads go direct to Supabase (RLS gates access); a read failure rejects with * `SkillCatalogReadError` AND lands in `error` + the `errorSink` — never silent. */ interface SkillCatalogPgError { message: string; code?: string; } interface SkillCatalogPgResult { data: unknown; error: SkillCatalogPgError | null; } /** The thenable PostgREST builder, structurally (the real builder's own methods). */ interface SkillCatalogQuery extends PromiseLike { select(columns: string): SkillCatalogQuery; eq(column: string, value: unknown): SkillCatalogQuery; in(column: string, values: readonly unknown[]): SkillCatalogQuery; order(column: string, options?: { ascending?: boolean; }): SkillCatalogQuery; limit(count: number): SkillCatalogQuery; maybeSingle(): PromiseLike; } /** REQUIRED. A structural subset of `SupabaseClient`. */ interface SkillCatalogClient { schema(name: string): { from(table: string): SkillCatalogQuery; }; } /** Skill ↔ project membership lives in `platform.associations`; the host (or `@ai-matrx/associations`) answers. */ interface SkillCatalogAssociations { /** Skill ids that are members of the project. */ skillIdsForProject(projectId: string): Promise<{ ok: true; ids: string[]; } | { ok: false; message: string; }>; /** `skillId → projectId[]` for a batch of skills. A failure answers `{}` (the list still renders). */ projectIdsBySkill(skillIds: readonly string[]): Promise>; } type SkillCatalogErrorSink = (event: { code: string; message: string; context?: object; }) => void; interface SkillCatalogConfig { client: SkillCatalogClient; associations: SkillCatalogAssociations; /** OPTIONAL — DEFAULT: a tagged `console.error`. */ errorSink?: SkillCatalogErrorSink; } /** A read failed. `operation` is the plain-English action ("load your skills") for the host's error door. */ declare class SkillCatalogReadError extends Error { readonly operation: string; readonly pgError: SkillCatalogPgError | null; constructor(operation: string, message: string, pgError: SkillCatalogPgError | null); } interface FetchSkillsArgs { categoryId?: string | undefined; isPublicOnly?: boolean | undefined; projectId?: string | undefined; limit?: number | undefined; } /** The key of one skills-list read — a mount with the same key reuses the list instead of reading again. */ declare function skillsListKey(args: FetchSkillsArgs | undefined): string; /** Payload of a stream `resource_changed` skill event. */ interface SkillStreamEventPayload { kind: string; action: "created" | "modified" | "deleted" | "invalidated"; resourceId: string; metadata: Record; } interface SkillCatalogState { skills: { byId: Record; allIds: string[]; status: AsyncStatus; error: string | null; /** Filter key of the list in `allIds`. */ loadedKey: string | null; }; categories: { byId: Record; allIds: string[]; status: AsyncStatus; error: string | null; }; /** Wall-clock millis of the last skill stream event (0 = none). Subscribers refetch when it moves. */ lastIngestAt: number; /** The report a `skills.ingested` event carried (counts only). */ lastStreamReport: IngestReport | null; } interface SkillCatalog { getState(): SkillCatalogState; subscribe(listener: () => void): () => void; fetchSkills(args?: FetchSkillsArgs): Promise; fetchSkillById(skillRef: string): Promise; fetchSkillCategories(): Promise; /** Record a skill stream event. Does not refetch — subscribers react to `lastIngestAt`. */ applyStreamEvent(event: SkillStreamEventPayload): void; applySkill(row: SkillRow): void; applySkillRemoved(id: string): void; applySkillProjects(skillId: string, projectIds: string[]): void; applyCategory(row: CategoryRow): void; applyCategoryRemoved(id: string): void; applyCategoriesReordered(parentId: string | null, orderedIds: string[]): void; /** Back to the empty initial state (sign-out / scope change). */ clear(): void; } declare function createSkillCatalog(config: SkillCatalogConfig): SkillCatalog; export { PLATFORM_SKILL_CATEGORY_SELECT, SkillCatalogReadError, createSkillCatalog, platformCategoryToSklRow, skillsListKey, supabaseRowToCategoryRow, supabaseRowToSkillRow, wireToCategoryRow, wireToIngestReport, wireToSkillRow }; export type { AsyncStatus, CategoryListWire, CategoryRow, CategoryRowWire, FetchSkillsArgs, IngestReport, IngestReportWire, IngestRequestWire, IngestSkillStatus, PlatformCategorySelectRow, ProjectAssociationWire, ResourceDraft, ResourceRow, ResourceRowWire, SkillCatalog, SkillCatalogAssociations, SkillCatalogClient, SkillCatalogConfig, SkillCatalogErrorSink, SkillCatalogPgError, SkillCatalogPgResult, SkillCatalogQuery, SkillCatalogState, SkillConfig, SkillCreateWire, SkillDraft, SkillPatchWire, SkillRow, SkillRowWire, SkillStreamEventPayload, SkillTier, SkillType, SkillsListWire, SklCategoryRow, SklDefinitionRow };