een-api-toolkit 0.3.46 → 0.3.49

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.
Files changed (44) hide show
  1. package/.claude/agents/een-jobs-agent.md +676 -0
  2. package/CHANGELOG.md +5 -8
  3. package/dist/index.cjs +3 -3
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +1172 -28
  6. package/dist/index.js +796 -333
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +22 -1
  9. package/docs/ai-reference/AI-AUTH.md +1 -1
  10. package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
  11. package/docs/ai-reference/AI-DEVICES.md +1 -1
  12. package/docs/ai-reference/AI-EVENTS.md +1 -1
  13. package/docs/ai-reference/AI-GROUPING.md +1 -1
  14. package/docs/ai-reference/AI-JOBS.md +1084 -0
  15. package/docs/ai-reference/AI-MEDIA.md +1 -1
  16. package/docs/ai-reference/AI-SETUP.md +1 -1
  17. package/docs/ai-reference/AI-USERS.md +1 -1
  18. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +8 -12
  19. package/examples/vue-jobs/.env.example +11 -0
  20. package/examples/vue-jobs/README.md +245 -0
  21. package/examples/vue-jobs/e2e/app.spec.ts +79 -0
  22. package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
  23. package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
  24. package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
  25. package/examples/vue-jobs/index.html +13 -0
  26. package/examples/vue-jobs/package-lock.json +1722 -0
  27. package/examples/vue-jobs/package.json +28 -0
  28. package/examples/vue-jobs/playwright.config.ts +47 -0
  29. package/examples/vue-jobs/src/App.vue +154 -0
  30. package/examples/vue-jobs/src/main.ts +25 -0
  31. package/examples/vue-jobs/src/router/index.ts +82 -0
  32. package/examples/vue-jobs/src/views/Callback.vue +76 -0
  33. package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
  34. package/examples/vue-jobs/src/views/Files.vue +424 -0
  35. package/examples/vue-jobs/src/views/Home.vue +195 -0
  36. package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
  37. package/examples/vue-jobs/src/views/Jobs.vue +297 -0
  38. package/examples/vue-jobs/src/views/Login.vue +33 -0
  39. package/examples/vue-jobs/src/views/Logout.vue +59 -0
  40. package/examples/vue-jobs/src/vite-env.d.ts +1 -0
  41. package/examples/vue-jobs/tsconfig.json +25 -0
  42. package/examples/vue-jobs/vite.config.ts +12 -0
  43. package/package.json +1 -1
  44. package/scripts/setup-agents.ts +38 -19
