@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/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 = 30000;
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
- { name: 'agent-tool-jobs', category: 'agent', description: 'Poll queued agent-backed tool jobs and fetch their results.' },
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
- { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
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
- { name: 'search-journey-run', category: 'vnext', description: 'Run a search journey for a subject across supported platforms.' },
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 (status === 'completed') return data;
1678
- if (status === 'failed') throw buildSearchJourneyRunFailure(data);
1679
- if (status === 'pending' || status === 'processing') continue;
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' || functionName === 'export_tracking_data';
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: isJsonObject(payload) && typeof payload.workspaceId === 'string'
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: KNOWN_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 KNOWN_TOOLS) {
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: opts.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 \"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`);
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')