@socialseal/cli 0.1.2 → 0.1.3

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,9 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.3 - 2026-03-19
6
+ - Republish the current CLI release line after the successful `0.1.2` npm publish, keeping the internal and OSS package versions aligned.
7
+
5
8
  ## 0.1.2 - 2026-03-18
6
9
  - Add `search-journey-run` async CLI ergonomics: `--async` starts the backend async mode, polling is on by default, and `--no-poll` returns the initial `runId` immediately.
7
10
  - Add `--poll-interval <ms>` for async `search-journey-run` status polling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 300000;
15
15
  const DEFAULT_POLL_INTERVAL_MS = 2000;
16
16
  const MAX_TIMEOUT_MS = 900000;
17
17
  const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
18
+ const STATIC_TOOL_REGISTRY_NOTE = 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration, so environment-specific availability can drift.';
18
19
  const EXIT_CODES = {
19
20
  OK: 0,
20
21
  UNKNOWN: 1,
@@ -29,18 +30,66 @@ const KNOWN_TOOLS = [
29
30
  { name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
30
31
  { name: 'workspace-notes', category: 'agent', description: 'Search, create, update, and pin workspace note memory.' },
31
32
  { name: 'workspace-onboarding', category: 'agent', description: 'Read or update workspace onboarding metadata used by the agent.' },
32
- { name: 'brand-group-management', category: 'brand', description: 'Manage brand groups, aliases, competitors, and rule configuration.' },
33
- { name: 'enqueue-brand-metrics-backfill', category: 'brand', description: 'Queue backfill jobs for brand metrics refreshes.' },
33
+ {
34
+ name: 'brand-group-management',
35
+ category: 'brand',
36
+ description: 'Manage brand groups, aliases, competitors, and rule configuration.',
37
+ objectType: 'brand_group',
38
+ transport: 'rest_edge_function',
39
+ workspaceScoped: true,
40
+ knownLocalDevState: 'enabled',
41
+ actionAliases: ['list', 'create', 'update', 'delete', 'add_member', 'remove_member'],
42
+ notes: 'Brand groups are not tracking groups.',
43
+ },
44
+ {
45
+ name: 'enqueue-brand-metrics-backfill',
46
+ category: 'brand',
47
+ description: 'Queue backfill jobs for brand metrics refreshes.',
48
+ objectType: 'brand_metrics_backfill_job',
49
+ transport: 'post_edge_function',
50
+ workspaceScoped: true,
51
+ knownLocalDevState: 'enabled',
52
+ notes: 'Refreshes brand metrics for brands/workspaces. It does not refresh a tracking group by UUID.',
53
+ },
34
54
  { name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
35
- { name: 'export_tracking_data', category: 'export', description: 'Stream tracking exports as CSV for a group or tracking item.' },
55
+ {
56
+ name: 'export_tracking_data',
57
+ category: 'export',
58
+ description: 'Stream tracking exports as CSV for a group or tracking item.',
59
+ objectType: 'tracking_export',
60
+ transport: 'post_edge_function',
61
+ workspaceScoped: true,
62
+ knownLocalDevState: 'disabled_by_default',
63
+ notes: 'group_id expects a numeric tracking_group id, not a brand_group UUID.',
64
+ },
36
65
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
37
66
  { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
38
67
  { name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
39
68
  { name: 'tiktok-geo-api', category: 'search', description: 'Query TikTok search and geo data.' },
40
69
  { name: 'xhs-geo-api', category: 'search', description: 'Query Xiaohongshu search and geo data.' },
41
70
  { name: 'youtube-geo-api', category: 'search', description: 'Query YouTube search and geo data.' },
42
- { name: 'group-management', category: 'tracking', description: 'Create, update, list, and delete tracking groups and memberships.' },
43
- { name: 'tracking', category: 'tracking', description: 'Create, update, list, refresh, and delete tracking items.' },
71
+ {
72
+ name: 'group-management',
73
+ category: 'tracking',
74
+ description: 'Manage tracking groups and memberships.',
75
+ objectType: 'tracking_group',
76
+ transport: 'rest_edge_function',
77
+ workspaceScoped: true,
78
+ knownLocalDevState: 'disabled_by_default',
79
+ actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh', 'list_items'],
80
+ notes: 'REST-style surface under /groups. Use action aliases via `tools call` or raw REST semantics.',
81
+ },
82
+ {
83
+ name: 'tracking',
84
+ category: 'tracking',
85
+ description: 'Manage tracking items.',
86
+ objectType: 'tracking_item',
87
+ transport: 'rest_edge_function',
88
+ workspaceScoped: true,
89
+ knownLocalDevState: 'enabled',
90
+ actionAliases: ['list', 'get', 'create', 'update', 'delete', 'refresh'],
91
+ notes: 'REST-style surface. The CLI normalizes common action payloads for this tool.',
92
+ },
44
93
  { name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
45
94
  { name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
46
95
  { name: 'search-journey-run', category: 'vnext', description: 'Run a search journey for a subject across supported platforms.' },
@@ -290,10 +339,660 @@ function mergeWorkspaceIdIntoPayload(payload, workspaceId) {
290
339
  return { ...payload, workspaceId };
291
340
  }
292
341
 
342
+ function hasOwn(value, key) {
343
+ return Boolean(value) && Object.prototype.hasOwnProperty.call(value, key);
344
+ }
345
+
346
+ function firstDefined(source, keys) {
347
+ if (!source || typeof source !== 'object' || Array.isArray(source)) return undefined;
348
+ for (const key of keys) {
349
+ if (hasOwn(source, key) && source[key] !== undefined && source[key] !== null) {
350
+ return source[key];
351
+ }
352
+ }
353
+ return undefined;
354
+ }
355
+
356
+ function trimString(value) {
357
+ return typeof value === 'string' ? value.trim() : '';
358
+ }
359
+
360
+ function stripUndefinedEntries(value) {
361
+ return Object.fromEntries(
362
+ Object.entries(value || {}).filter(([, entry]) => entry !== undefined),
363
+ );
364
+ }
365
+
366
+ function resolvePayloadWorkspaceId(payload, fallbackWorkspaceId) {
367
+ const workspaceId = firstDefined(payload, ['workspace_id', 'workspaceId']);
368
+ if (typeof workspaceId === 'string' && workspaceId.trim().length > 0) {
369
+ return workspaceId.trim();
370
+ }
371
+ return fallbackWorkspaceId || null;
372
+ }
373
+
374
+ function isUuidLike(value) {
375
+ return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f-]{27}$/i.test(value.trim());
376
+ }
377
+
378
+ function coercePositiveInteger(value, label) {
379
+ if (value === undefined || value === null || value === '') return undefined;
380
+ const parsed = Number(value);
381
+ if (Number.isInteger(parsed) && parsed > 0) {
382
+ return parsed;
383
+ }
384
+ throw new CliError(`Invalid ${label}: expected a positive integer.`, {
385
+ code: 'INVALID_ARGUMENT',
386
+ exitCode: EXIT_CODES.USAGE,
387
+ hint: isUuidLike(value)
388
+ ? `${label} expects a numeric tracking id. Brand-group ids are UUIDs and use brand-group-management instead.`
389
+ : null,
390
+ details: value,
391
+ });
392
+ }
393
+
394
+ function buildPathWithQuery(basePath, query) {
395
+ const params = new URLSearchParams();
396
+ for (const [key, rawValue] of Object.entries(query || {})) {
397
+ if (rawValue === undefined || rawValue === null || rawValue === '') continue;
398
+ if (Array.isArray(rawValue)) {
399
+ for (const entry of rawValue) {
400
+ if (entry !== undefined && entry !== null && entry !== '') {
401
+ params.append(key, String(entry));
402
+ }
403
+ }
404
+ continue;
405
+ }
406
+ params.set(key, String(rawValue));
407
+ }
408
+ const queryString = params.toString();
409
+ return queryString ? `${basePath}?${queryString}` : basePath;
410
+ }
411
+
412
+ function normalizeTrackingType(value) {
413
+ const normalized = trimString(value).toLowerCase();
414
+ if (!normalized) return undefined;
415
+ if (normalized === 'keyword' || normalized === 'search') return 'search';
416
+ if (normalized === 'account' || normalized === 'creator') return 'creator';
417
+ if (normalized === 'hashtag') return 'hashtag';
418
+ throw new CliError(`Invalid tracking type: ${value}`, {
419
+ code: 'INVALID_ARGUMENT',
420
+ exitCode: EXIT_CODES.USAGE,
421
+ hint: 'Use keyword/search, hashtag, or account/creator.',
422
+ });
423
+ }
424
+
425
+ function normalizeTrackingPayload(payload, fallbackWorkspaceId) {
426
+ const trackValue = firstDefined(payload, ['track_value', 'trackValue', 'value']);
427
+ const refreshFrequency = firstDefined(payload, ['refresh_frequency', 'refreshFrequency']);
428
+ const nextRefreshAt = firstDefined(payload, ['next_refresh_at', 'nextRefreshAt']);
429
+ const region = firstDefined(payload, ['region']);
430
+ const platform = firstDefined(payload, ['platform']);
431
+ const brandIds = firstDefined(payload, ['brand_ids', 'brandIds']);
432
+ const includeInactive = firstDefined(payload, ['includeInactive', 'include_inactive']);
433
+ const isActive = firstDefined(payload, ['is_active', 'isActive']);
434
+ const limit = firstDefined(payload, ['limit']);
435
+ const page = firstDefined(payload, ['page']);
436
+ const offset = firstDefined(payload, ['offset']);
437
+ const itemId = firstDefined(payload, ['item_id', 'itemId', 'id']);
438
+
439
+ return stripUndefinedEntries({
440
+ action: trimString(firstDefined(payload, ['action'])) || undefined,
441
+ workspaceId: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId),
442
+ item_id: coercePositiveInteger(itemId, 'item_id'),
443
+ name: trimString(firstDefined(payload, ['name'])) || (trimString(trackValue) || undefined),
444
+ track_type: normalizeTrackingType(firstDefined(payload, ['track_type', 'trackType', 'type'])),
445
+ track_value: trimString(trackValue) || undefined,
446
+ refresh_frequency: trimString(refreshFrequency) || undefined,
447
+ next_refresh_at: nextRefreshAt ?? undefined,
448
+ region: typeof region === 'string' ? region.trim() || undefined : region,
449
+ platform: trimString(platform) || undefined,
450
+ brand_ids: Array.isArray(brandIds) ? brandIds : undefined,
451
+ limit: limit !== undefined ? Number(limit) : undefined,
452
+ page: page !== undefined ? Number(page) : undefined,
453
+ offset: offset !== undefined ? Number(offset) : undefined,
454
+ is_active: typeof isActive === 'boolean' ? isActive : undefined,
455
+ include_inactive: typeof includeInactive === 'boolean' ? includeInactive : undefined,
456
+ });
457
+ }
458
+
459
+ function normalizeGroupManagementPayload(payload, fallbackWorkspaceId) {
460
+ const groupId = firstDefined(payload, ['group_id', 'groupId', 'id']);
461
+ const limit = firstDefined(payload, ['limit']);
462
+ const page = firstDefined(payload, ['page']);
463
+ return stripUndefinedEntries({
464
+ action: trimString(firstDefined(payload, ['action'])) || undefined,
465
+ workspaceId: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId),
466
+ group_id: coercePositiveInteger(groupId, 'group_id'),
467
+ name: trimString(firstDefined(payload, ['name'])) || undefined,
468
+ description: firstDefined(payload, ['description']),
469
+ platform: trimString(firstDefined(payload, ['platform', 'groupPlatform'])) || undefined,
470
+ refresh_frequency: trimString(firstDefined(payload, ['refresh_frequency', 'refreshFrequency'])) || undefined,
471
+ next_refresh_at: firstDefined(payload, ['next_refresh_at', 'nextRefreshAt']) ?? undefined,
472
+ brand_id: trimString(firstDefined(payload, ['brand_id', 'brandId'])) || undefined,
473
+ limit: limit !== undefined ? Number(limit) : undefined,
474
+ page: page !== undefined ? Number(page) : undefined,
475
+ });
476
+ }
477
+
478
+ function normalizeBrandGroupPayload(payload, fallbackWorkspaceId) {
479
+ return stripUndefinedEntries({
480
+ action: trimString(firstDefined(payload, ['action'])) || undefined,
481
+ workspaceId: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId),
482
+ workspace_id: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId) || undefined,
483
+ brand_group_id: trimString(firstDefined(payload, ['brand_group_id', 'brandGroupId', 'group_id', 'groupId', 'id'])) || undefined,
484
+ brand_id: trimString(firstDefined(payload, ['brand_id', 'brandId'])) || undefined,
485
+ name: trimString(firstDefined(payload, ['name'])) || undefined,
486
+ description: firstDefined(payload, ['description']),
487
+ });
488
+ }
489
+
490
+ function normalizeBackfillPayload(payload, fallbackWorkspaceId) {
491
+ return stripUndefinedEntries({
492
+ workspace_id: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId) || undefined,
493
+ brand_id: trimString(firstDefined(payload, ['brand_id', 'brandId'])) || undefined,
494
+ tracking_group_ids: firstDefined(payload, ['tracking_group_ids', 'trackingGroupIds']),
495
+ backfill_days: firstDefined(payload, ['backfill_days', 'backfillDays']),
496
+ max_tracking_groups: firstDefined(payload, ['max_tracking_groups', 'maxTrackingGroups']),
497
+ max_videos: firstDefined(payload, ['max_videos', 'maxVideos']),
498
+ max_summaries: firstDefined(payload, ['max_summaries', 'maxSummaries']),
499
+ bump_user_revision: firstDefined(payload, ['bump_user_revision', 'bumpUserRevision']),
500
+ bump_workspace_revision: firstDefined(payload, ['bump_workspace_revision', 'bumpWorkspaceRevision']),
501
+ });
502
+ }
503
+
504
+ function normalizeTrackingExportPayload(payload, fallbackWorkspaceId) {
505
+ const groupId = firstDefined(payload, ['group_id', 'groupId', 'tracking_group_id', 'trackingGroupId']);
506
+ const itemId = firstDefined(payload, ['tracking_item_id', 'trackingItemId', 'item_id', 'itemId']);
507
+ return stripUndefinedEntries({
508
+ workspace_id: resolvePayloadWorkspaceId(payload, fallbackWorkspaceId) || undefined,
509
+ group_id: coercePositiveInteger(groupId, 'group_id'),
510
+ tracking_item_id: coercePositiveInteger(itemId, 'tracking_item_id'),
511
+ time_period: trimString(firstDefined(payload, ['time_period', 'timePeriod'])) || undefined,
512
+ });
513
+ }
514
+
515
+ function translateTrackingAction(payload, workspaceId) {
516
+ const action = payload.action ? payload.action.toLowerCase() : null;
517
+ if (!action) {
518
+ return {
519
+ method: 'POST',
520
+ pathSuffix: '',
521
+ body: stripUndefinedEntries({
522
+ name: payload.name,
523
+ track_type: payload.track_type,
524
+ track_value: payload.track_value,
525
+ refresh_frequency: payload.refresh_frequency,
526
+ next_refresh_at: payload.next_refresh_at,
527
+ region: payload.region,
528
+ platform: payload.platform,
529
+ brand_ids: payload.brand_ids,
530
+ }),
531
+ workspaceId,
532
+ };
533
+ }
534
+
535
+ if (action === 'list' || action === 'item_list') {
536
+ const limit = Number.isFinite(payload.limit) ? payload.limit : 20;
537
+ const page = Number.isFinite(payload.page) ? Math.max(1, payload.page) : 1;
538
+ const offset = Number.isFinite(payload.offset) ? Math.max(0, payload.offset) : ((page - 1) * limit);
539
+ const isActive = typeof payload.is_active === 'boolean'
540
+ ? payload.is_active
541
+ : (payload.include_inactive ? undefined : true);
542
+ return {
543
+ method: 'GET',
544
+ pathSuffix: buildPathWithQuery('', {
545
+ workspace_id: workspaceId || undefined,
546
+ limit,
547
+ offset,
548
+ track_type: payload.track_type,
549
+ track_value: payload.track_value,
550
+ platform: payload.platform,
551
+ region: payload.region,
552
+ is_active: isActive,
553
+ }),
554
+ body: undefined,
555
+ workspaceId,
556
+ };
557
+ }
558
+
559
+ if (action === 'get' || action === 'item_get') {
560
+ const itemId = coercePositiveInteger(payload.item_id, 'item_id');
561
+ if (!itemId) {
562
+ throw new CliError('item_id is required for tracking get.', {
563
+ code: 'MISSING_ARGUMENT',
564
+ exitCode: EXIT_CODES.USAGE,
565
+ });
566
+ }
567
+ return {
568
+ method: 'GET',
569
+ pathSuffix: buildPathWithQuery(`/${itemId}`, { workspace_id: workspaceId || undefined }),
570
+ body: undefined,
571
+ workspaceId,
572
+ };
573
+ }
574
+
575
+ if (action === 'create' || action === 'item_create') {
576
+ return {
577
+ method: 'POST',
578
+ pathSuffix: '',
579
+ body: stripUndefinedEntries({
580
+ name: payload.name,
581
+ track_type: payload.track_type,
582
+ track_value: payload.track_value,
583
+ refresh_frequency: payload.refresh_frequency,
584
+ next_refresh_at: payload.next_refresh_at,
585
+ region: payload.region,
586
+ platform: payload.platform,
587
+ brand_ids: payload.brand_ids,
588
+ }),
589
+ workspaceId,
590
+ };
591
+ }
592
+
593
+ if (action === 'update' || action === 'item_update') {
594
+ const itemId = coercePositiveInteger(payload.item_id, 'item_id');
595
+ if (!itemId) {
596
+ throw new CliError('item_id is required for tracking update.', {
597
+ code: 'MISSING_ARGUMENT',
598
+ exitCode: EXIT_CODES.USAGE,
599
+ });
600
+ }
601
+ return {
602
+ method: 'PATCH',
603
+ pathSuffix: buildPathWithQuery(`/${itemId}`, { workspace_id: workspaceId || undefined }),
604
+ body: stripUndefinedEntries({
605
+ refresh_frequency: payload.refresh_frequency,
606
+ next_refresh_at: payload.next_refresh_at,
607
+ }),
608
+ workspaceId,
609
+ };
610
+ }
611
+
612
+ if (action === 'delete' || action === 'item_delete') {
613
+ const itemId = coercePositiveInteger(payload.item_id, 'item_id');
614
+ if (!itemId) {
615
+ throw new CliError('item_id is required for tracking delete.', {
616
+ code: 'MISSING_ARGUMENT',
617
+ exitCode: EXIT_CODES.USAGE,
618
+ });
619
+ }
620
+ return {
621
+ method: 'DELETE',
622
+ pathSuffix: buildPathWithQuery(`/${itemId}`, { workspace_id: workspaceId || undefined }),
623
+ body: undefined,
624
+ workspaceId,
625
+ };
626
+ }
627
+
628
+ if (action === 'refresh' || action === 'item_refresh') {
629
+ const itemId = coercePositiveInteger(payload.item_id, 'item_id');
630
+ if (!itemId) {
631
+ throw new CliError('item_id is required for tracking refresh.', {
632
+ code: 'MISSING_ARGUMENT',
633
+ exitCode: EXIT_CODES.USAGE,
634
+ });
635
+ }
636
+ return {
637
+ method: 'POST',
638
+ pathSuffix: buildPathWithQuery(`/${itemId}/refresh`, { workspace_id: workspaceId || undefined }),
639
+ body: {},
640
+ workspaceId,
641
+ };
642
+ }
643
+
644
+ throw new CliError(`Unsupported tracking action: ${payload.action}`, {
645
+ code: 'INVALID_ARGUMENT',
646
+ exitCode: EXIT_CODES.USAGE,
647
+ hint: 'Supported tracking actions: list, get, create, update, delete, refresh.',
648
+ });
649
+ }
650
+
651
+ function translateGroupManagementAction(payload, workspaceId, originalMethod) {
652
+ const action = payload.action ? payload.action.toLowerCase() : null;
653
+
654
+ if (!action && originalMethod === 'GET') {
655
+ return {
656
+ method: 'GET',
657
+ pathSuffix: buildPathWithQuery('/groups', { workspace_id: workspaceId || undefined }),
658
+ body: undefined,
659
+ workspaceId,
660
+ };
661
+ }
662
+
663
+ if (!action) {
664
+ return {
665
+ method: 'POST',
666
+ pathSuffix: buildPathWithQuery('/groups', { workspace_id: workspaceId || undefined }),
667
+ body: stripUndefinedEntries({
668
+ name: payload.name,
669
+ description: payload.description,
670
+ platform: payload.platform,
671
+ refresh_frequency: payload.refresh_frequency,
672
+ next_refresh_at: payload.next_refresh_at,
673
+ brand_id: payload.brand_id,
674
+ }),
675
+ workspaceId,
676
+ };
677
+ }
678
+
679
+ if (action === 'list' || action === 'group_list') {
680
+ return {
681
+ method: 'GET',
682
+ pathSuffix: buildPathWithQuery('/groups', { workspace_id: workspaceId || undefined }),
683
+ body: undefined,
684
+ workspaceId,
685
+ };
686
+ }
687
+
688
+ if (action === 'get' || action === 'group_get') {
689
+ const groupId = coercePositiveInteger(payload.group_id, 'group_id');
690
+ if (!groupId) {
691
+ throw new CliError('group_id is required for group get.', {
692
+ code: 'MISSING_ARGUMENT',
693
+ exitCode: EXIT_CODES.USAGE,
694
+ });
695
+ }
696
+ return {
697
+ method: 'GET',
698
+ pathSuffix: buildPathWithQuery(`/groups/${groupId}`, { workspace_id: workspaceId || undefined }),
699
+ body: undefined,
700
+ workspaceId,
701
+ };
702
+ }
703
+
704
+ if (action === 'create' || action === 'group_create') {
705
+ return {
706
+ method: 'POST',
707
+ pathSuffix: buildPathWithQuery('/groups', { workspace_id: workspaceId || undefined }),
708
+ body: stripUndefinedEntries({
709
+ name: payload.name,
710
+ description: payload.description,
711
+ platform: payload.platform,
712
+ refresh_frequency: payload.refresh_frequency,
713
+ next_refresh_at: payload.next_refresh_at,
714
+ brand_id: payload.brand_id,
715
+ }),
716
+ workspaceId,
717
+ };
718
+ }
719
+
720
+ if (action === 'update' || action === 'group_update') {
721
+ const groupId = coercePositiveInteger(payload.group_id, 'group_id');
722
+ if (!groupId) {
723
+ throw new CliError('group_id is required for group update.', {
724
+ code: 'MISSING_ARGUMENT',
725
+ exitCode: EXIT_CODES.USAGE,
726
+ });
727
+ }
728
+ return {
729
+ method: 'PATCH',
730
+ pathSuffix: buildPathWithQuery(`/groups/${groupId}`, { workspace_id: workspaceId || undefined }),
731
+ body: stripUndefinedEntries({
732
+ name: payload.name,
733
+ description: payload.description,
734
+ refresh_frequency: payload.refresh_frequency,
735
+ next_refresh_at: payload.next_refresh_at,
736
+ brand_id: payload.brand_id,
737
+ }),
738
+ workspaceId,
739
+ };
740
+ }
741
+
742
+ if (action === 'delete' || action === 'group_delete') {
743
+ const groupId = coercePositiveInteger(payload.group_id, 'group_id');
744
+ if (!groupId) {
745
+ throw new CliError('group_id is required for group delete.', {
746
+ code: 'MISSING_ARGUMENT',
747
+ exitCode: EXIT_CODES.USAGE,
748
+ });
749
+ }
750
+ return {
751
+ method: 'DELETE',
752
+ pathSuffix: buildPathWithQuery(`/groups/${groupId}`, { workspace_id: workspaceId || undefined }),
753
+ body: undefined,
754
+ workspaceId,
755
+ };
756
+ }
757
+
758
+ if (action === 'refresh' || action === 'group_refresh') {
759
+ const groupId = coercePositiveInteger(payload.group_id, 'group_id');
760
+ if (!groupId) {
761
+ throw new CliError('group_id is required for group refresh.', {
762
+ code: 'MISSING_ARGUMENT',
763
+ exitCode: EXIT_CODES.USAGE,
764
+ });
765
+ }
766
+ return {
767
+ method: 'POST',
768
+ pathSuffix: buildPathWithQuery(`/groups/${groupId}/refresh`, { workspace_id: workspaceId || undefined }),
769
+ body: {},
770
+ workspaceId,
771
+ };
772
+ }
773
+
774
+ if (action === 'list_items' || action === 'group_list_items') {
775
+ const groupId = coercePositiveInteger(payload.group_id, 'group_id');
776
+ if (!groupId) {
777
+ throw new CliError('group_id is required for group list_items.', {
778
+ code: 'MISSING_ARGUMENT',
779
+ exitCode: EXIT_CODES.USAGE,
780
+ });
781
+ }
782
+ return {
783
+ method: 'GET',
784
+ pathSuffix: buildPathWithQuery(`/groups/${groupId}/items`, {
785
+ workspace_id: workspaceId || undefined,
786
+ page: Number.isFinite(payload.page) ? payload.page : undefined,
787
+ limit: Number.isFinite(payload.limit) ? payload.limit : undefined,
788
+ }),
789
+ body: undefined,
790
+ workspaceId,
791
+ };
792
+ }
793
+
794
+ throw new CliError(`Unsupported group-management action: ${payload.action}`, {
795
+ code: 'INVALID_ARGUMENT',
796
+ exitCode: EXIT_CODES.USAGE,
797
+ hint: 'Supported group-management actions: list, get, create, update, delete, refresh, list_items.',
798
+ });
799
+ }
800
+
801
+ function translateBrandGroupAction(payload, workspaceId) {
802
+ const action = payload.action ? payload.action.toLowerCase() : null;
803
+ const brandGroupId = payload.brand_group_id || undefined;
804
+ const effectiveWorkspaceId = payload.workspace_id || workspaceId || undefined;
805
+
806
+ if (!action) {
807
+ return {
808
+ method: 'POST',
809
+ pathSuffix: '',
810
+ body: stripUndefinedEntries({
811
+ name: payload.name,
812
+ description: payload.description,
813
+ workspace_id: effectiveWorkspaceId,
814
+ }),
815
+ workspaceId: workspaceId || null,
816
+ };
817
+ }
818
+
819
+ if (action === 'list') {
820
+ return {
821
+ method: 'GET',
822
+ pathSuffix: buildPathWithQuery('', { workspace_id: effectiveWorkspaceId }),
823
+ body: undefined,
824
+ workspaceId: workspaceId || null,
825
+ };
826
+ }
827
+
828
+ if (action === 'create') {
829
+ return {
830
+ method: 'POST',
831
+ pathSuffix: '',
832
+ body: stripUndefinedEntries({
833
+ name: payload.name,
834
+ description: payload.description,
835
+ workspace_id: effectiveWorkspaceId,
836
+ }),
837
+ workspaceId: workspaceId || null,
838
+ };
839
+ }
840
+
841
+ if (action === 'update') {
842
+ if (!brandGroupId) {
843
+ throw new CliError('brand_group_id is required for brand-group update.', {
844
+ code: 'MISSING_ARGUMENT',
845
+ exitCode: EXIT_CODES.USAGE,
846
+ });
847
+ }
848
+ return {
849
+ method: 'PATCH',
850
+ pathSuffix: `/${brandGroupId}`,
851
+ body: stripUndefinedEntries({
852
+ name: payload.name,
853
+ description: payload.description,
854
+ }),
855
+ workspaceId: workspaceId || null,
856
+ };
857
+ }
858
+
859
+ if (action === 'delete') {
860
+ if (!brandGroupId) {
861
+ throw new CliError('brand_group_id is required for brand-group delete.', {
862
+ code: 'MISSING_ARGUMENT',
863
+ exitCode: EXIT_CODES.USAGE,
864
+ });
865
+ }
866
+ return {
867
+ method: 'DELETE',
868
+ pathSuffix: `/${brandGroupId}`,
869
+ body: undefined,
870
+ workspaceId: workspaceId || null,
871
+ };
872
+ }
873
+
874
+ if (action === 'add_member') {
875
+ if (!brandGroupId || !payload.brand_id) {
876
+ throw new CliError('brand_group_id and brand_id are required for brand-group add_member.', {
877
+ code: 'MISSING_ARGUMENT',
878
+ exitCode: EXIT_CODES.USAGE,
879
+ });
880
+ }
881
+ return {
882
+ method: 'POST',
883
+ pathSuffix: `/${brandGroupId}/members`,
884
+ body: { brand_id: payload.brand_id },
885
+ workspaceId: workspaceId || null,
886
+ };
887
+ }
888
+
889
+ if (action === 'remove_member') {
890
+ if (!brandGroupId || !payload.brand_id) {
891
+ throw new CliError('brand_group_id and brand_id are required for brand-group remove_member.', {
892
+ code: 'MISSING_ARGUMENT',
893
+ exitCode: EXIT_CODES.USAGE,
894
+ });
895
+ }
896
+ return {
897
+ method: 'DELETE',
898
+ pathSuffix: `/${brandGroupId}/members/${payload.brand_id}`,
899
+ body: undefined,
900
+ workspaceId: workspaceId || null,
901
+ };
902
+ }
903
+
904
+ throw new CliError(`Unsupported brand-group-management action: ${payload.action}`, {
905
+ code: 'INVALID_ARGUMENT',
906
+ exitCode: EXIT_CODES.USAGE,
907
+ hint: 'Supported brand-group-management actions: list, create, update, delete, add_member, remove_member.',
908
+ });
909
+ }
910
+
911
+ function translateToolInvocation({ functionName, method, payload, resolvedWorkspaceId }) {
912
+ if (!isJsonObject(payload)) {
913
+ return {
914
+ method,
915
+ pathSuffix: '',
916
+ body: payload,
917
+ workspaceId: resolvedWorkspaceId,
918
+ normalizedPayload: payload,
919
+ };
920
+ }
921
+
922
+ if (functionName === 'tracking') {
923
+ const normalizedPayload = normalizeTrackingPayload(payload, resolvedWorkspaceId);
924
+ const workspaceId = normalizedPayload.workspaceId || resolvedWorkspaceId || null;
925
+ const translated = translateTrackingAction(normalizedPayload, workspaceId);
926
+ return { ...translated, normalizedPayload };
927
+ }
928
+
929
+ if (functionName === 'group-management') {
930
+ const normalizedPayload = normalizeGroupManagementPayload(payload, resolvedWorkspaceId);
931
+ const workspaceId = normalizedPayload.workspaceId || resolvedWorkspaceId || null;
932
+ const translated = translateGroupManagementAction(normalizedPayload, workspaceId, method);
933
+ return { ...translated, normalizedPayload };
934
+ }
935
+
936
+ if (functionName === 'brand-group-management') {
937
+ const normalizedPayload = normalizeBrandGroupPayload(payload, resolvedWorkspaceId);
938
+ const workspaceId = normalizedPayload.workspaceId || resolvedWorkspaceId || null;
939
+ const translated = translateBrandGroupAction(normalizedPayload, workspaceId);
940
+ return { ...translated, normalizedPayload };
941
+ }
942
+
943
+ if (functionName === 'enqueue-brand-metrics-backfill') {
944
+ const normalizedPayload = normalizeBackfillPayload(payload, resolvedWorkspaceId);
945
+ if (!normalizedPayload.brand_id && hasOwn(payload, 'group_id')) {
946
+ throw new CliError('enqueue-brand-metrics-backfill expects brand_id, not group_id.', {
947
+ code: 'INVALID_ARGUMENT',
948
+ exitCode: EXIT_CODES.USAGE,
949
+ hint: 'Use group-management refresh for tracking groups. Backfill jobs refresh brand metrics for a workspace brand.',
950
+ });
951
+ }
952
+ return {
953
+ method,
954
+ pathSuffix: '',
955
+ body: normalizedPayload,
956
+ workspaceId: resolvedWorkspaceId,
957
+ normalizedPayload,
958
+ };
959
+ }
960
+
961
+ if (functionName === 'export_tracking_data') {
962
+ const normalizedPayload = normalizeTrackingExportPayload(payload, resolvedWorkspaceId);
963
+ return {
964
+ method,
965
+ pathSuffix: '',
966
+ body: normalizedPayload,
967
+ workspaceId: normalizedPayload.workspace_id || resolvedWorkspaceId || null,
968
+ normalizedPayload,
969
+ };
970
+ }
971
+
972
+ return {
973
+ method,
974
+ pathSuffix: '',
975
+ body: payload,
976
+ workspaceId: resolvedWorkspaceId,
977
+ normalizedPayload: payload,
978
+ };
979
+ }
980
+
293
981
  function isJsonObject(value) {
294
982
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
295
983
  }
296
984
 
985
+ function isStateChangingMethod(method) {
986
+ return !['GET', 'HEAD', 'OPTIONS'].includes(method);
987
+ }
988
+
989
+ function emitWorkspaceContext(opts, { workspaceId, source, functionName, method }) {
990
+ if (!workspaceId || !isStateChangingMethod(method)) return;
991
+ process.stderr.write(
992
+ `[socialseal] Workspace: ${workspaceId}${source ? ` (${source})` : ''} for ${functionName} ${method}\n`,
993
+ );
994
+ }
995
+
297
996
  function sleep(ms) {
298
997
  return new Promise((resolve) => setTimeout(resolve, ms));
299
998
  }
@@ -431,6 +1130,10 @@ function mapStatusToExitCode(status) {
431
1130
  return EXIT_CODES.UNKNOWN;
432
1131
  }
433
1132
 
1133
+ function isLocallyDisabledByDefaultFunction(functionName) {
1134
+ return functionName === 'group-management' || functionName === 'export_tracking_data';
1135
+ }
1136
+
434
1137
  function buildStatusHint(status, context = {}) {
435
1138
  switch (status) {
436
1139
  case 401:
@@ -438,13 +1141,16 @@ function buildStatusHint(status, context = {}) {
438
1141
  return 'Check your CLI key and workspace access.';
439
1142
  case 404:
440
1143
  if (context.functionName) {
1144
+ if (isLocallyDisabledByDefaultFunction(context.functionName)) {
1145
+ return `Unknown function "${context.functionName}". This tool is listed in the static registry, but it is disabled by default in some local Supabase environments. Check the deployment or enable it in supabase/config.toml.`;
1146
+ }
441
1147
  return `Unknown function "${context.functionName}". Double-check the name and API base.`;
442
1148
  }
443
1149
  return 'Check the API base URL and endpoint path.';
444
1150
  case 405:
445
1151
  return `Method not allowed. Try --method GET or ensure the endpoint supports ${context.method || 'this method'}.`;
446
1152
  case 422:
447
- return 'Validation error. Review the JSON payload schema.';
1153
+ return 'Validation error. Review the JSON payload schema. For tracking/group tools, prefer the CLI action aliases or the documented REST semantics.';
448
1154
  default:
449
1155
  return null;
450
1156
  }
@@ -991,19 +1697,38 @@ async function handleToolsCall(opts) {
991
1697
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
992
1698
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
993
1699
  const timeoutMs = resolveTimeoutMs(opts, config);
994
- const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
1700
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
995
1701
 
996
1702
  const parsedPayload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
997
1703
  const mergedPayload = mergeWorkspaceIdIntoPayload(parsedPayload, resolvedWorkspaceId);
998
- const method = normalizeMethod(opts.method);
999
- const payload = applySearchJourneyRunAsyncStart(mergedPayload, { ...opts, method });
1704
+ const requestedMethod = normalizeMethod(opts.method);
1705
+ const payload = applySearchJourneyRunAsyncStart(mergedPayload, { ...opts, method: requestedMethod });
1706
+ const translated = translateToolInvocation({
1707
+ functionName: opts.function,
1708
+ method: requestedMethod,
1709
+ payload,
1710
+ resolvedWorkspaceId,
1711
+ });
1712
+ const method = normalizeMethod(translated.method);
1713
+ const effectiveWorkspaceId = translated.workspaceId ?? resolvedWorkspaceId ?? null;
1714
+ const path = useGateway
1715
+ ? `/cli/tools/${opts.function}${translated.pathSuffix || ''}`
1716
+ : `/functions/v1/${opts.function}${translated.pathSuffix || ''}`;
1717
+
1718
+ emitWorkspaceContext(opts, {
1719
+ workspaceId: effectiveWorkspaceId,
1720
+ source: effectiveWorkspaceId === resolvedWorkspaceId ? workspaceSource : 'body',
1721
+ functionName: opts.function,
1722
+ method,
1723
+ });
1724
+
1000
1725
  const res = await callApi({
1001
1726
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
1002
1727
  apiKey,
1003
- path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
1728
+ path,
1004
1729
  method,
1005
- body: payload,
1006
- workspaceId: resolvedWorkspaceId,
1730
+ body: translated.body,
1731
+ workspaceId: effectiveWorkspaceId,
1007
1732
  timeoutMs,
1008
1733
  });
1009
1734
 
@@ -1068,7 +1793,7 @@ function handleToolsList(opts) {
1068
1793
  const payload = {
1069
1794
  discovery: 'built_in_registry',
1070
1795
  tools: KNOWN_TOOLS,
1071
- note: 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration.',
1796
+ note: STATIC_TOOL_REGISTRY_NOTE,
1072
1797
  };
1073
1798
 
1074
1799
  if (opts.json) {
@@ -1085,7 +1810,16 @@ function handleToolsList(opts) {
1085
1810
  currentCategory = tool.category;
1086
1811
  process.stdout.write(`\n${currentCategory}\n`);
1087
1812
  }
1088
- process.stdout.write(`- ${tool.name}: ${tool.description}\n`);
1813
+ const qualifiers = [
1814
+ tool.objectType ? `object=${tool.objectType}` : null,
1815
+ tool.transport ? `transport=${tool.transport}` : null,
1816
+ tool.knownLocalDevState ? `local=${tool.knownLocalDevState}` : null,
1817
+ ].filter(Boolean);
1818
+ const qualifierText = qualifiers.length > 0 ? ` [${qualifiers.join(', ')}]` : '';
1819
+ process.stdout.write(`- ${tool.name}${qualifierText}: ${tool.description}\n`);
1820
+ if (tool.notes) {
1821
+ process.stdout.write(` note: ${tool.notes}\n`);
1822
+ }
1089
1823
  }
1090
1824
 
1091
1825
  process.stdout.write('\n[socialseal] Call a tool with: socialseal tools call --function <name> --body @payload.json\n');
@@ -1107,6 +1841,13 @@ async function handleDataExportTracking(opts) {
1107
1841
  });
1108
1842
  }
1109
1843
 
1844
+ if (opts.groupId !== undefined) {
1845
+ opts.groupId = coercePositiveInteger(opts.groupId, 'group_id');
1846
+ }
1847
+ if (opts.itemId !== undefined) {
1848
+ opts.itemId = coercePositiveInteger(opts.itemId, 'tracking_item_id');
1849
+ }
1850
+
1110
1851
  const payload = {
1111
1852
  tracking_item_id: opts.itemId || undefined,
1112
1853
  group_id: opts.groupId || undefined,