@socialseal/cli 0.1.1 → 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/src/index.js CHANGED
@@ -12,8 +12,10 @@ const CLI_KEY_HEADER = 'X-CLI-Key';
12
12
  const WORKSPACE_HEADER = 'X-Workspace-Id';
13
13
  const DEFAULT_TIMEOUT_MS = 30000;
14
14
  const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 300000;
15
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
15
16
  const MAX_TIMEOUT_MS = 900000;
16
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.';
17
19
  const EXIT_CODES = {
18
20
  OK: 0,
19
21
  UNKNOWN: 1,
@@ -28,18 +30,66 @@ const KNOWN_TOOLS = [
28
30
  { name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
29
31
  { name: 'workspace-notes', category: 'agent', description: 'Search, create, update, and pin workspace note memory.' },
30
32
  { name: 'workspace-onboarding', category: 'agent', description: 'Read or update workspace onboarding metadata used by the agent.' },
31
- { name: 'brand-group-management', category: 'brand', description: 'Manage brand groups, aliases, competitors, and rule configuration.' },
32
- { 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
+ },
33
54
  { name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
34
- { 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
+ },
35
65
  { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
36
66
  { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
37
67
  { name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
38
68
  { name: 'tiktok-geo-api', category: 'search', description: 'Query TikTok search and geo data.' },
39
69
  { name: 'xhs-geo-api', category: 'search', description: 'Query Xiaohongshu search and geo data.' },
40
70
  { name: 'youtube-geo-api', category: 'search', description: 'Query YouTube search and geo data.' },
41
- { name: 'group-management', category: 'tracking', description: 'Create, update, list, and delete tracking groups and memberships.' },
42
- { 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
+ },
43
93
  { name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
44
94
  { name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
45
95
  { name: 'search-journey-run', category: 'vnext', description: 'Run a search journey for a subject across supported platforms.' },
@@ -58,8 +108,12 @@ const KNOWN_TOOLS = [
58
108
  { name: 'vnext-topics-auto-tag', category: 'vnext', description: 'Auto-tag keyword and topic assignments with Gemini-assisted review.' },
59
109
  ];
60
110
 
111
+ function getConfigPath() {
112
+ return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
113
+ }
114
+
61
115
  function loadConfig() {
62
- const configPath = process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
116
+ const configPath = getConfigPath();
63
117
  try {
64
118
  if (!fs.existsSync(configPath)) return {};
65
119
  const raw = fs.readFileSync(configPath, 'utf8');
@@ -70,6 +124,15 @@ function loadConfig() {
70
124
  }
71
125
  }
72
126
 
127
+ function saveConfig(config) {
128
+ const configPath = getConfigPath();
129
+ const normalizedConfig = Object.fromEntries(
130
+ Object.entries(config || {}).filter(([, value]) => value !== undefined),
131
+ );
132
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
133
+ fs.writeFileSync(configPath, `${JSON.stringify(normalizedConfig, null, 2)}\n`);
134
+ }
135
+
73
136
  function resolveApiKey(opts, config) {
74
137
  return opts.apiKey || process.env.SOCIALSEAL_API_KEY || config.apiKey;
75
138
  }
@@ -86,6 +149,19 @@ function resolveSupabaseUrl(opts, config) {
86
149
  return opts.supabaseUrl || process.env.SOCIALSEAL_SUPABASE_URL || config.supabaseUrl;
87
150
  }
88
151
 
152
+ function resolveWorkspaceSelection(opts, config) {
153
+ if (typeof opts.workspaceId === 'string' && opts.workspaceId.trim().length > 0) {
154
+ return { workspaceId: opts.workspaceId.trim(), source: 'flag' };
155
+ }
156
+ if (typeof process.env.SOCIALSEAL_WORKSPACE_ID === 'string' && process.env.SOCIALSEAL_WORKSPACE_ID.trim().length > 0) {
157
+ return { workspaceId: process.env.SOCIALSEAL_WORKSPACE_ID.trim(), source: 'env' };
158
+ }
159
+ if (typeof config.workspaceId === 'string' && config.workspaceId.trim().length > 0) {
160
+ return { workspaceId: config.workspaceId.trim(), source: 'config' };
161
+ }
162
+ return { workspaceId: null, source: null };
163
+ }
164
+
89
165
  class CliError extends Error {
90
166
  constructor(message, { code = 'CLI_ERROR', exitCode = EXIT_CODES.UNKNOWN, status, hint, details } = {}) {
91
167
  super(message);
@@ -252,6 +328,800 @@ function ensureJsonObject(value, label) {
252
328
  return value;
253
329
  }
254
330
 
331
+ function mergeWorkspaceIdIntoPayload(payload, workspaceId) {
332
+ if (!workspaceId) return payload;
333
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
334
+ return payload;
335
+ }
336
+ if (typeof payload.workspaceId === 'string' && payload.workspaceId.trim().length > 0) {
337
+ return payload;
338
+ }
339
+ return { ...payload, workspaceId };
340
+ }
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
+
981
+ function isJsonObject(value) {
982
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
983
+ }
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
+
996
+ function sleep(ms) {
997
+ return new Promise((resolve) => setTimeout(resolve, ms));
998
+ }
999
+
1000
+ function resolvePollIntervalMs(opts) {
1001
+ const raw = opts.pollInterval ?? process.env.SOCIALSEAL_POLL_INTERVAL_MS;
1002
+ return parseTimeoutMs(raw, { defaultValue: DEFAULT_POLL_INTERVAL_MS, label: 'poll interval' });
1003
+ }
1004
+
1005
+ function shouldHandleSearchJourneyRunAsync(functionName, method, payload, opts) {
1006
+ if (String(functionName || '').trim() !== 'search-journey-run') return false;
1007
+ if (method !== 'POST') return false;
1008
+ if (!isJsonObject(payload)) return false;
1009
+ if (payload.action === 'status') return false;
1010
+ return opts.async === true || payload.executionMode === 'async';
1011
+ }
1012
+
1013
+ function applySearchJourneyRunAsyncStart(payload, opts) {
1014
+ if (!shouldHandleSearchJourneyRunAsync(opts.function, normalizeMethod(opts.method), payload, opts)) {
1015
+ return payload;
1016
+ }
1017
+ return {
1018
+ ...payload,
1019
+ executionMode: 'async',
1020
+ };
1021
+ }
1022
+
1023
+ function formatJsonOutput(value, pretty) {
1024
+ return JSON.stringify(value, null, pretty ? 2 : 0);
1025
+ }
1026
+
1027
+ function emitJsonOutput(value, pretty) {
1028
+ process.stdout.write(formatJsonOutput(value, pretty) + '\n');
1029
+ }
1030
+
1031
+ function buildSearchJourneyRunFailure(data) {
1032
+ const message = isJsonObject(data) && typeof data.error === 'string' && data.error.trim().length > 0
1033
+ ? data.error
1034
+ : 'search-journey-run failed';
1035
+ return new CliError(message, {
1036
+ code: 'ASYNC_RUN_FAILED',
1037
+ exitCode: EXIT_CODES.SERVER,
1038
+ details: truncateDetails(data),
1039
+ });
1040
+ }
1041
+
1042
+ async function pollSearchJourneyRun({
1043
+ apiBase,
1044
+ apiKey,
1045
+ path,
1046
+ workspaceId,
1047
+ timeoutMs,
1048
+ pollIntervalMs,
1049
+ runId,
1050
+ opts,
1051
+ }) {
1052
+ if (!workspaceId) {
1053
+ throw new CliError('Async search-journey-run polling requires a workspace id.', {
1054
+ code: 'WORKSPACE_REQUIRED',
1055
+ exitCode: EXIT_CODES.USAGE,
1056
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
1057
+ });
1058
+ }
1059
+
1060
+ const deadline = Date.now() + timeoutMs;
1061
+ let lastStatus = null;
1062
+
1063
+ for (;;) {
1064
+ const remainingMs = deadline - Date.now();
1065
+ if (remainingMs <= 0) {
1066
+ throw new CliError('Timed out waiting for search-journey-run async completion.', {
1067
+ code: 'ASYNC_WAIT_TIMEOUT',
1068
+ exitCode: EXIT_CODES.SERVER,
1069
+ hint: 'Increase --timeout <ms> or use --no-poll to return the run id immediately.',
1070
+ details: truncateDetails({ runId, workspaceId, lastStatus }),
1071
+ });
1072
+ }
1073
+
1074
+ await sleep(Math.min(pollIntervalMs, remainingMs));
1075
+
1076
+ const res = await callApi({
1077
+ apiBase,
1078
+ apiKey,
1079
+ path,
1080
+ method: 'POST',
1081
+ body: {
1082
+ action: 'status',
1083
+ workspaceId,
1084
+ runId,
1085
+ },
1086
+ workspaceId,
1087
+ timeoutMs: remainingMs,
1088
+ });
1089
+
1090
+ if (!res.ok) {
1091
+ throw await buildHttpError(res, {
1092
+ label: 'Tool status poll',
1093
+ functionName: 'search-journey-run',
1094
+ method: 'POST',
1095
+ });
1096
+ }
1097
+
1098
+ const contentType = res.headers.get('content-type') || '';
1099
+ if (!contentType.includes('application/json')) {
1100
+ throw new CliError('search-journey-run status poll returned a non-JSON response.', {
1101
+ code: 'INVALID_STATUS_RESPONSE',
1102
+ exitCode: EXIT_CODES.SERVER,
1103
+ });
1104
+ }
1105
+
1106
+ const data = await res.json();
1107
+ const status = isJsonObject(data) && typeof data.status === 'string' ? data.status : null;
1108
+ if (status && status !== lastStatus) {
1109
+ emitInfo(opts, `search-journey-run status: ${status}`);
1110
+ lastStatus = status;
1111
+ }
1112
+
1113
+ if (status === 'completed') return data;
1114
+ if (status === 'failed') throw buildSearchJourneyRunFailure(data);
1115
+ if (status === 'pending' || status === 'processing') continue;
1116
+
1117
+ throw new CliError('search-journey-run status poll returned an unexpected payload.', {
1118
+ code: 'INVALID_STATUS_RESPONSE',
1119
+ exitCode: EXIT_CODES.SERVER,
1120
+ details: truncateDetails(data),
1121
+ });
1122
+ }
1123
+ }
1124
+
255
1125
  function mapStatusToExitCode(status) {
256
1126
  if (status === 401 || status === 403) return EXIT_CODES.AUTH;
257
1127
  if (status === 404) return EXIT_CODES.NOT_FOUND;
@@ -260,6 +1130,10 @@ function mapStatusToExitCode(status) {
260
1130
  return EXIT_CODES.UNKNOWN;
261
1131
  }
262
1132
 
1133
+ function isLocallyDisabledByDefaultFunction(functionName) {
1134
+ return functionName === 'group-management' || functionName === 'export_tracking_data';
1135
+ }
1136
+
263
1137
  function buildStatusHint(status, context = {}) {
264
1138
  switch (status) {
265
1139
  case 401:
@@ -267,13 +1141,16 @@ function buildStatusHint(status, context = {}) {
267
1141
  return 'Check your CLI key and workspace access.';
268
1142
  case 404:
269
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
+ }
270
1147
  return `Unknown function "${context.functionName}". Double-check the name and API base.`;
271
1148
  }
272
1149
  return 'Check the API base URL and endpoint path.';
273
1150
  case 405:
274
1151
  return `Method not allowed. Try --method GET or ensure the endpoint supports ${context.method || 'this method'}.`;
275
1152
  case 422:
276
- 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.';
277
1154
  default:
278
1155
  return null;
279
1156
  }
@@ -437,6 +1314,68 @@ async function callApi({ apiBase, apiKey, path, method = 'POST', body, workspace
437
1314
  return res;
438
1315
  }
439
1316
 
1317
+ async function fetchWorkspaceDirectory({ apiBase, apiKey, timeoutMs }) {
1318
+ const res = await callApi({
1319
+ apiBase,
1320
+ apiKey,
1321
+ path: '/cli/workspaces',
1322
+ method: 'GET',
1323
+ timeoutMs,
1324
+ });
1325
+ if (!res.ok) {
1326
+ throw await buildHttpError(res, { label: 'Workspace discovery' });
1327
+ }
1328
+ const payload = await res.json();
1329
+ return payload?.data || {};
1330
+ }
1331
+
1332
+ function matchWorkspaceIdentifier(workspaces, identifier) {
1333
+ const normalized = String(identifier || '').trim();
1334
+ if (!normalized) {
1335
+ throw new CliError('Missing workspace identifier.', {
1336
+ code: 'MISSING_ARGUMENT',
1337
+ exitCode: EXIT_CODES.USAGE,
1338
+ hint: 'Use a workspace id, slug, or exact name from `socialseal workspace list`.',
1339
+ });
1340
+ }
1341
+
1342
+ const exactId = workspaces.find((workspace) => workspace.id === normalized);
1343
+ if (exactId) return exactId;
1344
+
1345
+ const exactSlug = workspaces.find((workspace) => workspace.slug === normalized);
1346
+ if (exactSlug) return exactSlug;
1347
+
1348
+ const exactNameMatches = workspaces.filter(
1349
+ (workspace) => typeof workspace.name === 'string' && workspace.name.trim().toLowerCase() === normalized.toLowerCase(),
1350
+ );
1351
+ if (exactNameMatches.length === 1) {
1352
+ return exactNameMatches[0];
1353
+ }
1354
+ if (exactNameMatches.length > 1) {
1355
+ throw new CliError(`Workspace name "${normalized}" is ambiguous.`, {
1356
+ code: 'AMBIGUOUS_WORKSPACE',
1357
+ exitCode: EXIT_CODES.USAGE,
1358
+ hint: 'Use the workspace id or slug from `socialseal workspace list`.',
1359
+ });
1360
+ }
1361
+
1362
+ throw new CliError(`Workspace "${normalized}" was not found.`, {
1363
+ code: 'WORKSPACE_NOT_FOUND',
1364
+ exitCode: EXIT_CODES.NOT_FOUND,
1365
+ hint: 'Run `socialseal workspace list` to discover available workspaces.',
1366
+ });
1367
+ }
1368
+
1369
+ function formatWorkspaceLine(workspace, { isEffective = false, source = null, isSuggested = false } = {}) {
1370
+ const tags = [];
1371
+ if (workspace.isPersonalWorkspace) tags.push('personal');
1372
+ if (isEffective) tags.push(source === 'config' ? 'default' : `active:${source}`);
1373
+ if (isSuggested) tags.push('suggested');
1374
+ const tagText = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
1375
+ const slugText = workspace.slug ? ` slug=${workspace.slug}` : '';
1376
+ return `- ${workspace.name} (${workspace.id}) role=${workspace.role}${slugText}${tagText}`;
1377
+ }
1378
+
440
1379
  async function handleAgentRun(opts) {
441
1380
  const config = loadConfig();
442
1381
  const apiKey = requireApiKey(opts, config);
@@ -445,12 +1384,33 @@ async function handleAgentRun(opts) {
445
1384
  const { resolvedApiBase, legacyUrl } = resolveApiTarget({ apiBase, legacyUrl: agentUrl });
446
1385
  const timeoutMs = resolveTimeoutMs(opts, config);
447
1386
  const idleTimeoutMs = resolveAgentIdleTimeoutMs(opts, config, timeoutMs);
1387
+ const continuationToken = typeof opts.continue === 'string' ? opts.continue.trim() : '';
1388
+ const { workspaceId: resolvedWorkspaceIdInput } = resolveWorkspaceSelection(opts, config);
1389
+
1390
+ if (continuationToken && opts.conversationId) {
1391
+ throw new CliError('Use either --continue or --conversation-id, not both.', {
1392
+ code: 'INVALID_ARGUMENTS',
1393
+ exitCode: EXIT_CODES.USAGE,
1394
+ });
1395
+ }
1396
+ if (continuationToken && opts.createNew) {
1397
+ throw new CliError('Use either --continue or --create-new, not both.', {
1398
+ code: 'INVALID_ARGUMENTS',
1399
+ exitCode: EXIT_CODES.USAGE,
1400
+ });
1401
+ }
1402
+ if (opts.conversationId && opts.createNew) {
1403
+ throw new CliError('Use either --conversation-id or --create-new, not both.', {
1404
+ code: 'INVALID_ARGUMENTS',
1405
+ exitCode: EXIT_CODES.USAGE,
1406
+ });
1407
+ }
448
1408
 
449
1409
  const headers = {
450
1410
  'Content-Type': 'application/json',
451
1411
  [CLI_KEY_HEADER]: apiKey,
452
1412
  };
453
- if (opts.workspaceId) headers[WORKSPACE_HEADER] = opts.workspaceId;
1413
+ if (resolvedWorkspaceIdInput) headers[WORKSPACE_HEADER] = resolvedWorkspaceIdInput;
454
1414
 
455
1415
  const sessionUrl = resolvedApiBase
456
1416
  ? `${resolvedApiBase.replace(/\/$/, '')}/cli/agent/session`
@@ -460,8 +1420,9 @@ async function handleAgentRun(opts) {
460
1420
  method: 'POST',
461
1421
  headers,
462
1422
  body: JSON.stringify({
463
- conversationId: opts.conversationId || undefined,
464
- createNew: !!opts.createNew,
1423
+ continuationToken: continuationToken || undefined,
1424
+ conversationId: continuationToken ? undefined : (opts.conversationId || undefined),
1425
+ createNew: continuationToken || opts.conversationId ? undefined : true,
465
1426
  }),
466
1427
  }, timeoutMs);
467
1428
 
@@ -472,6 +1433,8 @@ async function handleAgentRun(opts) {
472
1433
  const sessionData = await sessionRes.json();
473
1434
  const sessionId = sessionData?.data?.sessionId || null;
474
1435
  const initialConversationId = sessionData?.data?.activeConversationId || opts.conversationId || null;
1436
+ const resolvedWorkspaceId = sessionData?.data?.workspaceId || resolvedWorkspaceIdInput || null;
1437
+ const nextContinuationToken = sessionData?.data?.continuationToken || null;
475
1438
  const wsUrl = sessionData?.data?.websocketUrl;
476
1439
  if (!wsUrl) {
477
1440
  throw new CliError('Missing websocketUrl in session response.', {
@@ -483,6 +1446,19 @@ async function handleAgentRun(opts) {
483
1446
  opts,
484
1447
  `Agent session created${sessionId ? ` (session ${sessionId})` : ''}${initialConversationId ? ` for conversation ${initialConversationId}` : ''}.`,
485
1448
  );
1449
+ if (opts.json) {
1450
+ process.stdout.write(JSON.stringify({
1451
+ type: 'session_bootstrap',
1452
+ payload: {
1453
+ sessionId,
1454
+ conversationId: initialConversationId,
1455
+ workspaceId: resolvedWorkspaceId,
1456
+ continuationToken: nextContinuationToken,
1457
+ },
1458
+ }) + '\n');
1459
+ } else if (nextContinuationToken) {
1460
+ process.stderr.write(`[socialseal] Continuation token: ${nextContinuationToken}\n`);
1461
+ }
486
1462
 
487
1463
  const context = parseJsonInput(opts.context, { label: 'context', allowString: true });
488
1464
  const message = opts.message;
@@ -721,16 +1697,38 @@ async function handleToolsCall(opts) {
721
1697
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
722
1698
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
723
1699
  const timeoutMs = resolveTimeoutMs(opts, config);
1700
+ const { workspaceId: resolvedWorkspaceId, source: workspaceSource } = resolveWorkspaceSelection(opts, config);
1701
+
1702
+ const parsedPayload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
1703
+ const mergedPayload = mergeWorkspaceIdIntoPayload(parsedPayload, resolvedWorkspaceId);
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
+ });
724
1724
 
725
- const payload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
726
- const method = normalizeMethod(opts.method);
727
1725
  const res = await callApi({
728
1726
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
729
1727
  apiKey,
730
- path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
1728
+ path,
731
1729
  method,
732
- body: payload,
733
- workspaceId: opts.workspaceId,
1730
+ body: translated.body,
1731
+ workspaceId: effectiveWorkspaceId,
734
1732
  timeoutMs,
735
1733
  });
736
1734
 
@@ -745,7 +1743,45 @@ async function handleToolsCall(opts) {
745
1743
  const contentType = res.headers.get('content-type') || '';
746
1744
  if (contentType.includes('application/json')) {
747
1745
  const data = await res.json();
748
- process.stdout.write(JSON.stringify(data, null, opts.pretty ? 2 : 0) + '\n');
1746
+ const shouldPoll = shouldHandleSearchJourneyRunAsync(opts.function, method, payload, opts) && opts.poll !== false;
1747
+ if (!shouldPoll) {
1748
+ emitJsonOutput(data, opts.pretty);
1749
+ return;
1750
+ }
1751
+
1752
+ const startStatus = isJsonObject(data) && typeof data.status === 'string' ? data.status : null;
1753
+ if (startStatus === 'failed') {
1754
+ throw buildSearchJourneyRunFailure(data);
1755
+ }
1756
+ if (startStatus === 'completed') {
1757
+ emitJsonOutput(data, opts.pretty);
1758
+ return;
1759
+ }
1760
+
1761
+ const runId = isJsonObject(data) && typeof data.runId === 'string' ? data.runId : null;
1762
+ if (!runId) {
1763
+ throw new CliError('Async search-journey-run start response did not include a runId.', {
1764
+ code: 'INVALID_START_RESPONSE',
1765
+ exitCode: EXIT_CODES.SERVER,
1766
+ details: truncateDetails(data),
1767
+ });
1768
+ }
1769
+
1770
+ emitInfo(opts, `search-journey-run async run started: ${runId}`);
1771
+ const finalData = await pollSearchJourneyRun({
1772
+ apiBase: useGateway ? resolvedApiBase : legacyUrl,
1773
+ apiKey,
1774
+ path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
1775
+ workspaceId: isJsonObject(payload) && typeof payload.workspaceId === 'string'
1776
+ ? payload.workspaceId
1777
+ : resolvedWorkspaceId,
1778
+ timeoutMs,
1779
+ pollIntervalMs: resolvePollIntervalMs(opts),
1780
+ runId,
1781
+ opts,
1782
+ });
1783
+
1784
+ emitJsonOutput(finalData, opts.pretty);
749
1785
  return;
750
1786
  }
751
1787
 
@@ -757,7 +1793,7 @@ function handleToolsList(opts) {
757
1793
  const payload = {
758
1794
  discovery: 'built_in_registry',
759
1795
  tools: KNOWN_TOOLS,
760
- note: 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration.',
1796
+ note: STATIC_TOOL_REGISTRY_NOTE,
761
1797
  };
762
1798
 
763
1799
  if (opts.json) {
@@ -774,7 +1810,16 @@ function handleToolsList(opts) {
774
1810
  currentCategory = tool.category;
775
1811
  process.stdout.write(`\n${currentCategory}\n`);
776
1812
  }
777
- 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
+ }
778
1823
  }
779
1824
 
780
1825
  process.stdout.write('\n[socialseal] Call a tool with: socialseal tools call --function <name> --body @payload.json\n');
@@ -787,6 +1832,7 @@ async function handleDataExportTracking(opts) {
787
1832
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
788
1833
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
789
1834
  const timeoutMs = resolveTimeoutMs(opts, config);
1835
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
790
1836
 
791
1837
  if (!opts.groupId && !opts.itemId) {
792
1838
  throw new CliError('Provide --group-id or --item-id.', {
@@ -795,6 +1841,13 @@ async function handleDataExportTracking(opts) {
795
1841
  });
796
1842
  }
797
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
+
798
1851
  const payload = {
799
1852
  tracking_item_id: opts.itemId || undefined,
800
1853
  group_id: opts.groupId || undefined,
@@ -807,7 +1860,7 @@ async function handleDataExportTracking(opts) {
807
1860
  path: useGateway ? '/cli/tools/export_tracking_data' : '/functions/v1/export_tracking_data',
808
1861
  method: 'POST',
809
1862
  body: payload,
810
- workspaceId: opts.workspaceId,
1863
+ workspaceId: resolvedWorkspaceId,
811
1864
  timeoutMs,
812
1865
  });
813
1866
 
@@ -838,6 +1891,7 @@ async function handleDataExportReport(opts) {
838
1891
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
839
1892
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
840
1893
  const timeoutMs = resolveTimeoutMs(opts, config);
1894
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
841
1895
 
842
1896
  const payload = ensureJsonObject(parseJsonInput(opts.payload, { label: 'payload' }), 'payload');
843
1897
 
@@ -851,7 +1905,7 @@ async function handleDataExportReport(opts) {
851
1905
  format: opts.format,
852
1906
  payload,
853
1907
  },
854
- workspaceId: opts.workspaceId,
1908
+ workspaceId: resolvedWorkspaceId,
855
1909
  timeoutMs,
856
1910
  });
857
1911
 
@@ -893,6 +1947,150 @@ async function handleDataExportReport(opts) {
893
1947
  process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
894
1948
  }
895
1949
 
1950
+ async function handleWorkspaceList(opts) {
1951
+ const config = loadConfig();
1952
+ const apiKey = requireApiKey(opts, config);
1953
+ const apiBase = resolveApiBase(opts, config);
1954
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
1955
+ const timeoutMs = resolveTimeoutMs(opts, config);
1956
+ const directory = await fetchWorkspaceDirectory({
1957
+ apiBase: resolvedApiBase,
1958
+ apiKey,
1959
+ timeoutMs,
1960
+ });
1961
+ const selection = resolveWorkspaceSelection({}, config);
1962
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
1963
+ const payload = {
1964
+ ...directory,
1965
+ effectiveWorkspaceId: selection.workspaceId,
1966
+ effectiveWorkspaceSource: selection.source,
1967
+ };
1968
+
1969
+ if (opts.json) {
1970
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
1971
+ return;
1972
+ }
1973
+
1974
+ process.stdout.write('[socialseal] Available workspaces\n');
1975
+ if (workspaces.length === 0) {
1976
+ process.stdout.write('[socialseal] No accessible workspaces were returned for this key.\n');
1977
+ return;
1978
+ }
1979
+
1980
+ for (const workspace of workspaces) {
1981
+ const isEffective = selection.workspaceId === workspace.id;
1982
+ const isSuggested = !selection.workspaceId && directory.defaultWorkspaceId === workspace.id;
1983
+ process.stdout.write(`${formatWorkspaceLine(workspace, { isEffective, source: selection.source, isSuggested })}\n`);
1984
+ }
1985
+
1986
+ if (!selection.workspaceId && directory.defaultWorkspaceId) {
1987
+ process.stdout.write('\n[socialseal] No local default is configured. Set one with: socialseal workspace use <id>\n');
1988
+ }
1989
+ }
1990
+
1991
+ async function handleWorkspaceCurrent(opts) {
1992
+ const config = loadConfig();
1993
+ const apiKey = requireApiKey(opts, config);
1994
+ const apiBase = resolveApiBase(opts, config);
1995
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
1996
+ const timeoutMs = resolveTimeoutMs(opts, config);
1997
+ const directory = await fetchWorkspaceDirectory({
1998
+ apiBase: resolvedApiBase,
1999
+ apiKey,
2000
+ timeoutMs,
2001
+ });
2002
+ const selection = resolveWorkspaceSelection({}, config);
2003
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
2004
+ const effectiveWorkspace = selection.workspaceId
2005
+ ? workspaces.find((workspace) => workspace.id === selection.workspaceId) || null
2006
+ : null;
2007
+
2008
+ if (selection.workspaceId && !effectiveWorkspace) {
2009
+ throw new CliError(`Configured workspace "${selection.workspaceId}" is not accessible with this CLI key.`, {
2010
+ code: 'WORKSPACE_NOT_ACCESSIBLE',
2011
+ exitCode: EXIT_CODES.NOT_FOUND,
2012
+ hint: 'Run `socialseal workspace list` to pick a valid workspace or `socialseal workspace clear` to unset the default.',
2013
+ });
2014
+ }
2015
+
2016
+ const payload = {
2017
+ effectiveWorkspaceId: selection.workspaceId,
2018
+ effectiveWorkspaceSource: selection.source,
2019
+ workspace: effectiveWorkspace,
2020
+ defaultWorkspaceId: directory.defaultWorkspaceId || null,
2021
+ personalWorkspaceId: directory.personalWorkspaceId || null,
2022
+ };
2023
+ if (opts.json) {
2024
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
2025
+ return;
2026
+ }
2027
+
2028
+ if (effectiveWorkspace) {
2029
+ process.stdout.write(`[socialseal] Effective workspace: ${effectiveWorkspace.name} (${effectiveWorkspace.id}) via ${selection.source}\n`);
2030
+ return;
2031
+ }
2032
+
2033
+ if (directory.defaultWorkspaceId) {
2034
+ const suggestedWorkspace = workspaces.find((workspace) => workspace.id === directory.defaultWorkspaceId) || null;
2035
+ if (suggestedWorkspace) {
2036
+ process.stdout.write(`[socialseal] No local default workspace is configured. Suggested workspace: ${suggestedWorkspace.name} (${suggestedWorkspace.id})\n`);
2037
+ return;
2038
+ }
2039
+ }
2040
+
2041
+ process.stdout.write('[socialseal] No default workspace is configured and no accessible workspace suggestion is available.\n');
2042
+ }
2043
+
2044
+ async function handleWorkspaceUse(opts) {
2045
+ const config = loadConfig();
2046
+ const apiKey = requireApiKey(opts, config);
2047
+ const apiBase = resolveApiBase(opts, config);
2048
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
2049
+ const timeoutMs = resolveTimeoutMs(opts, config);
2050
+ const directory = await fetchWorkspaceDirectory({
2051
+ apiBase: resolvedApiBase,
2052
+ apiKey,
2053
+ timeoutMs,
2054
+ });
2055
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
2056
+ const workspace = matchWorkspaceIdentifier(workspaces, opts.identifier);
2057
+ saveConfig({
2058
+ ...config,
2059
+ workspaceId: workspace.id,
2060
+ });
2061
+
2062
+ const payload = {
2063
+ success: true,
2064
+ workspaceId: workspace.id,
2065
+ workspace,
2066
+ configPath: getConfigPath(),
2067
+ };
2068
+ if (opts.json) {
2069
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
2070
+ return;
2071
+ }
2072
+
2073
+ process.stdout.write(`[socialseal] Default workspace set to ${workspace.name} (${workspace.id})\n`);
2074
+ }
2075
+
2076
+ function handleWorkspaceClear(opts) {
2077
+ const config = loadConfig();
2078
+ const nextConfig = { ...config };
2079
+ delete nextConfig.workspaceId;
2080
+ saveConfig(nextConfig);
2081
+
2082
+ const payload = {
2083
+ success: true,
2084
+ configPath: getConfigPath(),
2085
+ };
2086
+ if (opts.json) {
2087
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
2088
+ return;
2089
+ }
2090
+
2091
+ process.stdout.write('[socialseal] Default workspace cleared.\n');
2092
+ }
2093
+
896
2094
  const program = new Command();
897
2095
  program
898
2096
  .name('socialseal')
@@ -905,7 +2103,7 @@ if (typeof program.showHelpAfterError === 'function') {
905
2103
  if (typeof program.showSuggestionAfterError === 'function') {
906
2104
  program.showSuggestionAfterError(true);
907
2105
  }
908
- program.addHelpText('after', `\nExamples:\n socialseal agent run --message \"ping\"\n socialseal tools list\n socialseal tools call --function <tool> --body @payload.json\n socialseal data export-tracking --group-id 123 --time-period 30d\n`);
2106
+ program.addHelpText('after', `\nExamples:\n socialseal workspace list\n socialseal workspace use <workspace-id>\n socialseal agent run --message \"ping\"\n socialseal tools list\n socialseal tools call --function <tool> --body @payload.json\n socialseal tools call --function search-journey-run --body @payload.json --async --workspace-id <uuid>\n socialseal data export-tracking --group-id 123 --time-period 30d\n`);
909
2107
 
910
2108
  program
911
2109
  .command('agent')
@@ -916,6 +2114,7 @@ program
916
2114
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
917
2115
  .option('--api-key <key>', 'CLI API key')
918
2116
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
2117
+ .option('--continue <token>', 'Continuation token from a previous agent run')
919
2118
  .option('--conversation-id <id>', 'Conversation id to resume')
920
2119
  .option('--create-new', 'Create a new conversation')
921
2120
  .option('--json', 'Emit NDJSON events')
@@ -924,6 +2123,49 @@ program
924
2123
  .option('--verbose', 'Show error details')
925
2124
  .action((opts) => runCommand(handleAgentRun, opts));
926
2125
 
2126
+ const workspace = program.command('workspace').description('Discover and manage the default workspace');
2127
+
2128
+ workspace
2129
+ .command('list')
2130
+ .description('List accessible workspaces for this CLI key')
2131
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
2132
+ .option('--api-key <key>', 'CLI API key')
2133
+ .option('--json', 'Emit machine-readable output')
2134
+ .option('--pretty', 'Pretty-print JSON')
2135
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
2136
+ .option('--verbose', 'Show error details')
2137
+ .action((opts) => runCommand(handleWorkspaceList, opts));
2138
+
2139
+ workspace
2140
+ .command('current')
2141
+ .description('Show the effective default workspace')
2142
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
2143
+ .option('--api-key <key>', 'CLI API key')
2144
+ .option('--json', 'Emit machine-readable output')
2145
+ .option('--pretty', 'Pretty-print JSON')
2146
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
2147
+ .option('--verbose', 'Show error details')
2148
+ .action((opts) => runCommand(handleWorkspaceCurrent, opts));
2149
+
2150
+ workspace
2151
+ .command('use <identifier>')
2152
+ .description('Persist a default workspace by id, slug, or exact name')
2153
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
2154
+ .option('--api-key <key>', 'CLI API key')
2155
+ .option('--json', 'Emit machine-readable output')
2156
+ .option('--pretty', 'Pretty-print JSON')
2157
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
2158
+ .option('--verbose', 'Show error details')
2159
+ .action((identifier, opts) => runCommand(handleWorkspaceUse, { ...opts, identifier }));
2160
+
2161
+ workspace
2162
+ .command('clear')
2163
+ .description('Clear the locally configured default workspace')
2164
+ .option('--json', 'Emit machine-readable output')
2165
+ .option('--pretty', 'Pretty-print JSON')
2166
+ .option('--verbose', 'Show error details')
2167
+ .action((opts) => runCommand(handleWorkspaceClear, opts));
2168
+
927
2169
  const tools = program.command('tools').description('Call edge functions directly (tool backends)');
928
2170
 
929
2171
  tools
@@ -939,6 +2181,9 @@ tools
939
2181
  .requiredOption('--function <name>', 'Tool name (see official docs)')
940
2182
  .option('--method <method>', 'HTTP method', 'POST')
941
2183
  .option('--body <jsonOrFile>', 'JSON body or @file.json')
2184
+ .option('--async', 'Request async execution for supported tool backends')
2185
+ .option('--no-poll', 'Return immediately after async start instead of polling to completion')
2186
+ .option('--poll-interval <ms>', 'Polling interval in milliseconds for supported async tool calls')
942
2187
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
943
2188
  .option('--api-key <key>', 'CLI API key')
944
2189
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')