@socialseal/cli 0.1.5 → 0.1.7

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,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.7 - 2026-03-20
6
+ - Add `socialseal data export-search-results` for CLI-first enriched ranked-search exports, including direct CSV download handling.
7
+ - Add `search_results_enriched` as an alias on `socialseal data export-report` to map to the ranked-search export template.
8
+ - Add `socialseal data export-options` to make available export workflows discoverable from the CLI.
9
+ - Improve export ergonomics with local report-type validation and instructive failure guidance for processing, failed, and expired-download states.
10
+
11
+ ## 0.1.6 - 2026-03-19
12
+ - Fix runtime version reporting so `socialseal --version` reads from package metadata instead of a hardcoded source string.
13
+ - Fix `tracking` create request translation so `--workspace-id` is sent on the REST query path the backend uses for workspace binding.
14
+ - 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.
15
+ - Fail fast for `group-management` and `export_tracking_data` when no workspace is selected, instead of silently relying on backend personal-workspace fallback.
16
+ - Warn when `tracking create` runs without a workspace and when short numeric `--video-id` values look like internal row ids.
17
+ - Clarify in workspace discovery output and docs that `workspace_id` and `brand_id` are different identifiers.
18
+
5
19
  ## 0.1.5 - 2026-03-19
6
20
  - Add first-class tracked-video workflows with `video queue-analysis` and `video extract`.
7
21
  - Make `--video-id` the primary ergonomic selector for tracked-video analysis and asset extraction, while keeping `--search-result-id` as a fallback selector.
package/README.md CHANGED
@@ -56,21 +56,31 @@ Optional config file:
56
56
  - `socialseal video extract --body @payload.json --out-dir ./video-assets`
57
57
 
58
58
  - Data exports (provisional):
59
- - `socialseal data export-tracking --group-id 123 --time-period 30d --out out.csv`
59
+ - `socialseal data export-options`
60
+ - `socialseal data export-tracking --group-id 123 --time-period 30d --workspace-id <uuid> --out out.csv`
61
+ - `socialseal data export-search-results --group-ids 123,124 --workspace-id <uuid> --out ranked.csv`
62
+ - `socialseal data export-report --report-type search_results_enriched --format csv --payload @payload.json --workspace-id <uuid> --out ranked.csv`
60
63
  - `socialseal data export-report --report-type keyword_universe --format csv --payload @payload.json --out out.csv`
61
64
 
62
65
  ## Notes
63
- - `export-report` and `export_tracking_data` are provisional until CLI export specs are finalized.
66
+ - `export-report`, `export_tracking_data`, and `export-data`-backed exports are provisional until CLI export specs are finalized.
64
67
  - `tools list` ships a stable built-in registry of supported direct-call function targets. It is not live backend enumeration.
68
+ - `data export-search-results` maps to `export-data` template `tracking_ranked_videos_raw` and returns enriched ranked-search rows (search fields + video metadata + latest metrics + analysis). It downloads the signed CSV artifact when available.
69
+ - `data export-report --report-type search_results_enriched` is a compatibility alias to the same `export-data` template flow.
70
+ - `data export-report` now validates report types locally and shows the allowed list immediately; run `socialseal data export-options` when choosing between export flows.
71
+ - If an export returns metadata without a file URL (for example status `processing`), the CLI prints an explicit retry hint and returns the metadata JSON so automation can branch on status.
65
72
  - `--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).
66
73
  - `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
74
  - `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
75
  - `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.
76
+ - `--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.
77
+ - `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.
78
+ - `tracking create` without a workspace now prints a warning that the backend may create a personal/null-scope item.
79
+ - 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.
70
80
  - `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.
71
- - Effective workspace precedence is: `--workspace-id` → `SOCIALSEAL_WORKSPACE_ID` → config `workspaceId` backend personal-workspace fallback.
81
+ - 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.
72
82
  - `socialseal workspace use ...` writes a local default workspace into `~/.config/socialseal/config.json`, which the CLI reuses for `agent`, `tools`, and `data` commands.
73
- - `socialseal workspace list` discovers the workspaces accessible to the current CLI key and marks the active/suggested default.
83
+ - `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.
74
84
  - 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.
75
85
 
76
86
  ## Errors and exit codes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ const DEFAULT_POLL_INTERVAL_MS = 2000;
16
16
  const DEFAULT_FRAME_COUNT = 3;
17
17
  const MAX_TIMEOUT_MS = 900000;
18
18
  const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
