@socialseal/cli 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.11 - 2026-06-12
6
+
7
+ - Add ad hoc public video URL analysis parity for queue and extract workflows, including `--url` and `--allow-untracked` support.
8
+
5
9
  ## 0.1.10 - 2026-06-10
6
10
 
7
11
  - Clarify ranked search exports now include publish/observed dates and scoped tracked-search resurfacing history fields when the backend export template is deployed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -144,7 +144,7 @@ const KNOWN_TOOLS = [
144
144
  transport: 'post_edge_function',
145
145
  workspaceScoped: true,
146
146
  knownLocalDevState: 'enabled',
147
- notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items; videoId means video_uid or platform-native video id, not a tracking item id.',
147
+ notes: 'Accepts videoId/videoUid/platformVideoId/searchResultId items and public URL items with allowUntracked=true; videoId means video_uid or platform-native video id, not a tracking item id.',
148
148
  },
149
149
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
150
150
  {
@@ -258,6 +258,23 @@ const TOOL_SCHEMA_HINTS = {
258
258
  jobId: '11111111-1111-4111-8111-111111111111',
259
259
  },
260
260
  },
261
+ {
262
+ action: 'status',
263
+ required: ['action=status', 'workspaceId or --workspace-id', 'items[] with videoUid or platformVideoId'],
264
+ optional: ['includeRawAnalysis'],
265
+ notes: 'Status polling is read-only and does not accept URL items; use videoUid or platformVideoId returned by the initial URL extraction response.',
266
+ example: {
267
+ action: 'status',
268
+ workspaceId: '00000000-0000-4000-8000-000000000000',
269
+ items: [
270
+ {
271
+ videoUid: '11111111-1111-4111-8111-111111111111',
272
+ },
273
+ ],
274
+ includeAssets: false,
275
+ includeSourceVideo: false,
276
+ },
277
+ },
261
278
  ],
262
279
  cliExamples: [
263
280
  'socialseal tools call --function agent-tool-jobs --body \'{"action":"start","toolName":"search_videos","payload":{"query":"best africa safari itinerary","platform":"tiktok","region":"IN"}}\'',
@@ -376,6 +393,64 @@ const TOOL_SCHEMA_HINTS = {
376
393
  'socialseal tools call --function get-google-ai-search-results --body \'{"runId":6809,"includeCitations":true,"limit":10}\'',
377
394
  ],
378
395
  },
