@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/CHANGELOG.md +8 -0
- package/README.md +27 -0
- package/package.json +1 -1
- package/src/index.js +1266 -21
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
|
-
{
|
|
32
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
42
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
464
|
-
|
|
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
|
|
1728
|
+
path,
|
|
731
1729
|
method,
|
|
732
|
-
body:
|
|
733
|
-
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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)')
|