@socialseal/cli 0.1.10 → 0.1.12

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,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.12 - 2026-06-23
6
+
7
+ - Expose Asset Studio and video production tool surfaces through CLI discovery/schema output.
8
+ - Add category-filtered tool discovery for users and agents.
9
+
10
+ ## 0.1.11 - 2026-06-12
11
+
12
+ - Add ad hoc public video URL analysis parity for queue and extract workflows, including `--url` and `--allow-untracked` support.
13
+
5
14
  ## 0.1.10 - 2026-06-10
6
15
 
7
16
  - 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.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -144,7 +144,95 @@ 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
+ },
149
+ {
150
+ name: 'vnext-clips-read',
151
+ category: 'asset-studio',
152
+ description: 'List workspace clip-library items and optionally sign selected source videos.',
153
+ objectType: 'workspace_clip',
154
+ transport: 'post_edge_function',
155
+ workspaceScoped: true,
156
+ knownLocalDevState: 'enabled',
157
+ },
158
+ {
159
+ name: 'vnext-clips-create',
160
+ category: 'asset-studio',
161
+ description: 'Create signed clip upload targets and finalize uploaded clip metadata.',
162
+ objectType: 'workspace_clip',
163
+ transport: 'post_edge_function',
164
+ workspaceScoped: true,
165
+ knownLocalDevState: 'enabled',
166
+ actionAliases: ['create', 'finalize'],
167
+ notes: 'The create action returns signed upload URLs; upload bytes to storage before calling finalize.',
168
+ },
169
+ {
170
+ name: 'vnext-clip-shot-mappings-read',
171
+ category: 'asset-studio',
172
+ description: 'Read clip-to-blueprint shot mappings for Asset Studio.',
173
+ objectType: 'clip_shot_mapping',
174
+ transport: 'post_edge_function',
175
+ workspaceScoped: true,
176
+ knownLocalDevState: 'enabled',
177
+ },
178
+ {
179
+ name: 'vnext-clip-shot-mappings-write',
180
+ category: 'asset-studio',
181
+ description: 'Upsert or delete clip-to-blueprint shot mappings for Asset Studio.',
182
+ objectType: 'clip_shot_mapping',
183
+ transport: 'post_edge_function',
184
+ workspaceScoped: true,
185
+ knownLocalDevState: 'enabled',
186
+ actionAliases: ['upsert', 'delete'],
187
+ },
188
+ {
189
+ name: 'vnext-generated-assets-read',
190
+ category: 'asset-studio',
191
+ description: 'List generated rough cuts for a blueprint or read one generated asset.',
192
+ objectType: 'generated_asset',
193
+ transport: 'post_edge_function',
194
+ workspaceScoped: true,
195
+ knownLocalDevState: 'enabled',
196
+ actionAliases: ['list', 'detail'],
197
+ },
198
+ {
199
+ name: 'vnext-generated-asset-create',
200
+ category: 'asset-studio',
201
+ description: 'Create a generated rough-cut asset from an edit spec.',
202
+ objectType: 'generated_asset',
203
+ transport: 'post_edge_function',
204
+ workspaceScoped: true,
205
+ knownLocalDevState: 'enabled',
206
+ },
207
+ {
208
+ name: 'vnext-generated-asset-optimize',
209
+ category: 'asset-studio',
210
+ description: 'Optimize a generated asset or create a new revision.',
211
+ objectType: 'generated_asset_revision',
212
+ transport: 'post_edge_function',
213
+ workspaceScoped: true,
214
+ knownLocalDevState: 'enabled',
215
+ actionAliases: ['optimize', 'create-revision'],
216
+ },
217
+ {
218
+ name: 'vnext-generated-asset-export',
219
+ category: 'asset-studio',
220
+ description: 'Export a generated rough cut as FCPXML.',
221
+ objectType: 'generated_asset_export',
222
+ transport: 'post_edge_function',
223
+ workspaceScoped: true,
224
+ knownLocalDevState: 'enabled',
225
+ },
226
+ {
227
+ name: 'vnext-generated-asset-share',
228
+ category: 'asset-studio',
229
+ description: 'Create, read, or revoke generated-asset share links.',
230
+ objectType: 'generated_asset_share',
231
+ transport: 'post_edge_function',
232
+ workspaceScoped: false,
233
+ knownLocalDevState: 'enabled',
234
+ actionAliases: ['create', 'read', 'revoke'],
235
+ notes: 'create/revoke require workspaceId in the body; read uses shareToken and does not require workspace scope.',
148
236
  },
