@socialseal/cli 0.1.6 → 0.1.8
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 +11 -0
- package/README.md +15 -1
- package/package.json +1 -1
- package/src/index.js +1220 -16
package/src/index.js
CHANGED
|
@@ -10,14 +10,16 @@ const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.config', 'socialseal', 'co
|
|
|
10
10
|
const DEFAULT_API_BASE = 'https://api.socialseal.co';
|
|
11
11
|
const CLI_KEY_HEADER = 'X-CLI-Key';
|
|
12
12
|
const WORKSPACE_HEADER = 'X-Workspace-Id';
|
|
13
|
-
const DEFAULT_TIMEOUT_MS =
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 300000;
|
|
14
14
|
const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 300000;
|
|
15
15
|
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
16
|
+
const DEFAULT_STATUS_RESULTS_LIMIT = 10;
|
|
16
17
|
const DEFAULT_FRAME_COUNT = 3;
|
|
17
18
|
const MAX_TIMEOUT_MS = 900000;
|
|
18
19
|
const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
|
|
19
20
|
const CLI_VERSION = loadRuntimeVersion();
|
|
20
21
|
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.';
|
|
22
|
+
const STATIC_TOOL_SCHEMA_NOTE = 'Schema hints are static CLI docs for discoverability. Backend contracts can still evolve.';
|
|
21
23
|
const EXIT_CODES = {
|
|
22
24
|
OK: 0,
|
|
23
25
|
UNKNOWN: 1,
|
|
@@ -27,8 +29,58 @@ const EXIT_CODES = {
|
|
|
27
29
|
SERVER: 5,
|
|
28
30
|
};
|
|
29
31
|
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
32
|
+
const ACTIVE_STATUS_VALUES = new Set(['queued', 'pending', 'processing', 'in_progress', 'running']);
|
|
33
|
+
const TOOL_STATUS_KINDS = new Set(['auto', 'agent_job', 'google_ai_run', 'journey_run']);
|
|
34
|
+
const REPORT_TYPE_SEARCH_RESULTS_ENRICHED = 'search_results_enriched';
|
|
35
|
+
const EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW = 'tracking_ranked_videos_raw';
|
|
36
|
+
const SUPPORTED_EXPORT_REPORT_TYPES = [
|
|
37
|
+
'keyword_universe',
|
|
38
|
+
'cluster_insights',
|
|
39
|
+
'creator_signatures',
|
|
40
|
+
'post_publish',
|
|
41
|
+
'quick_audit',
|
|
42
|
+
REPORT_TYPE_SEARCH_RESULTS_ENRICHED,
|
|
43
|
+
];
|
|
44
|
+
const EXPORT_OPTIONS = [
|
|
45
|
+
{
|
|
46
|
+
id: 'tracking_csv',
|
|
47
|
+
command: 'socialseal data export-tracking --group-id <id> --time-period <window>',
|
|
48
|
+
summary: 'Legacy tracking CSV export for a group or tracking item.',
|
|
49
|
+
formats: ['csv'],
|
|
50
|
+
required: ['workspace id', '--group-id or --item-id', '--time-period'],
|
|
51
|
+
bestFor: 'Quick tracking-table exports and backwards-compatible pipelines.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'search_results_enriched',
|
|
55
|
+
command: 'socialseal data export-search-results --group-ids <id,id,...>',
|
|
56
|
+
summary: 'Enriched ranked search rows (search results + video + latest metrics + analysis).',
|
|
57
|
+
formats: ['csv'],
|
|
58
|
+
required: ['workspace id', '--group-ids'],
|
|
59
|
+
bestFor: 'SQL-like ranked-search datasets without using psql.',
|
|
60
|
+
alias: 'socialseal data export-report --report-type search_results_enriched --format csv --payload @payload.json',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'report_templates',
|
|
64
|
+
command: 'socialseal data export-report --report-type <type> --format <format> --payload @payload.json',
|
|
65
|
+
summary: 'Report-template exports via export-report.',
|
|
66
|
+
formats: ['csv', 'json', 'markdown', 'html', 'excel_data'],
|
|
67
|
+
required: ['payload JSON'],
|
|
68
|
+
bestFor: 'Keyword universe, clusters, creators, post-publish timeline, and quick-audit exports.',
|
|
69
|
+
reportTypes: SUPPORTED_EXPORT_REPORT_TYPES,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
30
72
|
const KNOWN_TOOLS = [
|
|
31
|
-
{
|
|
73
|
+
{
|
|
74
|
+
name: 'agent-tool-jobs',
|
|
75
|
+
category: 'agent',
|
|
76
|
+
description: 'Queue/poll agent-backed tool jobs.',
|
|
77
|
+
objectType: 'agent_tool_job',
|
|
78
|
+
transport: 'post_edge_function',
|
|
79
|
+
workspaceScoped: true,
|
|
80
|
+
knownLocalDevState: 'enabled',
|
|
81
|
+
actionAliases: ['start', 'status'],
|
|
82
|
+
notes: 'Use action=start to queue and action=status to read a UUID job id.',
|
|
83
|
+
},
|
|
32
84
|
{ name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
|
|
33
85
|
{ name: 'workspace-notes', category: 'agent', description: 'Search, create, update, and pin workspace note memory.' },
|
|
34
86
|
{ name: 'workspace-onboarding', category: 'agent', description: 'Read or update workspace onboarding metadata used by the agent.' },
|
|
@@ -54,6 +106,16 @@ const KNOWN_TOOLS = [
|
|
|
54
106
|
notes: 'Refreshes brand metrics for brands/workspaces. It does not refresh a tracking group by UUID.',
|
|
55
107
|
},
|
|
56
108
|
{ name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
|
|
109
|
+
{
|
|
110
|
+
name: 'export-data',
|
|
111
|
+
category: 'export',
|
|
112
|
+
description: 'Run raw workspace-scoped export templates with signed-URL artifacts.',
|
|
113
|
+
objectType: 'workspace_export',
|
|
114
|
+
transport: 'post_edge_function',
|
|
115
|
+
workspaceScoped: true,
|
|
116
|
+
knownLocalDevState: 'disabled_by_default',
|
|
117
|
+
notes: 'Includes template `tracking_ranked_videos_raw` for ranked search results with video + metrics + analysis enrichment.',
|
|
118
|
+
},
|
|
57
119
|
{
|
|
58
120
|
name: 'export_tracking_data',
|
|
59
121
|
category: 'export',
|
|
@@ -75,7 +137,36 @@ const KNOWN_TOOLS = [
|
|
|
75
137
|
notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; videoId means video_uid or platform-native video id, not a tracking item id.',
|
|
76
138
|
},
|
|
77
139
|
{ name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
|
|
78
|
-
{
|
|
140
|
+
{
|
|
141
|
+
name: 'google-ai-search',
|
|
142
|
+
category: 'search',
|
|
143
|
+
description: 'Queue Google AI search runs.',
|
|
144
|
+
objectType: 'google_ai_run',
|
|
145
|
+
transport: 'post_edge_function',
|
|
146
|
+
workspaceScoped: true,
|
|
147
|
+
knownLocalDevState: 'enabled',
|
|
148
|
+
notes: 'Returns numeric runId. Read status/results via get-google-ai-search-runs/get-google-ai-search-results or socialseal tools status <runId>.',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'get-google-ai-search-runs',
|
|
152
|
+
category: 'search',
|
|
153
|
+
description: 'Read Google AI run queue/progress by numeric run id.',
|
|
154
|
+
objectType: 'google_ai_run',
|
|
155
|
+
transport: 'post_edge_function',
|
|
156
|
+
workspaceScoped: true,
|
|
157
|
+
knownLocalDevState: 'enabled',
|
|
158
|
+
notes: 'Primary status endpoint for numeric Google AI run ids.',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'get-google-ai-search-results',
|
|
162
|
+
category: 'search',
|
|
163
|
+
description: 'Read Google AI summaries/citations by numeric run id.',
|
|
164
|
+
objectType: 'google_ai_summary',
|
|
165
|
+
transport: 'post_edge_function',
|
|
166
|
+
workspaceScoped: true,
|
|
167
|
+
knownLocalDevState: 'enabled',
|
|
168
|
+
notes: 'Use this after a run reaches succeeded/partial/failed to inspect summary-level output.',
|
|
169
|
+
},
|
|
79
170
|
{ name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
|
|
80
171
|
{ name: 'tiktok-geo-api', category: 'search', description: 'Query TikTok search and geo data.' },
|
|
81
172
|
{ name: 'xhs-geo-api', category: 'search', description: 'Query Xiaohongshu search and geo data.' },
|
|
@@ -104,7 +195,16 @@ const KNOWN_TOOLS = [
|
|
|
104
195
|
},
|
|
105
196
|
{ name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
|
|
106
197
|
{ name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
|
|
107
|
-
{
|
|
198
|
+
{
|
|
199
|
+
name: 'search-journey-run',
|
|
200
|
+
category: 'vnext',
|
|
201
|
+
description: 'Run or poll a search journey for a subject across supported platforms.',
|
|
202
|
+
objectType: 'search_journey_run',
|
|
203
|
+
transport: 'post_edge_function',
|
|
204
|
+
workspaceScoped: true,
|
|
205
|
+
knownLocalDevState: 'enabled',
|
|
206
|
+
notes: 'Async start returns runId; poll with action=status or socialseal tools status <runId> --kind journey_run --workspace-id <workspace-id>.',
|
|
207
|
+
},
|
|
108
208
|
{ name: 'vnext-blueprints-create', category: 'vnext', description: 'Create a vNext blueprint from grounded evidence.' },
|
|
109
209
|
{ name: 'vnext-blueprints-generate', category: 'vnext', description: 'Generate a vNext blueprint from workspace opportunity data.' },
|
|
110
210
|
{ name: 'vnext-blueprints-read', category: 'vnext', description: 'Read vNext blueprint history and specific versions.' },
|
|
@@ -120,6 +220,183 @@ const KNOWN_TOOLS = [
|
|
|
120
220
|
{ name: 'vnext-topics-auto-tag', category: 'vnext', description: 'Auto-tag keyword and topic assignments with Gemini-assisted review.' },
|
|
121
221
|
];
|
|
122
222
|
|
|
223
|
+
const TOOL_SCHEMA_HINTS = {
|
|
224
|
+
'agent-tool-jobs': {
|
|
225
|
+
summary: 'Queue agent-backed jobs and read UUID job status.',
|
|
226
|
+
operations: [
|
|
227
|
+
{
|
|
228
|
+
action: 'start',
|
|
229
|
+
required: ['action=start', 'toolName', 'payload'],
|
|
230
|
+
optional: [],
|
|
231
|
+
example: {
|
|
232
|
+
action: 'start',
|
|
233
|
+
toolName: 'search_videos',
|
|
234
|
+
payload: {
|
|
235
|
+
query: 'best africa safari itinerary',
|
|
236
|
+
platform: 'tiktok',
|
|
237
|
+
region: 'IN',
|
|
238
|
+
limit: 20,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
action: 'status',
|
|
244
|
+
required: ['action=status', 'jobId (uuid)'],
|
|
245
|
+
optional: [],
|
|
246
|
+
example: {
|
|
247
|
+
action: 'status',
|
|
248
|
+
jobId: '11111111-1111-4111-8111-111111111111',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
cliExamples: [
|
|
253
|
+
'socialseal tools call --function agent-tool-jobs --body \'{"action":"start","toolName":"search_videos","payload":{"query":"best africa safari itinerary","platform":"tiktok","region":"IN"}}\'',
|
|
254
|
+
'socialseal tools status 11111111-1111-4111-8111-111111111111 --kind agent_job',
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
'search-journey-run': {
|
|
258
|
+
summary: 'Start/poll journey keyword expansion runs.',
|
|
259
|
+
operations: [
|
|
260
|
+
{
|
|
261
|
+
action: 'start',
|
|
262
|
+
required: ['subject', 'subjectType', 'region', 'workspaceId'],
|
|
263
|
+
optional: [
|
|
264
|
+
'locale',
|
|
265
|
+
'platformKeys',
|
|
266
|
+
'seedKeywords',
|
|
267
|
+
'contentPillars',
|
|
268
|
+
'contentPillarIds',
|
|
269
|
+
'maxKeywords',
|
|
270
|
+
'maxKeywordsPerStage',
|
|
271
|
+
'includeRejected',
|
|
272
|
+
'skipCache',
|
|
273
|
+
'executionMode',
|
|
274
|
+
],
|
|
275
|
+
example: {
|
|
276
|
+
subject: 'Como Hotels',
|
|
277
|
+
subjectType: 'brand',
|
|
278
|
+
region: 'IN',
|
|
279
|
+
locale: 'en-IN',
|
|
280
|
+
workspaceId: '00000000-0000-4000-8000-000000000000',
|
|
281
|
+
executionMode: 'async',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
action: 'status',
|
|
286
|
+
required: ['action=status', 'workspaceId', 'runId (uuid)'],
|
|
287
|
+
optional: [],
|
|
288
|
+
example: {
|
|
289
|
+
action: 'status',
|
|
290
|
+
workspaceId: '00000000-0000-4000-8000-000000000000',
|
|
291
|
+
runId: '11111111-1111-4111-8111-111111111111',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
cliExamples: [
|
|
296
|
+
'socialseal tools call --function search-journey-run --body @journey.json --async --workspace-id <workspace-uuid>',
|
|
297
|
+
'socialseal tools status 11111111-1111-4111-8111-111111111111 --kind journey_run --workspace-id <workspace-uuid>',
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
'google-ai-search': {
|
|
301
|
+
summary: 'Queue Google AI runs (returns numeric runId).',
|
|
302
|
+
operations: [
|
|
303
|
+
{
|
|
304
|
+
action: 'start',
|
|
305
|
+
required: ['queries'],
|
|
306
|
+
optional: [
|
|
307
|
+
'workspaceId',
|
|
308
|
+
'trackingItemId',
|
|
309
|
+
'countryCode',
|
|
310
|
+
'searchLanguage',
|
|
311
|
+
'brandId',
|
|
312
|
+
'competitorBrandIds',
|
|
313
|
+
'brandDomains',
|
|
314
|
+
'competitorDomains',
|
|
315
|
+
'aiMode',
|
|
316
|
+
],
|
|
317
|
+
notes: 'region is commonly used in workflows, but the canonical field is countryCode.',
|
|
318
|
+
example: {
|
|
319
|
+
queries: ['east africa itinerary', 'kenya tanzania itinerary'],
|
|
320
|
+
countryCode: 'in',
|
|
321
|
+
searchLanguage: 'en',
|
|
322
|
+
workspaceId: '00000000-0000-4000-8000-000000000000',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
cliExamples: [
|
|
327
|
+
'socialseal tools call --function google-ai-search --body @google-ai-search.json --workspace-id <workspace-uuid>',
|
|
328
|
+
'socialseal tools status 6809 --kind google_ai_run',
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
'get-google-ai-search-runs': {
|
|
332
|
+
summary: 'Read Google AI run status/progress.',
|
|
333
|
+
operations: [
|
|
334
|
+
{
|
|
335
|
+
action: 'read',
|
|
336
|
+
required: [],
|
|
337
|
+
optional: ['runId', 'trackingItemId', 'limit', 'offset', 'skipCache'],
|
|
338
|
+
example: {
|
|
339
|
+
runId: 6809,
|
|
340
|
+
limit: 1,
|
|
341
|
+
offset: 0,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
cliExamples: [
|
|
346
|
+
'socialseal tools call --function get-google-ai-search-runs --body \'{"runId":6809,"limit":1}\'',
|
|
347
|
+
'socialseal tools status 6809 --kind google_ai_run',
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
'get-google-ai-search-results': {
|
|
351
|
+
summary: 'Read Google AI summary/citation rows.',
|
|
352
|
+
operations: [
|
|
353
|
+
{
|
|
354
|
+
action: 'read',
|
|
355
|
+
required: [],
|
|
356
|
+
optional: ['runId', 'query', 'trackingItemId', 'includeCitations', 'limit', 'offset', 'skipCache'],
|
|
357
|
+
example: {
|
|
358
|
+
runId: 6809,
|
|
359
|
+
includeCitations: true,
|
|
360
|
+
limit: 10,
|
|
361
|
+
offset: 0,
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
cliExamples: [
|
|
366
|
+
'socialseal tools call --function get-google-ai-search-results --body \'{"runId":6809,"includeCitations":true,"limit":10}\'',
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
function getToolSchemaHint(functionName) {
|
|
372
|
+
if (!functionName) return null;
|
|
373
|
+
return TOOL_SCHEMA_HINTS[functionName] || null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getKnownTool(functionName) {
|
|
377
|
+
return KNOWN_TOOLS.find((tool) => tool.name === functionName) || null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildSchemaAvailabilitySummary(schema) {
|
|
381
|
+
const firstOperation = Array.isArray(schema?.operations) ? schema.operations[0] : null;
|
|
382
|
+
if (!firstOperation || !Array.isArray(firstOperation.required) || firstOperation.required.length === 0) {
|
|
383
|
+
return 'optional body fields vary by read filter';
|
|
384
|
+
}
|
|
385
|
+
return `required: ${firstOperation.required.join(', ')}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildToolRegistry() {
|
|
389
|
+
return KNOWN_TOOLS.map((tool) => {
|
|
390
|
+
const schema = getToolSchemaHint(tool.name);
|
|
391
|
+
if (!schema) return tool;
|
|
392
|
+
return {
|
|
393
|
+
...tool,
|
|
394
|
+
schemaAvailable: true,
|
|
395
|
+
schemaSummary: buildSchemaAvailabilitySummary(schema),
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
123
400
|
function getConfigPath() {
|
|
124
401
|
return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
|
|
125
402
|
}
|
|
@@ -675,6 +952,60 @@ function isUuidLike(value) {
|
|
|
675
952
|
return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(value.trim());
|
|
676
953
|
}
|
|
677
954
|
|
|
955
|
+
function isPositiveIntegerString(value) {
|
|
956
|
+
return typeof value === 'string' && /^[1-9]\d*$/.test(value.trim());
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function normalizeStatusIdentifier(rawId) {
|
|
960
|
+
const id = trimString(rawId);
|
|
961
|
+
if (!id) {
|
|
962
|
+
throw new CliError('Missing status identifier.', {
|
|
963
|
+
code: 'MISSING_ARGUMENT',
|
|
964
|
+
exitCode: EXIT_CODES.USAGE,
|
|
965
|
+
hint: 'Pass a numeric Google AI runId or a UUID job/run id.',
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (isPositiveIntegerString(id)) {
|
|
970
|
+
const parsed = Number(id);
|
|
971
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) {
|
|
972
|
+
return { rawId: id, numericId: parsed, uuidId: null };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (isUuidLike(id)) {
|
|
977
|
+
return { rawId: id, numericId: null, uuidId: id };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
throw new CliError(`Unsupported status identifier: ${id}`, {
|
|
981
|
+
code: 'INVALID_ARGUMENT',
|
|
982
|
+
exitCode: EXIT_CODES.USAGE,
|
|
983
|
+
hint: 'Use a numeric Google AI runId (for example 6809) or UUID job/run id.',
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function parseToolStatusKind(rawKind) {
|
|
988
|
+
const normalized = trimString(rawKind || 'auto').toLowerCase();
|
|
989
|
+
if (TOOL_STATUS_KINDS.has(normalized)) {
|
|
990
|
+
return normalized;
|
|
991
|
+
}
|
|
992
|
+
throw new CliError(`Unsupported tools status kind: ${rawKind}`, {
|
|
993
|
+
code: 'INVALID_ARGUMENT',
|
|
994
|
+
exitCode: EXIT_CODES.USAGE,
|
|
995
|
+
hint: 'Use --kind auto|agent_job|google_ai_run|journey_run.',
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function normalizeStatusValue(value) {
|
|
1000
|
+
return trimString(value).toLowerCase();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function isTerminalStatusValue(value) {
|
|
1004
|
+
const normalized = normalizeStatusValue(value);
|
|
1005
|
+
if (!normalized) return true;
|
|
1006
|
+
return !ACTIVE_STATUS_VALUES.has(normalized);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
678
1009
|
function coercePositiveInteger(value, label) {
|
|
679
1010
|
if (value === undefined || value === null || value === '') return undefined;
|
|
680
1011
|
const parsed = Number(value);
|
|
@@ -691,6 +1022,88 @@ function coercePositiveInteger(value, label) {
|
|
|
691
1022
|
});
|
|
692
1023
|
}
|
|
693
1024
|
|
|
1025
|
+
function normalizePositiveIntegerList(value, label, { max } = {}) {
|
|
1026
|
+
if (value === undefined || value === null || value === '') return [];
|
|
1027
|
+
const entries = Array.isArray(value)
|
|
1028
|
+
? value
|
|
1029
|
+
: String(value)
|
|
1030
|
+
.split(',')
|
|
1031
|
+
.map((entry) => entry.trim())
|
|
1032
|
+
.filter(Boolean);
|
|
1033
|
+
const parsed = entries.map((entry, index) => {
|
|
1034
|
+
const normalized = coercePositiveInteger(entry, `${label}[${index}]`);
|
|
1035
|
+
if (!normalized) {
|
|
1036
|
+
throw new CliError(`Invalid ${label}[${index}] value.`, {
|
|
1037
|
+
code: 'INVALID_ARGUMENT',
|
|
1038
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return normalized;
|
|
1042
|
+
});
|
|
1043
|
+
const deduped = Array.from(new Set(parsed));
|
|
1044
|
+
if (max && deduped.length > max) {
|
|
1045
|
+
throw new CliError(`Too many ${label} values: received ${deduped.length}, max is ${max}.`, {
|
|
1046
|
+
code: 'INVALID_ARGUMENT',
|
|
1047
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
return deduped;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function normalizeIsoDateTime(value, label) {
|
|
1054
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
1055
|
+
if (typeof value !== 'string') {
|
|
1056
|
+
throw new CliError(`Invalid ${label}: expected an ISO datetime string.`, {
|
|
1057
|
+
code: 'INVALID_ARGUMENT',
|
|
1058
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
const trimmed = value.trim();
|
|
1062
|
+
const epoch = Date.parse(trimmed);
|
|
1063
|
+
if (Number.isNaN(epoch)) {
|
|
1064
|
+
throw new CliError(`Invalid ${label}: expected an ISO datetime string.`, {
|
|
1065
|
+
code: 'INVALID_ARGUMENT',
|
|
1066
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1067
|
+
details: value,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return new Date(epoch).toISOString();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function buildSearchResultsEnrichedExportPayload(rawPayload, workspaceId) {
|
|
1074
|
+
const payload = isJsonObject(rawPayload) ? rawPayload : {};
|
|
1075
|
+
const groupIds = normalizePositiveIntegerList(
|
|
1076
|
+
firstDefined(payload, ['groupIds', 'group_ids']),
|
|
1077
|
+
'groupIds',
|
|
1078
|
+
{ max: 100 },
|
|
1079
|
+
);
|
|
1080
|
+
if (groupIds.length === 0) {
|
|
1081
|
+
throw new CliError('search_results_enriched export requires at least one group id.', {
|
|
1082
|
+
code: 'MISSING_ARGUMENT',
|
|
1083
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1084
|
+
hint: 'Provide --group-ids for `data export-search-results`, or include groupIds in --payload for `data export-report`.',
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const trackingItemIds = normalizePositiveIntegerList(
|
|
1089
|
+
firstDefined(payload, ['trackingItemIds', 'tracking_item_ids']),
|
|
1090
|
+
'trackingItemIds',
|
|
1091
|
+
{ max: 1000 },
|
|
1092
|
+
);
|
|
1093
|
+
const dateFrom = normalizeIsoDateTime(firstDefined(payload, ['dateFrom', 'date_from']), 'dateFrom');
|
|
1094
|
+
const dateTo = normalizeIsoDateTime(firstDefined(payload, ['dateTo', 'date_to']), 'dateTo');
|
|
1095
|
+
const filename = trimString(firstDefined(payload, ['filename'])) || undefined;
|
|
1096
|
+
|
|
1097
|
+
return stripUndefinedEntries({
|
|
1098
|
+
workspaceId,
|
|
1099
|
+
groupIds,
|
|
1100
|
+
trackingItemIds: trackingItemIds.length > 0 ? trackingItemIds : undefined,
|
|
1101
|
+
dateFrom,
|
|
1102
|
+
dateTo,
|
|
1103
|
+
filename,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
694
1107
|
function buildPathWithQuery(basePath, query) {
|
|
695
1108
|
const params = new URLSearchParams();
|
|
696
1109
|
for (const [key, rawValue] of Object.entries(query || {})) {
|
|
@@ -1669,14 +2082,17 @@ async function pollSearchJourneyRun({
|
|
|
1669
2082
|
|
|
1670
2083
|
const data = await res.json();
|
|
1671
2084
|
const status = isJsonObject(data) && typeof data.status === 'string' ? data.status : null;
|
|
2085
|
+
const normalizedStatus = normalizeStatusValue(status);
|
|
1672
2086
|
if (status && status !== lastStatus) {
|
|
1673
2087
|
emitInfo(opts, `search-journey-run status: ${status}`);
|
|
1674
2088
|
lastStatus = status;
|
|
1675
2089
|
}
|
|
1676
2090
|
|
|
1677
|
-
if (
|
|
1678
|
-
if (
|
|
1679
|
-
|
|
2091
|
+
if (normalizedStatus === 'completed' || normalizedStatus === 'succeeded') return data;
|
|
2092
|
+
if (normalizedStatus === 'failed' || normalizedStatus === 'error') {
|
|
2093
|
+
throw buildSearchJourneyRunFailure(data);
|
|
2094
|
+
}
|
|
2095
|
+
if (normalizedStatus && ACTIVE_STATUS_VALUES.has(normalizedStatus)) continue;
|
|
1680
2096
|
|
|
1681
2097
|
throw new CliError('search-journey-run status poll returned an unexpected payload.', {
|
|
1682
2098
|
code: 'INVALID_STATUS_RESPONSE',
|
|
@@ -1695,7 +2111,9 @@ function mapStatusToExitCode(status) {
|
|
|
1695
2111
|
}
|
|
1696
2112
|
|
|
1697
2113
|
function isLocallyDisabledByDefaultFunction(functionName) {
|
|
1698
|
-
return functionName === 'group-management'
|
|
2114
|
+
return functionName === 'group-management'
|
|
2115
|
+
|| functionName === 'export_tracking_data'
|
|
2116
|
+
|| functionName === 'export-data';
|
|
1699
2117
|
}
|
|
1700
2118
|
|
|
1701
2119
|
function buildStatusHint(status, context = {}) {
|
|
@@ -1753,6 +2171,428 @@ async function buildHttpError(res, context = {}) {
|
|
|
1753
2171
|
});
|
|
1754
2172
|
}
|
|
1755
2173
|
|
|
2174
|
+
function resolveStatusResultsLimit(rawLimit) {
|
|
2175
|
+
if (rawLimit == null || rawLimit === '') return DEFAULT_STATUS_RESULTS_LIMIT;
|
|
2176
|
+
const parsed = Number(rawLimit);
|
|
2177
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2178
|
+
throw new CliError('Invalid results limit. Use a positive integer.', {
|
|
2179
|
+
code: 'INVALID_ARGUMENT',
|
|
2180
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2181
|
+
hint: 'Use --results-limit <n> where n is between 1 and 50.',
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return Math.min(parsed, 50);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
async function callToolJson({
|
|
2188
|
+
apiBase,
|
|
2189
|
+
apiKey,
|
|
2190
|
+
useGateway,
|
|
2191
|
+
legacyUrl,
|
|
2192
|
+
functionName,
|
|
2193
|
+
body,
|
|
2194
|
+
workspaceId,
|
|
2195
|
+
timeoutMs,
|
|
2196
|
+
label,
|
|
2197
|
+
}) {
|
|
2198
|
+
const path = useGateway ? `/cli/tools/${functionName}` : `/functions/v1/${functionName}`;
|
|
2199
|
+
const res = await callApi({
|
|
2200
|
+
apiBase: useGateway ? apiBase : legacyUrl,
|
|
2201
|
+
apiKey,
|
|
2202
|
+
path,
|
|
2203
|
+
method: 'POST',
|
|
2204
|
+
body,
|
|
2205
|
+
workspaceId,
|
|
2206
|
+
timeoutMs,
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
if (res.status === 404) {
|
|
2210
|
+
return { notFound: true, data: null };
|
|
2211
|
+
}
|
|
2212
|
+
if (!res.ok) {
|
|
2213
|
+
throw await buildHttpError(res, {
|
|
2214
|
+
label,
|
|
2215
|
+
functionName,
|
|
2216
|
+
method: 'POST',
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2221
|
+
if (!contentType.includes('application/json')) {
|
|
2222
|
+
throw new CliError(`${label} returned a non-JSON response.`, {
|
|
2223
|
+
code: 'INVALID_RESPONSE',
|
|
2224
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
return { notFound: false, data: await res.json() };
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
async function readAgentToolJobStatus({
|
|
2232
|
+
apiBase,
|
|
2233
|
+
apiKey,
|
|
2234
|
+
useGateway,
|
|
2235
|
+
legacyUrl,
|
|
2236
|
+
timeoutMs,
|
|
2237
|
+
jobId,
|
|
2238
|
+
}) {
|
|
2239
|
+
const response = await callToolJson({
|
|
2240
|
+
apiBase,
|
|
2241
|
+
apiKey,
|
|
2242
|
+
useGateway,
|
|
2243
|
+
legacyUrl,
|
|
2244
|
+
functionName: 'agent-tool-jobs',
|
|
2245
|
+
body: { action: 'status', jobId },
|
|
2246
|
+
workspaceId: null,
|
|
2247
|
+
timeoutMs,
|
|
2248
|
+
label: 'Agent tool job status',
|
|
2249
|
+
});
|
|
2250
|
+
if (response.notFound) return null;
|
|
2251
|
+
|
|
2252
|
+
const data = response.data;
|
|
2253
|
+
return {
|
|
2254
|
+
kind: 'agent_tool_job',
|
|
2255
|
+
id: jobId,
|
|
2256
|
+
status: isJsonObject(data) ? trimString(data.status || '') : '',
|
|
2257
|
+
workspaceId: isJsonObject(data) ? (data.workspaceId ?? null) : null,
|
|
2258
|
+
toolName: isJsonObject(data) ? (data.toolName ?? null) : null,
|
|
2259
|
+
result: isJsonObject(data) ? (data.result ?? null) : null,
|
|
2260
|
+
error: isJsonObject(data) ? (data.error ?? null) : null,
|
|
2261
|
+
raw: data,
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
async function readSearchJourneyRunStatus({
|
|
2266
|
+
apiBase,
|
|
2267
|
+
apiKey,
|
|
2268
|
+
useGateway,
|
|
2269
|
+
legacyUrl,
|
|
2270
|
+
timeoutMs,
|
|
2271
|
+
workspaceId,
|
|
2272
|
+
runId,
|
|
2273
|
+
}) {
|
|
2274
|
+
if (!workspaceId) {
|
|
2275
|
+
throw new CliError('Search journey run status requires a workspace id.', {
|
|
2276
|
+
code: 'WORKSPACE_REQUIRED',
|
|
2277
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2278
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const response = await callToolJson({
|
|
2283
|
+
apiBase,
|
|
2284
|
+
apiKey,
|
|
2285
|
+
useGateway,
|
|
2286
|
+
legacyUrl,
|
|
2287
|
+
functionName: 'search-journey-run',
|
|
2288
|
+
body: { action: 'status', workspaceId, runId },
|
|
2289
|
+
workspaceId,
|
|
2290
|
+
timeoutMs,
|
|
2291
|
+
label: 'Search journey status',
|
|
2292
|
+
});
|
|
2293
|
+
if (response.notFound) return null;
|
|
2294
|
+
|
|
2295
|
+
const data = response.data;
|
|
2296
|
+
return {
|
|
2297
|
+
kind: 'search_journey_run',
|
|
2298
|
+
id: runId,
|
|
2299
|
+
status: isJsonObject(data) ? trimString(data.status || '') : '',
|
|
2300
|
+
workspaceId,
|
|
2301
|
+
journeyId: isJsonObject(data) ? (data.journeyId ?? null) : null,
|
|
2302
|
+
stagedKeywordsCount:
|
|
2303
|
+
isJsonObject(data) && Array.isArray(data.stagedKeywords) ? data.stagedKeywords.length : 0,
|
|
2304
|
+
error: isJsonObject(data) ? (data.error ?? null) : null,
|
|
2305
|
+
raw: data,
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
async function readGoogleAiRunStatus({
|
|
2310
|
+
apiBase,
|
|
2311
|
+
apiKey,
|
|
2312
|
+
useGateway,
|
|
2313
|
+
legacyUrl,
|
|
2314
|
+
timeoutMs,
|
|
2315
|
+
runId,
|
|
2316
|
+
includeResults,
|
|
2317
|
+
resultsLimit,
|
|
2318
|
+
}) {
|
|
2319
|
+
const runsResponse = await callToolJson({
|
|
2320
|
+
apiBase,
|
|
2321
|
+
apiKey,
|
|
2322
|
+
useGateway,
|
|
2323
|
+
legacyUrl,
|
|
2324
|
+
functionName: 'get-google-ai-search-runs',
|
|
2325
|
+
body: {
|
|
2326
|
+
runId,
|
|
2327
|
+
limit: 1,
|
|
2328
|
+
offset: 0,
|
|
2329
|
+
},
|
|
2330
|
+
workspaceId: null,
|
|
2331
|
+
timeoutMs,
|
|
2332
|
+
label: 'Google AI run status',
|
|
2333
|
+
});
|
|
2334
|
+
if (runsResponse.notFound) return null;
|
|
2335
|
+
|
|
2336
|
+
const runsPayload = runsResponse.data;
|
|
2337
|
+
const runItem = isJsonObject(runsPayload) && Array.isArray(runsPayload.items)
|
|
2338
|
+
? runsPayload.items.find((item) => isJsonObject(item) && Number(item.id) === runId) || null
|
|
2339
|
+
: null;
|
|
2340
|
+
|
|
2341
|
+
if (!runItem) return null;
|
|
2342
|
+
|
|
2343
|
+
let resultsPayload = null;
|
|
2344
|
+
if (includeResults) {
|
|
2345
|
+
const resultsResponse = await callToolJson({
|
|
2346
|
+
apiBase,
|
|
2347
|
+
apiKey,
|
|
2348
|
+
useGateway,
|
|
2349
|
+
legacyUrl,
|
|
2350
|
+
functionName: 'get-google-ai-search-results',
|
|
2351
|
+
body: {
|
|
2352
|
+
runId,
|
|
2353
|
+
includeCitations: true,
|
|
2354
|
+
limit: resultsLimit,
|
|
2355
|
+
offset: 0,
|
|
2356
|
+
},
|
|
2357
|
+
workspaceId: null,
|
|
2358
|
+
timeoutMs,
|
|
2359
|
+
label: 'Google AI run results',
|
|
2360
|
+
});
|
|
2361
|
+
resultsPayload = resultsResponse.notFound ? null : resultsResponse.data;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return {
|
|
2365
|
+
kind: 'google_ai_run',
|
|
2366
|
+
id: runId,
|
|
2367
|
+
status: trimString(runItem.status || ''),
|
|
2368
|
+
progress: {
|
|
2369
|
+
completedQueries: typeof runItem.completedQueries === 'number' ? runItem.completedQueries : null,
|
|
2370
|
+
totalQueries: typeof runItem.totalQueries === 'number' ? runItem.totalQueries : null,
|
|
2371
|
+
progressPercent: typeof runItem.progressPercent === 'number' ? runItem.progressPercent : null,
|
|
2372
|
+
},
|
|
2373
|
+
lastErrorMessage: runItem.lastErrorMessage ?? null,
|
|
2374
|
+
run: runItem,
|
|
2375
|
+
results: resultsPayload,
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
function buildToolStatusNotFoundError(identifier, kind, workspaceId) {
|
|
2380
|
+
if (kind === 'google_ai_run') {
|
|
2381
|
+
return new CliError(`Google AI run not found: ${identifier.rawId}`, {
|
|
2382
|
+
code: 'STATUS_NOT_FOUND',
|
|
2383
|
+
exitCode: EXIT_CODES.NOT_FOUND,
|
|
2384
|
+
hint: 'Verify the run id and key scope, then retry with --kind google_ai_run.',
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
if (kind === 'agent_job') {
|
|
2388
|
+
return new CliError(`Agent tool job not found: ${identifier.rawId}`, {
|
|
2389
|
+
code: 'STATUS_NOT_FOUND',
|
|
2390
|
+
exitCode: EXIT_CODES.NOT_FOUND,
|
|
2391
|
+
hint: 'Verify the UUID job id, then retry with --kind agent_job.',
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
if (kind === 'journey_run') {
|
|
2395
|
+
return new CliError(`Search journey run not found: ${identifier.rawId}`, {
|
|
2396
|
+
code: 'STATUS_NOT_FOUND',
|
|
2397
|
+
exitCode: EXIT_CODES.NOT_FOUND,
|
|
2398
|
+
hint: 'Verify --workspace-id and the journey run UUID, then retry.',
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
return new CliError(`No matching status record found for ${identifier.rawId}.`, {
|
|
2402
|
+
code: 'STATUS_NOT_FOUND',
|
|
2403
|
+
exitCode: EXIT_CODES.NOT_FOUND,
|
|
2404
|
+
hint: workspaceId
|
|
2405
|
+
? 'Try --kind agent_job or --kind journey_run explicitly.'
|
|
2406
|
+
: 'Try --kind agent_job or provide --workspace-id to also check journey runs.',
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
async function resolveUnifiedToolStatus({
|
|
2411
|
+
apiBase,
|
|
2412
|
+
apiKey,
|
|
2413
|
+
useGateway,
|
|
2414
|
+
legacyUrl,
|
|
2415
|
+
timeoutMs,
|
|
2416
|
+
identifier,
|
|
2417
|
+
kind,
|
|
2418
|
+
workspaceId,
|
|
2419
|
+
includeResults,
|
|
2420
|
+
resultsLimit,
|
|
2421
|
+
}) {
|
|
2422
|
+
if (kind === 'google_ai_run') {
|
|
2423
|
+
if (identifier.numericId == null) {
|
|
2424
|
+
throw new CliError('google_ai_run status expects a numeric run id.', {
|
|
2425
|
+
code: 'INVALID_ARGUMENT',
|
|
2426
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
const result = await readGoogleAiRunStatus({
|
|
2430
|
+
apiBase,
|
|
2431
|
+
apiKey,
|
|
2432
|
+
useGateway,
|
|
2433
|
+
legacyUrl,
|
|
2434
|
+
timeoutMs,
|
|
2435
|
+
runId: identifier.numericId,
|
|
2436
|
+
includeResults,
|
|
2437
|
+
resultsLimit,
|
|
2438
|
+
});
|
|
2439
|
+
if (result) return result;
|
|
2440
|
+
throw buildToolStatusNotFoundError(identifier, kind, workspaceId);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
if (kind === 'agent_job') {
|
|
2444
|
+
if (!identifier.uuidId) {
|
|
2445
|
+
throw new CliError('agent_job status expects a UUID id.', {
|
|
2446
|
+
code: 'INVALID_ARGUMENT',
|
|
2447
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
const result = await readAgentToolJobStatus({
|
|
2451
|
+
apiBase,
|
|
2452
|
+
apiKey,
|
|
2453
|
+
useGateway,
|
|
2454
|
+
legacyUrl,
|
|
2455
|
+
timeoutMs,
|
|
2456
|
+
jobId: identifier.uuidId,
|
|
2457
|
+
});
|
|
2458
|
+
if (result) return result;
|
|
2459
|
+
throw buildToolStatusNotFoundError(identifier, kind, workspaceId);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (kind === 'journey_run') {
|
|
2463
|
+
if (!identifier.uuidId) {
|
|
2464
|
+
throw new CliError('journey_run status expects a UUID run id.', {
|
|
2465
|
+
code: 'INVALID_ARGUMENT',
|
|
2466
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
const result = await readSearchJourneyRunStatus({
|
|
2470
|
+
apiBase,
|
|
2471
|
+
apiKey,
|
|
2472
|
+
useGateway,
|
|
2473
|
+
legacyUrl,
|
|
2474
|
+
timeoutMs,
|
|
2475
|
+
workspaceId,
|
|
2476
|
+
runId: identifier.uuidId,
|
|
2477
|
+
});
|
|
2478
|
+
if (result) return result;
|
|
2479
|
+
throw buildToolStatusNotFoundError(identifier, kind, workspaceId);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (identifier.numericId != null) {
|
|
2483
|
+
const result = await readGoogleAiRunStatus({
|
|
2484
|
+
apiBase,
|
|
2485
|
+
apiKey,
|
|
2486
|
+
useGateway,
|
|
2487
|
+
legacyUrl,
|
|
2488
|
+
timeoutMs,
|
|
2489
|
+
runId: identifier.numericId,
|
|
2490
|
+
includeResults,
|
|
2491
|
+
resultsLimit,
|
|
2492
|
+
});
|
|
2493
|
+
if (result) return result;
|
|
2494
|
+
throw buildToolStatusNotFoundError(identifier, 'google_ai_run', workspaceId);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
const agentJob = await readAgentToolJobStatus({
|
|
2498
|
+
apiBase,
|
|
2499
|
+
apiKey,
|
|
2500
|
+
useGateway,
|
|
2501
|
+
legacyUrl,
|
|
2502
|
+
timeoutMs,
|
|
2503
|
+
jobId: identifier.uuidId,
|
|
2504
|
+
});
|
|
2505
|
+
if (agentJob) return agentJob;
|
|
2506
|
+
|
|
2507
|
+
if (workspaceId) {
|
|
2508
|
+
const journeyRun = await readSearchJourneyRunStatus({
|
|
2509
|
+
apiBase,
|
|
2510
|
+
apiKey,
|
|
2511
|
+
useGateway,
|
|
2512
|
+
legacyUrl,
|
|
2513
|
+
timeoutMs,
|
|
2514
|
+
workspaceId,
|
|
2515
|
+
runId: identifier.uuidId,
|
|
2516
|
+
});
|
|
2517
|
+
if (journeyRun) return journeyRun;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
throw buildToolStatusNotFoundError(identifier, 'auto', workspaceId);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function buildStatusCommandHint(result, workspaceId) {
|
|
2524
|
+
if (!result || !result.kind) return null;
|
|
2525
|
+
if (result.kind === 'google_ai_run') {
|
|
2526
|
+
return `socialseal tools status ${result.id} --kind google_ai_run`;
|
|
2527
|
+
}
|
|
2528
|
+
if (result.kind === 'agent_tool_job') {
|
|
2529
|
+
return `socialseal tools status ${result.id} --kind agent_job`;
|
|
2530
|
+
}
|
|
2531
|
+
if (result.kind === 'search_journey_run') {
|
|
2532
|
+
const scopedWorkspace = workspaceId || result.workspaceId;
|
|
2533
|
+
if (!scopedWorkspace) return null;
|
|
2534
|
+
return `socialseal tools status ${result.id} --kind journey_run --workspace-id ${scopedWorkspace}`;
|
|
2535
|
+
}
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function maybeEmitFollowupStatusHint({ functionName, data, workspaceId }) {
|
|
2540
|
+
if (!isJsonObject(data)) return;
|
|
2541
|
+
if (functionName === 'google-ai-search' && Number.isInteger(data.runId)) {
|
|
2542
|
+
process.stderr.write(
|
|
2543
|
+
`[socialseal] Google AI run queued: ${data.runId}. Use: socialseal tools status ${data.runId} --kind google_ai_run\n`,
|
|
2544
|
+
);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (functionName === 'agent-tool-jobs' && typeof data.jobId === 'string' && isUuidLike(data.jobId)) {
|
|
2548
|
+
process.stderr.write(
|
|
2549
|
+
`[socialseal] Agent tool job queued: ${data.jobId}. Use: socialseal tools status ${data.jobId} --kind agent_job\n`,
|
|
2550
|
+
);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
if (functionName === 'search-journey-run' && typeof data.runId === 'string' && isUuidLike(data.runId)) {
|
|
2554
|
+
const scopedWorkspace = trimString(workspaceId || data.workspaceId || '');
|
|
2555
|
+
const workspaceFlag = scopedWorkspace ? ` --workspace-id ${scopedWorkspace}` : '';
|
|
2556
|
+
process.stderr.write(
|
|
2557
|
+
`[socialseal] Search journey run id: ${data.runId}. Use: socialseal tools status ${data.runId} --kind journey_run${workspaceFlag}\n`,
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
async function pollUnifiedStatus({
|
|
2563
|
+
loader,
|
|
2564
|
+
timeoutMs,
|
|
2565
|
+
pollIntervalMs,
|
|
2566
|
+
opts,
|
|
2567
|
+
}) {
|
|
2568
|
+
const deadline = Date.now() + timeoutMs;
|
|
2569
|
+
let current = await loader();
|
|
2570
|
+
let lastStatus = normalizeStatusValue(current?.status);
|
|
2571
|
+
|
|
2572
|
+
while (!isTerminalStatusValue(current?.status)) {
|
|
2573
|
+
const remainingMs = deadline - Date.now();
|
|
2574
|
+
if (remainingMs <= 0) {
|
|
2575
|
+
throw new CliError('Timed out waiting for status completion.', {
|
|
2576
|
+
code: 'ASYNC_WAIT_TIMEOUT',
|
|
2577
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2578
|
+
hint: 'Increase --timeout <ms> or omit --wait to return current status immediately.',
|
|
2579
|
+
details: truncateDetails(current),
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
emitInfo(opts, `status: ${current.status}`);
|
|
2584
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
2585
|
+
current = await loader();
|
|
2586
|
+
const normalized = normalizeStatusValue(current?.status);
|
|
2587
|
+
if (normalized && normalized !== lastStatus) {
|
|
2588
|
+
emitInfo(opts, `status: ${current.status}`);
|
|
2589
|
+
lastStatus = normalized;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
return current;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
1756
2596
|
function emitError(err, opts = {}) {
|
|
1757
2597
|
const showDetails = opts.json || opts.verbose;
|
|
1758
2598
|
const payload = {
|
|
@@ -1812,6 +2652,18 @@ function requireApiKey(opts, config) {
|
|
|
1812
2652
|
return apiKey;
|
|
1813
2653
|
}
|
|
1814
2654
|
|
|
2655
|
+
function assertSupportedReportType(reportType) {
|
|
2656
|
+
const normalized = trimString(reportType);
|
|
2657
|
+
if (SUPPORTED_EXPORT_REPORT_TYPES.includes(normalized)) {
|
|
2658
|
+
return normalized;
|
|
2659
|
+
}
|
|
2660
|
+
throw new CliError(`Unsupported report type: ${reportType}`, {
|
|
2661
|
+
code: 'INVALID_ARGUMENT',
|
|
2662
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2663
|
+
hint: `Use one of: ${SUPPORTED_EXPORT_REPORT_TYPES.join(', ')}. Run \`socialseal data export-options\` to choose the right export flow.`,
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
|
|
1815
2667
|
function resolveApiTarget({ apiBase, legacyUrl }) {
|
|
1816
2668
|
const resolvedApiBase = apiBase || (!legacyUrl ? DEFAULT_API_BASE : null);
|
|
1817
2669
|
if (!resolvedApiBase && !legacyUrl) {
|
|
@@ -2359,6 +3211,11 @@ async function handleToolsCall(opts) {
|
|
|
2359
3211
|
const data = await res.json();
|
|
2360
3212
|
const shouldPoll = shouldHandleSearchJourneyRunAsync(opts.function, method, payload, opts) && opts.poll !== false;
|
|
2361
3213
|
if (!shouldPoll) {
|
|
3214
|
+
maybeEmitFollowupStatusHint({
|
|
3215
|
+
functionName: opts.function,
|
|
3216
|
+
data,
|
|
3217
|
+
workspaceId: effectiveWorkspaceId,
|
|
3218
|
+
});
|
|
2362
3219
|
emitJsonOutput(data, opts.pretty);
|
|
2363
3220
|
return;
|
|
2364
3221
|
}
|
|
@@ -2386,9 +3243,7 @@ async function handleToolsCall(opts) {
|
|
|
2386
3243
|
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
2387
3244
|
apiKey,
|
|
2388
3245
|
path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
|
|
2389
|
-
workspaceId:
|
|
2390
|
-
? payload.workspaceId
|
|
2391
|
-
: resolvedWorkspaceId,
|
|
3246
|
+
workspaceId: effectiveWorkspaceId,
|
|
2392
3247
|
timeoutMs,
|
|
2393
3248
|
pollIntervalMs: resolvePollIntervalMs(opts),
|
|
2394
3249
|
runId,
|
|
@@ -2404,10 +3259,12 @@ async function handleToolsCall(opts) {
|
|
|
2404
3259
|
}
|
|
2405
3260
|
|
|
2406
3261
|
function handleToolsList(opts) {
|
|
3262
|
+
const tools = buildToolRegistry();
|
|
2407
3263
|
const payload = {
|
|
2408
3264
|
discovery: 'built_in_registry',
|
|
2409
|
-
tools
|
|
3265
|
+
tools,
|
|
2410
3266
|
note: STATIC_TOOL_REGISTRY_NOTE,
|
|
3267
|
+
schemaNote: STATIC_TOOL_SCHEMA_NOTE,
|
|
2411
3268
|
};
|
|
2412
3269
|
|
|
2413
3270
|
if (opts.json) {
|
|
@@ -2417,9 +3274,10 @@ function handleToolsList(opts) {
|
|
|
2417
3274
|
|
|
2418
3275
|
process.stdout.write('[socialseal] Built-in tool registry\n');
|
|
2419
3276
|
process.stdout.write(`[socialseal] ${payload.note}\n`);
|
|
3277
|
+
process.stdout.write(`[socialseal] ${payload.schemaNote}\n`);
|
|
2420
3278
|
|
|
2421
3279
|
let currentCategory = null;
|
|
2422
|
-
for (const tool of
|
|
3280
|
+
for (const tool of tools) {
|
|
2423
3281
|
if (tool.category !== currentCategory) {
|
|
2424
3282
|
currentCategory = tool.category;
|
|
2425
3283
|
process.stdout.write(`\n${currentCategory}\n`);
|
|
@@ -2434,9 +3292,148 @@ function handleToolsList(opts) {
|
|
|
2434
3292
|
if (tool.notes) {
|
|
2435
3293
|
process.stdout.write(` note: ${tool.notes}\n`);
|
|
2436
3294
|
}
|
|
3295
|
+
if (tool.schemaAvailable) {
|
|
3296
|
+
process.stdout.write(` schema: ${tool.schemaSummary}\n`);
|
|
3297
|
+
process.stdout.write(` schema command: socialseal tools schema --function ${tool.name}\n`);
|
|
3298
|
+
}
|
|
2437
3299
|
}
|
|
2438
3300
|
|
|
2439
3301
|
process.stdout.write('\n[socialseal] Call a tool with: socialseal tools call --function <name> --body @payload.json\n');
|
|
3302
|
+
process.stdout.write('[socialseal] Inspect schema examples with: socialseal tools schema --function <name>\n');
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
function handleToolsSchema(opts) {
|
|
3306
|
+
const tools = buildToolRegistry();
|
|
3307
|
+
const functionName = trimString(opts.function || '');
|
|
3308
|
+
|
|
3309
|
+
if (!functionName) {
|
|
3310
|
+
const payload = {
|
|
3311
|
+
note: STATIC_TOOL_SCHEMA_NOTE,
|
|
3312
|
+
schemas: tools
|
|
3313
|
+
.filter((tool) => tool.schemaAvailable)
|
|
3314
|
+
.map((tool) => ({
|
|
3315
|
+
function: tool.name,
|
|
3316
|
+
summary: tool.schemaSummary,
|
|
3317
|
+
details: getToolSchemaHint(tool.name),
|
|
3318
|
+
})),
|
|
3319
|
+
};
|
|
3320
|
+
|
|
3321
|
+
if (opts.json) {
|
|
3322
|
+
emitJsonOutput(payload, opts.pretty);
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
process.stdout.write('[socialseal] Tool schema hints\n');
|
|
3327
|
+
process.stdout.write(`[socialseal] ${payload.note}\n`);
|
|
3328
|
+
for (const schemaEntry of payload.schemas) {
|
|
3329
|
+
process.stdout.write(`- ${schemaEntry.function}: ${schemaEntry.summary}\n`);
|
|
3330
|
+
process.stdout.write(` command: socialseal tools schema --function ${schemaEntry.function}\n`);
|
|
3331
|
+
}
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
const knownTool = getKnownTool(functionName);
|
|
3336
|
+
if (!knownTool) {
|
|
3337
|
+
throw new CliError(`Unknown tool: ${functionName}`, {
|
|
3338
|
+
code: 'INVALID_ARGUMENT',
|
|
3339
|
+
exitCode: EXIT_CODES.USAGE,
|
|
3340
|
+
hint: 'Run `socialseal tools list` to discover available tool names.',
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
const schema = getToolSchemaHint(functionName);
|
|
3345
|
+
if (!schema) {
|
|
3346
|
+
throw new CliError(`No schema hint is bundled for ${functionName}.`, {
|
|
3347
|
+
code: 'SCHEMA_NOT_AVAILABLE',
|
|
3348
|
+
exitCode: EXIT_CODES.NOT_FOUND,
|
|
3349
|
+
hint: 'Use `socialseal tools call --function <tool> --body @payload.json` and inspect backend validation errors for this tool.',
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
const payload = {
|
|
3354
|
+
function: functionName,
|
|
3355
|
+
note: STATIC_TOOL_SCHEMA_NOTE,
|
|
3356
|
+
schema,
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
if (opts.json) {
|
|
3360
|
+
emitJsonOutput(payload, opts.pretty);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
process.stdout.write(`[socialseal] Tool schema: ${functionName}\n`);
|
|
3365
|
+
process.stdout.write(`[socialseal] ${payload.note}\n`);
|
|
3366
|
+
process.stdout.write(`summary: ${schema.summary}\n`);
|
|
3367
|
+
if (Array.isArray(schema.operations) && schema.operations.length > 0) {
|
|
3368
|
+
process.stdout.write('operations:\n');
|
|
3369
|
+
for (const operation of schema.operations) {
|
|
3370
|
+
process.stdout.write(`- ${operation.action}\n`);
|
|
3371
|
+
if (Array.isArray(operation.required) && operation.required.length > 0) {
|
|
3372
|
+
process.stdout.write(` required: ${operation.required.join(', ')}\n`);
|
|
3373
|
+
}
|
|
3374
|
+
if (Array.isArray(operation.optional) && operation.optional.length > 0) {
|
|
3375
|
+
process.stdout.write(` optional: ${operation.optional.join(', ')}\n`);
|
|
3376
|
+
}
|
|
3377
|
+
if (operation.notes) {
|
|
3378
|
+
process.stdout.write(` note: ${operation.notes}\n`);
|
|
3379
|
+
}
|
|
3380
|
+
if (operation.example) {
|
|
3381
|
+
process.stdout.write(` example body: ${JSON.stringify(operation.example)}\n`);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (Array.isArray(schema.cliExamples) && schema.cliExamples.length > 0) {
|
|
3386
|
+
process.stdout.write('cli examples:\n');
|
|
3387
|
+
for (const example of schema.cliExamples) {
|
|
3388
|
+
process.stdout.write(`- ${example}\n`);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
async function handleToolsStatus(opts) {
|
|
3394
|
+
const config = loadConfig();
|
|
3395
|
+
const apiKey = requireApiKey(opts, config);
|
|
3396
|
+
const apiBase = resolveApiBase(opts, config);
|
|
3397
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
3398
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
3399
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
3400
|
+
const pollIntervalMs = resolvePollIntervalMs(opts);
|
|
3401
|
+
const includeResults = opts.includeResults === true;
|
|
3402
|
+
const resultsLimit = resolveStatusResultsLimit(opts.resultsLimit);
|
|
3403
|
+
const kind = parseToolStatusKind(opts.kind);
|
|
3404
|
+
const identifier = normalizeStatusIdentifier(opts.id);
|
|
3405
|
+
const { workspaceId } = resolveWorkspaceSelection(opts, config);
|
|
3406
|
+
|
|
3407
|
+
const loadStatus = async () =>
|
|
3408
|
+
await resolveUnifiedToolStatus({
|
|
3409
|
+
apiBase: resolvedApiBase,
|
|
3410
|
+
apiKey,
|
|
3411
|
+
useGateway,
|
|
3412
|
+
legacyUrl,
|
|
3413
|
+
timeoutMs,
|
|
3414
|
+
identifier,
|
|
3415
|
+
kind,
|
|
3416
|
+
workspaceId,
|
|
3417
|
+
includeResults,
|
|
3418
|
+
resultsLimit,
|
|
3419
|
+
});
|
|
3420
|
+
|
|
3421
|
+
let result = await loadStatus();
|
|
3422
|
+
if (opts.wait) {
|
|
3423
|
+
result = await pollUnifiedStatus({
|
|
3424
|
+
loader: loadStatus,
|
|
3425
|
+
timeoutMs,
|
|
3426
|
+
pollIntervalMs,
|
|
3427
|
+
opts,
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
const commandHint = buildStatusCommandHint(result, workspaceId);
|
|
3432
|
+
const payload = {
|
|
3433
|
+
...result,
|
|
3434
|
+
hint: commandHint,
|
|
3435
|
+
};
|
|
3436
|
+
emitJsonOutput(payload, opts.pretty);
|
|
2440
3437
|
}
|
|
2441
3438
|
|
|
2442
3439
|
async function handleDataExportTracking(opts) {
|
|
@@ -2509,6 +3506,24 @@ async function handleDataExportTracking(opts) {
|
|
|
2509
3506
|
}
|
|
2510
3507
|
|
|
2511
3508
|
async function handleDataExportReport(opts) {
|
|
3509
|
+
const reportType = assertSupportedReportType(opts.reportType);
|
|
3510
|
+
|
|
3511
|
+
if (reportType === REPORT_TYPE_SEARCH_RESULTS_ENRICHED) {
|
|
3512
|
+
if (opts.format !== 'csv') {
|
|
3513
|
+
throw new CliError('search_results_enriched supports only csv format.', {
|
|
3514
|
+
code: 'INVALID_ARGUMENT',
|
|
3515
|
+
exitCode: EXIT_CODES.USAGE,
|
|
3516
|
+
hint: 'Use --format csv.',
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
const payload = ensureJsonObject(parseJsonInput(opts.payload, { label: 'payload' }), 'payload');
|
|
3520
|
+
await handleDataExportSearchResults({
|
|
3521
|
+
...opts,
|
|
3522
|
+
__rawPayload: payload,
|
|
3523
|
+
});
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
|
|
2512
3527
|
const config = loadConfig();
|
|
2513
3528
|
const apiKey = requireApiKey(opts, config);
|
|
2514
3529
|
const apiBase = resolveApiBase(opts, config);
|
|
@@ -2525,7 +3540,7 @@ async function handleDataExportReport(opts) {
|
|
|
2525
3540
|
path: useGateway ? '/cli/tools/export-report' : '/functions/v1/export-report',
|
|
2526
3541
|
method: 'POST',
|
|
2527
3542
|
body: {
|
|
2528
|
-
reportType
|
|
3543
|
+
reportType,
|
|
2529
3544
|
format: opts.format,
|
|
2530
3545
|
payload,
|
|
2531
3546
|
},
|
|
@@ -2571,6 +3586,142 @@ async function handleDataExportReport(opts) {
|
|
|
2571
3586
|
process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
|
|
2572
3587
|
}
|
|
2573
3588
|
|
|
3589
|
+
async function handleDataExportSearchResults(opts) {
|
|
3590
|
+
const config = loadConfig();
|
|
3591
|
+
const apiKey = requireApiKey(opts, config);
|
|
3592
|
+
const apiBase = resolveApiBase(opts, config);
|
|
3593
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
3594
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
3595
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
3596
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
3597
|
+
|
|
3598
|
+
const rawPayload = opts.__rawPayload ?? stripUndefinedEntries({
|
|
3599
|
+
groupIds: normalizePositiveIntegerList(opts.groupIds, 'groupIds', { max: 100 }),
|
|
3600
|
+
trackingItemIds: normalizePositiveIntegerList(opts.trackingItemIds, 'trackingItemIds', { max: 1000 }),
|
|
3601
|
+
dateFrom: opts.dateFrom,
|
|
3602
|
+
dateTo: opts.dateTo,
|
|
3603
|
+
filename: opts.filename,
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
const payloadWorkspaceId = resolvePayloadWorkspaceId(rawPayload, null);
|
|
3607
|
+
const effectiveWorkspaceId = requireWorkspaceSelection(payloadWorkspaceId || resolvedWorkspaceId, {
|
|
3608
|
+
label: 'Search results enriched export',
|
|
3609
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting.',
|
|
3610
|
+
});
|
|
3611
|
+
const effectiveWorkspaceSource = payloadWorkspaceId ? 'body' : workspaceSource;
|
|
3612
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
3613
|
+
workspaceId: effectiveWorkspaceId,
|
|
3614
|
+
source: effectiveWorkspaceSource,
|
|
3615
|
+
label: 'search_results_enriched export',
|
|
3616
|
+
});
|
|
3617
|
+
|
|
3618
|
+
const normalizedPayload = buildSearchResultsEnrichedExportPayload(rawPayload, effectiveWorkspaceId);
|
|
3619
|
+
const requestedFilename = trimString(normalizedPayload.filename) || undefined;
|
|
3620
|
+
delete normalizedPayload.filename;
|
|
3621
|
+
|
|
3622
|
+
const exportResponse = await callApi({
|
|
3623
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
3624
|
+
apiKey,
|
|
3625
|
+
path: useGateway ? '/cli/tools/export-data' : '/functions/v1/export-data',
|
|
3626
|
+
method: 'POST',
|
|
3627
|
+
body: {
|
|
3628
|
+
template: EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW,
|
|
3629
|
+
format: 'csv',
|
|
3630
|
+
payload: normalizedPayload,
|
|
3631
|
+
filename: requestedFilename,
|
|
3632
|
+
},
|
|
3633
|
+
workspaceId: effectiveWorkspaceId,
|
|
3634
|
+
timeoutMs,
|
|
3635
|
+
});
|
|
3636
|
+
|
|
3637
|
+
if (!exportResponse.ok) {
|
|
3638
|
+
throw await buildHttpError(exportResponse, {
|
|
3639
|
+
label: 'Search results enriched export',
|
|
3640
|
+
functionName: 'export-data',
|
|
3641
|
+
method: 'POST',
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
const responseJson = await exportResponse.json();
|
|
3646
|
+
const metadata = isJsonObject(responseJson) && isJsonObject(responseJson.metadata)
|
|
3647
|
+
? responseJson.metadata
|
|
3648
|
+
: null;
|
|
3649
|
+
const fileUrl = trimString(metadata?.file_url || '');
|
|
3650
|
+
const status = trimString(metadata?.status || '').toLowerCase();
|
|
3651
|
+
|
|
3652
|
+
if (!fileUrl) {
|
|
3653
|
+
if (status === 'processing') {
|
|
3654
|
+
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');
|
|
3655
|
+
} else if (status === 'failed') {
|
|
3656
|
+
process.stderr.write('[socialseal] Export status is failed. Inspect the JSON metadata for details, then retry with corrected filters.\n');
|
|
3657
|
+
} else {
|
|
3658
|
+
process.stderr.write('[socialseal] Export did not include a file URL yet. Inspect the JSON metadata and retry if needed.\n');
|
|
3659
|
+
}
|
|
3660
|
+
emitJsonOutput(responseJson, opts.pretty);
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
const artifactResponse = await fetchWithTimeout(fileUrl, {
|
|
3665
|
+
method: 'GET',
|
|
3666
|
+
headers: { Accept: '*/*' },
|
|
3667
|
+
}, timeoutMs);
|
|
3668
|
+
|
|
3669
|
+
if (!artifactResponse.ok) {
|
|
3670
|
+
throw await buildHttpError(artifactResponse, {
|
|
3671
|
+
label: 'Search results enriched artifact download',
|
|
3672
|
+
method: 'GET',
|
|
3673
|
+
hint: 'The signed file URL may be expired or inaccessible. Re-run the export command to mint a fresh URL.',
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
if (!artifactResponse.body) {
|
|
3678
|
+
throw new CliError('Export artifact response contained no body.', {
|
|
3679
|
+
code: 'EMPTY_RESPONSE',
|
|
3680
|
+
exitCode: EXIT_CODES.SERVER,
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
const outPath = opts.stdout
|
|
3685
|
+
? null
|
|
3686
|
+
: (opts.out || trimString(metadata?.filename || '') || 'tracking-ranked-videos.csv');
|
|
3687
|
+
if (outPath) {
|
|
3688
|
+
await pipeline(artifactResponse.body, fs.createWriteStream(outPath));
|
|
3689
|
+
process.stderr.write(`[socialseal] Export written to ${outPath}\n`);
|
|
3690
|
+
} else {
|
|
3691
|
+
await pipeline(artifactResponse.body, process.stdout);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function handleDataExportOptions(opts) {
|
|
3696
|
+
const payload = {
|
|
3697
|
+
exports: EXPORT_OPTIONS,
|
|
3698
|
+
supportedReportTypes: SUPPORTED_EXPORT_REPORT_TYPES,
|
|
3699
|
+
note: 'Use this list to choose the right export surface before running data export commands.',
|
|
3700
|
+
};
|
|
3701
|
+
|
|
3702
|
+
if (opts.json) {
|
|
3703
|
+
emitJsonOutput(payload, opts.pretty);
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
process.stdout.write('[socialseal] Available export options\n');
|
|
3708
|
+
process.stdout.write('[socialseal] Choose a flow based on dataset shape, not endpoint name.\n\n');
|
|
3709
|
+
for (const option of EXPORT_OPTIONS) {
|
|
3710
|
+
process.stdout.write(`- ${option.id}: ${option.summary}\n`);
|
|
3711
|
+
process.stdout.write(` command: ${option.command}\n`);
|
|
3712
|
+
process.stdout.write(` formats: ${option.formats.join(', ')}\n`);
|
|
3713
|
+
process.stdout.write(` required: ${option.required.join(', ')}\n`);
|
|
3714
|
+
process.stdout.write(` best for: ${option.bestFor}\n`);
|
|
3715
|
+
if (option.alias) {
|
|
3716
|
+
process.stdout.write(` alias: ${option.alias}\n`);
|
|
3717
|
+
}
|
|
3718
|
+
if (Array.isArray(option.reportTypes)) {
|
|
3719
|
+
process.stdout.write(` report types: ${option.reportTypes.join(', ')}\n`);
|
|
3720
|
+
}
|
|
3721
|
+
process.stdout.write('\n');
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
2574
3725
|
async function handleVideoExtract(opts) {
|
|
2575
3726
|
const config = loadConfig();
|
|
2576
3727
|
const apiKey = requireApiKey(opts, config);
|
|
@@ -2902,7 +4053,7 @@ if (typeof program.showHelpAfterError === 'function') {
|
|
|
2902
4053
|
if (typeof program.showSuggestionAfterError === 'function') {
|
|
2903
4054
|
program.showSuggestionAfterError(true);
|
|
2904
4055
|
}
|
|
2905
|
-
program.addHelpText('after', `\nExamples:\n socialseal workspace list\n socialseal workspace use <workspace-id>\n socialseal agent run --message
|
|
4056
|
+
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 schema --function search-journey-run\n socialseal tools call --function <tool> --body @payload.json\n socialseal tools status 6809 --kind google_ai_run\n socialseal tools status <run-uuid> --kind journey_run --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`);
|
|
2906
4057
|
|
|
2907
4058
|
program
|
|
2908
4059
|
.command('agent')
|
|
@@ -2975,6 +4126,15 @@ tools
|
|
|
2975
4126
|
.option('--verbose', 'Show error details')
|
|
2976
4127
|
.action((opts) => runCommand(handleToolsList, opts));
|
|
2977
4128
|
|
|
4129
|
+
tools
|
|
4130
|
+
.command('schema')
|
|
4131
|
+
.description('Show static payload schema hints and examples for a tool')
|
|
4132
|
+
.option('--function <name>', 'Tool name (omit to list all schema hints)')
|
|
4133
|
+
.option('--json', 'Emit machine-readable output')
|
|
4134
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
4135
|
+
.option('--verbose', 'Show error details')
|
|
4136
|
+
.action((opts) => runCommand(handleToolsSchema, opts));
|
|
4137
|
+
|
|
2978
4138
|
tools
|
|
2979
4139
|
.command('call')
|
|
2980
4140
|
.requiredOption('--function <name>', 'Tool name (see official docs)')
|
|
@@ -2992,8 +4152,33 @@ tools
|
|
|
2992
4152
|
.option('--verbose', 'Show error details')
|
|
2993
4153
|
.action((opts) => runCommand(handleToolsCall, opts));
|
|
2994
4154
|
|
|
4155
|
+
tools
|
|
4156
|
+
.command('status <id>')
|
|
4157
|
+
.description('Read unified status for UUID jobs, journey run UUIDs, or numeric Google AI run ids')
|
|
4158
|
+
.option('--kind <kind>', 'auto|agent_job|google_ai_run|journey_run', 'auto')
|
|
4159
|
+
.option('--wait', 'Poll until status reaches a terminal state')
|
|
4160
|
+
.option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
|
|
4161
|
+
.option('--include-results', 'Include Google AI summary/citation rows when reading numeric run ids')
|
|
4162
|
+
.option('--results-limit <n>', 'Max Google AI summary rows to include when --include-results is set')
|
|
4163
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
4164
|
+
.option('--api-key <key>', 'CLI API key')
|
|
4165
|
+
.option('--workspace-id <id>', 'Workspace id (required for journey_run status)')
|
|
4166
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
4167
|
+
.option('--json', 'Emit machine-readable errors')
|
|
4168
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
4169
|
+
.option('--verbose', 'Show error details')
|
|
4170
|
+
.action((id, opts) => runCommand(handleToolsStatus, { ...opts, id }));
|
|
4171
|
+
|
|
2995
4172
|
const data = program.command('data').description('Data exports (provisional)');
|
|
2996
4173
|
|
|
4174
|
+
data
|
|
4175
|
+
.command('export-options')
|
|
4176
|
+
.description('List export flows, when to use each, and required inputs')
|
|
4177
|
+
.option('--json', 'Emit machine-readable output')
|
|
4178
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
4179
|
+
.option('--verbose', 'Show error details')
|
|
4180
|
+
.action((opts) => runCommand(handleDataExportOptions, opts));
|
|
4181
|
+
|
|
2997
4182
|
data
|
|
2998
4183
|
.command('export-tracking')
|
|
2999
4184
|
.description('Export tracking data as CSV')
|
|
@@ -3010,10 +4195,29 @@ data
|
|
|
3010
4195
|
.option('--verbose', 'Show error details')
|
|
3011
4196
|
.action((opts) => runCommand(handleDataExportTracking, opts));
|
|
3012
4197
|
|
|
4198
|
+
data
|
|
4199
|
+
.command('export-search-results')
|
|
4200
|
+
.description('Export enriched ranked search results (search_results + videos + latest metrics + analysis) as CSV')
|
|
4201
|
+
.requiredOption('--group-ids <ids>', 'Comma-separated tracking group ids (for example: 123,124,125)')
|
|
4202
|
+
.option('--tracking-item-ids <ids>', 'Optional comma-separated tracking item ids')
|
|
4203
|
+
.option('--date-from <iso>', 'Optional ISO datetime lower bound (inclusive)')
|
|
4204
|
+
.option('--date-to <iso>', 'Optional ISO datetime upper bound (inclusive)')
|
|
4205
|
+
.option('--filename <name>', 'Optional export filename stem (without extension)')
|
|
4206
|
+
.option('--out <path>', 'Output file path')
|
|
4207
|
+
.option('--stdout', 'Write to stdout')
|
|
4208
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
4209
|
+
.option('--api-key <key>', 'CLI API key')
|
|
4210
|
+
.option('--workspace-id <id>', 'Workspace id (for scoped keys)')
|
|
4211
|
+
.option('--pretty', 'Pretty-print JSON metadata when no file is ready')
|
|
4212
|
+
.option('--json', 'Emit machine-readable errors')
|
|
4213
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
4214
|
+
.option('--verbose', 'Show error details')
|
|
4215
|
+
.action((opts) => runCommand(handleDataExportSearchResults, opts));
|
|
4216
|
+
|
|
3013
4217
|
data
|
|
3014
4218
|
.command('export-report')
|
|
3015
4219
|
.description('Export report data via export-report (provisional)')
|
|
3016
|
-
.requiredOption('--report-type <type>', 'keyword_universe|cluster_insights|creator_signatures|post_publish|quick_audit')
|
|
4220
|
+
.requiredOption('--report-type <type>', 'keyword_universe|cluster_insights|creator_signatures|post_publish|quick_audit|search_results_enriched')
|
|
3017
4221
|
.option('--format <format>', 'csv|json|markdown|html|excel_data', 'csv')
|
|
3018
4222
|
.requiredOption('--payload <jsonOrFile>', 'Payload JSON or @file.json')
|
|
3019
4223
|
.option('--out <path>', 'Output file path')
|