@howells/stow-server 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -38,11 +38,67 @@ interface StowServerConfig {
38
38
  }
39
39
  interface UploadResult {
40
40
  contentType: string;
41
+ /** True when upload short-circuited to an existing file by content hash dedupe. */
42
+ deduped?: boolean;
41
43
  key: string;
42
44
  metadata?: Record<string, string>;
43
45
  size: number;
44
46
  url: string | null;
45
47
  }
48
+ interface BucketResult {
49
+ allowedTypes?: string[] | null;
50
+ createdAt?: string;
51
+ description?: string | null;
52
+ fileCount?: number;
53
+ fileCountLimit?: number | null;
54
+ id: string;
55
+ isPublic?: boolean;
56
+ maxFileSize?: number | null;
57
+ name: string;
58
+ searchable?: boolean;
59
+ storageQuota?: number | null;
60
+ usageBytes?: number;
61
+ }
62
+ interface ListBucketsResult {
63
+ buckets: BucketResult[];
64
+ }
65
+ interface CreateBucketRequest {
66
+ allowedTypes?: string[];
67
+ description?: string;
68
+ fileCountLimit?: number;
69
+ isPublic?: boolean;
70
+ maxFileSize?: number;
71
+ name: string;
72
+ searchable?: boolean;
73
+ storageQuota?: number;
74
+ webhookUrl?: string;
75
+ }
76
+ interface UpdateBucketRequest {
77
+ allowedTypes?: string[] | null;
78
+ description?: string | null;
79
+ fileCountLimit?: number | null;
80
+ isPublic?: boolean;
81
+ maxFileSize?: number | null;
82
+ name?: string;
83
+ searchable?: boolean;
84
+ storageQuota?: number | null;
85
+ webhookUrl?: string | null;
86
+ }
87
+ interface WhoamiResult {
88
+ key?: {
89
+ name: string;
90
+ permissions: Record<string, boolean>;
91
+ scope: string;
92
+ };
93
+ stats: {
94
+ bucketCount: number;
95
+ totalBytes: number;
96
+ totalFiles: number;
97
+ };
98
+ user: {
99
+ email: string;
100
+ };
101
+ }
46
102
  interface TransformOptions {
47
103
  format?: "webp" | "avif" | "jpeg" | "png";
48
104
  height?: number;
@@ -50,15 +106,69 @@ interface TransformOptions {
50
106
  width?: number;
51
107
  }
52
108
  interface ListFilesResult {
53
- files: Array<{
54
- key: string;
55
- size: number;
56
- lastModified: string;
57
- url: string | null;
58
- metadata?: Record<string, string>;
59
- }>;
109
+ files: ListFilesItem[];
60
110
  nextCursor: string | null;
61
111
  }
112
+ interface FileColor {
113
+ hex: string;
114
+ hsl: {
115
+ h: number;
116
+ s: number;
117
+ l: number;
118
+ };
119
+ name: string | null;
120
+ oklab: {
121
+ L: number;
122
+ a: number;
123
+ b: number;
124
+ } | null;
125
+ oklch: {
126
+ l: number;
127
+ c: number;
128
+ h: number;
129
+ } | null;
130
+ position: number;
131
+ proportion: number;
132
+ }
133
+ interface FileColorProfile {
134
+ accent: {
135
+ hex: string;
136
+ name: string | null;
137
+ oklab: {
138
+ L: number;
139
+ a: number;
140
+ b: number;
141
+ } | null;
142
+ oklch: {
143
+ l: number;
144
+ c: number;
145
+ h: number;
146
+ } | null;
147
+ } | null;
148
+ backgroundHex: string | null;
149
+ colorCount: number;
150
+ extractedAt: string;
151
+ palette: {
152
+ mood: string;
153
+ brightness: number;
154
+ temperature: number;
155
+ vibrancy: number;
156
+ complexity: number;
157
+ dominantFamily: string | null;
158
+ };
159
+ }
160
+ interface ListFilesItem {
161
+ colorProfile?: FileColorProfile | null;
162
+ colors?: FileColor[];
163
+ duration?: number | null;
164
+ height?: number | null;
165
+ key: string;
166
+ lastModified: string;
167
+ metadata?: Record<string, string>;
168
+ size: number;
169
+ url: string | null;
170
+ width?: number | null;
171
+ }
62
172
  interface DropResult {
63
173
  contentType: string;
64
174
  filename: string;
@@ -82,6 +192,127 @@ interface ListDropsResult {
82
192
  limit: number;
83
193
  };
84
194
  }
195
+ interface FileResult {
196
+ colorProfile: FileColorProfile | null;
197
+ colors: FileColor[];
198
+ contentType: string;
199
+ createdAt: string;
200
+ duration: number | null;
201
+ embeddingStatus: string | null;
202
+ height: number | null;
203
+ key: string;
204
+ metadata: Record<string, string> | null;
205
+ size: number;
206
+ url: string | null;
207
+ width: number | null;
208
+ }
209
+ interface ProfileCreateRequest {
210
+ bucket?: string;
211
+ fileKeys?: string[];
212
+ name?: string;
213
+ }
214
+ interface ProfileClusterResult {
215
+ description: string | null;
216
+ id: string;
217
+ index: number;
218
+ name: string | null;
219
+ nameGeneratedAt: string | null;
220
+ signalCount: number;
221
+ totalWeight: number;
222
+ }
223
+ interface ProfileResult {
224
+ clusters?: ProfileClusterResult[];
225
+ createdAt: string;
226
+ fileCount: number;
227
+ id: string;
228
+ name: string | null;
229
+ signalCount: number;
230
+ updatedAt: string;
231
+ vector: number[] | null;
232
+ }
233
+ interface ReclusterRequest {
234
+ clusterCount?: number;
235
+ }
236
+ interface ReclusterResult {
237
+ clustered: boolean;
238
+ clusters: ProfileClusterResult[];
239
+ profileId: string;
240
+ totalSignals: number;
241
+ }
242
+ interface RenameClusterRequest {
243
+ description?: string;
244
+ name?: string;
245
+ }
246
+ interface ProfileFilesResult {
247
+ fileCount: number;
248
+ id: string;
249
+ }
250
+ type ProfileSignalType = "view" | "view_long" | "click" | "like" | "save" | "choose" | "purchase" | "share" | "dismiss" | "skip" | "reject" | "report" | "custom";
251
+ interface ProfileSignalInput {
252
+ context?: Record<string, unknown>;
253
+ fileKey: string;
254
+ type: ProfileSignalType;
255
+ weight?: number;
256
+ }
257
+ interface ProfileSignalResult {
258
+ fileKey: string;
259
+ id: string;
260
+ type: ProfileSignalType;
261
+ weight: number;
262
+ }
263
+ interface ProfileSignalsResponse {
264
+ profileId: string;
265
+ signals: ProfileSignalResult[];
266
+ totalSignals: number;
267
+ vectorUpdated: boolean;
268
+ }
269
+ interface DeleteProfileSignalsResult {
270
+ profileId: string;
271
+ removed: number;
272
+ totalSignals: number;
273
+ vectorUpdated: boolean;
274
+ }
275
+ interface ReprocessResult {
276
+ key: string;
277
+ triggered: string[];
278
+ }
279
+ interface ReplaceResult {
280
+ contentType: string;
281
+ key: string;
282
+ size: number;
283
+ triggered: string[];
284
+ }
285
+ interface SearchFilters {
286
+ color?: {
287
+ dominantFamily?: string[];
288
+ mood?: string[];
289
+ temperature?: {
290
+ min?: number;
291
+ max?: number;
292
+ };
293
+ brightness?: {
294
+ min?: number;
295
+ max?: number;
296
+ };
297
+ vibrancy?: {
298
+ min?: number;
299
+ max?: number;
300
+ };
301
+ };
302
+ contentType?: string[];
303
+ metadata?: Record<string, string>;
304
+ tags?: string[];
305
+ taxonomies?: string[];
306
+ taxonomyGroups?: string[];
307
+ }
308
+ type SearchIncludeField = "tags" | "taxonomies" | "colors" | "colorProfile";
309
+ interface TextSearchRequest {
310
+ bucket?: string;
311
+ filters?: SearchFilters;
312
+ include?: SearchIncludeField[];
313
+ limit?: number;
314
+ query: string;
315
+ }
85
316
  /** Normal presign response — client must PUT to uploadUrl then call confirm. */
86
317
  interface PresignNewResult {
87
318
  /** URL to call to confirm the upload */
@@ -89,8 +320,6 @@ interface PresignNewResult {
89
320
  dedupe?: false;
90
321
  /** The file key that will be created */
91
322
  fileKey: string;
92
- /** The R2 key for the file */
93
- r2Key: string;
94
323
  /** URL to PUT the file to */
95
324
  uploadUrl: string;
96
325
  }
@@ -119,6 +348,8 @@ interface PresignRequest {
119
348
  size: number;
120
349
  }
121
350
  interface ConfirmUploadRequest {
351
+ /** Request AI-generated alt text for images */
352
+ altText?: boolean;
122
353
  /** Override the default bucket */
123
354
  bucket?: string;
124
355
  /** SHA-256 hex digest of file content. Stored with the file record for future dedup lookups. */
@@ -126,18 +357,32 @@ interface ConfirmUploadRequest {
126
357
  contentType: string;
127
358
  /** Fire KV sync in background instead of blocking. Saves ~100ms per upload. */
128
359
  deferKvSync?: boolean;
360
+ /** Request AI-generated description for images */
361
+ describe?: boolean;
129
362
  fileKey: string;
130
363
  /** Custom metadata to attach to the file */
131
364
  metadata?: Record<string, string>;
132
365
  size: number;
133
366
  /** Skip R2 HEAD verification — trust the presign PUT. Saves ~100ms per upload. */
134
367
  skipVerify?: boolean;
368
+ /** Request AI-generated title for images */
369
+ title?: boolean;
135
370
  }
136
371
  interface SimilarSearchRequest {
137
372
  /** Bucket name or ID to scope search */
138
373
  bucket?: string;
374
+ /** Use a specific cluster centroid instead of the profile master vector. Requires profileId. */
375
+ clusterId?: string;
376
+ /** Blend multiple cluster centroids as query vector. Requires profileId. */
377
+ clusterIds?: string[];
378
+ /** File keys to exclude from results (e.g. already-seen items). Max 200. */
379
+ excludeKeys?: string[];
139
380
  /** Find files similar to this file key */
140
381
  fileKey?: string;
382
+ /** Structured post-filters (taxonomy, tag, color, content type, metadata) */
383
+ filters?: SearchFilters;
384
+ /** Opt-in enrichment fields to include in results */
385
+ include?: SearchIncludeField[];
141
386
  /** Max results (default 10, max 50) */
142
387
  limit?: number;
143
388
  /** Search using a taste profile's vector */
@@ -145,49 +390,104 @@ interface SimilarSearchRequest {
145
390
  /** Search directly with a vector (1024 dimensions) */
146
391
  vector?: number[];
147
392
  }
148
- interface SimilarSearchResult {
149
- results: Array<{
150
- id: string;
151
- key: string;
152
- bucketId: string;
153
- originalFilename: string | null;
154
- size: number;
155
- contentType: string;
156
- metadata: Record<string, string> | null;
157
- embeddingStatus: string | null;
158
- similarity: number;
159
- createdAt: string;
160
- }>;
393
+ interface DiverseSearchRequest {
394
+ /** Bucket name or ID to scope search */
395
+ bucket?: string;
396
+ /** Use a specific cluster centroid instead of the profile master vector. Requires profileId. */
397
+ clusterId?: string;
398
+ /** Blend multiple cluster centroids as query vector. Requires profileId. */
399
+ clusterIds?: string[];
400
+ /** File keys to exclude from results (e.g. already-seen items). Max 200. */
401
+ excludeKeys?: string[];
402
+ /** Find diverse files seeded by this file key */
403
+ fileKey?: string;
404
+ /** Structured post-filters (taxonomy, tag, color, content type, metadata) */
405
+ filters?: SearchFilters;
406
+ /** Opt-in enrichment fields to include in results */
407
+ include?: SearchIncludeField[];
408
+ /** Tradeoff between relevance and diversity (0–1, default 0.5). 0 = pure diversity, 1 = pure similarity. */
409
+ lambda?: number;
410
+ /** Max results (default 10, max 50) */
411
+ limit?: number;
412
+ /** Search using a taste profile's vector as the relevance seed */
413
+ profileId?: string;
414
+ /** Search directly with a vector (1024 dimensions) as the relevance seed */
415
+ vector?: number[];
161
416
  }
162
- interface FileResult {
417
+ interface SearchResultItem {
418
+ bucketId: string;
419
+ colorProfile?: FileColorProfile | null;
420
+ colors?: FileColor[];
163
421
  contentType: string;
164
422
  createdAt: string;
423
+ duration?: number | null;
165
424
  embeddingStatus: string | null;
425
+ height?: number | null;
426
+ id: string;
166
427
  key: string;
167
428
  metadata: Record<string, string> | null;
429
+ originalFilename: string | null;
430
+ similarity: number;
168
431
  size: number;
169
- url: string | null;
432
+ tags?: Array<{
433
+ slug: string;
434
+ name: string;
435
+ }>;
436
+ taxonomies?: Array<{
437
+ slug: string;
438
+ name: string;
439
+ group: string;
440
+ similarity: number;
441
+ }>;
442
+ width?: number | null;
170
443
  }
171
- interface ProfileCreateRequest {
444
+ interface FilteredMetadata {
445
+ candidatesScanned: number;
446
+ requested: number;
447
+ returned: number;
448
+ }
449
+ interface SimilarSearchResult {
450
+ filtered?: FilteredMetadata;
451
+ results: SearchResultItem[];
452
+ }
453
+ interface ColorSearchRequest {
454
+ /** Bucket name or ID to scope search */
172
455
  bucket?: string;
173
- fileKeys?: string[];
174
- name?: string;
456
+ /** Only search the dominant (first) color per file */
457
+ dominantOnly?: boolean;
458
+ /** Target color as hex (e.g. "#FF0000") */
459
+ hex?: string;
460
+ /** Max results (default 10, max 50) */
461
+ limit?: number;
462
+ /** Minimum color proportion in the palette (0–1, default 0.05) */
463
+ minProportion?: number;
464
+ /** Target color in OKLab space */
465
+ oklab?: {
466
+ L: number;
467
+ a: number;
468
+ b: number;
469
+ };
175
470
  }
176
- interface ProfileResult {
471
+ interface ColorSearchResultItem {
472
+ bucketId: string;
473
+ colorDistance: number;
474
+ contentType: string;
177
475
  createdAt: string;
178
- fileCount: number;
179
- id: string;
180
- name: string | null;
181
- updatedAt: string;
182
- }
183
- interface ProfileFilesResult {
184
- fileCount: number;
185
476
  id: string;
477
+ key: string;
478
+ matchedColor: {
479
+ hex: string;
480
+ oklab: {
481
+ L: number;
482
+ a: number;
483
+ b: number;
484
+ } | null;
485
+ position: number;
486
+ proportion: number;
487
+ };
186
488
  }
187
- interface TextSearchRequest {
188
- bucket?: string;
189
- limit?: number;
190
- query: string;
489
+ interface ColorSearchResult {
490
+ results: ColorSearchResultItem[];
191
491
  }
192
492
  declare class StowServer {
193
493
  private readonly apiKey;
@@ -200,6 +500,38 @@ declare class StowServer {
200
500
  * Get the base URL for this instance (used by client SDK)
201
501
  */
202
502
  getBaseUrl(): string;
503
+ /**
504
+ * Return account usage and API key info for the current credential.
505
+ */
506
+ whoami(): Promise<WhoamiResult>;
507
+ /**
508
+ * List all buckets available to the current organization.
509
+ */
510
+ listBuckets(): Promise<ListBucketsResult>;
511
+ /**
512
+ * Create a new bucket.
513
+ */
514
+ createBucket(request: CreateBucketRequest): Promise<BucketResult>;
515
+ /**
516
+ * Get bucket details by id.
517
+ */
518
+ getBucket(id: string): Promise<BucketResult>;
519
+ /**
520
+ * Update bucket settings by id.
521
+ */
522
+ updateBucket(id: string, updates: UpdateBucketRequest): Promise<BucketResult>;
523
+ /**
524
+ * Rename/update a bucket by current bucket name.
525
+ */
526
+ updateBucketByName(name: string, updates: UpdateBucketRequest): Promise<BucketResult>;
527
+ /**
528
+ * Rename a bucket by current bucket name.
529
+ */
530
+ renameBucket(name: string, newName: string): Promise<BucketResult>;
531
+ /**
532
+ * Delete a bucket by id.
533
+ */
534
+ deleteBucket(id: string): Promise<void>;
203
535
  /**
204
536
  * Resolve the effective bucket for this request.
205
537
  * Per-call override > constructor default.
@@ -229,6 +561,12 @@ declare class StowServer {
229
561
  bucket?: string;
230
562
  /** Custom metadata to attach to the file */
231
563
  metadata?: Record<string, string>;
564
+ /** Request AI-generated title for images */
565
+ title?: boolean;
566
+ /** Request AI-generated description for images */
567
+ describe?: boolean;
568
+ /** Request AI-generated alt text for images */
569
+ altText?: boolean;
232
570
  }): Promise<UploadResult>;
233
571
  /**
234
572
  * Upload a file from a URL (server-side fetch + upload)
@@ -238,6 +576,12 @@ declare class StowServer {
238
576
  metadata?: Record<string, string>;
239
577
  /** Headers to forward when fetching the URL (e.g. User-Agent, Referer) */
240
578
  headers?: Record<string, string>;
579
+ /** Request AI-generated title for images */
580
+ title?: boolean;
581
+ /** Request AI-generated description for images */
582
+ describe?: boolean;
583
+ /** Request AI-generated alt text for images */
584
+ altText?: boolean;
241
585
  }): Promise<UploadResult>;
242
586
  /**
243
587
  * Get a presigned URL for direct client-side upload.
@@ -286,6 +630,25 @@ declare class StowServer {
286
630
  getFile(key: string, options?: {
287
631
  bucket?: string;
288
632
  }): Promise<FileResult>;
633
+ /**
634
+ * Reprocess a file: reset all derived data (embeddings, colors, dimensions,
635
+ * AI metadata, taxonomies) and re-trigger processing tasks.
636
+ */
637
+ reprocessFile(key: string, options?: {
638
+ bucket?: string;
639
+ }): Promise<ReprocessResult>;
640
+ /**
641
+ * Replace a file's content by fetching from a new URL.
642
+ *
643
+ * Keeps the same file key but replaces the stored object and resets all
644
+ * derived data (dimensions, embeddings, colors, AI metadata). Processing
645
+ * tasks are re-dispatched as if the file were newly uploaded.
646
+ */
647
+ replaceFile(key: string, url: string, options?: {
648
+ bucket?: string;
649
+ /** Headers to forward when fetching the URL (e.g. User-Agent, Referer) */
650
+ headers?: Record<string, string>;
651
+ }): Promise<ReplaceResult>;
289
652
  /**
290
653
  * Get a transform URL for an image.
291
654
  *
@@ -342,10 +705,14 @@ declare class StowServer {
342
705
  */
343
706
  get search(): {
344
707
  similar: (params: SimilarSearchRequest) => Promise<SimilarSearchResult>;
708
+ diverse: (params?: DiverseSearchRequest) => Promise<SimilarSearchResult>;
345
709
  text: (params: TextSearchRequest) => Promise<SimilarSearchResult>;
710
+ color: (params: ColorSearchRequest) => Promise<ColorSearchResult>;
346
711
  };
347
712
  private searchSimilar;
713
+ private searchDiverse;
348
714
  private searchText;
715
+ private searchColor;
349
716
  /**
350
717
  * Upload a file as a drop (quick share)
351
718
  *
@@ -379,12 +746,26 @@ declare class StowServer {
379
746
  delete: (id: string) => Promise<void>;
380
747
  addFiles: (id: string, fileKeys: string[], bucket?: string) => Promise<ProfileFilesResult>;
381
748
  removeFiles: (id: string, fileKeys: string[], bucket?: string) => Promise<ProfileFilesResult>;
749
+ signal: (id: string, signals: ProfileSignalInput[], bucket?: string) => Promise<ProfileSignalsResponse>;
750
+ deleteSignals: (id: string, signalIds: string[]) => Promise<DeleteProfileSignalsResult>;
751
+ clusters: (id: string) => Promise<{
752
+ profileId: string;
753
+ signalCount: number;
754
+ clusters: ProfileClusterResult[];
755
+ }>;
756
+ recluster: (id: string, params?: ReclusterRequest) => Promise<ReclusterResult>;
757
+ renameCluster: (profileId: string, clusterId: string, params: RenameClusterRequest) => Promise<ProfileClusterResult>;
382
758
  };
383
759
  private createProfile;
384
760
  private getProfile;
385
761
  private deleteProfile;
386
762
  private addProfileFiles;
387
763
  private removeProfileFiles;
764
+ private signalProfile;
765
+ private deleteProfileSignals;
766
+ private getProfileClusters;
767
+ private reclusterProfile;
768
+ private renameProfileCluster;
388
769
  }
389
770
 
390
- export { type ConfirmUploadRequest, type Drop, type DropResult, type FileResult, type ListDropsResult, type ListFilesResult, type PresignDedupeResult, type PresignNewResult, type PresignRequest, type PresignResult, type ProfileCreateRequest, type ProfileFilesResult, type ProfileResult, type SimilarSearchRequest, type SimilarSearchResult, StowError, StowServer, type StowServerConfig, type TextSearchRequest, type TransformOptions, type UploadResult };
771
+ export { type BucketResult, type ColorSearchRequest, type ColorSearchResult, type ColorSearchResultItem, type ConfirmUploadRequest, type CreateBucketRequest, type DeleteProfileSignalsResult, type DiverseSearchRequest, type Drop, type DropResult, type FileColor, type FileColorProfile, type FileResult, type FilteredMetadata, type ListBucketsResult, type ListDropsResult, type ListFilesItem, type ListFilesResult, type PresignDedupeResult, type PresignNewResult, type PresignRequest, type PresignResult, type ProfileClusterResult, type ProfileCreateRequest, type ProfileFilesResult, type ProfileResult, type ProfileSignalInput, type ProfileSignalResult, type ProfileSignalType, type ProfileSignalsResponse, type ReclusterRequest, type ReclusterResult, type RenameClusterRequest, type ReplaceResult, type ReprocessResult, type SearchFilters, type SearchIncludeField, type SearchResultItem, type SimilarSearchRequest, type SimilarSearchResult, StowError, StowServer, type StowServerConfig, type TextSearchRequest, type TransformOptions, type UpdateBucketRequest, type UploadResult, type WhoamiResult };