149
237
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
150
238
  {
@@ -218,7 +306,34 @@ const KNOWN_TOOLS = [
218
306
  { name: 'vnext-blueprints-create', category: 'vnext', description: 'Create a vNext blueprint from grounded evidence.' },
219
307
  { name: 'vnext-blueprints-generate', category: 'vnext', description: 'Generate a vNext blueprint from workspace opportunity data.' },
220
308
  { name: 'vnext-blueprints-read', category: 'vnext', description: 'Read vNext blueprint history and specific versions.' },
309
+ {
310
+ name: 'vnext-blueprints-shots-read',
311
+ category: 'video-production',
312
+ description: 'Read shot-lift and pinned shot assets for a blueprint.',
313
+ objectType: 'blueprint_shot_asset',
314
+ transport: 'post_edge_function',
315
+ workspaceScoped: true,
316
+ knownLocalDevState: 'enabled',
317
+ },
318
+ {
319
+ name: 'vnext-blueprints-shots-refresh',
320
+ category: 'video-production',
321
+ description: 'Queue a refresh for blueprint shot assets.',
322
+ objectType: 'blueprint_shots_job',
323
+ transport: 'post_edge_function',
324
+ workspaceScoped: true,
325
+ knownLocalDevState: 'enabled',
326
+ },
221
327
  { name: 'vnext-briefs-create', category: 'vnext', description: 'Create a vNext brief record.' },
328
+ {
329
+ name: 'vnext-briefs-export',
330
+ category: 'video-production',
331
+ description: 'Export a generated vNext brief as markdown.',
332
+ objectType: 'vnext_brief_export',
333
+ transport: 'post_edge_function',
334
+ workspaceScoped: true,
335
+ knownLocalDevState: 'enabled',
336
+ },
222
337
  { name: 'vnext-briefs-generate', category: 'vnext', description: 'Generate a vNext brief from a blueprint or opportunity.' },
223
338
  { name: 'vnext-briefs-read', category: 'vnext', description: 'Read generated vNext briefs and version history.' },
224
339
  { name: 'vnext-intents', category: 'vnext', description: 'List, create, update, or delete vNext intents.' },
@@ -258,6 +373,23 @@ const TOOL_SCHEMA_HINTS = {
258
373
  jobId: '11111111-1111-4111-8111-111111111111',
259
374
  },
260
375
  },
376
+ {
377
+ action: 'status',
378
+ required: ['action=status', 'workspaceId or --workspace-id', 'items[] with videoUid or platformVideoId'],
379
+ optional: ['includeRawAnalysis'],
380
+ notes: 'Status polling is read-only and does not accept URL items; use videoUid or platformVideoId returned by the initial URL extraction response.',
381
+ example: {
382
+ action: 'status',
383
+ workspaceId: '00000000-0000-4000-8000-000000000000',
384
+ items: [
385
+ {
386
+ videoUid: '11111111-1111-4111-8111-111111111111',
387
+ },
388
+ ],
389
+ includeAssets: false,
390
+ includeSourceVideo: false,
391
+ },
392
+ },
261
393
  ],
262
394
  cliExamples: [
263
395
  '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 +508,369 @@ const TOOL_SCHEMA_HINTS = {
376
508
  'socialseal tools call --function get-google-ai-search-results --body \'{"runId":6809,"includeCitations":true,"limit":10}\'',
377
509
  ],
378
510
  },
