diff --git a/seed/tasks/convert-to-mp3/TASK.md b/seed/tasks/convert-to-mp3/TASK.md new file mode 100644 index 00000000..4293fe6d --- /dev/null +++ b/seed/tasks/convert-to-mp3/TASK.md @@ -0,0 +1,35 @@ +--- +name: Convert To MP3 +description: Convert audio files to MP3 320kbps, preserving metadata. +version: 1 +mode: script +language: bash +triggers: + - type: file + extensions: + - flac + - wav + - ogg + - wma + - aac + - m4a + - opus + - aiff + - aif + - ape + - wv + - alac + - dsf + - dff + - type: directory +inputs: + file_path: + type: string + description: Path to an audio file or directory to convert. +args: [file_path] +--- + +# Convert To MP3 + +Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. +Supports single file conversion and batch directory conversion. diff --git a/seed/tasks/convert-to-mp3/run.sh b/seed/tasks/convert-to-mp3/run.sh new file mode 100755 index 00000000..c3b6c87f --- /dev/null +++ b/seed/tasks/convert-to-mp3/run.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Accepts path as $1 or $INPUT_FILE_PATH +TARGET="${1:-${INPUT_FILE_PATH:-}}" + +if [[ -z "$TARGET" ]]; then + echo "Error: No file or directory path provided" >&2 + exit 1 +fi + +if [[ ! -e "$TARGET" ]]; then + echo "Error: Path does not exist: $TARGET" >&2 + exit 1 +fi + +convert_file() { + local input="$1" + local ext="${input##*.}" + local dir + dir="$(dirname "$input")" + local base + base="$(basename "$input" ".$ext")" + local output="$dir/$base.mp3" + + if [[ "${ext,,}" == "mp3" ]]; then + echo "Skipping (already MP3): $input" + return 0 + fi + + if [[ -f "$output" ]]; then + echo "Skipping (output exists): $output" + return 0 + fi + + echo "Converting: $input → $output" + ffmpeg -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null + + if [[ $? -eq 0 ]]; then + echo " ✓ Done" + else + echo " ✗ Failed" >&2 + return 1 + fi +} + +AUDIO_EXTS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff" +converted=0 +failed=0 + +if [[ -f "$TARGET" ]]; then + if convert_file "$TARGET"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi +elif [[ -d "$TARGET" ]]; then + while IFS= read -r -d '' file; do + if convert_file "$file"; then + converted=$((converted + 1)) + else + failed=$((failed + 1)) + fi + done < <(find "$TARGET" -type f -regextype posix-extended -iregex ".*\.($AUDIO_EXTS)" -print0 | sort -z) + + if [[ $converted -eq 0 && $failed -eq 0 ]]; then + echo "No audio files found in: $TARGET" + exit 0 + fi +else + echo "Error: Not a file or directory: $TARGET" >&2 + exit 1 +fi + +echo "" +echo "Summary: $converted converted, $failed failed" diff --git a/src/databases/officer_db/migrations/0002_cute_doorman.sql b/src/databases/officer_db/migrations/0002_cute_doorman.sql new file mode 100644 index 00000000..85d4b91f --- /dev/null +++ b/src/databases/officer_db/migrations/0002_cute_doorman.sql @@ -0,0 +1,4 @@ +ALTER TABLE "tasks" ADD COLUMN "mode" text DEFAULT 'agentic' NOT NULL;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "language" text;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "implementation" text;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "args" jsonb; \ No newline at end of file diff --git a/src/databases/officer_db/migrations/meta/0002_snapshot.json b/src/databases/officer_db/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..8a5c98ef --- /dev/null +++ b/src/databases/officer_db/migrations/meta/0002_snapshot.json @@ -0,0 +1,2349 @@ +{ + "id": "9b81457f-9d9e-495c-8a22-0de8a220e29e", + "prevId": "32def243-70f8-4ad8-a66e-5ef27c3a594b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.passkey_challenges": { + "name": "passkey_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_challenges_user_id_users_id_fk": { + "name": "passkey_challenges_user_id_users_id_fk", + "tableFrom": "passkey_challenges", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkeys": { + "name": "passkeys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "passkeys_user_id_users_id_fk": { + "name": "passkeys_user_id_users_id_fk", + "tableFrom": "passkeys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_blacklist": { + "name": "token_blacklist", + "schema": "", + "columns": { + "jti": { + "name": "jti", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_token_blacklist_expires": { + "name": "idx_token_blacklist_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Unverified'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_changed_at": { + "name": "password_changed_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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dock_configs": { + "name": "dock_configs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "paths": { + "name": "paths", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dock_configs_user_id_users_id_fk": { + "name": "dock_configs_user_id_users_id_fk", + "tableFrom": "dock_configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_integrations": { + "name": "user_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_integration_id": { + "name": "server_integration_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_integrations_user_id_users_id_fk": { + "name": "user_integrations_user_id_users_id_fk", + "tableFrom": "user_integrations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_integrations_server_integration_id_server_integrations_id_fk": { + "name": "user_integrations_server_integration_id_server_integrations_id_fk", + "tableFrom": "user_integrations", + "tableTo": "server_integrations", + "columnsFrom": [ + "server_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_user_integrations_user_provider": { + "name": "uq_user_integrations_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_state": { + "name": "user_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_state_user_id_users_id_fk": { + "name": "user_state_user_id_users_id_fk", + "tableFrom": "user_state", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_sessions": { + "name": "saved_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_messages": { + "name": "raw_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_saved_sessions_user_created": { + "name": "idx_saved_sessions_user_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_sessions_user_id_users_id_fk": { + "name": "saved_sessions_user_id_users_id_fk", + "tableFrom": "saved_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_defaults": { + "name": "dashboard_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_defaults_user_id_users_id_fk": { + "name": "dashboard_defaults_user_id_users_id_fk", + "tableFrom": "dashboard_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_defaults_user_id_unique": { + "name": "dashboard_defaults_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboards_user_id_users_id_fk": { + "name": "dashboards_user_id_users_id_fk", + "tableFrom": "dashboards", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_dashboards_user_id": { + "name": "uq_dashboards_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_user_id_users_id_fk": { + "name": "projects_user_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_projects_user_slug": { + "name": "uq_projects_user_slug", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.screens": { + "name": "screens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "terminals": { + "name": "terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "host_terminals": { + "name": "host_terminals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "screens_user_id_users_id_fk": { + "name": "screens_user_id_users_id_fk", + "tableFrom": "screens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_screens_user_name": { + "name": "uq_screens_user_name", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.extensions": { + "name": "extensions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dir_name": { + "name": "dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "implementation": { + "name": "implementation", + "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": { + "idx_extensions_scope": { + "name": "idx_extensions_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_extensions_user": { + "name": "idx_extensions_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extensions_user_id_users_id_fk": { + "name": "extensions_user_id_users_id_fk", + "tableFrom": "extensions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_extensions_scope_user_dir": { + "name": "uq_extensions_scope_user_dir", + "nullsNotDistinct": false, + "columns": [ + "scope", + "user_id", + "dir_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.item_chats": { + "name": "item_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "item_type": { + "name": "item_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_item_chats_type_item": { + "name": "idx_item_chats_type_item", + "columns": [ + { + "expression": "item_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_item_chats_type_item": { + "name": "uq_item_chats_type_item", + "nullsNotDistinct": false, + "columns": [ + "item_type", + "item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.processes": { + "name": "processes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dir_name": { + "name": "dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_processes_scope": { + "name": "idx_processes_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_processes_user": { + "name": "idx_processes_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "processes_user_id_users_id_fk": { + "name": "processes_user_id_users_id_fk", + "tableFrom": "processes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_processes_scope_user_dir": { + "name": "uq_processes_scope_user_dir", + "nullsNotDistinct": false, + "columns": [ + "scope", + "user_id", + "dir_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dir_name": { + "name": "dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_skills_scope": { + "name": "idx_skills_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_skills_user": { + "name": "idx_skills_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_user_id_users_id_fk": { + "name": "skills_user_id_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_skills_scope_user_dir": { + "name": "uq_skills_scope_user_dir", + "nullsNotDistinct": false, + "columns": [ + "scope", + "user_id", + "dir_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dir_name": { + "name": "dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agentic'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "implementation": { + "name": "implementation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "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": { + "idx_tasks_scope": { + "name": "idx_tasks_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tasks_user": { + "name": "idx_tasks_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_user_id_users_id_fk": { + "name": "tasks_user_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_tasks_scope_user_dir": { + "name": "uq_tasks_scope_user_dir", + "nullsNotDistinct": false, + "columns": [ + "scope", + "user_id", + "dir_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tools": { + "name": "tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dir_name": { + "name": "dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "implementation": { + "name": "implementation", + "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": { + "idx_tools_scope": { + "name": "idx_tools_scope", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tools_user": { + "name": "idx_tools_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tools_user_id_users_id_fk": { + "name": "tools_user_id_users_id_fk", + "tableFrom": "tools", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_tools_scope_user_dir": { + "name": "uq_tools_scope_user_dir", + "nullsNotDistinct": false, + "columns": [ + "scope", + "user_id", + "dir_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.queue_jobs": { + "name": "queue_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lane": { + "name": "lane", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "current_step": { + "name": "current_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "meta": { + "name": "meta", + "type": "jsonb", + "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()" + }, + "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 + } + }, + "indexes": { + "idx_queue_jobs_status_lane": { + "name": "idx_queue_jobs_status_lane", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lane", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_queue_jobs_user": { + "name": "idx_queue_jobs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "queue_jobs_user_id_users_id_fk": { + "name": "queue_jobs_user_id_users_id_fk", + "tableFrom": "queue_jobs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_logs": { + "name": "task_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "task_name": { + "name": "task_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_dir_name": { + "name": "task_dir_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_name": { + "name": "entry_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_error": { + "name": "is_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_task_logs_user_started": { + "name": "idx_task_logs_user_started", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_logs_user_id_users_id_fk": { + "name": "task_logs_user_id_users_id_fk", + "tableFrom": "task_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.terminal_containers": { + "name": "terminal_containers", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "docker_id": { + "name": "docker_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "terminal_containers_user_id_users_id_fk": { + "name": "terminal_containers_user_id_users_id_fk", + "tableFrom": "terminal_containers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_config": { + "name": "server_config", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_integrations": { + "name": "server_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "server_integrations_provider_unique": { + "name": "server_integrations_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_accounts": { + "name": "email_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imap_host": { + "name": "imap_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imap_port": { + "name": "imap_port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "imap_secure": { + "name": "imap_secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credentials": { + "name": "credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connected'" + }, + "sync_meta": { + "name": "sync_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_accounts_user_id_users_id_fk": { + "name": "email_accounts_user_id_users_id_fk", + "tableFrom": "email_accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_email_accounts_user_email": { + "name": "uq_email_accounts_user_email", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/databases/officer_db/migrations/meta/_journal.json b/src/databases/officer_db/migrations/meta/_journal.json index 6f758c40..45830a64 100644 --- a/src/databases/officer_db/migrations/meta/_journal.json +++ b/src/databases/officer_db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1772799835768, "tag": "0001_freezing_carmella_unuscione", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1772985087180, + "tag": "0002_cute_doorman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/databases/officer_db/seed-tasks.ts b/src/databases/officer_db/seed-tasks.ts new file mode 100644 index 00000000..07e5b075 --- /dev/null +++ b/src/databases/officer_db/seed-tasks.ts @@ -0,0 +1,166 @@ +/** + * Seed native tasks into the database. + * Run: bun src/databases/officer_db/seed-tasks.ts + */ +import { eq, and } from 'drizzle-orm'; +import { db } from './src/db'; +import { tasks } from './src/schema/agent-items'; +import { readdirSync, readFileSync, existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +const SEED_TASKS_DIR = resolve(import.meta.dir, '../../../seed/tasks'); + +type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' }; + +function parseFrontmatter(content: string) { + const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); + if (!match) return { meta: {} as Record, body: content }; + + const yaml = match[1]!; + const body = match[2]!; + const meta: Record = {}; + + meta.name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? ''; + meta.description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? ''; + meta.mode = yaml.match(/^mode:\s*(.+)$/m)?.[1]?.trim() ?? 'agentic'; + meta.language = yaml.match(/^language:\s*(.+)$/m)?.[1]?.trim() ?? undefined; + meta.version = parseInt(yaml.match(/^version:\s*(.+)$/m)?.[1]?.trim() ?? '1', 10); + + // Parse args + const argsMatch = yaml.match(/^args:\s*\[([^\]]*)\]/m); + meta.args = argsMatch ? argsMatch[1]!.split(',').map((a) => a.trim()).filter(Boolean) : undefined; + + // Parse triggers + const triggers: TriggerConfig[] = []; + const triggerMatch = yaml.match(/^triggers?:\s*\n((?:[ \t]+.+\n?)*)/m); + if (triggerMatch) { + const items = triggerMatch[1]!.split(/(?=^\s+-\s*type:)/m); + for (const item of items) { + const type = item.match(/type:\s*(.+)/)?.[1]?.trim(); + if (type === 'directory') { + triggers.push({ type: 'directory' }); + } else if (type === 'file') { + const extBlock = item.match(/extensions:\s*\n((?:\s+-\s*.+\n?)*)/); + const extensions = extBlock ? [...extBlock[1]!.matchAll(/^\s+-\s*(.+)$/gm)].map((m) => m[1]!.trim()) : []; + if (extensions.length > 0) triggers.push({ type: 'file', extensions }); + } + } + } + meta.trigger = triggers.length > 0 ? triggers : undefined; + + // Parse inputs (simplified — store as-is for now) + const inputsMatch = yaml.match(/^inputs:\s*\n((?:[ \t]+.+\n?)*)/m); + if (inputsMatch) { + const inputBlock = inputsMatch[1]!; + const inputEntries: Record> = {}; + let currentInput: string | null = null; + for (const line of inputBlock.split('\n')) { + const topMatch = line.match(/^\s{2}(\w[\w_-]*):\s*$/); + if (topMatch) { + currentInput = topMatch[1]!; + inputEntries[currentInput] = {}; + continue; + } + const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.+)$/); + if (propMatch && currentInput) { + inputEntries[currentInput]![propMatch[1]!] = propMatch[2]!.trim(); + } + } + if (Object.keys(inputEntries).length > 0) { + meta.inputs = inputEntries; + } + } + + return { meta, body }; +} + +async function seedTasks() { + if (!existsSync(SEED_TASKS_DIR)) { + console.log('No seed tasks directory found'); + return; + } + + const entries = readdirSync(SEED_TASKS_DIR, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const taskDir = join(SEED_TASKS_DIR, entry.name); + const taskMdPath = join(taskDir, 'TASK.md'); + if (!existsSync(taskMdPath)) continue; + + const content = readFileSync(taskMdPath, 'utf-8'); + const { meta, body } = parseFrontmatter(content); + + // Find implementation file + let implementation: string | null = null; + const lang = meta.language as string | undefined; + const implCandidates = [ + { file: 'run.sh', lang: 'bash' }, + { file: 'index.ts', lang: 'typescript' }, + { file: 'run.py', lang: 'python' }, + { file: 'index.js', lang: 'javascript' }, + ]; + for (const c of implCandidates) { + const path = join(taskDir, c.file); + if (existsSync(path)) { + implementation = readFileSync(path, 'utf-8'); + break; + } + } + + const values = { + scope: 'native' as const, + userId: null, + dirName: entry.name, + name: (meta.name as string) || entry.name, + description: (meta.description as string) || null, + body: body || null, + version: (meta.version as number) || 1, + mode: (meta.mode as string) || 'agentic', + language: (meta.language as string) || null, + implementation, + args: (meta.args as string[]) || null, + trigger: (meta.trigger as TriggerConfig[]) || null, + inputs: (meta.inputs as Record) || null, + }; + + // Upsert: check by dirName + scope since unique constraint doesn't work with NULL userId + const existing = await db + .select({ id: tasks.id }) + .from(tasks) + .where(and(eq(tasks.dirName, values.dirName), eq(tasks.scope, 'native'))) + .limit(1); + + if (existing.length > 0) { + await db + .update(tasks) + .set({ + name: values.name, + description: values.description, + body: values.body, + version: values.version, + mode: values.mode, + language: values.language, + implementation: values.implementation, + args: values.args, + trigger: values.trigger, + inputs: values.inputs, + updatedAt: new Date(), + }) + .where(eq(tasks.id, existing[0]!.id)); + } else { + await db.insert(tasks).values(values); + } + + console.log(`Seeded: ${entry.name} (${meta.mode}/${meta.language})`); + } + + console.log('Done seeding tasks'); + process.exit(0); +} + +seedTasks().catch((err) => { + console.error('Seed failed:', err); + process.exit(1); +}); diff --git a/src/databases/officer_db/src/index.ts b/src/databases/officer_db/src/index.ts index 56169602..528ab813 100644 --- a/src/databases/officer_db/src/index.ts +++ b/src/databases/officer_db/src/index.ts @@ -72,5 +72,14 @@ export { deleteSavedSession, } from './queries/saved-sessions'; +export { + getTasksForUser, + getTaskById, + getTaskByDirName, + createTask, + updateTask, + deleteTask, +} from './queries/tasks'; + export { db } from './db'; export * as schema from './schema'; diff --git a/src/databases/officer_db/src/queries/tasks.ts b/src/databases/officer_db/src/queries/tasks.ts new file mode 100644 index 00000000..402e85ae --- /dev/null +++ b/src/databases/officer_db/src/queries/tasks.ts @@ -0,0 +1,74 @@ +import { eq, or, and, isNull, sql } from 'drizzle-orm'; +import { db } from '../db'; +import { tasks } from '../schema/agent-items'; + +export async function getTasksForUser(userId: number) { + return db + .select({ + id: tasks.id, + dirName: tasks.dirName, + name: tasks.name, + description: tasks.description, + mode: tasks.mode, + language: tasks.language, + version: tasks.version, + scope: tasks.scope, + trigger: tasks.trigger, + userId: tasks.userId, + }) + .from(tasks) + .where( + or( + eq(tasks.scope, 'native'), + eq(tasks.scope, 'global'), + and(eq(tasks.scope, 'user'), eq(tasks.userId, userId)), + ), + ) + .orderBy(tasks.name); +} + +export async function getTaskById(id: number) { + const rows = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1); + return rows[0] ?? null; +} + +export async function getTaskByDirName(dirName: string, userId: number) { + // User scope takes priority over global, which takes priority over native + const rows = await db + .select() + .from(tasks) + .where( + and( + eq(tasks.dirName, dirName), + or( + eq(tasks.scope, 'native'), + eq(tasks.scope, 'global'), + and(eq(tasks.scope, 'user'), eq(tasks.userId, userId)), + ), + ), + ) + .orderBy(sql`CASE scope WHEN 'user' THEN 0 WHEN 'global' THEN 1 ELSE 2 END`) + .limit(1); + + return rows[0] ?? null; +} + +type TaskInsert = typeof tasks.$inferInsert; + +export async function createTask(data: TaskInsert) { + const rows = await db.insert(tasks).values(data).returning(); + return rows[0]!; +} + +export async function updateTask(id: number, data: Partial) { + const rows = await db + .update(tasks) + .set({ ...data, updatedAt: new Date() }) + .where(eq(tasks.id, id)) + .returning(); + return rows[0] ?? null; +} + +export async function deleteTask(id: number) { + await db.delete(tasks).where(eq(tasks.id, id)); +} diff --git a/src/databases/officer_db/src/schema/agent-items.ts b/src/databases/officer_db/src/schema/agent-items.ts index 1cca46f6..67f917f9 100644 --- a/src/databases/officer_db/src/schema/agent-items.ts +++ b/src/databases/officer_db/src/schema/agent-items.ts @@ -19,6 +19,10 @@ export const tasks = pgTable( description: text('description'), body: text('body'), version: integer('version').notNull().default(1), + mode: text('mode').notNull().default('agentic'), + language: text('language'), + implementation: text('implementation'), + args: jsonb('args').$type(), tags: jsonb('tags').$type(), tools: jsonb('tools').$type(), skills: jsonb('skills').$type(), diff --git a/src/server.tsx b/src/server.tsx index a1b4e381..8e20ce03 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -6,6 +6,7 @@ import { verify } from './servers/jwt'; import { isTokenBlacklisted } from 'officerdb'; import { terminalWebsocket } from './servers/api/terminal/websocket'; import { piWebsocket } from './servers/api/pi/websocket'; +import { taskRunnerWebsocket } from './servers/api/tasks/task-executor'; import { cliampWebsocket } from './servers/api/cliamp/websocket'; import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws'; import { desktopWebsocket } from './servers/api/desktop/websocket'; @@ -23,7 +24,7 @@ type WSData = { email: string; username: string; role: string; - provider: 'terminal' | 'pi' | 'dev-server' | 'cliamp' | 'cliamp-audio' | 'desktop' | 'sidecar'; + provider: 'terminal' | 'pi' | 'task-runner' | 'dev-server' | 'cliamp' | 'cliamp-audio' | 'desktop' | 'sidecar'; sandboxed: boolean; sessionId?: string; cwd?: string; @@ -116,6 +117,7 @@ async function handleSidecarQueueCommand(ws: ServerWebSocket, msg: Recor const handlers: Record = { terminal: terminalWebsocket, pi: piWebsocket, + 'task-runner': taskRunnerWebsocket, cliamp: cliampWebsocket, 'cliamp-audio': cliampAudioWebsocket, desktop: desktopWebsocket, @@ -187,7 +189,7 @@ const devServerWebsocket = { }; handlers['dev-server'] = devServerWebsocket; -async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi' | 'cliamp' | 'cliamp-audio' | 'desktop') { +async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi' | 'task-runner' | 'cliamp' | 'cliamp-audio' | 'desktop') { const token = new URL(req.url).searchParams.get('token'); if (!token) return new Response('Unauthorized', { status: 401 }); @@ -271,6 +273,7 @@ const server = serve({ }); if (!ok) return new Response('Upgrade failed', { status: 500 }); }, + '/api/tasks/run/ws': (req, server) => upgradeWs(req, server, 'task-runner'), '/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'), '/api/pi/chat/ws': (req, server) => upgradeWs(req, server, 'pi'), '/api/cliamp/ws': (req, server) => upgradeWs(req, server, 'cliamp'), diff --git a/src/servers/api/tasks/task-executor.ts b/src/servers/api/tasks/task-executor.ts new file mode 100644 index 00000000..323e895e --- /dev/null +++ b/src/servers/api/tasks/task-executor.ts @@ -0,0 +1,255 @@ +import type { ServerWebSocket } from 'bun'; +import { join } from 'node:path'; +import { mkdirSync, writeFileSync, chmodSync, rmSync } from 'node:fs'; +import { getTaskByDirName } from 'officerdb'; +import { getHomeDirForRole, DATA_PATH } from '../../data-path'; +import { buildSandboxPrefix, buildRunuserSuffix } from '../../sidecar/sandbox'; + +type WSData = { + userId: number; + email: string; + username: string; + role: string; + sandboxed: boolean; +}; + +type RunMessage = { + type: 'run'; + taskDirName: string; + inputs: Record; + cwd?: string; +}; + +type StopMessage = { + type: 'stop'; +}; + +type ClientMessage = RunMessage | StopMessage; + +type OutMessage = + | { type: 'started'; taskName: string } + | { type: 'stdout'; data: string } + | { type: 'stderr'; data: string } + | { type: 'exit'; code: number } + | { type: 'error'; message: string }; + +// Active processes per WebSocket +const activeProcs = new WeakMap, { proc: ReturnType; kill: () => void }>(); + +import { tmpdir } from 'node:os'; + +function send(ws: ServerWebSocket, msg: OutMessage) { + if (ws.readyState === 1) ws.send(JSON.stringify(msg)); +} + +function getRunner(language: string): string[] { + switch (language) { + case 'bash': return ['bash']; + case 'python': return ['python3']; + case 'typescript': return ['bun', 'run']; + case 'javascript': return ['node']; + default: return ['bash']; + } +} + +function getFileName(language: string): string { + switch (language) { + case 'bash': return 'run.sh'; + case 'python': return 'run.py'; + case 'typescript': return 'index.ts'; + case 'javascript': return 'index.js'; + default: return 'run.sh'; + } +} + +// Write implementation to a temp file for execution, cleaned up after +function materializeScript(language: string, implementation: string): string { + const dir = join(tmpdir(), `officer-task-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(dir, { recursive: true }); + + const fileName = getFileName(language); + const filePath = join(dir, fileName); + + writeFileSync(filePath, implementation); + chmodSync(filePath, 0o755); + + return filePath; +} + +function buildInputEnv(inputs: Record): Record { + const env: Record = {}; + for (const [key, value] of Object.entries(inputs)) { + env[`INPUT_${key.toUpperCase()}`] = value; + } + return env; +} + +function buildArgs(inputs: Record, argsOrder?: string[] | null): string[] { + if (!argsOrder || argsOrder.length === 0) return []; + return argsOrder.map((name) => inputs[name] ?? ''); +} + +async function handleRun(ws: ServerWebSocket, msg: RunMessage) { + const { email, role, sandboxed, userId } = ws.data; + + // Resolve task from database + const task = await getTaskByDirName(msg.taskDirName, userId); + if (!task) { + send(ws, { type: 'error', message: `Task not found: ${msg.taskDirName}` }); + return; + } + + if (task.mode !== 'script') { + send(ws, { type: 'error', message: 'Task is not a script-mode task' }); + return; + } + + if (!task.implementation) { + send(ws, { type: 'error', message: `Task ${msg.taskDirName} has no implementation` }); + return; + } + + const language = task.language ?? 'bash'; + + // Write script to temp dir for execution + const scriptPath = materializeScript(language, task.implementation); + + // Build env vars from inputs + const inputEnv = buildInputEnv(msg.inputs); + + // Build positional args + const positionalArgs = buildArgs(msg.inputs, task.args); + + // Build the command + const runner = getRunner(language); + const cmd = [...runner, scriptPath, ...positionalArgs]; + + // Resolve cwd + const homeDir = getHomeDirForRole(email, role); + const cwd = msg.cwd ?? homeDir; + + let spawnCmd: string[]; + let spawnEnv: Record; + let spawnCwd: string; + + if (sandboxed) { + const prefix = buildSandboxPrefix(email); + const suffix = buildRunuserSuffix(); + + // Translate paths in inputs and args: DATA_PATH/{email}/... → /data/... + const userDataPrefix = join(DATA_PATH, email); + const translatePath = (v: string) => v.startsWith(userDataPrefix) ? '/data' + v.slice(userDataPrefix.length) : v; + + const envArgs: string[] = []; + for (const [key, value] of Object.entries(inputEnv)) { + envArgs.push('--setenv', key, translatePath(value)); + } + + // Translate positional args too + const sandboxCmd = cmd.map((arg) => translatePath(arg)); + + // Script is in /tmp which is a tmpfs inside bwrap — need to bind-mount the host tmp dir + const scriptDir = join(scriptPath, '..'); + const extraMounts = ['--ro-bind', scriptDir, scriptDir]; + + spawnCmd = [...prefix, ...extraMounts, ...envArgs, ...suffix, ...sandboxCmd]; + spawnEnv = {}; + spawnCwd = '/'; + } else { + spawnCmd = cmd; + spawnEnv = { ...process.env as Record, ...inputEnv }; + spawnCwd = cwd; + } + + const cleanup = () => { + try { rmSync(join(scriptPath, '..'), { recursive: true, force: true }); } catch { /* best effort */ } + }; + + send(ws, { type: 'started', taskName: task.name }); + + try { + const proc = Bun.spawn(spawnCmd, { + cwd: spawnCwd, + env: spawnEnv, + stdout: 'pipe', + stderr: 'pipe', + }); + + activeProcs.set(ws, { + proc, + kill: () => { + try { proc.kill(); } catch { /* already dead */ } + }, + }); + + const stdoutReader = proc.stdout.getReader(); + const stderrReader = proc.stderr.getReader(); + const decoder = new TextDecoder(); + + const readStream = async (reader: ReadableStreamDefaultReader, type: 'stdout' | 'stderr') => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + send(ws, { type, data: decoder.decode(value) }); + } + } catch { + // stream closed + } + }; + + const [, , exitCode] = await Promise.all([ + readStream(stdoutReader, 'stdout'), + readStream(stderrReader, 'stderr'), + proc.exited, + ]); + + activeProcs.delete(ws); + cleanup(); + send(ws, { type: 'exit', code: exitCode }); + } catch (err) { + activeProcs.delete(ws); + cleanup(); + send(ws, { type: 'error', message: `Failed to spawn: ${err instanceof Error ? err.message : String(err)}` }); + } +} + +export function open(_ws: ServerWebSocket) { + // nothing to do +} + +export function message(ws: ServerWebSocket, raw: string | Buffer) { + const data = typeof raw === 'string' ? raw : raw.toString(); + + try { + const msg = JSON.parse(data) as ClientMessage; + + if (msg.type === 'run') { + handleRun(ws, msg); + } else if (msg.type === 'stop') { + const active = activeProcs.get(ws); + if (active) { + active.kill(); + activeProcs.delete(ws); + send(ws, { type: 'exit', code: -1 }); + } + } + } catch { + send(ws, { type: 'error', message: 'Failed to parse message' }); + } +} + +export function close(ws: ServerWebSocket) { + const active = activeProcs.get(ws); + if (active) { + active.kill(); + activeProcs.delete(ws); + } +} + +export const taskRunnerWebsocket = { + open, + message, + close, + drain() {}, +}; diff --git a/src/servers/api/tasks/tasks.ts b/src/servers/api/tasks/tasks.ts index 834066a0..1b4c700e 100644 --- a/src/servers/api/tasks/tasks.ts +++ b/src/servers/api/tasks/tasks.ts @@ -1,77 +1,8 @@ import { createRouter } from '../../create-router'; -import { readdir, mkdir, rm } from 'node:fs/promises'; -import { join, dirname } from 'node:path'; -import { getNativeTasksDir, getGlobalTasksDir, getUserTasksDir } from '../../data-path'; +import { getTasksForUser, getTaskByDirName, getTaskById, createTask, updateTask, deleteTask } from 'officerdb'; type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' }; -type Frontmatter = { - name: string; - description: string; - triggers: TriggerConfig[]; -}; - -export function parseFrontmatter(raw: string): { frontmatter: Frontmatter; body: string; rawYaml: string } { - const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); - if (!match) return { frontmatter: { name: '', description: '', triggers: [] }, body: raw, rawYaml: '' }; - - const yaml = match[1]!; - const body = match[2]!; - - const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? ''; - const description = yaml.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? ''; - - const triggers: TriggerConfig[] = []; - const triggerMatch = yaml.match(/^trigger:\s*\n((?:[ \t]+.+\n?)*)/m); - if (triggerMatch) { - const items = triggerMatch[1]!.split(/(?=^\s+-\s*type:)/m); - for (const item of items) { - const type = item.match(/type:\s*(.+)/)?.[1]?.trim(); - if (type === 'directory') { - triggers.push({ type: 'directory' }); - } else if (type === 'file') { - const extBlock = item.match(/extensions:\s*\n((?:\s+-\s*.+\n?)*)/); - const extensions = extBlock ? [...extBlock[1]!.matchAll(/^\s+-\s*(.+)$/gm)].map((m) => m[1]!.trim()) : []; - if (extensions.length > 0) triggers.push({ type: 'file', extensions }); - } - } - } - - return { frontmatter: { name, description, triggers }, body, rawYaml: yaml }; -} - -export async function readTaskDirs(dir: string): Promise> { - const result = new Map(); - try { - const entries = await readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const taskFile = join(dir, entry.name, 'TASK.md'); - if (await Bun.file(taskFile).exists()) { - result.set(entry.name, taskFile); - } - } - } catch { - // directory doesn't exist yet - } - return result; -} - -type Scope = 'native' | 'global' | 'user'; - -function resolveScope(dirName: string, native: Map, global: Map, user: Map): Scope { - if (user.has(dirName)) return 'user'; - if (global.has(dirName)) return 'global'; - return 'native'; -} - -function resolveFile(name: string, native: Map, global: Map, user: Map): { filePath: string; scope: Scope } | null { - if (user.has(name)) return { filePath: user.get(name)!, scope: 'user' }; - if (global.has(name)) return { filePath: global.get(name)!, scope: 'global' }; - if (native.has(name)) return { filePath: native.get(name)!, scope: 'native' }; - return null; -} - function isPrivileged(role: string) { return role === 'Super Admin'; } @@ -80,29 +11,18 @@ export const tasksRouter = createRouter(); tasksRouter.get('/', async (ctx) => { const user = ctx.get('user'); - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); + const rows = await getTasksForUser(user.id); - const merged = new Map(nativeTasks); - for (const [name, path] of globalTasks) merged.set(name, path); - for (const [name, path] of userTasks) merged.set(name, path); - - const tasks = await Promise.all( - Array.from(merged.entries()).map(async ([dirName, filePath]) => { - const raw = await Bun.file(filePath).text(); - const { frontmatter } = parseFrontmatter(raw); - const scope = resolveScope(dirName, nativeTasks, globalTasks, userTasks); - return { - dirName, - name: frontmatter.name || dirName, - description: frontmatter.description, - scope, - triggers: frontmatter.triggers, - filePath, - }; - }), - ); + const tasks = rows.map((row) => ({ + id: row.id, + dirName: row.dirName, + name: row.name, + description: row.description, + scope: row.scope, + triggers: (row.trigger as TriggerConfig[]) ?? [], + mode: row.mode ?? 'agentic', + userId: row.userId, + })); return ctx.json(tasks); }); @@ -111,127 +31,62 @@ tasksRouter.get('/:name', async (ctx) => { const user = ctx.get('user'); const name = ctx.req.param('name'); - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); - - const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks); - if (!resolved) return ctx.text('Not found', 404); - - const raw = await Bun.file(resolved.filePath).text(); - const { frontmatter, body, rawYaml } = parseFrontmatter(raw); - - const chatMeta = join(dirname(resolved.filePath), 'chat', 'meta.json'); - const chatSessionId = await Bun.file(chatMeta).json().then((m: { id: string }) => m.id).catch(() => null); + const task = await getTaskByDirName(name, user.id); + if (!task) return ctx.text('Not found', 404); return ctx.json({ - name: frontmatter.name || name, - description: frontmatter.description, - scope: resolved.scope, - body, - rawFrontmatter: rawYaml, - filePath: resolved.filePath, - chatSessionId, + id: task.id, + dirName: task.dirName, + name: task.name, + description: task.description, + scope: task.scope, + mode: task.mode, + language: task.language, + body: task.body, + implementation: task.implementation, + inputs: task.inputs, + args: task.args, + trigger: task.trigger, + version: task.version, + userId: task.userId, }); }); -tasksRouter.get('/:name/chat', async (ctx) => { - const user = ctx.get('user'); - const name = ctx.req.param('name'); - - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); - - const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks); - if (!resolved) return ctx.text('Not found', 404); - - const chatDir = join(dirname(resolved.filePath), 'chat'); - const sessionId = await Bun.file(join(chatDir, 'meta.json')).json().then((m: { id: string }) => m.id).catch(() => null); - const messages = await Bun.file(join(chatDir, 'messages.json')).json().catch(() => []); - - return ctx.json({ sessionId, messages }); -}); - -tasksRouter.put('/:name/chat', async (ctx) => { - const user = ctx.get('user'); - const name = ctx.req.param('name'); - - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); - - const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks); - if (!resolved) return ctx.text('Not found', 404); - - if (resolved.scope !== 'user' && !isPrivileged(user.role)) return ctx.text('Forbidden', 403); - - const chatDir = join(dirname(resolved.filePath), 'chat'); - const { sessionId, messages } = await ctx.req.json<{ sessionId: string; messages: unknown[] }>(); - - await mkdir(chatDir, { recursive: true }); - await Bun.write(join(chatDir, 'messages.json'), JSON.stringify(messages)); - if (sessionId) await Bun.write(join(chatDir, 'meta.json'), JSON.stringify({ id: sessionId })); - - return ctx.json({ ok: true }); -}); - -tasksRouter.delete('/:name/chat', async (ctx) => { - const user = ctx.get('user'); - const name = ctx.req.param('name'); - - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); - - const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks); - if (!resolved) return ctx.text('Not found', 404); - - if (resolved.scope !== 'user' && !isPrivileged(user.role)) return ctx.text('Forbidden', 403); - - const chatDir = join(dirname(resolved.filePath), 'chat'); - await rm(chatDir, { recursive: true, force: true }); - - return ctx.json({ ok: true }); -}); - tasksRouter.post('/', async (ctx) => { const user = ctx.get('user'); - const { name } = await ctx.req.json<{ name: string }>(); - if (!name?.trim()) return ctx.text('Name is required', 400); + const body = await ctx.req.json<{ name: string; description?: string; mode?: string; language?: string }>(); - const dirName = name.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + if (!body.name?.trim()) return ctx.text('Name is required', 400); + + const dirName = body.name.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); if (!dirName) return ctx.text('Invalid name', 400); - const targetDir = isPrivileged(user.role) ? getGlobalTasksDir() : getUserTasksDir(user.email); - const scope: Scope = isPrivileged(user.role) ? 'global' : 'user'; + const scope = isPrivileged(user.role) ? 'global' : 'user'; - const dir = join(targetDir, dirName); - const filePath = join(dir, 'TASK.md'); + const task = await createTask({ + scope, + userId: user.id, + dirName, + name: body.name.trim(), + description: body.description ?? null, + mode: body.mode ?? 'agentic', + language: body.language ?? null, + }); - if (await Bun.file(filePath).exists()) { - return ctx.text('Task already exists', 409); - } - - await mkdir(dir, { recursive: true }); - await Bun.write(filePath, `---\nname: ${name.trim()}\ndescription: \n---\n`); - - return ctx.json({ name: name.trim(), dirName, filePath, scope }); + return ctx.json(task); }); tasksRouter.delete('/:name', async (ctx) => { const user = ctx.get('user'); const name = ctx.req.param('name'); - const nativeTasks = await readTaskDirs(getNativeTasksDir()); - const globalTasks = await readTaskDirs(getGlobalTasksDir()); - const userTasks = await readTaskDirs(getUserTasksDir(user.email)); + const task = await getTaskByDirName(name, user.id); + if (!task) return ctx.text('Not found', 404); - const resolved = resolveFile(name, nativeTasks, globalTasks, userTasks); - if (!resolved) return ctx.text('Not found', 404); + // Only owner or Super Admin can delete + if (task.scope === 'native') return ctx.text('Cannot delete native tasks', 403); + if (task.userId !== user.id && !isPrivileged(user.role)) return ctx.text('Forbidden', 403); - if (resolved.scope !== 'user' && !isPrivileged(user.role)) return ctx.text('Forbidden', 403); - - await rm(dirname(resolved.filePath), { recursive: true }); + await deleteTask(task.id); return ctx.json({ ok: true }); }); diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index 023cffee..2fa7b97e 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -8,6 +8,7 @@ import { usePiChat, MessageBubble, StreamingBubble, ModelSelector } from '../../ import { useSettings } from 'state/useSettings'; import { useUserVisibleModels } from 'state/useModels'; import type { TaskSummary } from '../../useTasks'; +import { useTaskRunner } from './useTaskRunner'; const playDing = () => { const ctx = new AudioContext(); @@ -190,6 +191,97 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P ); }; +// ── Script-mode runner ── + +type ScriptRunnerProps = { + taskDirName: string; + inputs: Record; + cwd?: string; +}; + +const ScriptRunner = ({ taskDirName, inputs, cwd }: ScriptRunnerProps) => { + const runner = useTaskRunner(); + const bottomRef = useRef(null); + + // Auto-scroll + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [runner.output]); + + // Ding on completion + const prevPhaseRef = useRef(runner.phase); + useEffect(() => { + if (prevPhaseRef.current === 'running' && runner.phase === 'done') { + playDing(); + } + prevPhaseRef.current = runner.phase; + }, [runner.phase]); + + const handleRun = () => { + runner.run(taskDirName, inputs, cwd); + }; + + if (runner.phase === 'ready') { + return ( +
+ +
+ ); + } + + return ( +
+
+
+          {runner.output.map((line, i) => (
+            
+              {line.text}
+            
+          ))}
+        
+
+
+
+ {runner.phase === 'running' ? ( + + ) : runner.exitCode !== 0 ? ( + + + Task failed (exit {runner.exitCode}) + + ) : ( + + + Task complete + + )} +
+
+ ); +}; + type TaskRunnerModalProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -207,12 +299,19 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull const { settings } = useSettings(); const taskSettings = settings.tasks; const entryRef = entryFullPath ?? entryName; + const isScript = task.mode === 'script'; + + // Agentic mode prompt const defaultInput = promptOverride ?? (entryRef && entryType - ? `Read the task instructions at ${task.filePath} and execute them on the ${entryType}: ${entryRef}` - : `Read the task instructions at ${task.filePath} and execute them`); + ? `Execute the task "${task.name}" (${task.dirName}) on the ${entryType}: ${entryRef}` + : `Execute the task "${task.name}" (${task.dirName})`); const taskInfo: TaskInfo = { taskName: task.name, taskDirName: task.dirName, entryName: entryName ?? '', entryType: entryType ?? 'file' }; + // Script mode inputs — for now, map the entry path to file_path + const scriptInputs: Record = {}; + if (entryFullPath) scriptInputs.file_path = entryFullPath; + return ( @@ -238,15 +337,24 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull )}
- {/* Task Runner */} - + {/* Task Runner — branch on mode */} + {isScript ? ( + + ) : ( + + )} diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/useTaskRunner.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/useTaskRunner.ts new file mode 100644 index 00000000..145d6795 --- /dev/null +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/useTaskRunner.ts @@ -0,0 +1,82 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; + +type Phase = 'ready' | 'running' | 'done'; + +type ServerMessage = + | { type: 'started'; taskName: string } + | { type: 'stdout'; data: string } + | { type: 'stderr'; data: string } + | { type: 'exit'; code: number } + | { type: 'error'; message: string }; + +export function useTaskRunner() { + const [phase, setPhase] = useState('ready'); + const [output, setOutput] = useState>([]); + const [exitCode, setExitCode] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const wsRef = useRef(null); + + useEffect(() => { + const token = localStorage.getItem('BEARER_TOKEN'); + if (!token) return; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${protocol}//${window.location.host}/api/tasks/run/ws?token=${token}`; + const ws = new WebSocket(url); + wsRef.current = ws; + + ws.addEventListener('open', () => setIsConnected(true)); + ws.addEventListener('close', () => setIsConnected(false)); + + ws.addEventListener('message', (ev) => { + try { + const msg = JSON.parse(ev.data) as ServerMessage; + + switch (msg.type) { + case 'started': + setOutput((prev) => [...prev, { stream: 'system', text: `Running: ${msg.taskName}\n` }]); + break; + case 'stdout': + setOutput((prev) => [...prev, { stream: 'stdout', text: msg.data }]); + break; + case 'stderr': + setOutput((prev) => [...prev, { stream: 'stderr', text: msg.data }]); + break; + case 'exit': + setExitCode(msg.code); + setPhase('done'); + break; + case 'error': + setOutput((prev) => [...prev, { stream: 'stderr', text: `Error: ${msg.message}\n` }]); + setPhase('done'); + setExitCode(-1); + break; + } + } catch { + // ignore + } + }); + + return () => { + ws.close(); + wsRef.current = null; + }; + }, []); + + const run = useCallback((taskDirName: string, inputs: Record, cwd?: string) => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + + setPhase('running'); + setOutput([]); + setExitCode(null); + + wsRef.current.send(JSON.stringify({ type: 'run', taskDirName, inputs, cwd })); + }, []); + + const stop = useCallback(() => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + wsRef.current.send(JSON.stringify({ type: 'stop' })); + }, []); + + return { phase, output, exitCode, isConnected, run, stop }; +} diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/useTasks.ts b/src/workspaces/officerdev/src/apps/FileBrowser/useTasks.ts index 4c6dcf85..acd40285 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/useTasks.ts +++ b/src/workspaces/officerdev/src/apps/FileBrowser/useTasks.ts @@ -4,12 +4,14 @@ import { useClient } from 'hooks/useClient'; type TriggerConfig = { type: 'file'; extensions: string[] } | { type: 'directory' }; export type TaskSummary = { + id: number; dirName: string; name: string; description: string; - scope: 'user' | 'global'; + scope: string; triggers: TriggerConfig[]; - filePath: string; + mode: 'script' | 'agentic'; + userId: number | null; }; export const useTasks = () => {