@socialseal/cli 0.1.3 → 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 +10 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/src/index.js +695 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.1.4 - 2026-03-19
|
|
11
|
+
- Add explicit `group_add_item` / `group_add_items` CLI aliases for tracking-group membership workflows.
|
|
12
|
+
- Add `tracking resolve` / `get_by_value` so existing tracked searches can be resolved by value using the same duplicate-detection semantics as create.
|
|
13
|
+
- Return operational duplicate metadata for tracking conflicts, including `existing_item_id`, `member_of_group_ids`, platform, region, workspace, and active state.
|
|
14
|
+
|
|
5
15
|
## 0.1.3 - 2026-03-19
|
|
6
16
|
- Republish the current CLI release line after the successful `0.1.2` npm publish, keeping the internal and OSS package versions aligned.
|
|
7
17
|
|
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
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.' },
|
|
@@ -76,8 +87,8 @@ const KNOWN_TOOLS = [
|
|
|
76
87
|
transport: 'rest_edge_function',
|
|
77
88
|
workspaceScoped: true,
|
|
78
89
|
knownLocalDevState: 'disabled_by_default',
|
|
79
|
-
actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh', 'list_items'],
|
|
80
|
-
notes: 'REST-style surface under /groups.
|
|
90
|
+
actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh', 'list_items', 'add_item', 'group_add_item', 'add_items', 'group_add_items', 'remove_item', 'group_remove_item'],
|
|
91
|
+
notes: 'REST-style surface under /groups. `add_item`/`group_add_item` accepts an existing `item_id`; `add_items`/`group_add_items` accepts `item_ids` or item payloads for bulk membership adds.',
|
|
81
92
|
},
|
|
82
93
|
{
|
|
83
94
|
name: 'tracking',
|
|
@@ -87,8 +98,8 @@ const KNOWN_TOOLS = [
|
|
|
87
98
|
transport: 'rest_edge_function',
|
|
88
99
|
workspaceScoped: true,
|
|
89
100
|
knownLocalDevState: 'enabled',
|
|
90
|
-
actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh'],
|
|
91
|
-
notes: 'REST-style surface.
|
|
101
|
+
actionAliases: ['list', 'get', 'resolve', 'get_by_value', 'create', 'update', 'delete', 'refresh'],
|
|
102
|
+
notes: 'REST-style surface. `resolve`/`get_by_value` uses the same workspace/platform/region duplicate-detection lookup as create and returns inactive matches too.',
|
|
92
103
|
},
|
|
93
104
|
{ name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
|
|
94
105
|
{ name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
|
|
@@ -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
|
}
|
|
@@ -458,18 +736,40 @@ function normalizeTrackingPayload(payload, fallbackWorkspaceId) {
|
|
|
458
736
|
|
|
459
737
|
function normalizeGroupManagementPayload(payload, fallbackWorkspaceId) {
|
|
460
738
|
const groupId = firstDefined(payload, ['group_id', 'groupId', 'id']);
|
|
739
|
+
const itemId = firstDefined(payload, ['item_id', 'itemId']);
|
|
740
|
+
const itemIds = firstDefined(payload, ['item_ids', 'itemIds']);
|
|
741
|
+
const items = firstDefined(payload, ['items']);
|
|
461
742
|
const limit = firstDefined(payload, ['limit']);
|
|
462
743
|
const page = firstDefined(payload, ['page']);
|
|
463
744
|
return stripUndefinedEntries({
|
|
464
745
|
action: trimString(firstDefined(payload, ['action'])) || undefined,
|
|
465
746
|
workspaceId: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId),
|
|
466
747
|
group_id: coercePositiveInteger(groupId, 'group_id'),
|
|
748
|
+
item_id: coercePositiveInteger(itemId, 'item_id'),
|
|
749
|
+
item_ids: Array.isArray(itemIds)
|
|
750
|
+
? itemIds.map((value, index) => {
|
|
751
|
+
const parsed = coercePositiveInteger(value, `item_ids[${index}]`);
|
|
752
|
+
if (!parsed) {
|
|
753
|
+
throw new CliError(`Invalid item_ids[${index}]: expected a positive integer.`, {
|
|
754
|
+
code: 'INVALID_ARGUMENT',
|
|
755
|
+
exitCode: EXIT_CODES.USAGE,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return parsed;
|
|
759
|
+
})
|
|
760
|
+
: undefined,
|
|
761
|
+
items: Array.isArray(items) ? items : undefined,
|
|
467
762
|
name: trimString(firstDefined(payload, ['name'])) || undefined,
|
|
468
763
|
description: firstDefined(payload, ['description']),
|
|
469
764
|
platform: trimString(firstDefined(payload, ['platform', 'groupPlatform'])) || undefined,
|
|
470
765
|
refresh_frequency: trimString(firstDefined(payload, ['refresh_frequency', 'refreshFrequency'])) || undefined,
|
|
471
766
|
next_refresh_at: firstDefined(payload, ['next_refresh_at', 'nextRefreshAt']) ?? undefined,
|
|
472
767
|
brand_id: trimString(firstDefined(payload, ['brand_id', 'brandId'])) || undefined,
|
|
768
|
+
track_type: normalizeTrackingType(firstDefined(payload, ['track_type', 'trackType', 'type'])),
|
|
769
|
+
track_value: trimString(firstDefined(payload, ['track_value', 'trackValue', 'value'])) || undefined,
|
|
770
|
+
region: typeof firstDefined(payload, ['region']) === 'string'
|
|
771
|
+
? trimString(firstDefined(payload, ['region'])) || undefined
|
|
772
|
+
: firstDefined(payload, ['region']),
|
|
473
773
|
limit: limit !== undefined ? Number(limit) : undefined,
|
|
474
774
|
page: page !== undefined ? Number(page) : undefined,
|
|
475
775
|
});
|
|
@@ -512,6 +812,122 @@ function normalizeTrackingExportPayload(payload, fallbackWorkspaceId) {
|
|
|
512
812
|
});
|
|
513
813
|
}
|
|
514
814
|
|
|
815
|
+
function buildGroupAddPayloadFromValue(rawValue, payload, label) {
|
|
816
|
+
const value = trimString(rawValue);
|
|
817
|
+
if (!value) {
|
|
818
|
+
throw new CliError(`Invalid ${label}: expected a non-empty tracking value.`, {
|
|
819
|
+
code: 'INVALID_ARGUMENT',
|
|
820
|
+
exitCode: EXIT_CODES.USAGE,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
if (!payload.track_type) {
|
|
824
|
+
throw new CliError(`${label} requires track_type/type when using raw values.`, {
|
|
825
|
+
code: 'MISSING_ARGUMENT',
|
|
826
|
+
exitCode: EXIT_CODES.USAGE,
|
|
827
|
+
hint: 'Use type=keyword/search, hashtag, or account/creator when adding items by value.',
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return stripUndefinedEntries({
|
|
831
|
+
name: value,
|
|
832
|
+
track_type: payload.track_type,
|
|
833
|
+
track_value: value,
|
|
834
|
+
refresh_frequency: payload.refresh_frequency,
|
|
835
|
+
next_refresh_at: payload.next_refresh_at,
|
|
836
|
+
region: payload.region,
|
|
837
|
+
platform: payload.platform,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function buildGroupAddPayloadFromItem(rawItem, payload, label) {
|
|
842
|
+
if (typeof rawItem === 'number') {
|
|
843
|
+
return { item_id: coercePositiveInteger(rawItem, label) };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (typeof rawItem === 'string') {
|
|
847
|
+
const trimmed = rawItem.trim();
|
|
848
|
+
if (/^\d+$/.test(trimmed)) {
|
|
849
|
+
return { item_id: coercePositiveInteger(trimmed, label) };
|
|
850
|
+
}
|
|
851
|
+
return buildGroupAddPayloadFromValue(rawItem, payload, label);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (!rawItem || typeof rawItem !== 'object' || Array.isArray(rawItem)) {
|
|
855
|
+
throw new CliError(`Invalid ${label}: expected an item id, string value, or object payload.`, {
|
|
856
|
+
code: 'INVALID_ARGUMENT',
|
|
857
|
+
exitCode: EXIT_CODES.USAGE,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const itemId = coercePositiveInteger(firstDefined(rawItem, ['item_id', 'itemId', 'id']), `${label}.item_id`);
|
|
862
|
+
if (itemId) {
|
|
863
|
+
return { item_id: itemId };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const trackValue = trimString(firstDefined(rawItem, ['track_value', 'trackValue', 'value'])) || undefined;
|
|
867
|
+
const name = trimString(firstDefined(rawItem, ['name'])) || trackValue;
|
|
868
|
+
const trackType = normalizeTrackingType(firstDefined(rawItem, ['track_type', 'trackType', 'type'])) || payload.track_type;
|
|
869
|
+
const region = firstDefined(rawItem, ['region']) ?? payload.region;
|
|
870
|
+
const platform = trimString(firstDefined(rawItem, ['platform'])) || payload.platform;
|
|
871
|
+
const refreshFrequency = trimString(firstDefined(rawItem, ['refresh_frequency', 'refreshFrequency'])) || payload.refresh_frequency;
|
|
872
|
+
const nextRefreshAt = firstDefined(rawItem, ['next_refresh_at', 'nextRefreshAt']) ?? payload.next_refresh_at;
|
|
873
|
+
|
|
874
|
+
if (!name || !trackValue || !trackType) {
|
|
875
|
+
throw new CliError(`${label} requires item_id or name/track_value + track_type.`, {
|
|
876
|
+
code: 'MISSING_ARGUMENT',
|
|
877
|
+
exitCode: EXIT_CODES.USAGE,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return stripUndefinedEntries({
|
|
882
|
+
name,
|
|
883
|
+
track_type: trackType,
|
|
884
|
+
track_value: trackValue,
|
|
885
|
+
refresh_frequency: refreshFrequency,
|
|
886
|
+
next_refresh_at: nextRefreshAt,
|
|
887
|
+
region,
|
|
888
|
+
platform,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function buildSingleGroupAddBody(payload) {
|
|
893
|
+
if (payload.item_id) {
|
|
894
|
+
return { item_id: payload.item_id };
|
|
895
|
+
}
|
|
896
|
+
if (payload.track_value || payload.name) {
|
|
897
|
+
return buildGroupAddPayloadFromItem({
|
|
898
|
+
name: payload.name,
|
|
899
|
+
track_type: payload.track_type,
|
|
900
|
+
track_value: payload.track_value,
|
|
901
|
+
refresh_frequency: payload.refresh_frequency,
|
|
902
|
+
next_refresh_at: payload.next_refresh_at,
|
|
903
|
+
region: payload.region,
|
|
904
|
+
platform: payload.platform,
|
|
905
|
+
}, payload, 'group add item payload');
|
|
906
|
+
}
|
|
907
|
+
throw new CliError('group add_item requires item_id or a tracking payload.', {
|
|
908
|
+
code: 'MISSING_ARGUMENT',
|
|
909
|
+
exitCode: EXIT_CODES.USAGE,
|
|
910
|
+
hint: 'Provide item_id to attach an existing tracking item, or provide track_type + track_value to create/link by value.',
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function buildBulkGroupAddBody(payload) {
|
|
915
|
+
if (Array.isArray(payload.item_ids) && payload.item_ids.length > 0) {
|
|
916
|
+
return payload.item_ids.map((item_id) => ({ item_id }));
|
|
917
|
+
}
|
|
918
|
+
if (Array.isArray(payload.items) && payload.items.length > 0) {
|
|
919
|
+
return payload.items.map((item, index) => buildGroupAddPayloadFromItem(item, payload, `items[${index}]`));
|
|
920
|
+
}
|
|
921
|
+
if (payload.item_id || payload.track_value || payload.name) {
|
|
922
|
+
return [buildSingleGroupAddBody(payload)];
|
|
923
|
+
}
|
|
924
|
+
throw new CliError('group add_items requires item_ids, items, or a single item payload.', {
|
|
925
|
+
code: 'MISSING_ARGUMENT',
|
|
926
|
+
exitCode: EXIT_CODES.USAGE,
|
|
927
|
+
hint: 'Use item_ids to bulk attach existing tracking items, or use items with track_type/type for value-based adds.',
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
515
931
|
function translateTrackingAction(payload, workspaceId) {
|
|
516
932
|
const action = payload.action ? payload.action.toLowerCase() : null;
|
|
517
933
|
if (!action) {
|
|
@@ -572,6 +988,28 @@ function translateTrackingAction(payload, workspaceId) {
|
|
|
572
988
|
};
|
|
573
989
|
}
|
|
574
990
|
|
|
991
|
+
if (action === 'resolve' || action === 'item_resolve' || action === 'get_by_value' || action === 'item_get_by_value') {
|
|
992
|
+
if (!payload.track_type || !payload.track_value) {
|
|
993
|
+
throw new CliError('track_type and track_value are required for tracking resolve.', {
|
|
994
|
+
code: 'MISSING_ARGUMENT',
|
|
995
|
+
exitCode: EXIT_CODES.USAGE,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
method: 'GET',
|
|
1000
|
+
pathSuffix: buildPathWithQuery('', {
|
|
1001
|
+
workspace_id: workspaceId || undefined,
|
|
1002
|
+
resolve: 'true',
|
|
1003
|
+
track_type: payload.track_type,
|
|
1004
|
+
track_value: payload.track_value,
|
|
1005
|
+
platform: payload.platform,
|
|
1006
|
+
region: payload.region,
|
|
1007
|
+
}),
|
|
1008
|
+
body: undefined,
|
|
1009
|
+
workspaceId,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
575
1013
|
if (action === 'create' || action === 'item_create') {
|
|
576
1014
|
return {
|
|
577
1015
|
method: 'POST',
|
|
@@ -644,7 +1082,7 @@ function translateTrackingAction(payload, workspaceId) {
|
|
|
644
1082
|
throw new CliError(`Unsupported tracking action: ${payload.action}`, {
|
|
645
1083
|
code: 'INVALID_ARGUMENT',
|
|
646
1084
|
exitCode: EXIT_CODES.USAGE,
|
|
647
|
-
hint: 'Supported tracking actions: list, get, create, update, delete, refresh.',
|
|
1085
|
+
hint: 'Supported tracking actions: list, get, resolve, create, update, delete, refresh.',
|
|
648
1086
|
});
|
|
649
1087
|
}
|
|
650
1088
|
|
|
@@ -791,10 +1229,59 @@ function translateGroupManagementAction(payload, workspaceId, originalMethod) {
|
|
|
791
1229
|
};
|
|
792
1230
|
}
|
|
793
1231
|
|
|
1232
|
+
if (action === 'add_item' || action === 'group_add_item') {
|
|
1233
|
+
const groupId = coercePositiveInteger(payload.group_id, 'group_id');
|
|
1234
|
+
if (!groupId) {
|
|
1235
|
+
throw new CliError('group_id is required for group add_item.', {
|
|
1236
|
+
code: 'MISSING_ARGUMENT',
|
|
1237
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
method: 'POST',
|
|
1242
|
+
pathSuffix: buildPathWithQuery(`/groups/${groupId}/items`, { workspace_id: workspaceId || undefined }),
|
|
1243
|
+
body: buildSingleGroupAddBody(payload),
|
|
1244
|
+
workspaceId,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (action === 'add_items' || action === 'group_add_items') {
|
|
1249
|
+
const groupId = coercePositiveInteger(payload.group_id, 'group_id');
|
|
1250
|
+
if (!groupId) {
|
|
1251
|
+
throw new CliError('group_id is required for group add_items.', {
|
|
1252
|
+
code: 'MISSING_ARGUMENT',
|
|
1253
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
method: 'POST',
|
|
1258
|
+
pathSuffix: buildPathWithQuery(`/groups/${groupId}/items/bulk`, { workspace_id: workspaceId || undefined }),
|
|
1259
|
+
body: buildBulkGroupAddBody(payload),
|
|
1260
|
+
workspaceId,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (action === 'remove_item' || action === 'group_remove_item') {
|
|
1265
|
+
const groupId = coercePositiveInteger(payload.group_id, 'group_id');
|
|
1266
|
+
const itemId = coercePositiveInteger(payload.item_id, 'item_id');
|
|
1267
|
+
if (!groupId || !itemId) {
|
|
1268
|
+
throw new CliError('group_id and item_id are required for group remove_item.', {
|
|
1269
|
+
code: 'MISSING_ARGUMENT',
|
|
1270
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
method: 'DELETE',
|
|
1275
|
+
pathSuffix: buildPathWithQuery(`/groups/${groupId}/items/${itemId}`, { workspace_id: workspaceId || undefined }),
|
|
1276
|
+
body: undefined,
|
|
1277
|
+
workspaceId,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
794
1281
|
throw new CliError(`Unsupported group-management action: ${payload.action}`, {
|
|
795
1282
|
code: 'INVALID_ARGUMENT',
|
|
796
1283
|
exitCode: EXIT_CODES.USAGE,
|
|
797
|
-
hint: 'Supported group-management actions: list, get, create, update, delete, refresh, list_items.',
|
|
1284
|
+
hint: 'Supported group-management actions: list, get, create, update, delete, refresh, list_items, add_item, add_items, remove_item.',
|
|
798
1285
|
});
|
|
799
1286
|
}
|
|
800
1287
|
|
|
@@ -1947,6 +2434,160 @@ async function handleDataExportReport(opts) {
|
|
|
1947
2434
|
process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
|
|
1948
2435
|
}
|
|
1949
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
|
+
|
|
1950
2591
|
async function handleWorkspaceList(opts) {
|
|
1951
2592
|
const config = loadConfig();
|
|
1952
2593
|
const apiKey = requireApiKey(opts, config);
|
|
@@ -2103,7 +2744,7 @@ if (typeof program.showHelpAfterError === 'function') {
|
|
|
2103
2744
|
if (typeof program.showSuggestionAfterError === 'function') {
|
|
2104
2745
|
program.showSuggestionAfterError(true);
|
|
2105
2746
|
}
|
|
2106
|
-
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`);
|
|
2107
2748
|
|
|
2108
2749
|
program
|
|
2109
2750
|
.command('agent')
|
|
@@ -2228,4 +2869,51 @@ data
|
|
|
2228
2869
|
.option('--verbose', 'Show error details')
|
|
2229
2870
|
.action((opts) => runCommand(handleDataExportReport, opts));
|
|
2230
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
|
+
|
|
2231
2919
|
program.parseAsync(process.argv);
|