@socialseal/cli 0.1.4 → 0.1.6
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 +13 -0
- package/README.md +15 -3
- package/package.json +1 -1
- package/src/index.js +647 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.6 - 2026-03-19
|
|
6
|
+
- Fix runtime version reporting so `socialseal --version` reads from package metadata instead of a hardcoded source string.
|
|
7
|
+
- Fix `tracking` create request translation so `--workspace-id` is sent on the REST query path the backend uses for workspace binding.
|
|
8
|
+
- Improve tracked-video extraction failure messages by avoiding `[object Object]` item errors and returning explicit guidance when `videoId` is actually a search-result id or tracking item id.
|
|
9
|
+
- Fail fast for `group-management` and `export_tracking_data` when no workspace is selected, instead of silently relying on backend personal-workspace fallback.
|
|
10
|
+
- Warn when `tracking create` runs without a workspace and when short numeric `--video-id` values look like internal row ids.
|
|
11
|
+
- Clarify in workspace discovery output and docs that `workspace_id` and `brand_id` are different identifiers.
|
|
12
|
+
|
|
13
|
+
## 0.1.5 - 2026-03-19
|
|
14
|
+
- Add first-class tracked-video workflows with `video queue-analysis` and `video extract`.
|
|
15
|
+
- Make `--video-id` the primary ergonomic selector for tracked-video analysis and asset extraction, while keeping `--search-result-id` as a fallback selector.
|
|
16
|
+
- Support batch queueing/extraction payloads plus optional asset downloads for thumbnails, source video, and extracted key frames.
|
|
17
|
+
|
|
5
18
|
## 0.1.4 - 2026-03-19
|
|
6
19
|
- Add explicit `group_add_item` / `group_add_items` CLI aliases for tracking-group membership workflows.
|
|
7
20
|
- 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,8 +49,14 @@ 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
|
-
- `socialseal data export-tracking --group-id 123 --time-period 30d --out out.csv`
|
|
59
|
+
- `socialseal data export-tracking --group-id 123 --time-period 30d --workspace-id <uuid> --out out.csv`
|
|
54
60
|
- `socialseal data export-report --report-type keyword_universe --format csv --payload @payload.json --out out.csv`
|
|
55
61
|
|
|
56
62
|
## Notes
|
|
@@ -58,10 +64,16 @@ 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. It does not accept tracking item ids. `--search-result-id` remains available when you are starting from a specific tracked rank row.
|
|
70
|
+
- `group-management` and `export_tracking_data` now fail fast when no workspace is selected, instead of letting the backend silently fall back to the personal workspace.
|
|
71
|
+
- `tracking create` without a workspace now prints a warning that the backend may create a personal/null-scope item.
|
|
72
|
+
- Short numeric `--video-id` inputs now print a warning that they may be internal row ids and that `--search-result-id` is often the intended selector.
|
|
61
73
|
- `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
|
-
- Effective workspace precedence is: `--workspace-id` → `SOCIALSEAL_WORKSPACE_ID` → config `workspaceId`
|
|
74
|
+
- Effective workspace precedence is: `--workspace-id` → `SOCIALSEAL_WORKSPACE_ID` → config `workspaceId`. For commands that are easy to misuse (`group-management`, `export_tracking_data`, tracked-video workflows), the CLI now requires an explicit or preconfigured workspace instead of relying on backend fallback.
|
|
63
75
|
- `socialseal workspace use ...` writes a local default workspace into `~/.config/socialseal/config.json`, which the CLI reuses for `agent`, `tools`, and `data` commands.
|
|
64
|
-
- `socialseal workspace list` discovers the workspaces accessible to the current CLI key
|
|
76
|
+
- `socialseal workspace list` discovers the workspaces accessible to the current CLI key, marks the active/suggested default, and reminds you that `workspace_id` and `brand_id` are different identifiers.
|
|
65
77
|
- If a scoped CLI key cannot safely infer a workspace, `agent run` now fails closed and tells you to set `--workspace-id` or configure a local default first.
|
|
66
78
|
|
|
67
79
|
## Errors and exit codes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -13,8 +13,10 @@ 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';
|
|
19
|
+
const CLI_VERSION = loadRuntimeVersion();
|
|
18
20
|
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.';
|
|
19
21
|
const EXIT_CODES = {
|
|
20
22
|
OK: 0,
|
|
@@ -60,7 +62,17 @@ const KNOWN_TOOLS = [
|
|
|
60
62
|
transport: 'post_edge_function',
|
|
61
63
|
workspaceScoped: true,
|
|
62
64
|
knownLocalDevState: 'disabled_by_default',
|
|
63
|
-
notes: 'group_id expects a numeric tracking_group id, not a brand_group UUID.',
|
|
65
|
+
notes: 'group_id expects a numeric tracking_group id, not a brand_group UUID. Always pass a workspace id or configure a default workspace so the export does not silently target the personal workspace.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'tracked-video-extract',
|
|
69
|
+
category: 'video',
|
|
70
|
+
description: 'Resolve tracked videos/results into structured analysis plus reference assets.',
|
|
71
|
+
objectType: 'tracked_video_extract',
|
|
72
|
+
transport: 'post_edge_function',
|
|
73
|
+
workspaceScoped: true,
|
|
74
|
+
knownLocalDevState: 'enabled',
|
|
75
|
+
notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; videoId means video_uid or platform-native video id, not a tracking item id.',
|
|
64
76
|
},
|
|
65
77
|
{ name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
|
|
66
78
|
{ name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
|
|
@@ -77,7 +89,7 @@ const KNOWN_TOOLS = [
|
|
|
77
89
|
workspaceScoped: true,
|
|
78
90
|
knownLocalDevState: 'disabled_by_default',
|
|
79
91
|
actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh', 'list_items', 'add_item', 'group_add_item', 'add_items', 'group_add_items', 'remove_item', 'group_remove_item'],
|
|
80
|
-
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.',
|
|
92
|
+
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. Always pass a workspace id or configure a default workspace so the backend does not fall back to the personal workspace.',
|
|
81
93
|
},
|
|
82
94
|
{
|
|
83
95
|
name: 'tracking',
|
|
@@ -112,6 +124,26 @@ function getConfigPath() {
|
|
|
112
124
|
return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
function loadRuntimeVersion() {
|
|
128
|
+
const envVersion = typeof process.env.npm_package_version === 'string'
|
|
129
|
+
? process.env.npm_package_version.trim()
|
|
130
|
+
: '';
|
|
131
|
+
if (envVersion) return envVersion;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const packageJsonPath = new URL('../package.json', import.meta.url);
|
|
135
|
+
const raw = fs.readFileSync(packageJsonPath, 'utf8');
|
|
136
|
+
const parsed = JSON.parse(raw);
|
|
137
|
+
if (typeof parsed?.version === 'string' && parsed.version.trim().length > 0) {
|
|
138
|
+
return parsed.version.trim();
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// fall through to the safe fallback below
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return '0.0.0';
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
function loadConfig() {
|
|
116
148
|
const configPath = getConfigPath();
|
|
117
149
|
try {
|
|
@@ -339,6 +371,274 @@ function mergeWorkspaceIdIntoPayload(payload, workspaceId) {
|
|
|
339
371
|
return { ...payload, workspaceId };
|
|
340
372
|
}
|
|
341
373
|
|
|
374
|
+
function sanitizeFileComponent(value, fallback = 'item') {
|
|
375
|
+
const normalized = String(value || '')
|
|
376
|
+
.trim()
|
|
377
|
+
.replace(/[^a-z0-9._-]+/gi, '-')
|
|
378
|
+
.replace(/-+/g, '-')
|
|
379
|
+
.replace(/^-|-$/g, '');
|
|
380
|
+
return normalized || fallback;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function inferExtension(urlValue, contentType, fallback = '.bin') {
|
|
384
|
+
const normalizedType = String(contentType || '').toLowerCase();
|
|
385
|
+
if (normalizedType.includes('jpeg')) return '.jpg';
|
|
386
|
+
if (normalizedType.includes('png')) return '.png';
|
|
387
|
+
if (normalizedType.includes('webp')) return '.webp';
|
|
388
|
+
if (normalizedType.includes('gif')) return '.gif';
|
|
389
|
+
if (normalizedType.includes('mp4')) return '.mp4';
|
|
390
|
+
if (normalizedType.includes('quicktime')) return '.mov';
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const pathname = new URL(urlValue).pathname || '';
|
|
394
|
+
const ext = path.extname(pathname);
|
|
395
|
+
if (ext) return ext;
|
|
396
|
+
} catch {
|
|
397
|
+
// ignore parse failures
|
|
398
|
+
}
|
|
399
|
+
return fallback;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeVideoExtractBody(body) {
|
|
403
|
+
const normalized = { ...body };
|
|
404
|
+
const hasInlineIdentifier =
|
|
405
|
+
normalized.videoId !== undefined ||
|
|
406
|
+
normalized.searchResultId !== undefined ||
|
|
407
|
+
normalized.videoUid !== undefined ||
|
|
408
|
+
normalized.platformVideoId !== undefined;
|
|
409
|
+
|
|
410
|
+
if (!Array.isArray(normalized.items) && hasInlineIdentifier) {
|
|
411
|
+
normalized.items = [{
|
|
412
|
+
videoId: normalized.videoId,
|
|
413
|
+
searchResultId: normalized.searchResultId,
|
|
414
|
+
videoUid: normalized.videoUid,
|
|
415
|
+
platformVideoId: normalized.platformVideoId,
|
|
416
|
+
platformId: normalized.platformId,
|
|
417
|
+
}];
|
|
418
|
+
delete normalized.videoId;
|
|
419
|
+
delete normalized.searchResultId;
|
|
420
|
+
delete normalized.videoUid;
|
|
421
|
+
delete normalized.platformVideoId;
|
|
422
|
+
delete normalized.platformId;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return normalized;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildVideoExtractBody(opts, workspaceId) {
|
|
429
|
+
const parsed = opts.body
|
|
430
|
+
? ensureJsonObject(parseJsonInput(opts.body, { label: 'body' }), 'body')
|
|
431
|
+
: {};
|
|
432
|
+
const normalized = normalizeVideoExtractBody(parsed);
|
|
433
|
+
|
|
434
|
+
if (!Array.isArray(normalized.items) || normalized.items.length === 0) {
|
|
435
|
+
const inlineItem = stripUndefinedEntries({
|
|
436
|
+
videoId: trimString(opts.videoId) || undefined,
|
|
437
|
+
searchResultId: opts.searchResultId !== undefined
|
|
438
|
+
? coercePositiveInteger(opts.searchResultId, 'searchResultId')
|
|
439
|
+
: undefined,
|
|
440
|
+
videoUid: trimString(opts.videoUid) || undefined,
|
|
441
|
+
platformVideoId: trimString(opts.platformVideoId) || undefined,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (Object.keys(inlineItem).length === 0) {
|
|
445
|
+
throw new CliError('Provide --body or one of --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
|
|
446
|
+
code: 'MISSING_ARGUMENT',
|
|
447
|
+
exitCode: EXIT_CODES.USAGE,
|
|
448
|
+
hint: '--video-id accepts a video_uid or platform video id. It does not accept tracking item ids.',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
normalized.items = [inlineItem];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const bodyWithWorkspace = mergeWorkspaceIdIntoPayload(normalized, workspaceId);
|
|
456
|
+
if (!bodyWithWorkspace.workspaceId) {
|
|
457
|
+
throw new CliError('tracked-video extraction requires a workspace id.', {
|
|
458
|
+
code: 'WORKSPACE_REQUIRED',
|
|
459
|
+
exitCode: EXIT_CODES.USAGE,
|
|
460
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const nextBody = { ...bodyWithWorkspace };
|
|
465
|
+
if (opts.wait) {
|
|
466
|
+
nextBody.ensureAnalysis = true;
|
|
467
|
+
} else if (opts.ensureAnalysis === true) {
|
|
468
|
+
nextBody.ensureAnalysis = true;
|
|
469
|
+
} else if (nextBody.ensureAnalysis === undefined) {
|
|
470
|
+
nextBody.ensureAnalysis = false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (opts.skipAssets === true) {
|
|
474
|
+
nextBody.includeAssets = false;
|
|
475
|
+
} else if (nextBody.includeAssets === undefined) {
|
|
476
|
+
nextBody.includeAssets = true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (opts.includeSourceVideo === true) {
|
|
480
|
+
nextBody.includeSourceVideo = true;
|
|
481
|
+
} else if (nextBody.includeSourceVideo === undefined) {
|
|
482
|
+
nextBody.includeSourceVideo = false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (opts.frameStrategy && nextBody.frameStrategy === undefined) {
|
|
486
|
+
nextBody.frameStrategy = opts.frameStrategy;
|
|
487
|
+
}
|
|
488
|
+
if (nextBody.frameStrategy === undefined) {
|
|
489
|
+
nextBody.frameStrategy = 'brief_shots';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (nextBody.frameCount === undefined) {
|
|
493
|
+
nextBody.frameCount = opts.frameCount !== undefined
|
|
494
|
+
? Number(opts.frameCount)
|
|
495
|
+
: DEFAULT_FRAME_COUNT;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (nextBody.signedUrlSeconds === undefined) {
|
|
499
|
+
nextBody.signedUrlSeconds = opts.signedUrlSeconds !== undefined
|
|
500
|
+
? Number(opts.signedUrlSeconds)
|
|
501
|
+
: 3600;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return nextBody;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildVideoQueueBody(opts, workspaceId) {
|
|
508
|
+
const body = buildVideoExtractBody(opts, workspaceId);
|
|
509
|
+
return {
|
|
510
|
+
...body,
|
|
511
|
+
ensureAnalysis: true,
|
|
512
|
+
queueOnly: true,
|
|
513
|
+
includeAssets: false,
|
|
514
|
+
includeRawAnalysis: false,
|
|
515
|
+
includeSourceVideo: false,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function hasPendingVideoExtractResults(payload) {
|
|
520
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
521
|
+
return results.some((result) => {
|
|
522
|
+
const status = String(result?.analysis?.status || '').trim().toLowerCase();
|
|
523
|
+
return status === 'pending' || status === 'processing';
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function downloadAssetToFile({ url, outDir, stem, timeoutMs }) {
|
|
528
|
+
const response = await fetchWithTimeout(url, {
|
|
529
|
+
method: 'GET',
|
|
530
|
+
headers: { Accept: '*/*' },
|
|
531
|
+
}, timeoutMs);
|
|
532
|
+
|
|
533
|
+
if (!response.ok) {
|
|
534
|
+
throw new CliError(`Asset download failed: ${response.status}`, {
|
|
535
|
+
code: 'ASSET_DOWNLOAD_FAILED',
|
|
536
|
+
exitCode: EXIT_CODES.SERVER,
|
|
537
|
+
details: await response.text().catch(() => null),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const extension = inferExtension(url, response.headers.get('content-type'));
|
|
542
|
+
const absolutePath = path.resolve(outDir, `${stem}${extension}`);
|
|
543
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
544
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
545
|
+
fs.writeFileSync(absolutePath, buffer);
|
|
546
|
+
return {
|
|
547
|
+
path: absolutePath,
|
|
548
|
+
bytes: buffer.length,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function downloadVideoExtractAssets(payload, outDir, timeoutMs) {
|
|
553
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
554
|
+
const absoluteBaseDir = path.resolve(outDir);
|
|
555
|
+
fs.mkdirSync(absoluteBaseDir, { recursive: true });
|
|
556
|
+
|
|
557
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
558
|
+
const result = results[index];
|
|
559
|
+
const resolved = result?.resolved || {};
|
|
560
|
+
const videoKey = sanitizeFileComponent(
|
|
561
|
+
resolved.videoUid || resolved.platformVideoId || resolved.searchResultId || `item-${index + 1}`,
|
|
562
|
+
`item-${index + 1}`,
|
|
563
|
+
);
|
|
564
|
+
const itemDir = path.join(absoluteBaseDir, `${String(index + 1).padStart(2, '0')}-${videoKey}`);
|
|
565
|
+
const downloads = {
|
|
566
|
+
directory: itemDir,
|
|
567
|
+
thumbnail: null,
|
|
568
|
+
sourceVideo: null,
|
|
569
|
+
frames: [],
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const thumbnailUrl = result?.assets?.thumbnail?.url;
|
|
573
|
+
if (typeof thumbnailUrl === 'string' && thumbnailUrl.length > 0) {
|
|
574
|
+
try {
|
|
575
|
+
downloads.thumbnail = await downloadAssetToFile({
|
|
576
|
+
url: thumbnailUrl,
|
|
577
|
+
outDir: itemDir,
|
|
578
|
+
stem: 'thumbnail',
|
|
579
|
+
timeoutMs,
|
|
580
|
+
});
|
|
581
|
+
} catch (error) {
|
|
582
|
+
downloads.thumbnail = {
|
|
583
|
+
error: error instanceof Error ? error.message : String(error),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const sourceVideoUrl = result?.assets?.sourceVideo?.signedUrl;
|
|
589
|
+
if (typeof sourceVideoUrl === 'string' && sourceVideoUrl.length > 0) {
|
|
590
|
+
try {
|
|
591
|
+
downloads.sourceVideo = await downloadAssetToFile({
|
|
592
|
+
url: sourceVideoUrl,
|
|
593
|
+
outDir: itemDir,
|
|
594
|
+
stem: 'source-video',
|
|
595
|
+
timeoutMs,
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
downloads.sourceVideo = {
|
|
599
|
+
error: error instanceof Error ? error.message : String(error),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const frames = Array.isArray(result?.assets?.frames) ? result.assets.frames : [];
|
|
605
|
+
for (let frameIndex = 0; frameIndex < frames.length; frameIndex += 1) {
|
|
606
|
+
const frame = frames[frameIndex];
|
|
607
|
+
const frameUrl = frame?.signedUrl;
|
|
608
|
+
if (typeof frameUrl !== 'string' || frameUrl.length === 0) continue;
|
|
609
|
+
const timestampToken = sanitizeFileComponent(frame?.timestamp || `frame-${frameIndex + 1}`, `frame-${frameIndex + 1}`);
|
|
610
|
+
try {
|
|
611
|
+
const download = await downloadAssetToFile({
|
|
612
|
+
url: frameUrl,
|
|
613
|
+
outDir: itemDir,
|
|
614
|
+
stem: `${frame?.kind || 'frame'}-${String(frameIndex + 1).padStart(2, '0')}-${timestampToken}`,
|
|
615
|
+
timeoutMs,
|
|
616
|
+
});
|
|
617
|
+
downloads.frames.push({
|
|
618
|
+
kind: frame?.kind || null,
|
|
619
|
+
timestamp: frame?.timestamp || null,
|
|
620
|
+
path: download.path,
|
|
621
|
+
bytes: download.bytes,
|
|
622
|
+
});
|
|
623
|
+
} catch (error) {
|
|
624
|
+
downloads.frames.push({
|
|
625
|
+
kind: frame?.kind || null,
|
|
626
|
+
timestamp: frame?.timestamp || null,
|
|
627
|
+
error: error instanceof Error ? error.message : String(error),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
result.downloads = downloads;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
...payload,
|
|
637
|
+
downloadsRoot: absoluteBaseDir,
|
|
638
|
+
results,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
342
642
|
function hasOwn(value, key) {
|
|
343
643
|
return Boolean(value) && Object.prototype.hasOwnProperty.call(value, key);
|
|
344
644
|
}
|
|
@@ -655,7 +955,7 @@ function translateTrackingAction(payload, workspaceId) {
|
|
|
655
955
|
if (!action) {
|
|
656
956
|
return {
|
|
657
957
|
method: 'POST',
|
|
658
|
-
pathSuffix: '',
|
|
958
|
+
pathSuffix: buildPathWithQuery('', { workspace_id: workspaceId || undefined }),
|
|
659
959
|
body: stripUndefinedEntries({
|
|
660
960
|
name: payload.name,
|
|
661
961
|
track_type: payload.track_type,
|
|
@@ -735,7 +1035,7 @@ function translateTrackingAction(payload, workspaceId) {
|
|
|
735
1035
|
if (action === 'create' || action === 'item_create') {
|
|
736
1036
|
return {
|
|
737
1037
|
method: 'POST',
|
|
738
|
-
pathSuffix: '',
|
|
1038
|
+
pathSuffix: buildPathWithQuery('', { workspace_id: workspaceId || undefined }),
|
|
739
1039
|
body: stripUndefinedEntries({
|
|
740
1040
|
name: payload.name,
|
|
741
1041
|
track_type: payload.track_type,
|
|
@@ -1202,6 +1502,61 @@ function emitWorkspaceContext(opts, { workspaceId, source, functionName, method
|
|
|
1202
1502
|
);
|
|
1203
1503
|
}
|
|
1204
1504
|
|
|
1505
|
+
function describeWorkspaceSource(source) {
|
|
1506
|
+
switch (source) {
|
|
1507
|
+
case 'flag':
|
|
1508
|
+
return '--workspace-id';
|
|
1509
|
+
case 'env':
|
|
1510
|
+
return 'SOCIALSEAL_WORKSPACE_ID';
|
|
1511
|
+
case 'config':
|
|
1512
|
+
return 'the saved default workspace';
|
|
1513
|
+
case 'body':
|
|
1514
|
+
return 'the request body';
|
|
1515
|
+
default:
|
|
1516
|
+
return 'implicit selection';
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function emitWorkspaceSelectionNotice(opts, { workspaceId, source, label }) {
|
|
1521
|
+
if (!workspaceId || !source || source === 'flag' || source === 'body') return;
|
|
1522
|
+
process.stderr.write(
|
|
1523
|
+
`[socialseal] Using workspace ${workspaceId} from ${describeWorkspaceSource(source)} for ${label}. Pass --workspace-id to override.\n`,
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function requireWorkspaceSelection(workspaceId, { label, hint }) {
|
|
1528
|
+
if (workspaceId) return workspaceId;
|
|
1529
|
+
throw new CliError(`${label} requires a workspace id.`, {
|
|
1530
|
+
code: 'WORKSPACE_REQUIRED',
|
|
1531
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1532
|
+
hint,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function emitTrackingCreateScopeWarning(action, workspaceId) {
|
|
1537
|
+
if (action !== 'create' || workspaceId) return;
|
|
1538
|
+
process.stderr.write(
|
|
1539
|
+
'[socialseal] tracking create is running without a workspace id. The backend may create a personal/null-scope item that is not attached to a workspace or group.\n',
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function looksLikeShortNumericVideoId(value) {
|
|
1544
|
+
return typeof value === 'string' && /^\d{1,7}$/.test(value.trim());
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function emitAmbiguousVideoIdWarnings(items) {
|
|
1548
|
+
const references = Array.isArray(items) ? items : [];
|
|
1549
|
+
for (const item of references) {
|
|
1550
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
1551
|
+
if (looksLikeShortNumericVideoId(item.videoId)) {
|
|
1552
|
+
process.stderr.write(
|
|
1553
|
+
`[socialseal] videoId "${item.videoId}" looks like a short internal row id. If you meant a ranked result row, use --search-result-id. If you meant a tracking item id, resolve it first and retry with --video-uid or --platform-video-id.\n`,
|
|
1554
|
+
);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1205
1560
|
function sleep(ms) {
|
|
1206
1561
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1207
1562
|
}
|
|
@@ -1351,7 +1706,7 @@ function buildStatusHint(status, context = {}) {
|
|
|
1351
1706
|
case 404:
|
|
1352
1707
|
if (context.functionName) {
|
|
1353
1708
|
if (isLocallyDisabledByDefaultFunction(context.functionName)) {
|
|
1354
|
-
return `Unknown function "${context.functionName}".
|
|
1709
|
+
return `Unknown function "${context.functionName}". The CLI ships a static registry, but availability depends on the backend you are calling. Verify the tool is deployed on the current API base; for local direct Supabase calls, enable it in supabase/config.toml.`;
|
|
1355
1710
|
}
|
|
1356
1711
|
return `Unknown function "${context.functionName}". Double-check the name and API base.`;
|
|
1357
1712
|
}
|
|
@@ -1919,14 +2274,64 @@ async function handleToolsCall(opts) {
|
|
|
1919
2274
|
resolvedWorkspaceId,
|
|
1920
2275
|
});
|
|
1921
2276
|
const method = normalizeMethod(translated.method);
|
|
1922
|
-
const
|
|
2277
|
+
const payloadWorkspaceId = isJsonObject(translated.normalizedPayload)
|
|
2278
|
+
? resolvePayloadWorkspaceId(translated.normalizedPayload, resolvedWorkspaceId)
|
|
2279
|
+
: (isJsonObject(translated.body)
|
|
2280
|
+
? resolvePayloadWorkspaceId(translated.body, resolvedWorkspaceId)
|
|
2281
|
+
: (resolvedWorkspaceId ?? null));
|
|
2282
|
+
const effectiveWorkspaceId = translated.workspaceId ?? payloadWorkspaceId ?? null;
|
|
2283
|
+
const effectiveWorkspaceSource =
|
|
2284
|
+
translated.workspaceId && translated.workspaceId !== resolvedWorkspaceId
|
|
2285
|
+
? 'body'
|
|
2286
|
+
: (payloadWorkspaceId && payloadWorkspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource);
|
|
1923
2287
|
const path = useGateway
|
|
1924
2288
|
? `/cli/tools/${opts.function}${translated.pathSuffix || ''}`
|
|
1925
2289
|
: `/functions/v1/${opts.function}${translated.pathSuffix || ''}`;
|
|
1926
2290
|
|
|
2291
|
+
if (opts.function === 'group-management') {
|
|
2292
|
+
requireWorkspaceSelection(effectiveWorkspaceId, {
|
|
2293
|
+
label: 'group-management',
|
|
2294
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace. Omitting workspace lets the backend fall back to the personal workspace.',
|
|
2295
|
+
});
|
|
2296
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2297
|
+
workspaceId: effectiveWorkspaceId,
|
|
2298
|
+
source: effectiveWorkspaceSource,
|
|
2299
|
+
label: 'group-management',
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (opts.function === 'export_tracking_data') {
|
|
2304
|
+
requireWorkspaceSelection(effectiveWorkspaceId, {
|
|
2305
|
+
label: 'export_tracking_data',
|
|
2306
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting tracking data.',
|
|
2307
|
+
});
|
|
2308
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2309
|
+
workspaceId: effectiveWorkspaceId,
|
|
2310
|
+
source: effectiveWorkspaceSource,
|
|
2311
|
+
label: 'export_tracking_data',
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
if (opts.function === 'tracked-video-extract') {
|
|
2316
|
+
requireWorkspaceSelection(effectiveWorkspaceId, {
|
|
2317
|
+
label: 'tracked-video-extract',
|
|
2318
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
|
|
2319
|
+
});
|
|
2320
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2321
|
+
workspaceId: effectiveWorkspaceId,
|
|
2322
|
+
source: effectiveWorkspaceSource,
|
|
2323
|
+
label: 'tracked-video-extract',
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
emitTrackingCreateScopeWarning(
|
|
2328
|
+
isJsonObject(translated.normalizedPayload) ? trimString(translated.normalizedPayload.action).toLowerCase() : '',
|
|
2329
|
+
effectiveWorkspaceId,
|
|
2330
|
+
);
|
|
2331
|
+
|
|
1927
2332
|
emitWorkspaceContext(opts, {
|
|
1928
2333
|
workspaceId: effectiveWorkspaceId,
|
|
1929
|
-
source:
|
|
2334
|
+
source: effectiveWorkspaceSource,
|
|
1930
2335
|
functionName: opts.function,
|
|
1931
2336
|
method,
|
|
1932
2337
|
});
|
|
@@ -2041,7 +2446,7 @@ async function handleDataExportTracking(opts) {
|
|
|
2041
2446
|
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
2042
2447
|
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
2043
2448
|
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
2044
|
-
const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
|
|
2449
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
2045
2450
|
|
|
2046
2451
|
if (!opts.groupId && !opts.itemId) {
|
|
2047
2452
|
throw new CliError('Provide --group-id or --item-id.', {
|
|
@@ -2063,6 +2468,16 @@ async function handleDataExportTracking(opts) {
|
|
|
2063
2468
|
time_period: opts.timePeriod,
|
|
2064
2469
|
};
|
|
2065
2470
|
|
|
2471
|
+
requireWorkspaceSelection(resolvedWorkspaceId, {
|
|
2472
|
+
label: 'Tracking export',
|
|
2473
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting tracking data.',
|
|
2474
|
+
});
|
|
2475
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2476
|
+
workspaceId: resolvedWorkspaceId,
|
|
2477
|
+
source: workspaceSource,
|
|
2478
|
+
label: 'tracking export',
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2066
2481
|
const res = await callApi({
|
|
2067
2482
|
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
2068
2483
|
apiKey,
|
|
@@ -2156,6 +2571,178 @@ async function handleDataExportReport(opts) {
|
|
|
2156
2571
|
process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
|
|
2157
2572
|
}
|
|
2158
2573
|
|
|
2574
|
+
async function handleVideoExtract(opts) {
|
|
2575
|
+
const config = loadConfig();
|
|
2576
|
+
const apiKey = requireApiKey(opts, config);
|
|
2577
|
+
const apiBase = resolveApiBase(opts, config);
|
|
2578
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
2579
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
2580
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
2581
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
2582
|
+
|
|
2583
|
+
const body = buildVideoExtractBody(opts, resolvedWorkspaceId);
|
|
2584
|
+
const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
|
|
2585
|
+
const effectiveWorkspaceId = body.workspaceId || resolvedWorkspaceId;
|
|
2586
|
+
const effectiveWorkspaceSource = body.workspaceId && body.workspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource;
|
|
2587
|
+
|
|
2588
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2589
|
+
workspaceId: effectiveWorkspaceId,
|
|
2590
|
+
source: effectiveWorkspaceSource,
|
|
2591
|
+
label: 'tracked-video extract',
|
|
2592
|
+
});
|
|
2593
|
+
emitAmbiguousVideoIdWarnings(body.items);
|
|
2594
|
+
|
|
2595
|
+
emitWorkspaceContext(opts, {
|
|
2596
|
+
workspaceId: effectiveWorkspaceId,
|
|
2597
|
+
source: effectiveWorkspaceSource,
|
|
2598
|
+
functionName: 'tracked-video-extract',
|
|
2599
|
+
method: 'POST',
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
const requestOnce = async (remainingTimeoutMs) => {
|
|
2603
|
+
const res = await callApi({
|
|
2604
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
2605
|
+
apiKey,
|
|
2606
|
+
path,
|
|
2607
|
+
method: 'POST',
|
|
2608
|
+
body,
|
|
2609
|
+
workspaceId: effectiveWorkspaceId,
|
|
2610
|
+
timeoutMs: remainingTimeoutMs,
|
|
2611
|
+
});
|
|
2612
|
+
|
|
2613
|
+
if (!res.ok) {
|
|
2614
|
+
throw await buildHttpError(res, {
|
|
2615
|
+
label: 'Tracked video extract',
|
|
2616
|
+
functionName: 'tracked-video-extract',
|
|
2617
|
+
method: 'POST',
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2622
|
+
if (!contentType.includes('application/json')) {
|
|
2623
|
+
throw new CliError('tracked-video-extract returned a non-JSON response.', {
|
|
2624
|
+
code: 'INVALID_RESPONSE',
|
|
2625
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
return await res.json();
|
|
2630
|
+
};
|
|
2631
|
+
|
|
2632
|
+
let payload = await requestOnce(timeoutMs);
|
|
2633
|
+
|
|
2634
|
+
if (opts.wait) {
|
|
2635
|
+
const pollIntervalMs = resolvePollIntervalMs(opts);
|
|
2636
|
+
const deadline = Date.now() + timeoutMs;
|
|
2637
|
+
|
|
2638
|
+
while (hasPendingVideoExtractResults(payload)) {
|
|
2639
|
+
const remainingMs = deadline - Date.now();
|
|
2640
|
+
if (remainingMs <= 0) {
|
|
2641
|
+
throw new CliError('Timed out waiting for tracked video analysis completion.', {
|
|
2642
|
+
code: 'ASYNC_WAIT_TIMEOUT',
|
|
2643
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2644
|
+
hint: 'Increase --timeout <ms> or omit --wait to return the current status immediately.',
|
|
2645
|
+
details: truncateDetails(payload),
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
emitInfo(opts, 'tracked-video-extract pending; polling for completion.');
|
|
2650
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
2651
|
+
payload = await requestOnce(Math.max(1000, deadline - Date.now()));
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
if (opts.outDir) {
|
|
2656
|
+
payload = await downloadVideoExtractAssets(payload, opts.outDir, timeoutMs);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
emitJsonOutput(payload, opts.pretty);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
async function handleVideoQueueAnalysis(opts) {
|
|
2663
|
+
const config = loadConfig();
|
|
2664
|
+
const apiKey = requireApiKey(opts, config);
|
|
2665
|
+
const apiBase = resolveApiBase(opts, config);
|
|
2666
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
2667
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
2668
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
2669
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
2670
|
+
|
|
2671
|
+
const body = buildVideoQueueBody(opts, resolvedWorkspaceId);
|
|
2672
|
+
const effectiveWorkspaceId = body.workspaceId || resolvedWorkspaceId;
|
|
2673
|
+
const effectiveWorkspaceSource = body.workspaceId && body.workspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource;
|
|
2674
|
+
|
|
2675
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
2676
|
+
workspaceId: effectiveWorkspaceId,
|
|
2677
|
+
source: effectiveWorkspaceSource,
|
|
2678
|
+
label: 'tracked-video queue-analysis',
|
|
2679
|
+
});
|
|
2680
|
+
emitAmbiguousVideoIdWarnings(body.items);
|
|
2681
|
+
const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
|
|
2682
|
+
|
|
2683
|
+
emitWorkspaceContext(opts, {
|
|
2684
|
+
workspaceId: effectiveWorkspaceId,
|
|
2685
|
+
source: effectiveWorkspaceSource,
|
|
2686
|
+
functionName: 'tracked-video-extract',
|
|
2687
|
+
method: 'POST',
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2690
|
+
const requestOnce = async (remainingTimeoutMs) => {
|
|
2691
|
+
const res = await callApi({
|
|
2692
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
2693
|
+
apiKey,
|
|
2694
|
+
path,
|
|
2695
|
+
method: 'POST',
|
|
2696
|
+
body,
|
|
2697
|
+
workspaceId: effectiveWorkspaceId,
|
|
2698
|
+
timeoutMs: remainingTimeoutMs,
|
|
2699
|
+
});
|
|
2700
|
+
|
|
2701
|
+
if (!res.ok) {
|
|
2702
|
+
throw await buildHttpError(res, {
|
|
2703
|
+
label: 'Tracked video queue-analysis',
|
|
2704
|
+
functionName: 'tracked-video-extract',
|
|
2705
|
+
method: 'POST',
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2710
|
+
if (!contentType.includes('application/json')) {
|
|
2711
|
+
throw new CliError('tracked-video-extract returned a non-JSON response.', {
|
|
2712
|
+
code: 'INVALID_RESPONSE',
|
|
2713
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
return await res.json();
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
let payload = await requestOnce(timeoutMs);
|
|
2721
|
+
|
|
2722
|
+
if (opts.wait) {
|
|
2723
|
+
const pollIntervalMs = resolvePollIntervalMs(opts);
|
|
2724
|
+
const deadline = Date.now() + timeoutMs;
|
|
2725
|
+
|
|
2726
|
+
while (hasPendingVideoExtractResults(payload)) {
|
|
2727
|
+
const remainingMs = deadline - Date.now();
|
|
2728
|
+
if (remainingMs <= 0) {
|
|
2729
|
+
throw new CliError('Timed out waiting for queued video analysis completion.', {
|
|
2730
|
+
code: 'ASYNC_WAIT_TIMEOUT',
|
|
2731
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2732
|
+
hint: 'Increase --timeout <ms> or omit --wait to return queue status immediately.',
|
|
2733
|
+
details: truncateDetails(payload),
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
emitInfo(opts, 'tracked-video queue-analysis pending; polling for completion.');
|
|
2738
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
2739
|
+
payload = await requestOnce(Math.max(1000, deadline - Date.now()));
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
emitJsonOutput(payload, opts.pretty);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2159
2746
|
async function handleWorkspaceList(opts) {
|
|
2160
2747
|
const config = loadConfig();
|
|
2161
2748
|
const apiKey = requireApiKey(opts, config);
|
|
@@ -2192,6 +2779,8 @@ async function handleWorkspaceList(opts) {
|
|
|
2192
2779
|
process.stdout.write(`${formatWorkspaceLine(workspace, { isEffective, source: selection.source, isSuggested })}\n`);
|
|
2193
2780
|
}
|
|
2194
2781
|
|
|
2782
|
+
process.stdout.write('\n[socialseal] Note: workspace ids are not brand ids. When a payload includes both workspace_id and brand_id, pass the workspace id to --workspace-id.\n');
|
|
2783
|
+
|
|
2195
2784
|
if (!selection.workspaceId && directory.defaultWorkspaceId) {
|
|
2196
2785
|
process.stdout.write('\n[socialseal] No local default is configured. Set one with: socialseal workspace use <id>\n');
|
|
2197
2786
|
}
|
|
@@ -2236,6 +2825,7 @@ async function handleWorkspaceCurrent(opts) {
|
|
|
2236
2825
|
|
|
2237
2826
|
if (effectiveWorkspace) {
|
|
2238
2827
|
process.stdout.write(`[socialseal] Effective workspace: ${effectiveWorkspace.name} (${effectiveWorkspace.id}) via ${selection.source}\n`);
|
|
2828
|
+
process.stdout.write('[socialseal] Note: workspace ids are not brand ids. Use the workspace id, not brand_id, with --workspace-id.\n');
|
|
2239
2829
|
return;
|
|
2240
2830
|
}
|
|
2241
2831
|
|
|
@@ -2304,7 +2894,7 @@ const program = new Command();
|
|
|
2304
2894
|
program
|
|
2305
2895
|
.name('socialseal')
|
|
2306
2896
|
.description('SocialSeal CLI (non-interactive)')
|
|
2307
|
-
.version(
|
|
2897
|
+
.version(CLI_VERSION);
|
|
2308
2898
|
|
|
2309
2899
|
if (typeof program.showHelpAfterError === 'function') {
|
|
2310
2900
|
program.showHelpAfterError(true);
|
|
@@ -2312,7 +2902,7 @@ if (typeof program.showHelpAfterError === 'function') {
|
|
|
2312
2902
|
if (typeof program.showSuggestionAfterError === 'function') {
|
|
2313
2903
|
program.showSuggestionAfterError(true);
|
|
2314
2904
|
}
|
|
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`);
|
|
2905
|
+
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
2906
|
|
|
2317
2907
|
program
|
|
2318
2908
|
.command('agent')
|
|
@@ -2437,4 +3027,51 @@ data
|
|
|
2437
3027
|
.option('--verbose', 'Show error details')
|
|
2438
3028
|
.action((opts) => runCommand(handleDataExportReport, opts));
|
|
2439
3029
|
|
|
3030
|
+
const video = program.command('video').description('Tracked video extraction workflows');
|
|
3031
|
+
|
|
3032
|
+
video
|
|
3033
|
+
.command('queue-analysis')
|
|
3034
|
+
.description('Queue video analysis for tracked videos or tracked search results')
|
|
3035
|
+
.option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
|
|
3036
|
+
.option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
|
|
3037
|
+
.option('--video-uid <id>', 'Canonical tracked video_uid')
|
|
3038
|
+
.option('--platform-video-id <id>', 'Platform-native video id')
|
|
3039
|
+
.option('--body <jsonOrFile>', 'JSON body or @payload.json for batch queueing')
|
|
3040
|
+
.option('--wait', 'Poll until queued/completing analyses settle')
|
|
3041
|
+
.option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
|
|
3042
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
3043
|
+
.option('--api-key <key>', 'CLI API key')
|
|
3044
|
+
.option('--workspace-id <id>', 'Workspace id (for scoped keys)')
|
|
3045
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
3046
|
+
.option('--json', 'Emit machine-readable errors')
|
|
3047
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
3048
|
+
.option('--verbose', 'Show error details')
|
|
3049
|
+
.action((opts) => runCommand(handleVideoQueueAnalysis, opts));
|
|
3050
|
+
|
|
3051
|
+
video
|
|
3052
|
+
.command('extract')
|
|
3053
|
+
.description('Resolve tracked videos/results into structured analysis plus reference assets')
|
|
3054
|
+
.option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
|
|
3055
|
+
.option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
|
|
3056
|
+
.option('--video-uid <id>', 'Canonical tracked video_uid')
|
|
3057
|
+
.option('--platform-video-id <id>', 'Platform-native video id')
|
|
3058
|
+
.option('--body <jsonOrFile>', 'JSON body or @payload.json for batch extraction')
|
|
3059
|
+
.option('--ensure-analysis', 'Queue analysis when it is missing')
|
|
3060
|
+
.option('--wait', 'Poll until queued/completing analyses settle')
|
|
3061
|
+
.option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
|
|
3062
|
+
.option('--skip-assets', 'Skip asset URL generation')
|
|
3063
|
+
.option('--include-source-video', 'Include a signed source MP4 URL when available')
|
|
3064
|
+
.option('--frame-strategy <strategy>', 'brief_shots|quartiles')
|
|
3065
|
+
.option('--frame-count <n>', 'Number of still frames to return (1-5)')
|
|
3066
|
+
.option('--signed-url-seconds <n>', 'Signed URL TTL in seconds')
|
|
3067
|
+
.option('--out-dir <path>', 'Download returned assets into this local directory')
|
|
3068
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
3069
|
+
.option('--api-key <key>', 'CLI API key')
|
|
3070
|
+
.option('--workspace-id <id>', 'Workspace id (for scoped keys)')
|
|
3071
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
3072
|
+
.option('--json', 'Emit machine-readable errors')
|
|
3073
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
3074
|
+
.option('--verbose', 'Show error details')
|
|
3075
|
+
.action((opts) => runCommand(handleVideoExtract, opts));
|
|
3076
|
+
|
|
2440
3077
|
program.parseAsync(process.argv);
|