@socialseal/cli 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.5 - 2026-03-19
6
+ - Add first-class tracked-video workflows with `video queue-analysis` and `video extract`.
7
+ - Make `--video-id` the primary ergonomic selector for tracked-video analysis and asset extraction, while keeping `--search-result-id` as a fallback selector.
8
+ - Support batch queueing/extraction payloads plus optional asset downloads for thumbnails, source video, and extracted key frames.
9
+
5
10
  ## 0.1.4 - 2026-03-19
6
11
  - Add explicit `group_add_item` / `group_add_items` CLI aliases for tracking-group membership workflows.
7
12
  - Add `tracking resolve` / `get_by_value` so existing tracked searches can be resolved by value using the same duplicate-detection semantics as create.
package/README.md CHANGED
@@ -49,6 +49,12 @@ Optional config file:
49
49
  - `socialseal tools call --function search-journey-run --body @payload.json --async --workspace-id <uuid>`
50
50
  - `socialseal tools call --function search-journey-run --body @payload.json --async --no-poll --workspace-id <uuid>`
51
51
 
52
+ - Tracked video extraction:
53
+ - `socialseal video queue-analysis --video-id 734829384 --workspace-id <uuid>`
54
+ - `socialseal video extract --video-id 734829384 --workspace-id <uuid>`
55
+ - `socialseal video extract --video-uid <video_uid> --wait --workspace-id <uuid>`
56
+ - `socialseal video extract --body @payload.json --out-dir ./video-assets`
57
+
52
58
  - Data exports (provisional):
53
59
  - `socialseal data export-tracking --group-id 123 --time-period 30d --out out.csv`
54
60
  - `socialseal data export-report --report-type keyword_universe --format csv --payload @payload.json --out out.csv`
@@ -58,6 +64,9 @@ Optional config file:
58
64
  - `tools list` ships a stable built-in registry of supported direct-call function targets. It is not live backend enumeration.
59
65
  - `--timeout <ms>` controls HTTP request timeouts. Agent runs default to a 5-minute WebSocket inactivity timeout unless you set `--idle-timeout <ms>` (or the matching env/config value).
60
66
  - `search-journey-run` supports CLI-managed async polling: `--async` starts backend async mode, polling is on by default, `--no-poll` returns the initial `runId`, and `--poll-interval <ms>` controls the status polling cadence.
67
+ - `video queue-analysis` wraps the tracked-video extraction backend in queue-only mode so you can queue one or many tracked videos without downloading assets first.
68
+ - `video extract` wraps the same backend in extraction mode and returns a normalized JSON payload with resolved tracking context, structured analysis, thumbnail/frame assets, and optional local downloads under `--out-dir`.
69
+ - `--video-id` is the primary ergonomic selector for video workflows. The backend tries it as `video_uid` first, then as platform video id. `--search-result-id` remains available when you are starting from a specific tracked rank row.
61
70
  - `socialseal agent run` now defaults to a fresh conversation. The CLI prints a continuation token to `stderr`; pass it back with `--continue <token>` to resume the same agent conversation explicitly.
62
71
  - Effective workspace precedence is: `--workspace-id` → `SOCIALSEAL_WORKSPACE_ID` → config `workspaceId` → backend personal-workspace fallback.
63
72
  - `socialseal workspace use ...` writes a local default workspace into `~/.config/socialseal/config.json`, which the CLI reuses for `agent`, `tools`, and `data` commands.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ const WORKSPACE_HEADER = 'X-Workspace-Id';
13
13
  const DEFAULT_TIMEOUT_MS = 30000;
14
14
  const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 300000;
15
15
  const DEFAULT_POLL_INTERVAL_MS = 2000;
16
+ const DEFAULT_FRAME_COUNT = 3;
16
17
  const MAX_TIMEOUT_MS = 900000;
17
18
  const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
