diff --git a/apps/cli/src/domains/cloud/setup.rs b/apps/cli/src/domains/cloud/setup.rs index 0ef7b166b..1bd02e7ca 100644 --- a/apps/cli/src/domains/cloud/setup.rs +++ b/apps/cli/src/domains/cloud/setup.rs @@ -238,8 +238,7 @@ async fn add_dropbox_interactive(ctx: &Context) -> Result<()> { let client_id = text("App Key (Client ID)", false)?.unwrap(); let client_secret = password("App Secret (Client Secret)", false)?.unwrap(); - println!("\nAfter authorizing, you'll receive tokens:"); - let access_token = password("Access Token", false)?.unwrap(); + println!("\nAfter completing OAuth flow, you'll receive a refresh token:"); let refresh_token = password("Refresh Token", false)?.unwrap(); println!("\nSummary:"); @@ -257,7 +256,6 @@ async fn add_dropbox_interactive(ctx: &Context) -> Result<()> { display_name: name.clone(), config: CloudStorageConfig::Dropbox { root, - access_token, refresh_token, client_id, client_secret, diff --git a/apps/cli/src/domains/volume/args.rs b/apps/cli/src/domains/volume/args.rs index abd46af78..df7bf2c08 100644 --- a/apps/cli/src/domains/volume/args.rs +++ b/apps/cli/src/domains/volume/args.rs @@ -177,9 +177,6 @@ impl VolumeAddCloudArgs { } } CloudServiceArg::Dropbox => { - let access_token = self - .access_token - .ok_or("--access-token is required for Dropbox")?; let refresh_token = self .refresh_token .ok_or("--refresh-token is required for Dropbox")?; @@ -192,7 +189,6 @@ impl VolumeAddCloudArgs { CloudStorageConfig::Dropbox { root: self.root, - access_token, refresh_token, client_id, client_secret, diff --git a/apps/tauri/packages/ts-client/src/generated/types.ts b/apps/tauri/packages/ts-client/src/generated/types.ts new file mode 100644 index 000000000..2e5210f81 --- /dev/null +++ b/apps/tauri/packages/ts-client/src/generated/types.ts @@ -0,0 +1,4420 @@ +// Generated by Spacedrive using Specta + rspc-inspired type extraction - DO NOT EDIT +// This file is auto-generated. See core/src/bin/generate_typescript_types.rs + +// Empty type for operations with no input +export type Empty = Record; + +// This file has been generated by Specta. DO NOT EDIT. + +export type ActionContextInfo = { action_type: string; initiated_at: string; initiated_by: string | null; action_input: JsonValue; context: JsonValue }; + +export type ActiveJobItem = { id: string; name: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null }; + +export type ActiveJobsInput = Record; + +export type ActiveJobsOutput = { jobs: ActiveJobItem[]; running_count: number; paused_count: number }; + +export type AddGroupInput = { space_id: string; name: string; group_type: GroupType }; + +export type AddGroupOutput = { group: SpaceGroup }; + +export type AddItemInput = { space_id: string; group_id: string | null; item_type: ItemType }; + +export type AddItemOutput = { item: SpaceItem }; + +/** + * Input for alternate instances query + */ +export type AlternateInstancesInput = { +/** + * The entry UUID to find alternates for + */ +entry_uuid: string }; + +/** + * Output containing alternate instances + */ +export type AlternateInstancesOutput = { +/** + * All instances of this file (including the original) + */ +instances: File[]; +/** + * Total number of instances found + */ +total_count: number }; + +/** + * Represents an APFS container (physical storage with multiple volumes) + */ +export type ApfsContainer = { container_id: string; uuid: string; physical_store: string; total_capacity: number; capacity_in_use: number; capacity_free: number; volumes: ApfsVolumeInfo[] }; + +/** + * APFS volume information within a container + */ +export type ApfsVolumeInfo = { disk_id: string; uuid: string; role: ApfsVolumeRole; name: string; mount_point: string | null; snapshot_mount_point: string | null; capacity_consumed: number; sealed: boolean; filevault: boolean }; + +/** + * APFS volume roles in the container + */ +export type ApfsVolumeRole = "System" | "Data" | "Preboot" | "Recovery" | "VM" | { Other: string }; + +export type ApplyTagsInput = { +/** + * What to tag: content identities or specific entries + */ +targets: TagTargets; +/** + * Tag IDs to apply + */ +tag_ids: string[]; +/** + * Source of the tag application + */ +source: TagSource | null; +/** + * Confidence score (for AI-applied tags) + */ +confidence: number | null; +/** + * Context when applying (e.g., "image_analysis", "user_input") + */ +applied_context: string | null; +/** + * Instance-specific attributes for this application + */ +instance_attributes: { [key in string]: JsonValue } | null }; + +export type ApplyTagsOutput = { +/** + * Number of entries that had tags applied + */ +entries_affected: number; +/** + * Number of tags that were applied + */ +tags_applied: number; +/** + * Tag IDs that were successfully applied + */ +applied_tag_ids: string[]; +/** + * Entry IDs that were successfully tagged + */ +tagged_entry_ids: number[]; +/** + * Any warnings or notes about the operation + */ +warnings: string[]; +/** + * Success message + */ +message: string }; + +/** + * Targets for immediately applying a newly created tag + */ +export type ApplyToTargets = +/** + * Apply to content identities (all instances) + */ +{ type: "Content"; ids: string[] } | +/** + * Apply to specific entries (single instance) + */ +{ type: "Entry"; ids: number[] }; + +/** + * Audio metadata extracted from FFmpeg + */ +export type AudioMediaData = { uuid: string; duration_seconds: number | null; bit_rate: number | null; sample_rate: number | null; channels: string | null; codec: string | null; title: string | null; artist: string | null; album: string | null; album_artist: string | null; genre: string | null; year: number | null; track_number: number | null; disc_number: number | null; composer: string | null; publisher: string | null; copyright: string | null }; + +/** + * Cloud service type identifier + */ +export type CloudServiceType = "s3" | "gdrive" | "dropbox" | "onedrive" | "gcs" | "azblob" | "b2" | "wasabi" | "spaces" | "cloud"; + +export type CloudStorageConfig = { type: "S3"; bucket: string; region: string; access_key_id: string; secret_access_key: string; endpoint: string | null } | { type: "GoogleDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | { type: "OneDrive"; root: string | null; access_token: string; refresh_token: string; client_id: string; client_secret: string } | { type: "Dropbox"; root: string | null; access_token: string | null; refresh_token: string; client_id: string; client_secret: string } | { type: "AzureBlob"; container: string; endpoint: string | null; account_name: string; account_key: string } | { type: "GoogleCloudStorage"; bucket: string; root: string | null; endpoint: string | null; credential: string }; + +/** + * Operators for combining tag attributes + */ +export type CompositionOperator = +/** + * All conditions must be true + */ +"And" | +/** + * Any condition must be true + */ +"Or" | +/** + * Must have this property + */ +"With" | +/** + * Must not have this property + */ +"Without"; + +/** + * Rules for composing attributes from multiple tags + */ +export type CompositionRule = { operator: CompositionOperator; operands: string[]; result_attribute: string }; + +/** + * Domain representation of content identity + */ +export type ContentIdentity = { uuid: string; kind: ContentKind; content_hash: string; integrity_hash: string | null; mime_type_id: number | null; text_content: string | null; total_size: number; entry_count: number; first_seen_at: string; last_verified_at: string }; + +/** + * Type of content + */ +export type ContentKind = "unknown" | "image" | "video" | "audio" | "document" | "archive" | "code" | "text" | "database" | "book" | "font" | "mesh" | "config" | "encrypted" | "key" | "executable" | "binary" | "spreadsheet" | "presentation" | "email" | "calendar" | "contact" | "web" | "shortcut" | "package" | "model_entry" | "memory"; + +/** + * A single content kind with its file count + */ +export type ContentKindStat = { +/** + * The content kind (image, video, audio, etc.) + */ +kind: ContentKind; +/** + * The name of the content kind + */ +name: string; +/** + * The number of files with this content kind + */ +file_count: number }; + +/** + * Input for content kind statistics query + */ +export type ContentKindStatsInput = Record; + +/** + * Output containing content kind statistics + */ +export type ContentKindStatsOutput = { +/** + * Statistics for each content kind + */ +stats: ContentKindStat[]; +/** + * Total number of files across all content kinds + */ +total_files: number }; + +/** + * Copy method preference for file operations + */ +export type CopyMethod = +/** + * Automatically select the best method based on source and destination + */ +"Auto" | +/** + * Use atomic operations (rename for moves, APFS clone for copies, etc.) + */ +"Atomic" | +/** + * Use streaming copy/move (works across all scenarios) + */ +"Streaming"; + +export type CoreStatus = { version: string; built_at: string; library_count: number; device_info: DeviceInfo; libraries: LibraryInfo[]; services: ServiceStatus; network: NetworkStatus; system: SystemInfo }; + +/** + * Input for creating a new folder + */ +export type CreateFolderInput = { +/** + * Parent directory where the folder will be created + */ +parent: SdPath; +/** + * Name for the new folder + */ +name: string; +/** + * Optional items to move into the new folder after creation + */ +items?: SdPath[] }; + +/** + * Output from creating a folder + */ +export type CreateFolderOutput = { +/** + * Path to the created folder + */ +folder_path: SdPath; +/** + * Job receipt if items were moved into the folder + */ +job_receipt?: JobReceipt | null }; + +export type CreateTagInput = { +/** + * The canonical name for this tag + */ +canonical_name: string; +/** + * Optional display name (if different from canonical) + */ +display_name: string | null; +/** + * Semantic variants + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType | null; +/** + * Visual properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean | null; privacy_level: PrivacyLevel | null; search_weight: number | null; +/** + * Initial attributes + */ +attributes: { [key in string]: JsonValue } | null; +/** + * Optional: Targets to immediately apply this tag to after creation + */ +apply_to: ApplyToTargets | null }; + +export type CreateTagOutput = { +/** + * The created tag's UUID + */ +tag_id: string; +/** + * The canonical name of the created tag + */ +canonical_name: string; +/** + * The namespace if specified + */ +namespace: string | null; +/** + * Success message + */ +message: string }; + +/** + * Data volume metrics snapshot + */ +export type DataVolumeSnapshot = { entries_synced: { [key in string]: number }; entries_by_device: { [key in string]: DeviceMetricsSnapshot }; bytes_sent: number; bytes_received: number; last_sync_per_peer: { [key in string]: string }; last_sync_per_model: { [key in string]: string } }; + +/** + * Time-based fields that can be filtered + */ +export type DateField = "CreatedAt" | "ModifiedAt" | "AccessedAt"; + +/** + * Filter for a time-based field + */ +export type DateRangeFilter = { field: DateField; start: string | null; end: string | null }; + +export type DeleteGroupInput = { group_id: string }; + +export type DeleteGroupOutput = { success: boolean }; + +export type DeleteItemInput = { item_id: string }; + +export type DeleteItemOutput = { success: boolean }; + +export type DeleteWhisperModelInput = { model: string }; + +export type DeleteWhisperModelOutput = { deleted: boolean }; + +/** + * A device running Spacedrive + * + * This is the canonical device type used throughout the application. + * It represents both database-registered devices and network-paired devices. + */ +export type Device = { +/** + * Unique identifier for this device + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Unique slug for URI addressing (e.g., "jamies-macbook") + */ +slug: string; +/** + * Operating system + */ +os: OperatingSystem; +/** + * Operating system version + */ +os_version: string | null; +/** + * Hardware model (e.g., "MacBook Pro", "iPhone 15") + */ +hardware_model: string | null; +/** + * CPU model name (e.g., "Apple M3 Max", "Intel Core i9-13900K") + */ +cpu_model: string | null; +/** + * CPU architecture (e.g., "arm64", "x86_64") + */ +cpu_architecture: string | null; +/** + * Number of physical CPU cores + */ +cpu_cores_physical: number | null; +/** + * Number of logical CPU cores (with hyperthreading) + */ +cpu_cores_logical: number | null; +/** + * CPU base frequency in MHz + */ +cpu_frequency_mhz: number | null; +/** + * Total system memory in bytes + */ +memory_total_bytes: number | null; +/** + * Device form factor + */ +form_factor: DeviceFormFactor | null; +/** + * Device manufacturer (e.g., "Apple", "Dell", "Lenovo") + */ +manufacturer: string | null; +/** + * GPU model names (can have multiple GPUs) + */ +gpu_models: string[] | null; +/** + * Boot disk type (e.g., "SSD", "HDD", "NVMe") + */ +boot_disk_type: string | null; +/** + * Boot disk capacity in bytes + */ +boot_disk_capacity_bytes: number | null; +/** + * Total swap space in bytes + */ +swap_total_bytes: number | null; +/** + * Network addresses for P2P connections + */ +network_addresses: string[]; +/** + * Device capabilities (indexing, P2P, volume detection, etc.) + */ +capabilities: JsonValue; +/** + * Whether this device is currently online + */ +is_online: boolean; +/** + * Last time this device was seen + */ +last_seen_at: string; +/** + * Whether sync is enabled for this device + */ +sync_enabled: boolean; +/** + * Last time this device synced + */ +last_sync_at: string | null; +/** + * When this device was first added + */ +created_at: string; +/** + * When this device info was last updated + */ +updated_at: string; +/** + * Whether this is the current device (computed) + */ +is_current?: boolean; +/** + * Whether this device is paired via network but not in library DB + */ +is_paired?: boolean; +/** + * Whether this device is currently connected via network + */ +is_connected?: boolean }; + +/** + * Device form factor types + */ +export type DeviceFormFactor = "Desktop" | "Laptop" | "Mobile" | "Tablet" | "Server" | "Other"; + +export type DeviceInfo = { id: string; name: string; os: string; hardware_model: string | null; created_at: string }; + +/** + * Device metrics snapshot + */ +export type DeviceMetricsSnapshot = { device_id: string; device_name: string; entries_received: number; last_seen: string; is_online: boolean }; + +export type DeviceRevokeInput = { device_id: string; +/** + * Whether to also remove the device from all library databases + * + * If false (default), only unpairs from network but keeps device history in libraries. + * If true, completely removes device from libraries (deletes all records). + */ +remove_from_library?: boolean }; + +export type DeviceRevokeOutput = { revoked: boolean }; + +/** + * Device sync state for state machine + */ +export type DeviceSyncState = +/** + * Not yet synced, no backfill started + */ +"Uninitialized" | +/** + * Currently backfilling from peer(s) + * Buffers all live updates during this phase + */ +{ Backfilling: { peer: string; progress: number } } | +/** + * Backfill complete, processing buffered updates + * Still buffers new updates while catching up + */ +{ CatchingUp: { buffered_count: number } } | +/** + * Fully synced, applying live updates immediately + */ +"Ready" | +/** + * Sync paused (offline or user disabled) + */ +"Paused"; + +/** + * Input for directory listing + */ +export type DirectoryListingInput = { +/** + * The directory path to list contents for + */ +path: SdPath; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Whether to include hidden files (default: false) + */ +include_hidden: boolean | null; +/** + * Sort order for results + */ +sort_by: DirectorySortBy; +/** + * Whether to show folders before files (default: false) + */ +folders_first: boolean | null }; + +/** + * Output containing directory contents + */ +export type DirectoryListingOutput = { +/** + * Direct children of the directory as File objects + */ +files: File[]; +/** + * Total count of direct children + */ +total_count: number; +/** + * Whether this directory has more children than returned + */ +has_more: boolean }; + +/** + * Sort options for directory listing + */ +export type DirectorySortBy = +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by size (largest first) + */ +"size" | +/** + * Sort by type (directories first, then files) + */ +"type"; + +export type DiscoverRemoteLibrariesInput = { +/** + * Device ID to query for libraries + */ +deviceId: string }; + +/** + * Output from discovering remote libraries + */ +export type DiscoverRemoteLibrariesOutput = { +/** + * Remote device ID that was queried + */ +deviceId: string; +/** + * Remote device name + */ +deviceName: string; +/** + * List of libraries available on the remote device + */ +libraries: RemoteLibraryInfo[]; +/** + * Whether the device is currently online + */ +isOnline: boolean }; + +/** + * Disk type classification + */ +export type DiskType = +/** + * Solid State Drive + */ +"SSD" | +/** + * Hard Disk Drive + */ +"HDD" | +/** + * Network storage + */ +"Network" | +/** + * Virtual/RAM disk + */ +"Virtual" | +/** + * Unknown type + */ +"Unknown"; + +export type DownloadWhisperModelInput = { +/** + * Model size: "tiny", "base", "small", "medium", "large" + */ +model: string }; + +export type DownloadWhisperModelOutput = { +/** + * Job ID for tracking download progress + */ +job_id: string }; + +export type EnableIndexingInput = { +/** + * UUID of the location to enable indexing for + */ +id: string; +/** + * Index mode to use (defaults to Deep if not specified) + */ +index_mode?: string }; + +export type EnableIndexingOutput = { +/** + * UUID of the location that had indexing enabled + */ +location_id: string; +/** + * Job ID of the indexing job that was started + */ +job_id: string }; + +/** + * Type of filesystem entry + */ +export type EntryKind = +/** + * Regular file + */ +"File" | +/** + * Directory + */ +"Directory" | +/** + * Symbolic link + */ +"Symlink"; + +/** + * Input for resetting the ephemeral cache + */ +export type EphemeralCacheResetInput = { +/** + * Confirmation flag to prevent accidental cache clearing + */ +confirm: boolean }; + +/** + * Output from resetting the ephemeral cache + */ +export type EphemeralCacheResetOutput = { +/** + * Number of paths that were cleared from the cache + */ +cleared_paths: number; +/** + * Message describing the result + */ +message: string }; + +/** + * Status of the unified ephemeral index cache + */ +export type EphemeralCacheStatus = { +/** + * Number of paths that have been indexed + */ +indexed_paths_count: number; +/** + * Number of paths currently being indexed + */ +indexing_in_progress_count: number; +/** + * Unified index statistics (shared arena and string interning) + */ +index_stats: UnifiedIndexStats; +/** + * List of indexed paths (directories whose contents are ready) + */ +indexed_paths: IndexedPathInfo[]; +/** + * List of paths currently being indexed + */ +paths_in_progress: string[]; total_indexes?: number | null; indexing_in_progress?: number | null; indexes?: EphemeralIndexInfo[] }; + +/** + * Input for the ephemeral cache status query + */ +export type EphemeralCacheStatusInput = { +/** + * Optional: only include indexed paths containing this substring + */ +path_filter?: string | null }; + +/** + * Legacy: Information about a single ephemeral index (for backward compatibility) + */ +export type EphemeralIndexInfo = { +/** + * Root path this index covers + */ +root_path: string; +/** + * Whether indexing is currently in progress + */ +indexing_in_progress: boolean; +/** + * Total entries in the arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names + */ +unique_names: number; +/** + * Number of interned strings in cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the index in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number; +/** + * Indexer job statistics (files/dirs/bytes counted) + */ +job_stats: JobStats }; + +/** + * Error event for tracking recent errors + */ +export type ErrorEvent = { timestamp: string; error_type: string; message: string; model_type: string | null; device_id: string | null }; + +/** + * Error metrics snapshot + */ +export type ErrorSnapshot = { total_errors: number; network_errors: number; database_errors: number; apply_errors: number; validation_errors: number; recent_errors: ErrorEvent[]; conflicts_detected: number; conflicts_resolved_by_hlc: number }; + +/** + * A central event type that represents all events that can be emitted throughout the system + */ +export type Event = "CoreStarted" | "CoreShutdown" | { LibraryCreated: { id: string; name: string; path: string; +/** + * How the library was created (manual, sync, cloud import) + */ +source?: LibraryCreationSource } } | { LibraryOpened: { id: string; name: string; path: string } } | { LibraryClosed: { id: string; name: string } } | { LibraryDeleted: { id: string; name: string; deleted_data: boolean } } | { LibraryLoadFailed: { +/** + * Library ID if config was readable, None otherwise + */ +id: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * Human-readable error message + */ +error: string; +/** + * Error type for frontend categorization (e.g., "DatabaseError", "ConfigError") + */ +error_type: string } } | { LibraryStatisticsUpdated: { library_id: string; statistics: LibraryStatistics } } | +/** + * Refresh event - signals that all frontend caches should be invalidated + * Emitted after major data recalculations (e.g., volume unique_bytes refresh) + */ +"Refresh" | { EntryCreated: { library_id: string; entry_id: string } } | { EntryModified: { library_id: string; entry_id: string } } | { EntryDeleted: { library_id: string; entry_id: string } } | { EntryMoved: { library_id: string; entry_id: string; old_path: string; new_path: string } } | { FsRawChange: { library_id: string; kind: FsRawEventKind } } | { VolumeAdded: Volume } | { VolumeRemoved: { fingerprint: VolumeFingerprint } } | { VolumeUpdated: { fingerprint: VolumeFingerprint; old_info: VolumeInfo; new_info: VolumeInfo } } | { VolumeSpeedTested: { fingerprint: VolumeFingerprint; read_speed_mbps: number; write_speed_mbps: number } } | { VolumeMountChanged: { fingerprint: VolumeFingerprint; is_mounted: boolean } } | { VolumeError: { fingerprint: VolumeFingerprint; error: string } } | { JobQueued: { job_id: string; job_type: string; device_id: string } } | { JobStarted: { job_id: string; job_type: string; device_id: string } } | { JobProgress: { job_id: string; job_type: string; device_id: string; progress: number; message: string | null; generic_progress: GenericProgress | null } } | { JobCompleted: { job_id: string; job_type: string; device_id: string; output: JobOutput } } | { JobFailed: { job_id: string; job_type: string; device_id: string; error: string } } | { JobCancelled: { job_id: string; job_type: string; device_id: string } } | { JobPaused: { job_id: string; device_id: string } } | { JobResumed: { job_id: string; device_id: string } } | { IndexingStarted: { location_id: string } } | { IndexingProgress: { location_id: string; processed: number; total: number | null } } | { IndexingCompleted: { location_id: string; total_files: number; total_dirs: number } } | { IndexingFailed: { location_id: string; error: string } } | { DeviceConnected: { device_id: string; device_name: string } } | { DeviceDisconnected: { device_id: string } } | { SyncStateChanged: { library_id: string; previous_state: string; new_state: string; timestamp: string } } | { SyncActivity: { library_id: string; peer_device_id: string; activity_type: SyncActivityType; model_type: string | null; count: number; timestamp: string } } | { SyncConnectionChanged: { library_id: string; peer_device_id: string; peer_name: string; connected: boolean; timestamp: string } } | { SyncError: { library_id: string; peer_device_id: string | null; error_type: string; message: string; timestamp: string } } | { ResourceChanged: { +/** + * Resource type identifier (e.g., "location", "tag", "album") + */ +resource_type: string; +/** + * The full resource data as JSON + */ +resource: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceChangedBatch: { +/** + * Resource type identifier (e.g., "file") + */ +resource_type: string; +/** + * Array of full resource data as JSON + * Used for batch updates during indexing to reduce event overhead + */ +resources: JsonValue; +/** + * Metadata for proper cache updates + */ +metadata?: ResourceMetadata | null } } | { ResourceDeleted: { +/** + * Resource type identifier + */ +resource_type: string; +/** + * The deleted resource's ID + */ +resource_id: string } } | { LocationAdded: { library_id: string; location_id: string; path: string } } | { LocationRemoved: { library_id: string; location_id: string } } | { FilesIndexed: { library_id: string; location_id: string; count: number } } | { ThumbnailsGenerated: { library_id: string; count: number } } | { FileOperationCompleted: { library_id: string; operation: FileOperation; affected_files: number } } | { FilesModified: { library_id: string; paths: string[] } } | { Custom: { event_type: string } }; + +/** + * Event category for grouping related events + */ +export type EventCategory = +/** + * State machine lifecycle events + */ +"lifecycle" | +/** + * Data synchronization flow + */ +"data_flow" | +/** + * Network communication + */ +"network" | +/** + * Errors and failures + */ +"error"; + +export type EventInfo = { +/** + * The event variant name (e.g., "JobProgress", "LibraryCreated") + */ +variant: string; +/** + * Whether this event is considered "noisy" (high frequency, should be excluded by default) + */ +is_noisy: boolean; +/** + * Human-readable description + */ +description: string }; + +/** + * Event severity level + */ +export type EventSeverity = +/** + * Debug-level information + */ +"debug" | +/** + * Informational event + */ +"info" | +/** + * Warning condition + */ +"warning" | +/** + * Error condition + */ +"error"; + +/** + * Statistics about what was exported + */ +export type ExportStats = { entries: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +export type ExtractTextInput = { +/** + * UUID of the entry to extract text from + */ +entry_uuid: string; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[] | null; +/** + * Force re-extraction even if text exists + */ +force: boolean }; + +export type ExtractTextOutput = { +/** + * Job ID for tracking OCR progress + */ +job_id: string }; + +/** + * Represents a file within the Spacedrive VDFS. + * + * This is a computed domain model that aggregates data from Entry, ContentIdentity, + * Tags, and Sidecars. It provides a rich, developer-friendly interface without + * duplicating data in the database. + */ +export type File = { +/** + * The unique identifier of the file entry + */ +id: string; +/** + * The universal path to the file in Spacedrive's VDFS + */ +sd_path: SdPath; +/** + * The file kind (file, directory, symlink) + */ +kind: EntryKind; +/** + * The name of the file, including the extension + */ +name: string; +/** + * The file extension (without dot) + */ +extension: string | null; +/** + * The size of the file in bytes + */ +size: number; +/** + * Information about the file's content, including its content hash + */ +content_identity: ContentIdentity | null; +/** + * A list of other paths that share the same content identity + */ +alternate_paths: SdPath[]; +/** + * The semantic tags associated with this file + */ +tags: Tag[]; +/** + * A list of sidecars associated with this file + */ +sidecars: Sidecar[]; +/** + * Media-specific metadata (extracted from EXIF/FFmpeg) + */ +image_media_data: ImageMediaData | null; video_media_data: VideoMediaData | null; audio_media_data: AudioMediaData | null; +/** + * Timestamps for creation, modification, and access + */ +created_at: string; modified_at: string; accessed_at: string | null; +/** + * Additional computed fields + */ +content_kind: ContentKind; is_local: boolean; +/** + * Video duration (for grid display optimization) + */ +duration_seconds: number | null }; + +/** + * Query to get a file by its ID with all related data + */ +export type FileByIdQuery = { file_id: string }; + +/** + * Query to get a file by its local path with all related data + */ +export type FileByPathQuery = { path: string }; + +/** + * Internal enum for file conflict resolution strategies + */ +export type FileConflictResolution = "Overwrite" | "AutoModifyName" | "Skip" | "Abort"; + +/** + * Core input structure for file copy operations + * This is the canonical interface that all external APIs (CLI, REST) convert to + */ +export type FileCopyInput = { +/** + * Source files or directories to copy (domain addressing) + */ +sources: SdPathBatch; +/** + * Destination path (domain addressing) + */ +destination: SdPath; +/** + * Whether to overwrite existing files + */ +overwrite: boolean; +/** + * Whether to verify checksums during copy + */ +verify_checksum: boolean; +/** + * Whether to preserve file timestamps + */ +preserve_timestamps: boolean; +/** + * Whether to delete source files after copying (move operation) + */ +move_files: boolean; +/** + * Preferred copy method to use + */ +copy_method: CopyMethod; +/** + * How to handle file conflicts (set by CLI confirmation) + */ +on_conflict: FileConflictResolution | null }; + +/** + * Input for deleting files + */ +export type FileDeleteInput = { +/** + * Files or directories to delete + */ +targets: SdPathBatch; +/** + * Whether to permanently delete (true) or move to trash (false) + */ +permanent: boolean; +/** + * Whether to delete directories recursively + */ +recursive: boolean }; + +/** + * Types of file operations + */ +export type FileOperation = "Copy" | "Move" | "Delete" | "Rename"; + +/** + * Input for renaming a file or directory + */ +export type FileRenameInput = { +/** + * The file or directory to rename + */ +target: SdPath; +/** + * The new name (filename only, no path separators) + */ +new_name: string }; + +/** + * Main input structure for file search operations + */ +export type FileSearchInput = { +/** + * Primary search query (filename, content, or natural language) + */ +query: string; +/** + * Search scope (library, location, or specific path) + */ +scope: SearchScope; +/** + * Search mode (fast, normal, full) + */ +mode: SearchMode; +/** + * Filters to narrow results + */ +filters: SearchFilters; +/** + * Sorting options + */ +sort: SortOptions; +/** + * Pagination + */ +pagination: PaginationOptions }; + +/** + * Main output structure for file search operations + */ +export type FileSearchOutput = { +/** + * Flat file array matching DirectoryListingOutput - primary field for explorer + */ +files: File[]; +/** + * Search results with scoring metadata - use for search-specific UI (scores, highlights) + */ +results: FileSearchResult[]; total_found: number; search_id: string; facets: SearchFacets; suggestions: string[]; pagination: PaginationInfo; execution_time_ms: number; +/** + * Which index type was used for this search + */ +index_type: IndexType; +/** + * Which filters are available for this search type + */ +available_filters: FilterKind[] }; + +/** + * Individual search result + */ +export type FileSearchResult = { file: File; score: number; score_breakdown: ScoreBreakdown; highlights: TextHighlight[]; matched_content: string | null }; + +/** + * Filesystem type + */ +export type FileSystem = +/** + * Apple File System + */ +"APFS" | +/** + * NT File System (Windows) + */ +"NTFS" | +/** + * Fourth Extended Filesystem (Linux) + */ +"Ext4" | +/** + * B-tree Filesystem (Linux) + */ +"Btrfs" | +/** + * ZFS + */ +"ZFS" | +/** + * Resilient File System (Windows) + */ +"ReFS" | +/** + * File Allocation Table 32 + */ +"FAT32" | +/** + * Extended File Allocation Table + */ +"ExFAT" | +/** + * Hierarchical File System Plus (macOS legacy) + */ +"HFSPlus" | +/** + * Network File System + */ +"NFS" | +/** + * Server Message Block + */ +"SMB" | +/** + * Other filesystem + */ +{ Other: string }; + +/** + * Indicates which filters are available for a given search type + */ +export type FilterKind = "FileTypes" | "DateRange" | "SizeRange" | "ContentTypes" | "Tags" | "Locations" | "Hidden" | "Archived"; + +/** + * Raw filesystem event kinds emitted by the watcher without DB resolution + */ +export type FsRawEventKind = { Create: { path: string } } | { Modify: { path: string } } | { Remove: { path: string } } | { Rename: { from: string; to: string } }; + +/** + * Generate proxy for a single video file + */ +export type GenerateProxyInput = { +/** + * UUID of the entry to generate proxy for + */ +entry_uuid: string; +/** + * Proxy resolution (scrubbing, ultra_low, quick, editing) + */ +resolution: string | null; +/** + * Force regeneration even if proxy exists + */ +force: boolean; +/** + * Use hardware acceleration if available + */ +use_hardware_accel: boolean | null }; + +export type GenerateProxyOutput = { +/** + * Number of proxies generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[]; +/** + * Total encoding time in seconds + */ +encoding_time_secs: number }; + +export type GenerateSplatInput = { entry_uuid: string; model_path: string | null }; + +export type GenerateSplatOutput = { +/** + * Job ID for tracking splat generation progress + */ +job_id: string }; + +/** + * Generate thumbstrip for a single video file + */ +export type GenerateThumbstripInput = { +/** + * UUID of the entry to generate thumbstrip for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to thumbstrip_preview) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbstrip exists + */ +force: boolean }; + +export type GenerateThumbstripOutput = { +/** + * Number of thumbstrips generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * Generic progress information that all job types can convert into + */ +export type GenericProgress = { +/** + * Current progress as a percentage (0.0 to 1.0) + */ +percentage: number; +/** + * Current phase or stage name (e.g., "Discovery", "Processing", "Finalizing") + */ +phase: string; +/** + * Current path being processed (if applicable) + */ +current_path: SdPath | null; +/** + * Human-readable message describing current activity + */ +message: string; +/** + * Completion metrics + */ +completion: ProgressCompletion; +/** + * Performance metrics + */ +performance: PerformanceMetrics }; + +/** + * Input for getting sync activity summary + */ +export type GetSyncActivityInput = Record; + +/** + * Sync activity summary for the UI + */ +export type GetSyncActivityOutput = { currentState: DeviceSyncState; peers: PeerActivity[]; errorCount: number }; + +export type GetSyncEventLogInput = { +/** + * Time range filter (start) + */ +start_time?: string | null; +/** + * Time range filter (end) + */ +end_time?: string | null; +/** + * Filter by event types + */ +event_types?: SyncEventType[] | null; +/** + * Filter by categories + */ +categories?: EventCategory[] | null; +/** + * Filter by severity levels + */ +severities?: EventSeverity[] | null; +/** + * Filter by peer device + */ +peer_id?: string | null; +/** + * Filter by model type + */ +model_type?: string | null; +/** + * Filter by correlation ID + */ +correlation_id?: string | null; +/** + * Maximum number of results + */ +limit?: number | null; +/** + * Offset for pagination + */ +offset?: number | null; +/** + * Include events from remote peers + */ +include_remote_peers?: boolean | null }; + +export type GetSyncEventLogOutput = { events: SyncEventLog[] }; + +export type GetSyncMetricsInput = { +/** + * Filter metrics since this time + */ +since: string | null; +/** + * Filter metrics for specific peer device + */ +peer_id: string | null; +/** + * Filter metrics for specific model type + */ +model_type: string | null; +/** + * Show only state metrics + */ +state_only: boolean | null; +/** + * Show only operation metrics + */ +operations_only: boolean | null; +/** + * Show only error metrics + */ +errors_only: boolean | null }; + +export type GetSyncMetricsOutput = { +/** + * The metrics snapshot + */ +metrics: SyncMetricsSnapshot }; + +/** + * Types of groups that can appear in a space + */ +export type GroupType = +/** + * Fixed quick navigation (Overview, Recents, Favorites) + */ +"QuickAccess" | +/** + * Device with its volumes and locations as children + */ +{ Device: { device_id: string } } | +/** + * All devices (library and paired) across the system + */ +"Devices" | +/** + * All locations across all devices + */ +"Locations" | +/** + * All volumes across all devices + */ +"Volumes" | +/** + * Tag collection + */ +"Tags" | +/** + * Cloud storage providers + */ +"Cloud" | +/** + * User-defined custom group + */ +"Custom"; + +/** + * Image metadata extracted from EXIF + */ +export type ImageMediaData = { uuid: string; width: number; height: number; blurhash: string | null; date_taken: string | null; latitude: number | null; longitude: number | null; camera_make: string | null; camera_model: string | null; lens_model: string | null; focal_length: string | null; aperture: string | null; shutter_speed: string | null; iso: number | null; orientation: number | null; color_space: string | null; color_profile: string | null; bit_depth: string | null; artist: string | null; copyright: string | null; description: string | null }; + +/** + * Statistics about what was imported + */ +export type ImportStats = { entries_imported: number; entries_skipped: number; content_identities: number; user_metadata: number; tags: number; media_data: number }; + +/** + * Canonical input for indexing requests from any interface (CLI, API, etc.) + */ +export type IndexInput = { +/** + * The library within which the operation runs + */ +library_id: string; +/** + * One or more filesystem paths to index + */ +paths: string[]; +/** + * Indexing scope (current directory only vs recursive) + */ +scope: IndexScope; +/** + * Indexing mode (shallow/content/deep) + */ +mode: IndexMode; +/** + * Whether to include hidden files/directories + */ +include_hidden: boolean; +/** + * Where results are stored (ephemeral vs persistent) + */ +persistence: IndexPersistence }; + +/** + * How deeply to index files in this location + */ +export type IndexMode = +/** + * Location exists but is not indexed + */ +"None" | +/** + * Just filesystem metadata (name, size, dates) + */ +"Shallow" | +/** + * Generate content IDs for deduplication + */ +"Content" | +/** + * Full indexing - content IDs, text extraction, thumbnails + */ +"Deep"; + +/** + * Whether to write indexing results to the database or keep them in memory. + * + * Ephemeral persistence allows users to browse external drives and network shares + * without adding them as managed locations. The in-memory index survives for the + * session duration and provides the same API surface as persistent entries, enabling + * features like search and navigation to work identically for both modes. If an + * ephemeral path is later promoted to a managed location, UUIDs are preserved to + * maintain continuity for user metadata. + */ +export type IndexPersistence = +/** + * Write all results to database (normal operation) + */ +"Persistent" | +/** + * Keep results in memory only (for unmanaged paths) + */ +"Ephemeral"; + +/** + * Whether to index just one directory level or recurse through subdirectories. + * + * Current scope is used for UI navigation where users expand folders on-demand, + * while Recursive scope is used for full location indexing. Current scope with + * persistent storage enables progressive indexing where the UI drives which + * directories get indexed based on user interaction. + */ +export type IndexScope = +/** + * Index only the current directory (single level) + */ +"Current" | +/** + * Index recursively through all subdirectories + */ +"Recursive"; + +/** + * Indicates which index type was used for a search query + */ +export type IndexType = +/** + * Database FTS5 search (persistent index) + */ +"Persistent" | +/** + * In-memory ephemeral search + */ +"Ephemeral" | +/** + * Mix of both (future: hybrid searches) + */ +"Hybrid"; + +export type IndexVerifyInput = { +/** + * Path to verify (can be a location root or subdirectory) + */ +path: string; +/** + * Whether to check content hashes (slower but more thorough) + */ +verify_content?: boolean; +/** + * Whether to include detailed file-by-file comparison + */ +detailed_report?: boolean; +/** + * Whether to fix issues automatically (future feature) + */ +auto_fix?: boolean }; + +/** + * Result of index integrity verification + */ +export type IndexVerifyOutput = { +/** + * Overall integrity status + */ +is_valid: boolean; +/** + * Integrity report with detailed findings + */ +report: IntegrityReport; +/** + * Path that was verified + */ +path: string; +/** + * Time taken to verify (seconds) + */ +duration_secs: number }; + +/** + * Input for volume indexing action + */ +export type IndexVolumeInput = { +/** + * Volume fingerprint to index + */ +fingerprint: string; +/** + * Indexing scope (defaults to Recursive for full volume) + */ +scope?: IndexScope }; + +/** + * Output from volume indexing action + */ +export type IndexVolumeOutput = { +/** + * UUID of the indexed volume + */ +volume_id: string; +/** + * Job ID for tracking progress + */ +job_id: string; +/** + * Total files found (if job completed) + */ +total_files: number | null; +/** + * Total directories found (if job completed) + */ +total_directories: number | null; +/** + * Success message + */ +message: string }; + +/** + * Information about an indexed path + */ +export type IndexedPathInfo = { +/** + * The directory path that was indexed + */ +path: string; +/** + * Number of direct children in this directory + */ +child_count: number }; + +/** + * Complete snapshot of indexer performance after job completion. + */ +export type IndexerMetrics = { total_duration: { secs: number; nanos: number }; discovery_duration: { secs: number; nanos: number }; processing_duration: { secs: number; nanos: number }; content_duration: { secs: number; nanos: number }; files_per_second: number; bytes_per_second: number; dirs_per_second: number; db_writes: number; db_reads: number; batch_count: number; avg_batch_size: number; total_errors: number; critical_errors: number; non_critical_errors: number; skipped_paths: number; peak_memory_bytes: number | null; avg_memory_bytes: number | null }; + +/** + * Indexer settings controlling rule toggles + */ +export type IndexerSettings = { no_system_files?: boolean; no_git?: boolean; no_dev_dirs?: boolean; no_hidden?: boolean; gitignore?: boolean; only_images?: boolean }; + +/** + * Cumulative statistics tracked throughout the indexing process. + */ +export type IndexerStats = { files: number; dirs: number; bytes: number; symlinks: number; skipped: number; errors: number }; + +/** + * Represents a single integrity difference + */ +export type IntegrityDifference = { +/** + * Path relative to verification root + */ +path: string; +/** + * Type of issue + */ +issue_type: IssueType; +/** + * Expected value (from filesystem or correct state) + */ +expected: string | null; +/** + * Actual value (from database) + */ +actual: string | null; +/** + * Human-readable description + */ +description: string; +/** + * Debug: database entry ID for investigation + */ +db_entry_id?: number | null; +/** + * Debug: database entry name + */ +db_entry_name?: string | null }; + +/** + * Detailed integrity report + */ +export type IntegrityReport = { +/** + * Total files found on filesystem + */ +filesystem_file_count: number; +/** + * Total files in database index + */ +database_file_count: number; +/** + * Total directories found on filesystem + */ +filesystem_dir_count: number; +/** + * Total directories in database index + */ +database_dir_count: number; +/** + * Files missing from index (on filesystem but not in DB) + */ +missing_from_index: IntegrityDifference[]; +/** + * Stale entries in index (in DB but not on filesystem) + */ +stale_in_index: IntegrityDifference[]; +/** + * Entries with incorrect metadata + */ +metadata_mismatches: IntegrityDifference[]; +/** + * Entries with incorrect parent relationships + */ +hierarchy_errors: IntegrityDifference[]; +/** + * Summary statistics + */ +summary: string }; + +export type IssueType = { type: "MissingFromIndex" } | { type: "StaleInIndex" } | { type: "SizeMismatch" } | { type: "ModifiedTimeMismatch" } | { type: "InodeMismatch" } | { type: "ExtensionMismatch" } | { type: "ParentMismatch" } | { type: "KindMismatch" }; + +/** + * Types of items that can appear in a group + */ +export type ItemType = +/** + * Overview screen (fixed) + */ +"Overview" | +/** + * Recent files (fixed) + */ +"Recents" | +/** + * Favorited files (fixed) + */ +"Favorites" | +/** + * File kinds (images, videos, audio, etc.) + */ +"FileKinds" | +/** + * Indexed location + */ +{ Location: { location_id: string } } | +/** + * Storage volume (with locations as children) + */ +{ Volume: { volume_id: string } } | +/** + * Tag filter + */ +{ Tag: { tag_id: string } } | +/** + * Any arbitrary path (dragged from explorer) + */ +{ Path: { sd_path: SdPath } }; + +export type JobCancelInput = { job_id: string }; + +export type JobCancelOutput = { job_id: string; success: boolean }; + +/** + * Unique identifier for a job + */ +export type JobId = string; + +export type JobInfoOutput = { id: string; name: string; status: JobStatus; progress: number; created_at: string; started_at: string | null; completed_at: string | null; error_message: string | null }; + +export type JobInfoQueryInput = { job_id: string }; + +export type JobListInput = { status: JobStatus | null }; + +export type JobListItem = { id: string; name: string; device_id: string; status: JobStatus; progress: number; action_type: string | null; action_context: ActionContextInfo | null; created_at: string; started_at: string | null; completed_at: string | null }; + +export type JobListOutput = { jobs: JobListItem[] }; + +/** + * Output from a completed job + */ +export type JobOutput = +/** + * Job completed successfully with no specific output + */ +{ type: "Success" } | +/** + * File copy job output + */ +{ type: "FileCopy"; data: { copied_count: number; total_bytes: number } } | +/** + * Indexer job output + */ +{ type: "Indexed"; data: { stats: IndexerStats; metrics: IndexerMetrics } } | +/** + * Thumbnail generation output + */ +{ type: "ThumbnailsGenerated"; data: { generated_count: number; failed_count: number } } | +/** + * Thumbnail generation output (detailed) + */ +{ type: "ThumbnailGeneration"; data: { generated_count: number; skipped_count: number; error_count: number; total_size_bytes: number } } | +/** + * File move/rename operation output + */ +{ type: "FileMove"; data: { moved_count: number; failed_count: number; total_bytes: number } } | +/** + * File delete operation output + */ +{ type: "FileDelete"; data: { deleted_count: number; failed_count: number; total_bytes: number } } | +/** + * Duplicate detection output + */ +{ type: "DuplicateDetection"; data: { duplicate_groups: number; total_duplicates: number; potential_savings: number } } | +/** + * File validation output + */ +{ type: "FileValidation"; data: { validated_count: number; issues_found: number; total_bytes_validated: number } } | +/** + * OCR text extraction output + */ +{ type: "OcrExtraction"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Speech-to-text transcription output + */ +{ type: "SpeechToText"; data: { total_processed: number; success_count: number; error_count: number } } | +/** + * Gaussian splat generation output + */ +{ type: "GaussianSplat"; data: { total_processed: number; success_count: number; error_count: number } }; + +export type JobPauseInput = { job_id: string }; + +export type JobPauseOutput = { job_id: string; success: boolean }; + +/** + * Job execution policies for a location + * + * Controls which automated jobs run on this location and their configuration. + * This allows per-location customization of thumbnail generation, OCR, speech-to-text, etc. + */ +export type JobPolicies = { +/** + * Thumbnail generation policy + */ +thumbnail?: ThumbnailPolicy; +/** + * Thumbstrip generation policy + */ +thumbstrip?: ThumbstripPolicy; +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +proxy?: ProxyPolicy; +/** + * OCR (text extraction) policy + */ +ocr?: OcrPolicy; +/** + * Speech-to-text transcription policy + */ +speech_to_text?: SpeechPolicy; +/** + * Object detection policy (future) + */ +object_detection?: ObjectDetectionPolicy }; + +export type JobReceipt = { id: JobId; job_name: string }; + +export type JobResumeInput = { job_id: string }; + +export type JobResumeOutput = { job_id: string; success: boolean }; + +/** + * Statistics from the indexer job + */ +export type JobStats = { +/** + * Number of files indexed + */ +files: number; +/** + * Number of directories indexed + */ +dirs: number; +/** + * Number of symlinks indexed + */ +symlinks: number; +/** + * Total bytes indexed + */ +bytes: number }; + +/** + * Current status of a job + */ +export type JobStatus = +/** + * Job is waiting to be executed + */ +"queued" | +/** + * Job is currently running + */ +"running" | +/** + * Job has been paused + */ +"paused" | +/** + * Job completed successfully + */ +"completed" | +/** + * Job failed with an error + */ +"failed" | +/** + * Job was cancelled + */ +"cancelled"; + +/** + * Type of job to trigger for a location + */ +export type JobType = "thumbnail" | "thumbstrip" | "ocr" | "speech_to_text" | "object_detection"; + +export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }; + +/** + * Latency metrics snapshot + */ +export type LatencySnapshot = { count: number; avg_ms: number; min_ms: number; max_ms: number }; + +/** + * A Spacedrive library - the canonical domain model + * + * This is the resource type sent to the frontend for the normalized cache. + * It contains all the information needed to display library info in the UI. + */ +export type Library = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Optional description + */ +description: string | null; +/** + * Path to the library directory + */ +path: string; +/** + * When the library was created + */ +created_at: string; +/** + * When the library was last modified + */ +updated_at: string; +/** + * Library-specific settings + */ +settings: LibrarySettings; +/** + * Library statistics + */ +statistics: LibraryStatistics }; + +/** + * Input for creating a new library + */ +export type LibraryCreateInput = { +/** + * Name of the library + */ +name: string; +/** + * Optional path for the library (if not provided, will use default location) + */ +path: string | null }; + +/** + * Output from library create action dispatch + */ +export type LibraryCreateOutput = { library_id: string; name: string; path: string }; + +/** + * Source of library creation for automatic switching behavior + */ +export type LibraryCreationSource = +/** + * User created locally via UI + */ +"Manual" | +/** + * Received via network sync from another device + */ +"Sync" | +/** + * Imported from cloud storage + */ +"CloudImport"; + +/** + * Input for deleting a library + */ +export type LibraryDeleteInput = { +/** + * ID of the library to delete + */ +library_id: string; +/** + * Whether to also delete the library's data directory + */ +delete_data: boolean }; + +/** + * Output from library delete action dispatch + */ +export type LibraryDeleteOutput = { library_id: string; name: string }; + +/** + * Input for exporting a library + */ +export type LibraryExportInput = { library_id: string; export_path: string; include_thumbnails: boolean; include_previews: boolean }; + +export type LibraryExportOutput = { library_id: string; library_name: string; export_path: string; exported_files: string[] }; + +/** + * Information about a library for listing purposes + */ +export type LibraryInfo = { +/** + * Library unique identifier + */ +id: string; +/** + * Human-readable library name + */ +name: string; +/** + * Path to the library directory + */ +path: string; +/** + * Optional statistics if requested + */ +stats: LibraryStatistics | null }; + +/** + * Input for library info query + */ +export type LibraryInfoQueryInput = null; + +export type LibraryOpenInput = { +/** + * Path to the library directory to open + */ +path: string }; + +export type LibraryOpenOutput = { +/** + * ID of the opened library + */ +library_id: string; +/** + * Name of the opened library + */ +name: string; +/** + * Path where the library is located + */ +path: string }; + +export type LibraryRenameInput = { library_id: string; new_name: string }; + +export type LibraryRenameOutput = { library_id: string; old_name: string; new_name: string }; + +/** + * Library-specific settings + */ +export type LibrarySettings = { +/** + * Whether to generate thumbnails for media files + */ +generate_thumbnails: boolean; +/** + * Thumbnail quality (0-100) + */ +thumbnail_quality: number; +/** + * Whether to enable AI-powered tagging + */ +enable_ai_tagging: boolean; +/** + * Whether sync is enabled for this library + */ +sync_enabled: boolean; +/** + * Whether the library is encrypted at rest + */ +encryption_enabled: boolean; +/** + * Custom thumbnail sizes to generate + */ +thumbnail_sizes: number[]; +/** + * File extensions to ignore during indexing + */ +ignored_extensions: string[]; +/** + * TODO: ai slop config pls remove this + */ +max_file_size: number | null; +/** + * Whether to automatically track system volumes + */ +auto_track_system_volumes: boolean; +/** + * Whether to automatically track external volumes when connected + */ +auto_track_external_volumes: boolean; +/** + * Indexer settings (rule toggles and related) + */ +indexer?: IndexerSettings }; + +/** + * Library statistics + */ +export type LibraryStatistics = { +/** + * Total number of files indexed + */ +total_files: number; +/** + * Total size of all files in bytes + */ +total_size: number; +/** + * Number of locations in this library + */ +location_count: number; +/** + * Number of tags created + */ +tag_count: number; +/** + * Number of devices in this library (v2 field, defaults to 0 for old configs) + */ +device_count?: number; +/** + * Number of unique content identities in this library (v2 field, defaults to 0 for old configs) + */ +unique_content_count?: number; +/** + * Total storage capacity across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +total_capacity?: number; +/** + * Available storage across all volumes in bytes (v2 field, defaults to 0 for old configs) + */ +available_capacity?: number; +/** + * Number of thumbnails generated + */ +thumbnail_count: number; +/** + * Database file size in bytes + */ +database_size: number; +/** + * Last time the library was fully indexed + */ +last_indexed: string | null; +/** + * When these statistics were last updated + */ +updated_at: string }; + +/** + * Action to take when setting up library sync + */ +export type LibrarySyncAction = +/** + * Share local library to remote device (creates same library with same UUID on remote) + * This is the primary way to create a shared library + */ +{ type: "shareLocalLibrary"; libraryName: string } | +/** + * Join an existing remote library (creates same library with same UUID locally) + * Use this when the other device has already shared their library + */ +{ type: "joinRemoteLibrary"; remoteLibraryId: string; remoteLibraryName: string } | +/** + * Future: Merge two different libraries into one (combines data from both) + * Not yet implemented - requires full sync system + */ +{ type: "mergeLibraries"; localLibraryId: string; remoteLibraryId: string; mergedName: string }; + +/** + * Input for setting up library sync between paired devices + */ +export type LibrarySyncSetupInput = { +/** + * Local device ID (should be current device) + */ +localDeviceId: string; +/** + * Remote paired device ID + */ +remoteDeviceId: string; +/** + * Local library to set up sync for + */ +localLibraryId: string; +/** + * Remote library to sync with (optional for RegisterOnly) + */ +remoteLibraryId: string | null; +/** + * Sync action to perform + */ +action: LibrarySyncAction; +/** + * DEPRICATED: Which device should be the sync leader (for future sync implementation) + */ +leaderDeviceId: string }; + +/** + * Result of library sync setup operation + */ +export type LibrarySyncSetupOutput = { +/** + * Whether setup was successful + */ +success: boolean; +/** + * Local library ID that was configured + */ +localLibraryId: string; +/** + * Remote library ID that was linked (if applicable) + */ +remoteLibraryId: string | null; +/** + * Whether devices were successfully registered in each other's libraries + */ +devicesRegistered: boolean; +/** + * Message describing the result + */ +message: string }; + +export type ListEventsInput = Record; + +export type ListEventsOutput = { +/** + * All available event types + */ +all_events: string[]; +/** + * Events that are high-frequency and should be excluded by default + */ +noisy_events: string[]; +/** + * Detailed information about each event + */ +event_info: EventInfo[] }; + +export type ListLibrariesInput = { +/** + * Whether to include detailed statistics for each library + */ +include_stats: boolean }; + +/** + * Input for listing devices from library database + */ +export type ListLibraryDevicesInput = { +/** + * Whether to include offline devices (default: true) + */ +include_offline: boolean; +/** + * Whether to include detailed capabilities and sync leadership info (default: false) + */ +include_details: boolean; +/** + * Whether to also include paired network devices (default: false) + */ +show_paired?: boolean }; + +export type ListPairedDevicesInput = { +/** + * Whether to include only connected devices + */ +connectedOnly?: boolean }; + +/** + * Output from listing paired devices + */ +export type ListPairedDevicesOutput = { +/** + * List of paired devices + */ +devices: PairedDeviceInfo[]; +/** + * Total number of paired devices + */ +total: number; +/** + * Number of currently connected devices + */ +connected: number }; + +export type ListWhisperModelsInput = Record; + +export type ListWhisperModelsOutput = { models: ModelInfo[]; total_downloaded_size: number }; + +/** + * An indexed directory that Spacedrive monitors + */ +export type Location = { +/** + * Unique identifier + */ +id: string; +/** + * Library this location belongs to + */ +library_id: string; +/** + * Root path of this location (includes device!) + */ +sd_path: SdPath; +/** + * Human-friendly name + */ +name: string; +/** + * Indexing configuration + */ +index_mode: IndexMode; +/** + * How often to rescan (None = manual only) + */ +scan_interval: { secs: number; nanos: number } | null; +/** + * Statistics + */ +total_size: number; file_count: number; directory_count: number; +/** + * Current state + */ +scan_state: ScanState; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_scan_at: string | null; +/** + * Whether this location is currently available + */ +is_available: boolean; +/** + * Hidden glob patterns (e.g., [".*", "node_modules"]) + */ +ignore_patterns: string[]; +/** + * Job execution policies for this location + */ +job_policies?: JobPolicies }; + +export type LocationAddInput = { path: SdPath; name: string | null; mode: IndexMode; job_policies: JsonValue | null }; + +/** + * Output from location add action dispatch + */ +export type LocationAddOutput = { location_id: string; path: SdPath; name: string | null; job_id: string | null }; + +/** + * Input for exporting a location + */ +export type LocationExportInput = { +/** + * The UUID of the location to export + */ +location_uuid: string; +/** + * Path where the SQL dump file will be written + */ +export_path: string; +/** + * Include content identities (file hashes, dedup info) + */ +include_content_identities?: boolean; +/** + * Include media metadata (EXIF, video/audio info) + */ +include_media_data?: boolean; +/** + * Include user metadata (notes, favorites) + */ +include_user_metadata?: boolean; +/** + * Include tags and tag relationships + */ +include_tags?: boolean }; + +/** + * Output from location export action + */ +export type LocationExportOutput = { location_uuid: string; location_name: string | null; export_path: string; file_size_bytes: number; stats: ExportStats }; + +/** + * Input for importing a location from SQL dump + */ +export type LocationImportInput = { +/** + * Path to the SQL dump file to import + */ +import_path: string; +/** + * Optional new name for the imported location (overrides name in dump) + */ +new_name: string | null; +/** + * Whether to skip entries that already exist (by UUID) + */ +skip_existing?: boolean }; + +/** + * Output from location import action + */ +export type LocationImportOutput = { location_uuid: string; location_name: string | null; import_path: string; stats: ImportStats }; + +export type LocationRemoveInput = { location_id: string }; + +/** + * Output from location remove action dispatch + */ +export type LocationRemoveOutput = { location_id: string; path: string | null }; + +export type LocationRescanInput = { location_id: string; full_rescan: boolean }; + +export type LocationRescanOutput = { location_id: string; location_path: string; job_id: string; full_rescan: boolean }; + +export type LocationTriggerJobInput = { +/** + * UUID of the location to run the job on + */ +location_id: string; +/** + * Type of job to trigger + */ +job_type: JobType; +/** + * Force the job to run even if disabled in the location's policy + */ +force?: boolean }; + +export type LocationTriggerJobOutput = { +/** + * UUID of the dispatched job + */ +job_id: string; +/** + * Type of job that was triggered + */ +job_type: JobType; +/** + * UUID of the location the job is running on + */ +location_id: string }; + +export type LocationUpdateInput = { +/** + * UUID of the location to update + */ +id: string; +/** + * Optional new name for the location + */ +name: string | null; +/** + * Optional job policies to update + */ +job_policies: JobPolicies | null }; + +export type LocationUpdateOutput = { +/** + * UUID of the updated location + */ +id: string }; + +/** + * Output for location list queries + */ +export type LocationsListOutput = { locations: Location[] }; + +export type LocationsListQueryInput = null; + +/** + * Input for media listing + */ +export type MediaListingInput = { +/** + * The directory path to list media for + */ +path: SdPath; +/** + * Whether to include media from descendant directories (default: false) + */ +include_descendants: boolean | null; +/** + * Which media types to include (default: both Image and Video) + */ +media_types: ContentKind[] | null; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Sort order for results + */ +sort_by: MediaSortBy }; + +/** + * Output containing media files + */ +export type MediaListingOutput = { +/** + * Media files (images/videos) + */ +files: File[]; +/** + * Total count of media files found + */ +total_count: number; +/** + * Whether there are more results than returned + */ +has_more: boolean }; + +/** + * Sort options for media listing + */ +export type MediaSortBy = +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by creation date (newest first) + */ +"created" | +/** + * Sort by date taken/captured (newest first) + */ +"datetaken" | +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by size (largest first) + */ +"size"; + +/** + * Information about a model + */ +export type ModelInfo = { +/** + * Unique model identifier + */ +id: string; +/** + * Human-readable name + */ +name: string; +/** + * Model type + */ +model_type: ModelType; +/** + * File size in bytes + */ +size_bytes: number; +/** + * Where to download from + */ +provider: ModelProvider; +/** + * Filename on disk + */ +filename: string; +/** + * Whether this model is currently downloaded + */ +downloaded: boolean; +/** + * Optional description + */ +description: string | null }; + +/** + * Model provider + */ +export type ModelProvider = +/** + * Hugging Face + */ +{ HuggingFace: { repo: string } } | +/** + * GitHub Release + */ +{ GitHub: { owner: string; repo: string } } | +/** + * Direct URL + */ +{ Direct: { url: string } }; + +/** + * Type of model + */ +export type ModelType = +/** + * Whisper speech-to-text model + */ +"Whisper" | +/** + * Tesseract OCR language data + */ +"Tesseract"; + +/** + * Mount type classification + */ +export type MountType = +/** + * System mount (root, boot, etc.) + */ +"System" | +/** + * External device mount + */ +"External" | +/** + * Network mount + */ +"Network" | +/** + * User mount + */ +"User"; + +export type NetworkStartInput = Record; + +export type NetworkStartOutput = { started: boolean }; + +export type NetworkStatus = { running: boolean; node_id: string | null; addresses: string[]; paired_devices: number; connected_devices: number; version: string; relay_url: string | null }; + +export type NetworkStatusQueryInput = null; + +export type NetworkStopInput = Record; + +export type NetworkStopOutput = { stopped: boolean }; + +/** + * Object detection policy (for future AI features) + */ +export type ObjectDetectionPolicy = { +/** + * Whether to run object detection on this location + */ +enabled: boolean; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Categories to detect (empty = all) + */ +categories: string[]; +/** + * Whether to reprocess files that already have object data + */ +reprocess: boolean }; + +/** + * OCR (text extraction) policy + */ +export type OcrPolicy = { +/** + * Whether to run OCR on this location + */ +enabled: boolean; +/** + * Languages to use for OCR (e.g., ["eng", "spa"]) + */ +languages: string[]; +/** + * Minimum confidence threshold (0.0 - 1.0) + */ +min_confidence: number; +/** + * Whether to reprocess files that already have text + */ +reprocess: boolean }; + +/** + * Operating system types + */ +export type OperatingSystem = "MacOS" | "Windows" | "Linux" | "IOs" | "Android" | "Other"; + +/** + * Operation metrics snapshot + */ +export type OperationSnapshot = { broadcasts_sent: number; state_changes_broadcast: number; shared_changes_broadcast: number; broadcast_batches_sent: number; failed_broadcasts: number; changes_received: number; changes_applied: number; changes_rejected: number; buffer_queue_depth: number; active_backfill_sessions: number; backfill_sessions_completed: number; backfill_pagination_rounds: number; retry_queue_depth: number; retry_attempts: number; retry_successes: number }; + +/** + * Pagination information + */ +export type PaginationInfo = { current_page: number; total_pages: number; has_next: boolean; has_previous: boolean; limit: number; offset: number }; + +/** + * Pagination options + */ +export type PaginationOptions = { limit: number; offset: number }; + +export type PairCancelInput = { session_id: string }; + +export type PairCancelOutput = { cancelled: boolean }; + +export type PairGenerateInput = Record; + +export type PairGenerateOutput = { code: string; session_id: string; expires_at: string; +/** + * QR code JSON format (includes NodeId for remote pairing) + */ +qr_json: string; +/** + * Node ID for relay-based pairing (share this for cross-network pairing) + */ +node_id: string | null }; + +export type PairJoinInput = { code: string; +/** + * Optional node ID for relay-based pairing (enables cross-network connections) + */ +node_id: string | null }; + +export type PairJoinOutput = { paired_device_id: string; device_name: string }; + +export type PairStatusOutput = { sessions: PairingSessionSummary[] }; + +export type PairStatusQueryInput = null; + +/** + * Information about a paired device + */ +export type PairedDeviceInfo = { +/** + * Device ID + */ +id: string; +/** + * Device name + */ +name: string; +/** + * Device type + */ +deviceType: string; +/** + * OS version + */ +osVersion: string; +/** + * App version + */ +appVersion: string; +/** + * Whether the device is currently connected + */ +isConnected: boolean; +/** + * When the device was last seen + */ +lastSeen: string }; + +export type PairingSessionSummary = { id: string; state: SerializablePairingState; remote_device_id: string | null; expires_at: string | null }; + +/** + * Path mapping for resolving virtual paths to actual storage locations + */ +export type PathMapping = { virtual_path: string; actual_path: string }; + +/** + * Per-peer activity information + */ +export type PeerActivity = { deviceId: string; deviceName: string; isOnline: boolean; lastSeen: string; entriesReceived: number; bytesReceived: number; bytesSent: number; watermarkLagMs: number | null }; + +/** + * Performance and timing metrics + */ +export type PerformanceMetrics = { +/** + * Processing rate (items per second) + */ +rate: number; +/** + * Estimated time remaining + */ +estimated_remaining: { secs: number; nanos: number } | null; +/** + * Time elapsed since start + */ +elapsed: { secs: number; nanos: number } | null; +/** + * Number of errors encountered + */ +error_count: number; +/** + * Number of warnings + */ +warning_count: number }; + +/** + * Performance metrics snapshot + */ +export type PerformanceSnapshot = { broadcast_latency: LatencySnapshot; apply_latency: LatencySnapshot; backfill_request_latency: LatencySnapshot; state_watermark: string; shared_watermark: string; watermark_lag_ms: { [key in string]: number }; hlc_physical_drift_ms: number; hlc_counter_max: number; db_query_duration: LatencySnapshot; db_query_count: number }; + +export type PingInput = { message: string; count?: number | null }; + +export type PingOutput = { echo: string; count: number; extension_works: boolean }; + +/** + * Privacy levels for tag visibility control + */ +export type PrivacyLevel = +/** + * Standard visibility in all contexts + */ +"Normal" | +/** + * Hidden from normal searches but accessible via direct query + */ +"Archive" | +/** + * Completely hidden from standard UI + */ +"Hidden"; + +/** + * Progress completion information + */ +export type ProgressCompletion = { +/** + * Items completed (files, entries, operations, etc.) + */ +completed: number; +/** + * Total items to complete + */ +total: number; +/** + * Bytes processed (if applicable) + */ +bytes_completed: number | null; +/** + * Total bytes to process (if applicable) + */ +total_bytes: number | null }; + +/** + * Proxy/sidecar generation policy (video scrubbing) + */ +export type ProxyPolicy = { +/** + * Whether to generate proxy files for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing proxies + */ +regenerate: boolean }; + +export type RegenerateThumbnailInput = { +/** + * UUID of the entry to regenerate thumbnails for + */ +entry_uuid: string; +/** + * Optional variant names (defaults to grid@1x, grid@2x, detail@1x) + */ +variants: string[] | null; +/** + * Force regeneration even if thumbnails exist + */ +force: boolean }; + +export type RegenerateThumbnailOutput = { +/** + * Number of thumbnails generated + */ +generated_count: number; +/** + * Variant names that were generated + */ +variants: string[] }; + +/** + * State of a job running on a remote device + */ +export type RemoteJobState = { job_id: string; job_type: string; library_id: string; device_id: string; device_name: string; status: JobStatus; progress: number | null; message: string | null; generic_progress: GenericProgress | null; started_at: string | null; completed_at: string | null; error: string | null }; + +/** + * Query for all remote jobs across all devices + */ +export type RemoteJobsAllDevicesInput = Record; + +export type RemoteJobsAllDevicesOutput = { jobs_by_device: { [key in string]: RemoteJobState[] } }; + +/** + * Query for remote jobs on a specific device + */ +export type RemoteJobsForDeviceInput = { device_id: string }; + +export type RemoteJobsForDeviceOutput = { jobs: RemoteJobState[] }; + +/** + * Information about a library discovered on a remote device + */ +export type RemoteLibraryInfo = { +/** + * Library ID + */ +id: string; +/** + * Library name + */ +name: string; +/** + * Library description (if any) + */ +description: string | null; +/** + * When the library was created + */ +createdAt: string; +/** + * Statistics about the library + */ +statistics: LibraryStatistics }; + +export type ReorderGroupsInput = { space_id: string; group_ids: string[] }; + +export type ReorderItemsInput = { group_id: string | null; item_ids: string[] }; + +export type ReorderOutput = { success: boolean }; + +export type ResetDataInput = { +/** + * Confirmation flag to prevent accidental data loss + */ +confirm: boolean }; + +export type ResetDataOutput = { +/** + * Whether the reset was successful + */ +success: boolean; +/** + * Message describing the result + */ +message: string }; + +/** + * Metadata for resource cache updates + */ +export type ResourceMetadata = { +/** + * Fields that should be replaced, not merged + */ +no_merge_fields: string[]; +/** + * Alternate IDs for matching (besides primary ID) + */ +alternate_ids: string[]; +/** + * Paths affected by this resource event (for path-scoped filtering) + */ +affected_paths?: SdPath[] }; + +/** + * Risk level for adding a path as a location + */ +export type RiskLevel = +/** + * Safe - nested path in user directories + */ +"low" | +/** + * Caution - shallow path on primary volume (e.g., /Users/jamie) + */ +"medium" | +/** + * Warning - system directory or root-level path (e.g., /, /System) + */ +"high"; + +/** + * Current scanning state of a location + */ +export type ScanState = +/** + * Not currently being scanned + */ +"Idle" | +/** + * Currently scanning + */ +{ Scanning: { +/** + * Progress percentage (0-100) + */ +progress: number } } | +/** + * Scan completed successfully + */ +"Completed" | +/** + * Scan failed with error + */ +"Failed" | +/** + * Scan was paused + */ +"Paused"; + +/** + * Detailed breakdown of how the score was calculated + */ +export type ScoreBreakdown = { temporal_score: number; semantic_score: number | null; metadata_score: number; recency_boost: number; user_preference_boost: number; final_score: number }; + +/** + * A path within the Spacedrive Virtual Distributed File System + * + * This is the core abstraction that enables cross-device operations. + * An SdPath can represent: + * - A physical file at a specific path on a specific device + * - A content-addressed file that can be sourced from any device + * - A sidecar (derivative data) attached to content + * + * This enum-based approach enables resilient file operations by allowing + * content-based paths to be resolved to optimal physical locations at runtime. + */ +export type SdPath = +/** + * A direct pointer to a file at a specific path on a specific device + */ +{ Physical: { +/** + * The device slug (e.g., "jamies-macbook") + */ +device_slug: string; +/** + * The local path on that device + */ +path: string } } | +/** + * A cloud storage path within a cloud volume + */ +{ Cloud: { +/** + * The cloud service type (S3, GoogleDrive, etc.) + */ +service: CloudServiceType; +/** + * The cloud identifier (bucket name, drive name, etc.) + */ +identifier: string; +/** + * The cloud-native path (e.g., "bucket/key" for S3) + */ +path: string } } | +/** + * An abstract, location-independent handle that refers to file content + */ +{ Content: { +/** + * The unique content identifier + */ +content_id: string } } | +/** + * A derivative data file (thumbnail, OCR text, embedding, etc.) + * Sidecars are content-scoped and addressed by content + kind + variant + */ +{ Sidecar: { +/** + * The content this sidecar is derived from + */ +content_id: string; +/** + * The type of sidecar (thumb, ocr, embeddings, etc.) + */ +kind: SidecarKind; +/** + * The specific variant (e.g., "grid@2x", "1080p", "all-MiniLM-L6-v2") + */ +variant: SidecarVariant; +/** + * The storage format (webp, json, msgpack, etc.) + */ +format: SidecarFormat } }; + +/** + * A batch of SdPaths, useful for operations on multiple files + */ +export type SdPathBatch = { paths: SdPath[] }; + +/** + * Search facets for filtering UI + */ +export type SearchFacets = { file_types: { [key in string]: number }; tags: { [key in string]: number }; locations: { [key in string]: number }; date_ranges: { [key in string]: number }; size_ranges: { [key in string]: number } }; + +/** + * Container for all structured filters + */ +export type SearchFilters = { file_types: string[] | null; tags: TagFilter | null; date_range: DateRangeFilter | null; size_range: SizeRangeFilter | null; locations: string[] | null; content_types: ContentKind[] | null; include_hidden: boolean | null; include_archived: boolean | null }; + +/** + * Defines the search mode and performance characteristics + */ +export type SearchMode = +/** + * Fast, metadata-only search (<10ms) + */ +"Fast" | +/** + * Normal search with semantic ranking (<100ms) + */ +"Normal" | +/** + * Full search with content analysis (<500ms) + */ +"Full"; + +/** + * Defines the scope of the filesystem to search within + */ +export type SearchScope = +/** + * Search the entire library (default) + */ +"Library" | +/** + * Restrict search to a specific location by its ID + */ +{ Location: { location_id: string } } | +/** + * Restrict search to a specific directory path and all its descendants + */ +{ Path: { path: SdPath } }; + +export type SearchTagsInput = { +/** + * Search query (searches across all name variants) + */ +query: string; +/** + * Optional namespace filter + */ +namespace: string | null; +/** + * Optional tag type filter + */ +tag_type: TagType | null; +/** + * Whether to include archived/hidden tags + */ +include_archived: boolean | null; +/** + * Maximum number of results to return + */ +limit: number | null; +/** + * Whether to resolve ambiguous results using context + */ +resolve_ambiguous: boolean | null; +/** + * Context tags for disambiguation (UUIDs) + */ +context_tag_ids: string[] | null }; + +export type SearchTagsOutput = { +/** + * Tags found by the search + */ +tags: TagSearchResult[]; +/** + * Total number of results found (may be more than returned if limited) + */ +total_found: number; +/** + * Whether results were disambiguated using context + */ +disambiguated: boolean; +/** + * Search query that was executed + */ +query: string; +/** + * Applied filters + */ +filters: TagSearchFilters }; + +export type SerializablePairingState = "Idle" | "GeneratingCode" | "Broadcasting" | "Scanning" | "WaitingForConnection" | "Connecting" | "Authenticating" | "ExchangingKeys" | "AwaitingConfirmation" | "EstablishingSession" | "ChallengeReceived" | "ResponsePending" | "ResponseSent" | "Completed" | { Failed: { reason: string } }; + +export type ServiceState = { running: boolean; details: string | null }; + +export type ServiceStatus = { location_watcher: ServiceState; networking: ServiceState; volume_monitor: ServiceState; file_sharing: ServiceState }; + +/** + * Domain representation of a sidecar + */ +export type Sidecar = { id: number; content_uuid: string; kind: string; variant: string; format: string; status: string; size: number; created_at: string; updated_at: string }; + +/** + * Format for storing sidecar files + * + * Format selection guidelines: + * - Webp: Thumbnails and image derivatives (compressed images) + * - Mp4: Video/audio proxies (standard media format) + * - Json: Text-based structured data (OCR, transcripts) + * - MessagePack: Binary structured data (embeddings, vectors) + * - Text: Plain text extractions + * - Ply: 3D model format for Gaussian splats + * + * MessagePack is preferred for embeddings because: + * - 6x smaller than JSON (1.7KB vs 10KB per 384-dim vector) + * - 10x faster to parse + * - Already used in Spacedrive (job serialization) + * - Enables sub-30ms semantic search on 1M+ files + */ +export type SidecarFormat = "webp" | "mp_4" | "json" | "message_pack" | "text" | "ply"; + +export type SidecarKind = "thumb" | "thumbstrip" | "proxy" | "embeddings" | "ocr" | "transcript" | "gaussian_splat"; + +export type SidecarVariant = string; + +/** + * Filter for file size in bytes + */ +export type SizeRangeFilter = { min: number | null; max: number | null }; + +/** + * Sort direction + */ +export type SortDirection = "Asc" | "Desc"; + +/** + * Fields that can be used for sorting + */ +export type SortField = "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedAt"; + +/** + * Sorting options for search results + */ +export type SortOptions = { field: SortField; direction: SortDirection }; + +/** + * A Space defines a sidebar layout and filtering context + */ +export type Space = { +/** + * Unique identifier + */ +id: string; +/** + * Human-friendly name (e.g., "All Devices", "Work Files") + */ +name: string; +/** + * Icon identifier (Phosphor icon name or emoji) + */ +icon: string; +/** + * Color for visual identification (hex format: #RRGGBB) + */ +color: string; +/** + * Sort order in space switcher + */ +order: number; +/** + * Timestamps + */ +created_at: string; updated_at: string }; + +export type SpaceCreateInput = { name: string; icon: string; color: string }; + +export type SpaceCreateOutput = { space: Space }; + +export type SpaceDeleteInput = { space_id: string }; + +export type SpaceDeleteOutput = { success: boolean }; + +export type SpaceGetOutput = { space: Space }; + +export type SpaceGetQueryInput = { space_id: string }; + +/** + * A SpaceGroup is a collapsible section in the sidebar + */ +export type SpaceGroup = { +/** + * Unique identifier + */ +id: string; +/** + * Space this group belongs to + */ +space_id: string; +/** + * Group name (e.g., "Quick Access", "MacBook Pro") + */ +name: string; +/** + * Type of group (determines content and behavior) + */ +group_type: GroupType; +/** + * Whether group is collapsed + */ +is_collapsed: boolean; +/** + * Sort order within space + */ +order: number; +/** + * Timestamp + */ +created_at: string }; + +/** + * A group with its items + */ +export type SpaceGroupWithItems = { +/** + * The group + */ +group: SpaceGroup; +/** + * Items in this group (sorted by order) + */ +items: SpaceItem[] }; + +/** + * An item within a space (can be space-level or within a group) + */ +export type SpaceItem = { +/** + * Unique identifier + */ +id: string; +/** + * Space this item belongs to + */ +space_id: string; +/** + * Group this item belongs to (None = space-level item) + */ +group_id: string | null; +/** + * Type discriminant (for quick type checking) + */ +item_type: ItemType; +/** + * Sort order within space or group + */ +order: number; +/** + * Timestamp + */ +created_at: string; +/** + * Resolved file data for Path items (populated by get_layout query) + */ +resolved_file?: File | null }; + +/** + * Complete sidebar layout for a space + */ +export type SpaceLayout = { +/** + * Unique identifier (same as space.id for cache matching) + */ +id: string; +/** + * The space + */ +space: Space; +/** + * Space-level items (pinned shortcuts, no group) + */ +space_items: SpaceItem[]; +/** + * Groups with their items + */ +groups: SpaceGroupWithItems[] }; + +export type SpaceLayoutQueryInput = { space_id: string }; + +export type SpaceUpdateInput = { space_id: string; name: string | null; icon: string | null; color: string | null }; + +export type SpaceUpdateOutput = { space: Space }; + +export type SpacedropSendInput = { device_id: string; paths: SdPath[]; sender: string | null }; + +export type SpacedropSendOutput = { job_id: string | null; session_id: string | null }; + +export type SpacesListOutput = { spaces: Space[] }; + +export type SpacesListQueryInput = null; + +/** + * Speech-to-text transcription policy + */ +export type SpeechPolicy = { +/** + * Whether to run speech-to-text on this location + */ +enabled: boolean; +/** + * Language for transcription + */ +language: string | null; +/** + * Model to use (e.g., "base", "small", "medium", "large") + */ +model: string; +/** + * Whether to reprocess files that already have transcriptions + */ +reprocess: boolean }; + +/** + * State transition event + */ +export type StateTransition = { from: DeviceSyncState; to: DeviceSyncState; timestamp: string; reason: string | null }; + +export type SuggestedLocation = { name: string; path: string; sd_path: SdPath }; + +export type SuggestedLocationsOutput = { locations: SuggestedLocation[] }; + +export type SuggestedLocationsQueryInput = null; + +/** + * Sync activity types for detailed sync monitoring + */ +export type SyncActivityType = { type: "BroadcastSent"; data: { changes: number } } | { type: "ChangesReceived"; data: { changes: number } } | { type: "ChangesApplied"; data: { changes: number } } | { type: "BackfillStarted" } | { type: "BackfillCompleted"; data: { records: number } } | { type: "CatchUpStarted" } | { type: "CatchUpCompleted" }; + +/** + * A logged sync event + */ +export type SyncEventLog = { id: number | null; timestamp: string; device_id: string; event_type: SyncEventType; category: EventCategory; severity: EventSeverity; summary: string; details?: JsonValue | null; correlation_id?: string | null; peer_device_id?: string | null; model_types?: string[] | null; record_count?: number | null; duration_ms?: number | null }; + +/** + * High-level sync event types + */ +export type SyncEventType = +/** + * State machine transition (Uninitialized → Backfilling → CatchingUp → Ready ⇄ Paused) + */ +"state_transition" | +/** + * Backfill session started + */ +"backfill_session_started" | +/** + * Backfill session completed successfully + */ +"backfill_session_completed" | +/** + * Backfill session failed + */ +"backfill_session_failed" | +/** + * Catch-up session started (incremental sync) + */ +"catch_up_session_started" | +/** + * Catch-up session completed + */ +"catch_up_session_completed" | +/** + * Batch of records ingested (aggregated, not per-record) + */ +"batch_ingestion" | +/** + * Sent backfill request to peer + */ +"backfill_request_sent" | +/** + * Received backfill request from peer + */ +"backfill_request_received" | +/** + * Sent backfill response to peer + */ +"backfill_response_sent" | +/** + * Peer device connected + */ +"peer_connected" | +/** + * Peer device disconnected + */ +"peer_disconnected" | +/** + * Sync error occurred + */ +"sync_error"; + +/** + * Point-in-time snapshot of all sync metrics + */ +export type SyncMetricsSnapshot = { +/** + * When this snapshot was taken + */ +timestamp: string; +/** + * State metrics + */ +state: SyncStateSnapshot; +/** + * Operation metrics + */ +operations: OperationSnapshot; +/** + * Data volume metrics + */ +data_volume: DataVolumeSnapshot; +/** + * Performance metrics + */ +performance: PerformanceSnapshot; +/** + * Error metrics + */ +errors: ErrorSnapshot }; + +/** + * State metrics snapshot + */ +export type SyncStateSnapshot = { current_state: DeviceSyncState; state_entered_at: string; uptime_seconds: number; state_history: StateTransition[]; total_time_in_state: ([DeviceSyncState, number])[]; transition_count: ([[DeviceSyncState, DeviceSyncState], number])[] }; + +export type SystemInfo = { uptime: number | null; data_directory: string; instance_name: string | null; current_library: string | null }; + +/** + * A tag with advanced capabilities for contextual organization + */ +export type Tag = { +/** + * Unique identifier + */ +id: string; +/** + * Core identity + */ +canonical_name: string; display_name: string | null; +/** + * Semantic variants for flexible access + */ +formal_name: string | null; abbreviation: string | null; aliases: string[]; +/** + * Context and categorization + */ +namespace: string | null; tag_type: TagType; +/** + * Visual and behavioral properties + */ +color: string | null; icon: string | null; description: string | null; +/** + * Advanced capabilities + */ +is_organizational_anchor: boolean; privacy_level: PrivacyLevel; search_weight: number; +/** + * Compositional attributes + */ +attributes: { [key in string]: JsonValue }; composition_rules: CompositionRule[]; +/** + * Metadata + */ +created_at: string; updated_at: string; created_by_device: string }; + +/** + * Filter for tags, supporting complex boolean logic + */ +export type TagFilter = { +/** + * Must have all of these tag IDs + */ +include: string[]; +/** + * Must not have any of these tag IDs + */ +exclude: string[] }; + +export type TagSearchFilters = { namespace: string | null; tag_type: string | null; include_archived: boolean; limit: number | null }; + +export type TagSearchResult = { +/** + * The semantic tag + */ +tag: Tag; +/** + * Relevance score (0.0-1.0) + */ +relevance: number; +/** + * Which name variant matched the search + */ +matched_variant: string | null; +/** + * Context score if disambiguation was used + */ +context_score: number | null }; + +/** + * Source of tag application + */ +export type TagSource = +/** + * Manually applied by user + */ +"User" | +/** + * Applied by AI analysis + */ +"AI" | +/** + * Imported from external source + */ +"Import" | +/** + * Synchronized from another device + */ +"Sync"; + +/** + * Specifies what to tag: content (all instances) or specific entries + */ +export type TagTargets = +/** + * Tag by content identity (applies to ALL instances of this content across devices) + * This is the preferred/default approach + */ +{ type: "Content"; ids: string[] } | +/** + * Tag by entry ID (applies to ONLY this specific file instance) + * Use when you want instance-specific tags + */ +{ type: "Entry"; ids: number[] }; + +/** + * Types of semantic tags with different behaviors + */ +export type TagType = +/** + * Standard user-created tag + */ +"Standard" | +/** + * Creates visual hierarchies in the interface + */ +"Organizational" | +/** + * Controls search and display visibility + */ +"Privacy" | +/** + * System-generated tag (AI, import, etc.) + */ +"System"; + +/** + * Text highlighting information + */ +export type TextHighlight = { field: string; text: string; start: number; end: number }; + +export type ThumbnailInput = { paths: string[]; size: number; quality: number }; + +/** + * Thumbnail generation policy + */ +export type ThumbnailPolicy = { +/** + * Whether to generate thumbnails for this location + */ +enabled: boolean; +/** + * Specific thumbnail sizes to generate (empty = use defaults) + */ +sizes: number[]; +/** + * JPEG quality (0-100) + */ +quality: number; +/** + * Whether to regenerate existing thumbnails + */ +regenerate: boolean }; + +/** + * Thumbstrip generation policy + */ +export type ThumbstripPolicy = { +/** + * Whether to generate thumbstrips for this location + */ +enabled: boolean; +/** + * Whether to regenerate existing thumbstrips + */ +regenerate: boolean }; + +export type TranscribeAudioInput = { entry_uuid: string; model: string | null; language: string | null }; + +export type TranscribeAudioOutput = { +/** + * Job ID for tracking transcription progress + */ +job_id: string }; + +/** + * Statistics for the unified ephemeral index + */ +export type UnifiedIndexStats = { +/** + * Total entries in the shared arena + */ +total_entries: number; +/** + * Number of entries indexed by path + */ +path_index_count: number; +/** + * Number of unique interned names (shared across all paths) + */ +unique_names: number; +/** + * Number of interned strings in shared cache + */ +interned_strings: number; +/** + * Number of content kinds stored + */ +content_kinds: number; +/** + * Estimated memory usage in bytes + */ +memory_bytes: number; +/** + * Age of the cache in seconds + */ +age_seconds: number; +/** + * Seconds since last access + */ +idle_seconds: number }; + +/** + * Input for finding files unique to a location + */ +export type UniqueToLocationInput = { +/** + * The location ID to find unique files for + */ +location_id: string; +/** + * Optional limit on number of results + */ +limit: number | null }; + +/** + * Output containing files that are unique to the specified location + */ +export type UniqueToLocationOutput = { +/** + * Files that exist only in the specified location + */ +unique_files: File[]; +/** + * Total count of unique files + */ +total_count: number; +/** + * Total size of unique files in bytes + */ +total_size: number }; + +export type UpdateGroupInput = { group_id: string; name: string | null; is_collapsed: boolean | null }; + +export type UpdateGroupOutput = { group: SpaceGroup }; + +/** + * Input for location path validation + */ +export type ValidateLocationPathInput = { path: SdPath }; + +/** + * Output from location path validation + */ +export type ValidateLocationPathOutput = { +/** + * Whether this path is recommended for use as a location + */ +is_recommended: boolean; +/** + * Risk level assessment + */ +risk_level: RiskLevel; +/** + * List of warnings (empty if no issues) + */ +warnings: ValidationWarning[]; +/** + * Alternative suggestion to use volume indexing + */ +suggested_alternative: VolumeIndexingSuggestion | null; +/** + * Path depth from root (number of components) + */ +path_depth: number; +/** + * Whether path is on the primary system volume + */ +is_on_primary_volume: boolean }; + +/** + * A validation warning message + */ +export type ValidationWarning = { message: string; suggestion: string | null }; + +/** + * Video metadata extracted from FFmpeg + */ +export type VideoMediaData = { uuid: string; width: number; height: number; blurhash: string | null; duration_seconds: number | null; bit_rate: number | null; codec: string | null; pixel_format: string | null; color_space: string | null; color_range: string | null; color_primaries: string | null; color_transfer: string | null; fps_num: number | null; fps_den: number | null; audio_codec: string | null; audio_channels: string | null; audio_sample_rate: number | null; audio_bit_rate: number | null; title: string | null; artist: string | null; album: string | null; creation_time: string | null; date_captured: string | null }; + +/** + * A volume in Spacedrive - unified model for runtime and database + */ +export type Volume = { +/** + * Unique identifier (used in SdPath addressing) + */ +id: string; +/** + * Volume fingerprint for identification + */ +fingerprint: VolumeFingerprint; +/** + * Device this volume is attached to + */ +device_id: string; +/** + * Human-readable name + */ +name: string; +/** + * Library this volume belongs to (None for untracked volumes) + */ +library_id: string | null; +/** + * Whether this volume is being tracked by Spacedrive + */ +is_tracked: boolean; +/** + * Primary mount point + */ +mount_point: string; +/** + * Additional mount points for the same volume + */ +mount_points: string[]; +/** + * Volume type/category + */ +volume_type: VolumeType; +/** + * Mount type classification + */ +mount_type: MountType; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: DiskType; +/** + * Filesystem type + */ +file_system: FileSystem; +/** + * Total capacity in bytes + */ +total_capacity: number; +/** + * Currently available space in bytes + */ +available_space: number; +/** + * Whether volume is read-only + */ +is_read_only: boolean; +/** + * Whether volume is currently mounted/available + */ +is_mounted: boolean; +/** + * Hardware identifier (device path, UUID, etc.) + */ +hardware_id: string | null; +/** + * Cloud identifier (bucket/drive/container name) for cloud volumes + * This is separate from mount_point to allow display names with suffixes + * while maintaining the correct cloud resource identifier for backend operations + */ +cloud_identifier: string | null; +/** + * Cloud service configuration (service-specific settings like region, endpoint) + */ +cloud_config: JsonValue | null; +/** + * APFS container information (macOS only) + */ +apfs_container: ApfsContainer | null; +/** + * Container-relative volume ID for same-container detection + */ +container_volume_id: string | null; +/** + * Path resolution mappings (for firmlinks/symlinks) + */ +path_mappings: PathMapping[]; +/** + * Whether this volume should be visible in default views + */ +is_user_visible: boolean; +/** + * Whether this volume should be auto-tracked + */ +auto_track_eligible: boolean; +/** + * Performance metrics + */ +read_speed_mbps: number | null; write_speed_mbps: number | null; +/** + * Timestamps + */ +created_at: string; updated_at: string; last_seen_at: string; +/** + * Statistics + */ +total_files: number | null; total_directories: number | null; last_stats_update: string | null; +/** + * User preferences + */ +display_name: string | null; is_favorite: boolean; color: string | null; icon: string | null; +/** + * Error state + */ +error_message: string | null }; + +export type VolumeAddCloudInput = { service: CloudServiceType; display_name: string; config: CloudStorageConfig }; + +export type VolumeAddCloudOutput = { fingerprint: VolumeFingerprint; volume_name: string; service: CloudServiceType }; + +export type VolumeFilter = +/** + * Only return tracked volumes + */ +"TrackedOnly" | +/** + * Only return untracked volumes + */ +"UntrackedOnly" | +/** + * Return all volumes (tracked and untracked) + */ +"All"; + +/** + * Unique fingerprint for a storage volume + */ +export type VolumeFingerprint = string; + +/** + * Suggestion to use volume indexing instead + */ +export type VolumeIndexingSuggestion = { volume_fingerprint: string; volume_name: string; message: string }; + +/** + * Summary information about a volume (for updates and caching) + */ +export type VolumeInfo = { is_mounted: boolean; total_bytes_available: number; read_speed_mbps: number | null; write_speed_mbps: number | null; error_status: string | null }; + +export type VolumeItem = { id: string; name: string; fingerprint: VolumeFingerprint; volume_type: string; mount_point: string | null; +/** + * Whether this volume is currently tracked in the library + */ +is_tracked: boolean; +/** + * Whether this volume is currently online/mounted + */ +is_online: boolean; +/** + * Total capacity in bytes + */ +total_capacity: number | null; +/** + * Available capacity in bytes + */ +available_capacity: number | null; +/** + * Unique bytes (deduplicated by content_identity) + */ +unique_bytes: number | null; +/** + * Filesystem type (APFS, NTFS, ext4, etc.) + */ +file_system: string | null; +/** + * Disk type (SSD, HDD, etc.) + */ +disk_type: string | null; +/** + * Read speed in MB/s + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s + */ +write_speed_mbps: number | null; +/** + * Device ID that owns this volume + */ +device_id: string; +/** + * Device slug for constructing SdPaths + */ +device_slug: string }; + +export type VolumeListOutput = { volumes: VolumeItem[] }; + +export type VolumeListQueryInput = { +/** + * Filter volumes by tracking status (default: TrackedOnly) + */ +filter?: VolumeFilter }; + +export type VolumeRefreshInput = { +/** + * Optional: Set to true to force recalculation even if recently calculated + */ +force?: boolean }; + +export type VolumeRefreshOutput = { +/** + * Number of volumes that had their unique_bytes calculated + */ +volumes_refreshed: number; +/** + * Number of volumes that failed to refresh + */ +volumes_failed: number }; + +export type VolumeRemoveCloudInput = { fingerprint: VolumeFingerprint }; + +export type VolumeRemoveCloudOutput = { fingerprint: VolumeFingerprint }; + +export type VolumeSpeedTestInput = { fingerprint: VolumeFingerprint }; + +/** + * Output from volume speed test operation + */ +export type VolumeSpeedTestOutput = { +/** + * The fingerprint of the tested volume + */ +fingerprint: VolumeFingerprint; +/** + * Read speed in MB/s (if measured) + */ +read_speed_mbps: number | null; +/** + * Write speed in MB/s (if measured) + */ +write_speed_mbps: number | null }; + +export type VolumeTrackInput = { +/** + * Fingerprint of the volume to track + */ +fingerprint: string; +/** + * Optional custom display name + */ +display_name: string | null }; + +export type VolumeTrackOutput = { +/** + * UUID of the tracked volume + */ +volume_id: string; +/** + * Fingerprint of the volume + */ +fingerprint: string; +/** + * Display name + */ +name: string; +/** + * Whether the volume is currently online + */ +is_online: boolean }; + +/** + * Volume type classification + */ +export type VolumeType = +/** + * Primary system drive containing OS and user data + */ +"Primary" | +/** + * Dedicated user data volumes (separate from OS) + */ +"UserData" | +/** + * External or removable storage devices + */ +"External" | +/** + * Secondary internal storage (additional drives/partitions) + */ +"Secondary" | +/** + * System/OS internal volumes (hidden from normal view) + */ +"System" | +/** + * Network attached storage + */ +"Network" | +/** + * Cloud storage mounts + */ +"Cloud" | +/** + * Virtual/temporary storage + */ +"Virtual" | +/** + * Unknown or unclassified volumes + */ +"Unknown"; + +export type VolumeUntrackInput = { +/** + * UUID of the volume to untrack + */ +volume_id: string }; + +export type VolumeUntrackOutput = { +/** + * UUID of the untracked volume + */ +volume_id: string; +/** + * Whether the operation was successful + */ +success: boolean }; +// ===== API Type Unions ===== + +export type CoreAction = + { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput } + | { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput } + | { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput } + | { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } + | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } + | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } + | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } + | { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } + | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } + | { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput } + | { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput } + | { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput } + | { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput } + | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } + | { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } +; + +export type LibraryAction = + { type: 'indexing.start'; input: IndexInput; output: JobReceipt } + | { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } + | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } + | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } + | { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput } + | { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput } + | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } + | { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput } + | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } + | { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput } + | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } + | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } + | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } + | { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput } + | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } + | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } + | { type: 'files.rename'; input: FileRenameInput; output: JobReceipt } + | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } + | { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput } + | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } + | { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput } + | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } + | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } + | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } + | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } + | { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput } + | { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput } + | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } + | { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput } + | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } + | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput } + | { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput } + | { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput } + | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } + | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } + | { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput } + | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } + | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } + | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } + | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } + | { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput } + | { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput } + | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } +; + +export type CoreQuery = + { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } + | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } + | { type: 'core.status'; input: Empty; output: CoreStatus } + | { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput } + | { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput } + | { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus } + | { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput } + | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } + | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } + | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + | { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } +; + +export type LibraryQuery = + { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } + | { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput } + | { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput } + | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library } + | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } + | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout } + | { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] } + | { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput } + | { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput } + | { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput } + | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } + | { type: 'files.by_path'; input: FileByPathQuery; output: File } + | { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput } + | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'test.ping'; input: PingInput; output: PingOutput } + | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } + | { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput } + | { type: 'files.by_id'; input: FileByIdQuery; output: File } + | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } + | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } + | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } + | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } + | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } + | { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } +; + +// ===== Wire Method Mappings ===== + +export const WIRE_METHODS = { + coreActions: { + 'core.reset': 'action:core.reset.input', + 'libraries.open': 'action:libraries.open.input', + 'network.start': 'action:network.start.input', + 'network.sync_setup': 'action:network.sync_setup.input', + 'network.spacedrop.send': 'action:network.spacedrop.send.input', + 'network.pair.join': 'action:network.pair.join.input', + 'network.pair.generate': 'action:network.pair.generate.input', + 'libraries.delete': 'action:libraries.delete.input', + 'network.stop': 'action:network.stop.input', + 'core.ephemeral_reset': 'action:core.ephemeral_reset.input', + 'network.device.revoke': 'action:network.device.revoke.input', + 'models.whisper.download': 'action:models.whisper.download.input', + 'models.whisper.delete': 'action:models.whisper.delete.input', + 'libraries.create': 'action:libraries.create.input', + 'network.pair.cancel': 'action:network.pair.cancel.input', + }, + + libraryActions: { + 'indexing.start': 'action:indexing.start.input', + 'files.copy': 'action:files.copy.input', + 'indexing.verify': 'action:indexing.verify.input', + 'libraries.export': 'action:libraries.export.input', + 'media.speech.transcribe': 'action:media.speech.transcribe.input', + 'volumes.refresh': 'action:volumes.refresh.input', + 'spaces.delete_group': 'action:spaces.delete_group.input', + 'locations.import': 'action:locations.import.input', + 'media.thumbnail': 'action:media.thumbnail.input', + 'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input', + 'spaces.update': 'action:spaces.update.input', + 'spaces.update_group': 'action:spaces.update_group.input', + 'locations.rescan': 'action:locations.rescan.input', + 'locations.update': 'action:locations.update.input', + 'locations.add': 'action:locations.add.input', + 'volumes.track': 'action:volumes.track.input', + 'files.rename': 'action:files.rename.input', + 'jobs.cancel': 'action:jobs.cancel.input', + 'volumes.index': 'action:volumes.index.input', + 'spaces.add_group': 'action:spaces.add_group.input', + 'media.proxy.generate': 'action:media.proxy.generate.input', + 'spaces.add_item': 'action:spaces.add_item.input', + 'jobs.pause': 'action:jobs.pause.input', + 'volumes.speed_test': 'action:volumes.speed_test.input', + 'spaces.delete': 'action:spaces.delete.input', + 'locations.enable_indexing': 'action:locations.enable_indexing.input', + 'locations.triggerJob': 'action:locations.triggerJob.input', + 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', + 'libraries.rename': 'action:libraries.rename.input', + 'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input', + 'files.delete': 'action:files.delete.input', + 'locations.remove': 'action:locations.remove.input', + 'media.ocr.extract': 'action:media.ocr.extract.input', + 'spaces.reorder_groups': 'action:spaces.reorder_groups.input', + 'spaces.reorder_items': 'action:spaces.reorder_items.input', + 'spaces.create': 'action:spaces.create.input', + 'tags.create': 'action:tags.create.input', + 'media.splat.generate': 'action:media.splat.generate.input', + 'tags.apply': 'action:tags.apply.input', + 'spaces.delete_item': 'action:spaces.delete_item.input', + 'jobs.resume': 'action:jobs.resume.input', + 'volumes.add_cloud': 'action:volumes.add_cloud.input', + 'locations.export': 'action:locations.export.input', + 'files.createFolder': 'action:files.createFolder.input', + 'volumes.untrack': 'action:volumes.untrack.input', + }, + + coreQueries: { + 'network.sync_setup.discover': 'query:network.sync_setup.discover', + 'network.devices.list': 'query:network.devices.list', + 'core.status': 'query:core.status', + 'jobs.remote.all_devices': 'query:jobs.remote.all_devices', + 'jobs.remote.for_device': 'query:jobs.remote.for_device', + 'core.ephemeral_status': 'query:core.ephemeral_status', + 'models.whisper.list': 'query:models.whisper.list', + 'network.pair.status': 'query:network.pair.status', + 'network.status': 'query:network.status', + 'libraries.list': 'query:libraries.list', + 'core.events.list': 'query:core.events.list', + }, + + libraryQueries: { + 'locations.list': 'query:locations.list', + 'jobs.active': 'query:jobs.active', + 'files.content_kind_stats': 'query:files.content_kind_stats', + 'libraries.info': 'query:libraries.info', + 'locations.validate_path': 'query:locations.validate_path', + 'volumes.list': 'query:volumes.list', + 'spaces.get_layout': 'query:spaces.get_layout', + 'devices.list': 'query:devices.list', + 'spaces.list': 'query:spaces.list', + 'sync.eventLog': 'query:sync.eventLog', + 'sync.activity': 'query:sync.activity', + 'files.directory_listing': 'query:files.directory_listing', + 'files.by_path': 'query:files.by_path', + 'spaces.get': 'query:spaces.get', + 'tags.search': 'query:tags.search', + 'test.ping': 'query:test.ping', + 'search.files': 'query:search.files', + 'files.alternate_instances': 'query:files.alternate_instances', + 'files.by_id': 'query:files.by_id', + 'files.media_listing': 'query:files.media_listing', + 'jobs.list': 'query:jobs.list', + 'locations.suggested': 'query:locations.suggested', + 'jobs.info': 'query:jobs.info', + 'sync.metrics': 'query:sync.metrics', + 'files.unique_to_location': 'query:files.unique_to_location', + }, +} as const; diff --git a/core/src/crypto/cloud_credentials.rs b/core/src/crypto/cloud_credentials.rs index b1d55ca4e..fac541f07 100644 --- a/core/src/crypto/cloud_credentials.rs +++ b/core/src/crypto/cloud_credentials.rs @@ -276,6 +276,8 @@ pub enum CredentialData { OAuth { access_token: String, refresh_token: String, + client_id: String, + client_secret: String, }, /// Simple API key @@ -310,6 +312,8 @@ impl CloudCredential { service: crate::volume::CloudServiceType, access_token: String, refresh_token: String, + client_id: String, + client_secret: String, expires_at: Option>, ) -> Self { Self { @@ -317,6 +321,8 @@ impl CloudCredential { data: CredentialData::OAuth { access_token, refresh_token, + client_id, + client_secret, }, created_at: chrono::Utc::now(), expires_at, @@ -426,6 +432,8 @@ mod tests { crate::volume::CloudServiceType::GoogleDrive, "access_token".to_string(), "refresh_token".to_string(), + "client_id".to_string(), + "client_secret".to_string(), Some(future), ); @@ -433,6 +441,8 @@ mod tests { crate::volume::CloudServiceType::GoogleDrive, "access_token".to_string(), "refresh_token".to_string(), + "client_id".to_string(), + "client_secret".to_string(), Some(past), ); diff --git a/core/src/ops/volumes/add_cloud/action.rs b/core/src/ops/volumes/add_cloud/action.rs index 4b1b15159..7b591b771 100644 --- a/core/src/ops/volumes/add_cloud/action.rs +++ b/core/src/ops/volumes/add_cloud/action.rs @@ -32,6 +32,8 @@ pub enum CloudStorageConfig { secret_access_key: String, endpoint: Option, }, + /// Google Drive with OAuth 2.0 credentials. + /// Requires both access_token and refresh_token for automatic token renewal. GoogleDrive { root: Option, access_token: String, @@ -39,6 +41,8 @@ pub enum CloudStorageConfig { client_id: String, client_secret: String, }, + /// OneDrive with OAuth 2.0 credentials. + /// Requires both access_token and refresh_token for automatic token renewal. OneDrive { root: Option, access_token: String, @@ -46,9 +50,11 @@ pub enum CloudStorageConfig { client_id: String, client_secret: String, }, + /// Dropbox with OAuth 2.0 refresh token for long-term access. + /// OpenDAL automatically obtains and refreshes access tokens as needed. + /// Only refresh_token is required (not access_token). Dropbox { root: Option, - access_token: String, refresh_token: String, client_id: String, client_secret: String, @@ -148,6 +154,28 @@ impl LibraryAction for VolumeAddCloudAction { client_id, client_secret, } => { + // Validate required OAuth credentials for Google Drive + if access_token.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Google Drive requires a valid access_token".to_string(), + )); + } + if refresh_token.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Google Drive requires a valid refresh_token".to_string(), + )); + } + if client_id.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Google Drive requires a valid client_id".to_string(), + )); + } + if client_secret.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Google Drive requires a valid client_secret".to_string(), + )); + } + let backend = CloudBackend::new_google_drive( access_token, refresh_token, @@ -167,6 +195,8 @@ impl LibraryAction for VolumeAddCloudAction { CloudServiceType::GoogleDrive, access_token.clone(), refresh_token.clone(), + client_id.clone(), + client_secret.clone(), None, // Google Drive tokens typically don't have a fixed expiry in the refresh flow ); @@ -190,6 +220,28 @@ impl LibraryAction for VolumeAddCloudAction { client_id, client_secret, } => { + // Validate required OAuth credentials for OneDrive + if access_token.trim().is_empty() { + return Err(ActionError::InvalidInput( + "OneDrive requires a valid access_token".to_string(), + )); + } + if refresh_token.trim().is_empty() { + return Err(ActionError::InvalidInput( + "OneDrive requires a valid refresh_token".to_string(), + )); + } + if client_id.trim().is_empty() { + return Err(ActionError::InvalidInput( + "OneDrive requires a valid client_id".to_string(), + )); + } + if client_secret.trim().is_empty() { + return Err(ActionError::InvalidInput( + "OneDrive requires a valid client_secret".to_string(), + )); + } + let backend = CloudBackend::new_onedrive( access_token, refresh_token, @@ -206,6 +258,8 @@ impl LibraryAction for VolumeAddCloudAction { CloudServiceType::OneDrive, access_token.clone(), refresh_token.clone(), + client_id.clone(), + client_secret.clone(), None, ); @@ -224,13 +278,28 @@ impl LibraryAction for VolumeAddCloudAction { } CloudStorageConfig::Dropbox { root, - access_token, refresh_token, client_id, client_secret, } => { + // Validate required OAuth credentials for Dropbox + if refresh_token.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Dropbox requires a valid refresh_token".to_string(), + )); + } + if client_id.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Dropbox requires a valid client_id".to_string(), + )); + } + if client_secret.trim().is_empty() { + return Err(ActionError::InvalidInput( + "Dropbox requires a valid client_secret".to_string(), + )); + } + let backend = CloudBackend::new_dropbox( - access_token, refresh_token, client_id, client_secret, @@ -243,8 +312,10 @@ impl LibraryAction for VolumeAddCloudAction { let credential = CloudCredential::new_oauth( CloudServiceType::Dropbox, - access_token.clone(), + "".to_string(), refresh_token.clone(), + client_id.clone(), + client_secret.clone(), None, ); diff --git a/core/src/volume/backend/cloud.rs b/core/src/volume/backend/cloud.rs index 4e78f70c5..d797efcfb 100644 --- a/core/src/volume/backend/cloud.rs +++ b/core/src/volume/backend/cloud.rs @@ -136,15 +136,17 @@ impl CloudBackend { } /// Create a new cloud backend for Dropbox + /// + /// Uses OAuth 2.0 refresh token for long-term access. OpenDAL automatically + /// refreshes the access token when it expires, ensuring continuous operation + /// without manual intervention. pub async fn new_dropbox( - access_token: impl AsRef, refresh_token: impl AsRef, client_id: impl AsRef, client_secret: impl AsRef, root: Option, ) -> Result { let mut builder = opendal::services::Dropbox::default() - .access_token(access_token.as_ref()) .refresh_token(refresh_token.as_ref()) .client_id(client_id.as_ref()) .client_secret(client_secret.as_ref()); diff --git a/core/src/volume/manager.rs b/core/src/volume/manager.rs index e215bb6d0..553d4fb64 100644 --- a/core/src/volume/manager.rs +++ b/core/src/volume/manager.rs @@ -238,13 +238,15 @@ impl VolumeManager { if let crate::crypto::cloud_credentials::CredentialData::OAuth { access_token, refresh_token, + client_id, + client_secret, } = &credential.data { crate::volume::CloudBackend::new_google_drive( access_token, refresh_token, - "", // client_id not stored yet - "", // client_secret not stored yet + client_id, + client_secret, Some(cloud_identifier.clone()), ).await } else { @@ -256,13 +258,15 @@ impl VolumeManager { if let crate::crypto::cloud_credentials::CredentialData::OAuth { access_token, refresh_token, + client_id, + client_secret, } = &credential.data { crate::volume::CloudBackend::new_onedrive( access_token, refresh_token, - "", - "", + client_id, + client_secret, Some(cloud_identifier.clone()), ).await } else { @@ -272,15 +276,16 @@ impl VolumeManager { } crate::volume::CloudServiceType::Dropbox => { if let crate::crypto::cloud_credentials::CredentialData::OAuth { - access_token, refresh_token, + client_id, + client_secret, + .. } = &credential.data { crate::volume::CloudBackend::new_dropbox( - access_token, refresh_token, - "", - "", + client_id, + client_secret, Some(cloud_identifier.clone()), ).await } else {