511
+ 'tracked-video-extract': {
512
+ summary: 'Extract assets and queue/read analysis for tracked identifiers or ad hoc public video URLs.',
513
+ operations: [
514
+ {
515
+ action: 'extract',
516
+ required: ['workspaceId or --workspace-id', 'items[] with exactly one selector'],
517
+ optional: [
518
+ 'items[].searchResultId',
519
+ 'items[].videoId',
520
+ 'items[].videoUid',
521
+ 'items[].platformVideoId',
522
+ 'items[].url',
523
+ 'allowUntracked',
524
+ 'ensureAnalysis',
525
+ 'includeAssets',
526
+ 'includeSourceVideo',
527
+ 'frameStrategy',
528
+ 'frameCount',
529
+ 'signedUrlSeconds',
530
+ ],
531
+ notes: 'URL items require request-level allowUntracked:true. Supported selectors are url, searchResultId, videoId, videoUid, or platformVideoId.',
532
+ example: {
533
+ workspaceId: '00000000-0000-4000-8000-000000000000',
534
+ allowUntracked: true,
535
+ ensureAnalysis: true,
536
+ items: [
537
+ {
538
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
539
+ },
540
+ ],
541
+ },
542
+ },
543
+ {
544
+ action: 'queue-analysis',
545
+ required: ['workspaceId or --workspace-id', 'items[] with exactly one selector'],
546
+ optional: ['allowUntracked for URL items', 'queueOnly', 'includeRawAnalysis'],
547
+ notes: 'Set ensureAnalysis:true and queueOnly:true to enqueue analysis without asset URL generation.',
548
+ example: {
549
+ workspaceId: '00000000-0000-4000-8000-000000000000',
550
+ allowUntracked: true,
551
+ ensureAnalysis: true,
552
+ queueOnly: true,
553
+ includeAssets: false,
554
+ items: [
555
+ {
556
+ url: 'https://www.tiktok.com/@creator/video/7348293840000000000',
557
+ },
558
+ ],
559
+ },
560
+ },
561
+ ],
562
+ cliExamples: [
563
+ 'socialseal video extract --url https://www.youtube.com/watch?v=dQw4w9WgXcQ --allow-untracked --wait --out-dir ./video-assets --workspace-id <workspace-uuid>',
564
+ 'socialseal video queue-analysis --url https://www.tiktok.com/@creator/video/7348293840000000000 --allow-untracked --wait --workspace-id <workspace-uuid>',
565
+ 'socialseal video extract --video-uid <video-uuid> --wait --workspace-id <workspace-uuid>',
566
+ 'socialseal tools call --function tracked-video-extract --workspace-id <workspace-uuid> --body \'{"allowUntracked":true,"items":[{"url":"https://www.instagram.com/reel/SHORTCODE/"}]}\'',
567
+ ],
568
+ },
569
+ 'vnext-clips-read': {
570
+ summary: 'List Asset Studio clip-library items and optionally sign selected video URLs.',
571
+ operations: [
572
+ {
573
+ action: 'list',
574
+ required: ['workspaceId or --workspace-id'],
575
+ optional: ['videoClipIds[] to include signed source video URLs'],
576
+ example: {
577
+ workspaceId: '00000000-0000-4000-8000-000000000000',
578
+ videoClipIds: ['11111111-1111-4111-8111-111111111111'],
579
+ },
580
+ },
581
+ ],
582
+ cliExamples: [
583
+ 'socialseal tools call --function vnext-clips-read --workspace-id <workspace-uuid> --body \'{"videoClipIds":["<clip-uuid>"]}\'',
584
+ ],
585
+ },
586
+ 'vnext-clips-create': {
587
+ summary: 'Create signed upload targets for clips and finalize uploaded clip metadata.',
588
+ operations: [
589
+ {
590
+ action: 'create',
591
+ required: ['action=create', 'workspaceId or --workspace-id', 'fileName', 'mimeType'],
592
+ optional: [],
593
+ example: {
594
+ action: 'create',
595
+ workspaceId: '00000000-0000-4000-8000-000000000000',
596
+ fileName: 'hero-shot.mp4',
597
+ mimeType: 'video/mp4',
598
+ },
599
+ },
600
+ {
601
+ action: 'finalize',
602
+ required: ['action=finalize', 'workspaceId or --workspace-id', 'clipId', 'fileName', 'storagePath', 'mimeType', 'sizeBytes', 'rightsAttested=true'],
603
+ optional: ['durationSeconds', 'width', 'height', 'posterPath'],
604
+ example: {
605
+ action: 'finalize',
606
+ workspaceId: '00000000-0000-4000-8000-000000000000',
607
+ clipId: '11111111-1111-4111-8111-111111111111',
608
+ fileName: 'hero-shot.mp4',
609
+ storagePath: 'workspace-00000000-0000-4000-8000-000000000000/11111111-1111-4111-8111-111111111111.mp4',
610
+ mimeType: 'video/mp4',
611
+ sizeBytes: 1048576,
612
+ rightsAttested: true,
613
+ },
614
+ },
615
+ ],
616
+ cliExamples: [
617
+ 'socialseal tools call --function vnext-clips-create --workspace-id <workspace-uuid> --body \'{"action":"create","fileName":"hero-shot.mp4","mimeType":"video/mp4"}\'',
618
+ 'socialseal tools call --function vnext-clips-create --workspace-id <workspace-uuid> --body @clip-finalize.json',
619
+ ],
620
+ },
621
+ 'vnext-clip-shot-mappings-read': {
622
+ summary: 'Read Asset Studio clip-to-shot mappings for a blueprint.',
623
+ operations: [
624
+ {
625
+ action: 'read',
626
+ required: ['workspaceId or --workspace-id', 'blueprintId'],
627
+ optional: [],
628
+ example: {
629
+ workspaceId: '00000000-0000-4000-8000-000000000000',
630
+ blueprintId: '22222222-2222-4222-8222-222222222222',
631
+ },
632
+ },
633
+ ],
634
+ cliExamples: [
635
+ 'socialseal tools call --function vnext-clip-shot-mappings-read --workspace-id <workspace-uuid> --body \'{"blueprintId":"<blueprint-uuid>"}\'',
636
+ ],
637
+ },
638
+ 'vnext-clip-shot-mappings-write': {
639
+ summary: 'Upsert or delete Asset Studio clip-to-shot mappings.',
640
+ operations: [
641
+ {
642
+ action: 'upsert',
643
+ required: ['action=upsert', 'workspaceId or --workspace-id', 'blueprintId', 'panelId', 'clipId'],
644
+ optional: ['source (suggested|override)', 'score'],
645
+ example: {
646
+ action: 'upsert',
647
+ workspaceId: '00000000-0000-4000-8000-000000000000',
648
+ blueprintId: '22222222-2222-4222-8222-222222222222',
649
+ panelId: 'panel-1',
650
+ clipId: '11111111-1111-4111-8111-111111111111',
651
+ source: 'override',
652
+ },
653
+ },
654
+ {
655
+ action: 'delete',
656
+ required: ['action=delete', 'workspaceId or --workspace-id', 'blueprintId', 'panelId'],
657
+ optional: [],
658
+ example: {
659
+ action: 'delete',
660
+ workspaceId: '00000000-0000-4000-8000-000000000000',
661
+ blueprintId: '22222222-2222-4222-8222-222222222222',
662
+ panelId: 'panel-1',
663
+ },
664
+ },
665
+ ],
666
+ cliExamples: [
667
+ 'socialseal tools call --function vnext-clip-shot-mappings-write --workspace-id <workspace-uuid> --body \'{"action":"upsert","blueprintId":"<blueprint-uuid>","panelId":"panel-1","clipId":"<clip-uuid>"}\'',
668
+ ],
669
+ },
670
+ 'vnext-generated-assets-read': {
671
+ summary: 'List generated rough cuts for a blueprint or read one generated asset.',
672
+ operations: [
673
+ {
674
+ action: 'list',
675
+ required: ['action=list', 'workspaceId or --workspace-id', 'blueprintId'],
676
+ optional: [],
677
+ example: {
678
+ action: 'list',
679
+ workspaceId: '00000000-0000-4000-8000-000000000000',
680
+ blueprintId: '22222222-2222-4222-8222-222222222222',
681
+ },
682
+ },
683
+ {
684
+ action: 'detail',
685
+ required: ['action=detail', 'workspaceId or --workspace-id', 'assetId'],
686
+ optional: [],
687
+ example: {
688
+ action: 'detail',
689
+ workspaceId: '00000000-0000-4000-8000-000000000000',
690
+ assetId: '33333333-3333-4333-8333-333333333333',
691
+ },
692
+ },
693
+ ],
694
+ cliExamples: [
695
+ 'socialseal tools call --function vnext-generated-assets-read --workspace-id <workspace-uuid> --body \'{"action":"list","blueprintId":"<blueprint-uuid>"}\'',
696
+ 'socialseal tools call --function vnext-generated-assets-read --workspace-id <workspace-uuid> --body \'{"action":"detail","assetId":"<asset-uuid>"}\'',
697
+ ],
698
+ },
699
+ 'vnext-generated-asset-create': {
700
+ summary: 'Create a generated rough cut from an Asset Studio edit spec.',
701
+ operations: [
702
+ {
703
+ action: 'create',
704
+ required: ['workspaceId or --workspace-id', 'blueprintId', 'title', 'editSpec'],
705
+ optional: [],
706
+ example: {
707
+ workspaceId: '00000000-0000-4000-8000-000000000000',
708
+ blueprintId: '22222222-2222-4222-8222-222222222222',
709
+ title: 'Homepage rough cut',
710
+ editSpec: {
711
+ version: 1,
712
+ fps: 30,
713
+ width: 1080,
714
+ height: 1920,
715
+ totalDurationSeconds: 3,
716
+ shots: [
717
+ {
718
+ panelId: 'panel-1',
719
+ clipId: '11111111-1111-4111-8111-111111111111',
720
+ title: 'Opening hook',
721
+ kind: 'hook',
722
+ shotLabel: 'Hero exterior',
723
+ sourceStartSeconds: 0,
724
+ durationSeconds: 3,
725
+ evidenceIds: [],
726
+ },
727
+ ],
728
+ },
729
+ },
730
+ },
731
+ ],
732
+ cliExamples: [
733
+ 'socialseal tools call --function vnext-generated-asset-create --workspace-id <workspace-uuid> --body @edit-spec.json',
734
+ ],
735
+ },
736
+ 'vnext-generated-asset-optimize': {
737
+ summary: 'Optimize a generated asset or create a new revision.',
738
+ operations: [
739
+ {
740
+ action: 'optimize',
741
+ required: ['action=optimize', 'workspaceId or --workspace-id', 'assetId'],
742
+ optional: [],
743
+ example: {
744
+ action: 'optimize',
745
+ workspaceId: '00000000-0000-4000-8000-000000000000',
746
+ assetId: '33333333-3333-4333-8333-333333333333',
747
+ },
748
+ },
749
+ {
750
+ action: 'create-revision',
751
+ required: ['action=create-revision', 'workspaceId or --workspace-id', 'assetId'],
752
+ optional: [],
753
+ example: {
754
+ action: 'create-revision',
755
+ workspaceId: '00000000-0000-4000-8000-000000000000',
756
+ assetId: '33333333-3333-4333-8333-333333333333',
757
+ },
758
+ },
759
+ ],
760
+ cliExamples: [
761
+ 'socialseal tools call --function vnext-generated-asset-optimize --workspace-id <workspace-uuid> --body \'{"action":"optimize","assetId":"<asset-uuid>"}\'',
762
+ ],
763
+ },
764
+ 'vnext-generated-asset-export': {
765
+ summary: 'Export a generated rough cut as FCPXML.',
766
+ operations: [
767
+ {
768
+ action: 'export',
769
+ required: ['workspaceId or --workspace-id', 'assetId'],
770
+ optional: ['format=fcpxml'],
771
+ example: {
772
+ workspaceId: '00000000-0000-4000-8000-000000000000',
773
+ assetId: '33333333-3333-4333-8333-333333333333',
774
+ format: 'fcpxml',
775
+ },
776
+ },
777
+ ],
778
+ cliExamples: [
779
+ 'socialseal tools call --function vnext-generated-asset-export --workspace-id <workspace-uuid> --body \'{"assetId":"<asset-uuid>","format":"fcpxml"}\'',
780
+ ],
781
+ },
782
+ 'vnext-generated-asset-share': {
783
+ summary: 'Create, read, or revoke generated rough-cut share links.',
784
+ operations: [
785
+ {
786
+ action: 'create',
787
+ required: ['action=create', 'workspaceId', 'assetId'],
788
+ optional: ['ttlSeconds', 'shareBaseUrl'],
789
+ example: {
790
+ action: 'create',
791
+ workspaceId: '00000000-0000-4000-8000-000000000000',
792
+ assetId: '33333333-3333-4333-8333-333333333333',
793
+ ttlSeconds: 604800,
794
+ },
795
+ },
796
+ {
797
+ action: 'read',
798
+ required: ['action=read', 'shareToken'],
799
+ optional: [],
800
+ example: {
801
+ action: 'read',
802
+ shareToken: '0123456789abcdef0123456789abcdef',
803
+ },
804
+ },
805
+ {
806
+ action: 'revoke',
807
+ required: ['action=revoke', 'workspaceId', 'shareLinkId'],
808
+ optional: [],
809
+ example: {
810
+ action: 'revoke',
811
+ workspaceId: '00000000-0000-4000-8000-000000000000',
812
+ shareLinkId: '44444444-4444-4444-8444-444444444444',
813
+ },
814
+ },
815
+ ],
816
+ cliExamples: [
817
+ 'socialseal tools call --function vnext-generated-asset-share --body \'{"action":"create","workspaceId":"<workspace-uuid>","assetId":"<asset-uuid>"}\'',
818
+ 'socialseal tools call --function vnext-generated-asset-share --body \'{"action":"read","shareToken":"<share-token>"}\'',
819
+ ],
820
+ },
821
+ 'vnext-blueprints-shots-read': {
822
+ summary: 'Read blueprint shot-lift rows and pinned shot assets with signed URLs.',
823
+ operations: [
824
+ {
825
+ action: 'read',
826
+ required: ['workspaceId or --workspace-id', 'blueprintId'],
827
+ optional: ['signedUrlSeconds'],
828
+ example: {
829
+ workspaceId: '00000000-0000-4000-8000-000000000000',
830
+ blueprintId: '22222222-2222-4222-8222-222222222222',
831
+ signedUrlSeconds: 3600,
832
+ },
833
+ },
834
+ ],
835
+ cliExamples: [
836
+ 'socialseal tools call --function vnext-blueprints-shots-read --workspace-id <workspace-uuid> --body \'{"blueprintId":"<blueprint-uuid>"}\'',
837
+ ],
838
+ },
839
+ 'vnext-blueprints-shots-refresh': {
840
+ summary: 'Queue a refresh for blueprint shot assets.',
841
+ operations: [
842
+ {
843
+ action: 'refresh',
844
+ required: ['workspaceId or --workspace-id', 'blueprintId'],
845
+ optional: [],
846
+ example: {
847
+ workspaceId: '00000000-0000-4000-8000-000000000000',
848
+ blueprintId: '22222222-2222-4222-8222-222222222222',
849
+ },
850
+ },
851
+ ],
852
+ cliExamples: [
853
+ 'socialseal tools call --function vnext-blueprints-shots-refresh --workspace-id <workspace-uuid> --body \'{"blueprintId":"<blueprint-uuid>"}\'',
854
+ ],
855
+ },
856
+ 'vnext-briefs-export': {
857
+ summary: 'Export the latest or selected generated vNext brief as markdown.',
858
+ operations: [
859
+ {
860
+ action: 'export',
861
+ required: ['workspaceId or --workspace-id', 'opportunityKey'],
862
+ optional: ['version'],
863
+ example: {
864
+ workspaceId: '00000000-0000-4000-8000-000000000000',
865
+ opportunityKey: 'opportunity-key',
866
+ version: 1,
867
+ },
868
+ },
869
+ ],
870
+ cliExamples: [
871
+ 'socialseal tools call --function vnext-briefs-export --workspace-id <workspace-uuid> --body \'{"opportunityKey":"<opportunity-key>"}\'',
872
+ ],
873
+ },
379
874
  'group-management': {
380
875
  summary: 'Manage single-platform tracking groups and memberships.',
381
876
  operations: [
@@ -469,6 +964,18 @@ function buildToolRegistry() {
469
964
  });
470
965
  }
