@socialseal/cli 0.1.8 → 0.1.9
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 +6 -0
- package/package.json +1 -1
- package/src/index.js +774 -42
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.9 - 2026-05-13
|
|
6
|
+
- Fail `group-management add_items` on backend partial failures, expected-count mismatches, and failed backend verification.
|
|
7
|
+
- Add `data export-group-evidence` to route social ranked evidence and Google AI evidence to the right raw export template with metadata.
|
|
8
|
+
- Add `data group-completeness` with backend-first completeness checks, manifest fallback, and refresh status visibility.
|
|
9
|
+
- Harden `tools status --wait` terminal failure handling and empty group refresh guardrails.
|
|
10
|
+
|
|
5
11
|
## 0.1.8 - 2026-04-13
|
|
6
12
|
- Increase default CLI timeout from 30s to 5m to reduce false timeout failures on heavy tool/export workflows.
|
|
7
13
|
- Harden async `search-journey-run` polling to accept additional active/terminal status labels (`queued`, `in_progress`, `running`, `succeeded`, `error`) instead of failing on unexpected variants.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -33,6 +33,8 @@ const ACTIVE_STATUS_VALUES = new Set(['queued', 'pending', 'processing', 'in_pro
|
|
|
33
33
|
const TOOL_STATUS_KINDS = new Set(['auto', 'agent_job', 'google_ai_run', 'journey_run']);
|
|
34
34
|
const REPORT_TYPE_SEARCH_RESULTS_ENRICHED = 'search_results_enriched';
|
|
35
35
|
const EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW = 'tracking_ranked_videos_raw';
|
|
36
|
+
const EXPORT_DATA_TEMPLATE_GOOGLE_AI_SEARCH_SUMMARIES_RAW = 'google_ai_search_summaries_raw';
|
|
37
|
+
const PLATFORM_ID_GOOGLE_AI = 11;
|
|
36
38
|
const SUPPORTED_EXPORT_REPORT_TYPES = [
|
|
37
39
|
'keyword_universe',
|
|
38
40
|
'cluster_insights',
|
|
@@ -59,6 +61,14 @@ const EXPORT_OPTIONS = [
|
|
|
59
61
|
bestFor: 'SQL-like ranked-search datasets without using psql.',
|
|
60
62
|
alias: 'socialseal data export-report --report-type search_results_enriched --format csv --payload @payload.json',
|
|
61
63
|
},
|
|
64
|
+
{
|
|
65
|
+
id: 'group_evidence',
|
|
66
|
+
command: 'socialseal data export-group-evidence --group-id <id> --workspace-id <uuid>',
|
|
67
|
+
summary: 'Unified group evidence export that routes social groups and Google AI groups to the right raw export template.',
|
|
68
|
+
formats: ['csv'],
|
|
69
|
+
required: ['workspace id', '--group-id'],
|
|
70
|
+
bestFor: 'Automation that needs usable evidence without knowing whether the group is social ranked search or Google AI.',
|
|
71
|
+
},
|
|
62
72
|
{
|
|
63
73
|
id: 'report_templates',
|
|
64
74
|
command: 'socialseal data export-report --report-type <type> --format <format> --payload @payload.json',
|
|
@@ -180,7 +190,7 @@ const KNOWN_TOOLS = [
|
|
|
180
190
|
workspaceScoped: true,
|
|
181
191
|
knownLocalDevState: 'disabled_by_default',
|
|
182
192
|
actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh', 'list_items', 'add_item', 'group_add_item', 'add_items', 'group_add_items', 'remove_item', 'group_remove_item'],
|
|
183
|
-
notes: 'REST-style surface under /groups. `add_item`/`group_add_item` accepts an existing `item_id`; `add_items`/`group_add_items` accepts `item_ids` or item payloads for bulk membership adds. Always pass a workspace id or configure a default workspace so the backend does not fall back to the personal workspace.',
|
|
193
|
+
notes: 'REST-style surface under /groups. `add_item`/`group_add_item` accepts an existing `item_id`; `add_items`/`group_add_items` accepts `item_ids` or item payloads for bulk membership adds. `completeness` checks expected memberships and refresh queue visibility. Always pass a workspace id or configure a default workspace so the backend does not fall back to the personal workspace.',
|
|
184
194
|
},
|
|
185
195
|
{
|
|
186
196
|
name: 'tracking',
|
|
@@ -366,6 +376,68 @@ const TOOL_SCHEMA_HINTS = {
|
|
|
366
376
|
'socialseal tools call --function get-google-ai-search-results --body \'{"runId":6809,"includeCitations":true,"limit":10}\'',
|
|
367
377
|
],
|
|
368
378
|
},
|
|
379
|
+
'group-management': {
|
|
380
|
+
summary: 'Manage single-platform tracking groups and memberships.',
|
|
381
|
+
operations: [
|
|
382
|
+
{
|
|
383
|
+
action: 'create',
|
|
384
|
+
required: ['action=create', 'name', 'workspaceId or --workspace-id'],
|
|
385
|
+
optional: [
|
|
386
|
+
'platform (defaults to tiktok)',
|
|
387
|
+
'description',
|
|
388
|
+
'refresh_frequency',
|
|
389
|
+
'next_refresh_at',
|
|
390
|
+
'brand_id',
|
|
391
|
+
],
|
|
392
|
+
notes: 'Supported platform values: tiktok, instagram, youtube, ig_reels, yt_shorts, douyin, xhs, google_ai.',
|
|
393
|
+
example: {
|
|
394
|
+
action: 'create',
|
|
395
|
+
name: 'YouTube competitor searches',
|
|
396
|
+
platform: 'youtube',
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
action: 'add_items',
|
|
401
|
+
required: ['action=add_items', 'group_id', 'workspaceId or --workspace-id'],
|
|
402
|
+
optional: ['item_ids', 'items', 'platform/groupPlatform for item payload defaults'],
|
|
403
|
+
notes: 'When adding item payloads, omit item platform to inherit the group platform, or pass platform explicitly.',
|
|
404
|
+
example: {
|
|
405
|
+
action: 'add_items',
|
|
406
|
+
group_id: 123,
|
|
407
|
+
items: [
|
|
408
|
+
{
|
|
409
|
+
name: 'best kenya safari',
|
|
410
|
+
type: 'keyword',
|
|
411
|
+
value: 'best kenya safari',
|
|
412
|
+
region: 'US',
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
action: 'completeness',
|
|
419
|
+
required: ['action=completeness', 'group_id', 'items or expected_items', 'workspaceId or --workspace-id'],
|
|
420
|
+
optional: ['include_refresh_status'],
|
|
421
|
+
notes: 'Returns durable setup completeness and aggregate refresh queue status for the group.',
|
|
422
|
+
example: {
|
|
423
|
+
action: 'completeness',
|
|
424
|
+
group_id: 123,
|
|
425
|
+
expected_items: [
|
|
426
|
+
{
|
|
427
|
+
track_type: 'search',
|
|
428
|
+
track_value: 'best kenya safari',
|
|
429
|
+
region: 'US',
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
cliExamples: [
|
|
436
|
+
'socialseal tools call --function group-management --workspace-id <workspace-uuid> --body \'{"action":"create","name":"YouTube group","platform":"youtube"}\'',
|
|
437
|
+
'socialseal tools call --function group-management --workspace-id <workspace-uuid> --body \'{"action":"create","name":"Instagram group","platform":"instagram"}\'',
|
|
438
|
+
'socialseal tools call --function group-management --workspace-id <workspace-uuid> --body \'{"action":"add_items","group_id":123,"items":[{"name":"best kenya safari","type":"keyword","value":"best kenya safari","region":"US"}]}\'',
|
|
439
|
+
],
|
|
440
|
+
},
|
|
369
441
|
};
|
|
370
442
|
|
|
371
443
|
function getToolSchemaHint(functionName) {
|
|
@@ -1006,6 +1078,11 @@ function isTerminalStatusValue(value) {
|
|
|
1006
1078
|
return !ACTIVE_STATUS_VALUES.has(normalized);
|
|
1007
1079
|
}
|
|
1008
1080
|
|
|
1081
|
+
function isFailedStatusValue(value) {
|
|
1082
|
+
const normalized = normalizeStatusValue(value);
|
|
1083
|
+
return normalized === 'failed' || normalized === 'error';
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1009
1086
|
function coercePositiveInteger(value, label) {
|
|
1010
1087
|
if (value === undefined || value === null || value === '') return undefined;
|
|
1011
1088
|
const parsed = Number(value);
|
|
@@ -1104,6 +1181,200 @@ function buildSearchResultsEnrichedExportPayload(rawPayload, workspaceId) {
|
|
|
1104
1181
|
});
|
|
1105
1182
|
}
|
|
1106
1183
|
|
|
1184
|
+
function buildGoogleAiSearchSummariesExportPayload(rawPayload, workspaceId) {
|
|
1185
|
+
const payload = isJsonObject(rawPayload) ? rawPayload : {};
|
|
1186
|
+
const groupId = coercePositiveInteger(firstDefined(payload, ['groupId', 'group_id']), 'groupId');
|
|
1187
|
+
if (!groupId) {
|
|
1188
|
+
throw new CliError('Google AI evidence export requires a group id.', {
|
|
1189
|
+
code: 'MISSING_ARGUMENT',
|
|
1190
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1191
|
+
hint: 'Provide --group-id or groupId in the payload.',
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const trackingItemIds = normalizePositiveIntegerList(
|
|
1196
|
+
firstDefined(payload, ['trackingItemIds', 'tracking_item_ids']),
|
|
1197
|
+
'trackingItemIds',
|
|
1198
|
+
{ max: 1000 },
|
|
1199
|
+
);
|
|
1200
|
+
const filename = trimString(firstDefined(payload, ['filename'])) || undefined;
|
|
1201
|
+
|
|
1202
|
+
return stripUndefinedEntries({
|
|
1203
|
+
workspaceId,
|
|
1204
|
+
groupId,
|
|
1205
|
+
trackingItemIds: trackingItemIds.length > 0 ? trackingItemIds : undefined,
|
|
1206
|
+
filename,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function normalizeEvidenceSurface(value) {
|
|
1211
|
+
const normalized = trimString(value || 'auto').toLowerCase().replace(/-/g, '_').replace(/\s+/g, '_');
|
|
1212
|
+
if (['auto', 'social', 'ranked', 'ranked_search', 'google_ai', 'google'].includes(normalized)) {
|
|
1213
|
+
if (normalized === 'ranked' || normalized === 'ranked_search') return 'social';
|
|
1214
|
+
if (normalized === 'google') return 'google_ai';
|
|
1215
|
+
return normalized;
|
|
1216
|
+
}
|
|
1217
|
+
throw new CliError(`Unsupported evidence surface: ${value}`, {
|
|
1218
|
+
code: 'INVALID_ARGUMENT',
|
|
1219
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1220
|
+
hint: 'Use --surface auto|social|google_ai.',
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function normalizePlatformKey(value) {
|
|
1225
|
+
const normalized = trimString(value).toLowerCase().replace(/-/g, '_').replace(/\s+/g, '_');
|
|
1226
|
+
if (!normalized) return null;
|
|
1227
|
+
if (['google', 'google_ai', 'google_ai_overview', 'ai_search'].includes(normalized)) return 'google_ai';
|
|
1228
|
+
return normalized;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function resolveEvidenceSurfaceFromGroup(group) {
|
|
1232
|
+
if (!isJsonObject(group)) return 'social';
|
|
1233
|
+
const platformId = firstDefined(group, ['platform_id', 'platformId']);
|
|
1234
|
+
if (Number(platformId) === PLATFORM_ID_GOOGLE_AI) return 'google_ai';
|
|
1235
|
+
const platform = normalizePlatformKey(firstDefined(group, ['platform', 'platform_key', 'platformKey']));
|
|
1236
|
+
return platform === 'google_ai' ? 'google_ai' : 'social';
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function buildGroupEvidenceMetadata({ surface, groupId, workspaceId, responseJson }) {
|
|
1240
|
+
const metadata = isJsonObject(responseJson) && isJsonObject(responseJson.metadata)
|
|
1241
|
+
? responseJson.metadata
|
|
1242
|
+
: {};
|
|
1243
|
+
const rowCount = Number.isFinite(metadata.row_count) ? metadata.row_count : null;
|
|
1244
|
+
return {
|
|
1245
|
+
group_id: groupId,
|
|
1246
|
+
workspace_id: workspaceId,
|
|
1247
|
+
platform: surface === 'google_ai' ? 'google_ai' : 'social',
|
|
1248
|
+
surface,
|
|
1249
|
+
row_count: rowCount,
|
|
1250
|
+
generated_at: new Date().toISOString(),
|
|
1251
|
+
header_only: rowCount === 0,
|
|
1252
|
+
template: metadata.template ?? (surface === 'google_ai'
|
|
1253
|
+
? EXPORT_DATA_TEMPLATE_GOOGLE_AI_SEARCH_SUMMARIES_RAW
|
|
1254
|
+
: EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function normalizeCompletenessPlatform(rawItem) {
|
|
1259
|
+
const platformId = firstDefined(rawItem, ['platform_id', 'platformId']);
|
|
1260
|
+
if (Number.isFinite(Number(platformId))) return `id:${Number(platformId)}`;
|
|
1261
|
+
const platform = normalizePlatformKey(firstDefined(rawItem, ['platform', 'platform_key', 'platformKey']));
|
|
1262
|
+
return platform ? `key:${platform}` : '';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function normalizeCompletenessItem(rawItem, index, source) {
|
|
1266
|
+
if (!isJsonObject(rawItem)) {
|
|
1267
|
+
throw new CliError(`Invalid ${source} item at index ${index}: expected an object.`, {
|
|
1268
|
+
code: 'INVALID_MANIFEST',
|
|
1269
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const id = coercePositiveInteger(firstDefined(rawItem, ['item_id', 'itemId', 'id']), `${source}[${index}].item_id`);
|
|
1274
|
+
const rawValue = firstDefined(rawItem, ['track_value', 'trackValue', 'value', 'name']);
|
|
1275
|
+
const value = trimString(rawValue).toLowerCase();
|
|
1276
|
+
const rawType = firstDefined(rawItem, ['track_type', 'trackType', 'type']);
|
|
1277
|
+
const type = normalizeTrackingType(rawType) || trimString(rawType).toLowerCase() || '';
|
|
1278
|
+
const region = trimString(firstDefined(rawItem, ['region'])).toUpperCase();
|
|
1279
|
+
const platform = normalizeCompletenessPlatform(rawItem);
|
|
1280
|
+
const hasSemanticKey = Boolean(value && type);
|
|
1281
|
+
const key = hasSemanticKey ? `${type}|${value}|${region}|${platform}` : `id:${id}`;
|
|
1282
|
+
const looseKey = hasSemanticKey ? `${type}|${value}|${region}` : `id:${id}`;
|
|
1283
|
+
|
|
1284
|
+
if (!id && (!value || !type)) {
|
|
1285
|
+
throw new CliError(`Invalid ${source} item at index ${index}: expected item_id or track_type + track_value.`, {
|
|
1286
|
+
code: 'INVALID_MANIFEST',
|
|
1287
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return {
|
|
1292
|
+
key,
|
|
1293
|
+
looseKey,
|
|
1294
|
+
item_id: id ?? null,
|
|
1295
|
+
track_type: type || null,
|
|
1296
|
+
track_value: value || null,
|
|
1297
|
+
region: region || null,
|
|
1298
|
+
platform: platform || null,
|
|
1299
|
+
raw: rawItem,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function extractExpectedManifestItems(manifest) {
|
|
1304
|
+
if (Array.isArray(manifest)) return manifest;
|
|
1305
|
+
if (!isJsonObject(manifest)) {
|
|
1306
|
+
throw new CliError('Invalid completeness manifest: expected an array or object with items.', {
|
|
1307
|
+
code: 'INVALID_MANIFEST',
|
|
1308
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
const items = firstDefined(manifest, ['items', 'expectedItems', 'expected_items']);
|
|
1312
|
+
if (!Array.isArray(items)) {
|
|
1313
|
+
throw new CliError('Invalid completeness manifest: expected items, expectedItems, or expected_items array.', {
|
|
1314
|
+
code: 'INVALID_MANIFEST',
|
|
1315
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
return items;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function normalizeActualGroupItem(rawItem, index) {
|
|
1322
|
+
const tracking = isJsonObject(rawItem?.user_tracking) ? rawItem.user_tracking : rawItem;
|
|
1323
|
+
return normalizeCompletenessItem(tracking, index, 'actual_items');
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function buildCompletenessDiff({ expectedItems, actualItems, groupId, workspaceId }) {
|
|
1327
|
+
const expected = expectedItems.map((item, index) =>
|
|
1328
|
+
normalizeCompletenessItem(item, index, 'expected_items')
|
|
1329
|
+
);
|
|
1330
|
+
const actual = actualItems.map((item, index) => normalizeActualGroupItem(item, index));
|
|
1331
|
+
const actualByKey = new Map(actual.map((item) => [item.key, item]));
|
|
1332
|
+
const expectedByKey = new Map(expected.map((item) => [item.key, item]));
|
|
1333
|
+
const actualByLooseKey = new Map(actual.map((item) => [item.looseKey, item]));
|
|
1334
|
+
const expectedByLooseKey = new Map(expected.map((item) => [item.looseKey, item]));
|
|
1335
|
+
|
|
1336
|
+
const missing = [];
|
|
1337
|
+
const platformMismatches = [];
|
|
1338
|
+
for (const expectedItem of expected) {
|
|
1339
|
+
if (actualByKey.has(expectedItem.key)) continue;
|
|
1340
|
+
const looseMatch = actualByLooseKey.get(expectedItem.looseKey);
|
|
1341
|
+
if (looseMatch && expectedItem.platform !== looseMatch.platform) {
|
|
1342
|
+
platformMismatches.push({
|
|
1343
|
+
expected: expectedItem.raw,
|
|
1344
|
+
actual: looseMatch.raw,
|
|
1345
|
+
expected_platform: expectedItem.platform,
|
|
1346
|
+
actual_platform: looseMatch.platform,
|
|
1347
|
+
});
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
missing.push(expectedItem.raw);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const extra = [];
|
|
1354
|
+
for (const actualItem of actual) {
|
|
1355
|
+
if (expectedByKey.has(actualItem.key)) continue;
|
|
1356
|
+
const looseMatch = expectedByLooseKey.get(actualItem.looseKey);
|
|
1357
|
+
if (looseMatch && looseMatch.platform !== actualItem.platform) continue;
|
|
1358
|
+
extra.push(actualItem.raw);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const complete = missing.length === 0 && extra.length === 0 && platformMismatches.length === 0;
|
|
1362
|
+
return {
|
|
1363
|
+
success: complete,
|
|
1364
|
+
complete,
|
|
1365
|
+
group_id: groupId,
|
|
1366
|
+
workspace_id: workspaceId,
|
|
1367
|
+
expected_count: expected.length,
|
|
1368
|
+
actual_count: actual.length,
|
|
1369
|
+
missing_count: missing.length,
|
|
1370
|
+
extra_count: extra.length,
|
|
1371
|
+
platform_mismatch_count: platformMismatches.length,
|
|
1372
|
+
missing,
|
|
1373
|
+
extra,
|
|
1374
|
+
platform_mismatches: platformMismatches,
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1107
1378
|
function buildPathWithQuery(basePath, query) {
|
|
1108
1379
|
const params = new URLSearchParams();
|
|
1109
1380
|
for (const [key, rawValue] of Object.entries(query || {})) {
|
|
@@ -1174,8 +1445,10 @@ function normalizeGroupManagementPayload(payload, fallbackWorkspaceId) {
|
|
|
1174
1445
|
const itemId = firstDefined(payload, ['item_id', 'itemId']);
|
|
1175
1446
|
const itemIds = firstDefined(payload, ['item_ids', 'itemIds']);
|
|
1176
1447
|
const items = firstDefined(payload, ['items']);
|
|
1448
|
+
const expectedItems = firstDefined(payload, ['expected_items', 'expectedItems']);
|
|
1177
1449
|
const limit = firstDefined(payload, ['limit']);
|
|
1178
1450
|
const page = firstDefined(payload, ['page']);
|
|
1451
|
+
const force = firstDefined(payload, ['force']);
|
|
1179
1452
|
return stripUndefinedEntries({
|
|
1180
1453
|
action: trimString(firstDefined(payload, ['action'])) || undefined,
|
|
1181
1454
|
workspaceId: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId),
|
|
@@ -1194,6 +1467,7 @@ function normalizeGroupManagementPayload(payload, fallbackWorkspaceId) {
|
|
|
1194
1467
|
})
|
|
1195
1468
|
: undefined,
|
|
1196
1469
|
items: Array.isArray(items) ? items : undefined,
|
|
1470
|
+
expected_items: Array.isArray(expectedItems) ? expectedItems : undefined,
|
|
1197
1471
|
name: trimString(firstDefined(payload, ['name'])) || undefined,
|
|
1198
1472
|
description: firstDefined(payload, ['description']),
|
|
1199
1473
|
platform: trimString(firstDefined(payload, ['platform', 'groupPlatform'])) || undefined,
|
|
@@ -1207,6 +1481,7 @@ function normalizeGroupManagementPayload(payload, fallbackWorkspaceId) {
|
|
|
1207
1481
|
: firstDefined(payload, ['region']),
|
|
1208
1482
|
limit: limit !== undefined ? Number(limit) : undefined,
|
|
1209
1483
|
page: page !== undefined ? Number(page) : undefined,
|
|
1484
|
+
force: force === true || trimString(force).toLowerCase() === 'true' ? true : undefined,
|
|
1210
1485
|
});
|
|
1211
1486
|
}
|
|
1212
1487
|
|
|
@@ -1696,6 +1971,28 @@ function translateGroupManagementAction(payload, workspaceId, originalMethod) {
|
|
|
1696
1971
|
};
|
|
1697
1972
|
}
|
|
1698
1973
|
|
|
1974
|
+
if (action === 'completeness' || action === 'group_completeness') {
|
|
1975
|
+
const groupId = coercePositiveInteger(payload.group_id, 'group_id');
|
|
1976
|
+
if (!groupId) {
|
|
1977
|
+
throw new CliError('group_id is required for group completeness.', {
|
|
1978
|
+
code: 'MISSING_ARGUMENT',
|
|
1979
|
+
exitCode: EXIT_CODES.USAGE,
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
const expectedItems = Array.isArray(payload.expected_items)
|
|
1983
|
+
? payload.expected_items
|
|
1984
|
+
: (Array.isArray(payload.items) ? payload.items : undefined);
|
|
1985
|
+
return {
|
|
1986
|
+
method: 'POST',
|
|
1987
|
+
pathSuffix: buildPathWithQuery(`/groups/${groupId}/completeness`, { workspace_id: workspaceId || undefined }),
|
|
1988
|
+
body: stripUndefinedEntries({
|
|
1989
|
+
expected_items: expectedItems,
|
|
1990
|
+
include_refresh_status: firstDefined(payload, ['include_refresh_status', 'includeRefreshStatus']),
|
|
1991
|
+
}),
|
|
1992
|
+
workspaceId,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1699
1996
|
if (action === 'remove_item' || action === 'group_remove_item') {
|
|
1700
1997
|
const groupId = coercePositiveInteger(payload.group_id, 'group_id');
|
|
1701
1998
|
const itemId = coercePositiveInteger(payload.item_id, 'item_id');
|
|
@@ -1716,7 +2013,7 @@ function translateGroupManagementAction(payload, workspaceId, originalMethod) {
|
|
|
1716
2013
|
throw new CliError(`Unsupported group-management action: ${payload.action}`, {
|
|
1717
2014
|
code: 'INVALID_ARGUMENT',
|
|
1718
2015
|
exitCode: EXIT_CODES.USAGE,
|
|
1719
|
-
hint: 'Supported group-management actions: list, get, create, update, delete, refresh, list_items, add_item, add_items, remove_item.',
|
|
2016
|
+
hint: 'Supported group-management actions: list, get, create, update, delete, refresh, list_items, add_item, add_items, completeness, remove_item.',
|
|
1720
2017
|
});
|
|
1721
2018
|
}
|
|
1722
2019
|
|
|
@@ -2005,6 +2302,145 @@ function emitJsonOutput(value, pretty) {
|
|
|
2005
2302
|
process.stdout.write(formatJsonOutput(value, pretty) + '\n');
|
|
2006
2303
|
}
|
|
2007
2304
|
|
|
2305
|
+
function isGroupManagementBulkAddInvocation(functionName, translated) {
|
|
2306
|
+
if (functionName !== 'group-management') return false;
|
|
2307
|
+
const action = isJsonObject(translated.normalizedPayload)
|
|
2308
|
+
? trimString(translated.normalizedPayload.action).toLowerCase()
|
|
2309
|
+
: '';
|
|
2310
|
+
return action === 'add_items' || action === 'group_add_items';
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
function getBulkAddExpectedCount(translated) {
|
|
2314
|
+
return Array.isArray(translated.body) ? translated.body.length : null;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function maybeThrowGroupManagementBulkAddPartialFailure(data, translated) {
|
|
2318
|
+
if (!isJsonObject(data)) return;
|
|
2319
|
+
|
|
2320
|
+
const errors = Array.isArray(data.errors) ? data.errors : [];
|
|
2321
|
+
const expected = getBulkAddExpectedCount(translated);
|
|
2322
|
+
const processed = Number.isFinite(data.items_processed) ? data.items_processed : null;
|
|
2323
|
+
const added = Number.isFinite(data.items_added) ? data.items_added : 0;
|
|
2324
|
+
const linked = Number.isFinite(data.items_linked) ? data.items_linked : 0;
|
|
2325
|
+
const alreadyLinked = Number.isFinite(data.items_already_linked) ? data.items_already_linked : 0;
|
|
2326
|
+
const successful = added + linked + alreadyLinked;
|
|
2327
|
+
const failed = errors.length;
|
|
2328
|
+
const hasErrors = failed > 0;
|
|
2329
|
+
const hasProcessedMismatch = expected !== null && processed !== null && processed !== expected;
|
|
2330
|
+
const hasSuccessMismatch = expected !== null && successful !== expected;
|
|
2331
|
+
const hasVerificationFailure = data.verified === false;
|
|
2332
|
+
|
|
2333
|
+
if (!hasErrors && !hasProcessedMismatch && !hasSuccessMismatch && !hasVerificationFailure) return;
|
|
2334
|
+
|
|
2335
|
+
throw new CliError('group-management add_items partially failed.', {
|
|
2336
|
+
code: 'PARTIAL_FAILURE',
|
|
2337
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2338
|
+
hint: 'Inspect errors[] and re-run add_items after fixing failed items; automation should treat this as an unsuccessful setup.',
|
|
2339
|
+
details: truncateDetails({
|
|
2340
|
+
expected,
|
|
2341
|
+
processed,
|
|
2342
|
+
added,
|
|
2343
|
+
linked,
|
|
2344
|
+
alreadyLinked,
|
|
2345
|
+
successful,
|
|
2346
|
+
failed,
|
|
2347
|
+
verified: data.verified,
|
|
2348
|
+
finalGroupItemCount: data.final_group_item_count,
|
|
2349
|
+
expectedFinalGroupItemCount: data.expected_final_group_item_count,
|
|
2350
|
+
errors,
|
|
2351
|
+
response: data,
|
|
2352
|
+
}),
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function getGroupManagementAction(translated) {
|
|
2357
|
+
return isJsonObject(translated.normalizedPayload)
|
|
2358
|
+
? trimString(translated.normalizedPayload.action).toLowerCase()
|
|
2359
|
+
: '';
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function isGroupManagementRefreshInvocation(functionName, translated) {
|
|
2363
|
+
if (functionName !== 'group-management') return false;
|
|
2364
|
+
const action = getGroupManagementAction(translated);
|
|
2365
|
+
return action === 'refresh' || action === 'group_refresh';
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function getGroupManagementGroupId(translated) {
|
|
2369
|
+
return isJsonObject(translated.normalizedPayload)
|
|
2370
|
+
? coercePositiveInteger(translated.normalizedPayload.group_id, 'group_id')
|
|
2371
|
+
: undefined;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function getGroupItemsCount(data) {
|
|
2375
|
+
if (Array.isArray(data)) return data.length;
|
|
2376
|
+
if (!isJsonObject(data)) return null;
|
|
2377
|
+
if (Number.isFinite(data.total)) return data.total;
|
|
2378
|
+
if (Array.isArray(data.items)) return data.items.length;
|
|
2379
|
+
if (Array.isArray(data.data)) return data.data.length;
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
async function preflightGroupRefreshNotEmpty({
|
|
2384
|
+
opts,
|
|
2385
|
+
translated,
|
|
2386
|
+
apiBase,
|
|
2387
|
+
apiKey,
|
|
2388
|
+
pathPrefix,
|
|
2389
|
+
workspaceId,
|
|
2390
|
+
timeoutMs,
|
|
2391
|
+
}) {
|
|
2392
|
+
if (!isGroupManagementRefreshInvocation(opts.function, translated)) return;
|
|
2393
|
+
if (translated.normalizedPayload?.force === true) return;
|
|
2394
|
+
|
|
2395
|
+
const groupId = getGroupManagementGroupId(translated);
|
|
2396
|
+
if (!groupId) return;
|
|
2397
|
+
|
|
2398
|
+
const res = await callApi({
|
|
2399
|
+
apiBase,
|
|
2400
|
+
apiKey,
|
|
2401
|
+
path: `${pathPrefix}/groups/${groupId}/items?${new URLSearchParams({
|
|
2402
|
+
workspace_id: workspaceId,
|
|
2403
|
+
page: '1',
|
|
2404
|
+
limit: '1',
|
|
2405
|
+
}).toString()}`,
|
|
2406
|
+
method: 'GET',
|
|
2407
|
+
workspaceId,
|
|
2408
|
+
timeoutMs,
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
if (!res.ok) {
|
|
2412
|
+
throw await buildHttpError(res, {
|
|
2413
|
+
label: 'Group refresh preflight',
|
|
2414
|
+
functionName: 'group-management',
|
|
2415
|
+
method: 'GET',
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2420
|
+
if (!contentType.includes('application/json')) {
|
|
2421
|
+
throw new CliError('Group refresh preflight returned a non-JSON response.', {
|
|
2422
|
+
code: 'INVALID_RESPONSE',
|
|
2423
|
+
exitCode: EXIT_CODES.SERVER,
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
const data = await res.json();
|
|
2428
|
+
const itemCount = getGroupItemsCount(data);
|
|
2429
|
+
if (itemCount === 0) {
|
|
2430
|
+
throw new CliError('Refusing to refresh an empty tracking group.', {
|
|
2431
|
+
code: 'EMPTY_GROUP_REFRESH',
|
|
2432
|
+
exitCode: EXIT_CODES.USAGE,
|
|
2433
|
+
hint: 'Add items to the group first, or pass force:true in the group-management payload to override.',
|
|
2434
|
+
details: truncateDetails({
|
|
2435
|
+
groupId,
|
|
2436
|
+
workspaceId,
|
|
2437
|
+
itemCount,
|
|
2438
|
+
response: data,
|
|
2439
|
+
}),
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2008
2444
|
function buildSearchJourneyRunFailure(data) {
|
|
2009
2445
|
const message = isJsonObject(data) && typeof data.error === 'string' && data.error.trim().length > 0
|
|
2010
2446
|
? data.error
|
|
@@ -3188,6 +3624,18 @@ async function handleToolsCall(opts) {
|
|
|
3188
3624
|
method,
|
|
3189
3625
|
});
|
|
3190
3626
|
|
|
3627
|
+
if (isGroupManagementRefreshInvocation(opts.function, translated)) {
|
|
3628
|
+
await preflightGroupRefreshNotEmpty({
|
|
3629
|
+
opts,
|
|
3630
|
+
translated,
|
|
3631
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
3632
|
+
apiKey,
|
|
3633
|
+
pathPrefix: useGateway ? '/cli/tools/group-management' : '/functions/v1/group-management',
|
|
3634
|
+
workspaceId: effectiveWorkspaceId,
|
|
3635
|
+
timeoutMs,
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3191
3639
|
const res = await callApi({
|
|
3192
3640
|
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
3193
3641
|
apiKey,
|
|
@@ -3211,6 +3659,9 @@ async function handleToolsCall(opts) {
|
|
|
3211
3659
|
const data = await res.json();
|
|
3212
3660
|
const shouldPoll = shouldHandleSearchJourneyRunAsync(opts.function, method, payload, opts) && opts.poll !== false;
|
|
3213
3661
|
if (!shouldPoll) {
|
|
3662
|
+
if (isGroupManagementBulkAddInvocation(opts.function, translated)) {
|
|
3663
|
+
maybeThrowGroupManagementBulkAddPartialFailure(data, translated);
|
|
3664
|
+
}
|
|
3214
3665
|
maybeEmitFollowupStatusHint({
|
|
3215
3666
|
functionName: opts.function,
|
|
3216
3667
|
data,
|
|
@@ -3433,6 +3884,14 @@ async function handleToolsStatus(opts) {
|
|
|
3433
3884
|
...result,
|
|
3434
3885
|
hint: commandHint,
|
|
3435
3886
|
};
|
|
3887
|
+
if (opts.wait && isFailedStatusValue(result.status)) {
|
|
3888
|
+
throw new CliError(`${result.kind} reached terminal ${result.status} status.`, {
|
|
3889
|
+
code: 'STATUS_FAILED',
|
|
3890
|
+
exitCode: EXIT_CODES.SERVER,
|
|
3891
|
+
hint: commandHint,
|
|
3892
|
+
details: truncateDetails(payload),
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3436
3895
|
emitJsonOutput(payload, opts.pretty);
|
|
3437
3896
|
}
|
|
3438
3897
|
|
|
@@ -3586,46 +4045,27 @@ async function handleDataExportReport(opts) {
|
|
|
3586
4045
|
process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
|
|
3587
4046
|
}
|
|
3588
4047
|
|
|
3589
|
-
async function
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
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
|
-
|
|
4048
|
+
async function runExportDataCsv({
|
|
4049
|
+
opts,
|
|
4050
|
+
label,
|
|
4051
|
+
template,
|
|
4052
|
+
normalizedPayload,
|
|
4053
|
+
requestedFilename,
|
|
4054
|
+
effectiveWorkspaceId,
|
|
4055
|
+
apiBase,
|
|
4056
|
+
apiKey,
|
|
4057
|
+
useGateway,
|
|
4058
|
+
legacyUrl,
|
|
4059
|
+
timeoutMs,
|
|
4060
|
+
decorateResponse,
|
|
4061
|
+
}) {
|
|
3622
4062
|
const exportResponse = await callApi({
|
|
3623
|
-
apiBase: useGateway ?
|
|
4063
|
+
apiBase: useGateway ? apiBase : legacyUrl,
|
|
3624
4064
|
apiKey,
|
|
3625
4065
|
path: useGateway ? '/cli/tools/export-data' : '/functions/v1/export-data',
|
|
3626
4066
|
method: 'POST',
|
|
3627
4067
|
body: {
|
|
3628
|
-
template
|
|
4068
|
+
template,
|
|
3629
4069
|
format: 'csv',
|
|
3630
4070
|
payload: normalizedPayload,
|
|
3631
4071
|
filename: requestedFilename,
|
|
@@ -3636,13 +4076,16 @@ async function handleDataExportSearchResults(opts) {
|
|
|
3636
4076
|
|
|
3637
4077
|
if (!exportResponse.ok) {
|
|
3638
4078
|
throw await buildHttpError(exportResponse, {
|
|
3639
|
-
label
|
|
4079
|
+
label,
|
|
3640
4080
|
functionName: 'export-data',
|
|
3641
4081
|
method: 'POST',
|
|
3642
4082
|
});
|
|
3643
4083
|
}
|
|
3644
4084
|
|
|
3645
4085
|
const responseJson = await exportResponse.json();
|
|
4086
|
+
const decoratedResponse = typeof decorateResponse === 'function'
|
|
4087
|
+
? decorateResponse(responseJson)
|
|
4088
|
+
: responseJson;
|
|
3646
4089
|
const metadata = isJsonObject(responseJson) && isJsonObject(responseJson.metadata)
|
|
3647
4090
|
? responseJson.metadata
|
|
3648
4091
|
: null;
|
|
@@ -3657,7 +4100,7 @@ async function handleDataExportSearchResults(opts) {
|
|
|
3657
4100
|
} else {
|
|
3658
4101
|
process.stderr.write('[socialseal] Export did not include a file URL yet. Inspect the JSON metadata and retry if needed.\n');
|
|
3659
4102
|
}
|
|
3660
|
-
emitJsonOutput(
|
|
4103
|
+
emitJsonOutput(decoratedResponse, opts.pretty);
|
|
3661
4104
|
return;
|
|
3662
4105
|
}
|
|
3663
4106
|
|
|
@@ -3668,7 +4111,7 @@ async function handleDataExportSearchResults(opts) {
|
|
|
3668
4111
|
|
|
3669
4112
|
if (!artifactResponse.ok) {
|
|
3670
4113
|
throw await buildHttpError(artifactResponse, {
|
|
3671
|
-
label:
|
|
4114
|
+
label: `${label} artifact download`,
|
|
3672
4115
|
method: 'GET',
|
|
3673
4116
|
hint: 'The signed file URL may be expired or inaccessible. Re-run the export command to mint a fresh URL.',
|
|
3674
4117
|
});
|
|
@@ -3683,7 +4126,7 @@ async function handleDataExportSearchResults(opts) {
|
|
|
3683
4126
|
|
|
3684
4127
|
const outPath = opts.stdout
|
|
3685
4128
|
? null
|
|
3686
|
-
: (opts.out || trimString(metadata?.filename || '') ||
|
|
4129
|
+
: (opts.out || trimString(metadata?.filename || '') || `${template}.csv`);
|
|
3687
4130
|
if (outPath) {
|
|
3688
4131
|
await pipeline(artifactResponse.body, fs.createWriteStream(outPath));
|
|
3689
4132
|
process.stderr.write(`[socialseal] Export written to ${outPath}\n`);
|
|
@@ -3692,6 +4135,261 @@ async function handleDataExportSearchResults(opts) {
|
|
|
3692
4135
|
}
|
|
3693
4136
|
}
|
|
3694
4137
|
|
|
4138
|
+
async function handleDataExportSearchResults(opts) {
|
|
4139
|
+
const config = loadConfig();
|
|
4140
|
+
const apiKey = requireApiKey(opts, config);
|
|
4141
|
+
const apiBase = resolveApiBase(opts, config);
|
|
4142
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
4143
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
4144
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
4145
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
4146
|
+
|
|
4147
|
+
const rawPayload = opts.__rawPayload ?? stripUndefinedEntries({
|
|
4148
|
+
groupIds: normalizePositiveIntegerList(opts.groupIds, 'groupIds', { max: 100 }),
|
|
4149
|
+
trackingItemIds: normalizePositiveIntegerList(opts.trackingItemIds, 'trackingItemIds', { max: 1000 }),
|
|
4150
|
+
dateFrom: opts.dateFrom,
|
|
4151
|
+
dateTo: opts.dateTo,
|
|
4152
|
+
filename: opts.filename,
|
|
4153
|
+
});
|
|
4154
|
+
|
|
4155
|
+
const payloadWorkspaceId = resolvePayloadWorkspaceId(rawPayload, null);
|
|
4156
|
+
const effectiveWorkspaceId = requireWorkspaceSelection(payloadWorkspaceId || resolvedWorkspaceId, {
|
|
4157
|
+
label: 'Search results enriched export',
|
|
4158
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting.',
|
|
4159
|
+
});
|
|
4160
|
+
const effectiveWorkspaceSource = payloadWorkspaceId ? 'body' : workspaceSource;
|
|
4161
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
4162
|
+
workspaceId: effectiveWorkspaceId,
|
|
4163
|
+
source: effectiveWorkspaceSource,
|
|
4164
|
+
label: 'search_results_enriched export',
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
const normalizedPayload = buildSearchResultsEnrichedExportPayload(rawPayload, effectiveWorkspaceId);
|
|
4168
|
+
const requestedFilename = trimString(normalizedPayload.filename) || undefined;
|
|
4169
|
+
delete normalizedPayload.filename;
|
|
4170
|
+
|
|
4171
|
+
await runExportDataCsv({
|
|
4172
|
+
opts,
|
|
4173
|
+
label: 'Search results enriched export',
|
|
4174
|
+
template: EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW,
|
|
4175
|
+
normalizedPayload,
|
|
4176
|
+
requestedFilename,
|
|
4177
|
+
effectiveWorkspaceId,
|
|
4178
|
+
apiBase: resolvedApiBase,
|
|
4179
|
+
apiKey,
|
|
4180
|
+
useGateway,
|
|
4181
|
+
legacyUrl,
|
|
4182
|
+
timeoutMs,
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
async function handleDataExportGroupEvidence(opts) {
|
|
4187
|
+
const config = loadConfig();
|
|
4188
|
+
const apiKey = requireApiKey(opts, config);
|
|
4189
|
+
const apiBase = resolveApiBase(opts, config);
|
|
4190
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
4191
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
4192
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
4193
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
4194
|
+
|
|
4195
|
+
const groupId = coercePositiveInteger(opts.groupId, 'group_id');
|
|
4196
|
+
const effectiveWorkspaceId = requireWorkspaceSelection(resolvedWorkspaceId, {
|
|
4197
|
+
label: 'Group evidence export',
|
|
4198
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before exporting group evidence.',
|
|
4199
|
+
});
|
|
4200
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
4201
|
+
workspaceId: effectiveWorkspaceId,
|
|
4202
|
+
source: workspaceSource,
|
|
4203
|
+
label: 'group evidence export',
|
|
4204
|
+
});
|
|
4205
|
+
|
|
4206
|
+
const requestedSurface = normalizeEvidenceSurface(opts.surface);
|
|
4207
|
+
let surface = requestedSurface;
|
|
4208
|
+
if (requestedSurface === 'auto') {
|
|
4209
|
+
const groupResponse = await callApi({
|
|
4210
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
4211
|
+
apiKey,
|
|
4212
|
+
path: useGateway
|
|
4213
|
+
? `/cli/tools/group-management/groups/${groupId}?workspace_id=${encodeURIComponent(effectiveWorkspaceId)}`
|
|
4214
|
+
: `/functions/v1/group-management/groups/${groupId}?workspace_id=${encodeURIComponent(effectiveWorkspaceId)}`,
|
|
4215
|
+
method: 'GET',
|
|
4216
|
+
workspaceId: effectiveWorkspaceId,
|
|
4217
|
+
timeoutMs,
|
|
4218
|
+
});
|
|
4219
|
+
|
|
4220
|
+
if (!groupResponse.ok) {
|
|
4221
|
+
throw await buildHttpError(groupResponse, {
|
|
4222
|
+
label: 'Group evidence platform detection',
|
|
4223
|
+
functionName: 'group-management',
|
|
4224
|
+
method: 'GET',
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
const groupData = await groupResponse.json();
|
|
4229
|
+
surface = resolveEvidenceSurfaceFromGroup(groupData);
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
const rawPayload = stripUndefinedEntries({
|
|
4233
|
+
groupId,
|
|
4234
|
+
groupIds: surface === 'social' ? [groupId] : undefined,
|
|
4235
|
+
trackingItemIds: normalizePositiveIntegerList(opts.trackingItemIds, 'trackingItemIds', { max: 1000 }),
|
|
4236
|
+
dateFrom: opts.dateFrom,
|
|
4237
|
+
dateTo: opts.dateTo,
|
|
4238
|
+
filename: opts.filename,
|
|
4239
|
+
});
|
|
4240
|
+
const normalizedPayload = surface === 'google_ai'
|
|
4241
|
+
? buildGoogleAiSearchSummariesExportPayload(rawPayload, effectiveWorkspaceId)
|
|
4242
|
+
: buildSearchResultsEnrichedExportPayload(rawPayload, effectiveWorkspaceId);
|
|
4243
|
+
const requestedFilename = trimString(normalizedPayload.filename) || undefined;
|
|
4244
|
+
delete normalizedPayload.filename;
|
|
4245
|
+
const template = surface === 'google_ai'
|
|
4246
|
+
? EXPORT_DATA_TEMPLATE_GOOGLE_AI_SEARCH_SUMMARIES_RAW
|
|
4247
|
+
: EXPORT_DATA_TEMPLATE_TRACKING_RANKED_VIDEOS_RAW;
|
|
4248
|
+
|
|
4249
|
+
await runExportDataCsv({
|
|
4250
|
+
opts,
|
|
4251
|
+
label: 'Group evidence export',
|
|
4252
|
+
template,
|
|
4253
|
+
normalizedPayload,
|
|
4254
|
+
requestedFilename,
|
|
4255
|
+
effectiveWorkspaceId,
|
|
4256
|
+
apiBase: resolvedApiBase,
|
|
4257
|
+
apiKey,
|
|
4258
|
+
useGateway,
|
|
4259
|
+
legacyUrl,
|
|
4260
|
+
timeoutMs,
|
|
4261
|
+
decorateResponse: (responseJson) => ({
|
|
4262
|
+
...responseJson,
|
|
4263
|
+
evidence: buildGroupEvidenceMetadata({
|
|
4264
|
+
surface,
|
|
4265
|
+
groupId,
|
|
4266
|
+
workspaceId: effectiveWorkspaceId,
|
|
4267
|
+
responseJson,
|
|
4268
|
+
}),
|
|
4269
|
+
}),
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
async function handleDataGroupCompleteness(opts) {
|
|
4274
|
+
const config = loadConfig();
|
|
4275
|
+
const apiKey = requireApiKey(opts, config);
|
|
4276
|
+
const apiBase = resolveApiBase(opts, config);
|
|
4277
|
+
const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
|
|
4278
|
+
const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
|
|
4279
|
+
const timeoutMs = resolveTimeoutMs(opts, config);
|
|
4280
|
+
const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
|
|
4281
|
+
|
|
4282
|
+
const groupId = coercePositiveInteger(opts.groupId, 'group_id');
|
|
4283
|
+
const effectiveWorkspaceId = requireWorkspaceSelection(resolvedWorkspaceId, {
|
|
4284
|
+
label: 'Group completeness check',
|
|
4285
|
+
hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace before checking group completeness.',
|
|
4286
|
+
});
|
|
4287
|
+
emitWorkspaceSelectionNotice(opts, {
|
|
4288
|
+
workspaceId: effectiveWorkspaceId,
|
|
4289
|
+
source: workspaceSource,
|
|
4290
|
+
label: 'group completeness check',
|
|
4291
|
+
});
|
|
4292
|
+
|
|
4293
|
+
const manifest = parseJsonInput(opts.manifest, { label: 'manifest' });
|
|
4294
|
+
const expectedItems = extractExpectedManifestItems(manifest);
|
|
4295
|
+
const backendPath = useGateway
|
|
4296
|
+
? `/cli/tools/group-management/groups/${groupId}/completeness?${new URLSearchParams({
|
|
4297
|
+
workspace_id: effectiveWorkspaceId,
|
|
4298
|
+
}).toString()}`
|
|
4299
|
+
: `/functions/v1/group-management/groups/${groupId}/completeness?${new URLSearchParams({
|
|
4300
|
+
workspace_id: effectiveWorkspaceId,
|
|
4301
|
+
}).toString()}`;
|
|
4302
|
+
|
|
4303
|
+
const backendRes = await callApi({
|
|
4304
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
4305
|
+
apiKey,
|
|
4306
|
+
path: backendPath,
|
|
4307
|
+
method: 'POST',
|
|
4308
|
+
body: {
|
|
4309
|
+
expected_items: expectedItems,
|
|
4310
|
+
include_refresh_status: true,
|
|
4311
|
+
},
|
|
4312
|
+
workspaceId: effectiveWorkspaceId,
|
|
4313
|
+
timeoutMs,
|
|
4314
|
+
});
|
|
4315
|
+
|
|
4316
|
+
if (backendRes.ok) {
|
|
4317
|
+
const contentType = backendRes.headers.get('content-type') || '';
|
|
4318
|
+
if (!contentType.includes('application/json')) {
|
|
4319
|
+
throw new CliError('Group completeness check returned a non-JSON response.', {
|
|
4320
|
+
code: 'INVALID_RESPONSE',
|
|
4321
|
+
exitCode: EXIT_CODES.SERVER,
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
const backendData = await backendRes.json();
|
|
4325
|
+
emitJsonOutput(backendData, opts.pretty);
|
|
4326
|
+
if (isJsonObject(backendData) && backendData.complete === false) {
|
|
4327
|
+
process.exitCode = EXIT_CODES.SERVER;
|
|
4328
|
+
}
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
if (backendRes.status !== 404) {
|
|
4333
|
+
throw await buildHttpError(backendRes, {
|
|
4334
|
+
label: 'Group completeness check',
|
|
4335
|
+
functionName: 'group-management',
|
|
4336
|
+
method: 'POST',
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
const res = await callApi({
|
|
4341
|
+
apiBase: useGateway ? resolvedApiBase : legacyUrl,
|
|
4342
|
+
apiKey,
|
|
4343
|
+
path: useGateway
|
|
4344
|
+
? `/cli/tools/group-management/groups/${groupId}/items?${new URLSearchParams({
|
|
4345
|
+
workspace_id: effectiveWorkspaceId,
|
|
4346
|
+
page: '1',
|
|
4347
|
+
limit: String(Math.max(expectedItems.length + 100, 1000)),
|
|
4348
|
+
}).toString()}`
|
|
4349
|
+
: `/functions/v1/group-management/groups/${groupId}/items?${new URLSearchParams({
|
|
4350
|
+
workspace_id: effectiveWorkspaceId,
|
|
4351
|
+
page: '1',
|
|
4352
|
+
limit: String(Math.max(expectedItems.length + 100, 1000)),
|
|
4353
|
+
}).toString()}`,
|
|
4354
|
+
method: 'GET',
|
|
4355
|
+
workspaceId: effectiveWorkspaceId,
|
|
4356
|
+
timeoutMs,
|
|
4357
|
+
});
|
|
4358
|
+
|
|
4359
|
+
if (!res.ok) {
|
|
4360
|
+
throw await buildHttpError(res, {
|
|
4361
|
+
label: 'Group completeness check',
|
|
4362
|
+
functionName: 'group-management',
|
|
4363
|
+
method: 'GET',
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
const contentType = res.headers.get('content-type') || '';
|
|
4368
|
+
if (!contentType.includes('application/json')) {
|
|
4369
|
+
throw new CliError('Group completeness check returned a non-JSON response.', {
|
|
4370
|
+
code: 'INVALID_RESPONSE',
|
|
4371
|
+
exitCode: EXIT_CODES.SERVER,
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
const data = await res.json();
|
|
4376
|
+
const actualItems = isJsonObject(data) && Array.isArray(data.items)
|
|
4377
|
+
? data.items
|
|
4378
|
+
: (Array.isArray(data) ? data : []);
|
|
4379
|
+
const diff = buildCompletenessDiff({
|
|
4380
|
+
expectedItems,
|
|
4381
|
+
actualItems,
|
|
4382
|
+
groupId,
|
|
4383
|
+
workspaceId: effectiveWorkspaceId,
|
|
4384
|
+
});
|
|
4385
|
+
diff.source = 'manifest_fallback';
|
|
4386
|
+
|
|
4387
|
+
emitJsonOutput(diff, opts.pretty);
|
|
4388
|
+
if (!diff.complete) {
|
|
4389
|
+
process.exitCode = EXIT_CODES.SERVER;
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
|
|
3695
4393
|
function handleDataExportOptions(opts) {
|
|
3696
4394
|
const payload = {
|
|
3697
4395
|
exports: EXPORT_OPTIONS,
|
|
@@ -4053,7 +4751,7 @@ if (typeof program.showHelpAfterError === 'function') {
|
|
|
4053
4751
|
if (typeof program.showSuggestionAfterError === 'function') {
|
|
4054
4752
|
program.showSuggestionAfterError(true);
|
|
4055
4753
|
}
|
|
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`);
|
|
4754
|
+
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 socialseal data export-group-evidence --group-id 123 --workspace-id <uuid> --out evidence.csv\n`);
|
|
4057
4755
|
|
|
4058
4756
|
program
|
|
4059
4757
|
.command('agent')
|
|
@@ -4214,6 +4912,40 @@ data
|
|
|
4214
4912
|
.option('--verbose', 'Show error details')
|
|
4215
4913
|
.action((opts) => runCommand(handleDataExportSearchResults, opts));
|
|
4216
4914
|
|
|
4915
|
+
data
|
|
4916
|
+
.command('export-group-evidence')
|
|
4917
|
+
.description('Export usable group evidence, routing social groups and Google AI groups to the correct CSV export')
|
|
4918
|
+
.requiredOption('--group-id <id>', 'Tracking group id')
|
|
4919
|
+
.option('--surface <surface>', 'auto|social|google_ai', 'auto')
|
|
4920
|
+
.option('--tracking-item-ids <ids>', 'Optional comma-separated tracking item ids')
|
|
4921
|
+
.option('--date-from <iso>', 'Optional ISO datetime lower bound for social ranked exports')
|
|
4922
|
+
.option('--date-to <iso>', 'Optional ISO datetime upper bound for social ranked exports')
|
|
4923
|
+
.option('--filename <name>', 'Optional export filename stem (without extension)')
|
|
4924
|
+
.option('--out <path>', 'Output file path')
|
|
4925
|
+
.option('--stdout', 'Write to stdout')
|
|
4926
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
4927
|
+
.option('--api-key <key>', 'CLI API key')
|
|
4928
|
+
.option('--workspace-id <id>', 'Workspace id (for scoped keys)')
|
|
4929
|
+
.option('--pretty', 'Pretty-print JSON metadata when no file is ready')
|
|
4930
|
+
.option('--json', 'Emit machine-readable errors')
|
|
4931
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
4932
|
+
.option('--verbose', 'Show error details')
|
|
4933
|
+
.action((opts) => runCommand(handleDataExportGroupEvidence, opts));
|
|
4934
|
+
|
|
4935
|
+
data
|
|
4936
|
+
.command('group-completeness')
|
|
4937
|
+
.description('Compare an expected tracking-item manifest against current group items')
|
|
4938
|
+
.requiredOption('--group-id <id>', 'Tracking group id')
|
|
4939
|
+
.requiredOption('--manifest <jsonOrFile>', 'Expected items manifest JSON or @file.json')
|
|
4940
|
+
.option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
|
|
4941
|
+
.option('--api-key <key>', 'CLI API key')
|
|
4942
|
+
.option('--workspace-id <id>', 'Workspace id (for scoped keys)')
|
|
4943
|
+
.option('--pretty', 'Pretty-print JSON')
|
|
4944
|
+
.option('--json', 'Emit machine-readable errors')
|
|
4945
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
|
4946
|
+
.option('--verbose', 'Show error details')
|
|
4947
|
+
.action((opts) => runCommand(handleDataGroupCompleteness, opts));
|
|
4948
|
+
|
|
4217
4949
|
data
|
|
4218
4950
|
.command('export-report')
|
|
4219
4951
|
.description('Export report data via export-report (provisional)')
|