From 3e05841759e89aed144914498ef88228913da46c Mon Sep 17 00:00:00 2001 From: Steve Jobs Date: Tue, 2 Jun 2026 16:53:37 +0200 Subject: [PATCH] feat(skills): track last used timestamp --- drizzle/0011_skill_last_used_at.sql | 1 + drizzle/meta/0011_snapshot.json | 1427 +++++++++++++++++++++++ drizzle/meta/_journal.json | 9 +- src/lib/server/db/schema.ts | 1 + src/lib/server/mcp/tools/skills.ts | 4 +- src/lib/server/services/skills.ts | 12 + src/routes/(app)/skills/+page.svelte | 4 +- src/routes/api/skills/[slug]/+server.ts | 4 +- tests/integration/mcp.test.ts | 10 +- tests/integration/skills.test.ts | 15 +- 10 files changed, 1479 insertions(+), 8 deletions(-) create mode 100644 drizzle/0011_skill_last_used_at.sql create mode 100644 drizzle/meta/0011_snapshot.json diff --git a/drizzle/0011_skill_last_used_at.sql b/drizzle/0011_skill_last_used_at.sql new file mode 100644 index 0000000..fba5888 --- /dev/null +++ b/drizzle/0011_skill_last_used_at.sql @@ -0,0 +1 @@ +ALTER TABLE "skills" ADD COLUMN "last_used_at" timestamp with time zone; diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..76c9a44 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1427 @@ +{ + "id": "5f3c8c75-a843-439a-be44-0dd1de71de5e", + "prevId": "c5c3ef51-3063-47d0-ac56-d4172231d5ac", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_id": { + "name": "token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "token_name": { + "name": "token_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_slug": { + "name": "target_slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "guard_action": { + "name": "guard_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "guard_reason": { + "name": "guard_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_token_id_idx": { + "name": "audit_logs_token_id_idx", + "columns": [ + { + "expression": "token_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_target_id_idx": { + "name": "audit_logs_target_id_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_token_id_tokens_id_fk": { + "name": "audit_logs_token_id_tokens_id_fk", + "tableFrom": "audit_logs", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_target_id_targets_id_fk": { + "name": "audit_logs_target_id_targets_id_fk", + "tableFrom": "audit_logs", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memories": { + "name": "memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_id": { + "name": "token_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_identifier": { + "name": "user_identifier", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_memories_token": { + "name": "idx_memories_token", + "columns": [ + { + "expression": "token_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_memories_visibility": { + "name": "idx_memories_visibility", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_memories_user": { + "name": "idx_memories_user", + "columns": [ + { + "expression": "user_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memories_token_id_tokens_id_fk": { + "name": "memories_token_id_tokens_id_fk", + "tableFrom": "memories", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "content_md": { + "name": "content_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_slug_unique": { + "name": "skills_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.target_auth_methods": { + "name": "target_auth_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential": { + "name": "credential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_hint": { + "name": "credential_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "target_auth_methods_target_id_targets_id_fk": { + "name": "target_auth_methods_target_id_targets_id_fk", + "tableFrom": "target_auth_methods", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.targets": { + "name": "targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "targets_slug_unique": { + "name": "targets_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_permissions": { + "name": "token_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_id": { + "name": "token_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "token_permissions_token_id_tokens_id_fk": { + "name": "token_permissions_token_id_tokens_id_fk", + "tableFrom": "token_permissions", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "token_permissions_target_id_targets_id_fk": { + "name": "token_permissions_target_id_targets_id_fk", + "tableFrom": "token_permissions", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "token_permissions_token_id_target_id_unique": { + "name": "token_permissions_token_id_target_id_unique", + "nullsNotDistinct": false, + "columns": [ + "token_id", + "target_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_vault_permissions": { + "name": "token_vault_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_id": { + "name": "token_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "vault_id": { + "name": "vault_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "token_vault_permissions_token_id_tokens_id_fk": { + "name": "token_vault_permissions_token_id_tokens_id_fk", + "tableFrom": "token_vault_permissions", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "token_vault_permissions_vault_id_vaults_id_fk": { + "name": "token_vault_permissions_vault_id_vaults_id_fk", + "tableFrom": "token_vault_permissions", + "tableTo": "vaults", + "columnsFrom": [ + "vault_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "token_vault_permissions_token_id_vault_id_unique": { + "name": "token_vault_permissions_token_id_vault_id_unique", + "nullsNotDistinct": false, + "columns": [ + "token_id", + "vault_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tokens": { + "name": "tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_ips": { + "name": "allowed_ips", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "default_user": { + "name": "default_user", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tokens_token_hash_unique": { + "name": "tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_item_fields": { + "name": "vault_item_fields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "item_id": { + "name": "item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "vault_item_fields_item_id_vault_items_id_fk": { + "name": "vault_item_fields_item_id_vault_items_id_fk", + "tableFrom": "vault_item_fields", + "tableTo": "vault_items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_item_fields_item_id_name_unique": { + "name": "vault_item_fields_item_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "item_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_items": { + "name": "vault_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "vault_id": { + "name": "vault_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_items_vault_id_vaults_id_fk": { + "name": "vault_items_vault_id_vaults_id_fk", + "tableFrom": "vault_items", + "tableTo": "vaults", + "columnsFrom": [ + "vault_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_items_vault_id_slug_unique": { + "name": "vault_items_vault_id_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "vault_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vaults": { + "name": "vaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vaults_slug_unique": { + "name": "vaults_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoints": { + "name": "webhook_endpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_id": { + "name": "token_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature_header": { + "name": "signature_header", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handling_instructions": { + "name": "handling_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webhook_endpoints_token_id_tokens_id_fk": { + "name": "webhook_endpoints_token_id_tokens_id_fk", + "tableFrom": "webhook_endpoints", + "tableTo": "tokens", + "columnsFrom": [ + "token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_endpoints_slug_unique": { + "name": "webhook_endpoints_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "webhook_events_endpoint_status_idx": { + "name": "webhook_events_endpoint_status_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_events_expires_at_idx": { + "name": "webhook_events_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_endpoint_id_webhook_endpoints_id_fk": { + "name": "webhook_events_endpoint_id_webhook_endpoints_id_fk", + "tableFrom": "webhook_events", + "tableTo": "webhook_endpoints", + "columnsFrom": [ + "endpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_pages": { + "name": "wiki_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "namespace": { + "name": "namespace", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "slug": { + "name": "slug", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_by": { + "name": "updated_by", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_wiki_namespace_slug": { + "name": "uq_wiki_namespace_slug", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_wiki_namespace": { + "name": "idx_wiki_namespace", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_wiki_status": { + "name": "idx_wiki_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 80d572a..4651657 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1778152691434, "tag": "0010_hard_whirlwind", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1780411720000, + "tag": "0011_skill_last_used_at", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 4631c54..28712a4 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -197,6 +197,7 @@ export const skills = pgTable("skills", { updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), + lastUsedAt: timestamp("last_used_at", { withTimezone: true }), }); export type Skill = typeof skills.$inferSelect; diff --git a/src/lib/server/mcp/tools/skills.ts b/src/lib/server/mcp/tools/skills.ts index 5ee28e6..0fca437 100644 --- a/src/lib/server/mcp/tools/skills.ts +++ b/src/lib/server/mcp/tools/skills.ts @@ -1,4 +1,4 @@ -import { listSkills, getSkill, createSkill, updateSkill, deleteSkill } from "$lib/server/services/skills"; +import { listSkills, getSkill, createSkill, updateSkill, deleteSkill, markSkillUsed } from "$lib/server/services/skills"; import { parseSkillMd } from "$lib/server/utils/skill-parser"; import { isBuiltInSkill } from "$lib/server/built-in-skills"; @@ -9,11 +9,13 @@ export async function skillList() { export async function skillRead(args: { slug: string }) { const skill = await getSkill(args.slug); if (!skill) return { error: `Skill "${args.slug}" not found` }; + const lastUsedAt = skill.builtIn ? skill.lastUsedAt : await markSkillUsed(skill.slug); return { slug: skill.slug, description: skill.description, content: skill.contentMd, version: skill.version, + last_used_at: lastUsedAt?.toISOString() ?? null, }; } diff --git a/src/lib/server/services/skills.ts b/src/lib/server/services/skills.ts index fa97f64..fa015f8 100644 --- a/src/lib/server/services/skills.ts +++ b/src/lib/server/services/skills.ts @@ -9,6 +9,7 @@ export async function listSkills() { .select({ slug: skills.slug, description: skills.description, + last_used_at: skills.lastUsedAt, }) .from(skills) .orderBy(skills.slug); @@ -16,6 +17,7 @@ export async function listSkills() { const builtIn = getBuiltInSkills().map((s) => ({ slug: s.slug, description: s.description, + last_used_at: null, builtIn: true, })); @@ -36,6 +38,7 @@ export async function getSkill(slug: string) { builtIn: true, createdAt: new Date(0), updatedAt: new Date(0), + lastUsedAt: null, }; } @@ -47,6 +50,15 @@ export async function getSkill(slug: string) { return row ? { ...row, builtIn: false } : null; } +export async function markSkillUsed(slug: string) { + const [row] = await db + .update(skills) + .set({ lastUsedAt: new Date() }) + .where(eq(skills.slug, slug)) + .returning({ lastUsedAt: skills.lastUsedAt }); + return row?.lastUsedAt ?? null; +} + export async function createSkill(contentMd: string) { const { slug, description } = parseSkillMd(contentMd); const [row] = await db diff --git a/src/routes/(app)/skills/+page.svelte b/src/routes/(app)/skills/+page.svelte index ff58981..2c036c4 100644 --- a/src/routes/(app)/skills/+page.svelte +++ b/src/routes/(app)/skills/+page.svelte @@ -12,7 +12,7 @@ let { data } = $props(); - type SkillEntry = { slug: string; description: string; builtIn: boolean }; + type SkillEntry = { slug: string; description: string; builtIn: boolean; last_used_at: Date | string | null }; let localSkills = $state(null); let skills = $derived(localSkills ?? data.skills); @@ -111,7 +111,7 @@ createSubmitting = false; if (result.type === "success" && result.data?.created) { const created = result.data.created as { slug: string; description: string }; - localSkills = [...skills, { slug: created.slug, description: created.description, builtIn: false }]; + localSkills = [...skills, { slug: created.slug, description: created.description, builtIn: false, last_used_at: null }]; createOpen = false; toast.success("Skill created"); } else if (result.type === "failure") { diff --git a/src/routes/api/skills/[slug]/+server.ts b/src/routes/api/skills/[slug]/+server.ts index ff8000a..1c0cb79 100644 --- a/src/routes/api/skills/[slug]/+server.ts +++ b/src/routes/api/skills/[slug]/+server.ts @@ -1,18 +1,20 @@ import { json, error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import { requireBearerOrAdmin } from "$lib/server/api-auth"; -import { getSkill, updateSkill, deleteSkill } from "$lib/server/services/skills"; +import { getSkill, updateSkill, deleteSkill, markSkillUsed } from "$lib/server/services/skills"; export const GET: RequestHandler = async ({ request, params }) => { await requireBearerOrAdmin(request); const skill = await getSkill(params.slug); if (!skill) throw error(404, "Skill not found"); + const lastUsedAt = skill.builtIn ? skill.lastUsedAt : await markSkillUsed(skill.slug); return json({ slug: skill.slug, description: skill.description, content: skill.contentMd, version: skill.version, + last_used_at: lastUsedAt?.toISOString() ?? null, }); }; diff --git a/tests/integration/mcp.test.ts b/tests/integration/mcp.test.ts index 38f4dc5..93ab3ed 100644 --- a/tests/integration/mcp.test.ts +++ b/tests/integration/mcp.test.ts @@ -190,7 +190,7 @@ describe("MCP tools", () => { await createSkill("---\nname: test-skill\ndescription: A test skill\n---\n# Test\nSome content."); const handler = createMcpToolHandler(token); - const result = await handler("org_skill_list", {}) as Array<{ slug: string; description: string; builtIn: boolean }>; + const result = await handler("org_skill_list", {}) as Array<{ slug: string; description: string; last_used_at: Date | null; builtIn: boolean }>; expect(Array.isArray(result)).toBe(true); // Includes built-in skills + 1 DB skill @@ -198,6 +198,7 @@ describe("MCP tools", () => { expect(dbSkills).toHaveLength(1); expect(dbSkills[0].slug).toBe("test-skill"); expect(dbSkills[0].description).toBe("A test skill"); + expect(dbSkills[0].last_used_at).toBeNull(); }); }); @@ -208,12 +209,17 @@ describe("MCP tools", () => { await createSkill(content); const handler = createMcpToolHandler(token); - const result = await handler("org_skill_read", { slug: "my-skill" }) as { slug: string; description: string; content: string; version: number }; + const result = await handler("org_skill_read", { slug: "my-skill" }) as { slug: string; description: string; content: string; version: number; last_used_at: string | null }; expect(result.slug).toBe("my-skill"); expect(result.description).toBe("My skill description"); expect(result.content).toBe(content); expect(result.version).toBe(1); + expect(result.last_used_at).toEqual(expect.any(String)); + + const list = await handler("org_skill_list", {}) as Array<{ slug: string; last_used_at: Date | null; builtIn: boolean }>; + const skill = list.find((s) => s.slug === "my-skill" && !s.builtIn); + expect(skill?.last_used_at).toBeInstanceOf(Date); }); it("returns error for nonexistent slug", async () => { diff --git a/tests/integration/skills.test.ts b/tests/integration/skills.test.ts index d73c6ee..047488c 100644 --- a/tests/integration/skills.test.ts +++ b/tests/integration/skills.test.ts @@ -54,7 +54,7 @@ describe("skills service", () => { await expect(createSkill(validSkillMd)).rejects.toThrow(); }); - it("lists skills with slug and description only", async () => { + it("lists skills with slug, description and last_used_at only", async () => { const { createSkill, listSkills } = await import("$lib/server/services/skills"); await createSkill(validSkillMd); await createSkill(anotherSkillMd); @@ -66,6 +66,7 @@ describe("skills service", () => { expect(dbSkills).toHaveLength(2); expect(list[0]).toHaveProperty("slug"); expect(list[0]).toHaveProperty("description"); + expect(list[0]).toHaveProperty("last_used_at"); expect(list[0]).not.toHaveProperty("contentMd"); }); @@ -79,6 +80,18 @@ describe("skills service", () => { expect(skill!.contentMd).toBe(validSkillMd); }); + it("tracks last_used_at when a skill is marked as used", async () => { + const { createSkill, getSkill, markSkillUsed } = await import("$lib/server/services/skills"); + await createSkill(validSkillMd); + + expect((await getSkill("deploy-hotfix"))!.lastUsedAt).toBeNull(); + + const lastUsedAt = await markSkillUsed("deploy-hotfix"); + + expect(lastUsedAt).toBeInstanceOf(Date); + expect((await getSkill("deploy-hotfix"))!.lastUsedAt).toBeInstanceOf(Date); + }); + it("returns null for non-existent skill", async () => { const { getSkill } = await import("$lib/server/services/skills"); const skill = await getSkill("non-existent");