396
+ 'tracked-video-extract': {
397
+ summary: 'Extract assets and queue/read analysis for tracked identifiers or ad hoc public video URLs.',
398
+ operations: [
399
+ {
400
+ action: 'extract',
401
+ required: ['workspaceId or --workspace-id', 'items[] with exactly one selector'],
402
+ optional: [
403
+ 'items[].searchResultId',
404
+ 'items[].videoId',
405
+ 'items[].videoUid',
406
+ 'items[].platformVideoId',
407
+ 'items[].url',
408
+ 'allowUntracked',
409
+ 'ensureAnalysis',
410
+ 'includeAssets',
411
+ 'includeSourceVideo',
412
+ 'frameStrategy',
413
+ 'frameCount',
414
+ 'signedUrlSeconds',
415
+ ],
416
+ notes: 'URL items require request-level allowUntracked:true. Supported selectors are url, searchResultId, videoId, videoUid, or platformVideoId.',
417
+ example: {
418
+ workspaceId: '00000000-0000-4000-8000-000000000000',
419
+ allowUntracked: true,
420
+ ensureAnalysis: true,
421
+ items: [
422
+ {
423
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
424
+ },
425
+ ],
426
+ },
427
+ },
428
+ {
429
+ action: 'queue-analysis',
430
+ required: ['workspaceId or --workspace-id', 'items[] with exactly one selector'],
431
+ optional: ['allowUntracked for URL items', 'queueOnly', 'includeRawAnalysis'],
432
+ notes: 'Set ensureAnalysis:true and queueOnly:true to enqueue analysis without asset URL generation.',
433
+ example: {
434
+ workspaceId: '00000000-0000-4000-8000-000000000000',
435
+ allowUntracked: true,
436
+ ensureAnalysis: true,
437
+ queueOnly: true,
438
+ includeAssets: false,
439
+ items: [
440
+ {
441
+ url: 'https://www.tiktok.com/@creator/video/7348293840000000000',
442
+ },
443
+ ],
444
+ },
445
+ },
446
+ ],
447
+ cliExamples: [
448
+ 'socialseal video extract --url https://www.youtube.com/watch?v=dQw4w9WgXcQ --allow-untracked --wait --out-dir ./video-assets --workspace-id <workspace-uuid>',
449
+ 'socialseal video queue-analysis --url https://www.tiktok.com/@creator/video/7348293840000000000 --allow-untracked --wait --workspace-id <workspace-uuid>',
450
+ 'socialseal video extract --video-uid <video-uuid> --wait --workspace-id <workspace-uuid>',
451
+ 'socialseal tools call --function tracked-video-extract --workspace-id <workspace-uuid> --body \'{"allowUntracked":true,"items":[{"url":"https://www.instagram.com/reel/SHORTCODE/"}]}\'',
452
+ ],
453
+ },
379
454
  'group-management': {
380
455
  summary: 'Manage single-platform tracking groups and memberships.',
381
456
  operations: [
@@ -751,6 +826,7 @@ function inferExtension(urlValue, contentType, fallback = '.bin') {
751
826
  function normalizeVideoExtractBody(body) {
752
827
  const normalized = { ...body };
753
828
  const hasInlineIdentifier =
829
+ normalized.url !== undefined ||
754
830
  normalized.videoId !== undefined ||
755
831
  normalized.searchResultId !== undefined ||
756
832
  normalized.videoUid !== undefined ||
@@ -758,12 +834,14 @@ function normalizeVideoExtractBody(body) {
758
834
 
759
835
  if (!Array.isArray(normalized.items) && hasInlineIdentifier) {
760
836
  normalized.items = [{
837
+ url: normalized.url,
761
838
  videoId: normalized.videoId,
762
839
  searchResultId: normalized.searchResultId,
763
840
  videoUid: normalized.videoUid,
764
841
  platformVideoId: normalized.platformVideoId,
765
842
  platformId: normalized.platformId,
766
843
  }];
844
+ delete normalized.url;
767
845
  delete normalized.videoId;
768
846
  delete normalized.searchResultId;
769
847
  delete normalized.videoUid;
@@ -774,6 +852,11 @@ function normalizeVideoExtractBody(body) {
774
852
  return normalized;
775
853
  }
776
854
 
855
+ function hasUrlVideoItems(body) {
856
+ const items = Array.isArray(body?.items) ? body.items : [];
857
+ return items.some((item) => typeof item?.url === 'string' && item.url.trim().length > 0);
858
+ }
859
+
777
860
  function buildVideoExtractBody(opts, workspaceId) {
778
861
  const parsed = opts.body
779
862
  ? ensureJsonObject(parseJsonInput(opts.body, { label: 'body' }), 'body')
@@ -782,6 +865,7 @@ function buildVideoExtractBody(opts, workspaceId) {
782
865
 
783
866
  if (!Array.isArray(normalized.items) || normalized.items.length === 0) {
784
867
  const inlineItem = stripUndefinedEntries({
868
+ url: trimString(opts.url) || undefined,
785
869
  videoId: trimString(opts.videoId) || undefined,
786
870
  searchResultId: opts.searchResultId !== undefined
787
871
  ? coercePositiveInteger(opts.searchResultId, 'searchResultId')
@@ -791,10 +875,10 @@ function buildVideoExtractBody(opts, workspaceId) {
791
875
  });
792
876
 
793
877
  if (Object.keys(inlineItem).length === 0) {
794
- throw new CliError('Provide --body or one of --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
878
+ throw new CliError('Provide --body or one of --url, --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
795
879
  code: 'MISSING_ARGUMENT',
796
880
  exitCode: EXIT_CODES.USAGE,
797
- hint: '--video-id accepts a video_uid or platform video id. It does not accept tracking item ids.',
881
+ hint: '--url requires --allow-untracked. --video-id accepts a video_uid or platform video id; it does not accept tracking item ids.',
798
882
  });
799
883
  }
800
884
 
@@ -811,6 +895,17 @@ function buildVideoExtractBody(opts, workspaceId) {
811
895
  }
812
896
 
813
897
  const nextBody = { ...bodyWithWorkspace };
898
+ if (opts.allowUntracked === true) {
899
+ nextBody.allowUntracked = true;
900
+ }
901
+ if (hasUrlVideoItems(nextBody) && nextBody.allowUntracked !== true) {
902
+ throw new CliError('URL video analysis requires --allow-untracked or allowUntracked:true in --body.', {
903
+ code: 'ALLOW_UNTRACKED_REQUIRED',
904
+ exitCode: EXIT_CODES.USAGE,
905
+ hint: 'Pass --allow-untracked for ad hoc public URL analysis. Existing tracked identifier flows do not need this flag.',
906
+ });
907
+ }
908
+
814
909
  if (opts.wait) {
815
910
  nextBody.ensureAnalysis = true;
816
911
  } else if (opts.ensureAnalysis === true) {
@@ -868,11 +963,60 @@ function buildVideoQueueBody(opts, workspaceId) {
868
963
  function hasPendingVideoExtractResults(payload) {
869
964
  const results = Array.isArray(payload?.results) ? payload.results : [];
870
965
  return results.some((result) => {
871
- const status = String(result?.analysis?.status || '').trim().toLowerCase();
872
- return status === 'pending' || status === 'processing';
966
+ const itemStatus = String(result?.status || '').trim().toLowerCase();
967
+ const analysisStatus = String(
968
+ result?.analysis?.normalizedStatus || result?.analysis?.status || '',
969
+ ).trim().toLowerCase();
970
+ const status = itemStatus || analysisStatus;
971
+ return ACTIVE_STATUS_VALUES.has(status);
873
972
  });
874
973
  }
875
974
 
975
+ function buildVideoExtractStatusPollBody(originalBody, payload) {
976
+ const results = Array.isArray(payload?.results) ? payload.results : [];
977
+ const items = results
978
+ .map((result) => {
979
+ const resolved = isJsonObject(result?.resolvedVideo)
980
+ ? result.resolvedVideo
981
+ : (isJsonObject(result?.resolved) ? result.resolved : {});
982
+ const videoUid = trimString(resolved.videoUid || resolved.video_uid);
983
+ if (videoUid) return { videoUid };
984
+ const platformVideoId = trimString(
985
+ resolved.platformVideoId || resolved.platform_video_id,
986
+ );
987
+ if (platformVideoId) {
988
+ const item = { platformVideoId };
989
+ if (Number.isInteger(resolved.platformId)) {
990
+ item.platformId = resolved.platformId;
991
+ } else if (Number.isInteger(resolved.platform_id)) {
992
+ item.platformId = resolved.platform_id;
993
+ }
994
+ return item;
995
+ }
996
+ const request = isJsonObject(result?.request) ? result.request : null;
997
+ if (!request || request.url) return null;
998
+ return request;
999
+ })
1000
+ .filter(Boolean);
1001
+
1002
+ if (items.length === 0) {
1003
+ const originalHasUrl = Array.isArray(originalBody.items) &&
1004
+ originalBody.items.some((item) => Boolean(item?.url));
1005
+ if (originalHasUrl) return null;
1006
+ return originalBody;
1007
+ }
1008
+
1009
+ return {
1010
+ workspaceId: originalBody.workspaceId,
1011
+ action: 'status',
1012
+ items,
1013
+ includeRawAnalysis: originalBody.includeRawAnalysis === true,
1014
+ includeAssets: false,
1015
+ includeSourceVideo: false,
1016
+ ensureAnalysis: false,
1017
+ };
1018
+ }
1019
+
876
1020
  async function downloadAssetToFile({ url, outDir, stem, timeoutMs }) {
877
1021
  const response = await fetchWithTimeout(url, {
878
1022
  method: 'GET',
@@ -4448,13 +4592,13 @@ async function handleVideoExtract(opts) {
4448
4592
  method: 'POST',
4449
4593
  });
4450
4594
 
4451
- const requestOnce = async (remainingTimeoutMs) => {
4595
+ const requestOnce = async (remainingTimeoutMs, requestBody = body) => {
4452
4596
  const res = await callApi({
4453
4597
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
4454
4598
  apiKey,
4455
4599
  path,
4456
4600
  method: 'POST',
4457
- body,
4601
+ body: requestBody,
4458
4602
  workspaceId: effectiveWorkspaceId,
4459
4603
  timeoutMs: remainingTimeoutMs,
4460
4604
  });
@@ -4479,6 +4623,7 @@ async function handleVideoExtract(opts) {
4479
4623
  };
4480
4624
 
4481
4625
  let payload = await requestOnce(timeoutMs);
4626
+ let pollBody = buildVideoExtractStatusPollBody(body, payload);
4482
4627
 
4483
4628
  if (opts.wait) {
4484
4629
  const pollIntervalMs = resolvePollIntervalMs(opts);
@@ -4497,7 +4642,16 @@ async function handleVideoExtract(opts) {
4497
4642
 
4498
4643
  emitInfo(opts, 'tracked-video-extract pending; polling for completion.');
4499
4644
  await sleep(Math.min(pollIntervalMs, remainingMs));
4500
- payload = await requestOnce(Math.max(1000, deadline - Date.now()));
4645
+ if (!pollBody) {
4646
+ throw new CliError('Cannot poll URL analysis without a resolved video identifier.', {
4647
+ code: 'MISSING_RESOLVED_VIDEO_ID',
4648
+ exitCode: EXIT_CODES.SERVER,
4649
+ hint: 'Retry without --wait, then poll with the returned videoUid or platformVideoId.',
4650
+ details: truncateDetails(payload),
4651
+ });
4652
+ }
4653
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()), pollBody);
4654
+ pollBody = buildVideoExtractStatusPollBody(body, payload);
4501
4655
  }
4502
4656
  }
4503
4657
 
@@ -4536,13 +4690,13 @@ async function handleVideoQueueAnalysis(opts) {
4536
4690
  method: 'POST',
4537
4691
  });
4538
4692
 
4539
- const requestOnce = async (remainingTimeoutMs) => {
4693
+ const requestOnce = async (remainingTimeoutMs, requestBody = body) => {
4540
4694
  const res = await callApi({
4541
4695
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
4542
4696
  apiKey,
4543
4697
  path,
4544
4698
  method: 'POST',
4545
- body,
4699
+ body: requestBody,
4546
4700
  workspaceId: effectiveWorkspaceId,
4547
4701
  timeoutMs: remainingTimeoutMs,
4548
4702
  });
@@ -4567,6 +4721,7 @@ async function handleVideoQueueAnalysis(opts) {
4567
4721
  };
4568
4722
 
4569
4723
  let payload = await requestOnce(timeoutMs);
4724
+ let pollBody = buildVideoExtractStatusPollBody(body, payload);
4570
4725
 
4571
4726
  if (opts.wait) {
4572
4727
  const pollIntervalMs = resolvePollIntervalMs(opts);
@@ -4585,7 +4740,16 @@ async function handleVideoQueueAnalysis(opts) {
4585
4740
 
4586
4741
  emitInfo(opts, 'tracked-video queue-analysis pending; polling for completion.');
4587
4742
  await sleep(Math.min(pollIntervalMs, remainingMs));
4588
- payload = await requestOnce(Math.max(1000, deadline - Date.now()));
4743
+ if (!pollBody) {
4744
+ throw new CliError('Cannot poll URL analysis without a resolved video identifier.', {
4745
+ code: 'MISSING_RESOLVED_VIDEO_ID',
4746
+ exitCode: EXIT_CODES.SERVER,
4747
+ hint: 'Retry without --wait, then poll with the returned videoUid or platformVideoId.',
4748
+ details: truncateDetails(payload),
4749
+ });
4750
+ }
4751
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()), pollBody);
4752
+ pollBody = buildVideoExtractStatusPollBody(body, payload);
4589
4753
  }
4590
4754
  }
4591
4755
 
@@ -4967,12 +5131,14 @@ const video = program.command('video').description('Tracked video extraction wor
4967
5131
 
4968
5132
  video
4969
5133
  .command('queue-analysis')
4970
- .description('Queue video analysis for tracked videos or tracked search results')
5134
+ .description('Queue video analysis for tracked videos, tracked search results, or ad hoc public URLs')
5135
+ .option('--url <url>', 'Public TikTok, Instagram, or YouTube video URL (requires --allow-untracked)')
4971
5136
  .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
4972
5137
  .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
4973
5138
  .option('--video-uid <id>', 'Canonical tracked video_uid')
4974
5139
  .option('--platform-video-id <id>', 'Platform-native video id')
4975
5140
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch queueing')
5141
+ .option('--allow-untracked', 'Allow ad hoc public URL analysis for videos not already tracked')
4976
5142
  .option('--wait', 'Poll until queued/completing analyses settle')
4977
5143
  .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
4978
5144
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
@@ -4986,12 +5152,14 @@ video
4986
5152
 
4987
5153
  video
4988
5154
  .command('extract')
4989
- .description('Resolve tracked videos/results into structured analysis plus reference assets')
5155
+ .description('Resolve tracked videos/results or ad hoc public URLs into structured analysis plus reference assets')
5156
+ .option('--url <url>', 'Public TikTok, Instagram, or YouTube video URL (requires --allow-untracked)')
4990
5157
  .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
4991
5158
  .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
4992
5159
  .option('--video-uid <id>', 'Canonical tracked video_uid')
4993
5160
  .option('--platform-video-id <id>', 'Platform-native video id')
4994
5161
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch extraction')
5162
+ .option('--allow-untracked', 'Allow ad hoc public URL analysis for videos not already tracked')
4995
5163
  .option('--ensure-analysis', 'Queue analysis when it is missing')
4996
5164
  .option('--wait', 'Poll until queued/completing analyses settle')
4997
5165
  .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')