@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 +14 -0
- package/README.md +15 -5
- package/package.json +1 -1
- package/src/index.js +511 -28
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-
|
|
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 `
|
|
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`
|
|
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
|
|
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
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;
|
|
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'
|
|
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}".
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
2451
|
-
source:
|
|
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:
|
|
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:
|
|
2530
|
-
source:
|
|
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:
|
|
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(
|
|
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
|
|
2878
|
-
.option('--search-result-id <id>', 'Tracked search result id')
|
|
2879
|
-
.option('--video-uid <id>', '
|
|
2880
|
-
.option('--platform-video-id <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
|
|
2897
|
-
.option('--search-result-id <id>', 'Tracked search result id')
|
|
2898
|
-
.option('--video-uid <id>', '
|
|
2899
|
-
.option('--platform-video-id <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')
|