From b19d0b6f3b3295f2fc96b01208bd36db02e98411 Mon Sep 17 00:00:00 2001 From: JonCSykes Date: Fri, 6 Mar 2026 16:39:35 -0500 Subject: [PATCH 01/51] Add support for company logos, including schema adjustments, validation, assets handling, and UI display enhancements. --- .gitignore | 4 +- cli/src/__tests__/company-delete.test.ts | 1 + docs/api/companies.md | 19 + .../src/migrations/0026_high_anita_blake.sql | 1 + .../db/src/migrations/meta/0026_snapshot.json | 5855 +++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/companies.ts | 1 + packages/shared/src/types/company.ts | 1 + packages/shared/src/validators/company.ts | 10 + pnpm-lock.yaml | 21 +- server/src/__tests__/assets.test.ts | 157 + server/src/routes/assets.ts | 9 +- ui/src/api/companies.ts | 9 +- ui/src/components/CompanyPatternIcon.tsx | 32 +- ui/src/components/CompanyRail.tsx | 1 + ui/src/context/CompanyContext.tsx | 15 +- ui/src/pages/CompanySettings.tsx | 94 +- 17 files changed, 6211 insertions(+), 26 deletions(-) create mode 100644 packages/db/src/migrations/0026_high_anita_blake.sql create mode 100644 packages/db/src/migrations/meta/0026_snapshot.json create mode 100644 server/src/__tests__/assets.test.ts diff --git a/.gitignore b/.gitignore index 9d9f5e35..6b68f737 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ tmp/ *.tmp .vscode/ .claude/settings.local.json -.paperclip-local/ \ No newline at end of file +.paperclip-local/ +/.idea/ +/.agents/ diff --git a/cli/src/__tests__/company-delete.test.ts b/cli/src/__tests__/company-delete.test.ts index 6858a3d1..65e5a021 100644 --- a/cli/src/__tests__/company-delete.test.ts +++ b/cli/src/__tests__/company-delete.test.ts @@ -14,6 +14,7 @@ function makeCompany(overrides: Partial): Company { spentMonthlyCents: 0, requireBoardApprovalForNewAgents: false, brandColor: null, + logoUrl: null, createdAt: new Date(), updatedAt: new Date(), ...overrides, diff --git a/docs/api/companies.md b/docs/api/companies.md index a0aafae5..4b92fe1e 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -42,6 +42,24 @@ PATCH /api/companies/{companyId} } ``` +## Upload Company Logo + +Upload an image for a company icon and store it as that company’s logo. + +``` +POST /api/companies/{companyId}/assets/images +Content-Type: multipart/form-data +``` + +Valid image content types: + +- `image/png` +- `image/jpeg` +- `image/jpg` +- `image/webp` +- `image/gif` +- `image/svg+xml` (`.svg`) + ## Archive Company ``` @@ -58,6 +76,7 @@ Archives a company. Archived companies are hidden from default listings. | `name` | string | Company name | | `description` | string | Company description | | `status` | string | `active`, `paused`, `archived` | +| `logoUrl` | string | Optional path or URL for the logo image | | `budgetMonthlyCents` | number | Monthly budget limit | | `createdAt` | string | ISO timestamp | | `updatedAt` | string | ISO timestamp | diff --git a/packages/db/src/migrations/0026_high_anita_blake.sql b/packages/db/src/migrations/0026_high_anita_blake.sql new file mode 100644 index 00000000..17be3222 --- /dev/null +++ b/packages/db/src/migrations/0026_high_anita_blake.sql @@ -0,0 +1 @@ +ALTER TABLE "companies" ADD COLUMN IF NOT EXISTS "logo_url" text; diff --git a/packages/db/src/migrations/meta/0026_snapshot.json b/packages/db/src/migrations/meta/0026_snapshot.json new file mode 100644 index 00000000..400b7eb5 --- /dev/null +++ b/packages/db/src/migrations/meta/0026_snapshot.json @@ -0,0 +1,5855 @@ +{ + "id": "ada32d9a-1735-4149-91c7-83ae8f5dd482", + "prevId": "bd8d9b8d-3012-4c58-bcfd-b3215c164f82", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "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": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "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": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "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": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "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": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "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": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "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": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "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 + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "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": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "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": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "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": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "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": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "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": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "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": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "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": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "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": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": 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": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "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": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "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": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "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": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index c3e25050..22a695b4 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1772807461603, "tag": "0025_nasty_salo", "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1772823634634, + "tag": "0026_high_anita_blake", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/companies.ts b/packages/db/src/schema/companies.ts index 29c82b71..3e75d4e6 100644 --- a/packages/db/src/schema/companies.ts +++ b/packages/db/src/schema/companies.ts @@ -15,6 +15,7 @@ export const companies = pgTable( .notNull() .default(true), brandColor: text("brand_color"), + logoUrl: text("logo_url"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, diff --git a/packages/shared/src/types/company.ts b/packages/shared/src/types/company.ts index 435be80d..3002c7d3 100644 --- a/packages/shared/src/types/company.ts +++ b/packages/shared/src/types/company.ts @@ -11,6 +11,7 @@ export interface Company { spentMonthlyCents: number; requireBoardApprovalForNewAgents: boolean; brandColor: string | null; + logoUrl: string | null; createdAt: Date; updatedAt: Date; } diff --git a/packages/shared/src/validators/company.ts b/packages/shared/src/validators/company.ts index 407d2ae4..65083c91 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -1,10 +1,19 @@ import { z } from "zod"; import { COMPANY_STATUSES } from "../constants.js"; +const logoUrlSchema = z + .string() + .trim() + .max(2048) + .regex(/^\/api\/assets\/[^\s]+$|^https?:\/\/[^\s]+$/) + .nullable() + .optional(); + export const createCompanySchema = z.object({ name: z.string().min(1), description: z.string().optional().nullable(), budgetMonthlyCents: z.number().int().nonnegative().optional().default(0), + logoUrl: logoUrlSchema, }); export type CreateCompany = z.infer; @@ -16,6 +25,7 @@ export const updateCompanySchema = createCompanySchema spentMonthlyCents: z.number().int().nonnegative().optional(), requireBoardApprovalForNewAgents: z.boolean().optional(), brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(), + logoUrl: logoUrlSchema, }); export type UpdateCompany = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492cd35a..b9b5f3e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: specifier: ^1.1.1 version: 1.1.1 devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -156,8 +159,8 @@ importers: version: 1.1.1 devDependencies: '@types/node': - specifier: ^22.12.0 - version: 22.19.11 + specifier: ^24.6.0 + version: 24.12.0 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -8162,7 +8165,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@types/chai@5.2.3': dependencies: @@ -8171,7 +8174,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@types/cookiejar@2.1.5': {} @@ -8189,7 +8192,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -8246,18 +8249,18 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 24.12.0 + '@types/node': 25.2.3 form-data: 4.0.5 '@types/supertest@6.0.3': @@ -8271,7 +8274,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.12.0 + '@types/node': 25.2.3 '@ungap/structured-clone@1.3.0': {} diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts new file mode 100644 index 00000000..eb58e24a --- /dev/null +++ b/server/src/__tests__/assets.test.ts @@ -0,0 +1,157 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import express from "express"; +import request from "supertest"; +import { assetRoutes } from "../routes/assets.js"; +import type { StorageService } from "../storage/types.js"; + +const { createAssetMock, getAssetByIdMock, logActivityMock } = vi.hoisted(() => ({ + createAssetMock: vi.fn(), + getAssetByIdMock: vi.fn(), + logActivityMock: vi.fn(), +})); + +vi.mock("../services/index.js", () => ({ + assetService: vi.fn(() => ({ + create: createAssetMock, + getById: getAssetByIdMock, + })), + logActivity: logActivityMock, +})); + +function createAsset() { + const now = new Date("2026-01-01T00:00:00.000Z"); + return { + id: "asset-1", + companyId: "company-1", + provider: "local", + objectKey: "assets/abc", + contentType: "image/svg+xml", + byteSize: 40, + sha256: "sha256-sample", + originalFilename: "logo.svg", + createdByAgentId: null, + createdByUserId: "user-1", + createdAt: now, + updatedAt: now, + }; +} + +function createStorageService(contentType = "image/svg+xml"): StorageService { + const putFile: StorageService["putFile"] = vi.fn(async (input: { + companyId: string; + namespace: string; + originalFilename: string | null; + contentType: string; + body: Buffer; + }) => { + return { + provider: "local_disk" as const, + objectKey: `${input.namespace}/${input.originalFilename ?? "upload"}`, + contentType: contentType || input.contentType, + byteSize: input.body.length, + sha256: "sha256-sample", + originalFilename: input.originalFilename, + }; + }); + + return { + provider: "local_disk" as const, + putFile, + getObject: vi.fn(), + headObject: vi.fn(), + deleteObject: vi.fn(), + }; +} + +function createApp(storage: ReturnType) { + const app = express(); + app.use((req, _res, next) => { + req.actor = { + type: "board", + source: "local_implicit", + userId: "user-1", + }; + next(); + }); + app.use("/api", assetRoutes({} as any, storage)); + return app; +} + +describe("POST /api/companies/:companyId/assets/images", () => { + afterEach(() => { + createAssetMock.mockReset(); + getAssetByIdMock.mockReset(); + logActivityMock.mockReset(); + }); + + it("accepts SVG image uploads and returns an asset path", async () => { + const svg = createStorageService("image/svg+xml"); + const app = createApp(svg); + + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "companies") + .attach("file", Buffer.from(""), "logo.svg"); + + expect(res.status).toBe(201); + expect(res.body.contentPath).toBe("/api/assets/asset-1/content"); + expect(createAssetMock).toHaveBeenCalledTimes(1); + expect(svg.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/companies", + originalFilename: "logo.svg", + contentType: "image/svg+xml", + body: expect.any(Buffer), + }); + }); + + it("rejects files larger than 100 KB", async () => { + const app = createApp(createStorageService()); + createAssetMock.mockResolvedValue(createAsset()); + + const file = Buffer.alloc(100 * 1024 + 1, "a"); + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "companies") + .attach("file", file, "too-large.png"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe("Image exceeds 102400 bytes"); + }); + + it("allows larger non-logo images within the general asset limit", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "image/png", + originalFilename: "goal.png", + }); + + const file = Buffer.alloc(150 * 1024, "a"); + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "goals") + .attach("file", file, "goal.png"); + + expect(res.status).toBe(201); + expect(createAssetMock).toHaveBeenCalledTimes(1); + }); + + it("rejects unsupported image types", async () => { + const app = createApp(createStorageService("text/plain")); + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "companies") + .attach("file", Buffer.from("not an image"), "note.txt"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe("Unsupported image type: text/plain"); + expect(createAssetMock).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index cde29ada..0b1b81cb 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -7,12 +7,14 @@ import { assetService, logActivity } from "../services/index.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; const MAX_ASSET_IMAGE_BYTES = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024; +const MAX_COMPANY_LOGO_BYTES = 100 * 1024; const ALLOWED_IMAGE_CONTENT_TYPES = new Set([ "image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif", + "image/svg+xml", ]); export function assetRoutes(db: Db, storage: StorageService) { @@ -73,6 +75,12 @@ export function assetRoutes(db: Db, storage: StorageService) { } const namespaceSuffix = parsedMeta.data.namespace ?? "general"; + const isCompanyLogoNamespace = namespaceSuffix === "companies" || namespaceSuffix.startsWith("companies/"); + if (isCompanyLogoNamespace && file.buffer.length > MAX_COMPANY_LOGO_BYTES) { + res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); + return; + } + const actor = getActorInfo(req); const stored = await storage.putFile({ companyId, @@ -150,4 +158,3 @@ export function assetRoutes(db: Db, storage: StorageService) { return router; } - diff --git a/ui/src/api/companies.ts b/ui/src/api/companies.ts index 583d9e69..eb53524b 100644 --- a/ui/src/api/companies.ts +++ b/ui/src/api/companies.ts @@ -14,14 +14,19 @@ export const companiesApi = { list: () => api.get("/companies"), get: (companyId: string) => api.get(`/companies/${companyId}`), stats: () => api.get("/companies/stats"), - create: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => + create: (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + logoUrl?: string | null; + }) => api.post("/companies", data), update: ( companyId: string, data: Partial< Pick< Company, - "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" + "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" | "logoUrl" > >, ) => api.patch(`/companies/${companyId}`, data), diff --git a/ui/src/components/CompanyPatternIcon.tsx b/ui/src/components/CompanyPatternIcon.tsx index c7e5acc3..6ea40788 100644 --- a/ui/src/components/CompanyPatternIcon.tsx +++ b/ui/src/components/CompanyPatternIcon.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { cn } from "../lib/utils"; const BAYER_4X4 = [ @@ -10,6 +10,7 @@ const BAYER_4X4 = [ interface CompanyPatternIconProps { companyName: string; + logoUrl?: string | null; brandColor?: string | null; className?: string; } @@ -159,8 +160,18 @@ function makeCompanyPatternDataUrl(seed: string, brandColor?: string | null, log return canvas.toDataURL("image/png"); } -export function CompanyPatternIcon({ companyName, brandColor, className }: CompanyPatternIconProps) { +export function CompanyPatternIcon({ + companyName, + logoUrl, + brandColor, + className, +}: CompanyPatternIconProps) { const initial = companyName.trim().charAt(0).toUpperCase() || "?"; + const [imageError, setImageError] = useState(false); + const logo = !imageError && typeof logoUrl === "string" && logoUrl.trim().length > 0 ? logoUrl : null; + useEffect(() => { + setImageError(false); + }, [logoUrl]); const patternDataUrl = useMemo( () => makeCompanyPatternDataUrl(companyName.trim().toLowerCase(), brandColor), [companyName, brandColor], @@ -173,7 +184,14 @@ export function CompanyPatternIcon({ companyName, brandColor, className }: Compa className, )} > - {patternDataUrl ? ( + {logo ? ( + {`${companyName} setImageError(true)} + className="absolute inset-0 h-full w-full object-cover" + /> + ) : patternDataUrl ? ( )} - - {initial} - + {!logo && ( + + {initial} + + )} ); } diff --git a/ui/src/components/CompanyRail.tsx b/ui/src/components/CompanyRail.tsx index 62a8bf3e..46a6d155 100644 --- a/ui/src/components/CompanyRail.tsx +++ b/ui/src/components/CompanyRail.tsx @@ -121,6 +121,7 @@ function SortableCompanyItem({ > Promise; } @@ -86,7 +87,12 @@ export function CompanyProvider({ children }: { children: ReactNode }) { }, [queryClient]); const createMutation = useMutation({ - mutationFn: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => + mutationFn: (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + logoUrl?: string | null; + }) => companiesApi.create(data), onSuccess: (company) => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); @@ -95,7 +101,12 @@ export function CompanyProvider({ children }: { children: ReactNode }) { }); const createCompany = useCallback( - async (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => { + async (data: { + name: string; + description?: string | null; + budgetMonthlyCents?: number; + logoUrl?: string | null; + }) => { return createMutation.mutateAsync(data); }, [createMutation], diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 0b9cd255..7dd51da0 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { companiesApi } from "../api/companies"; import { accessApi } from "../api/access"; +import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import { Settings, Check } from "lucide-react"; @@ -34,6 +35,8 @@ export function CompanySettings() { const [companyName, setCompanyName] = useState(""); const [description, setDescription] = useState(""); const [brandColor, setBrandColor] = useState(""); + const [logoUrl, setLogoUrl] = useState(""); + const [logoUploadError, setLogoUploadError] = useState(null); // Sync local state from selected company useEffect(() => { @@ -41,6 +44,7 @@ export function CompanySettings() { setCompanyName(selectedCompany.name); setDescription(selectedCompany.description ?? ""); setBrandColor(selectedCompany.brandColor ?? ""); + setLogoUrl(selectedCompany.logoUrl ?? ""); }, [selectedCompany]); const [inviteError, setInviteError] = useState(null); @@ -130,6 +134,46 @@ export function CompanySettings() { } }); + const syncLogoState = (nextLogoUrl: string | null) => { + setLogoUrl(nextLogoUrl ?? ""); + void queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); + }; + + const logoUploadMutation = useMutation({ + mutationFn: (file: File) => + assetsApi + .uploadImage(selectedCompanyId!, file, "companies") + .then((asset) => companiesApi.update(selectedCompanyId!, { logoUrl: asset.contentPath })), + onSuccess: (company) => { + syncLogoState(company.logoUrl); + setLogoUploadError(null); + } + }); + + const clearLogoMutation = useMutation({ + mutationFn: () => companiesApi.update(selectedCompanyId!, { logoUrl: null }), + onSuccess: (company) => { + setLogoUploadError(null); + syncLogoState(company.logoUrl); + } + }); + + function handleLogoFileChange(event: ChangeEvent) { + const file = event.target.files?.[0] ?? null; + event.currentTarget.value = ""; + if (!file) return; + if (file.size >= 100 * 1024) { + setLogoUploadError("Logo image must be smaller than 100 KB."); + return; + } + setLogoUploadError(null); + logoUploadMutation.mutate(file); + } + + function handleClearLogo() { + clearLogoMutation.mutate(); + } + useEffect(() => { setInviteError(null); setInviteSnippet(null); @@ -226,11 +270,53 @@ export function CompanySettings() {
-
+
+ +
+ + {logoUrl && ( +
+ +
+ )} + {(logoUploadMutation.isError || logoUploadError) && ( + + {logoUploadError ?? + (logoUploadMutation.error instanceof Error + ? logoUploadMutation.error.message + : "Logo upload failed")} + + )} + {clearLogoMutation.isError && ( + + {clearLogoMutation.error.message} + + )} + {logoUploadMutation.isPending && ( + Uploading logo... + )} +
+
- {generalMutation.error instanceof Error - ? generalMutation.error.message - : "Failed to save"} + {generalMutation.error.message} )}
From 1448b55ca42509c186f38b942994bb3e4337c4e7 Mon Sep 17 00:00:00 2001 From: JonCSykes Date: Fri, 6 Mar 2026 16:47:04 -0500 Subject: [PATCH 02/51] Improve error handling in CompanySettings for mutation failure messages. --- ui/src/pages/CompanySettings.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 7dd51da0..8d68fcff 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -372,7 +372,9 @@ export function CompanySettings() { )} {generalMutation.isError && ( - {generalMutation.error.message} + {generalMutation.error instanceof Error + ? generalMutation.error.message + : "Failed to save"} )}
From a4702e48f976351f8ca0f3d29a41bba9564cf011 Mon Sep 17 00:00:00 2001 From: JonCSykes Date: Fri, 6 Mar 2026 17:18:43 -0500 Subject: [PATCH 03/51] Add sanitization for SVG uploads and enhance security headers for asset responses - Introduced SVG sanitization using `dompurify` to prevent malicious content. - Updated tests to validate SVG sanitization with various scenarios. - Enhanced response headers for assets, adding CSP and nosniff for SVGs. - Adjusted UI to better clarify supported file types for logo uploads. - Updated dependencies to include `jsdom` and `dompurify`. --- docs/api/companies.md | 2 +- pnpm-lock.yaml | 429 +++++++++++++++++++++++++++- server/package.json | 5 +- server/src/__tests__/assets.test.ts | 68 ++++- server/src/routes/assets.ts | 93 +++++- ui/src/pages/CompanySettings.tsx | 4 +- 6 files changed, 569 insertions(+), 32 deletions(-) diff --git a/docs/api/companies.md b/docs/api/companies.md index 4b92fe1e..efcac8c2 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -58,7 +58,7 @@ Valid image content types: - `image/jpg` - `image/webp` - `image/gif` -- `image/svg+xml` (`.svg`) +- `image/svg+xml` ## Archive Company diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9b5f3e0..053c76c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) cli: dependencies: @@ -126,9 +126,6 @@ importers: specifier: ^1.1.1 version: 1.1.1 devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -191,7 +188,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) packages/shared: dependencies: @@ -234,10 +231,13 @@ importers: version: link:../packages/shared better-auth: specifier: 1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) detect-port: specifier: ^2.1.0 version: 2.1.0 + dompurify: + specifier: ^3.3.2 + version: 3.3.2 dotenv: specifier: ^17.0.1 version: 17.3.1 @@ -250,6 +250,9 @@ importers: express: specifier: ^5.1.0 version: 5.2.1 + jsdom: + specifier: ^28.1.0 + version: 28.1.0(@noble/hashes@2.0.1) multer: specifier: ^2.0.2 version: 2.0.2 @@ -278,6 +281,9 @@ importers: '@types/express-serve-static-core': specifier: ^5.0.0 version: 5.1.1 + '@types/jsdom': + specifier: ^28.0.0 + version: 28.0.0 '@types/multer': specifier: ^2.0.0 version: 2.0.0 @@ -304,7 +310,7 @@ importers: version: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) ui: dependencies: @@ -410,10 +416,23 @@ importers: version: 6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -689,6 +708,10 @@ packages: '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -855,6 +878,37 @@ packages: react: ^16.8.0 || ^17 || ^18 || ^19 react-dom: ^16.8.0 || ^17 || ^18 || ^19 + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.0': + resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -1383,6 +1437,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -2825,6 +2888,9 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/jsdom@28.0.0': + resolution: {integrity: sha512-A8TBQQC/xAOojy9kM8E46cqT00sF0h7dWjV8t8BJhUi2rG6JRh7XXQo/oLoENuZIQEpXsxLccLCnknyQd7qssQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2875,6 +2941,12 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2940,6 +3012,10 @@ packages: resolution: {integrity: sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==} engines: {node: '>= 16.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + anser@2.3.5: resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} @@ -3070,6 +3146,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -3232,11 +3311,19 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -3244,6 +3331,10 @@ packages: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -3256,6 +3347,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -3320,6 +3414,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dompurify@3.3.2: + resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} + engines: {node: '>=20'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -3458,6 +3556,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -3700,6 +3802,10 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3707,6 +3813,14 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -3778,6 +3892,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3824,6 +3941,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3940,6 +4066,10 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4019,6 +4149,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4270,6 +4403,12 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4422,6 +4561,10 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -4567,6 +4710,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -4611,6 +4758,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4766,6 +4917,9 @@ packages: resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -4808,6 +4962,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.25: + resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==} + + tldts@7.0.25: + resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4816,6 +4977,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4855,6 +5024,13 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.22.0: + resolution: {integrity: sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==} + + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + unidiff@1.0.4: resolution: {integrity: sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ==} @@ -5052,6 +5228,22 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5081,6 +5273,13 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -5103,6 +5302,26 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + + '@asamuzakjp/css-color@5.0.1': + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.6 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.6 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -5731,6 +5950,10 @@ snapshots: '@better-fetch/fetch@1.1.21': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 @@ -6179,6 +6402,28 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-is: 17.0.2 + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.0': {} + + '@csstools/css-tokenizer@4.0.0': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -6465,6 +6710,10 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)': + optionalDependencies: + '@noble/hashes': 2.0.1 + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -8209,6 +8458,13 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/jsdom@28.0.0': + dependencies: + '@types/node': 25.2.3 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + undici-types: 7.22.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -8268,6 +8524,11 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/tough-cookie@4.0.5': {} + + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8353,6 +8614,8 @@ snapshots: address@2.0.3: {} + agent-base@7.1.4: {} + anser@2.3.5: {} ansi-colors@4.1.3: {} @@ -8389,7 +8652,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -8409,7 +8672,7 @@ snapshots: pg: 8.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) better-call@1.1.8(zod@4.3.6): dependencies: @@ -8424,6 +8687,10 @@ snapshots: dependencies: is-windows: 1.0.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -8587,8 +8854,20 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + cssesc@3.0.0: {} + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.0.1 + '@csstools/css-syntax-patches-for-csstree': 1.1.0 + css-tree: 3.2.1 + lru-cache: 11.2.6 + csstype@3.2.3: {} d@1.0.2: @@ -8596,12 +8875,21 @@ snapshots: es5-ext: 0.10.64 type: 2.7.3 + data-urls@7.0.0(@noble/hashes@2.0.1): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + dateformat@4.6.3: {} debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -8650,6 +8938,10 @@ snapshots: dependencies: path-type: 4.0.0 + dompurify@3.3.2: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv@16.6.1: {} dotenv@17.3.1: {} @@ -8723,6 +9015,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@6.0.1: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -9092,6 +9386,12 @@ snapshots: help-me@5.0.0: {} + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + html-url-attributes@3.0.1: {} http-errors@2.0.1: @@ -9102,6 +9402,20 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-id@4.1.3: {} iconv-lite@0.7.2: @@ -9149,6 +9463,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-subdir@1.2.0: @@ -9184,6 +9500,33 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@28.1.0(@noble/hashes@2.0.1): + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) + cssstyle: 6.2.0 + data-urls: 7.0.0(@noble/hashes@2.0.1) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + undici: 7.22.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + jsesc@3.1.0: {} json5@2.2.3: {} @@ -9265,6 +9608,8 @@ snapshots: loupe@3.2.1: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -9475,6 +9820,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -9881,6 +10228,14 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -10034,6 +10389,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -10250,6 +10607,8 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -10315,6 +10674,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -10490,6 +10853,8 @@ snapshots: transitivePeerDependencies: - supports-color + symbol-tree@3.2.4: {} + tabbable@6.4.0: {} tailwind-merge@3.4.1: {} @@ -10519,12 +10884,26 @@ snapshots: tinyspy@4.0.4: {} + tldts-core@7.0.25: {} + + tldts@7.0.25: + dependencies: + tldts-core: 7.0.25 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.25 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -10559,6 +10938,10 @@ snapshots: undici-types@7.16.0: {} + undici-types@7.22.0: {} + + undici@7.22.0: {} + unidiff@1.0.4: dependencies: diff: 5.2.2 @@ -10752,7 +11135,7 @@ snapshots: lightningcss: 1.30.2 tsx: 4.21.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -10780,6 +11163,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.12.0 + jsdom: 28.1.0(@noble/hashes@2.0.1) transitivePeerDependencies: - jiti - less @@ -10794,7 +11178,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -10822,6 +11206,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 25.2.3 + jsdom: 28.1.0(@noble/hashes@2.0.1) transitivePeerDependencies: - jiti - less @@ -10838,6 +11223,22 @@ snapshots: w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which@2.0.2: dependencies: isexe: 2.0.0 @@ -10856,6 +11257,10 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} yallist@3.1.1: {} diff --git a/server/package.json b/server/package.json index 2e470111..cf68566b 100644 --- a/server/package.json +++ b/server/package.json @@ -34,17 +34,19 @@ "@paperclipai/adapter-claude-local": "workspace:*", "@paperclipai/adapter-codex-local": "workspace:*", "@paperclipai/adapter-cursor-local": "workspace:*", - "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-openclaw": "workspace:*", + "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", "better-auth": "1.4.18", "detect-port": "^2.1.0", + "dompurify": "^3.3.2", "dotenv": "^17.0.1", "drizzle-orm": "^0.38.4", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", + "jsdom": "^28.1.0", "multer": "^2.0.2", "open": "^11.0.0", "pino": "^9.6.0", @@ -56,6 +58,7 @@ "devDependencies": { "@types/express": "^5.0.0", "@types/express-serve-static-core": "^5.0.0", + "@types/jsdom": "^28.0.0", "@types/multer": "^2.0.0", "@types/node": "^24.6.0", "@types/supertest": "^6.0.2", diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index eb58e24a..85ddf56a 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -25,10 +25,10 @@ function createAsset() { companyId: "company-1", provider: "local", objectKey: "assets/abc", - contentType: "image/svg+xml", + contentType: "image/png", byteSize: 40, sha256: "sha256-sample", - originalFilename: "logo.svg", + originalFilename: "logo.png", createdByAgentId: null, createdByUserId: "user-1", createdAt: now, @@ -36,7 +36,7 @@ function createAsset() { }; } -function createStorageService(contentType = "image/svg+xml"): StorageService { +function createStorageService(contentType = "image/png"): StorageService { const putFile: StorageService["putFile"] = vi.fn(async (input: { companyId: string; namespace: string; @@ -84,29 +84,63 @@ describe("POST /api/companies/:companyId/assets/images", () => { logActivityMock.mockReset(); }); - it("accepts SVG image uploads and returns an asset path", async () => { - const svg = createStorageService("image/svg+xml"); - const app = createApp(svg); + it("accepts PNG image uploads and returns an asset path", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); createAssetMock.mockResolvedValue(createAsset()); const res = await request(app) .post("/api/companies/company-1/assets/images") .field("namespace", "companies") - .attach("file", Buffer.from(""), "logo.svg"); + .attach("file", Buffer.from("png"), "logo.png"); expect(res.status).toBe(201); expect(res.body.contentPath).toBe("/api/assets/asset-1/content"); expect(createAssetMock).toHaveBeenCalledTimes(1); - expect(svg.putFile).toHaveBeenCalledWith({ + expect(png.putFile).toHaveBeenCalledWith({ companyId: "company-1", namespace: "assets/companies", - originalFilename: "logo.svg", - contentType: "image/svg+xml", + originalFilename: "logo.png", + contentType: "image/png", body: expect.any(Buffer), }); }); + it("sanitizes SVG image uploads before storing them", async () => { + const svg = createStorageService("image/svg+xml"); + const app = createApp(svg); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "image/svg+xml", + originalFilename: "logo.svg", + }); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "companies") + .attach( + "file", + Buffer.from( + "", + ), + "logo.svg", + ); + + expect(res.status).toBe(201); + expect(svg.putFile).toHaveBeenCalledTimes(1); + const stored = (svg.putFile as ReturnType).mock.calls[0]?.[0]; + expect(stored.contentType).toBe("image/svg+xml"); + expect(stored.originalFilename).toBe("logo.svg"); + const body = stored.body.toString("utf8"); + expect(body).toContain(" { const app = createApp(createStorageService()); createAssetMock.mockResolvedValue(createAsset()); @@ -154,4 +188,18 @@ describe("POST /api/companies/:companyId/assets/images", () => { expect(res.body.error).toBe("Unsupported image type: text/plain"); expect(createAssetMock).not.toHaveBeenCalled(); }); + + it("rejects SVG image uploads that cannot be sanitized", async () => { + const app = createApp(createStorageService("image/svg+xml")); + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "companies") + .attach("file", Buffer.from("not actually svg"), "logo.svg"); + + expect(res.status).toBe(422); + expect(res.body.error).toBe("SVG could not be sanitized"); + expect(createAssetMock).not.toHaveBeenCalled(); + }); }); diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index 0b1b81cb..7af319e0 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -1,5 +1,7 @@ import { Router, type Request, type Response } from "express"; import multer from "multer"; +import createDOMPurify from "dompurify"; +import { JSDOM } from "jsdom"; import type { Db } from "@paperclipai/db"; import { createAssetImageMetadataSchema } from "@paperclipai/shared"; import type { StorageService } from "../storage/types.js"; @@ -8,15 +10,80 @@ import { assertCompanyAccess, getActorInfo } from "./authz.js"; const MAX_ASSET_IMAGE_BYTES = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024; const MAX_COMPANY_LOGO_BYTES = 100 * 1024; +const SVG_CONTENT_TYPE = "image/svg+xml"; const ALLOWED_IMAGE_CONTENT_TYPES = new Set([ "image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif", - "image/svg+xml", + SVG_CONTENT_TYPE, ]); +function sanitizeSvgBuffer(input: Buffer): Buffer | null { + const raw = input.toString("utf8").trim(); + if (!raw) return null; + + const baseDom = new JSDOM(""); + const domPurify = createDOMPurify( + baseDom.window as unknown as Parameters[0], + ); + domPurify.addHook("uponSanitizeAttribute", (_node, data) => { + const attrName = data.attrName.toLowerCase(); + const attrValue = (data.attrValue ?? "").trim(); + + if (attrName.startsWith("on")) { + data.keepAttr = false; + return; + } + + if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) { + data.keepAttr = false; + } + }); + + let parsedDom: JSDOM | null = null; + try { + const sanitized = domPurify.sanitize(raw, { + USE_PROFILES: { svg: true, svgFilters: true, html: false }, + FORBID_TAGS: ["script", "foreignObject"], + FORBID_CONTENTS: ["script", "foreignObject"], + RETURN_TRUSTED_TYPE: false, + }); + + parsedDom = new JSDOM(sanitized, { contentType: SVG_CONTENT_TYPE }); + const document = parsedDom.window.document; + const root = document.documentElement; + if (!root || root.tagName.toLowerCase() !== "svg") return null; + + for (const el of Array.from(root.querySelectorAll("script, foreignObject"))) { + el.remove(); + } + for (const el of Array.from(root.querySelectorAll("*"))) { + for (const attr of Array.from(el.attributes)) { + const attrName = attr.name.toLowerCase(); + const attrValue = attr.value.trim(); + if (attrName.startsWith("on")) { + el.removeAttribute(attr.name); + continue; + } + if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) { + el.removeAttribute(attr.name); + } + } + } + + const output = root.outerHTML.trim(); + if (!output || !/^]/i.test(output)) return null; + return Buffer.from(output, "utf8"); + } catch { + return null; + } finally { + parsedDom?.window.close(); + baseDom.window.close(); + } +} + export function assetRoutes(db: Db, storage: StorageService) { const router = Router(); const svc = assetService(db); @@ -58,12 +125,21 @@ export function assetRoutes(db: Db, storage: StorageService) { return; } - const contentType = (file.mimetype || "").toLowerCase(); + let contentType = (file.mimetype || "").toLowerCase(); if (!ALLOWED_IMAGE_CONTENT_TYPES.has(contentType)) { res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` }); return; } - if (file.buffer.length <= 0) { + let fileBody = file.buffer; + if (contentType === SVG_CONTENT_TYPE) { + const sanitized = sanitizeSvgBuffer(file.buffer); + if (!sanitized || sanitized.length <= 0) { + res.status(422).json({ error: "SVG could not be sanitized" }); + return; + } + fileBody = sanitized; + } + if (fileBody.length <= 0) { res.status(422).json({ error: "Image is empty" }); return; } @@ -76,7 +152,7 @@ export function assetRoutes(db: Db, storage: StorageService) { const namespaceSuffix = parsedMeta.data.namespace ?? "general"; const isCompanyLogoNamespace = namespaceSuffix === "companies" || namespaceSuffix.startsWith("companies/"); - if (isCompanyLogoNamespace && file.buffer.length > MAX_COMPANY_LOGO_BYTES) { + if (isCompanyLogoNamespace && fileBody.length > MAX_COMPANY_LOGO_BYTES) { res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); return; } @@ -87,7 +163,7 @@ export function assetRoutes(db: Db, storage: StorageService) { namespace: `assets/${namespaceSuffix}`, originalFilename: file.originalname || null, contentType, - body: file.buffer, + body: fileBody, }); const asset = await svc.create(companyId, { @@ -144,9 +220,14 @@ export function assetRoutes(db: Db, storage: StorageService) { assertCompanyAccess(req, asset.companyId); const object = await storage.getObject(asset.companyId, asset.objectKey); - res.setHeader("Content-Type", asset.contentType || object.contentType || "application/octet-stream"); + const responseContentType = asset.contentType || object.contentType || "application/octet-stream"; + res.setHeader("Content-Type", responseContentType); res.setHeader("Content-Length", String(asset.byteSize || object.contentLength || 0)); res.setHeader("Cache-Control", "private, max-age=60"); + res.setHeader("X-Content-Type-Options", "nosniff"); + if (responseContentType === SVG_CONTENT_TYPE) { + res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'"); + } const filename = asset.originalFilename ?? "asset"; res.setHeader("Content-Disposition", `inline; filename=\"${filename.replaceAll("\"", "")}\"`); diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 8d68fcff..cf174c9d 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -278,12 +278,12 @@ export function CompanySettings() {
From f44efce2658484829ae96b60a38df01b946f8dd2 Mon Sep 17 00:00:00 2001 From: JonCSykes Date: Fri, 6 Mar 2026 23:15:45 -0500 Subject: [PATCH 04/51] Add `@types/node` as a devDependency in cursor-local package --- packages/adapters/cursor-local/package.json | 1 + pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/adapters/cursor-local/package.json b/packages/adapters/cursor-local/package.json index 575f9e1b..4ef66052 100644 --- a/packages/adapters/cursor-local/package.json +++ b/packages/adapters/cursor-local/package.json @@ -45,6 +45,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { + "@types/node": "^24.6.0", "typescript": "^5.7.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 053c76c5..2a6c5003 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: specifier: ^1.1.1 version: 1.1.1 devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 typescript: specifier: ^5.7.3 version: 5.9.3 From 5114c32810c7957e5126e073dc7c5e6dcf6820a7 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Mar 2026 19:01:04 +0000 Subject: [PATCH 05/51] Fix opencode-local adapter: parser, UI, CLI, and environment tests - Move costUsd to top-level return field in parseOpenCodeJsonl (out of usage) - Fix session-not-found regex to match "Session not found" pattern - Use callID for toolUseId in UI stdout parser, add status/metadata header - Fix CLI formatter: separate tool_call/tool_result lines, split step_finish - Enable createIfMissing for cwd validation in environment tests - Add empty OPENAI_API_KEY override detection - Classify ProviderModelNotFoundError as warning during model discovery - Make model discovery best-effort when no model is configured Co-Authored-By: Claude Opus 4.6 --- .../opencode-local/src/cli/format-event.ts | 26 +++--- .../opencode-local/src/server/execute.ts | 2 +- .../opencode-local/src/server/parse.test.ts | 2 +- .../opencode-local/src/server/parse.ts | 7 +- .../opencode-local/src/server/test.ts | 83 +++++++++++++++---- .../opencode-local/src/ui/parse-stdout.ts | 15 +++- 6 files changed, 100 insertions(+), 35 deletions(-) diff --git a/packages/adapters/opencode-local/src/cli/format-event.ts b/packages/adapters/opencode-local/src/cli/format-event.ts index 00d0ec76..58d2038d 100644 --- a/packages/adapters/opencode-local/src/cli/format-event.ts +++ b/packages/adapters/opencode-local/src/cli/format-event.ts @@ -74,20 +74,25 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void { if (type === "tool_use") { const part = asRecord(parsed.part); const tool = asString(part?.tool, "tool"); + const callID = asString(part?.callID); const state = asRecord(part?.state); const status = asString(state?.status); - const summary = `tool_${status || "event"}: ${tool}`; const isError = status === "error"; - console.log((isError ? pc.red : pc.yellow)(summary)); - const input = state?.input; - if (input !== undefined) { - try { - console.log(pc.gray(JSON.stringify(input, null, 2))); - } catch { - console.log(pc.gray(String(input))); + const metadata = asRecord(state?.metadata); + + console.log(pc.yellow(`tool_call: ${tool}${callID ? ` (${callID})` : ""}`)); + + if (status) { + const metaParts = [`status=${status}`]; + if (metadata) { + for (const [key, value] of Object.entries(metadata)) { + if (value !== undefined && value !== null) metaParts.push(`${key}=${value}`); + } } + console.log((isError ? pc.red : pc.gray)(`tool_result ${metaParts.join(" ")}`)); } - const output = asString(state?.output) || asString(state?.error); + + const output = (asString(state?.output) || asString(state?.error)).trim(); if (output) console.log((isError ? pc.red : pc.gray)(output)); return; } @@ -101,7 +106,8 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void { const cached = asNumber(cache?.read, 0); const cost = asNumber(part?.cost, 0); const reason = asString(part?.reason, "step"); - console.log(pc.blue(`step finished (${reason}) tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`)); + console.log(pc.blue(`step finished: reason=${reason}`)); + console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`)); return; } diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 338646b3..970896af 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -340,7 +340,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { inputTokens: 120, cachedInputTokens: 20, outputTokens: 50, - costUsd: 0.0025, }); + expect(parsed.costUsd).toBeCloseTo(0.0025, 6); expect(parsed.errorMessage).toContain("model unavailable"); }); diff --git a/packages/adapters/opencode-local/src/server/parse.ts b/packages/adapters/opencode-local/src/server/parse.ts index 5cbfa46c..96af0ed1 100644 --- a/packages/adapters/opencode-local/src/server/parse.ts +++ b/packages/adapters/opencode-local/src/server/parse.ts @@ -27,8 +27,8 @@ export function parseOpenCodeJsonl(stdout: string) { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, - costUsd: 0, }; + let costUsd = 0; for (const rawLine of stdout.split(/\r?\n/)) { const line = rawLine.trim(); @@ -56,7 +56,7 @@ export function parseOpenCodeJsonl(stdout: string) { usage.inputTokens += asNumber(tokens.input, 0); usage.cachedInputTokens += asNumber(cache.read, 0); usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0); - usage.costUsd += asNumber(part.cost, 0); + costUsd += asNumber(part.cost, 0); continue; } @@ -81,6 +81,7 @@ export function parseOpenCodeJsonl(stdout: string) { sessionId, summary: messages.join("\n\n").trim(), usage, + costUsd, errorMessage: errors.length > 0 ? errors.join("\n") : null, }; } @@ -92,7 +93,7 @@ export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): b .filter(Boolean) .join("\n"); - return /unknown\s+session|session\s+.*\s+not\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test( + return /unknown\s+session|session\b.*\bnot\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test( haystack, ); } diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index 569f0d75..ac40d456 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -59,7 +59,7 @@ export async function testEnvironment( const cwd = asString(config.cwd, process.cwd()); try { - await ensureAbsoluteDirectory(cwd, { createIfMissing: false }); + await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); checks.push({ code: "opencode_cwd_valid", level: "info", @@ -79,6 +79,17 @@ export async function testEnvironment( for (const [key, value] of Object.entries(envConfig)) { if (typeof value === "string") env[key] = value; } + + const openaiKeyOverride = "OPENAI_API_KEY" in envConfig ? asString(envConfig.OPENAI_API_KEY, "") : null; + if (openaiKeyOverride !== null && openaiKeyOverride.trim() === "") { + checks.push({ + code: "opencode_openai_api_key_missing", + level: "warn", + message: "OPENAI_API_KEY override is empty.", + hint: "The OPENAI_API_KEY override is empty. Set a valid key or remove the override.", + }); + } + const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env })); const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid"); @@ -111,7 +122,9 @@ export async function testEnvironment( checks.every((check) => check.code !== "opencode_cwd_invalid" && check.code !== "opencode_command_unresolvable"); let modelValidationPassed = false; - if (canRunProbe) { + const configuredModel = asString(config.model, "").trim(); + + if (canRunProbe && configuredModel) { try { const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv }); if (discovered.length > 0) { @@ -129,24 +142,52 @@ export async function testEnvironment( }); } } catch (err) { - checks.push({ - code: "opencode_models_discovery_failed", - level: "error", - message: err instanceof Error ? err.message : "OpenCode model discovery failed.", - hint: "Run `opencode models` manually to verify provider auth and config.", - }); + const errMsg = err instanceof Error ? err.message : String(err); + if (/ProviderModelNotFoundError/i.test(errMsg)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + detail: errMsg, + hint: "Run `opencode models` and choose an available provider/model ID.", + }); + } else { + checks.push({ + code: "opencode_models_discovery_failed", + level: "error", + message: errMsg || "OpenCode model discovery failed.", + hint: "Run `opencode models` manually to verify provider auth and config.", + }); + } + } + } else if (canRunProbe && !configuredModel) { + try { + const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv }); + if (discovered.length > 0) { + checks.push({ + code: "opencode_models_discovered", + level: "info", + message: `Discovered ${discovered.length} model(s) from OpenCode providers.`, + }); + } + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + if (/ProviderModelNotFoundError/i.test(errMsg)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + detail: errMsg, + hint: "Run `opencode models` and choose an available provider/model ID.", + }); + } } } - const configuredModel = asString(config.model, "").trim(); - if (!configuredModel) { - checks.push({ - code: "opencode_model_required", - level: "error", - message: "OpenCode requires a configured model in provider/model format.", - hint: "Set adapterConfig.model using an ID from `opencode models`.", - }); - } else if (canRunProbe) { + const modelUnavailable = checks.some((check) => check.code === "opencode_hello_probe_model_unavailable"); + if (!configuredModel && !modelUnavailable) { + // No model configured – skip model requirement if no model-related checks exist + } else if (configuredModel && canRunProbe) { try { await ensureOpenCodeModelConfiguredAndAvailable({ model: configuredModel, @@ -226,6 +267,14 @@ export async function testEnvironment( hint: "Run `opencode run --format json` manually and prompt `Respond with hello` to inspect output.", }), }); + } else if (/ProviderModelNotFoundError/i.test(authEvidence)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + ...(detail ? { detail } : {}), + hint: "Run `opencode models` and choose an available provider/model ID.", + }); } else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) { checks.push({ code: "opencode_hello_probe_auth_required", diff --git a/packages/adapters/opencode-local/src/ui/parse-stdout.ts b/packages/adapters/opencode-local/src/ui/parse-stdout.ts index dc48e5d1..2060125a 100644 --- a/packages/adapters/opencode-local/src/ui/parse-stdout.ts +++ b/packages/adapters/opencode-local/src/ui/parse-stdout.ts @@ -56,19 +56,28 @@ function parseToolUse(parsed: Record, ts: string): TranscriptEn const status = asString(state?.status); if (status !== "completed" && status !== "error") return [callEntry]; - const output = + const rawOutput = asString(state?.output) || asString(state?.error) || asString(part.title) || `${toolName} ${status}`; + const metadata = asRecord(state?.metadata); + const headerParts: string[] = [`status: ${status}`]; + if (metadata) { + for (const [key, value] of Object.entries(metadata)) { + if (value !== undefined && value !== null) headerParts.push(`${key}: ${value}`); + } + } + const content = `${headerParts.join("\n")}\n\n${rawOutput}`.trim(); + return [ callEntry, { kind: "tool_result", ts, - toolUseId: asString(part.id, toolName), - content: output, + toolUseId: asString(part.callID) || asString(part.id, toolName), + content, isError: status === "error", }, ]; From fa7acd2482631a417a0a74f189737eb89482790e Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Mar 2026 14:12:22 -0500 Subject: [PATCH 06/51] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/adapters/opencode-local/src/server/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index ac40d456..8c203266 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -59,7 +59,7 @@ export async function testEnvironment( const cwd = asString(config.cwd, process.cwd()); try { - await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); + await ensureAbsoluteDirectory(cwd, { createIfMissing: false }); checks.push({ code: "opencode_cwd_valid", level: "info", From fb684f25e93be97d5dc10b90a6a1fe3fb25336d2 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 7 Mar 2026 19:15:10 +0000 Subject: [PATCH 07/51] Address PR feedback: keep testEnvironment non-destructive, warn on swallowed errors - Update cwd test to expect an error for missing directories (matches createIfMissing: false accepted from review) - Add warn-level check for non-ProviderModelNotFoundError failures during best-effort model discovery when no model is configured Co-Authored-By: Claude Opus 4.6 --- packages/adapters/opencode-local/src/server/test.ts | 7 +++++++ .../opencode-local-adapter-environment.test.ts | 10 ++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index 8c203266..5bb7aa36 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -180,6 +180,13 @@ export async function testEnvironment( detail: errMsg, hint: "Run `opencode models` and choose an available provider/model ID.", }); + } else { + checks.push({ + code: "opencode_models_discovery_failed", + level: "warn", + message: errMsg || "OpenCode model discovery failed (best-effort, no model configured).", + hint: "Run `opencode models` manually to verify provider auth and config.", + }); } } } diff --git a/server/src/__tests__/opencode-local-adapter-environment.test.ts b/server/src/__tests__/opencode-local-adapter-environment.test.ts index c539d771..736dd9f8 100644 --- a/server/src/__tests__/opencode-local-adapter-environment.test.ts +++ b/server/src/__tests__/opencode-local-adapter-environment.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { testEnvironment } from "@paperclipai/adapter-opencode-local/server"; describe("opencode_local environment diagnostics", () => { - it("creates a missing working directory when cwd is absolute", async () => { + it("reports a missing working directory as an error when cwd is absolute", async () => { const cwd = path.join( os.tmpdir(), `paperclip-opencode-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`, @@ -23,11 +23,9 @@ describe("opencode_local environment diagnostics", () => { }, }); - expect(result.checks.some((check) => check.code === "opencode_cwd_valid")).toBe(true); - expect(result.checks.some((check) => check.level === "error")).toBe(false); - const stats = await fs.stat(cwd); - expect(stats.isDirectory()).toBe(true); - await fs.rm(path.dirname(cwd), { recursive: true, force: true }); + expect(result.checks.some((check) => check.code === "opencode_cwd_invalid")).toBe(true); + expect(result.checks.some((check) => check.level === "error")).toBe(true); + expect(result.status).toBe("fail"); }); it("treats an empty OPENAI_API_KEY override as missing", async () => { From 97d628d7847d78557605951f32141ac8ef3e6868 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Tue, 10 Mar 2026 23:12:13 -0700 Subject: [PATCH 08/51] feat: add Hermes Agent adapter (hermes_local) Adds support for Hermes Agent (https://github.com/NousResearch/hermes-agent) as a managed employee in Paperclip companies. Hermes Agent is a full-featured AI agent by Nous Research with 30+ native tools, persistent memory, session persistence, 80+ skills, MCP support, and multi-provider model access. Changes: - Add 'hermes_local' to AGENT_ADAPTER_TYPES (packages/shared) - Add @nousresearch/paperclip-adapter-hermes dependency (server) - Register hermesLocalAdapter in the adapter registry (server) The adapter package is maintained at: https://github.com/NousResearch/hermes-paperclip-adapter --- packages/shared/src/constants.ts | 1 + server/package.json | 1 + server/src/adapters/registry.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index ba75dc8e..c78d1dd0 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -30,6 +30,7 @@ export const AGENT_ADAPTER_TYPES = [ "pi_local", "cursor", "openclaw_gateway", + "hermes_local", ] as const; export type AgentAdapterType = (typeof AGENT_ADAPTER_TYPES)[number]; diff --git a/server/package.json b/server/package.json index aeb09944..63585fae 100644 --- a/server/package.json +++ b/server/package.json @@ -40,6 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", + "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 9fe536a0..571d8131 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -45,6 +45,14 @@ import { import { agentConfigurationDoc as piAgentConfigurationDoc, } from "@paperclipai/adapter-pi-local"; +import { + execute as hermesExecute, + testEnvironment as hermesTestEnvironment, +} from "@nousresearch/paperclip-adapter-hermes/server"; +import { + agentConfigurationDoc as hermesAgentConfigurationDoc, + models as hermesModels, +} from "@nousresearch/paperclip-adapter-hermes"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; @@ -111,6 +119,15 @@ const piLocalAdapter: ServerAdapterModule = { agentConfigurationDoc: piAgentConfigurationDoc, }; +const hermesLocalAdapter: ServerAdapterModule = { + type: "hermes_local", + execute: hermesExecute, + testEnvironment: hermesTestEnvironment, + models: hermesModels, + supportsLocalAgentJwt: false, + agentConfigurationDoc: hermesAgentConfigurationDoc, +}; + const adaptersByType = new Map( [ claudeLocalAdapter, @@ -119,6 +136,7 @@ const adaptersByType = new Map( piLocalAdapter, cursorLocalAdapter, openclawGatewayAdapter, + hermesLocalAdapter, processAdapter, httpAdapter, ].map((a) => [a.type, a]), From 4e354ad00d7a1592e5e8d800dababc0bc3db73af Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 17:03:49 -0700 Subject: [PATCH 09/51] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20pin=20dependency=20and=20add=20sessionCodec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin @nousresearch/paperclip-adapter-hermes to v0.1.0 tag for reproducible builds and supply-chain safety - Import and wire hermesSessionCodec into the adapter registration for structured session parameter validation (matching claude_local, codex_local, and other adapters that support session persistence) --- server/package.json | 2 +- server/src/adapters/registry.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index 63585fae..e3a9b821 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter", + "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter#v0.1.0", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 571d8131..f112f788 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -48,6 +48,7 @@ import { import { execute as hermesExecute, testEnvironment as hermesTestEnvironment, + sessionCodec as hermesSessionCodec, } from "@nousresearch/paperclip-adapter-hermes/server"; import { agentConfigurationDoc as hermesAgentConfigurationDoc, @@ -123,6 +124,7 @@ const hermesLocalAdapter: ServerAdapterModule = { type: "hermes_local", execute: hermesExecute, testEnvironment: hermesTestEnvironment, + sessionCodec: hermesSessionCodec, models: hermesModels, supportsLocalAgentJwt: false, agentConfigurationDoc: hermesAgentConfigurationDoc, From e84c0e8df2f76f3d843d7bd4824bfbc195ae1142 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Thu, 12 Mar 2026 17:23:24 -0700 Subject: [PATCH 10/51] fix: use npm package instead of GitHub URL dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Published hermes-paperclip-adapter@0.1.0 to npm registry - Replaced github:NousResearch/hermes-paperclip-adapter with hermes-paperclip-adapter ^0.1.0 (proper semver, reproducible builds) - Updated imports from @nousresearch/paperclip-adapter-hermes to hermes-paperclip-adapter - Wired in hermesSessionCodec for structured session validation Addresses both review items from greptile-apps: 1. Unpinned GitHub dependency → now a proper npm package with semver 2. Missing sessionCodec → now imported and registered --- server/package.json | 2 +- server/src/adapters/registry.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/package.json b/server/package.json index e3a9b821..dfdb46fb 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "@nousresearch/paperclip-adapter-hermes": "github:NousResearch/hermes-paperclip-adapter#v0.1.0", + "hermes-paperclip-adapter": "^0.1.0", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index f112f788..35d407d3 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -49,11 +49,11 @@ import { execute as hermesExecute, testEnvironment as hermesTestEnvironment, sessionCodec as hermesSessionCodec, -} from "@nousresearch/paperclip-adapter-hermes/server"; +} from "hermes-paperclip-adapter/server"; import { agentConfigurationDoc as hermesAgentConfigurationDoc, models as hermesModels, -} from "@nousresearch/paperclip-adapter-hermes"; +} from "hermes-paperclip-adapter"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; From 303c00b61b529d7986f7313a3bc2204289693cf9 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:22:27 -0700 Subject: [PATCH 11/51] fix(server): load .env from cwd as fallback when .paperclip/.env is missing The server only loads environment variables from .paperclip/.env, which is not the standard location users expect. When DATABASE_URL is set in a .env file in the project root (cwd), it is silently ignored, requiring users to manually export the variable. Add a fallback that loads cwd/.env after .paperclip/.env with override:false, so the Paperclip-specific env file always takes precedence but standard .env files in the project root are also picked up. Co-Authored-By: Claude Opus 4.6 Co-Authored-By: Paperclip --- server/src/config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/config.ts b/server/src/config.ts index 983eba22..0d25172c 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,5 +1,6 @@ import { readConfigFile } from "./config-file.js"; import { existsSync } from "node:fs"; +import { resolve } from "node:path"; import { config as loadDotenv } from "dotenv"; import { resolvePaperclipEnvPath } from "./paths.js"; import { @@ -27,6 +28,11 @@ if (existsSync(PAPERCLIP_ENV_FILE_PATH)) { loadDotenv({ path: PAPERCLIP_ENV_FILE_PATH, override: false, quiet: true }); } +const CWD_ENV_PATH = resolve(process.cwd(), ".env"); +if (CWD_ENV_PATH !== PAPERCLIP_ENV_FILE_PATH && existsSync(CWD_ENV_PATH)) { + loadDotenv({ path: CWD_ENV_PATH, override: false, quiet: true }); +} + type DatabaseMode = "embedded-postgres" | "postgres"; export interface Config { From 93faf6d361a1f92c174953a9a0297de8830d2dcf Mon Sep 17 00:00:00 2001 From: teknium1 Date: Fri, 13 Mar 2026 20:26:27 -0700 Subject: [PATCH 12/51] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20pin=20version,=20enable=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin hermes-paperclip-adapter to exact version 0.1.1 (was ^0.1.0). Avoids auto-pulling potentially breaking patches from a 0.x package. - Enable supportsLocalAgentJwt (was false). The adapter uses buildPaperclipEnv which passes the JWT to the child process, matching the pattern of all other local adapters. --- server/package.json | 2 +- server/src/adapters/registry.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/package.json b/server/package.json index dfdb46fb..745abfd8 100644 --- a/server/package.json +++ b/server/package.json @@ -40,7 +40,7 @@ "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "hermes-paperclip-adapter": "^0.1.0", + "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/shared": "workspace:*", diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 35d407d3..f2ca65ed 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -126,7 +126,7 @@ const hermesLocalAdapter: ServerAdapterModule = { testEnvironment: hermesTestEnvironment, sessionCodec: hermesSessionCodec, models: hermesModels, - supportsLocalAgentJwt: false, + supportsLocalAgentJwt: true, agentConfigurationDoc: hermesAgentConfigurationDoc, }; From ff4f326341680c2dcf92610d7db4149eb13c781a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:05:51 -0700 Subject: [PATCH 13/51] fix(server): use realpathSync for .env path dedup to handle symlinks realpathSync resolves symlinks and normalizes case, preventing double-loading the same .env file when paths differ only by symlink indirection or filesystem case. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index 0d25172c..6943af7a 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,5 +1,5 @@ import { readConfigFile } from "./config-file.js"; -import { existsSync } from "node:fs"; +import { existsSync, realpathSync } from "node:fs"; import { resolve } from "node:path"; import { config as loadDotenv } from "dotenv"; import { resolvePaperclipEnvPath } from "./paths.js"; @@ -29,7 +29,10 @@ if (existsSync(PAPERCLIP_ENV_FILE_PATH)) { } const CWD_ENV_PATH = resolve(process.cwd(), ".env"); -if (CWD_ENV_PATH !== PAPERCLIP_ENV_FILE_PATH && existsSync(CWD_ENV_PATH)) { +const isSameFile = existsSync(CWD_ENV_PATH) && existsSync(PAPERCLIP_ENV_FILE_PATH) + ? realpathSync(CWD_ENV_PATH) === realpathSync(PAPERCLIP_ENV_FILE_PATH) + : CWD_ENV_PATH === PAPERCLIP_ENV_FILE_PATH; +if (!isSameFile && existsSync(CWD_ENV_PATH)) { loadDotenv({ path: CWD_ENV_PATH, override: false, quiet: true }); } From b13c530024d60d4c673f7acf631dd05be9730a40 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 13:02:21 -0700 Subject: [PATCH 14/51] Refine heartbeatService to only target runs stuck in "running" state --- server/src/services/heartbeat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index bddf6a83..e924359c 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1222,11 +1222,11 @@ export function heartbeatService(db: Db) { const staleThresholdMs = opts?.staleThresholdMs ?? 0; const now = new Date(); - // Find all runs in "queued" or "running" state + // Find all runs stuck in "running" state (queued runs are legitimately waiting; resumeQueuedRuns handles them) const activeRuns = await db .select() .from(heartbeatRuns) - .where(inArray(heartbeatRuns.status, ["queued", "running"])); + .where(eq(heartbeatRuns.status, "running")); const reaped: string[] = []; From 2dc3b4df24008360ae32071752702a887fbb984f Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 13:10:01 -0700 Subject: [PATCH 15/51] Add buildPluginSdk function to build the plugin SDK during dev run --- scripts/dev-runner.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index d4e4c231..9cf5df52 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -156,6 +156,20 @@ async function maybePreflightMigrations() { await maybePreflightMigrations(); +async function buildPluginSdk() { + console.log("[paperclip] building plugin sdk..."); + const result = await runPnpm( + ["--filter", "@paperclipai/plugin-sdk", "build"], + { stdio: "inherit" }, + ); + if (result.code !== 0) { + console.error("[paperclip] plugin sdk build failed"); + process.exit(result.code); + } +} + +await buildPluginSdk(); + if (mode === "watch") { env.PAPERCLIP_MIGRATION_PROMPT = "never"; } From 6b17f7caa8985fdb2a9da39daeb1075d3aaa7c5b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 13:29:46 -0700 Subject: [PATCH 16/51] Update scripts/dev-runner.mjs Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- scripts/dev-runner.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 9cf5df52..df55799f 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -162,11 +162,16 @@ async function buildPluginSdk() { ["--filter", "@paperclipai/plugin-sdk", "build"], { stdio: "inherit" }, ); + if (result.signal) { + process.kill(process.pid, result.signal); + return; + } if (result.code !== 0) { console.error("[paperclip] plugin sdk build failed"); process.exit(result.code); } } +} await buildPluginSdk(); From 30e2914424343e2673ba084b1740dd5781c2dd74 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:44:26 -0700 Subject: [PATCH 17/51] feat(plugins): bridge core domain events to plugin event bus The plugin event bus accepts subscriptions for core events like issue.created but nothing emits them. This adds a bridge in logActivity() so every domain action that's already logged also fires a PluginEvent to subscribing plugins. Uses a module-level setter (same pattern as publishLiveEvent) to avoid threading the bus through all route handlers. Only actions matching PLUGIN_EVENT_TYPES are forwarded. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/app.ts | 2 ++ server/src/services/activity-log.ts | 32 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/server/src/app.ts b/server/src/app.ts index 22183848..c2034eba 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -37,6 +37,7 @@ import { pluginLifecycleManager } from "./services/plugin-lifecycle.js"; import { createPluginJobCoordinator } from "./services/plugin-job-coordinator.js"; import { buildHostServices, flushPluginLogBuffer } from "./services/plugin-host-services.js"; import { createPluginEventBus } from "./services/plugin-event-bus.js"; +import { setPluginEventBus } from "./services/activity-log.js"; import { createPluginDevWatcher } from "./services/plugin-dev-watcher.js"; import { createPluginHostServiceCleanup } from "./services/plugin-host-service-cleanup.js"; import { pluginRegistryService } from "./services/plugin-registry.js"; @@ -141,6 +142,7 @@ export async function createApp( const workerManager = createPluginWorkerManager(); const pluginRegistry = pluginRegistryService(db); const eventBus = createPluginEventBus(); + setPluginEventBus(eventBus); const jobStore = pluginJobStore(db); const lifecycle = pluginLifecycleManager(db, { workerManager }); const scheduler = createPluginJobScheduler({ diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index cdef68ec..4c7248bd 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -1,8 +1,21 @@ +import { randomUUID } from "node:crypto"; import type { Db } from "@paperclipai/db"; import { activityLog } from "@paperclipai/db"; +import { PLUGIN_EVENT_TYPES, type PluginEventType } from "@paperclipai/shared"; +import type { PluginEvent } from "@paperclipai/plugin-sdk"; import { publishLiveEvent } from "./live-events.js"; import { redactCurrentUserValue } from "../log-redaction.js"; import { sanitizeRecord } from "../redaction.js"; +import type { PluginEventBus } from "./plugin-event-bus.js"; + +const PLUGIN_EVENT_SET: ReadonlySet = new Set(PLUGIN_EVENT_TYPES); + +let _pluginEventBus: PluginEventBus | null = null; + +/** Wire the plugin event bus so domain events are forwarded to plugins. */ +export function setPluginEventBus(bus: PluginEventBus): void { + _pluginEventBus = bus; +} export interface LogActivityInput { companyId: string; @@ -45,4 +58,23 @@ export async function logActivity(db: Db, input: LogActivityInput) { details: redactedDetails, }, }); + + if (_pluginEventBus && PLUGIN_EVENT_SET.has(input.action)) { + const event: PluginEvent = { + eventId: randomUUID(), + eventType: input.action as PluginEventType, + occurredAt: new Date().toISOString(), + actorId: input.actorId, + actorType: input.actorType, + entityId: input.entityId, + entityType: input.entityType, + companyId: input.companyId, + payload: { + ...redactedDetails, + agentId: input.agentId ?? null, + runId: input.runId ?? null, + }, + }; + void _pluginEventBus.emit(event).catch(() => {}); + } } From a6c7e09e2a01ef1520967d9654e7fe3f9e14b008 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:51:41 -0700 Subject: [PATCH 18/51] fix(plugins): log plugin handler errors, warn on double-init Address Greptile review feedback: - Log plugin event handler errors via logger.warn instead of silently discarding the emit() result - Warn if setPluginEventBus is called more than once Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/services/activity-log.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index 4c7248bd..16758b94 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -6,6 +6,7 @@ import type { PluginEvent } from "@paperclipai/plugin-sdk"; import { publishLiveEvent } from "./live-events.js"; import { redactCurrentUserValue } from "../log-redaction.js"; import { sanitizeRecord } from "../redaction.js"; +import { logger } from "../middleware/logger.js"; import type { PluginEventBus } from "./plugin-event-bus.js"; const PLUGIN_EVENT_SET: ReadonlySet = new Set(PLUGIN_EVENT_TYPES); @@ -14,6 +15,9 @@ let _pluginEventBus: PluginEventBus | null = null; /** Wire the plugin event bus so domain events are forwarded to plugins. */ export function setPluginEventBus(bus: PluginEventBus): void { + if (_pluginEventBus) { + logger.warn("setPluginEventBus called more than once, replacing existing bus"); + } _pluginEventBus = bus; } @@ -75,6 +79,10 @@ export async function logActivity(db: Db, input: LogActivityInput) { runId: input.runId ?? null, }, }; - void _pluginEventBus.emit(event).catch(() => {}); + void _pluginEventBus.emit(event).then(({ errors }) => { + for (const { pluginId, error } of errors) { + logger.warn({ pluginId, eventType: event.eventType, err: error }, "plugin event handler failed"); + } + }).catch(() => {}); } } From 0f831e09c1e9e8c2f3b8a05694aa67bd08c53600 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 13:58:43 -0700 Subject: [PATCH 19/51] Add plugin cli commands --- cli/src/commands/client/plugin.ts | 365 ++++++++++++++++++++++++++++++ cli/src/index.ts | 2 + 2 files changed, 367 insertions(+) create mode 100644 cli/src/commands/client/plugin.ts diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts new file mode 100644 index 00000000..dd2fb08e --- /dev/null +++ b/cli/src/commands/client/plugin.ts @@ -0,0 +1,365 @@ +import path from "node:path"; +import { Command } from "commander"; +import pc from "picocolors"; +import { + addCommonClientOptions, + handleCommandError, + printOutput, + resolveCommandContext, + type BaseClientOptions, +} from "./common.js"; + +// --------------------------------------------------------------------------- +// Types mirroring server-side shapes +// --------------------------------------------------------------------------- + +interface PluginRecord { + id: string; + pluginKey: string; + packageName: string; + version: string; + status: string; + displayName?: string; + lastError?: string | null; + installedAt: string; + updatedAt: string; +} + + +// --------------------------------------------------------------------------- +// Option types +// --------------------------------------------------------------------------- + +interface PluginListOptions extends BaseClientOptions { + status?: string; +} + +interface PluginInstallOptions extends BaseClientOptions { + local?: boolean; + version?: string; +} + +interface PluginUninstallOptions extends BaseClientOptions { + force?: boolean; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Resolve a local path argument to an absolute path so the server can find the + * plugin on disk regardless of where the user ran the CLI. + */ +function resolvePackageArg(packageArg: string, isLocal: boolean): string { + if (!isLocal) return packageArg; + // Already absolute + if (path.isAbsolute(packageArg)) return packageArg; + return path.resolve(process.cwd(), packageArg); +} + +function formatPlugin(p: PluginRecord): string { + const statusColor = + p.status === "ready" + ? pc.green(p.status) + : p.status === "error" + ? pc.red(p.status) + : p.status === "disabled" + ? pc.dim(p.status) + : pc.yellow(p.status); + + const parts = [ + `key=${pc.bold(p.pluginKey)}`, + `status=${statusColor}`, + `version=${p.version}`, + `id=${pc.dim(p.id)}`, + ]; + + if (p.lastError) { + parts.push(`error=${pc.red(p.lastError.slice(0, 80))}`); + } + + return parts.join(" "); +} + +// --------------------------------------------------------------------------- +// Command registration +// --------------------------------------------------------------------------- + +export function registerPluginCommands(program: Command): void { + const plugin = program.command("plugin").description("Plugin lifecycle management"); + + // ------------------------------------------------------------------------- + // plugin list + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("list") + .description("List installed plugins") + .option("--status ", "Filter by status (ready, error, disabled, installed, upgrade_pending)") + .action(async (opts: PluginListOptions) => { + try { + const ctx = resolveCommandContext(opts); + const qs = opts.status ? `?status=${encodeURIComponent(opts.status)}` : ""; + const plugins = await ctx.api.get(`/api/plugins${qs}`); + + if (ctx.json) { + printOutput(plugins, { json: true }); + return; + } + + const rows = plugins ?? []; + if (rows.length === 0) { + console.log(pc.dim("No plugins installed.")); + return; + } + + for (const p of rows) { + console.log(formatPlugin(p)); + } + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin install + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("install ") + .description( + "Install a plugin from a local path or npm package.\n" + + " Examples:\n" + + " paperclipai plugin install ./my-plugin # local path\n" + + " paperclipai plugin install @acme/plugin-linear # npm package\n" + + " paperclipai plugin install @acme/plugin-linear@1.2 # pinned version", + ) + .option("-l, --local", "Treat as a local filesystem path", false) + .option("--version ", "Specific npm version to install (npm packages only)") + .action(async (packageArg: string, opts: PluginInstallOptions) => { + try { + const ctx = resolveCommandContext(opts); + + // Auto-detect local paths: starts with . or / or ~ or is an absolute path + const isLocal = + opts.local || + packageArg.startsWith("./") || + packageArg.startsWith("../") || + packageArg.startsWith("/") || + packageArg.startsWith("~"); + + const resolvedPackage = resolvePackageArg(packageArg, isLocal); + + if (!ctx.json) { + console.log( + pc.dim( + isLocal + ? `Installing plugin from local path: ${resolvedPackage}` + : `Installing plugin: ${resolvedPackage}${opts.version ? `@${opts.version}` : ""}`, + ), + ); + } + + const plugin = await ctx.api.post("/api/plugins/install", { + packageName: resolvedPackage, + version: opts.version, + isLocalPath: isLocal, + }); + + if (ctx.json) { + printOutput(plugin, { json: true }); + return; + } + + if (!plugin) { + console.log(pc.dim("Install returned no plugin record.")); + return; + } + + console.log(pc.green(`✓ Installed ${pc.bold(plugin.pluginKey)} v${plugin.version} (${plugin.status})`)); + + if (plugin.lastError) { + console.log(pc.red(` Warning: ${plugin.lastError}`)); + } + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin uninstall + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("uninstall ") + .description( + "Uninstall a plugin by its plugin key or database ID.\n" + + " Use --force to hard-purge all state and config.", + ) + .option("--force", "Purge all plugin state and config (hard delete)", false) + .action(async (pluginKey: string, opts: PluginUninstallOptions) => { + try { + const ctx = resolveCommandContext(opts); + const purge = opts.force === true; + const qs = purge ? "?purge=true" : ""; + + if (!ctx.json) { + console.log( + pc.dim( + purge + ? `Uninstalling and purging plugin: ${pluginKey}` + : `Uninstalling plugin: ${pluginKey}`, + ), + ); + } + + const result = await ctx.api.delete( + `/api/plugins/${encodeURIComponent(pluginKey)}${qs}`, + ); + + if (ctx.json) { + printOutput(result, { json: true }); + return; + } + + console.log(pc.green(`✓ Uninstalled ${pc.bold(pluginKey)}${purge ? " (purged)" : ""}`)); + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin enable + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("enable ") + .description("Enable a disabled or errored plugin") + .action(async (pluginKey: string, opts: BaseClientOptions) => { + try { + const ctx = resolveCommandContext(opts); + const result = await ctx.api.post( + `/api/plugins/${encodeURIComponent(pluginKey)}/enable`, + ); + + if (ctx.json) { + printOutput(result, { json: true }); + return; + } + + console.log(pc.green(`✓ Enabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`)); + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin disable + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("disable ") + .description("Disable a running plugin without uninstalling it") + .action(async (pluginKey: string, opts: BaseClientOptions) => { + try { + const ctx = resolveCommandContext(opts); + const result = await ctx.api.post( + `/api/plugins/${encodeURIComponent(pluginKey)}/disable`, + ); + + if (ctx.json) { + printOutput(result, { json: true }); + return; + } + + console.log(pc.dim(`Disabled ${pc.bold(pluginKey)} — status: ${result?.status ?? "unknown"}`)); + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin inspect + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("inspect ") + .description("Show full details for an installed plugin") + .action(async (pluginKey: string, opts: BaseClientOptions) => { + try { + const ctx = resolveCommandContext(opts); + const result = await ctx.api.get( + `/api/plugins/${encodeURIComponent(pluginKey)}`, + ); + + if (!result) { + console.log(pc.red(`Plugin not found: ${pluginKey}`)); + process.exit(1); + } + + if (ctx.json) { + printOutput(result, { json: true }); + return; + } + + console.log(formatPlugin(result)); + if (result.lastError) { + console.log(`\n${pc.red("Last error:")}\n${result.lastError}`); + } + } catch (err) { + handleCommandError(err); + } + }), + ); + + // ------------------------------------------------------------------------- + // plugin examples + // ------------------------------------------------------------------------- + addCommonClientOptions( + plugin + .command("examples") + .description("List bundled example plugins available for local install") + .action(async (opts: BaseClientOptions) => { + try { + const ctx = resolveCommandContext(opts); + const examples = await ctx.api.get< + Array<{ + packageName: string; + pluginKey: string; + displayName: string; + description: string; + localPath: string; + tag: string; + }> + >("/api/plugins/examples"); + + if (ctx.json) { + printOutput(examples, { json: true }); + return; + } + + const rows = examples ?? []; + if (rows.length === 0) { + console.log(pc.dim("No bundled examples available.")); + return; + } + + for (const ex of rows) { + console.log( + `${pc.bold(ex.displayName)} ${pc.dim(ex.pluginKey)}\n` + + ` ${ex.description}\n` + + ` ${pc.cyan(`paperclipai plugin install ${ex.localPath}`)}`, + ); + } + } catch (err) { + handleCommandError(err); + } + }), + ); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 19ef69f9..628cd7e7 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -18,6 +18,7 @@ import { registerDashboardCommands } from "./commands/client/dashboard.js"; import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir.js"; import { loadPaperclipEnvFile } from "./config/env.js"; import { registerWorktreeCommands } from "./commands/worktree.js"; +import { registerPluginCommands } from "./commands/client/plugin.js"; const program = new Command(); const DATA_DIR_OPTION_HELP = @@ -136,6 +137,7 @@ registerApprovalCommands(program); registerActivityCommands(program); registerDashboardCommands(program); registerWorktreeCommands(program); +registerPluginCommands(program); const auth = program.command("auth").description("Authentication and bootstrap utilities"); From bc5d65024866650e83a95e8a6abf385e88fc7c14 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:05:29 -0700 Subject: [PATCH 20/51] Add global toolbar slots to BreadcrumbBar component --- ui/src/components/BreadcrumbBar.tsx | 72 ++++++++++++++++++----------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index a93d96c8..c4523d9f 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -2,6 +2,7 @@ import { Link } from "@/lib/router"; import { Menu } from "lucide-react"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useSidebar } from "../context/SidebarContext"; +import { useCompany } from "../context/CompanyContext"; import { Button } from "@/components/ui/button"; import { Breadcrumb, @@ -12,10 +13,12 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { Fragment } from "react"; +import { PluginSlotOutlet } from "@/plugins/slots"; export function BreadcrumbBar() { const { breadcrumbs } = useBreadcrumbs(); const { toggleSidebar, isMobile } = useSidebar(); + const { selectedCompanyId, selectedCompany } = useCompany(); if (breadcrumbs.length === 0) return null; @@ -31,43 +34,60 @@ export function BreadcrumbBar() { ); + const globalToolbarSlots = ( + + ); + // Single breadcrumb = page title (uppercase) if (breadcrumbs.length === 1) { return ( -
+
{menuButton} -

- {breadcrumbs[0].label} -

+
+

+ {breadcrumbs[0].label} +

+
+ {globalToolbarSlots}
); } // Multiple breadcrumbs = breadcrumb trail return ( -
+
{menuButton} - - - {breadcrumbs.map((crumb, i) => { - const isLast = i === breadcrumbs.length - 1; - return ( - - {i > 0 && } - - {isLast || !crumb.href ? ( - {crumb.label} - ) : ( - - {crumb.label} - - )} - - - ); - })} - - +
+ + + {breadcrumbs.map((crumb, i) => { + const isLast = i === breadcrumbs.length - 1; + return ( + + {i > 0 && } + + {isLast || !crumb.href ? ( + {crumb.label} + ) : ( + + {crumb.label} + + )} + + + ); + })} + + +
+ {globalToolbarSlots}
); } From d0677dcd915471fca41ffa90a5b42138e159bc2e Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:08:03 -0700 Subject: [PATCH 21/51] Update cli/src/commands/client/plugin.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- cli/src/commands/client/plugin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts index dd2fb08e..c032b988 100644 --- a/cli/src/commands/client/plugin.ts +++ b/cli/src/commands/client/plugin.ts @@ -55,6 +55,11 @@ function resolvePackageArg(packageArg: string, isLocal: boolean): string { if (!isLocal) return packageArg; // Already absolute if (path.isAbsolute(packageArg)) return packageArg; + // Expand leading ~ to home directory + if (packageArg.startsWith("~")) { + const home = process.env.HOME ?? process.env.USERPROFILE ?? ""; + return path.resolve(home, packageArg.slice(1).replace(/^[\\/]/, "")); + } return path.resolve(process.cwd(), packageArg); } From 4f8df1804df03557f59bd377063f1f2195032c30 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:08:15 -0700 Subject: [PATCH 22/51] Update cli/src/commands/client/plugin.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- cli/src/commands/client/plugin.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts index c032b988..bb54f00d 100644 --- a/cli/src/commands/client/plugin.ts +++ b/cli/src/commands/client/plugin.ts @@ -303,14 +303,15 @@ export function registerPluginCommands(program: Command): void { `/api/plugins/${encodeURIComponent(pluginKey)}`, ); + if (ctx.json) { + printOutput(result, { json: true }); + return; + } + if (!result) { console.log(pc.red(`Plugin not found: ${pluginKey}`)); process.exit(1); } - - if (ctx.json) { - printOutput(result, { json: true }); - return; } console.log(formatPlugin(result)); From 0afd5d5630f394c22e585ec803498e4c1b1f53d2 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:08:21 -0700 Subject: [PATCH 23/51] Update cli/src/commands/client/plugin.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- cli/src/commands/client/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts index bb54f00d..59733915 100644 --- a/cli/src/commands/client/plugin.ts +++ b/cli/src/commands/client/plugin.ts @@ -167,7 +167,7 @@ export function registerPluginCommands(program: Command): void { ); } - const plugin = await ctx.api.post("/api/plugins/install", { + const installedPlugin = await ctx.api.post("/api/plugins/install", { packageName: resolvedPackage, version: opts.version, isLocalPath: isLocal, From e219761d9591c17e92663251ae3eff08856acfdf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:15:42 -0700 Subject: [PATCH 24/51] Fix plugin installation output and error handling in registerPluginCommands --- cli/src/commands/client/plugin.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts index 59733915..9031d696 100644 --- a/cli/src/commands/client/plugin.ts +++ b/cli/src/commands/client/plugin.ts @@ -174,19 +174,23 @@ export function registerPluginCommands(program: Command): void { }); if (ctx.json) { - printOutput(plugin, { json: true }); + printOutput(installedPlugin, { json: true }); return; } - if (!plugin) { + if (!installedPlugin) { console.log(pc.dim("Install returned no plugin record.")); return; } - console.log(pc.green(`✓ Installed ${pc.bold(plugin.pluginKey)} v${plugin.version} (${plugin.status})`)); + console.log( + pc.green( + `✓ Installed ${pc.bold(installedPlugin.pluginKey)} v${installedPlugin.version} (${installedPlugin.status})`, + ), + ); - if (plugin.lastError) { - console.log(pc.red(` Warning: ${plugin.lastError}`)); + if (installedPlugin.lastError) { + console.log(pc.red(` Warning: ${installedPlugin.lastError}`)); } } catch (err) { handleCommandError(err); @@ -312,7 +316,6 @@ export function registerPluginCommands(program: Command): void { console.log(pc.red(`Plugin not found: ${pluginKey}`)); process.exit(1); } - } console.log(formatPlugin(result)); if (result.lastError) { From 7e3a04c76c71d11784d6a0929b8e2580b35623d1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 14 Mar 2026 14:19:32 -0700 Subject: [PATCH 25/51] Refactor BreadcrumbBar to use useMemo for global toolbar slot context and improve rendering logic --- ui/src/components/BreadcrumbBar.tsx | 37 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index c4523d9f..6f1f9bbf 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -12,7 +12,7 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { Fragment } from "react"; +import { Fragment, useMemo } from "react"; import { PluginSlotOutlet } from "@/plugins/slots"; export function BreadcrumbBar() { @@ -20,7 +20,29 @@ export function BreadcrumbBar() { const { toggleSidebar, isMobile } = useSidebar(); const { selectedCompanyId, selectedCompany } = useCompany(); - if (breadcrumbs.length === 0) return null; + const globalToolbarSlotContext = useMemo( + () => ({ + companyId: selectedCompanyId ?? null, + companyPrefix: selectedCompany?.issuePrefix ?? null, + }), + [selectedCompanyId, selectedCompany?.issuePrefix], + ); + + const globalToolbarSlots = ( + + ); + + if (breadcrumbs.length === 0) { + return ( +
+ {globalToolbarSlots} +
+ ); + } const menuButton = isMobile && ( +
+
+ + )}
); } diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 42bb5b86..5134c22b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -274,6 +274,21 @@ export function ProjectDetail() { onSuccess: invalidateProject, }); + const archiveProject = useMutation({ + mutationFn: (archived: boolean) => + projectsApi.update( + projectLookupRef, + { archivedAt: archived ? new Date().toISOString() : null }, + resolvedCompanyId ?? lookupCompanyId, + ), + onSuccess: (_, archived) => { + invalidateProject(); + if (archived) { + navigate("/projects"); + } + }, + }); + const uploadImage = useMutation({ mutationFn: async (file: File) => { if (!resolvedCompanyId) throw new Error("No company selected"); @@ -476,6 +491,8 @@ export function ProjectDetail() { onUpdate={(data) => updateProject.mutate(data)} onFieldUpdate={updateProjectField} getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"} + onArchive={(archived) => archiveProject.mutate(archived)} + archivePending={archiveProject.isPending} />
)} diff --git a/ui/src/pages/Projects.tsx b/ui/src/pages/Projects.tsx index 6fe80ada..886a2b60 100644 --- a/ui/src/pages/Projects.tsx +++ b/ui/src/pages/Projects.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; @@ -22,11 +22,15 @@ export function Projects() { setBreadcrumbs([{ label: "Projects" }]); }, [setBreadcrumbs]); - const { data: projects, isLoading, error } = useQuery({ + const { data: allProjects, isLoading, error } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); + const projects = useMemo( + () => (allProjects ?? []).filter((p) => !p.archivedAt), + [allProjects], + ); if (!selectedCompanyId) { return ; @@ -47,7 +51,7 @@ export function Projects() { {error &&

{error.message}

} - {projects && projects.length === 0 && ( + {!isLoading && projects.length === 0 && ( )} - {projects && projects.length > 0 && ( + {projects.length > 0 && (
{projects.map((project) => ( Date: Sun, 15 Mar 2026 10:48:27 -0500 Subject: [PATCH 36/51] Restyle markdown code blocks: dark background, smaller font, compact padding - Switch code block background from transparent accent to dark (#1e1e2e) with light text (#cdd6f4) for better readability in both light and dark modes - Reduce code font size from 0.84em to 0.78em - Compact padding and margins on pre blocks - Hide MDXEditor code block toolbar by default, show on hover/focus to prevent overlap with code content on mobile - Use horizontal scroll instead of word-wrap for code blocks to preserve formatting Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 46 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index ca9624c0..1242fa8a 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 10:49:24 -0500 Subject: [PATCH 37/51] Fix sidebar scrollbar: hide track background when not hovering The scrollbar track background was still visible as a colored "well" even when the thumb was hidden. Now both track and thumb are fully transparent by default, only appearing on container hover. Co-Authored-By: Paperclip --- ui/src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/index.css b/ui/src/index.css index 3b8a5af6..a2a3b794 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -178,12 +178,13 @@ background: oklch(0.5 0 0); } -/* Auto-hide scrollbar: fully transparent by default, visible on container hover */ +/* Auto-hide scrollbar: fully invisible by default, visible on container hover */ .scrollbar-auto-hide::-webkit-scrollbar-track { background: transparent !important; } .scrollbar-auto-hide::-webkit-scrollbar-thumb { background: transparent !important; + transition: background 150ms ease; } .scrollbar-auto-hide:hover::-webkit-scrollbar-track { background: oklch(0.205 0 0) !important; From d7f45eac14845555c4c92b179ea4f3047b517c6a Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 10:55:53 -0500 Subject: [PATCH 38/51] Add doc-maintenance skill for periodic documentation accuracy audits Skill detects documentation drift by scanning git history since last review, cross-referencing shipped features against README, SPEC, and PRODUCT docs, and opening PRs with minimal fixes. Includes audit checklist and section map references. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .agents/skills/doc-maintenance/SKILL.md | 201 ++++++++++++++++++ .../references/audit-checklist.md | 85 ++++++++ .../doc-maintenance/references/section-map.md | 22 ++ 3 files changed, 308 insertions(+) create mode 100644 .agents/skills/doc-maintenance/SKILL.md create mode 100644 .agents/skills/doc-maintenance/references/audit-checklist.md create mode 100644 .agents/skills/doc-maintenance/references/section-map.md diff --git a/.agents/skills/doc-maintenance/SKILL.md b/.agents/skills/doc-maintenance/SKILL.md new file mode 100644 index 00000000..a597e90c --- /dev/null +++ b/.agents/skills/doc-maintenance/SKILL.md @@ -0,0 +1,201 @@ +--- +name: doc-maintenance +description: > + Audit top-level documentation (README, SPEC, PRODUCT) against recent git + history to find drift — shipped features missing from docs or features + listed as upcoming that already landed. Proposes minimal edits, creates + a branch, and opens a PR. Use when asked to review docs for accuracy, + after major feature merges, or on a periodic schedule. +--- + +# Doc Maintenance Skill + +Detect documentation drift and fix it via PR — no rewrites, no churn. + +## When to Use + +- Periodic doc review (e.g. weekly or after releases) +- After major feature merges +- When asked "are our docs up to date?" +- When asked to audit README / SPEC / PRODUCT accuracy + +## Target Documents + +| Document | Path | What matters | +|----------|------|-------------| +| README | `README.md` | Features table, roadmap, quickstart, "what is" accuracy, "works with" table | +| SPEC | `doc/SPEC.md` | No false "not supported" claims, major model/schema accuracy | +| PRODUCT | `doc/PRODUCT.md` | Core concepts, feature list, principles accuracy | + +Out of scope: DEVELOPING.md, DATABASE.md, CLI.md, doc/plans/, skill files, +release notes. These are dev-facing or ephemeral — lower risk of user-facing +confusion. + +## Workflow + +### Step 1 — Detect what changed + +Find the last review cursor: + +```bash +# Read the last-reviewed commit SHA +CURSOR_FILE=".doc-review-cursor" +if [ -f "$CURSOR_FILE" ]; then + LAST_SHA=$(cat "$CURSOR_FILE" | head -1) +else + # First run: look back 60 days + LAST_SHA=$(git log --format="%H" --after="60 days ago" --reverse | head -1) +fi +``` + +Then gather commits since the cursor: + +```bash +git log "$LAST_SHA"..HEAD --oneline --no-merges +``` + +### Step 2 — Classify changes + +Scan commit messages and changed files. Categorize into: + +- **Feature** — new capabilities (keywords: `feat`, `add`, `implement`, `support`) +- **Breaking** — removed/renamed things (keywords: `remove`, `breaking`, `drop`, `rename`) +- **Structural** — new directories, config changes, new adapters, new CLI commands + +**Ignore:** refactors, test-only changes, CI config, dependency bumps, doc-only +changes, style/formatting commits. These don't affect doc accuracy. + +For borderline cases, check the actual diff — a commit titled "refactor: X" +that adds a new public API is a feature. + +### Step 3 — Build a change summary + +Produce a concise list like: + +``` +Since last review (, ): +- FEATURE: Plugin system merged (runtime, SDK, CLI, slots, event bridge) +- FEATURE: Project archiving added +- BREAKING: Removed legacy webhook adapter +- STRUCTURAL: New .agents/skills/ directory convention +``` + +If there are no notable changes, skip to Step 7 (update cursor and exit). + +### Step 4 — Audit each target doc + +For each target document, read it fully and cross-reference against the change +summary. Check for: + +1. **False negatives** — major shipped features not mentioned at all +2. **False positives** — features listed as "coming soon" / "roadmap" / "planned" + / "not supported" / "TBD" that already shipped +3. **Quickstart accuracy** — install commands, prereqs, and startup instructions + still correct (README only) +4. **Feature table accuracy** — does the features section reflect current + capabilities? (README only) +5. **Works-with accuracy** — are supported adapters/integrations listed correctly? + +Use `references/audit-checklist.md` as the structured checklist. +Use `references/section-map.md` to know where to look for each feature area. + +### Step 5 — Create branch and apply minimal edits + +```bash +# Create a branch for the doc updates +BRANCH="docs/maintenance-$(date +%Y%m%d)" +git checkout -b "$BRANCH" +``` + +Apply **only** the edits needed to fix drift. Rules: + +- **Minimal patches only.** Fix inaccuracies, don't rewrite sections. +- **Preserve voice and style.** Match the existing tone of each document. +- **No cosmetic changes.** Don't fix typos, reformat tables, or reorganize + sections unless they're part of a factual fix. +- **No new sections.** If a feature needs a whole new section, note it in the + PR description as a follow-up — don't add it in a maintenance pass. +- **Roadmap items:** Move shipped features out of Roadmap. Add a brief mention + in the appropriate existing section if there isn't one already. Don't add + long descriptions. + +### Step 6 — Open a PR + +Commit the changes and open a PR: + +```bash +git add README.md doc/SPEC.md doc/PRODUCT.md .doc-review-cursor +git commit -m "docs: update documentation for accuracy + +- [list each fix briefly] + +Co-Authored-By: Paperclip " + +git push -u origin "$BRANCH" + +gh pr create \ + --title "docs: periodic documentation accuracy update" \ + --body "$(cat <<'EOF' +## Summary +Automated doc maintenance pass. Fixes documentation drift detected since +last review. + +### Changes +- [list each fix] + +### Change summary (since last review) +- [list notable code changes that triggered doc updates] + +## Review notes +- Only factual accuracy fixes — no style/cosmetic changes +- Preserves existing voice and structure +- Larger doc additions (new sections, tutorials) noted as follow-ups + +🤖 Generated by doc-maintenance skill +EOF +)" +``` + +### Step 7 — Update the cursor + +After a successful audit (whether or not edits were needed), update the cursor: + +```bash +git rev-parse HEAD > .doc-review-cursor +``` + +If edits were made, this is already committed in the PR branch. If no edits +were needed, commit the cursor update to the current branch. + +## Change Classification Rules + +| Signal | Category | Doc update needed? | +|--------|----------|-------------------| +| `feat:`, `add`, `implement`, `support` in message | Feature | Yes if user-facing | +| `remove`, `drop`, `breaking`, `!:` in message | Breaking | Yes | +| New top-level directory or config file | Structural | Maybe | +| `fix:`, `bugfix` | Fix | No (unless it changes behavior described in docs) | +| `refactor:`, `chore:`, `ci:`, `test:` | Maintenance | No | +| `docs:` | Doc change | No (already handled) | +| Dependency bumps only | Maintenance | No | + +## Patch Style Guide + +- Fix the fact, not the prose +- If removing a roadmap item, don't leave a gap — remove the bullet cleanly +- If adding a feature mention, match the format of surrounding entries + (e.g. if features are in a table, add a table row) +- Keep README changes especially minimal — it shouldn't churn often +- For SPEC/PRODUCT, prefer updating existing statements over adding new ones + (e.g. change "not supported in V1" to "supported via X" rather than adding + a new section) + +## Output + +When the skill completes, report: + +- How many commits were scanned +- How many notable changes were found +- How many doc edits were made (and to which files) +- PR link (if edits were made) +- Any follow-up items that need larger doc work diff --git a/.agents/skills/doc-maintenance/references/audit-checklist.md b/.agents/skills/doc-maintenance/references/audit-checklist.md new file mode 100644 index 00000000..9c13a437 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/audit-checklist.md @@ -0,0 +1,85 @@ +# Doc Maintenance Audit Checklist + +Use this checklist when auditing each target document. For each item, compare +against the change summary from git history. + +## README.md + +### Features table +- [ ] Each feature card reflects a shipped capability +- [ ] No feature cards for things that don't exist yet +- [ ] No major shipped features missing from the table + +### Roadmap +- [ ] Nothing listed as "planned" or "coming soon" that already shipped +- [ ] No removed/cancelled items still listed +- [ ] Items reflect current priorities (cross-check with recent PRs) + +### Quickstart +- [ ] `npx paperclipai onboard` command is correct +- [ ] Manual install steps are accurate (clone URL, commands) +- [ ] Prerequisites (Node version, pnpm version) are current +- [ ] Server URL and port are correct + +### "What is Paperclip" section +- [ ] High-level description is accurate +- [ ] Step table (Define goal / Hire team / Approve and run) is correct + +### "Works with" table +- [ ] All supported adapters/runtimes are listed +- [ ] No removed adapters still listed +- [ ] Logos and labels match current adapter names + +### "Paperclip is right for you if" +- [ ] Use cases are still accurate +- [ ] No claims about capabilities that don't exist + +### "Why Paperclip is special" +- [ ] Technical claims are accurate (atomic execution, governance, etc.) +- [ ] No features listed that were removed or significantly changed + +### FAQ +- [ ] Answers are still correct +- [ ] No references to removed features or outdated behavior + +### Development section +- [ ] Commands are accurate (`pnpm dev`, `pnpm build`, etc.) +- [ ] Link to DEVELOPING.md is correct + +## doc/SPEC.md + +### Company Model +- [ ] Fields match current schema +- [ ] Governance model description is accurate + +### Agent Model +- [ ] Adapter types match what's actually supported +- [ ] Agent configuration description is accurate +- [ ] No features described as "not supported" or "not V1" that shipped + +### Task Model +- [ ] Task hierarchy description is accurate +- [ ] Status values match current implementation + +### Extensions / Plugins +- [ ] If plugins are shipped, no "not in V1" or "future" language +- [ ] Plugin model description matches implementation + +### Open Questions +- [ ] Resolved questions removed or updated +- [ ] No "TBD" items that have been decided + +## doc/PRODUCT.md + +### Core Concepts +- [ ] Company, Employees, Task Management descriptions accurate +- [ ] Agent Execution modes described correctly +- [ ] No missing major concepts + +### Principles +- [ ] Principles haven't been contradicted by shipped features +- [ ] No principles referencing removed capabilities + +### User Flow +- [ ] Dream scenario still reflects actual onboarding +- [ ] Steps are achievable with current features diff --git a/.agents/skills/doc-maintenance/references/section-map.md b/.agents/skills/doc-maintenance/references/section-map.md new file mode 100644 index 00000000..4ec64f83 --- /dev/null +++ b/.agents/skills/doc-maintenance/references/section-map.md @@ -0,0 +1,22 @@ +# Section Map + +Maps feature areas to specific document sections so the skill knows where to +look when a feature ships or changes. + +| Feature Area | README Section | SPEC Section | PRODUCT Section | +|-------------|---------------|-------------|----------------| +| Plugins / Extensions | Features table, Roadmap | Extensions, Agent Model | Core Concepts | +| Adapters (new runtimes) | "Works with" table, FAQ | Agent Model, Agent Configuration | Employees & Agents, Agent Execution | +| Governance / Approvals | Features table, "Why special" | Board Governance, Board Approval Gates | Principles | +| Budget / Cost Control | Features table, "Why special" | Budget Delegation | Company (revenue & expenses) | +| Task Management | Features table | Task Model | Task Management | +| Org Chart / Hierarchy | Features table | Agent Model (reporting) | Employees & Agents | +| Multi-Company | Features table, FAQ | Company Model | Company | +| Heartbeats | Features table, FAQ | Agent Execution | Agent Execution | +| CLI Commands | Development section | — | — | +| Onboarding / Quickstart | Quickstart, FAQ | — | User Flow | +| Skills / Skill Injection | "Why special" | — | — | +| Company Templates | "Why special", Roadmap (ClipMart) | — | — | +| Mobile / UI | Features table | — | — | +| Project Archiving | — | — | — | +| OpenClaw Integration | "Works with" table, FAQ | Agent Model | Agent Execution | From 41e03bae61d1ad5c96bf250043076b682cd25d5e Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:08:37 -0500 Subject: [PATCH 39/51] Fix org chart canvas height to fit viewport without scrolling The height calc subtracted only 4rem but the actual overhead is ~6rem (3rem breadcrumb bar + 3rem main padding). Also use dvh for better mobile support. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/pages/OrgChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/OrgChart.tsx b/ui/src/pages/OrgChart.tsx index 981545c0..7eb0f0d9 100644 --- a/ui/src/pages/OrgChart.tsx +++ b/ui/src/pages/OrgChart.tsx @@ -269,7 +269,7 @@ export function OrgChart() { return (
Date: Sun, 15 Mar 2026 14:18:56 -0500 Subject: [PATCH 40/51] Add Docker setup for untrusted PR review in isolated containers Adds a dedicated Docker environment for reviewing untrusted pull requests with codex/claude, keeping CLI auth state in volumes and using a separate scratch workspace for PR checkouts. Co-Authored-By: Claude Opus 4.6 (1M context) --- doc/DEVELOPING.md | 4 + doc/DOCKER.md | 6 + doc/UNTRUSTED-PR-REVIEW.md | 135 ++++++++++++++++++ docker-compose.untrusted-review.yml | 33 +++++ docker/untrusted-review/Dockerfile | 44 ++++++ .../untrusted-review/bin/review-checkout-pr | 65 +++++++++ 6 files changed, 287 insertions(+) create mode 100644 doc/UNTRUSTED-PR-REVIEW.md create mode 100644 docker-compose.untrusted-review.yml create mode 100644 docker/untrusted-review/Dockerfile create mode 100644 docker/untrusted-review/bin/review-checkout-pr diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index e3668516..b39839c1 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -89,6 +89,10 @@ docker compose -f docker-compose.quickstart.yml up --build See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details. +## Docker For Untrusted PR Review + +For a separate review-oriented container that keeps `codex`/`claude` login state in Docker volumes and checks out PRs into an isolated scratch workspace, see `doc/UNTRUSTED-PR-REVIEW.md`. + ## Database in Dev (Auto-Handled) For local development, leave `DATABASE_URL` unset. diff --git a/doc/DOCKER.md b/doc/DOCKER.md index 82559bf8..6f6ca374 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -93,6 +93,12 @@ Notes: - Without API keys, the app still runs normally. - Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites. +## Untrusted PR Review Container + +If you want a separate Docker environment for reviewing untrusted pull requests with `codex` or `claude`, use the dedicated review workflow in `doc/UNTRUSTED-PR-REVIEW.md`. + +That setup keeps CLI auth state in Docker volumes instead of your host home directory and uses a separate scratch workspace for PR checkouts and preview runs. + ## Onboard Smoke Test (Ubuntu + npm only) Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify: diff --git a/doc/UNTRUSTED-PR-REVIEW.md b/doc/UNTRUSTED-PR-REVIEW.md new file mode 100644 index 00000000..0061a581 --- /dev/null +++ b/doc/UNTRUSTED-PR-REVIEW.md @@ -0,0 +1,135 @@ +# Untrusted PR Review In Docker + +Use this workflow when you want Codex or Claude to inspect a pull request that you do not want touching your host machine directly. + +This is intentionally separate from the normal Paperclip dev image. + +## What this container isolates + +- `codex` auth/session state in a Docker volume, not your host `~/.codex` +- `claude` auth/session state in a Docker volume, not your host `~/.claude` +- `gh` auth state in the same container-local home volume +- review clones, worktrees, dependency installs, and local databases in a writable scratch volume under `/work` + +By default this workflow does **not** mount your host repo checkout, your host home directory, or your SSH agent. + +## Files + +- `docker/untrusted-review/Dockerfile` +- `docker-compose.untrusted-review.yml` +- `review-checkout-pr` inside the container + +## Build and start a shell + +```sh +docker compose -f docker-compose.untrusted-review.yml build +docker compose -f docker-compose.untrusted-review.yml run --rm --service-ports review +``` + +That opens an interactive shell in the review container with: + +- Node + Corepack/pnpm +- `codex` +- `claude` +- `gh` +- `git`, `rg`, `fd`, `jq` + +## First-time login inside the container + +Run these once. The resulting login state persists in the `review-home` Docker volume. + +```sh +gh auth login +codex login +claude login +``` + +If you prefer API-key auth instead of CLI login, pass keys through Compose env: + +```sh +OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker-compose.untrusted-review.yml run --rm review +``` + +## Check out a PR safely + +Inside the container: + +```sh +review-checkout-pr paperclipai/paperclip 432 +cd /work/checkouts/paperclipai-paperclip/pr-432 +``` + +What this does: + +1. Creates or reuses a repo clone under `/work/repos/...` +2. Fetches `pull//head` from GitHub +3. Creates a detached git worktree under `/work/checkouts/...` + +The checkout lives entirely inside the container volume. + +## Ask Codex or Claude to review it + +Inside the PR checkout: + +```sh +codex +``` + +Then give it a prompt like: + +```text +Review this PR as hostile input. Focus on security issues, data exfiltration paths, sandbox escapes, dangerous install/runtime scripts, auth changes, and subtle behavioral regressions. Do not modify files. Produce findings ordered by severity with file references. +``` + +Or with Claude: + +```sh +claude +``` + +## Preview the Paperclip app from the PR + +Only do this when you intentionally want to execute the PR's code inside the container. + +Inside the PR checkout: + +```sh +pnpm install +HOST=0.0.0.0 pnpm dev +``` + +Open from the host: + +- `http://localhost:3100` + +The Compose file also exposes Vite's default port: + +- `http://localhost:5173` + +Notes: + +- `pnpm install` can run untrusted lifecycle scripts from the PR. That is why this happens inside the isolated container instead of on your host. +- If you only want static inspection, do not run install/dev commands. +- Paperclip's embedded PostgreSQL and local storage stay inside the container home volume via `PAPERCLIP_HOME=/home/reviewer/.paperclip-review`. + +## Reset state + +Remove the review container volumes when you want a clean environment: + +```sh +docker compose -f docker-compose.untrusted-review.yml down -v +``` + +That deletes: + +- Codex/Claude/GitHub login state stored in `review-home` +- cloned repos, worktrees, installs, and scratch data stored in `review-work` + +## Security limits + +This is a useful isolation boundary, but it is still Docker, not a full VM. + +- A reviewed PR can still access the container's network unless you disable it. +- Any secrets you pass into the container are available to code you execute inside it. +- Do not mount your host repo, host home, `.ssh`, or Docker socket unless you are intentionally weakening the boundary. +- If you need a stronger boundary than this, use a disposable VM instead of Docker. diff --git a/docker-compose.untrusted-review.yml b/docker-compose.untrusted-review.yml new file mode 100644 index 00000000..ff11148a --- /dev/null +++ b/docker-compose.untrusted-review.yml @@ -0,0 +1,33 @@ +services: + review: + build: + context: . + dockerfile: docker/untrusted-review/Dockerfile + init: true + tty: true + stdin_open: true + working_dir: /work + environment: + HOME: "/home/reviewer" + CODEX_HOME: "/home/reviewer/.codex" + CLAUDE_HOME: "/home/reviewer/.claude" + PAPERCLIP_HOME: "/home/reviewer/.paperclip-review" + OPENAI_API_KEY: "${OPENAI_API_KEY:-}" + ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" + GITHUB_TOKEN: "${GITHUB_TOKEN:-}" + ports: + - "${REVIEW_PAPERCLIP_PORT:-3100}:3100" + - "${REVIEW_VITE_PORT:-5173}:5173" + volumes: + - review-home:/home/reviewer + - review-work:/work + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp:mode=1777,size=1g + +volumes: + review-home: + review-work: diff --git a/docker/untrusted-review/Dockerfile b/docker/untrusted-review/Dockerfile new file mode 100644 index 00000000..c8b1f432 --- /dev/null +++ b/docker/untrusted-review/Dockerfile @@ -0,0 +1,44 @@ +FROM node:lts-trixie-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + fd-find \ + gh \ + git \ + jq \ + less \ + openssh-client \ + procps \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* + +RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd + +RUN corepack enable \ + && npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest + +RUN useradd --create-home --shell /bin/bash reviewer + +ENV HOME=/home/reviewer \ + CODEX_HOME=/home/reviewer/.codex \ + CLAUDE_HOME=/home/reviewer/.claude \ + PAPERCLIP_HOME=/home/reviewer/.paperclip-review \ + PNPM_HOME=/home/reviewer/.local/share/pnpm \ + PATH=/home/reviewer/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +WORKDIR /work + +COPY --chown=reviewer:reviewer docker/untrusted-review/bin/review-checkout-pr /usr/local/bin/review-checkout-pr + +RUN chmod +x /usr/local/bin/review-checkout-pr \ + && mkdir -p /work \ + && chown -R reviewer:reviewer /work + +USER reviewer + +EXPOSE 3100 5173 + +CMD ["bash", "-l"] diff --git a/docker/untrusted-review/bin/review-checkout-pr b/docker/untrusted-review/bin/review-checkout-pr new file mode 100644 index 00000000..abca98ad --- /dev/null +++ b/docker/untrusted-review/bin/review-checkout-pr @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: review-checkout-pr [checkout-dir] + +Examples: + review-checkout-pr paperclipai/paperclip 432 + review-checkout-pr https://github.com/paperclipai/paperclip.git 432 +EOF +} + +if [[ $# -lt 2 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +normalize_repo_slug() { + local raw="$1" + raw="${raw#git@github.com:}" + raw="${raw#ssh://git@github.com/}" + raw="${raw#https://github.com/}" + raw="${raw#http://github.com/}" + raw="${raw%.git}" + printf '%s\n' "${raw#/}" +} + +repo_slug="$(normalize_repo_slug "$1")" +pr_number="$2" + +if [[ ! "$repo_slug" =~ ^[^/]+/[^/]+$ ]]; then + echo "Expected GitHub repo slug like owner/repo or a GitHub repo URL, got: $1" >&2 + exit 1 +fi + +if [[ ! "$pr_number" =~ ^[0-9]+$ ]]; then + echo "PR number must be numeric, got: $pr_number" >&2 + exit 1 +fi + +repo_key="${repo_slug//\//-}" +mirror_dir="/work/repos/${repo_key}" +checkout_dir="${3:-/work/checkouts/${repo_key}/pr-${pr_number}}" +pr_ref="refs/remotes/origin/pr/${pr_number}" + +mkdir -p "$(dirname "$mirror_dir")" "$(dirname "$checkout_dir")" + +if [[ ! -d "$mirror_dir/.git" ]]; then + if command -v gh >/dev/null 2>&1; then + gh repo clone "$repo_slug" "$mirror_dir" -- --filter=blob:none + else + git clone --filter=blob:none "https://github.com/${repo_slug}.git" "$mirror_dir" + fi +fi + +git -C "$mirror_dir" fetch --force origin "pull/${pr_number}/head:${pr_ref}" + +if [[ -e "$checkout_dir" ]]; then + printf '%s\n' "$checkout_dir" + exit 0 +fi + +git -C "$mirror_dir" worktree add --detach "$checkout_dir" "$pr_ref" >/dev/null +printf '%s\n' "$checkout_dir" From 597c4b1d457d2207dee01863c6ade1078d7f6c83 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:27:34 -0500 Subject: [PATCH 41/51] Fix code block styles with robust prose overrides Previous attempt was being overridden by Tailwind prose/prose-invert CSS variables. This fix: - Overrides --tw-prose-pre-bg and --tw-prose-invert-pre-bg CSS variables on .paperclip-markdown to force dark background in both modes - Uses .paperclip-markdown pre with \!important for bulletproof overrides - Removes conflicting prose-pre: utility classes from MarkdownBody - Adds explicit pre code reset (inherit color/size, no background) - Verified visually with Playwright at desktop and mobile viewports Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 2 +- ui/src/index.css | 36 +++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 1242fa8a..683adc53 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) { return (
Date: Sun, 15 Mar 2026 14:33:22 -0500 Subject: [PATCH 42/51] Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/server/package.json b/server/package.json index a2387fe9..1887d64c 100644 --- a/server/package.json +++ b/server/package.json @@ -41,7 +41,6 @@ "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", - "@paperclipai/adapter-openclaw-gateway": "workspace:*", "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", From 16ab8c830325d36cb532e3e8701d9f6c59c683a6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:39:09 -0500 Subject: [PATCH 43/51] Dark theme for CodeMirror code blocks in MDXEditor The code blocks users see in issue documents are rendered by CodeMirror (via MDXEditor's codeMirrorPlugin), not by MarkdownBody. MDXEditor bundles cm6-theme-basic-light which gives them a white background. Added dark overrides for all CodeMirror elements: - .cm-editor: dark background (#1e1e2e), light text (#cdd6f4) - .cm-gutters: darker gutter with muted line numbers - .cm-activeLine, .cm-selectionBackground: subtle dark highlights - .cm-cursor: light cursor for visibility - Language selector dropdown: dark-themed to match - Reduced pre padding to 0 since CodeMirror handles its own spacing Uses \!important to beat CodeMirror's programmatically-injected theme styles (EditorView.theme generates high-specificity scoped selectors). Co-Authored-By: Paperclip --- ui/src/index.css | 50 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index 1172f103..c9ee652f 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -417,7 +417,7 @@ .paperclip-mdxeditor-content pre { margin: 0.4rem 0; - padding: 0.5rem 0.65rem; + padding: 0; border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent); border-radius: calc(var(--radius) - 3px); background: #1e1e2e; @@ -425,7 +425,46 @@ overflow-x: auto; } -/* MDXEditor code block language selector – keep it out of the way on small screens */ +/* Dark theme for CodeMirror code blocks inside the MDXEditor. + Overrides the default cm6-theme-basic-light that MDXEditor bundles. */ +.paperclip-mdxeditor .cm-editor { + background-color: #1e1e2e !important; + color: #cdd6f4 !important; + font-size: 0.78em; +} + +.paperclip-mdxeditor .cm-gutters { + background-color: #181825 !important; + color: #585b70 !important; + border-right: 1px solid #313244 !important; +} + +.paperclip-mdxeditor .cm-activeLineGutter { + background-color: #1e1e2e !important; +} + +.paperclip-mdxeditor .cm-activeLine { + background-color: color-mix(in oklab, #cdd6f4 5%, transparent) !important; +} + +.paperclip-mdxeditor .cm-cursor, +.paperclip-mdxeditor .cm-dropCursor { + border-left-color: #cdd6f4 !important; +} + +.paperclip-mdxeditor .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 25%, transparent) !important; +} + +.paperclip-mdxeditor .cm-focused .cm-selectionBackground { + background-color: color-mix(in oklab, #89b4fa 30%, transparent) !important; +} + +.paperclip-mdxeditor .cm-content { + caret-color: #cdd6f4; +} + +/* MDXEditor code block language selector – show on hover only */ .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"] { position: relative; } @@ -440,6 +479,13 @@ transition: opacity 150ms ease; } +.paperclip-mdxeditor-content [class*="_codeMirrorToolbar_"] select, +.paperclip-mdxeditor-content [class*="_codeBlockToolbar_"] select { + background-color: #313244; + color: #cdd6f4; + border-color: #45475a; +} + .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeMirrorToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeBlockToolbar_"], .paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:focus-within [class*="_codeMirrorToolbar_"], From c5cc191a08fbd9da2e1b68b525ffd48c14160bc0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 14:44:01 -0500 Subject: [PATCH 44/51] chore: ignore superset artifacts --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 066fcc68..06303bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ tmp/ # Playwright tests/e2e/test-results/ -tests/e2e/playwright-report/ \ No newline at end of file +tests/e2e/playwright-report/ +.superset/ \ No newline at end of file From 3bffe3e479c65987ab8f91cf356b00fc8debdbcc Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 20:30:54 -0500 Subject: [PATCH 45/51] docs: update documentation for accuracy after plugin system launch - README: mark plugin system as shipped in roadmap - SPEC: update adapter table with openclaw_gateway, gemini-local, hermes_local - SPEC: update plugin architecture section to reflect shipped status - Add .doc-review-cursor for future maintenance runs Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .doc-review-cursor | 1 + README.md | 2 +- doc/SPEC.md | 15 +++++++++------ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 .doc-review-cursor diff --git a/.doc-review-cursor b/.doc-review-cursor new file mode 100644 index 00000000..789f2092 --- /dev/null +++ b/.doc-review-cursor @@ -0,0 +1 @@ +16ab8c8 diff --git a/README.md b/README.md index 70ddee5f..391a0feb 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide. - ⚪ ClipMart - buy and sell entire agent companies - ⚪ Easy agent configurations / easier to understand - ⚪ Better support for harness engineering -- ⚪ Plugin system (e.g. if you want to add a knowledgebase, custom tracing, queues, etc) +- 🟢 Plugin system (e.g. if you want to add a knowledgebase, custom tracing, queues, etc) - ⚪ Better docs
diff --git a/doc/SPEC.md b/doc/SPEC.md index 33c24b3a..44cfe8a8 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -188,12 +188,15 @@ The heartbeat is a protocol, not a runtime. Paperclip defines how to initiate an Agent configuration includes an **adapter** that defines how Paperclip invokes the agent. Initial adapters: -| Adapter | Mechanism | Example | -| --------- | ----------------------- | --------------------------------------------- | -| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | -| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | +| Adapter | Mechanism | Example | +| -------------------- | ----------------------- | --------------------------------------------- | +| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | +| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | +| `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | +| `gemini-local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | +| `hermes_local` | Hermes agent process | Local Hermes agent | -The `process` and `http` adapters ship as defaults. Additional adapters can be added via the plugin system (see Plugin / Extension Architecture). +The `process` and `http` adapters ship as defaults. Additional adapters have been added for specific agent runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture). ### Adapter Interface @@ -429,7 +432,7 @@ The core Paperclip system must be extensible. Features like knowledge bases, ext - **Agent Adapter plugins** — new Adapter types can be registered via the plugin system - Plugin-registrable UI components (future) -This isn't a V1 deliverable (we're not building a plugin framework upfront), but the architecture should not paint us into a corner. Keep boundaries clean so extensions are possible. +The plugin framework has shipped. Plugins can register new adapter types, hook into lifecycle events, and contribute UI components (e.g. global toolbar buttons). A plugin SDK and CLI commands (`paperclipai plugin`) are available for authoring and installing plugins. --- From 52b12784a04706efce9a6013bc7f665126c849b2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 15 Mar 2026 21:08:19 -0500 Subject: [PATCH 46/51] =?UTF-8?q?docs:=20fix=20documentation=20drift=20?= =?UTF-8?q?=E2=80=94=20adapters,=20plugins,=20tech=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix gemini adapter name: `gemini-local` → `gemini_local` (matches registry.ts) - Move .doc-review-cursor to .gitignore (tooling state, not source) Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- .doc-review-cursor | 1 - .gitignore | 3 +++ doc/SPEC.md | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 .doc-review-cursor diff --git a/.doc-review-cursor b/.doc-review-cursor deleted file mode 100644 index 789f2092..00000000 --- a/.doc-review-cursor +++ /dev/null @@ -1 +0,0 @@ -16ab8c8 diff --git a/.gitignore b/.gitignore index 066fcc68..ecb6e9a7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ tmp/ .claude/settings.local.json .paperclip-local/ +# Doc maintenance cursor +.doc-review-cursor + # Playwright tests/e2e/test-results/ tests/e2e/playwright-report/ \ No newline at end of file diff --git a/doc/SPEC.md b/doc/SPEC.md index 44cfe8a8..82315bce 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -193,7 +193,7 @@ Agent configuration includes an **adapter** that defines how Paperclip invokes t | `process` | Execute a child process | `python run_agent.py --agent-id {id}` | | `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | | `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | -| `gemini-local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | +| `gemini_local` | Gemini CLI process | Local Gemini CLI with sandbox and approval | | `hermes_local` | Hermes agent process | Local Hermes agent | The `process` and `http` adapters ship as defaults. Additional adapters have been added for specific agent runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture). From ccb6729ec8f1004c10404327ebda077e3eab99b9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 16 Mar 2026 08:08:38 -0500 Subject: [PATCH 47/51] fix: use appType "custom" for Vite dev server so worktree branding is applied Vite's "spa" appType adds its own SPA fallback middleware that serves index.html directly, bypassing the custom catch-all route that calls applyUiBranding(). Changing to "custom" lets our route handle HTML serving, which injects the worktree-colored favicon and banner meta tags. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/app.ts b/server/src/app.ts index c2034eba..f0d8ee72 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -242,7 +242,7 @@ export async function createApp( const { createServer: createViteServer } = await import("vite"); const vite = await createViteServer({ root: uiRoot, - appType: "spa", + appType: "custom", server: { middlewareMode: true, hmr: { From e538329b0ad9f10477ab25deaf323a8577fce98d Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 16 Mar 2026 09:25:39 -0500 Subject: [PATCH 48/51] Use asset-backed company logos --- cli/src/__tests__/company-delete.test.ts | 1 + docs/api/companies.md | 8 +- .../db/src/migrations/0030_hot_slipstream.sql | 1 - .../db/src/migrations/0030_rich_magneto.sql | 12 ++ .../db/src/migrations/meta/0030_snapshot.json | 112 +++++++++++++- packages/db/src/migrations/meta/_journal.json | 4 +- packages/db/src/schema/companies.ts | 1 - packages/db/src/schema/company_logos.ts | 18 +++ packages/db/src/schema/index.ts | 1 + packages/shared/src/types/company.ts | 1 + packages/shared/src/validators/company.ts | 11 +- server/src/__tests__/assets.test.ts | 25 ++++ server/src/routes/assets.ts | 21 +-- server/src/services/companies.ts | 138 +++++++++++++++--- ui/src/api/companies.ts | 3 +- ui/src/context/CompanyContext.tsx | 3 - ui/src/pages/CompanySettings.tsx | 4 +- 17 files changed, 307 insertions(+), 57 deletions(-) delete mode 100644 packages/db/src/migrations/0030_hot_slipstream.sql create mode 100644 packages/db/src/migrations/0030_rich_magneto.sql create mode 100644 packages/db/src/schema/company_logos.ts diff --git a/cli/src/__tests__/company-delete.test.ts b/cli/src/__tests__/company-delete.test.ts index 65e5a021..18a98cea 100644 --- a/cli/src/__tests__/company-delete.test.ts +++ b/cli/src/__tests__/company-delete.test.ts @@ -14,6 +14,7 @@ function makeCompany(overrides: Partial): Company { spentMonthlyCents: 0, requireBoardApprovalForNewAgents: false, brandColor: null, + logoAssetId: null, logoUrl: null, createdAt: new Date(), updatedAt: new Date(), diff --git a/docs/api/companies.md b/docs/api/companies.md index efcac8c2..e48d0176 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -38,7 +38,8 @@ PATCH /api/companies/{companyId} { "name": "Updated Name", "description": "Updated description", - "budgetMonthlyCents": 100000 + "budgetMonthlyCents": 100000, + "logoAssetId": "b9f5e911-6de5-4cd0-8dc6-a55a13bc02f6" } ``` @@ -60,6 +61,8 @@ Valid image content types: - `image/gif` - `image/svg+xml` +Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`. + ## Archive Company ``` @@ -76,7 +79,8 @@ Archives a company. Archived companies are hidden from default listings. | `name` | string | Company name | | `description` | string | Company description | | `status` | string | `active`, `paused`, `archived` | -| `logoUrl` | string | Optional path or URL for the logo image | +| `logoAssetId` | string | Optional asset id for the stored logo image | +| `logoUrl` | string | Optional Paperclip asset content path for the stored logo image | | `budgetMonthlyCents` | number | Monthly budget limit | | `createdAt` | string | ISO timestamp | | `updatedAt` | string | ISO timestamp | diff --git a/packages/db/src/migrations/0030_hot_slipstream.sql b/packages/db/src/migrations/0030_hot_slipstream.sql deleted file mode 100644 index c00d29a3..00000000 --- a/packages/db/src/migrations/0030_hot_slipstream.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "companies" ADD COLUMN "logo_url" text; \ No newline at end of file diff --git a/packages/db/src/migrations/0030_rich_magneto.sql b/packages/db/src/migrations/0030_rich_magneto.sql new file mode 100644 index 00000000..76d44de7 --- /dev/null +++ b/packages/db/src/migrations/0030_rich_magneto.sql @@ -0,0 +1,12 @@ +CREATE TABLE "company_logos" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "asset_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "company_logos" ADD CONSTRAINT "company_logos_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "company_logos" ADD CONSTRAINT "company_logos_asset_id_assets_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "company_logos_company_uq" ON "company_logos" USING btree ("company_id");--> statement-breakpoint +CREATE UNIQUE INDEX "company_logos_asset_uq" ON "company_logos" USING btree ("asset_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0030_snapshot.json b/packages/db/src/migrations/meta/0030_snapshot.json index 539d4be0..4f21ce46 100644 --- a/packages/db/src/migrations/meta/0030_snapshot.json +++ b/packages/db/src/migrations/meta/0030_snapshot.json @@ -1,5 +1,5 @@ { - "id": "eb9b85ec-2048-4168-bff1-0e987773342a", + "id": "ff007d90-e1a0-4df3-beab-a5be4a47273c", "prevId": "fdb36f4e-6463-497d-b704-22d33be9b450", "version": "7", "dialect": "postgresql", @@ -2140,12 +2140,6 @@ "primaryKey": false, "notNull": false }, - "logo_url": { - "name": "logo_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -2185,6 +2179,110 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": 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": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.company_memberships": { "name": "company_memberships", "schema": "", diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 852fa998..696c437a 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -215,8 +215,8 @@ { "idx": 30, "version": "7", - "when": 1773668505562, - "tag": "0030_hot_slipstream", + "when": 1773670925214, + "tag": "0030_rich_magneto", "breakpoints": true } ] diff --git a/packages/db/src/schema/companies.ts b/packages/db/src/schema/companies.ts index 3e75d4e6..29c82b71 100644 --- a/packages/db/src/schema/companies.ts +++ b/packages/db/src/schema/companies.ts @@ -15,7 +15,6 @@ export const companies = pgTable( .notNull() .default(true), brandColor: text("brand_color"), - logoUrl: text("logo_url"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, diff --git a/packages/db/src/schema/company_logos.ts b/packages/db/src/schema/company_logos.ts new file mode 100644 index 00000000..13e0abe0 --- /dev/null +++ b/packages/db/src/schema/company_logos.ts @@ -0,0 +1,18 @@ +import { pgTable, uuid, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { assets } from "./assets.js"; + +export const companyLogos = pgTable( + "company_logos", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }), + assetId: uuid("asset_id").notNull().references(() => assets.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyUq: uniqueIndex("company_logos_company_uq").on(table.companyId), + assetUq: uniqueIndex("company_logos_asset_uq").on(table.assetId), + }), +); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index f173db45..422d7cdd 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -1,4 +1,5 @@ export { companies } from "./companies.js"; +export { companyLogos } from "./company_logos.js"; export { authUsers, authSessions, authAccounts, authVerifications } from "./auth.js"; export { instanceUserRoles } from "./instance_user_roles.js"; export { agents } from "./agents.js"; diff --git a/packages/shared/src/types/company.ts b/packages/shared/src/types/company.ts index 3002c7d3..e9022b93 100644 --- a/packages/shared/src/types/company.ts +++ b/packages/shared/src/types/company.ts @@ -11,6 +11,7 @@ export interface Company { spentMonthlyCents: number; requireBoardApprovalForNewAgents: boolean; brandColor: string | null; + logoAssetId: string | null; logoUrl: string | null; createdAt: Date; updatedAt: Date; diff --git a/packages/shared/src/validators/company.ts b/packages/shared/src/validators/company.ts index 65083c91..bb4851f4 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -1,19 +1,12 @@ import { z } from "zod"; import { COMPANY_STATUSES } from "../constants.js"; -const logoUrlSchema = z - .string() - .trim() - .max(2048) - .regex(/^\/api\/assets\/[^\s]+$|^https?:\/\/[^\s]+$/) - .nullable() - .optional(); +const logoAssetIdSchema = z.string().uuid().nullable().optional(); export const createCompanySchema = z.object({ name: z.string().min(1), description: z.string().optional().nullable(), budgetMonthlyCents: z.number().int().nonnegative().optional().default(0), - logoUrl: logoUrlSchema, }); export type CreateCompany = z.infer; @@ -25,7 +18,7 @@ export const updateCompanySchema = createCompanySchema spentMonthlyCents: z.number().int().nonnegative().optional(), requireBoardApprovalForNewAgents: z.boolean().optional(), brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(), - logoUrl: logoUrlSchema, + logoAssetId: logoAssetIdSchema, }); export type UpdateCompany = z.infer; diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index 85ddf56a..745b9a76 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -189,6 +189,31 @@ describe("POST /api/companies/:companyId/assets/images", () => { expect(createAssetMock).not.toHaveBeenCalled(); }); + it("allows supported non-image attachments outside the company logo namespace", async () => { + const text = createStorageService("text/plain"); + const app = createApp(text); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "text/plain", + originalFilename: "note.txt", + }); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "issues/drafts") + .attach("file", Buffer.from("hello"), "note.txt"); + + expect(res.status).toBe(201); + expect(text.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/issues/drafts", + originalFilename: "note.txt", + contentType: "text/plain", + body: expect.any(Buffer), + }); + }); + it("rejects SVG image uploads that cannot be sanitized", async () => { const app = createApp(createStorageService("image/svg+xml")); createAssetMock.mockResolvedValue(createAsset()); diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index 67c9751f..0ec6ffb1 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -116,7 +116,19 @@ export function assetRoutes(db: Db, storage: StorageService) { return; } + const parsedMeta = createAssetImageMetadataSchema.safeParse(req.body ?? {}); + if (!parsedMeta.success) { + res.status(400).json({ error: "Invalid image metadata", details: parsedMeta.error.issues }); + return; + } + + const namespaceSuffix = parsedMeta.data.namespace ?? "general"; + const isCompanyLogoNamespace = namespaceSuffix === "companies" || namespaceSuffix.startsWith("companies/"); const contentType = (file.mimetype || "").toLowerCase(); + if (isCompanyLogoNamespace && !contentType.startsWith("image/")) { + res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` }); + return; + } if (contentType !== SVG_CONTENT_TYPE && !isAllowedContentType(contentType)) { res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` }); return; @@ -134,15 +146,6 @@ export function assetRoutes(db: Db, storage: StorageService) { res.status(422).json({ error: "Image is empty" }); return; } - - const parsedMeta = createAssetImageMetadataSchema.safeParse(req.body ?? {}); - if (!parsedMeta.success) { - res.status(400).json({ error: "Invalid image metadata", details: parsedMeta.error.issues }); - return; - } - - const namespaceSuffix = parsedMeta.data.namespace ?? "general"; - const isCompanyLogoNamespace = namespaceSuffix === "companies" || namespaceSuffix.startsWith("companies/"); if (isCompanyLogoNamespace && fileBody.length > MAX_COMPANY_LOGO_BYTES) { res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); return; diff --git a/server/src/services/companies.ts b/server/src/services/companies.ts index 38a1f12f..42c4e972 100644 --- a/server/src/services/companies.ts +++ b/server/src/services/companies.ts @@ -2,6 +2,8 @@ import { eq, count } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { companies, + companyLogos, + assets, agents, agentApiKeys, agentRuntimeState, @@ -23,10 +25,41 @@ import { principalPermissionGrants, companyMemberships, } from "@paperclipai/db"; +import { notFound, unprocessable } from "../errors.js"; export function companyService(db: Db) { const ISSUE_PREFIX_FALLBACK = "CMP"; + const companySelection = { + id: companies.id, + name: companies.name, + description: companies.description, + status: companies.status, + issuePrefix: companies.issuePrefix, + issueCounter: companies.issueCounter, + budgetMonthlyCents: companies.budgetMonthlyCents, + spentMonthlyCents: companies.spentMonthlyCents, + requireBoardApprovalForNewAgents: companies.requireBoardApprovalForNewAgents, + brandColor: companies.brandColor, + logoAssetId: companyLogos.assetId, + createdAt: companies.createdAt, + updatedAt: companies.updatedAt, + }; + + function enrichCompany(company: T) { + return { + ...company, + logoUrl: company.logoAssetId ? `/api/assets/${company.logoAssetId}/content` : null, + }; + } + + function getCompanyQuery(database: Pick) { + return database + .select(companySelection) + .from(companies) + .leftJoin(companyLogos, eq(companyLogos.companyId, companies.id)); + } + function deriveIssuePrefixBase(name: string) { const normalized = name.toUpperCase().replace(/[^A-Z]/g, ""); return normalized.slice(0, 3) || ISSUE_PREFIX_FALLBACK; @@ -70,32 +103,97 @@ export function companyService(db: Db) { } return { - list: () => db.select().from(companies), + list: () => + getCompanyQuery(db).then((rows) => rows.map((row) => enrichCompany(row))), getById: (id: string) => - db - .select() - .from(companies) + getCompanyQuery(db) .where(eq(companies.id, id)) - .then((rows) => rows[0] ?? null), + .then((rows) => (rows[0] ? enrichCompany(rows[0]) : null)), - create: async (data: typeof companies.$inferInsert) => createCompanyWithUniquePrefix(data), + create: async (data: typeof companies.$inferInsert) => { + const created = await createCompanyWithUniquePrefix(data); + const row = await getCompanyQuery(db) + .where(eq(companies.id, created.id)) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Company not found after creation"); + return enrichCompany(row); + }, - update: (id: string, data: Partial) => - db - .update(companies) - .set({ ...data, updatedAt: new Date() }) - .where(eq(companies.id, id)) - .returning() - .then((rows) => rows[0] ?? null), + update: ( + id: string, + data: Partial & { logoAssetId?: string | null }, + ) => + db.transaction(async (tx) => { + const existing = await getCompanyQuery(tx) + .where(eq(companies.id, id)) + .then((rows) => rows[0] ?? null); + if (!existing) return null; + + const { logoAssetId, ...companyPatch } = data; + + if (logoAssetId !== undefined && logoAssetId !== null) { + const nextLogoAsset = await tx + .select({ id: assets.id, companyId: assets.companyId }) + .from(assets) + .where(eq(assets.id, logoAssetId)) + .then((rows) => rows[0] ?? null); + if (!nextLogoAsset) throw notFound("Logo asset not found"); + if (nextLogoAsset.companyId !== existing.id) { + throw unprocessable("Logo asset must belong to the same company"); + } + } + + const updated = await tx + .update(companies) + .set({ ...companyPatch, updatedAt: new Date() }) + .where(eq(companies.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + if (!updated) return null; + + if (logoAssetId === null) { + await tx.delete(companyLogos).where(eq(companyLogos.companyId, id)); + } else if (logoAssetId !== undefined) { + await tx + .insert(companyLogos) + .values({ + companyId: id, + assetId: logoAssetId, + }) + .onConflictDoUpdate({ + target: companyLogos.companyId, + set: { + assetId: logoAssetId, + updatedAt: new Date(), + }, + }); + } + + if (logoAssetId !== undefined && existing.logoAssetId && existing.logoAssetId !== logoAssetId) { + await tx.delete(assets).where(eq(assets.id, existing.logoAssetId)); + } + + return enrichCompany({ + ...updated, + logoAssetId: logoAssetId === undefined ? existing.logoAssetId : logoAssetId, + }); + }), archive: (id: string) => - db - .update(companies) - .set({ status: "archived", updatedAt: new Date() }) - .where(eq(companies.id, id)) - .returning() - .then((rows) => rows[0] ?? null), + db.transaction(async (tx) => { + const updated = await tx + .update(companies) + .set({ status: "archived", updatedAt: new Date() }) + .where(eq(companies.id, id)) + .returning() + .then((rows) => rows[0] ?? null); + if (!updated) return null; + const row = await getCompanyQuery(tx) + .where(eq(companies.id, id)) + .then((rows) => rows[0] ?? null); + return row ? enrichCompany(row) : null; + }), remove: (id: string) => db.transaction(async (tx) => { @@ -116,6 +214,8 @@ export function companyService(db: Db) { await tx.delete(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, id)); await tx.delete(companyMemberships).where(eq(companyMemberships.companyId, id)); await tx.delete(issues).where(eq(issues.companyId, id)); + await tx.delete(companyLogos).where(eq(companyLogos.companyId, id)); + await tx.delete(assets).where(eq(assets.companyId, id)); await tx.delete(goals).where(eq(goals.companyId, id)); await tx.delete(projects).where(eq(projects.companyId, id)); await tx.delete(agents).where(eq(agents.companyId, id)); diff --git a/ui/src/api/companies.ts b/ui/src/api/companies.ts index eb53524b..bc21414e 100644 --- a/ui/src/api/companies.ts +++ b/ui/src/api/companies.ts @@ -18,7 +18,6 @@ export const companiesApi = { name: string; description?: string | null; budgetMonthlyCents?: number; - logoUrl?: string | null; }) => api.post("/companies", data), update: ( @@ -26,7 +25,7 @@ export const companiesApi = { data: Partial< Pick< Company, - "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" | "logoUrl" + "name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents" | "brandColor" | "logoAssetId" > >, ) => api.patch(`/companies/${companyId}`, data), diff --git a/ui/src/context/CompanyContext.tsx b/ui/src/context/CompanyContext.tsx index 6785f8ff..86afa175 100644 --- a/ui/src/context/CompanyContext.tsx +++ b/ui/src/context/CompanyContext.tsx @@ -28,7 +28,6 @@ interface CompanyContextValue { name: string; description?: string | null; budgetMonthlyCents?: number; - logoUrl?: string | null; }) => Promise; } @@ -90,7 +89,6 @@ export function CompanyProvider({ children }: { children: ReactNode }) { name: string; description?: string | null; budgetMonthlyCents?: number; - logoUrl?: string | null; }) => companiesApi.create(data), onSuccess: (company) => { @@ -104,7 +102,6 @@ export function CompanyProvider({ children }: { children: ReactNode }) { name: string; description?: string | null; budgetMonthlyCents?: number; - logoUrl?: string | null; }) => { return createMutation.mutateAsync(data); }, diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index c8a5d5aa..3cd310ca 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -141,7 +141,7 @@ export function CompanySettings() { mutationFn: (file: File) => assetsApi .uploadImage(selectedCompanyId!, file, "companies") - .then((asset) => companiesApi.update(selectedCompanyId!, { logoUrl: asset.contentPath })), + .then((asset) => companiesApi.update(selectedCompanyId!, { logoAssetId: asset.assetId })), onSuccess: (company) => { syncLogoState(company.logoUrl); setLogoUploadError(null); @@ -149,7 +149,7 @@ export function CompanySettings() { }); const clearLogoMutation = useMutation({ - mutationFn: () => companiesApi.update(selectedCompanyId!, { logoUrl: null }), + mutationFn: () => companiesApi.update(selectedCompanyId!, { logoAssetId: null }), onSuccess: (company) => { setLogoUploadError(null); syncLogoState(company.logoUrl); From 2d548a9da0148c43ef80c3202fef1706eaef76d3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 16 Mar 2026 09:37:23 -0500 Subject: [PATCH 49/51] Drop lockfile from PR branch --- pnpm-lock.yaml | 782 +------------------------------------------------ 1 file changed, 9 insertions(+), 773 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddd2d955..f6820f52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) cli: dependencies: @@ -244,181 +244,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) - - packages/plugins/create-paperclip-plugin: - dependencies: - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../sdk - devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/plugins/examples/plugin-authoring-smoke-example: - dependencies: - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../../sdk - react: - specifier: '>=18' - version: 19.2.4 - devDependencies: - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.3(rollup@4.57.1) - '@rollup/plugin-typescript': - specifier: ^12.1.2 - version: 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - '@types/react': - specifier: ^19.0.8 - version: 19.2.14 - esbuild: - specifier: ^0.27.3 - version: 0.27.3 - rollup: - specifier: ^4.38.0 - version: 4.57.1 - tslib: - specifier: ^2.8.1 - version: 2.8.1 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - vitest: - specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) - - packages/plugins/examples/plugin-file-browser-example: - dependencies: - '@codemirror/lang-javascript': - specifier: ^6.2.2 - version: 6.2.4 - '@codemirror/language': - specifier: ^6.11.0 - version: 6.12.1 - '@codemirror/state': - specifier: ^6.4.0 - version: 6.5.4 - '@codemirror/view': - specifier: ^6.28.0 - version: 6.39.15 - '@lezer/highlight': - specifier: ^1.2.1 - version: 1.2.3 - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../../sdk - codemirror: - specifier: ^6.0.1 - version: 6.0.2 - devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - '@types/react': - specifier: ^19.0.8 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.14) - esbuild: - specifier: ^0.27.3 - version: 0.27.3 - react: - specifier: ^19.0.0 - version: 19.2.4 - react-dom: - specifier: ^19.0.0 - version: 19.2.4(react@19.2.4) - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/plugins/examples/plugin-hello-world-example: - dependencies: - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../../sdk - devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - '@types/react': - specifier: ^19.0.8 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.14) - react: - specifier: ^19.0.0 - version: 19.2.4 - react-dom: - specifier: ^19.0.0 - version: 19.2.4(react@19.2.4) - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/plugins/examples/plugin-kitchen-sink-example: - dependencies: - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../../sdk - '@paperclipai/shared': - specifier: workspace:* - version: link:../../../shared - devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - '@types/react': - specifier: ^19.0.8 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.14) - esbuild: - specifier: ^0.27.3 - version: 0.27.3 - react: - specifier: ^19.0.0 - version: 19.2.4 - react-dom: - specifier: ^19.0.0 - version: 19.2.4(react@19.2.4) - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/plugins/sdk: - dependencies: - '@paperclipai/shared': - specifier: workspace:* - version: link:../../shared - react: - specifier: '>=18' - version: 19.2.4 - zod: - specifier: ^3.24.2 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^24.6.0 - version: 24.12.0 - '@types/react': - specifier: ^19.0.8 - version: 19.2.14 - typescript: - specifier: ^5.7.3 - version: 5.9.3 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) packages/shared: dependencies: @@ -462,30 +288,15 @@ importers: '@paperclipai/db': specifier: workspace:* version: link:../packages/db - '@paperclipai/plugin-sdk': - specifier: workspace:* - version: link:../packages/plugins/sdk '@paperclipai/shared': specifier: workspace:* version: link:../packages/shared - ajv: - specifier: ^8.18.0 - version: 8.18.0 - ajv-formats: - specifier: ^3.0.1 - version: 3.0.1(ajv@8.18.0) better-auth: specifier: 1.4.18 - version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)) - chokidar: - specifier: ^4.0.3 - version: 4.0.3 + version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) detect-port: specifier: ^2.1.0 version: 2.1.0 - dompurify: - specifier: ^3.3.2 - version: 3.3.2 dotenv: specifier: ^17.0.1 version: 17.3.1 @@ -498,12 +309,6 @@ importers: express: specifier: ^5.1.0 version: 5.2.1 - hermes-paperclip-adapter: - specifier: 0.1.1 - version: 0.1.1 - jsdom: - specifier: ^28.1.0 - version: 28.1.0(@noble/hashes@2.0.1) multer: specifier: ^2.0.2 version: 2.0.2 @@ -532,9 +337,6 @@ importers: '@types/express-serve-static-core': specifier: ^5.0.0 version: 5.1.1 - '@types/jsdom': - specifier: ^28.0.0 - version: 28.0.0 '@types/multer': specifier: ^2.0.0 version: 2.0.0 @@ -564,7 +366,7 @@ importers: version: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) ui: dependencies: @@ -679,26 +481,13 @@ importers: version: 6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) packages: - '@acemir/cssom@0.9.31': - resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} - '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@asamuzakjp/css-color@5.0.1': - resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@6.8.1': - resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -977,10 +766,6 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@bramus/specificity@2.4.2': - resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} - hasBin: true - '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -1162,42 +947,6 @@ packages: react: ^16.8.0 || ^17 || ^18 || ^19 react-dom: ^16.8.0 || ^17 || ^18 || ^19 - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} - engines: {node: '>=20.19.0'} - - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-color-parser@4.0.2': - resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-syntax-patches-for-csstree@1.1.1': - resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} - peerDependencies: - css-tree: ^3.2.1 - peerDependenciesMeta: - css-tree: - optional: true - - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} - engines: {node: '>=20.19.0'} - '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -1729,15 +1478,6 @@ packages: cpu: [x64] os: [win32] - '@exodus/bytes@1.15.0': - resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - '@noble/hashes': ^1.8.0 || ^2.0.0 - peerDependenciesMeta: - '@noble/hashes': - optional: true - '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -1965,9 +1705,6 @@ packages: '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - '@paperclipai/adapter-utils@0.3.1': - resolution: {integrity: sha512-W66k+hJkQE8ma0asM/Sd90AC8HHy/BLG/sd0aOC+rDWw+gOasQyUkTnDoPv1zhQuTyKEEvLFV6ByOOKqEiAz/A==} - '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -2699,37 +2436,6 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-typescript@12.3.0': - resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -3324,9 +3030,6 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/jsdom@28.0.0': - resolution: {integrity: sha512-A8TBQQC/xAOojy9kM8E46cqT00sF0h7dWjV8t8BJhUi2rG6JRh7XXQo/oLoENuZIQEpXsxLccLCnknyQd7qssQ==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3365,9 +3068,6 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -3380,9 +3080,6 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3451,21 +3148,6 @@ packages: resolution: {integrity: sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==} engines: {node: '>= 16.0.0'} - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - anser@2.3.5: resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} @@ -3596,9 +3278,6 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -3682,10 +3361,6 @@ packages: chevrotain@11.1.2: resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3795,19 +3470,11 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-tree@3.2.1: - resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - cssstyle@6.2.0: - resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} - engines: {node: '>=20'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -3971,10 +3638,6 @@ packages: dagre-d3-es@7.0.13: resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -3990,9 +3653,6 @@ packages: supports-color: optional: true - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -4000,10 +3660,6 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -4206,10 +3862,6 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4289,9 +3941,6 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -4322,9 +3971,6 @@ packages: fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4332,9 +3978,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.3.6: resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} hasBin: true @@ -4469,14 +4112,6 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} - hermes-paperclip-adapter@0.1.1: - resolution: {integrity: sha512-kbdX349VxExSkVL8n4RwTpP9fUBf2yWpsTsJp02X12A9NynRJatlpYqt0vEkFyE/X7qEXqdJvpBm9tlvUHahsA==} - engines: {node: '>=20.0.0'} - - html-encoding-sniffer@6.0.0: - resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -4484,14 +4119,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -4538,10 +4165,6 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4570,9 +4193,6 @@ packages: engines: {node: '>=14.16'} hasBin: true - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4581,9 +4201,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -4630,23 +4247,11 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@28.1.0: - resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4778,10 +4383,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@11.2.7: - resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} - engines: {node: 20 || >=22} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4866,9 +4467,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdn-data@2.27.1: - resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -5129,12 +4727,6 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - parse5@8.0.0: - resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5150,9 +4742,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -5312,10 +4901,6 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -5445,10 +5030,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -5465,10 +5046,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5476,11 +5053,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5527,10 +5099,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -5689,13 +5257,6 @@ packages: resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -5742,13 +5303,6 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@7.0.26: - resolution: {integrity: sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==} - - tldts@7.0.26: - resolution: {integrity: sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==} - hasBin: true - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5757,14 +5311,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} - engines: {node: '>=16'} - - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -5811,13 +5357,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici-types@7.24.4: - resolution: {integrity: sha512-cRaY9PagdEZoRmcwzk3tUV3SVGrVQkR6bcSilav/A0vXsfpW4Lvd0BvgRMwTEDTLLGN+QdyBTG+nnvTgJhdt6w==} - - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} - unidiff@1.0.4: resolution: {integrity: sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ==} @@ -6039,22 +5578,6 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} - - webidl-conversions@8.0.1: - resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} - engines: {node: '>=20'} - - whatwg-mimetype@5.0.0: - resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} - engines: {node: '>=20'} - - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6084,13 +5607,6 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} - xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} - - xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6113,31 +5629,11 @@ packages: snapshots: - '@acemir/cssom@0.9.31': {} - '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@asamuzakjp/css-color@5.0.1': - dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.7 - - '@asamuzakjp/dom-selector@6.8.1': - dependencies: - '@asamuzakjp/nwsapi': 2.3.9 - bidi-js: 1.0.3 - css-tree: 3.2.1 - is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 - - '@asamuzakjp/nwsapi@2.3.9': {} - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -6768,10 +6264,6 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} - '@bramus/specificity@2.4.2': - dependencies: - css-tree: 3.2.1 - '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 @@ -7237,30 +6729,6 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-is: 17.0.2 - '@csstools/color-helpers@6.0.2': {} - - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': - optionalDependencies: - css-tree: 3.2.1 - - '@csstools/css-tokenizer@4.0.0': {} - '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -7549,10 +7017,6 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)': - optionalDependencies: - '@noble/hashes': 2.0.1 - '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -7966,8 +7430,6 @@ snapshots: '@open-draft/deferred-promise@2.2.0': {} - '@paperclipai/adapter-utils@0.3.1': {} - '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -8750,33 +8212,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/plugin-node-resolve@16.0.3(rollup@4.57.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.11 - optionalDependencies: - rollup: 4.57.1 - - '@rollup/plugin-typescript@12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - resolve: 1.22.11 - typescript: 5.9.3 - optionalDependencies: - rollup: 4.57.1 - tslib: 2.8.1 - - '@rollup/pluginutils@5.3.0(rollup@4.57.1)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.57.1 - '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -9461,13 +8896,6 @@ snapshots: '@types/http-errors@2.0.5': {} - '@types/jsdom@28.0.0': - dependencies: - '@types/node': 25.2.3 - '@types/tough-cookie': 4.0.5 - parse5: 7.3.0 - undici-types: 7.24.4 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -9506,8 +8934,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/resolve@1.20.2': {} - '@types/send@1.2.1': dependencies: '@types/node': 25.2.3 @@ -9529,8 +8955,6 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/tough-cookie@4.0.5': {} - '@types/trusted-types@2.0.7': optional: true @@ -9619,19 +9043,6 @@ snapshots: address@2.0.3: {} - agent-base@7.1.4: {} - - ajv-formats@3.0.1(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - - ajv@8.18.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - anser@2.3.5: {} ansi-colors@4.1.3: {} @@ -9668,7 +9079,7 @@ snapshots: baseline-browser-mapping@2.9.19: {} - better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)): + better-auth@1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -9688,7 +9099,7 @@ snapshots: pg: 8.18.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) better-call@1.1.8(zod@4.3.6): dependencies: @@ -9703,10 +9114,6 @@ snapshots: dependencies: is-windows: 1.0.2 - bidi-js@1.0.3: - dependencies: - require-from-string: 2.0.2 - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -9802,10 +9209,6 @@ snapshots: '@chevrotain/utils': 11.1.2 lodash-es: 4.17.23 - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -9907,20 +9310,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-tree@3.2.1: - dependencies: - mdn-data: 2.27.1 - source-map-js: 1.2.1 - cssesc@3.0.0: {} - cssstyle@6.2.0: - dependencies: - '@asamuzakjp/css-color': 5.0.1 - '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) - css-tree: 3.2.1 - lru-cache: 11.2.7 - csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): @@ -10112,13 +9503,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.17.23 - data-urls@7.0.0(@noble/hashes@2.0.1): - dependencies: - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) - transitivePeerDependencies: - - '@noble/hashes' - dateformat@4.6.3: {} dayjs@1.11.19: {} @@ -10127,16 +9511,12 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js@10.6.0: {} - decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 deep-eql@5.0.2: {} - deepmerge@4.3.1: {} - default-browser-id@5.0.1: {} default-browser@5.5.0: @@ -10260,8 +9640,6 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - entities@6.0.1: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -10411,8 +9789,6 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 - estree-walker@2.0.2: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -10469,8 +9845,6 @@ snapshots: fast-copy@4.0.2: {} - fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -10481,8 +9855,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.0: {} - fast-xml-parser@5.3.6: dependencies: strnum: 2.1.2 @@ -10642,17 +10014,6 @@ snapshots: help-me@5.0.0: {} - hermes-paperclip-adapter@0.1.1: - dependencies: - '@paperclipai/adapter-utils': 0.3.1 - picocolors: 1.1.1 - - html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): - dependencies: - '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) - transitivePeerDependencies: - - '@noble/hashes' - html-url-attributes@3.0.1: {} http-errors@2.0.1: @@ -10663,20 +10024,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - human-id@4.1.3: {} iconv-lite@0.6.3: @@ -10710,10 +10057,6 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -10732,14 +10075,10 @@ snapshots: dependencies: is-docker: 3.0.0 - is-module@1.0.0: {} - is-number@7.0.0: {} is-plain-obj@4.1.0: {} - is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} is-subdir@1.2.0: @@ -10775,37 +10114,8 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@28.1.0(@noble/hashes@2.0.1): - dependencies: - '@acemir/cssom': 0.9.31 - '@asamuzakjp/dom-selector': 6.8.1 - '@bramus/specificity': 2.4.2 - '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) - cssstyle: 6.2.0 - data-urls: 7.0.0(@noble/hashes@2.0.1) - decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - is-potential-custom-element-name: 1.0.1 - parse5: 8.0.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.24.4 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 8.0.1 - whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@2.0.1) - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - '@noble/hashes' - - supports-color - jsesc@3.1.0: {} - json-schema-traverse@1.0.0: {} - json5@2.2.3: {} jsonfile@4.0.0: @@ -10905,8 +10215,6 @@ snapshots: loupe@3.2.1: {} - lru-cache@11.2.7: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -11119,8 +10427,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdn-data@2.27.1: {} - media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -11559,14 +10865,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - parse5@8.0.0: - dependencies: - entities: 6.0.1 - parseurl@1.3.3: {} path-data-parser@0.1.0: {} @@ -11575,8 +10873,6 @@ snapshots: path-key@3.1.1: {} - path-parse@1.0.7: {} - path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -11745,8 +11041,6 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - punycode@2.3.1: {} - qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -11927,8 +11221,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readdirp@4.1.2: {} - real-require@0.2.0: {} remark-gfm@4.0.1: @@ -11965,18 +11257,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - require-from-string@2.0.2: {} - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} robust-predicates@3.0.2: {} @@ -12049,10 +11333,6 @@ snapshots: safer-buffer@2.1.2: {} - saxes@6.0.0: - dependencies: - xmlchars: 2.2.0 - scheduler@0.27.0: {} secure-json-parse@4.1.0: {} @@ -12230,10 +11510,6 @@ snapshots: transitivePeerDependencies: - supports-color - supports-preserve-symlinks-flag@1.0.0: {} - - symbol-tree@3.2.4: {} - tabbable@6.4.0: {} tailwind-merge@3.4.1: {} @@ -12265,26 +11541,12 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@7.0.26: {} - - tldts@7.0.26: - dependencies: - tldts-core: 7.0.26 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toidentifier@1.0.1: {} - tough-cookie@6.0.1: - dependencies: - tldts: 7.0.26 - - tr46@6.0.0: - dependencies: - punycode: 2.3.1 - trim-lines@3.0.1: {} trough@2.2.0: {} @@ -12323,10 +11585,6 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.24.4: {} - - undici@7.24.4: {} - unidiff@1.0.4: dependencies: diff: 5.2.2 @@ -12522,7 +11780,7 @@ snapshots: lightningcss: 1.30.2 tsx: 4.21.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -12550,7 +11808,6 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.12.0 - jsdom: 28.1.0(@noble/hashes@2.0.1) transitivePeerDependencies: - jiti - less @@ -12565,7 +11822,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -12593,7 +11850,6 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 25.2.3 - jsdom: 28.1.0(@noble/hashes@2.0.1) transitivePeerDependencies: - jiti - less @@ -12627,22 +11883,6 @@ snapshots: w3c-keyname@2.2.8: {} - w3c-xmlserializer@5.0.0: - dependencies: - xml-name-validator: 5.0.0 - - webidl-conversions@8.0.1: {} - - whatwg-mimetype@5.0.0: {} - - whatwg-url@16.0.1(@noble/hashes@2.0.1): - dependencies: - '@exodus/bytes': 1.15.0(@noble/hashes@2.0.1) - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - which@2.0.2: dependencies: isexe: 2.0.0 @@ -12661,10 +11901,6 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} - xtend@4.0.2: {} yallist@3.1.1: {} From 4dfd862f11da853e606be69ee919d4d533b20856 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 16 Mar 2026 10:05:14 -0500 Subject: [PATCH 50/51] Address Greptile company logo feedback --- docs/api/companies.md | 2 +- server/src/__tests__/assets.test.ts | 131 ++++++++++++++++------------ server/src/routes/assets.ts | 128 +++++++++++++++++++++++++-- ui/src/api/assets.ts | 12 ++- ui/src/pages/CompanySettings.tsx | 6 +- 5 files changed, 208 insertions(+), 71 deletions(-) diff --git a/docs/api/companies.md b/docs/api/companies.md index e48d0176..ca9fa2f1 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -48,7 +48,7 @@ PATCH /api/companies/{companyId} Upload an image for a company icon and store it as that company’s logo. ``` -POST /api/companies/{companyId}/assets/images +POST /api/companies/{companyId}/logo Content-Type: multipart/form-data ``` diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index 745b9a76..b6f740b7 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -92,7 +92,62 @@ describe("POST /api/companies/:companyId/assets/images", () => { const res = await request(app) .post("/api/companies/company-1/assets/images") - .field("namespace", "companies") + .field("namespace", "goals") + .attach("file", Buffer.from("png"), "logo.png"); + + expect(res.status).toBe(201); + expect(res.body.contentPath).toBe("/api/assets/asset-1/content"); + expect(createAssetMock).toHaveBeenCalledTimes(1); + expect(png.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/goals", + originalFilename: "logo.png", + contentType: "image/png", + body: expect.any(Buffer), + }); + }); + + it("allows supported non-image attachments outside the company logo flow", async () => { + const text = createStorageService("text/plain"); + const app = createApp(text); + + createAssetMock.mockResolvedValue({ + ...createAsset(), + contentType: "text/plain", + originalFilename: "note.txt", + }); + + const res = await request(app) + .post("/api/companies/company-1/assets/images") + .field("namespace", "issues/drafts") + .attach("file", Buffer.from("hello"), { filename: "note.txt", contentType: "text/plain" }); + + expect(res.status).toBe(201); + expect(text.putFile).toHaveBeenCalledWith({ + companyId: "company-1", + namespace: "assets/issues/drafts", + originalFilename: "note.txt", + contentType: "text/plain", + body: expect.any(Buffer), + }); + }); +}); + +describe("POST /api/companies/:companyId/logo", () => { + afterEach(() => { + createAssetMock.mockReset(); + getAssetByIdMock.mockReset(); + logActivityMock.mockReset(); + }); + + it("accepts PNG logo uploads and returns an asset path", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); + + createAssetMock.mockResolvedValue(createAsset()); + + const res = await request(app) + .post("/api/companies/company-1/logo") .attach("file", Buffer.from("png"), "logo.png"); expect(res.status).toBe(201); @@ -107,7 +162,7 @@ describe("POST /api/companies/:companyId/assets/images", () => { }); }); - it("sanitizes SVG image uploads before storing them", async () => { + it("sanitizes SVG logo uploads before storing them", async () => { const svg = createStorageService("image/svg+xml"); const app = createApp(svg); @@ -118,8 +173,7 @@ describe("POST /api/companies/:companyId/assets/images", () => { }); const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "companies") + .post("/api/companies/company-1/logo") .attach( "file", Buffer.from( @@ -141,47 +195,38 @@ describe("POST /api/companies/:companyId/assets/images", () => { expect(body).not.toContain("https://evil.example/"); }); - it("rejects files larger than 100 KB", async () => { + it("allows a logo exactly 100 KB in size", async () => { + const png = createStorageService("image/png"); + const app = createApp(png); + createAssetMock.mockResolvedValue(createAsset()); + + const file = Buffer.alloc(100 * 1024, "a"); + const res = await request(app) + .post("/api/companies/company-1/logo") + .attach("file", file, "exact-limit.png"); + + expect(res.status).toBe(201); + }); + + it("rejects logo files larger than 100 KB", async () => { const app = createApp(createStorageService()); createAssetMock.mockResolvedValue(createAsset()); const file = Buffer.alloc(100 * 1024 + 1, "a"); const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "companies") + .post("/api/companies/company-1/logo") .attach("file", file, "too-large.png"); expect(res.status).toBe(422); expect(res.body.error).toBe("Image exceeds 102400 bytes"); }); - it("allows larger non-logo images within the general asset limit", async () => { - const png = createStorageService("image/png"); - const app = createApp(png); - - createAssetMock.mockResolvedValue({ - ...createAsset(), - contentType: "image/png", - originalFilename: "goal.png", - }); - - const file = Buffer.alloc(150 * 1024, "a"); - const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "goals") - .attach("file", file, "goal.png"); - - expect(res.status).toBe(201); - expect(createAssetMock).toHaveBeenCalledTimes(1); - }); - it("rejects unsupported image types", async () => { const app = createApp(createStorageService("text/plain")); createAssetMock.mockResolvedValue(createAsset()); const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "companies") + .post("/api/companies/company-1/logo") .attach("file", Buffer.from("not an image"), "note.txt"); expect(res.status).toBe(422); @@ -189,38 +234,12 @@ describe("POST /api/companies/:companyId/assets/images", () => { expect(createAssetMock).not.toHaveBeenCalled(); }); - it("allows supported non-image attachments outside the company logo namespace", async () => { - const text = createStorageService("text/plain"); - const app = createApp(text); - - createAssetMock.mockResolvedValue({ - ...createAsset(), - contentType: "text/plain", - originalFilename: "note.txt", - }); - - const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "issues/drafts") - .attach("file", Buffer.from("hello"), "note.txt"); - - expect(res.status).toBe(201); - expect(text.putFile).toHaveBeenCalledWith({ - companyId: "company-1", - namespace: "assets/issues/drafts", - originalFilename: "note.txt", - contentType: "text/plain", - body: expect.any(Buffer), - }); - }); - it("rejects SVG image uploads that cannot be sanitized", async () => { const app = createApp(createStorageService("image/svg+xml")); createAssetMock.mockResolvedValue(createAsset()); const res = await request(app) - .post("/api/companies/company-1/assets/images") - .field("namespace", "companies") + .post("/api/companies/company-1/logo") .attach("file", Buffer.from("not actually svg"), "logo.svg"); expect(res.status).toBe(422); diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index 0ec6ffb1..ce2793a5 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -10,6 +10,14 @@ import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types. import { assertCompanyAccess, getActorInfo } from "./authz.js"; const MAX_COMPANY_LOGO_BYTES = 100 * 1024; const SVG_CONTENT_TYPE = "image/svg+xml"; +const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([ + "image/png", + "image/jpeg", + "image/jpg", + "image/webp", + "image/gif", + SVG_CONTENT_TYPE, +]); function sanitizeSvgBuffer(input: Buffer): Buffer | null { const raw = input.toString("utf8").trim(); @@ -78,12 +86,20 @@ function sanitizeSvgBuffer(input: Buffer): Buffer | null { export function assetRoutes(db: Db, storage: StorageService) { const router = Router(); const svc = assetService(db); - const upload = multer({ + const assetUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }, }); + const companyLogoUpload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_COMPANY_LOGO_BYTES + 1, files: 1 }, + }); - async function runSingleFileUpload(req: Request, res: Response) { + async function runSingleFileUpload( + upload: ReturnType, + req: Request, + res: Response, + ) { await new Promise((resolve, reject) => { upload.single("file")(req, res, (err: unknown) => { if (err) reject(err); @@ -97,7 +113,7 @@ export function assetRoutes(db: Db, storage: StorageService) { assertCompanyAccess(req, companyId); try { - await runSingleFileUpload(req, res); + await runSingleFileUpload(assetUpload, req, res); } catch (err) { if (err instanceof multer.MulterError) { if (err.code === "LIMIT_FILE_SIZE") { @@ -123,12 +139,7 @@ export function assetRoutes(db: Db, storage: StorageService) { } const namespaceSuffix = parsedMeta.data.namespace ?? "general"; - const isCompanyLogoNamespace = namespaceSuffix === "companies" || namespaceSuffix.startsWith("companies/"); const contentType = (file.mimetype || "").toLowerCase(); - if (isCompanyLogoNamespace && !contentType.startsWith("image/")) { - res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` }); - return; - } if (contentType !== SVG_CONTENT_TYPE && !isAllowedContentType(contentType)) { res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` }); return; @@ -146,7 +157,7 @@ export function assetRoutes(db: Db, storage: StorageService) { res.status(422).json({ error: "Image is empty" }); return; } - if (isCompanyLogoNamespace && fileBody.length > MAX_COMPANY_LOGO_BYTES) { + if (fileBody.length > MAX_COMPANY_LOGO_BYTES) { res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); return; } @@ -204,6 +215,105 @@ export function assetRoutes(db: Db, storage: StorageService) { }); }); + router.post("/companies/:companyId/logo", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + + try { + await runSingleFileUpload(companyLogoUpload, req, res); + } catch (err) { + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); + return; + } + res.status(400).json({ error: err.message }); + return; + } + throw err; + } + + const file = (req as Request & { file?: { mimetype: string; buffer: Buffer; originalname: string } }).file; + if (!file) { + res.status(400).json({ error: "Missing file field 'file'" }); + return; + } + + const contentType = (file.mimetype || "").toLowerCase(); + if (!ALLOWED_COMPANY_LOGO_CONTENT_TYPES.has(contentType)) { + res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` }); + return; + } + + let fileBody = file.buffer; + if (contentType === SVG_CONTENT_TYPE) { + const sanitized = sanitizeSvgBuffer(file.buffer); + if (!sanitized || sanitized.length <= 0) { + res.status(422).json({ error: "SVG could not be sanitized" }); + return; + } + fileBody = sanitized; + } + + if (fileBody.length <= 0) { + res.status(422).json({ error: "Image is empty" }); + return; + } + + const actor = getActorInfo(req); + const stored = await storage.putFile({ + companyId, + namespace: "assets/companies", + originalFilename: file.originalname || null, + contentType, + body: fileBody, + }); + + const asset = await svc.create(companyId, { + provider: stored.provider, + objectKey: stored.objectKey, + contentType: stored.contentType, + byteSize: stored.byteSize, + sha256: stored.sha256, + originalFilename: stored.originalFilename, + createdByAgentId: actor.agentId, + createdByUserId: actor.actorType === "user" ? actor.actorId : null, + }); + + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "asset.created", + entityType: "asset", + entityId: asset.id, + details: { + originalFilename: asset.originalFilename, + contentType: asset.contentType, + byteSize: asset.byteSize, + namespace: "assets/companies", + }, + }); + + res.status(201).json({ + assetId: asset.id, + companyId: asset.companyId, + provider: asset.provider, + objectKey: asset.objectKey, + contentType: asset.contentType, + byteSize: asset.byteSize, + sha256: asset.sha256, + originalFilename: asset.originalFilename, + createdByAgentId: asset.createdByAgentId, + createdByUserId: asset.createdByUserId, + createdAt: asset.createdAt, + updatedAt: asset.updatedAt, + contentPath: `/api/assets/${asset.id}/content`, + }); + }); + router.get("/assets/:assetId/content", async (req, res, next) => { const assetId = req.params.assetId as string; const asset = await svc.getById(assetId); diff --git a/ui/src/api/assets.ts b/ui/src/api/assets.ts index 8b3d056c..6fcf323f 100644 --- a/ui/src/api/assets.ts +++ b/ui/src/api/assets.ts @@ -11,11 +11,19 @@ export const assetsApi = { const safeFile = new File([buffer], file.name, { type: file.type }); const form = new FormData(); - form.append("file", safeFile); if (namespace && namespace.trim().length > 0) { form.append("namespace", namespace.trim()); } + form.append("file", safeFile); return api.postForm(`/companies/${companyId}/assets/images`, form); }, -}; + uploadCompanyLogo: async (companyId: string, file: File) => { + const buffer = await file.arrayBuffer(); + const safeFile = new File([buffer], file.name, { type: file.type }); + + const form = new FormData(); + form.append("file", safeFile); + return api.postForm(`/companies/${companyId}/logo`, form); + }, +}; diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index 3cd310ca..f805eebc 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -140,7 +140,7 @@ export function CompanySettings() { const logoUploadMutation = useMutation({ mutationFn: (file: File) => assetsApi - .uploadImage(selectedCompanyId!, file, "companies") + .uploadCompanyLogo(selectedCompanyId!, file) .then((asset) => companiesApi.update(selectedCompanyId!, { logoAssetId: asset.assetId })), onSuccess: (company) => { syncLogoState(company.logoUrl); @@ -160,8 +160,8 @@ export function CompanySettings() { const file = event.target.files?.[0] ?? null; event.currentTarget.value = ""; if (!file) return; - if (file.size >= 100 * 1024) { - setLogoUploadError("Logo image must be smaller than 100 KB."); + if (file.size > 100 * 1024) { + setLogoUploadError("Logo image must be 100 KB or smaller."); return; } setLogoUploadError(null); From 6eceb9b88621a8a17da974f963ef260eb5a1464f Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 16 Mar 2026 10:13:19 -0500 Subject: [PATCH 51/51] Use attachment-size limit for company logos --- docs/api/companies.md | 2 ++ server/src/__tests__/assets.test.ts | 13 +++++++------ server/src/routes/assets.ts | 9 ++------- ui/src/pages/CompanySettings.tsx | 6 +----- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/api/companies.md b/docs/api/companies.md index ca9fa2f1..00e7ab66 100644 --- a/docs/api/companies.md +++ b/docs/api/companies.md @@ -61,6 +61,8 @@ Valid image content types: - `image/gif` - `image/svg+xml` +Company logo uploads use the normal Paperclip attachment size limit. + Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`. ## Archive Company diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index b6f740b7..b7bec332 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import express from "express"; import request from "supertest"; +import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; import { assetRoutes } from "../routes/assets.js"; import type { StorageService } from "../storage/types.js"; @@ -195,30 +196,30 @@ describe("POST /api/companies/:companyId/logo", () => { expect(body).not.toContain("https://evil.example/"); }); - it("allows a logo exactly 100 KB in size", async () => { + it("allows logo uploads within the general attachment limit", async () => { const png = createStorageService("image/png"); const app = createApp(png); createAssetMock.mockResolvedValue(createAsset()); - const file = Buffer.alloc(100 * 1024, "a"); + const file = Buffer.alloc(150 * 1024, "a"); const res = await request(app) .post("/api/companies/company-1/logo") - .attach("file", file, "exact-limit.png"); + .attach("file", file, "within-limit.png"); expect(res.status).toBe(201); }); - it("rejects logo files larger than 100 KB", async () => { + it("rejects logo files larger than the general attachment limit", async () => { const app = createApp(createStorageService()); createAssetMock.mockResolvedValue(createAsset()); - const file = Buffer.alloc(100 * 1024 + 1, "a"); + const file = Buffer.alloc(MAX_ATTACHMENT_BYTES + 1, "a"); const res = await request(app) .post("/api/companies/company-1/logo") .attach("file", file, "too-large.png"); expect(res.status).toBe(422); - expect(res.body.error).toBe("Image exceeds 102400 bytes"); + expect(res.body.error).toBe(`Image exceeds ${MAX_ATTACHMENT_BYTES} bytes`); }); it("rejects unsupported image types", async () => { diff --git a/server/src/routes/assets.ts b/server/src/routes/assets.ts index ce2793a5..0a6f857a 100644 --- a/server/src/routes/assets.ts +++ b/server/src/routes/assets.ts @@ -8,7 +8,6 @@ import type { StorageService } from "../storage/types.js"; import { assetService, logActivity } from "../services/index.js"; import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js"; -const MAX_COMPANY_LOGO_BYTES = 100 * 1024; const SVG_CONTENT_TYPE = "image/svg+xml"; const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([ "image/png", @@ -92,7 +91,7 @@ export function assetRoutes(db: Db, storage: StorageService) { }); const companyLogoUpload = multer({ storage: multer.memoryStorage(), - limits: { fileSize: MAX_COMPANY_LOGO_BYTES + 1, files: 1 }, + limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }, }); async function runSingleFileUpload( @@ -157,10 +156,6 @@ export function assetRoutes(db: Db, storage: StorageService) { res.status(422).json({ error: "Image is empty" }); return; } - if (fileBody.length > MAX_COMPANY_LOGO_BYTES) { - res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); - return; - } const actor = getActorInfo(req); const stored = await storage.putFile({ @@ -224,7 +219,7 @@ export function assetRoutes(db: Db, storage: StorageService) { } catch (err) { if (err instanceof multer.MulterError) { if (err.code === "LIMIT_FILE_SIZE") { - res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); + res.status(422).json({ error: `Image exceeds ${MAX_ATTACHMENT_BYTES} bytes` }); return; } res.status(400).json({ error: err.message }); diff --git a/ui/src/pages/CompanySettings.tsx b/ui/src/pages/CompanySettings.tsx index f805eebc..225b7398 100644 --- a/ui/src/pages/CompanySettings.tsx +++ b/ui/src/pages/CompanySettings.tsx @@ -160,10 +160,6 @@ export function CompanySettings() { const file = event.target.files?.[0] ?? null; event.currentTarget.value = ""; if (!file) return; - if (file.size > 100 * 1024) { - setLogoUploadError("Logo image must be 100 KB or smaller."); - return; - } setLogoUploadError(null); logoUploadMutation.mutate(file); } @@ -276,7 +272,7 @@ export function CompanySettings() {