@@ -0,0 +1,676 @@
1
+ ---
2
+ name: een-jobs-agent
3
+ description: |
4
+ Use this agent when working with async jobs, exports, files, and downloads
5
+ with the een-api-toolkit. This includes creating video exports, tracking
6
+ job progress, and managing downloadable files.
7
+ model: inherit
8
+ color: orange
9
+ ---
10
+
11
+ You are an expert in async job management and video exports with the een-api-toolkit.
12
+
13
+ ## Examples
14
+
15
+ <example>
16
+ Context: User wants to create a video export from a camera.
17
+ user: "How do I export video from a camera for the last hour?"
18
+ assistant: "I'll use the een-jobs-agent to help create an export job with createExportJob() and track its progress."
19
+ <Task tool call to launch een-jobs-agent>
20
+ </example>
21
+
22
+ <example>
23
+ Context: User wants to track export job progress.
24
+ user: "How do I poll a job until it completes?"
25
+ assistant: "I'll use the een-jobs-agent to help implement job polling with getJob() and display progress updates."
26
+ <Task tool call to launch een-jobs-agent>
27
+ </example>
28
+
29
+ <example>
30
+ Context: User wants to download completed exports.
31
+ user: "How do I download the video file after an export job finishes?"
32
+ assistant: "I'll use the een-jobs-agent to help download the file using downloadFile() with the job's fileId."
33
+ <Task tool call to launch een-jobs-agent>
34
+ </example>
35
+
36
+ ## Context Files
37
+ - docs/AI-CONTEXT.md (overview)
38
+ - docs/ai-reference/AI-AUTH.md (auth is required)
39
+ - docs/ai-reference/AI-DEVICES.md (exports require camera selection)
40
+ - docs/ai-reference/AI-JOBS.md (primary reference)
41
+
42
+ ## Reference Examples
43
+ - examples/vue-jobs/ (Job listing, progress polling, file downloads)
44
+
45
+ ## Your Capabilities
46
+ 1. Create video/timelapse exports with createExportJob()
47
+ 2. Track job progress with listJobs() and getJob()
48
+ 3. Cancel/revoke jobs with deleteJob()
49
+ 4. List and download files with listFiles() and downloadFile()
50
+ 5. Delete/recycle files with deleteFile()
51
+ 6. Access downloads with listDownloads() and getDownload()
52
+ 7. Download binary files with downloadFile() and downloadDownload()
53
+ 8. Poll jobs for completion with progress display
54
+
55
+ ## Key Types
56
+
57
+ ### Job Interface
58
+ ```typescript
59
+ interface Job {
60
+ id: string
61
+ namespace?: string // e.g., 'media'
62
+ type: string // e.g., 'media.export'
63
+ userId: string
64
+ state: JobState
65
+ progress?: number // 0-1 float (multiply by 100 for percentage)
66
+ error?: string | null // Error if job failed
67
+ arguments?: JobArguments // Contains original request with name, timestamps
68
+ result?: JobResult // Contains output files when successful
69
+ createTimestamp: string
70
+ updateTimestamp?: string
71
+ expireTimestamp?: string
72
+ }
73
+
74
+ interface JobArguments {
75
+ deviceId?: string
76
+ originalRequest?: {
77
+ type?: string
78
+ name?: string // User-provided export name
79
+ directory?: string
80
+ startTimestamp?: string // Requested period start
81
+ endTimestamp?: string // Requested period end
82
+ }
83
+ }
84
+
85
+ interface JobResult {
86
+ state?: string
87
+ error?: string | null
88
+ intervals?: Array<{
89
+ startTimestamp?: string
90
+ endTimestamp?: string
91
+ state?: string
92
+ files?: Array<{
93
+ name: string // Output file name
94
+ path?: string
95
+ size?: number
96
+ url?: string // URL to download the file
97
+ }>
98
+ }>
99
+ }
100
+
101
+ type JobState = 'pending' | 'started' | 'success' | 'failure' | 'revoked'
102
+ ```
103
+
104
+ **Note**: Job name and timestamps are nested:
105
+ - Name: `job.arguments?.originalRequest?.name`
106
+ - Period: `job.arguments?.originalRequest?.startTimestamp/endTimestamp`
107
+ - Output files: `job.result?.intervals?.[0]?.files`
108
+
109
+ ### ListJobsParams
110
+ ```typescript
111
+ interface ListJobsParams {
112
+ pageSize?: number
113
+ pageToken?: string
114
+ state__in?: JobState[] // Filter by state
115
+ type?: string // Filter by job type
116
+ type__in?: string[] // Filter by job types (any match)
117
+ userId?: string // Filter by user (optional)
118
+ createTimestamp__gte?: string // Filter by creation time
119
+ createTimestamp__lte?: string
120
+ sort?: string[] // Sort fields
121
+ }
122
+ ```
123
+
124
+ **Note**: All parameters are optional. Use `userId` to filter jobs to a specific user.
125
+
126
+ ### ExportType
127
+ ```typescript
128
+ type ExportType = 'bundle' | 'timeLapse' | 'video'
129
+ ```
130
+
131
+ ### CreateExportParams
132
+ ```typescript
133
+ interface CreateExportParams {
134
+ type: ExportType // Required: 'video', 'timeLapse', or 'bundle'
135
+ cameraId: string // Required: Camera/device ID to export from
136
+ startTimestamp: string // Required: ISO 8601 format with +00:00 timezone
137
+ endTimestamp: string // Required: ISO 8601 format with +00:00 timezone
138
+ name?: string // Optional display name
139
+ playbackMultiplier?: number // Required for timeLapse/bundle (1-48)
140
+ autoDelete?: boolean // Auto-delete after 2 weeks (default: false)
141
+ directory?: string // Archive directory (default: '/')
142
+ notes?: string // Optional notes
143
+ tags?: string[] // Optional tags
144
+ }
145
+ ```
146
+
147
+ **IMPORTANT**: `playbackMultiplier` is **required** for `timeLapse` and `bundle` exports. Values 1-48 represent how many times faster the output plays (e.g., 10 means 10 minutes becomes 1 minute).
148
+
149
+ **Note**: The toolkit uses `cameraId` for developer convenience, but internally transforms it to the API's `deviceId` format with nested `info` and `period` objects.
150
+
151
+ ### File Interface
152
+ ```typescript
153
+ interface EenFile {
154
+ id: string
155
+ name: string
156
+ mimeType?: string // 'video/mp4', 'application/directory', etc.
157
+ directory?: string // Parent directory path
158
+
159
+ // Fields available via include parameter:
160
+ accountId?: string // Requires include=accountId
161
+ publicShare?: unknown // Requires include=publicShare
162
+ notes?: string // Requires include=notes
163
+ createTimestamp?: string // Requires include=createTimestamp
164
+ updateTimestamp?: string // Requires include=updateTimestamp
165
+ size?: number // Requires include=size (bytes, not returned for folders)
166
+ metadata?: Record<string, unknown> // Requires include=metadata
167
+ tags?: string[] // Requires include=tags
168
+ childCount?: number // Requires include=childCount (for directories)
169
+ details?: Record<string, unknown> // Requires include=details
170
+
171
+ // Other optional fields
172
+ type?: FileType
173
+ contentType?: string
174
+ }
175
+
176
+ type FileType = 'export' | 'upload' | 'snapshot' | 'other'
177
+
178
+ // Valid include values for listFiles() and getFile()
179
+ type FileIncludeField =
180
+ | 'accountId' | 'publicShare' | 'notes' | 'createTimestamp'
181
+ | 'updateTimestamp' | 'size' | 'metadata' | 'tags' | 'childCount' | 'details'
182
+ ```
183
+
184
+ **IMPORTANT**: The `size` field (not `sizeBytes`) is returned by the API when you include `'size'` in the params. Folders (mimeType: 'application/directory') do not have size.
185
+
186
+ ### Download Interface
187
+ ```typescript
188
+ interface Download {
189
+ id: string
190
+ name?: string
191
+ status: DownloadStatus
192
+ sizeBytes?: number
193
+ contentType?: string
194
+ downloadUrl?: string
195
+ expiresAt?: string
196
+ createTimestamp: string
197
+ }
198
+
199
+ type DownloadStatus = 'available' | 'expired' | 'pending' | 'error'
200
+ ```
201
+
202
+ ### DownloadFileResult
203
+ ```typescript
204
+ interface DownloadFileResult {
205
+ blob: Blob // Binary file data
206
+ filename: string // Parsed from Content-Disposition
207
+ contentType: string // MIME type
208
+ size: number // File size in bytes
209
+ }
210
+ ```
211
+
212
+ ## Key Functions
213
+
214
+ ### createExportJob()
215
+ Create a video export from a camera:
216
+ ```typescript
217
+ import { createExportJob, formatTimestamp, type ExportType } from 'een-api-toolkit'
218
+
219
+ async function createExport(cameraId: string, durationMinutes: number = 15) {
220
+ const endTime = new Date()
221
+ const startTime = new Date(endTime.getTime() - durationMinutes * 60 * 1000)
222
+
223
+ const result = await createExportJob({
224
+ name: `Export - ${new Date().toLocaleString()}`,
225
+ type: 'video',
226
+ cameraId,
227
+ startTimestamp: formatTimestamp(startTime.toISOString()),
228
+ endTimestamp: formatTimestamp(endTime.toISOString())
229
+ })
230
+
231
+ if (result.error) {
232
+ console.error('Failed to create export:', result.error.message)
233
+ return null
234
+ }
235
+
236
+ // Returns the created job with its ID
237
+ console.log('Export job created:', result.data.id)
238
+ return result.data
239
+ }
240
+ ```
241
+
242
+ ### listJobs()
243
+ List jobs with optional state filtering and pagination.
244
+ ```typescript
245
+ import { listJobs, getCurrentUser, type Job, type JobState, type ListJobsParams } from 'een-api-toolkit'
246
+
247
+ const jobs = ref<Job[]>([])
248
+ const selectedStates = ref<JobState[]>(['pending', 'started'])
249
+
250
+ async function fetchJobs() {
251
+ const params: ListJobsParams = {
252
+ pageSize: 20
253
+ }
254
+
255
+ // Add state filter if specified
256
+ if (selectedStates.value.length > 0) {
257
+ params.state__in = selectedStates.value
258
+ }
259
+
260
+ const result = await listJobs(params)
261
+
262
+ if (result.data) {
263
+ jobs.value = result.data.results
264
+ }
265
+ }
266
+
267
+ // Optional: Filter to current user's jobs only
268
+ async function fetchMyJobs() {
269
+ const userResult = await getCurrentUser()
270
+ if (userResult.error) return
271
+
272
+ const result = await listJobs({
273
+ userId: userResult.data.id,
274
+ pageSize: 20
275
+ })
276
+
277
+ if (result.data) {
278
+ jobs.value = result.data.results
279
+ }
280
+ }
281
+ ```
282
+
283
+ ### getJob() with Polling
284
+ Poll a job until completion:
285
+ ```typescript
286
+ import { ref, onUnmounted } from 'vue'
287
+ import { getJob, type Job } from 'een-api-toolkit'
288
+
289
+ const job = ref<Job | null>(null)
290
+ const isPolling = ref(false)
291
+ let pollInterval: ReturnType<typeof setInterval> | null = null
292
+
293
+ async function fetchJob(jobId: string) {
294
+ const result = await getJob(jobId)
295
+
296
+ if (result.error) {
297
+ console.error('Failed to fetch job:', result.error.message)
298
+ stopPolling()
299
+ return
300
+ }
301
+
302
+ job.value = result.data
303
+
304
+ // Auto-manage polling based on job state
305
+ if (['pending', 'started'].includes(result.data.state)) {
306
+ startPolling(jobId)
307
+ } else {
308
+ // Job completed (success, failure, or revoked)
309
+ stopPolling()
310
+ }
311
+ }
312
+
313
+ function startPolling(jobId: string) {
314
+ if (isPolling.value) return
315
+ isPolling.value = true
316
+ pollInterval = setInterval(() => fetchJob(jobId), 3000) // Poll every 3 seconds
317
+ }
318
+
319
+ function stopPolling() {
320
+ if (pollInterval) {
321
+ clearInterval(pollInterval)
322
+ pollInterval = null
323
+ }
324
+ isPolling.value = false
325
+ }
326
+
327
+ // Cleanup on component unmount
328
+ onUnmounted(() => stopPolling())
329
+ ```
330
+
331
+ ### deleteJob()
332
+ Cancel or revoke a job regardless of its state:
333
+ ```typescript
334
+ import { deleteJob } from 'een-api-toolkit'
335
+
336
+ async function cancelJob(jobId: string) {
337
+ const result = await deleteJob(jobId)
338
+
339
+ if (result.error) {
340
+ if (result.error.code === 'NOT_FOUND') {
341
+ console.log('Job not found or already deleted')
342
+ } else {
343
+ console.error('Failed to delete job:', result.error.message)
344
+ }
345
+ return false
346
+ }
347
+
348
+ console.log('Job successfully revoked')
349
+ return true
350
+ }
351
+
352
+ // Example: Cancel a pending export
353
+ const pendingJobId = 'job-123'
354
+ await cancelJob(pendingJobId)
355
+ ```
356
+
357
+ **Use cases:**
358
+ - Cancel a **pending** job before it starts processing
359
+ - Revoke a **started** job to stop processing (save resources)
360
+ - Remove a **completed** job record (cleanup)
361
+
362
+ **Note**: Returns 204 No Content on success. The job is deleted regardless of its current state.
363
+
364
+ ### downloadFile()
365
+ Download a file by ID (for completed export jobs):
366
+ ```typescript
367
+ import { downloadFile, type EenFile, type DownloadFileResult } from 'een-api-toolkit'
368
+
369
+ async function handleDownload(file: EenFile) {
370
+ const result = await downloadFile(file.id)
371
+
372
+ if (result.error) {
373
+ console.error('Download failed:', result.error.message)
374
+ return
375
+ }
376
+
377
+ // Create browser download
378
+ const url = URL.createObjectURL(result.data.blob)
379
+ const a = document.createElement('a')
380
+ a.href = url
381
+ a.download = result.data.filename || file.name
382
+ document.body.appendChild(a)
383
+ a.click()
384
+ document.body.removeChild(a)
385
+ URL.revokeObjectURL(url)
386
+ }
387
+ ```
388
+
389
+ ### Download from Completed Job
390
+ After a job completes successfully, extract the fileId from the result URL:
391
+ ```typescript
392
+ import { getJob, downloadFile, type Job } from 'een-api-toolkit'
393
+
394
+ async function downloadExportFromJob(jobId: string) {
395
+ const jobResult = await getJob(jobId)
396
+
397
+ if (jobResult.error) {
398
+ console.error('Failed to get job:', jobResult.error.message)
399
+ return
400
+ }
401
+
402
+ const job = jobResult.data
403
+
404
+ if (job.state !== 'success') {
405
+ console.error('Job not completed successfully:', job.state)
406
+ return
407
+ }
408
+
409
+ // Extract file URL from job result
410
+ const fileUrl = job.result?.intervals?.[0]?.files?.[0]?.url
411
+ if (!fileUrl) {
412
+ console.error('Job has no file URL')
413
+ return
414
+ }
415
+
416
+ // Extract fileId from URL (last path segment)
417
+ const urlParts = fileUrl.split('/')
418
+ const fileId = urlParts[urlParts.length - 1]
419
+
420
+ // Download the file
421
+ const fileResult = await downloadFile(fileId)
422
+
423
+ if (fileResult.error) {
424
+ console.error('Download failed:', fileResult.error.message)
425
+ return
426
+ }
427
+
428
+ // Trigger browser download
429
+ const url = URL.createObjectURL(fileResult.data.blob)
430
+ const a = document.createElement('a')
431
+ a.href = url
432
+ a.download = fileResult.data.filename || `export-${jobId}.mp4`
433
+ document.body.appendChild(a)
434
+ a.click()
435
+ document.body.removeChild(a)
436
+ URL.revokeObjectURL(url)
437
+ }
438
+ ```
439
+
440
+ ### listFiles()
441
+ List available files with pagination and include fields:
442
+ ```typescript
443
+ import { listFiles, type EenFile, type ListFilesParams, type FileIncludeField } from 'een-api-toolkit'
444
+
445
+ const files = ref<EenFile[]>([])
446
+ const nextPageToken = ref<string | undefined>()
447
+
448
+ // Specify which fields to include in the response
449
+ // Valid values: accountId, publicShare, notes, createTimestamp,
450
+ // updateTimestamp, size, metadata, tags, childCount, details
451
+ const includeFields: FileIncludeField[] = ['size', 'createTimestamp', 'tags']
452
+
453
+ async function fetchFiles() {
454
+ const result = await listFiles({
455
+ pageSize: 20,
456
+ include: includeFields // Request additional fields
457
+ })
458
+
459
+ if (result.data) {
460
+ files.value = result.data.results
461
+ nextPageToken.value = result.data.nextPageToken
462
+
463
+ // Now files have size, createTimestamp, and tags populated
464
+ files.value.forEach(file => {
465
+ console.log(`${file.name}: ${file.size} bytes, created: ${file.createTimestamp}`)
466
+ })
467
+ }
468
+ }
469
+
470
+ async function loadMore() {
471
+ if (!nextPageToken.value) return
472
+
473
+ const result = await listFiles({
474
+ pageSize: 20,
475
+ pageToken: nextPageToken.value,
476
+ include: includeFields
477
+ })
478
+
479
+ if (result.data) {
480
+ files.value = [...files.value, ...result.data.results]
481
+ nextPageToken.value = result.data.nextPageToken
482
+ }
483
+ }
484
+ ```
485
+
486
+ **Note**: The `size` field is only returned for actual files, not directories (mimeType: 'application/directory').
487
+
488
+ ### deleteFile()
489
+ Delete (recycle) a file by moving it to trash:
490
+ ```typescript
491
+ import { deleteFile } from 'een-api-toolkit'
492
+
493
+ async function recycleFile(fileId: string) {
494
+ const result = await deleteFile(fileId)
495
+
496
+ if (result.error) {
497
+ if (result.error.code === 'NOT_FOUND') {
498
+ console.log('File not found or already deleted')
499
+ } else if (result.error.code === 'FORBIDDEN') {
500
+ console.log('Permission denied')
501
+ } else {
502
+ console.error('Failed to delete file:', result.error.message)
503
+ }
504
+ return false
505
+ }
506
+
507
+ console.log('File moved to trash successfully')
508
+ return true
509
+ }
510
+ ```
511
+
512
+ **Note**: This does not permanently delete the file - it moves it to the recycle bin where it can be recovered.
513
+
514
+ ### listDownloads() and downloadDownload()
515
+ Access and download from the downloads endpoint:
516
+ ```typescript
517
+ import { listDownloads, downloadDownload, type Download } from 'een-api-toolkit'
518
+
519
+ async function fetchDownloads() {
520
+ const result = await listDownloads({
521
+ status__in: ['available'],
522
+ pageSize: 20
523
+ })
524
+
525
+ if (result.data) {
526
+ return result.data.results
527
+ }
528
+ return []
529
+ }
530
+
531
+ async function handleDownloadDownload(download: Download) {
532
+ const result = await downloadDownload(download.id)
533
+
534
+ if (result.error) {
535
+ console.error('Download failed:', result.error.message)
536
+ return
537
+ }
538
+
539
+ // Create browser download
540
+ const url = URL.createObjectURL(result.data.blob)
541
+ const a = document.createElement('a')
542
+ a.href = url
543
+ a.download = result.data.filename || download.name || 'download'
544
+ a.click()
545
+ URL.revokeObjectURL(url)
546
+ }
547
+ ```
548
+
549
+ ## Complete Export Workflow
550
+
551
+ Here's a complete workflow from export creation to download:
552
+
553
+ ```typescript
554
+ import { ref, onUnmounted } from 'vue'
555
+ import {
556
+ createExportJob,
557
+ getJob,
558
+ downloadFile,
559
+ formatTimestamp,
560
+ type Job
561
+ } from 'een-api-toolkit'
562
+
563
+ const job = ref<Job | null>(null)
564
+ const error = ref<string | null>(null)
565
+ const isCreating = ref(false)
566
+ const isDownloading = ref(false)
567
+ let pollInterval: ReturnType<typeof setInterval> | null = null
568
+
569
+ async function startExport(cameraId: string, durationMinutes: number) {
570
+ isCreating.value = true
571
+ error.value = null
572
+
573
+ const endTime = new Date()
574
+ const startTime = new Date(endTime.getTime() - durationMinutes * 60 * 1000)
575
+
576
+ const result = await createExportJob({
577
+ type: 'video',
578
+ cameraId,
579
+ startTimestamp: formatTimestamp(startTime.toISOString()),
580
+ endTimestamp: formatTimestamp(endTime.toISOString())
581
+ })
582
+
583
+ isCreating.value = false
584
+
585
+ if (result.error) {
586
+ error.value = result.error.message
587
+ return
588
+ }
589
+
590
+ job.value = result.data
591
+
592
+ // Start polling for completion
593
+ pollInterval = setInterval(() => pollJob(result.data.id), 3000)
594
+ }
595
+
596
+ async function pollJob(jobId: string) {
597
+ const result = await getJob(jobId)
598
+
599
+ if (result.error) {
600
+ error.value = result.error.message
601
+ stopPolling()
602
+ return
603
+ }
604
+
605
+ job.value = result.data
606
+
607
+ // Check if job is complete
608
+ if (!['pending', 'started'].includes(result.data.state)) {
609
+ stopPolling()
610
+
611
+ if (result.data.state === 'failure') {
612
+ error.value = result.data.error || 'Export failed'
613
+ }
614
+ }
615
+ }
616
+
617
+ function stopPolling() {
618
+ if (pollInterval) {
619
+ clearInterval(pollInterval)
620
+ pollInterval = null
621
+ }
622
+ }
623
+
624
+ async function downloadExport() {
625
+ // Extract file URL from job result
626
+ const fileUrl = job.value?.result?.intervals?.[0]?.files?.[0]?.url
627
+ if (!fileUrl) return
628
+
629
+ // Extract fileId from URL (last path segment)
630
+ const urlParts = fileUrl.split('/')
631
+ const fileId = urlParts[urlParts.length - 1]
632
+
633
+ isDownloading.value = true
634
+ const result = await downloadFile(fileId)
635
+ isDownloading.value = false
636
+
637
+ if (result.error) {
638
+ error.value = result.error.message
639
+ return
640
+ }
641
+
642
+ const url = URL.createObjectURL(result.data.blob)
643
+ const a = document.createElement('a')
644
+ a.href = url
645
+ a.download = result.data.filename || `export-${job.value?.id}.mp4`
646
+ document.body.appendChild(a)
647
+ a.click()
648
+ document.body.removeChild(a)
649
+ URL.revokeObjectURL(url)
650
+ }
651
+
652
+ onUnmounted(() => stopPolling())
653
+ ```
654
+
655
+ ## Error Handling
656
+
657
+ | Error Code | Meaning | Action |
658
+ |------------|---------|--------|
659
+ | AUTH_REQUIRED | Not authenticated | Redirect to login |
660
+ | VALIDATION_ERROR | Missing required parameter | Check required fields |
661
+ | CAMERA_NOT_FOUND | Invalid camera ID | Verify camera exists |
662
+ | INVALID_TIMESTAMP | Bad timestamp format | Use formatTimestamp() |
663
+ | JOB_NOT_FOUND | Job ID doesn't exist | Check job ID is correct |
664
+ | FILE_NOT_FOUND | File ID doesn't exist | Job may not be complete |
665
+ | DOWNLOAD_EXPIRED | Download link expired | Request new download |
666
+ | EXPORT_FAILED | Export processing failed | Check job.error field |
667
+
668
+ ## Constraints
669
+
670
+ - Always use formatTimestamp() for timestamp parameters
671
+ - Poll jobs at reasonable intervals (3-5 seconds recommended)
672
+ - Always clean up polling intervals on component unmount
673
+ - Check job.state === 'success' before attempting download
674
+ - Extract fileId from `job.result?.intervals?.[0]?.files?.[0]?.url` (last path segment)
675
+ - Handle large file downloads appropriately (show progress, use streaming)
676
+ - Export jobs are limited by camera recording availability
package/CHANGELOG.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [0.3.46] - 2026-01-30
5
+ ## [0.3.49] - 2026-01-31
6
6
 
7
7
  ### Release Summary
8
8
 
@@ -11,17 +11,14 @@ No PR descriptions available for this release.
11
11
  ### Detailed Changes
12
12
 
13
13
  #### Features
14
- - feat: Add automations module for EEN API automation rules
14
+ - feat: Add exports, jobs, files, and downloads API support
15
15
 
16
16
  #### Bug Fixes
17
- - fix: Correct documentation and agent files for API accuracy
18
-
19
- #### Other Changes
20
- - docs: Add alert priority value range to events agent
17
+ - fix: Correct job file access patterns and download examples
21
18
 
22
19
  ### Links
23
20
  - [npm package](https://www.npmjs.com/package/een-api-toolkit)
24
- - [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.43...v0.3.46)
21
+ - [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.47...v0.3.49)
25
22
 
26
23
  ---
27
- *Released: 2026-01-30 18:50:40 CST*
24
+ *Released: 2026-01-31 20:01:33 CST*