19
+ const CLI_VERSION = loadRuntimeVersion();
19
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.';
20
21
  const EXIT_CODES = {
21
22
  OK: 0,
@@ -26,6 +27,44 @@ const EXIT_CODES = {
26
27
  SERVER: 5,
27
28
  };
28
29
  const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
30
+ const REPORT_TYPE_SEARCH_RESULTS_ENRICHED = 'search_results_enriched';
31
+ const EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW = 'tracking_ranked_videos_raw';
32
+ const SUPPORTED_EXPORT_REPORT_TYPES = [
33
+ 'keyword_universe',
34
+ 'cluster_insights',
35
+ 'creator_signatures',
36
+ 'post_publish',
37
+ 'quick_audit',
38
+ REPORT_TYPE_SEARCH_RESULTS_ENRICHED,
39
+ ];
40
+ const EXPORT_OPTIONS = [
41
+ {
42
+ id: 'tracking_csv',
43
+ command: 'socialseal data export-tracking --group-id <id> --time-period <window>',
44
+ summary: 'Legacy tracking CSV export for a group or tracking item.',
45
+ formats: ['csv'],
46
+ required: ['workspace id', '--group-id or --item-id', '--time-period'],
47
+ bestFor: 'Quick tracking-table exports and backwards-compatible pipelines.',
48
+ },
49
+ {
50
+ id: 'search_results_enriched',
51
+ command: 'socialseal data export-search-results --group-ids <id,id,...>',
52
+ summary: 'Enriched ranked search rows (search results + video + latest metrics + analysis).',
53
+ formats: ['csv'],
54
+ required: ['workspace id', '--group-ids'],
55
+ bestFor: 'SQL-like ranked-search datasets without using psql.',
56
+ alias: 'socialseal data export-report --report-type search_results_enriched --format csv --payload @payload.json',
57
+ },
58
+ {
59
+ id: 'report_templates',
60
+ command: 'socialseal data export-report --report-type <type> --format <format> --payload @payload.json',
61
+ summary: 'Report-template exports via export-report.',
62
+ formats: ['csv', 'json', 'markdown', 'html', 'excel_data'],
63
+ required: ['payload JSON'],
64
+ bestFor: 'Keyword universe, clusters, creators, post-publish timeline, and quick-audit exports.',
65
+ reportTypes: SUPPORTED_EXPORT_REPORT_TYPES,
66
+ },
67
+ ];
29
68
  const KNOWN_TOOLS = [
30
69
  { name: 'agent-tool-jobs', category: 'agent', description: 'Poll queued agent-backed tool jobs and fetch their results.' },
31
70
  { name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
@@ -53,6 +92,16 @@ const KNOWN_TOOLS = [
53
92
  notes: 'Refreshes brand metrics for brands/workspaces. It does not refresh a tracking group by UUID.',
54
93
  },
55
94
  { name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
95
+ {
96
+ name: 'export-data',
97
+ category: 'export',
98
+ description: 'Run raw workspace-scoped export templates with signed-URL artifacts.',
99
+ objectType: 'workspace_export',
100
+ transport: 'post_edge_function',
101
+ workspaceScoped: true,
102
+ knownLocalDevState: 'disabled_by_default',
103
+ notes: 'Includes template `tracking_ranked_videos_raw` for ranked search results with video + metrics + analysis enrichment.',
104
+ },
56
105
  {
57
106
  name: 'export_tracking_data',
58
107
  category: 'export',
@@ -61,7 +110,7 @@ const KNOWN_TOOLS = [
61
110
  transport: 'post_edge_function',
62
111
  workspaceScoped: true,
63
112
  knownLocalDevState: 'disabled_by_default',
64
- notes: 'group_id expects a numeric tracking_group id, not a brand_group UUID.',
113
+ 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.',
65
114
  },
66
115
  {
67
116
  name: 'tracked-video-extract',
@@ -71,7 +120,7 @@ const KNOWN_TOOLS = [
71
120
  transport: 'post_edge_function',
72
121
  workspaceScoped: true,
73
122
  knownLocalDevState: 'enabled',
74
- notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; can queue analysis and return/download asset URLs.',
123
+ notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; videoId means video_uid or platform-native video id, not a tracking item id.',
75
124
  },
76
125
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
77
126
  { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
@@ -88,7 +137,7 @@ const KNOWN_TOOLS = [
88
137
  workspaceScoped: true,
89
138
  knownLocalDevState: 'disabled_by_default',
90
139
  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.',
140
+ 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.',
92
141
  },
93
142
  {
94
143
  name: 'tracking',
@@ -123,6 +172,26 @@ function getConfigPath() {
123
172
  return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
124
173
  }
125
174
 
175
+ function loadRuntimeVersion() {
176
+ const envVersion = typeof process.env.npm_package_version === 'string'
177
+ ? process.env.npm_package_version.trim()
178
+ : '';
179
+ if (envVersion) return envVersion;
180
+
181
+ try {
182
+ const packageJsonPath = new URL('../package.json', import.meta.url);
183
+ const raw = fs.readFileSync(packageJsonPath, 'utf8');
184
+ const parsed = JSON.parse(raw);
185
+ if (typeof parsed?.version === 'string' && parsed.version.trim().length > 0) {
186
+ return parsed.version.trim();
187
+ }
188
+ } catch {
189
+ // fall through to the safe fallback below
190
+ }
191
+
192
+ return '0.0.0';
193
+ }
194
+
126
195
  function loadConfig() {
127
196
  const configPath = getConfigPath();
128
197
  try {
@@ -424,6 +493,7 @@ function buildVideoExtractBody(opts, workspaceId) {
424
493
  throw new CliError('Provide --body or one of --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
425
494
  code: 'MISSING_ARGUMENT',
426
495
  exitCode: EXIT_CODES.USAGE,
496
+ hint: '--video-id accepts a video_uid or platform video id. It does not accept tracking item ids.',
427
497
  });
428
498
  }
429
499
 
@@ -669,6 +739,88 @@ function coercePositiveInteger(value, label) {
669
739
  });
670
740
  }
671
741
 
742
+ function normalizePositiveIntegerList(value, label, { max } = {}) {
743
+ if (value === undefined || value === null || value === '') return [];
744
+ const entries = Array.isArray(value)
745
+ ? value
746
+ : String(value)
747
+ .split(',')
748
+ .map((entry) => entry.trim())
749
+ .filter(Boolean);
750
+ const parsed = entries.map((entry, index) => {
751
+ const normalized = coercePositiveInteger(entry, `${label}[${index}]`);
752
+ if (!normalized) {
753
+ throw new CliError(`Invalid ${label}[${index}] value.`, {
754
+ code: 'INVALID_ARGUMENT',
755
+ exitCode: EXIT_CODES.USAGE,
756
+ });
757
+ }
758
+ return normalized;
759
+ });
760
+ const deduped = Array.from(new Set(parsed));
761
+ if (max && deduped.length > max) {
762
+ throw new CliError(`Too many ${label} values: received ${deduped.length}, max is ${max}.`, {
763
+ code: 'INVALID_ARGUMENT',
764
+ exitCode: EXIT_CODES.USAGE,
765
+ });
766
+ }
767
+ return deduped;
768
+ }
769
+
770
+ function normalizeIsoDateTime(value, label) {
771
+ if (value === undefined || value === null || value === '') return undefined;
772
+ if (typeof value !== 'string') {
773
+ throw new CliError(`Invalid ${label}: expected an ISO datetime string.`, {
774
+ code: 'INVALID_ARGUMENT',
775
+ exitCode: EXIT_CODES.USAGE,
776
+ });
777
+ }
778
+ const trimmed = value.trim();
779
+ const epoch = Date.parse(trimmed);
780
+ if (Number.isNaN(epoch)) {
781
+ throw new CliError(`Invalid ${label}: expected an ISO datetime string.`, {
782
+ code: 'INVALID_ARGUMENT',
783
+ exitCode: EXIT_CODES.USAGE,
784
+ details: value,
785
+ });
786
+ }
787
+ return new Date(epoch).toISOString();
788
+ }
789
+
790
+ function buildSearchResultsEnrichedExportPayload(rawPayload, workspaceId) {
791
+ const payload = isJsonObject(rawPayload) ? rawPayload : {};
792
+ const groupIds = normalizePositiveIntegerList(
793
+ firstDefined(payload, ['groupIds', 'group_ids']),
794
+ 'groupIds',
795
+ { max: 100 },
796
+ );
797
+ if (groupIds.length === 0) {
798
+ throw new CliError('search_results_enriched export requires at least one group id.', {
799
+ code: 'MISSING_ARGUMENT',
800
+ exitCode: EXIT_CODES.USAGE,
801
+ hint: 'Provide --group-ids for `data export-search-results`, or include groupIds in --payload for `data export-report`.',
802
+ });
803
+ }
804
+
805
+ const trackingItemIds = normalizePositiveIntegerList(
806
+ firstDefined(payload, ['trackingItemIds', 'tracking_item_ids']),
807
+ 'trackingItemIds',
808
+ { max: 1000 },
809
+ );
810
+ const dateFrom = normalizeIsoDateTime(firstDefined(payload, ['dateFrom', 'date_from']), 'dateFrom');
811
+ const dateTo = normalizeIsoDateTime(firstDefined(payload, ['dateTo', 'date_to']), 'dateTo');
812
+ const filename = trimString(firstDefined(payload, ['filename'])) || undefined;
813
+
814
+ return stripUndefinedEntries({
815
+ workspaceId,
816
+ groupIds,
817
+ trackingItemIds: trackingItemIds.length > 0 ? trackingItemIds : undefined,
818
+ dateFrom,
819
+ dateTo,
820
+ filename,
821
+ });
822
+ }
823
+
672
824
  function buildPathWithQuery(basePath, query) {
673
825
  const params = new URLSearchParams();
674
826
  for (const [key, rawValue] of Object.entries(query || {})) {
@@ -933,7 +1085,7 @@ function translateTrackingAction(payload, workspaceId) {
933
1085
  if (!action) {
934
1086
  return {
935
1087
  method: 'POST',
936
- pathSuffix: '',
1088
+ pathSuffix: buildPathWithQuery('', { workspace_id: workspaceId || undefined }),
937
1089
  body: stripUndefinedEntries({
938
1090
  name: payload.name,
939
1091
  track_type: payload.track_type,
@@ -1013,7 +1165,7 @@ function translateTrackingAction(payload, workspaceId) {
1013
1165
  if (action === 'create' || action === 'item_create') {
1014
1166
  return {
1015
1167
  method: 'POST',
1016
- pathSuffix: '',
1168
+ pathSuffix: buildPathWithQuery('', { workspace_id: workspaceId || undefined }),
1017
1169
  body: stripUndefinedEntries({
1018
1170
  name: payload.name,
1019
1171
  track_type: payload.track_type,
@@ -1480,6 +1632,61 @@ function emitWorkspaceContext(opts, { workspaceId, source, functionName, method
1480
1632
  );
1481
1633
  }
1482
1634
 
1635
+ function describeWorkspaceSource(source) {
1636
+ switch (source) {
1637
+ case 'flag':
1638
+ return '--workspace-id';
1639
+ case 'env':
1640
+ return 'SOCIALSEAL_WORKSPACE_ID';
1641
+ case 'config':
1642
+ return 'the saved default workspace';
1643
+ case 'body':
1644
+ return 'the request body';
1645
+ default:
1646
+ return 'implicit selection';
1647
+ }
1648
+ }
1649
+
1650
+ function emitWorkspaceSelectionNotice(opts, { workspaceId, source, label }) {
1651
+ if (!workspaceId || !source || source === 'flag' || source === 'body') return;
1652
+ process.stderr.write(
1653
+ `[socialseal] Using workspace ${workspaceId} from ${describeWorkspaceSource(source)} for ${label}. Pass --workspace-id to override.\n`,
1654
+ );
1655
+ }
1656
+
1657
+ function requireWorkspaceSelection(workspaceId, { label, hint }) {
1658
+ if (workspaceId) return workspaceId;
1659
+ throw new CliError(`${label} requires a workspace id.`, {
1660
+ code: 'WORKSPACE_REQUIRED',
1661
+ exitCode: EXIT_CODES.USAGE,
1662
+ hint,
1663
+ });
1664
+ }
1665
+
1666
+ function emitTrackingCreateScopeWarning(action, workspaceId) {
1667
+ if (action !== 'create' || workspaceId) return;
1668
+ process.stderr.write(
1669
+ '[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',
1670
+ );
1671
+ }
1672
+
1673
+ function looksLikeShortNumericVideoId(value) {
1674
+ return typeof value === 'string' && /^\d{1,7}$/.test(value.trim());
1675
+ }
1676
+
1677
+ function emitAmbiguousVideoIdWarnings(items) {
1678
+ const references = Array.isArray(items) ? items : [];
1679
+ for (const item of references) {
1680
+ if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
1681
+ if (looksLikeShortNumericVideoId(item.videoId)) {
1682
+ process.stderr.write(
1683
+ `[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`,
1684
+ );
1685
+ return;
1686
+ }
1687
+ }
1688
+ }
1689
+
1483
1690
  function sleep(ms) {
1484
1691
  return new Promise((resolve) => setTimeout(resolve, ms));
1485
1692
  }
@@ -1618,7 +1825,9 @@ function mapStatusToExitCode(status) {
1618
1825
  }
1619
1826
 
1620
1827
  function isLocallyDisabledByDefaultFunction(functionName) {
1621
- return functionName === 'group-management' || functionName === 'export_tracking_data';
1828
+ return functionName === 'group-management'
1829
+ || functionName === 'export_tracking_data'
1830
+ || functionName === 'export-data';
1622
1831
  }
1623
1832
 
1624
1833
  function buildStatusHint(status, context = {}) {
@@ -1629,7 +1838,7 @@ function buildStatusHint(status, context = {}) {
1629
1838
  case 404:
1630
1839
  if (context.functionName) {
1631
1840
  if (isLocallyDisabledByDefaultFunction(context.functionName)) {
1632
- return `Unknown function "${context.functionName}". This tool is listed in the static registry, but it is disabled by default in some local Supabase environments. Check the deployment or enable it in supabase/config.toml.`;
1841
+ 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.`;
1633
1842
  }
1634
1843
  return `Unknown function "${context.functionName}". Double-check the name and API base.`;
1635
1844
  }
@@ -1735,6 +1944,18 @@ function requireApiKey(opts, config) {
1735
1944
  return apiKey;
1736
1945
  }
1737
1946
 
1947
+ function assertSupportedReportType(reportType) {
1948
+ const normalized = trimString(reportType);
1949
+ if (SUPPORTED_EXPORT_REPORT_TYPES.includes(normalized)) {
1950
+ return normalized;
1951
+ }
1952
+ throw new CliError(`Unsupported report type: ${reportType}`, {
1953
+ code: 'INVALID_ARGUMENT',
1954
+ exitCode: EXIT_CODES.USAGE,
1955
+ hint: `Use one of: ${SUPPORTED_EXPORT_REPORT_TYPES.join(', ')}. Run \`socialseal data export-options\` to choose the right export flow.`,
1956
+ });
1957
+ }
1958
+
1738
1959
  function resolveApiTarget({ apiBase, legacyUrl }) {
1739
1960
  const resolvedApiBase = apiBase || (!legacyUrl ? DEFAULT_API_BASE : null);
1740
1961
  if (!resolvedApiBase && !legacyUrl) {
@@ -2197,14 +2418,64 @@ async function handleToolsCall(opts) {
2197
2418
  resolvedWorkspaceId,
2198
2419
  });
2199
2420
  const method = normalizeMethod(translated.method);
2200
- const effectiveWorkspaceId = translated.workspaceId ?? resolvedWorkspaceId ?? null;
2421
+ const payloadWorkspaceId = isJsonObject(translated.normalizedPayload)
2422
+ ? resolvePayloadWorkspaceId(translated.normalizedPayload, resolvedWorkspaceId)
2423
+ : (isJsonObject(translated.body)
2424
+ ? resolvePayloadWorkspaceId(translated.body, resolvedWorkspaceId)
2425
+ : (resolvedWorkspaceId ?? null));
2426
+ const effectiveWorkspaceId = translated.workspaceId ?? payloadWorkspaceId ?? null;
2427
+ const effectiveWorkspaceSource =
2428
+ translated.workspaceId && translated.workspaceId !== resolvedWorkspaceId
2429
+ ? 'body'
2430
+ : (payloadWorkspaceId && payloadWorkspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource);
2201
2431
  const path = useGateway
2202
2432
  ? `/cli/tools/${opts.function}${translated.pathSuffix || ''}`
2203
2433
  : `/functions/v1/${opts.function}${translated.pathSuffix || ''}`;
2204
2434
 
2435
+ if (opts.function === 'group-management') {
2436
+ requireWorkspaceSelection(effectiveWorkspaceId, {
2437
+ label: 'group-management',
2438
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace. Omitting workspace lets the backend fall back to the personal workspace.',
2439
+ });
2440
+ emitWorkspaceSelectionNotice(opts, {
2441
+ workspaceId: effectiveWorkspaceId,
2442
+ source: effectiveWorkspaceSource,
2443
+ label: 'group-management',
2444
+ });
2445
+ }
2446
+
2447
+ if (opts.function === 'export_tracking_data') {
2448
+ requireWorkspaceSelection(effectiveWorkspaceId, {
2449
+ label: 'export_tracking_data',
2450
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting tracking data.',
2451
+ });
2452
+ emitWorkspaceSelectionNotice(opts, {
2453
+ workspaceId: effectiveWorkspaceId,
2454
+ source: effectiveWorkspaceSource,
2455
+ label: 'export_tracking_data',
2456
+ });
2457
+ }
2458
+
2459
+ if (opts.function === 'tracked-video-extract') {
2460
+ requireWorkspaceSelection(effectiveWorkspaceId, {
2461
+ label: 'tracked-video-extract',
2462
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
2463
+ });
2464
+ emitWorkspaceSelectionNotice(opts, {
2465
+ workspaceId: effectiveWorkspaceId,
2466
+ source: effectiveWorkspaceSource,
2467
+ label: 'tracked-video-extract',
2468
+ });
2469
+ }
2470
+
2471
+ emitTrackingCreateScopeWarning(
2472
+ isJsonObject(translated.normalizedPayload) ? trimString(translated.normalizedPayload.action).toLowerCase() : '',
2473
+ effectiveWorkspaceId,
2474
+ );
2475
+
2205
2476
  emitWorkspaceContext(opts, {
2206
2477
  workspaceId: effectiveWorkspaceId,
2207
- source: effectiveWorkspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
2478
+ source: effectiveWorkspaceSource,
2208
2479
  functionName: opts.function,
2209
2480
  method,
2210
2481
  });
@@ -2319,7 +2590,7 @@ async function handleDataExportTracking(opts) {
2319
2590
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
2320
2591
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
2321
2592
  const timeoutMs = resolveTimeoutMs(opts, config);
2322
- const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
2593
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
2323
2594
 
2324
2595
  if (!opts.groupId && !opts.itemId) {
2325
2596
  throw new CliError('Provide --group-id or --item-id.', {
@@ -2341,6 +2612,16 @@ async function handleDataExportTracking(opts) {
2341
2612
  time_period: opts.timePeriod,
2342
2613
  };
2343
2614
 
2615
+ requireWorkspaceSelection(resolvedWorkspaceId, {
2616
+ label: 'Tracking export',
2617
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting tracking data.',
2618
+ });
2619
+ emitWorkspaceSelectionNotice(opts, {
2620
+ workspaceId: resolvedWorkspaceId,
2621
+ source: workspaceSource,
2622
+ label: 'tracking export',
2623
+ });
2624
+
2344
2625
  const res = await callApi({
2345
2626
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
2346
2627
  apiKey,
@@ -2372,6 +2653,24 @@ async function handleDataExportTracking(opts) {
2372
2653
  }
2373
2654
 
2374
2655
  async function handleDataExportReport(opts) {
2656
+ const reportType = assertSupportedReportType(opts.reportType);
2657
+
2658
+ if (reportType === REPORT_TYPE_SEARCH_RESULTS_ENRICHED) {
2659
+ if (opts.format !== 'csv') {
2660
+ throw new CliError('search_results_enriched supports only csv format.', {
2661
+ code: 'INVALID_ARGUMENT',
2662
+ exitCode: EXIT_CODES.USAGE,
2663
+ hint: 'Use --format csv.',
2664
+ });
2665
+ }
2666
+ const payload = ensureJsonObject(parseJsonInput(opts.payload, { label: 'payload' }), 'payload');
2667
+ await handleDataExportSearchResults({
2668
+ ...opts,
2669
+ __rawPayload: payload,
2670
+ });
2671
+ return;
2672
+ }
2673
+
2375
2674
  const config = loadConfig();
2376
2675
  const apiKey = requireApiKey(opts, config);
2377
2676
  const apiBase = resolveApiBase(opts, config);
@@ -2388,7 +2687,7 @@ async function handleDataExportReport(opts) {
2388
2687
  path: useGateway ? '/cli/tools/export-report' : '/functions/v1/export-report',
2389
2688
  method: 'POST',
2390
2689
  body: {
2391
- reportType: opts.reportType,
2690
+ reportType,
2392
2691
  format: opts.format,
2393
2692
  payload,
2394
2693
  },
@@ -2434,6 +2733,142 @@ async function handleDataExportReport(opts) {
2434
2733
  process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
2435
2734
  }
2436
2735
 
2736
+ async function handleDataExportSearchResults(opts) {
2737
+ const config = loadConfig();
2738
+ const apiKey = requireApiKey(opts, config);
2739
+ const apiBase = resolveApiBase(opts, config);
2740
+ const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
2741
+ const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
2742
+ const timeoutMs = resolveTimeoutMs(opts, config);
2743
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
2744
+
2745
+ const rawPayload = opts.__rawPayload ?? stripUndefinedEntries({
2746
+ groupIds: normalizePositiveIntegerList(opts.groupIds, 'groupIds', { max: 100 }),
2747
+ trackingItemIds: normalizePositiveIntegerList(opts.trackingItemIds, 'trackingItemIds', { max: 1000 }),
2748
+ dateFrom: opts.dateFrom,
2749
+ dateTo: opts.dateTo,
2750
+ filename: opts.filename,
2751
+ });
2752
+
2753
+ const payloadWorkspaceId = resolvePayloadWorkspaceId(rawPayload, null);
2754
+ const effectiveWorkspaceId = requireWorkspaceSelection(payloadWorkspaceId || resolvedWorkspaceId, {
2755
+ label: 'Search results enriched export',
2756
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting.',
2757
+ });
2758
+ const effectiveWorkspaceSource = payloadWorkspaceId ? 'body' : workspaceSource;
2759
+ emitWorkspaceSelectionNotice(opts, {
2760
+ workspaceId: effectiveWorkspaceId,
2761
+ source: effectiveWorkspaceSource,
2762
+ label: 'search_results_enriched export',
2763
+ });
2764
+
2765
+ const normalizedPayload = buildSearchResultsEnrichedExportPayload(rawPayload, effectiveWorkspaceId);
2766
+ const requestedFilename = trimString(normalizedPayload.filename) || undefined;
2767
+ delete normalizedPayload.filename;
2768
+
2769
+ const exportResponse = await callApi({
2770
+ apiBase: useGateway ? resolvedApiBase : legacyUrl,
2771
+ apiKey,
2772
+ path: useGateway ? '/cli/tools/export-data' : '/functions/v1/export-data',
2773
+ method: 'POST',
2774
+ body: {
2775
+ template: EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW,
2776
+ format: 'csv',
2777
+ payload: normalizedPayload,
2778
+ filename: requestedFilename,
2779
+ },
2780
+ workspaceId: effectiveWorkspaceId,
2781
+ timeoutMs,
2782
+ });
2783
+
2784
+ if (!exportResponse.ok) {
2785
+ throw await buildHttpError(exportResponse, {
2786
+ label: 'Search results enriched export',
2787
+ functionName: 'export-data',
2788
+ method: 'POST',
2789
+ });
2790
+ }
2791
+
2792
+ const responseJson = await exportResponse.json();
2793
+ const metadata = isJsonObject(responseJson) && isJsonObject(responseJson.metadata)
2794
+ ? responseJson.metadata
2795
+ : null;
2796
+ const fileUrl = trimString(metadata?.file_url || '');
2797
+ const status = trimString(metadata?.status || '').toLowerCase();
2798
+
2799
+ if (!fileUrl) {
2800
+ if (status === 'processing') {
2801
+ process.stderr.write('[socialseal] Export is still processing. Re-run the same command shortly; the backend dedupes and returns the finished artifact when ready.\n');
2802
+ } else if (status === 'failed') {
2803
+ process.stderr.write('[socialseal] Export status is failed. Inspect the JSON metadata for details, then retry with corrected filters.\n');
2804
+ } else {
2805
+ process.stderr.write('[socialseal] Export did not include a file URL yet. Inspect the JSON metadata and retry if needed.\n');
2806
+ }
2807
+ emitJsonOutput(responseJson, opts.pretty);
2808
+ return;
2809
+ }
2810
+
2811
+ const artifactResponse = await fetchWithTimeout(fileUrl, {
2812
+ method: 'GET',
2813
+ headers: { Accept: '*/*' },
2814
+ }, timeoutMs);
2815
+
2816
+ if (!artifactResponse.ok) {
2817
+ throw await buildHttpError(artifactResponse, {
2818
+ label: 'Search results enriched artifact download',
2819
+ method: 'GET',
2820
+ hint: 'The signed file URL may be expired or inaccessible. Re-run the export command to mint a fresh URL.',
2821
+ });
2822
+ }
2823
+
2824
+ if (!artifactResponse.body) {
2825
+ throw new CliError('Export artifact response contained no body.', {
2826
+ code: 'EMPTY_RESPONSE',
2827
+ exitCode: EXIT_CODES.SERVER,
2828
+ });
2829
+ }
2830
+
2831
+ const outPath = opts.stdout
2832
+ ? null
2833
+ : (opts.out || trimString(metadata?.filename || '') || 'tracking-ranked-videos.csv');
2834
+ if (outPath) {
2835
+ await pipeline(artifactResponse.body, fs.createWriteStream(outPath));
2836
+ process.stderr.write(`[socialseal] Export written to ${outPath}\n`);
2837
+ } else {
2838
+ await pipeline(artifactResponse.body, process.stdout);
2839
+ }
2840
+ }
2841
+
2842
+ function handleDataExportOptions(opts) {
2843
+ const payload = {
2844
+ exports: EXPORT_OPTIONS,
2845
+ supportedReportTypes: SUPPORTED_EXPORT_REPORT_TYPES,
2846
+ note: 'Use this list to choose the right export surface before running data export commands.',
2847
+ };
2848
+
2849
+ if (opts.json) {
2850
+ emitJsonOutput(payload, opts.pretty);
2851
+ return;
2852
+ }
2853
+
2854
+ process.stdout.write('[socialseal] Available export options\n');
2855
+ process.stdout.write('[socialseal] Choose a flow based on dataset shape, not endpoint name.\n\n');
2856
+ for (const option of EXPORT_OPTIONS) {
2857
+ process.stdout.write(`- ${option.id}: ${option.summary}\n`);
2858
+ process.stdout.write(` command: ${option.command}\n`);
2859
+ process.stdout.write(` formats: ${option.formats.join(', ')}\n`);
2860
+ process.stdout.write(` required: ${option.required.join(', ')}\n`);
2861
+ process.stdout.write(` best for: ${option.bestFor}\n`);
2862
+ if (option.alias) {
2863
+ process.stdout.write(` alias: ${option.alias}\n`);
2864
+ }
2865
+ if (Array.isArray(option.reportTypes)) {
2866
+ process.stdout.write(` report types: ${option.reportTypes.join(', ')}\n`);
2867
+ }
2868
+ process.stdout.write('\n');
2869
+ }
2870
+ }
2871
+
2437
2872
  async function handleVideoExtract(opts) {
2438
2873
  const config = loadConfig();
2439
2874
  const apiKey = requireApiKey(opts, config);
@@ -2445,10 +2880,19 @@ async function handleVideoExtract(opts) {
2445
2880
 
2446
2881
  const body = buildVideoExtractBody(opts, resolvedWorkspaceId);
2447
2882
  const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
2883
+ const effectiveWorkspaceId = body.workspaceId || resolvedWorkspaceId;
2884
+ const effectiveWorkspaceSource = body.workspaceId && body.workspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource;
2885
+
2886
+ emitWorkspaceSelectionNotice(opts, {
2887
+ workspaceId: effectiveWorkspaceId,
2888
+ source: effectiveWorkspaceSource,
2889
+ label: 'tracked-video extract',
2890
+ });
2891
+ emitAmbiguousVideoIdWarnings(body.items);
2448
2892
 
2449
2893
  emitWorkspaceContext(opts, {
2450
- workspaceId: body.workspaceId || resolvedWorkspaceId,
2451
- source: body.workspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
2894
+ workspaceId: effectiveWorkspaceId,
2895
+ source: effectiveWorkspaceSource,
2452
2896
  functionName: 'tracked-video-extract',
2453
2897
  method: 'POST',
2454
2898
  });
@@ -2460,7 +2904,7 @@ async function handleVideoExtract(opts) {
2460
2904
  path,
2461
2905
  method: 'POST',
2462
2906
  body,
2463
- workspaceId: body.workspaceId || resolvedWorkspaceId,
2907
+ workspaceId: effectiveWorkspaceId,
2464
2908
  timeoutMs: remainingTimeoutMs,
2465
2909
  });
2466
2910
 
@@ -2523,11 +2967,20 @@ async function handleVideoQueueAnalysis(opts) {
2523
2967
  const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
2524
2968
 
2525
2969
  const body = buildVideoQueueBody(opts, resolvedWorkspaceId);
2970
+ const effectiveWorkspaceId = body.workspaceId || resolvedWorkspaceId;
2971
+ const effectiveWorkspaceSource = body.workspaceId && body.workspaceId !== resolvedWorkspaceId ? 'body' : workspaceSource;
2972
+
2973
+ emitWorkspaceSelectionNotice(opts, {
2974
+ workspaceId: effectiveWorkspaceId,
2975
+ source: effectiveWorkspaceSource,
2976
+ label: 'tracked-video queue-analysis',
2977
+ });
2978
+ emitAmbiguousVideoIdWarnings(body.items);
2526
2979
  const path = useGateway ? '/cli/tools/tracked-video-extract' : '/functions/v1/tracked-video-extract';
2527
2980
 
2528
2981
  emitWorkspaceContext(opts, {
2529
- workspaceId: body.workspaceId || resolvedWorkspaceId,
2530
- source: body.workspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
2982
+ workspaceId: effectiveWorkspaceId,
2983
+ source: effectiveWorkspaceSource,
2531
2984
  functionName: 'tracked-video-extract',
2532
2985
  method: 'POST',
2533
2986
  });
@@ -2539,7 +2992,7 @@ async function handleVideoQueueAnalysis(opts) {
2539
2992
  path,
2540
2993
  method: 'POST',
2541
2994
  body,
2542
- workspaceId: body.workspaceId || resolvedWorkspaceId,
2995
+ workspaceId: effectiveWorkspaceId,
2543
2996
  timeoutMs: remainingTimeoutMs,
2544
2997
  });
2545
2998
 
@@ -2624,6 +3077,8 @@ async function handleWorkspaceList(opts) {
2624
3077
  process.stdout.write(`${formatWorkspaceLine(workspace, { isEffective, source: selection.source, isSuggested })}\n`);
2625
3078
  }
2626
3079
 
3080
+ 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');
3081
+
2627
3082
  if (!selection.workspaceId && directory.defaultWorkspaceId) {
2628
3083
  process.stdout.write('\n[socialseal] No local default is configured. Set one with: socialseal workspace use <id>\n');
2629
3084
  }
@@ -2668,6 +3123,7 @@ async function handleWorkspaceCurrent(opts) {
2668
3123
 
2669
3124
  if (effectiveWorkspace) {
2670
3125
  process.stdout.write(`[socialseal] Effective workspace: ${effectiveWorkspace.name} (${effectiveWorkspace.id}) via ${selection.source}\n`);
3126
+ process.stdout.write('[socialseal] Note: workspace ids are not brand ids. Use the workspace id, not brand_id, with --workspace-id.\n');
2671
3127
  return;
2672
3128
  }
2673
3129
 
@@ -2736,7 +3192,7 @@ const program = new Command();
2736
3192
  program
2737
3193
  .name('socialseal')
2738
3194
  .description('SocialSeal CLI (non-interactive)')
2739
- .version('0.1.1');
3195
+ .version(CLI_VERSION);
2740
3196
 
2741
3197
  if (typeof program.showHelpAfterError === 'function') {
2742
3198
  program.showHelpAfterError(true);
@@ -2744,7 +3200,7 @@ if (typeof program.showHelpAfterError === 'function') {
2744
3200
  if (typeof program.showSuggestionAfterError === 'function') {
2745
3201
  program.showSuggestionAfterError(true);
2746
3202
  }
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`);
3203
+ 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-options\n socialseal data export-tracking --group-id 123 --time-period 30d\n socialseal data export-search-results --group-ids 123,124 --workspace-id <uuid> --out ranked.csv\n`);
2748
3204
 
2749
3205
  program
2750
3206
  .command('agent')
@@ -2836,6 +3292,14 @@ tools
2836
3292
 
2837
3293
  const data = program.command('data').description('Data exports (provisional)');
2838
3294
 
3295
+ data
3296
+ .command('export-options')
3297
+ .description('List export flows, when to use each, and required inputs')
3298
+ .option('--json', 'Emit machine-readable output')
3299
+ .option('--pretty', 'Pretty-print JSON')
3300
+ .option('--verbose', 'Show error details')
3301
+ .action((opts) => runCommand(handleDataExportOptions, opts));
3302
+
2839
3303
  data
2840
3304
  .command('export-tracking')
2841
3305
  .description('Export tracking data as CSV')
@@ -2852,10 +3316,29 @@ data
2852
3316
  .option('--verbose', 'Show error details')
2853
3317
  .action((opts) => runCommand(handleDataExportTracking, opts));
2854
3318
 
3319
+ data
3320
+ .command('export-search-results')
3321
+ .description('Export enriched ranked search results (search_results + videos + latest metrics + analysis) as CSV')
3322
+ .requiredOption('--group-ids <ids>', 'Comma-separated tracking group ids (for example: 123,124,125)')
3323
+ .option('--tracking-item-ids <ids>', 'Optional comma-separated tracking item ids')
3324
+ .option('--date-from <iso>', 'Optional ISO datetime lower bound (inclusive)')
3325
+ .option('--date-to <iso>', 'Optional ISO datetime upper bound (inclusive)')
3326
+ .option('--filename <name>', 'Optional export filename stem (without extension)')
3327
+ .option('--out <path>', 'Output file path')
3328
+ .option('--stdout', 'Write to stdout')
3329
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
3330
+ .option('--api-key <key>', 'CLI API key')
3331
+ .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
3332
+ .option('--pretty', 'Pretty-print JSON metadata when no file is ready')
3333
+ .option('--json', 'Emit machine-readable errors')
3334
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
3335
+ .option('--verbose', 'Show error details')
3336
+ .action((opts) => runCommand(handleDataExportSearchResults, opts));
3337
+
2855
3338
  data
2856
3339
  .command('export-report')
2857
3340
  .description('Export report data via export-report (provisional)')
2858
- .requiredOption('--report-type <type>', 'keyword_universe|cluster_insights|creator_signatures|post_publish|quick_audit')
3341
+ .requiredOption('--report-type <type>', 'keyword_universe|cluster_insights|creator_signatures|post_publish|quick_audit|search_results_enriched')
2859
3342
  .option('--format <format>', 'csv|json|markdown|html|excel_data', 'csv')
2860
3343
  .requiredOption('--payload <jsonOrFile>', 'Payload JSON or @file.json')
2861
3344
  .option('--out <path>', 'Output file path')
@@ -2874,10 +3357,10 @@ const video = program.command('video').description('Tracked video extraction wor
2874
3357
  video
2875
3358
  .command('queue-analysis')
2876
3359
  .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')
3360
+ .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
3361
+ .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
3362
+ .option('--video-uid <id>', 'Canonical tracked video_uid')
3363
+ .option('--platform-video-id <id>', 'Platform-native video id')
2881
3364
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch queueing')
2882
3365
  .option('--wait', 'Poll until queued/completing analyses settle')
2883
3366
  .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
@@ -2893,10 +3376,10 @@ video
2893
3376
  video
2894
3377
  .command('extract')
2895
3378
  .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')
3379
+ .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
3380
+ .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
3381
+ .option('--video-uid <id>', 'Canonical tracked video_uid')
3382
+ .option('--platform-video-id <id>', 'Platform-native video id')
2900
3383
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch extraction')
2901
3384
  .option('--ensure-analysis', 'Queue analysis when it is missing')
2902
3385
  .option('--wait', 'Poll until queued/completing analyses settle')