471
966
 
967
+ function filterToolRegistry(tools, category) {
968
+ const normalizedCategory = trimString(category).toLowerCase();
969
+ const filtered = normalizedCategory
970
+ ? tools.filter((tool) => trimString(tool.category).toLowerCase() === normalizedCategory)
971
+ : tools;
972
+ return [...filtered].sort((a, b) => {
973
+ const categoryCompare = trimString(a.category).localeCompare(trimString(b.category));
974
+ if (categoryCompare !== 0) return categoryCompare;
975
+ return trimString(a.name).localeCompare(trimString(b.name));
976
+ });
977
+ }
978
+
472
979
  function getConfigPath() {
473
980
  return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
474
981
  }
@@ -751,6 +1258,7 @@ function inferExtension(urlValue, contentType, fallback = '.bin') {
751
1258
  function normalizeVideoExtractBody(body) {
752
1259
  const normalized = { ...body };
753
1260
  const hasInlineIdentifier =
1261
+ normalized.url !== undefined ||
754
1262
  normalized.videoId !== undefined ||
755
1263
  normalized.searchResultId !== undefined ||
756
1264
  normalized.videoUid !== undefined ||
@@ -758,12 +1266,14 @@ function normalizeVideoExtractBody(body) {
758
1266
 
759
1267
  if (!Array.isArray(normalized.items) && hasInlineIdentifier) {
760
1268
  normalized.items = [{
1269
+ url: normalized.url,
761
1270
  videoId: normalized.videoId,
762
1271
  searchResultId: normalized.searchResultId,
763
1272
  videoUid: normalized.videoUid,
764
1273
  platformVideoId: normalized.platformVideoId,
765
1274
  platformId: normalized.platformId,
766
1275
  }];
1276
+ delete normalized.url;
767
1277
  delete normalized.videoId;
768
1278
  delete normalized.searchResultId;
769
1279
  delete normalized.videoUid;
@@ -774,6 +1284,11 @@ function normalizeVideoExtractBody(body) {
774
1284
  return normalized;
775
1285
  }
776
1286
 
1287
+ function hasUrlVideoItems(body) {
1288
+ const items = Array.isArray(body?.items) ? body.items : [];
1289
+ return items.some((item) => typeof item?.url === 'string' && item.url.trim().length > 0);
1290
+ }
1291
+
777
1292
  function buildVideoExtractBody(opts, workspaceId) {
778
1293
  const parsed = opts.body
779
1294
  ? ensureJsonObject(parseJsonInput(opts.body, { label: 'body' }), 'body')
@@ -782,6 +1297,7 @@ function buildVideoExtractBody(opts, workspaceId) {
782
1297
 
783
1298
  if (!Array.isArray(normalized.items) || normalized.items.length === 0) {
784
1299
  const inlineItem = stripUndefinedEntries({
1300
+ url: trimString(opts.url) || undefined,
785
1301
  videoId: trimString(opts.videoId) || undefined,
786
1302
  searchResultId: opts.searchResultId !== undefined
787
1303
  ? coercePositiveInteger(opts.searchResultId, 'searchResultId')
@@ -791,10 +1307,10 @@ function buildVideoExtractBody(opts, workspaceId) {
791
1307
  });
792
1308
 
793
1309
  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.', {
1310
+ throw new CliError('Provide --body or one of --url, --video-id, --video-uid, --platform-video-id, or --search-result-id.', {
795
1311
  code: 'MISSING_ARGUMENT',
796
1312
  exitCode: EXIT_CODES.USAGE,
797
- hint: '--video-id accepts a video_uid or platform video id. It does not accept tracking item ids.',
1313
+ hint: '--url requires --allow-untracked. --video-id accepts a video_uid or platform video id; it does not accept tracking item ids.',
798
1314
  });
799
1315
  }
800
1316
 
@@ -811,6 +1327,17 @@ function buildVideoExtractBody(opts, workspaceId) {
811
1327
  }
812
1328
 
813
1329
  const nextBody = { ...bodyWithWorkspace };
1330
+ if (opts.allowUntracked === true) {
1331
+ nextBody.allowUntracked = true;
1332
+ }
1333
+ if (hasUrlVideoItems(nextBody) && nextBody.allowUntracked !== true) {
1334
+ throw new CliError('URL video analysis requires --allow-untracked or allowUntracked:true in --body.', {
1335
+ code: 'ALLOW_UNTRACKED_REQUIRED',
1336
+ exitCode: EXIT_CODES.USAGE,
1337
+ hint: 'Pass --allow-untracked for ad hoc public URL analysis. Existing tracked identifier flows do not need this flag.',
1338
+ });
1339
+ }
1340
+
814
1341
  if (opts.wait) {
815
1342
  nextBody.ensureAnalysis = true;
816
1343
  } else if (opts.ensureAnalysis === true) {
@@ -868,11 +1395,60 @@ function buildVideoQueueBody(opts, workspaceId) {
868
1395
  function hasPendingVideoExtractResults(payload) {
869
1396
  const results = Array.isArray(payload?.results) ? payload.results : [];
870
1397
  return results.some((result) => {
871
- const status = String(result?.analysis?.status || '').trim().toLowerCase();
872
- return status === 'pending' || status === 'processing';
1398
+ const itemStatus = String(result?.status || '').trim().toLowerCase();
1399
+ const analysisStatus = String(
1400
+ result?.analysis?.normalizedStatus || result?.analysis?.status || '',
1401
+ ).trim().toLowerCase();
1402
+ const status = itemStatus || analysisStatus;
1403
+ return ACTIVE_STATUS_VALUES.has(status);
873
1404
  });
874
1405
  }
875
1406
 
1407
+ function buildVideoExtractStatusPollBody(originalBody, payload) {
1408
+ const results = Array.isArray(payload?.results) ? payload.results : [];
1409
+ const items = results
1410
+ .map((result) => {
1411
+ const resolved = isJsonObject(result?.resolvedVideo)
1412
+ ? result.resolvedVideo
1413
+ : (isJsonObject(result?.resolved) ? result.resolved : {});
1414
+ const videoUid = trimString(resolved.videoUid || resolved.video_uid);
1415
+ if (videoUid) return { videoUid };
1416
+ const platformVideoId = trimString(
1417
+ resolved.platformVideoId || resolved.platform_video_id,
1418
+ );
1419
+ if (platformVideoId) {
1420
+ const item = { platformVideoId };
1421
+ if (Number.isInteger(resolved.platformId)) {
1422
+ item.platformId = resolved.platformId;
1423
+ } else if (Number.isInteger(resolved.platform_id)) {
1424
+ item.platformId = resolved.platform_id;
1425
+ }
1426
+ return item;
1427
+ }
1428
+ const request = isJsonObject(result?.request) ? result.request : null;
1429
+ if (!request || request.url) return null;
1430
+ return request;
1431
+ })
1432
+ .filter(Boolean);
1433
+
1434
+ if (items.length === 0) {
1435
+ const originalHasUrl = Array.isArray(originalBody.items) &&
1436
+ originalBody.items.some((item) => Boolean(item?.url));
1437
+ if (originalHasUrl) return null;
1438
+ return originalBody;
1439
+ }
1440
+
1441
+ return {
1442
+ workspaceId: originalBody.workspaceId,
1443
+ action: 'status',
1444
+ items,
1445
+ includeRawAnalysis: originalBody.includeRawAnalysis === true,
1446
+ includeAssets: false,
1447
+ includeSourceVideo: false,
1448
+ ensureAnalysis: false,
1449
+ };
1450
+ }
1451
+
876
1452
  async function downloadAssetToFile({ url, outDir, stem, timeoutMs }) {
877
1453
  const response = await fetchWithTimeout(url, {
878
1454
  method: 'GET',
@@ -1020,6 +1596,23 @@ function resolvePayloadWorkspaceId(payload, fallbackWorkspaceId) {
1020
1596
  return fallbackWorkspaceId || null;
1021
1597
  }
1022
1598
 
1599
+ function isGeneratedAssetShareScopedAction(functionName, payload) {
1600
+ if (functionName !== 'vnext-generated-asset-share' || !isJsonObject(payload)) {
1601
+ return false;
1602
+ }
1603
+ const action = trimString(payload.action).toLowerCase();
1604
+ return action === 'create' || action === 'revoke';
1605
+ }
1606
+
1607
+ function shouldRequireToolWorkspace(functionName, payload) {
1608
+ const tool = getKnownTool(functionName);
1609
+ const category = trimString(tool?.category).toLowerCase();
1610
+ return (
1611
+ Boolean(tool?.workspaceScoped) &&
1612
+ (category === 'asset-studio' || category === 'video-production')
1613
+ ) || isGeneratedAssetShareScopedAction(functionName, payload);
1614
+ }
1615
+
1023
1616
  function isUuidLike(value) {
1024
1617
  return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(value.trim());
1025
1618
  }
@@ -3612,6 +4205,26 @@ async function handleToolsCall(opts) {
3612
4205
  });
3613
4206
  }
3614
4207
 
4208
+ const scopedPayload = isJsonObject(translated.normalizedPayload)
4209
+ ? translated.normalizedPayload
4210
+ : (isJsonObject(translated.body) ? translated.body : payload);
4211
+ const hasSpecialWorkspaceHandling = new Set([
4212
+ 'group-management',
4213
+ 'export_tracking_data',
4214
+ 'tracked-video-extract',
4215
+ ]).has(opts.function);
4216
+ if (!hasSpecialWorkspaceHandling && shouldRequireToolWorkspace(opts.function, scopedPayload)) {
4217
+ requireWorkspaceSelection(effectiveWorkspaceId, {
4218
+ label: opts.function,
4219
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, include workspaceId in the body, or configure a default workspace.',
4220
+ });
4221
+ emitWorkspaceSelectionNotice(opts, {
4222
+ workspaceId: effectiveWorkspaceId,
4223
+ source: effectiveWorkspaceSource,
4224
+ label: opts.function,
4225
+ });
4226
+ }
4227
+
3615
4228
  emitTrackingCreateScopeWarning(
3616
4229
  isJsonObject(translated.normalizedPayload) ? trimString(translated.normalizedPayload.action).toLowerCase() : '',
3617
4230
  effectiveWorkspaceId,
@@ -3710,9 +4323,10 @@ async function handleToolsCall(opts) {
3710
4323
  }
3711
4324
 
3712
4325
  function handleToolsList(opts) {
3713
- const tools = buildToolRegistry();
4326
+ const tools = filterToolRegistry(buildToolRegistry(), opts.category);
3714
4327
  const payload = {
3715
4328
  discovery: 'built_in_registry',
4329
+ category: trimString(opts.category) || null,
3716
4330
  tools,
3717
4331
  note: STATIC_TOOL_REGISTRY_NOTE,
3718
4332
  schemaNote: STATIC_TOOL_SCHEMA_NOTE,
@@ -3724,6 +4338,9 @@ function handleToolsList(opts) {
3724
4338
  }
3725
4339
 
3726
4340
  process.stdout.write('[socialseal] Built-in tool registry\n');
4341
+ if (payload.category) {
4342
+ process.stdout.write(`[socialseal] Category filter: ${payload.category}\n`);
4343
+ }
3727
4344
  process.stdout.write(`[socialseal] ${payload.note}\n`);
3728
4345
  process.stdout.write(`[socialseal] ${payload.schemaNote}\n`);
3729
4346
 
@@ -4448,13 +5065,13 @@ async function handleVideoExtract(opts) {
4448
5065
  method: 'POST',
4449
5066
  });
4450
5067
 
4451
- const requestOnce = async (remainingTimeoutMs) => {
5068
+ const requestOnce = async (remainingTimeoutMs, requestBody = body) => {
4452
5069
  const res = await callApi({
4453
5070
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
4454
5071
  apiKey,
4455
5072
  path,
4456
5073
  method: 'POST',
4457
- body,
5074
+ body: requestBody,
4458
5075
  workspaceId: effectiveWorkspaceId,
4459
5076
  timeoutMs: remainingTimeoutMs,
4460
5077
  });
@@ -4479,6 +5096,7 @@ async function handleVideoExtract(opts) {
4479
5096
  };
4480
5097
 
4481
5098
  let payload = await requestOnce(timeoutMs);
5099
+ let pollBody = buildVideoExtractStatusPollBody(body, payload);
4482
5100
 
4483
5101
  if (opts.wait) {
4484
5102
  const pollIntervalMs = resolvePollIntervalMs(opts);
@@ -4497,7 +5115,16 @@ async function handleVideoExtract(opts) {
4497
5115
 
4498
5116
  emitInfo(opts, 'tracked-video-extract pending; polling for completion.');
4499
5117
  await sleep(Math.min(pollIntervalMs, remainingMs));
4500
- payload = await requestOnce(Math.max(1000, deadline - Date.now()));
5118
+ if (!pollBody) {
5119
+ throw new CliError('Cannot poll URL analysis without a resolved video identifier.', {
5120
+ code: 'MISSING_RESOLVED_VIDEO_ID',
5121
+ exitCode: EXIT_CODES.SERVER,
5122
+ hint: 'Retry without --wait, then poll with the returned videoUid or platformVideoId.',
5123
+ details: truncateDetails(payload),
5124
+ });
5125
+ }
5126
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()), pollBody);
5127
+ pollBody = buildVideoExtractStatusPollBody(body, payload);
4501
5128
  }
4502
5129
  }
4503
5130
 
@@ -4536,13 +5163,13 @@ async function handleVideoQueueAnalysis(opts) {
4536
5163
  method: 'POST',
4537
5164
  });
4538
5165
 
4539
- const requestOnce = async (remainingTimeoutMs) => {
5166
+ const requestOnce = async (remainingTimeoutMs, requestBody = body) => {
4540
5167
  const res = await callApi({
4541
5168
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
4542
5169
  apiKey,
4543
5170
  path,
4544
5171
  method: 'POST',
4545
- body,
5172
+ body: requestBody,
4546
5173
  workspaceId: effectiveWorkspaceId,
4547
5174
  timeoutMs: remainingTimeoutMs,
4548
5175
  });
@@ -4567,6 +5194,7 @@ async function handleVideoQueueAnalysis(opts) {
4567
5194
  };
4568
5195
 
4569
5196
  let payload = await requestOnce(timeoutMs);
5197
+ let pollBody = buildVideoExtractStatusPollBody(body, payload);
4570
5198
 
4571
5199
  if (opts.wait) {
4572
5200
  const pollIntervalMs = resolvePollIntervalMs(opts);
@@ -4585,7 +5213,16 @@ async function handleVideoQueueAnalysis(opts) {
4585
5213
 
4586
5214
  emitInfo(opts, 'tracked-video queue-analysis pending; polling for completion.');
4587
5215
  await sleep(Math.min(pollIntervalMs, remainingMs));
4588
- payload = await requestOnce(Math.max(1000, deadline - Date.now()));
5216
+ if (!pollBody) {
5217
+ throw new CliError('Cannot poll URL analysis without a resolved video identifier.', {
5218
+ code: 'MISSING_RESOLVED_VIDEO_ID',
5219
+ exitCode: EXIT_CODES.SERVER,
5220
+ hint: 'Retry without --wait, then poll with the returned videoUid or platformVideoId.',
5221
+ details: truncateDetails(payload),
5222
+ });
5223
+ }
5224
+ payload = await requestOnce(Math.max(1000, deadline - Date.now()), pollBody);
5225
+ pollBody = buildVideoExtractStatusPollBody(body, payload);
4589
5226
  }
4590
5227
  }
4591
5228
 
@@ -4819,6 +5456,7 @@ const tools = program.command('tools').description('Call edge functions directly
4819
5456
  tools
4820
5457
  .command('list')
4821
5458
  .description('List built-in tool registry entries')
5459
+ .option('--category <name>', 'Filter tools by category')
4822
5460
  .option('--json', 'Emit machine-readable output')
4823
5461
  .option('--pretty', 'Pretty-print JSON')
4824
5462
  .option('--verbose', 'Show error details')
@@ -4967,12 +5605,14 @@ const video = program.command('video').description('Tracked video extraction wor
4967
5605
 
4968
5606
  video
4969
5607
  .command('queue-analysis')
4970
- .description('Queue video analysis for tracked videos or tracked search results')
5608
+ .description('Queue video analysis for tracked videos, tracked search results, or ad hoc public URLs')
5609
+ .option('--url <url>', 'Public TikTok, Instagram, or YouTube video URL (requires --allow-untracked)')
4971
5610
  .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
4972
5611
  .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
4973
5612
  .option('--video-uid <id>', 'Canonical tracked video_uid')
4974
5613
  .option('--platform-video-id <id>', 'Platform-native video id')
4975
5614
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch queueing')
5615
+ .option('--allow-untracked', 'Allow ad hoc public URL analysis for videos not already tracked')
4976
5616
  .option('--wait', 'Poll until queued/completing analyses settle')
4977
5617
  .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')
4978
5618
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
@@ -4986,12 +5626,14 @@ video
4986
5626
 
4987
5627
  video
4988
5628
  .command('extract')
4989
- .description('Resolve tracked videos/results into structured analysis plus reference assets')
5629
+ .description('Resolve tracked videos/results or ad hoc public URLs into structured analysis plus reference assets')
5630
+ .option('--url <url>', 'Public TikTok, Instagram, or YouTube video URL (requires --allow-untracked)')
4990
5631
  .option('--video-id <id>', 'Tracked video identifier (video_uid first, then platform video id; not a tracking item id)')
4991
5632
  .option('--search-result-id <id>', 'Tracked search result id for a ranked result row')
4992
5633
  .option('--video-uid <id>', 'Canonical tracked video_uid')
4993
5634
  .option('--platform-video-id <id>', 'Platform-native video id')
4994
5635
  .option('--body <jsonOrFile>', 'JSON body or @payload.json for batch extraction')
5636
+ .option('--allow-untracked', 'Allow ad hoc public URL analysis for videos not already tracked')
4995
5637
  .option('--ensure-analysis', 'Queue analysis when it is missing')
4996
5638
  .option('--wait', 'Poll until queued/completing analyses settle')
4997
5639
  .option('--poll-interval <ms>', 'Polling interval in milliseconds when --wait is enabled')