18
19
  const STATIC_TOOL_REGISTRY_NOTE = 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration, so environment-specific availability can drift.';
@@ -62,6 +63,16 @@ const KNOWN_TOOLS = [
62
63
  knownLocalDevState: 'disabled_by_default',
63
64
  notes: 'group_id expects a numeric tracking_group id, not a brand_group UUID.',
64
65
  },
66
+ {
67
+ name: 'tracked-video-extract',
68
+ category: 'video',
69
+ description: 'Resolve tracked videos/results into structured analysis plus reference assets.',
70
+ objectType: 'tracked_video_extract',
71
+ transport: 'post_edge_function',
72
+ workspaceScoped: true,
73
+ knownLocalDevState: 'enabled',
74
+ notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; can queue analysis and return/download asset URLs.',
75
+ },
65
76
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
66
77
  { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
67
78
  { name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
@@ -339,6 +350,273 @@ function mergeWorkspaceIdIntoPayload(payload, workspaceId) {
339
350
  return { ...payload, workspaceId };
340
351
  }
341
352
 
353
+ function sanitizeFileComponent(value, fallback = 'item') {
354
+ const normalized = String(value || '')
355
+ .trim()
356
+ .replace(/[^a-z0-9._-]+/gi, '-')
357
+ .replace(/-+/g, '-')
358
+ .replace(/^-|-$/g, '');
359
+ return normalized || fallback;
360
+ }
361
+
362
+ function inferExtension(urlValue, contentType, fallback = '.bin') {
363
+ const normalizedType = String(contentType || '').toLowerCase();
364
+ if (normalizedType.includes('jpeg')) return '.jpg';
365
+ if (normalizedType.includes('png')) return '.png';
366
+ if (normalizedType.includes('webp')) return '.webp';
367
+ if (normalizedType.includes('gif')) return '.gif';
368
+ if (normalizedType.includes('mp4')) return '.mp4';
369
+ if (normalizedType.includes('quicktime')) return '.mov';
370
+
371
+ try {
372
+ const pathname = new URL(urlValue).pathname || '';
373
+ const ext = path.extname(pathname);
374
+ if (ext) return ext;
375
+ } catch {
376
+ // ignore parse failures
377
+ }
378
+ return fallback;
379
+ }
380
+
381
+ function normalizeVideoExtractBody(body) {
382
+ const normalized = { ...body };
383
+ const hasInlineIdentifier =
384
+ normalized.videoId !== undefined ||
385
+ normalized.searchResultId !== undefined ||
386
+ normalized.videoUid !== undefined ||
387
+ normalized.platformVideoId !== undefined;
388
+
389
+ if (!Array.isArray(normalized.items) && hasInlineIdentifier) {
390
+ normalized.items = [{
391
+ videoId: normalized.videoId,
392
+ searchResultId: normalized.searchResultId,
393
+ videoUid: normalized.videoUid,
394
+ platformVideoId: normalized.platformVideoId,
395
+ platformId: normalized.platformId,
396
+ }];
397
+ delete normalized.videoId;
398
+ delete normalized.searchResultId;
399
+ delete normalized.videoUid;
400
+ delete normalized.platformVideoId;
401
+ delete normalized.platformId;
402
+ }
403
+
404
+ return normalized;
405
+ }
406
+
407
+ function buildVideoExtractBody(opts, workspaceId) {
408
+ const parsed = opts.body
409
+ ? ensureJsonObject(parseJsonInput(opts.body, { label: 'body' }), 'body')
410
+ : {};
411
+ const normalized = normalizeVideoExtractBody(parsed);
412
+
413
+ if (!Array.isArray(normalized.items) || normalized.items.length === 0) {
414
+ const inlineItem = stripUndefinedEntries({
415
+ videoId: trimString(opts.videoId) || undefined,
416
+ searchResultId: opts.searchResultId !== undefined
417
+ ? coercePositiveInteger(opts.searchResultId, 'searchResultId')
418
+ : undefined,
419
+ videoUid: trimString(opts.videoUid) || undefined,
420
+ platformVideoId: trimString(opts.platformVideoId) || undefined,
421
+ });
422
+
423
+ if (Object.keys(inlineItem).length === 0) {
424
+ throw new CliError('Provide --body or one of --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
425
+ code: 'MISSING_ARGUMENT',
426
+ exitCode: EXIT_CODES.USAGE,
427
+ });
428
+ }
429
+
430
+ normalized.items = [inlineItem];
431
+ }
432
+
433
+ const bodyWithWorkspace = mergeWorkspaceIdIntoPayload(normalized, workspaceId);
434
+ if (!bodyWithWorkspace.workspaceId) {
435
+ throw new CliError('tracked-video extraction requires a workspace id.', {
436
+ code: 'WORKSPACE_REQUIRED',
437
+ exitCode: EXIT_CODES.USAGE,
438
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
439
+ });
440
+ }
441
+
442
+ const nextBody = { ...bodyWithWorkspace };
443
+ if (opts.wait) {
444
+ nextBody.ensureAnalysis = true;
445
+ } else if (opts.ensureAnalysis === true) {
446
+ nextBody.ensureAnalysis = true;
447
+ } else if (nextBody.ensureAnalysis === undefined) {
448
+ nextBody.ensureAnalysis = false;
449
+ }
450
+
451
+ if (opts.skipAssets === true) {
452
+ nextBody.includeAssets = false;
453
+ } else if (nextBody.includeAssets === undefined) {
454
+ nextBody.includeAssets = true;
455
+ }
456
+
457
+ if (opts.includeSourceVideo === true) {
458
+ nextBody.includeSourceVideo = true;
459
+ } else if (nextBody.includeSourceVideo === undefined) {
460
+ nextBody.includeSourceVideo = false;
461
+ }
462
+
463
+ if (opts.frameStrategy && nextBody.frameStrategy === undefined) {
464
+ nextBody.frameStrategy = opts.frameStrategy;
465
+ }
466
+ if (nextBody.frameStrategy === undefined) {
467
+ nextBody.frameStrategy = 'brief_shots';
468
+ }
469
+
470
+ if (nextBody.frameCount === undefined) {
471
+ nextBody.frameCount = opts.frameCount !== undefined
472
+ ? Number(opts.frameCount)
473
+ : DEFAULT_FRAME_COUNT;
474
+ }
475
+
476
+ if (nextBody.signedUrlSeconds === undefined) {
477
+ nextBody.signedUrlSeconds = opts.signedUrlSeconds !== undefined
478
+ ? Number(opts.signedUrlSeconds)
479
+ : 3600;
480
+ }
481
+
482
+ return nextBody;
483
+ }
484
+
485
+ function buildVideoQueueBody(opts, workspaceId) {
486
+ const body = buildVideoExtractBody(opts, workspaceId);
487
+ return {
488
+ ...body,
489
+ ensureAnalysis: true,
490
+ queueOnly: true,
491
+ includeAssets: false,
492
+ includeRawAnalysis: false,
493
+ includeSourceVideo: false,
494
+ };
495
+ }
496
+
497
+ function hasPendingVideoExtractResults(payload) {
498
+ const results = Array.isArray(payload?.results) ? payload.results : [];
499
+ return results.some((result) => {
500
+ const status = String(result?.analysis?.status || '').trim().toLowerCase();
501
+ return status === 'pending' || status === 'processing';
502
+ });
503
+ }
504
+
505
+ async function downloadAssetToFile({ url, outDir, stem, timeoutMs }) {
506
+ const response = await fetchWithTimeout(url, {
507
+ method: 'GET',
508
+ headers: { Accept: '*/*' },
509
+ }, timeoutMs);
510
+
511
+ if (!response.ok) {
512
+ throw new CliError(`Asset download failed: ${response.status}`, {
513
+ code: 'ASSET_DOWNLOAD_FAILED',
514
+ exitCode: EXIT_CODES.SERVER,
515
+ details: await response.text().catch(() => null),
516
+ });
517
+ }
518
+
519
+ const extension = inferExtension(url, response.headers.get('content-type'));
520
+ const absolutePath = path.resolve(outDir, `${stem}${extension}`);
521
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
522
+ const buffer = Buffer.from(await response.arrayBuffer());
523
+ fs.writeFileSync(absolutePath, buffer);
524
+ return {
525
+ path: absolutePath,
526
+ bytes: buffer.length,
527
+ };
528
+ }
529
+
530
+ async function downloadVideoExtractAssets(payload, outDir, timeoutMs) {
531
+ const results = Array.isArray(payload?.results) ? payload.results : [];
532
+ const absoluteBaseDir = path.resolve(outDir);
533
+ fs.mkdirSync(absoluteBaseDir, { recursive: true });
534
+
535
+ for (let index = 0; index < results.length; index += 1) {
536
+ const result = results[index];
537
+ const resolved = result?.resolved || {};
538
+ const videoKey = sanitizeFileComponent(
539
+ resolved.videoUid || resolved.platformVideoId || resolved.searchResultId || `item-${index + 1}`,
540
+ `item-${index + 1}`,
541
+ );
542
+ const itemDir = path.join(absoluteBaseDir, `${String(index + 1).padStart(2, '0')}-${videoKey}`);
543
+ const downloads = {
544
+ directory: itemDir,
545
+ thumbnail: null,
546
+ sourceVideo: null,
547
+ frames: [],
548
+ };
549
+
550
+ const thumbnailUrl = result?.assets?.thumbnail?.url;
551
+ if (typeof thumbnailUrl === 'string' && thumbnailUrl.length > 0) {
552
+ try {
553
+ downloads.thumbnail = await downloadAssetToFile({
554
+ url: thumbnailUrl,
555
+ outDir: itemDir,
556
+ stem: 'thumbnail',
557
+ timeoutMs,
558
+ });
559
+ } catch (error) {
560
+ downloads.thumbnail = {
561
+ error: error instanceof Error ? error.message : String(error),
562
+ };
563
+ }
564
+ }
565
+
566
+ const sourceVideoUrl = result?.assets?.sourceVideo?.signedUrl;
567
+ if (typeof sourceVideoUrl === 'string' && sourceVideoUrl.length > 0) {
568
+ try {
569
+ downloads.sourceVideo = await downloadAssetToFile({
570
+ url: sourceVideoUrl,
571
+ outDir: itemDir,
572
+ stem: 'source-video',
573
+ timeoutMs,
574
+ });
575
+ } catch (error) {
576
+ downloads.sourceVideo = {
577
+ error: error instanceof Error ? error.message : String(error),
578
+ };
579
+ }
580
+ }
581
+
582
+ const frames = Array.isArray(result?.assets?.frames) ? result.assets.frames : [];
583
+ for (let frameIndex = 0; frameIndex < frames.length; frameIndex += 1) {
584
+ const frame = frames[frameIndex];
585
+ const frameUrl = frame?.signedUrl;
586
+ if (typeof frameUrl !== 'string' || frameUrl.length === 0) continue;
587
+ const timestampToken = sanitizeFileComponent(frame?.timestamp || `frame-${frameIndex + 1}`, `frame-${frameIndex + 1}`);
588
+ try {
589
+ const download = await downloadAssetToFile({
590
+ url: frameUrl,
591
+ outDir: itemDir,
592
+ stem: `${frame?.kind || 'frame'}-${String(frameIndex + 1).padStart(2, '0')}-${timestampToken}`,
593
+ timeoutMs,
594
+ });
595
+ downloads.frames.push({
596
+ kind: frame?.kind || null,
597
+ timestamp: frame?.timestamp || null,
598
+ path: download.path,
599
+ bytes: download.bytes,
600
+ });
601
+ } catch (error) {
602
+ downloads.frames.push({
603
+ kind: frame?.kind || null,
604
+ timestamp: frame?.timestamp || null,
605
+ error: error instanceof Error ? error.message : String(error),
606
+ });
607
+ }
608
+ }
609
+
610
+ result.downloads = downloads;
611
+ }
612
+
613
+ return {
614
+ ...payload,
615
+ downloadsRoot: absoluteBaseDir,
616
+ results,
617
+ };
618
+ }
619
+
342
620
  function hasOwn(value, key) {
343
621
  return Boolean(value) && Object.prototype.hasOwnProperty.call(value, key);
344
622
  }
@@ -2156,6 +2434,160 @@ async function handleDataExportReport(opts) {
2156
2434
  process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
2157
2435
  }
2158
2436
 
2437
+ async function handleVideoExtract(opts) {
2438
+ const config = loadConfig();
2439
+ const apiKey = requireApiKey(opts, config);
2440
+ const apiBase = resolveApiBase(opts, config);
2441
+ const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
2442
+ const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
2443
+ const timeoutMs = resolveTimeoutMs(opts, config);
2444
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
2445
+
2446
+ const body = buildVideoExtractBody(opts, resolvedWorkspaceId);
2447
+ const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
2448
+
2449
+ emitWorkspaceContext(opts, {
2450
+ workspaceId: body.workspaceId || resolvedWorkspaceId,
2451
+ source: body.workspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
2452
+ functionName: 'tracked-video-extract',
2453
+ method: 'POST',
2454
+ });
2455
+
2456
+ const requestOnce = async (remainingTimeoutMs) => {
2457
+ const res = await callApi({
2458
+ apiBase: useGateway ? resolvedApiBase : legacyUrl,
2459
+ apiKey,
2460
+ path,
2461
+ method: 'POST',
2462
+ body,
2463
+ workspaceId: body.workspaceId || resolvedWorkspaceId,
2464
+ timeoutMs: remainingTimeoutMs,
2465
+ });
2466
+
2467
+ if (!res.ok) {
2468
+ throw await buildHttpError(res, {
2469
+ label: 'Tracked video extract',
2470
+ functionName: 'tracked-video-extract',
2471
+ method: 'POST',
2472
+ });
2473
+ }
2474
+
2475
+ const contentType = res.headers.get('content-type') || '';
2476
+ if (!contentType.includes('application/json')) {
2477
+ throw new CliError('tracked-video-extract returned a non-JSON response.', {
2478
+ code: 'INVALID_RESPONSE',
2479
+ exitCode: EXIT_CODES.SERVER,
2480
+ });
2481
+ }
2482
+
2483
+ return await res.json();
2484
+ };
2485
+
2486
+ let payload = await requestOnce(timeoutMs);
2487
+
2488
+ if (opts.wait) {
2489
+ const pollIntervalMs = resolvePollIntervalMs(opts);
2490
+ const deadline = Date.now() + timeoutMs;
2491
+
2492
+ while (hasPendingVideoExtractResults(payload)) {
2493
+ const remainingMs = deadline - Date.now();
2494
+ if (remainingMs <= 0) {
2495
+ throw new CliError('Timed out waiting for tracked video analysis completion.', {
2496
+ code: 'ASYNC_WAIT_TIMEOUT',
2497
+ exitCode: EXIT_CODES.SERVER,
2498
+ hint: 'Increase --timeout <ms> or omit --wait to return the current status immediately.',
2499
+ details: truncateDetails(payload),
2500
+ });
2501
+ }
2502
+
2503
+ emitInfo(opts, 'tracked-video-extract pending; polling for completion.');
2504
+ await sleep(Math.min(pollIntervalMs, remainingMs));
2505
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()));
2506
+ }
2507
+ }
2508
+
2509
+ if (opts.outDir) {
2510
+ payload = await downloadVideoExtractAssets(payload, opts.outDir, timeoutMs);
2511
+ }
2512
+
2513
+ emitJsonOutput(payload, opts.pretty);
2514
+ }
2515
+
2516
+ async function handleVideoQueueAnalysis(opts) {
2517
+ const config = loadConfig();
2518
+ const apiKey = requireApiKey(opts, config);
2519
+ const apiBase = resolveApiBase(opts, config);
2520
+ const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
2521
+ const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
2522
+ const timeoutMs = resolveTimeoutMs(opts, config);
2523
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
2524
+
2525
+ const body = buildVideoQueueBody(opts, resolvedWorkspaceId);
2526
+ const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
2527
+
2528
+ emitWorkspaceContext(opts, {
2529
+ workspaceId: body.workspaceId || resolvedWorkspaceId,
2530
+ source: body.workspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
2531
+ functionName: 'tracked-video-extract',
2532
+ method: 'POST',
2533
+ });
2534
+
2535
+ const requestOnce = async (remainingTimeoutMs) => {
2536
+ const res = await callApi({
2537
+ apiBase: useGateway ? resolvedApiBase : legacyUrl,
2538
+ apiKey,
2539
+ path,
2540
+ method: 'POST',
2541
+ body,
2542
+ workspaceId: body.workspaceId || resolvedWorkspaceId,
2543
+ timeoutMs: remainingTimeoutMs,
2544
+ });
2545
+
2546
+ if (!res.ok) {
2547
+ throw await buildHttpError(res, {
2548
+ label: 'Tracked video queue-analysis',
2549
+ functionName: 'tracked-video-extract',
2550
+ method: 'POST',
2551
+ });
2552
+ }
2553
+
2554
+ const contentType = res.headers.get('content-type') || '';
2555
+ if (!contentType.includes('application/json')) {
2556
+ throw new CliError('tracked-video-extract returned a non-JSON response.', {
2557
+ code: 'INVALID_RESPONSE',
2558
+ exitCode: EXIT_CODES.SERVER,
2559
+ });
2560
+ }
2561
+
2562
+ return await res.json();
2563
+ };
2564
+
2565
+ let payload = await requestOnce(timeoutMs);
2566
+
2567
+ if (opts.wait) {
2568
+ const pollIntervalMs = resolvePollIntervalMs(opts);
2569
+ const deadline = Date.now() + timeoutMs;
2570
+
2571
+ while (hasPendingVideoExtractResults(payload)) {
2572
+ const remainingMs = deadline - Date.now();
2573
+ if (remainingMs <= 0) {
2574
+ throw new CliError('Timed out waiting for queued video analysis completion.', {
2575
+ code: 'ASYNC_WAIT_TIMEOUT',
2576
+ exitCode: EXIT_CODES.SERVER,
2577
+ hint: 'Increase --timeout <ms> or omit --wait to return queue status immediately.',
2578
+ details: truncateDetails(payload),
2579
+ });
2580
+ }
2581
+
2582
+ emitInfo(opts, 'tracked-video queue-analysis pending; polling for completion.');
2583
+ await sleep(Math.min(pollIntervalMs, remainingMs));
2584
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()));
2585
+ }
2586
+ }
2587
+
2588
+ emitJsonOutput(payload, opts.pretty);
2589
+ }
2590
+
2159
2591
  async function handleWorkspaceList(opts) {
2160
2592
  const config = loadConfig();
2161
2593
  const apiKey = requireApiKey(opts, config);
@@ -2312,7 +2744,7 @@ if (typeof program.showHelpAfterError === 'function') {
2312
2744
  if (typeof program.showSuggestionAfterError === 'function') {
2313
2745
  program.showSuggestionAfterError(true);
2314
2746
  }
2315
- program.addHelpText('after', `\nExamples:\n socialseal workspace list\n socialseal workspace use <workspace-id>\n socialseal agent run --message \"ping\"\n socialseal tools list\n socialseal tools call --function <tool> --body @payload.json\n socialseal tools call --function search-journey-run --body @payload.json --async --workspace-id <uuid>\n socialseal data export-tracking --group-id 123 --time-period 30d\n`);
2747
+ program.addHelpText('after', `\nExamples:\n socialseal workspace list\n socialseal workspace use <workspace-id>\n socialseal agent run --message \"ping\"\n socialseal tools list\n socialseal tools call --function <tool> --body @payload.json\n socialseal tools call --function search-journey-run --body @payload.json --async --workspace-id <uuid>\n socialseal video queue-analysis --video-id 734829384 --workspace-id <uuid>\n socialseal video extract --video-id 734829384 --wait --out-dir ./video-assets\n socialseal data export-tracking --group-id 123 --time-period 30d\n`);
2316
2748
 
2317
2749
  program
2318
2750
  .command('agent')
@@ -2437,4 +2869,51 @@ data
2437
2869
  .option('--verbose', 'Show error details')
2438
2870
  .action((opts) => runCommand(handleDataExportReport, opts));
2439
2871
 
2872
+ const video = program.command('video').description('Tracked video extraction workflows');
2873
+
2874
+ video
2875
+ .command('queue-analysis')
2876
+ .description('Queue video analysis for tracked videos or tracked search results')
2877
+ .option('--video-id <id>', 'Tracked video id (tries video_uid first, then platform video id)')
2878
+ .option('--search-result-id <id>', 'Tracked search result id')
2879
+ .option('--video-uid <id>', 'Tracked video_uid')
2880
+ .option('--platform-video-id <id>', 'Tracked platform video id')
2881
+ .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch queueing')
2882
+ .option('--wait', 'Poll until queued/completing analyses settle')
2883
+ .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
2884
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
2885
+ .option('--api-key <key>', 'CLI API key')
2886
+ .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
2887
+ .option('--pretty', 'Pretty-print JSON')
2888
+ .option('--json', 'Emit machine-readable errors')
2889
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
2890
+ .option('--verbose', 'Show error details')
2891
+ .action((opts) => runCommand(handleVideoQueueAnalysis, opts));
2892
+
2893
+ video
2894
+ .command('extract')
2895
+ .description('Resolve tracked videos/results into structured analysis plus reference assets')
2896
+ .option('--video-id <id>', 'Tracked video id (tries video_uid first, then platform video id)')
2897
+ .option('--search-result-id <id>', 'Tracked search result id')
2898
+ .option('--video-uid <id>', 'Tracked video_uid')
2899
+ .option('--platform-video-id <id>', 'Tracked platform video id')
2900
+ .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch extraction')
2901
+ .option('--ensure-analysis', 'Queue analysis when it is missing')
2902
+ .option('--wait', 'Poll until queued/completing analyses settle')
2903
+ .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
2904
+ .option('--skip-assets', 'Skip asset URL generation')
2905
+ .option('--include-source-video', 'Include a signed source MP4 URL when available')
2906
+ .option('--frame-strategy <strategy>', 'brief_shots|quartiles')
2907
+ .option('--frame-count <n>', 'Number of still frames to return (1-5)')
2908
+ .option('--signed-url-seconds <n>', 'Signed URL TTL in seconds')
2909
+ .option('--out-dir <path>', 'Download returned assets into this local directory')
2910
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
2911
+ .option('--api-key <key>', 'CLI API key')
2912
+ .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
2913
+ .option('--pretty', 'Pretty-print JSON')
2914
+ .option('--json', 'Emit machine-readable errors')
2915
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
2916
+ .option('--verbose', 'Show error details')
2917
+ .action((opts) => runCommand(handleVideoExtract, opts));
2918
+
2440
2919
  program.parseAsync(process.argv);