@socialseal/cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.2 - 2026-03-18
6
+ - Add `search-journey-run` async CLI ergonomics: `--async` starts the backend async mode, polling is on by default, and `--no-poll` returns the initial `runId` immediately.
7
+ - Add `--poll-interval <ms>` for async `search-journey-run` status polling.
8
+ - Treat terminal async `search-journey-run` failures as non-zero CLI exits instead of silent `200` JSON output.
9
+
5
10
  ## 0.1.1 - 2026-03-13
6
11
  - Document public base URL and CLI error output.
7
12
  - Add request timeouts, verbose error output, and OSS-safe tool discovery behavior.
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  Environment variables:
10
10
  - `SOCIALSEAL_API_KEY`
11
11
  - `SOCIALSEAL_API_BASE` (default `https://api.socialseal.co`)
12
+ - `SOCIALSEAL_WORKSPACE_ID` (optional workspace override; takes precedence over config)
12
13
  - `SOCIALSEAL_TIMEOUT_MS` (optional request timeout override)
13
14
  - `SOCIALSEAL_AGENT_IDLE_TIMEOUT_MS` (optional agent WebSocket inactivity timeout override; default 300000)
14
15
 
@@ -19,14 +20,22 @@ Optional config file:
19
20
  {
20
21
  "apiKey": "ss_cli_...",
21
22
  "apiBase": "https://api.socialseal.co",
23
+ "workspaceId": "00000000-0000-0000-0000-000000000000",
22
24
  "timeoutMs": 30000,
23
25
  "agentIdleTimeoutMs": 300000
24
26
  }
25
27
  ```
26
28
 
27
29
  ## Commands
30
+ - Workspace discovery/defaults:
31
+ - `socialseal workspace list`
32
+ - `socialseal workspace current`
33
+ - `socialseal workspace use <workspace-id|slug|exact-name>`
34
+ - `socialseal workspace clear`
35
+
28
36
  - Agent (non-interactive, streaming):
29
37
  - `socialseal agent run --message "..." --api-base https://api.socialseal.co --api-key <key> [--workspace-id <uuid>]`
38
+ - `socialseal agent run --message "..." --continue <token>`
30
39
  - `socialseal agent run --message "..." --timeout 60000`
31
40
  - `socialseal agent run --message "..." --idle-timeout 300000 --verbose`
32
41
 
@@ -37,6 +46,8 @@ Optional config file:
37
46
  - Tools (direct edge function call):
38
47
  - `socialseal tools call --function <tool> --body @payload.json --api-base https://api.socialseal.co --api-key <key>`
39
48
  - `socialseal tools call --function <tool> --body @payload.json --json`
49
+ - `socialseal tools call --function search-journey-run --body @payload.json --async --workspace-id <uuid>`
50
+ - `socialseal tools call --function search-journey-run --body @payload.json --async --no-poll --workspace-id <uuid>`
40
51
 
41
52
  - Data exports (provisional):
42
53
  - `socialseal data export-tracking --group-id 123 --time-period 30d --out out.csv`
@@ -46,12 +57,28 @@ Optional config file:
46
57
  - `export-report` and `export_tracking_data` are provisional until CLI export specs are finalized.
47
58
  - `tools list` ships a stable built-in registry of supported direct-call function targets. It is not live backend enumeration.
48
59
  - `--timeout <ms>` controls HTTP request timeouts. Agent runs default to a 5-minute WebSocket inactivity timeout unless you set `--idle-timeout <ms>` (or the matching env/config value).
60
+ - `search-journey-run` supports CLI-managed async polling: `--async` starts backend async mode, polling is on by default, `--no-poll` returns the initial `runId`, and `--poll-interval <ms>` controls the status polling cadence.
61
+ - `socialseal agent run` now defaults to a fresh conversation. The CLI prints a continuation token to `stderr`; pass it back with `--continue <token>` to resume the same agent conversation explicitly.
62
+ - Effective workspace precedence is: `--workspace-id` → `SOCIALSEAL_WORKSPACE_ID` → config `workspaceId` → backend personal-workspace fallback.
63
+ - `socialseal workspace use ...` writes a local default workspace into `~/.config/socialseal/config.json`, which the CLI reuses for `agent`, `tools`, and `data` commands.
64
+ - `socialseal workspace list` discovers the workspaces accessible to the current CLI key and marks the active/suggested default.
65
+ - If a scoped CLI key cannot safely infer a workspace, `agent run` now fails closed and tells you to set `--workspace-id` or configure a local default first.
49
66
 
50
67
  ## Errors and exit codes
51
68
  - Exit codes: `2` (usage), `3` (auth), `4` (not found), `5` (server), `1` (unknown)
52
69
  - Add `--json` to `tools call` or `data` commands to emit machine-readable errors.
53
70
  - Add `--verbose` to print error details plus agent session/tool progress diagnostics.
54
71
 
72
+ ## Troubleshooting
73
+ - `SUPABASE_ANON_KEY not configured`
74
+ - This comes from the CLI gateway, not the local CLI install.
75
+ - The deployed gateway is missing its `SUPABASE_ANON_KEY` secret, so `/cli/tools/*` cannot proxy to Supabase Edge Functions.
76
+ - Fix on the server side with `wrangler secret put SUPABASE_ANON_KEY --env <staging|production>` for the gateway Worker, then re-run a `socialseal tools call ...` smoke test.
77
+ - `AI_UNSUPPORTED_LOCATION` or `The live agent is unavailable in this region right now.`
78
+ - This is raised when the upstream Gemini API rejects the worker egress location.
79
+ - The agent worker currently uses Google Gemini directly from Cloudflare Workers; there is no SocialSeal-side region allowlist in the CLI.
80
+ - If this reproduces from a supported Google AI region, treat it as an infrastructure/runtime issue. Practical workarounds are to run the agent from a worker placement/egress region that Google accepts, or switch the agent runtime to Vertex AI for server-side calls.
81
+
55
82
  ## Smoke Test (manual)
56
83
  1. `SOCIALSEAL_API_KEY=... socialseal agent run --message "ping"`
57
84
  2. `SOCIALSEAL_API_KEY=... socialseal tools call --function <tool> --body @payload.json`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ 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';
17
18
  const EXIT_CODES = {
@@ -58,8 +59,12 @@ const KNOWN_TOOLS = [
58
59
  { name: 'vnext-topics-auto-tag', category: 'vnext', description: 'Auto-tag keyword and topic assignments with Gemini-assisted review.' },
59
60
  ];
60
61
 
62
+ function getConfigPath() {
63
+ return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
64
+ }
65
+
61
66
  function loadConfig() {
62
- const configPath = process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
67
+ const configPath = getConfigPath();
63
68
  try {
64
69
  if (!fs.existsSync(configPath)) return {};
65
70
  const raw = fs.readFileSync(configPath, 'utf8');
@@ -70,6 +75,15 @@ function loadConfig() {
70
75
  }
71
76
  }
72
77
 
78
+ function saveConfig(config) {
79
+ const configPath = getConfigPath();
80
+ const normalizedConfig = Object.fromEntries(
81
+ Object.entries(config || {}).filter(([, value]) => value !== undefined),
82
+ );
83
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
84
+ fs.writeFileSync(configPath, `${JSON.stringify(normalizedConfig, null, 2)}\n`);
85
+ }
86
+
73
87
  function resolveApiKey(opts, config) {
74
88
  return opts.apiKey || process.env.SOCIALSEAL_API_KEY || config.apiKey;
75
89
  }
@@ -86,6 +100,19 @@ function resolveSupabaseUrl(opts, config) {
86
100
  return opts.supabaseUrl || process.env.SOCIALSEAL_SUPABASE_URL || config.supabaseUrl;
87
101
  }
88
102
 
103
+ function resolveWorkspaceSelection(opts, config) {
104
+ if (typeof opts.workspaceId === 'string' && opts.workspaceId.trim().length > 0) {
105
+ return { workspaceId: opts.workspaceId.trim(), source: 'flag' };
106
+ }
107
+ if (typeof process.env.SOCIALSEAL_WORKSPACE_ID === 'string' && process.env.SOCIALSEAL_WORKSPACE_ID.trim().length > 0) {
108
+ return { workspaceId: process.env.SOCIALSEAL_WORKSPACE_ID.trim(), source: 'env' };
109
+ }
110
+ if (typeof config.workspaceId === 'string' && config.workspaceId.trim().length > 0) {
111
+ return { workspaceId: config.workspaceId.trim(), source: 'config' };
112
+ }
113
+ return { workspaceId: null, source: null };
114
+ }
115
+
89
116
  class CliError extends Error {
90
117
  constructor(message, { code = 'CLI_ERROR', exitCode = EXIT_CODES.UNKNOWN, status, hint, details } = {}) {
91
118
  super(message);
@@ -252,6 +279,150 @@ function ensureJsonObject(value, label) {
252
279
  return value;
253
280
  }
254
281
 
282
+ function mergeWorkspaceIdIntoPayload(payload, workspaceId) {
283
+ if (!workspaceId) return payload;
284
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
285
+ return payload;
286
+ }
287
+ if (typeof payload.workspaceId === 'string' && payload.workspaceId.trim().length > 0) {
288
+ return payload;
289
+ }
290
+ return { ...payload, workspaceId };
291
+ }
292
+
293
+ function isJsonObject(value) {
294
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
295
+ }
296
+
297
+ function sleep(ms) {
298
+ return new Promise((resolve) => setTimeout(resolve, ms));
299
+ }
300
+
301
+ function resolvePollIntervalMs(opts) {
302
+ const raw = opts.pollInterval ?? process.env.SOCIALSEAL_POLL_INTERVAL_MS;
303
+ return parseTimeoutMs(raw, { defaultValue: DEFAULT_POLL_INTERVAL_MS, label: 'poll interval' });
304
+ }
305
+
306
+ function shouldHandleSearchJourneyRunAsync(functionName, method, payload, opts) {
307
+ if (String(functionName || '').trim() !== 'search-journey-run') return false;
308
+ if (method !== 'POST') return false;
309
+ if (!isJsonObject(payload)) return false;
310
+ if (payload.action === 'status') return false;
311
+ return opts.async === true || payload.executionMode === 'async';
312
+ }
313
+
314
+ function applySearchJourneyRunAsyncStart(payload, opts) {
315
+ if (!shouldHandleSearchJourneyRunAsync(opts.function, normalizeMethod(opts.method), payload, opts)) {
316
+ return payload;
317
+ }
318
+ return {
319
+ ...payload,
320
+ executionMode: 'async',
321
+ };
322
+ }
323
+
324
+ function formatJsonOutput(value, pretty) {
325
+ return JSON.stringify(value, null, pretty ? 2 : 0);
326
+ }
327
+
328
+ function emitJsonOutput(value, pretty) {
329
+ process.stdout.write(formatJsonOutput(value, pretty) + '\n');
330
+ }
331
+
332
+ function buildSearchJourneyRunFailure(data) {
333
+ const message = isJsonObject(data) && typeof data.error === 'string' && data.error.trim().length > 0
334
+ ? data.error
335
+ : 'search-journey-run failed';
336
+ return new CliError(message, {
337
+ code: 'ASYNC_RUN_FAILED',
338
+ exitCode: EXIT_CODES.SERVER,
339
+ details: truncateDetails(data),
340
+ });
341
+ }
342
+
343
+ async function pollSearchJourneyRun({
344
+ apiBase,
345
+ apiKey,
346
+ path,
347
+ workspaceId,
348
+ timeoutMs,
349
+ pollIntervalMs,
350
+ runId,
351
+ opts,
352
+ }) {
353
+ if (!workspaceId) {
354
+ throw new CliError('Async search-journey-run polling requires a workspace id.', {
355
+ code: 'WORKSPACE_REQUIRED',
356
+ exitCode: EXIT_CODES.USAGE,
357
+ hint: 'Pass --workspace-id, set SOCIALSEAL_WORKSPACE_ID, or configure a default workspace.',
358
+ });
359
+ }
360
+
361
+ const deadline = Date.now() + timeoutMs;
362
+ let lastStatus = null;
363
+
364
+ for (;;) {
365
+ const remainingMs = deadline - Date.now();
366
+ if (remainingMs <= 0) {
367
+ throw new CliError('Timed out waiting for search-journey-run async completion.', {
368
+ code: 'ASYNC_WAIT_TIMEOUT',
369
+ exitCode: EXIT_CODES.SERVER,
370
+ hint: 'Increase --timeout <ms> or use --no-poll to return the run id immediately.',
371
+ details: truncateDetails({ runId, workspaceId, lastStatus }),
372
+ });
373
+ }
374
+
375
+ await sleep(Math.min(pollIntervalMs, remainingMs));
376
+
377
+ const res = await callApi({
378
+ apiBase,
379
+ apiKey,
380
+ path,
381
+ method: 'POST',
382
+ body: {
383
+ action: 'status',
384
+ workspaceId,
385
+ runId,
386
+ },
387
+ workspaceId,
388
+ timeoutMs: remainingMs,
389
+ });
390
+
391
+ if (!res.ok) {
392
+ throw await buildHttpError(res, {
393
+ label: 'Tool status poll',
394
+ functionName: 'search-journey-run',
395
+ method: 'POST',
396
+ });
397
+ }
398
+
399
+ const contentType = res.headers.get('content-type') || '';
400
+ if (!contentType.includes('application/json')) {
401
+ throw new CliError('search-journey-run status poll returned a non-JSON response.', {
402
+ code: 'INVALID_STATUS_RESPONSE',
403
+ exitCode: EXIT_CODES.SERVER,
404
+ });
405
+ }
406
+
407
+ const data = await res.json();
408
+ const status = isJsonObject(data) && typeof data.status === 'string' ? data.status : null;
409
+ if (status && status !== lastStatus) {
410
+ emitInfo(opts, `search-journey-run status: ${status}`);
411
+ lastStatus = status;
412
+ }
413
+
414
+ if (status === 'completed') return data;
415
+ if (status === 'failed') throw buildSearchJourneyRunFailure(data);
416
+ if (status === 'pending' || status === 'processing') continue;
417
+
418
+ throw new CliError('search-journey-run status poll returned an unexpected payload.', {
419
+ code: 'INVALID_STATUS_RESPONSE',
420
+ exitCode: EXIT_CODES.SERVER,
421
+ details: truncateDetails(data),
422
+ });
423
+ }
424
+ }
425
+
255
426
  function mapStatusToExitCode(status) {
256
427
  if (status === 401 || status === 403) return EXIT_CODES.AUTH;
257
428
  if (status === 404) return EXIT_CODES.NOT_FOUND;
@@ -437,6 +608,68 @@ async function callApi({ apiBase, apiKey, path, method = 'POST', body, workspace
437
608
  return res;
438
609
  }
439
610
 
611
+ async function fetchWorkspaceDirectory({ apiBase, apiKey, timeoutMs }) {
612
+ const res = await callApi({
613
+ apiBase,
614
+ apiKey,
615
+ path: '/cli/workspaces',
616
+ method: 'GET',
617
+ timeoutMs,
618
+ });
619
+ if (!res.ok) {
620
+ throw await buildHttpError(res, { label: 'Workspace discovery' });
621
+ }
622
+ const payload = await res.json();
623
+ return payload?.data || {};
624
+ }
625
+
626
+ function matchWorkspaceIdentifier(workspaces, identifier) {
627
+ const normalized = String(identifier || '').trim();
628
+ if (!normalized) {
629
+ throw new CliError('Missing workspace identifier.', {
630
+ code: 'MISSING_ARGUMENT',
631
+ exitCode: EXIT_CODES.USAGE,
632
+ hint: 'Use a workspace id, slug, or exact name from `socialseal workspace list`.',
633
+ });
634
+ }
635
+
636
+ const exactId = workspaces.find((workspace) => workspace.id === normalized);
637
+ if (exactId) return exactId;
638
+
639
+ const exactSlug = workspaces.find((workspace) => workspace.slug === normalized);
640
+ if (exactSlug) return exactSlug;
641
+
642
+ const exactNameMatches = workspaces.filter(
643
+ (workspace) => typeof workspace.name === 'string' && workspace.name.trim().toLowerCase() === normalized.toLowerCase(),
644
+ );
645
+ if (exactNameMatches.length === 1) {
646
+ return exactNameMatches[0];
647
+ }
648
+ if (exactNameMatches.length > 1) {
649
+ throw new CliError(`Workspace name "${normalized}" is ambiguous.`, {
650
+ code: 'AMBIGUOUS_WORKSPACE',
651
+ exitCode: EXIT_CODES.USAGE,
652
+ hint: 'Use the workspace id or slug from `socialseal workspace list`.',
653
+ });
654
+ }
655
+
656
+ throw new CliError(`Workspace "${normalized}" was not found.`, {
657
+ code: 'WORKSPACE_NOT_FOUND',
658
+ exitCode: EXIT_CODES.NOT_FOUND,
659
+ hint: 'Run `socialseal workspace list` to discover available workspaces.',
660
+ });
661
+ }
662
+
663
+ function formatWorkspaceLine(workspace, { isEffective = false, source = null, isSuggested = false } = {}) {
664
+ const tags = [];
665
+ if (workspace.isPersonalWorkspace) tags.push('personal');
666
+ if (isEffective) tags.push(source === 'config' ? 'default' : `active:${source}`);
667
+ if (isSuggested) tags.push('suggested');
668
+ const tagText = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
669
+ const slugText = workspace.slug ? ` slug=${workspace.slug}` : '';
670
+ return `- ${workspace.name} (${workspace.id}) role=${workspace.role}${slugText}${tagText}`;
671
+ }
672
+
440
673
  async function handleAgentRun(opts) {
441
674
  const config = loadConfig();
442
675
  const apiKey = requireApiKey(opts, config);
@@ -445,12 +678,33 @@ async function handleAgentRun(opts) {
445
678
  const { resolvedApiBase, legacyUrl } = resolveApiTarget({ apiBase, legacyUrl: agentUrl });
446
679
  const timeoutMs = resolveTimeoutMs(opts, config);
447
680
  const idleTimeoutMs = resolveAgentIdleTimeoutMs(opts, config, timeoutMs);
681
+ const continuationToken = typeof opts.continue === 'string' ? opts.continue.trim() : '';
682
+ const { workspaceId: resolvedWorkspaceIdInput } = resolveWorkspaceSelection(opts, config);
683
+
684
+ if (continuationToken && opts.conversationId) {
685
+ throw new CliError('Use either --continue or --conversation-id, not both.', {
686
+ code: 'INVALID_ARGUMENTS',
687
+ exitCode: EXIT_CODES.USAGE,
688
+ });
689
+ }
690
+ if (continuationToken && opts.createNew) {
691
+ throw new CliError('Use either --continue or --create-new, not both.', {
692
+ code: 'INVALID_ARGUMENTS',
693
+ exitCode: EXIT_CODES.USAGE,
694
+ });
695
+ }
696
+ if (opts.conversationId && opts.createNew) {
697
+ throw new CliError('Use either --conversation-id or --create-new, not both.', {
698
+ code: 'INVALID_ARGUMENTS',
699
+ exitCode: EXIT_CODES.USAGE,
700
+ });
701
+ }
448
702
 
449
703
  const headers = {
450
704
  'Content-Type': 'application/json',
451
705
  [CLI_KEY_HEADER]: apiKey,
452
706
  };
453
- if (opts.workspaceId) headers[WORKSPACE_HEADER] = opts.workspaceId;
707
+ if (resolvedWorkspaceIdInput) headers[WORKSPACE_HEADER] = resolvedWorkspaceIdInput;
454
708
 
455
709
  const sessionUrl = resolvedApiBase
456
710
  ? `${resolvedApiBase.replace(/\/$/, '')}/cli/agent/session`
@@ -460,8 +714,9 @@ async function handleAgentRun(opts) {
460
714
  method: 'POST',
461
715
  headers,
462
716
  body: JSON.stringify({
463
- conversationId: opts.conversationId || undefined,
464
- createNew: !!opts.createNew,
717
+ continuationToken: continuationToken || undefined,
718
+ conversationId: continuationToken ? undefined : (opts.conversationId || undefined),
719
+ createNew: continuationToken || opts.conversationId ? undefined : true,
465
720
  }),
466
721
  }, timeoutMs);
467
722
 
@@ -472,6 +727,8 @@ async function handleAgentRun(opts) {
472
727
  const sessionData = await sessionRes.json();
473
728
  const sessionId = sessionData?.data?.sessionId || null;
474
729
  const initialConversationId = sessionData?.data?.activeConversationId || opts.conversationId || null;
730
+ const resolvedWorkspaceId = sessionData?.data?.workspaceId || resolvedWorkspaceIdInput || null;
731
+ const nextContinuationToken = sessionData?.data?.continuationToken || null;
475
732
  const wsUrl = sessionData?.data?.websocketUrl;
476
733
  if (!wsUrl) {
477
734
  throw new CliError('Missing websocketUrl in session response.', {
@@ -483,6 +740,19 @@ async function handleAgentRun(opts) {
483
740
  opts,
484
741
  `Agent session created${sessionId ? ` (session ${sessionId})` : ''}${initialConversationId ? ` for conversation ${initialConversationId}` : ''}.`,
485
742
  );
743
+ if (opts.json) {
744
+ process.stdout.write(JSON.stringify({
745
+ type: 'session_bootstrap',
746
+ payload: {
747
+ sessionId,
748
+ conversationId: initialConversationId,
749
+ workspaceId: resolvedWorkspaceId,
750
+ continuationToken: nextContinuationToken,
751
+ },
752
+ }) + '\n');
753
+ } else if (nextContinuationToken) {
754
+ process.stderr.write(`[socialseal] Continuation token: ${nextContinuationToken}\n`);
755
+ }
486
756
 
487
757
  const context = parseJsonInput(opts.context, { label: 'context', allowString: true });
488
758
  const message = opts.message;
@@ -721,16 +991,19 @@ async function handleToolsCall(opts) {
721
991
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
722
992
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
723
993
  const timeoutMs = resolveTimeoutMs(opts, config);
994
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
724
995
 
725
- const payload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
996
+ const parsedPayload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
997
+ const mergedPayload = mergeWorkspaceIdIntoPayload(parsedPayload, resolvedWorkspaceId);
726
998
  const method = normalizeMethod(opts.method);
999
+ const payload = applySearchJourneyRunAsyncStart(mergedPayload, { ...opts, method });
727
1000
  const res = await callApi({
728
1001
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
729
1002
  apiKey,
730
1003
  path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
731
1004
  method,
732
1005
  body: payload,
733
- workspaceId: opts.workspaceId,
1006
+ workspaceId: resolvedWorkspaceId,
734
1007
  timeoutMs,
735
1008
  });
736
1009
 
@@ -745,7 +1018,45 @@ async function handleToolsCall(opts) {
745
1018
  const contentType = res.headers.get('content-type') || '';
746
1019
  if (contentType.includes('application/json')) {
747
1020
  const data = await res.json();
748
- process.stdout.write(JSON.stringify(data, null, opts.pretty ? 2 : 0) + '\n');
1021
+ const shouldPoll = shouldHandleSearchJourneyRunAsync(opts.function, method, payload, opts) && opts.poll !== false;
1022
+ if (!shouldPoll) {
1023
+ emitJsonOutput(data, opts.pretty);
1024
+ return;
1025
+ }
1026
+
1027
+ const startStatus = isJsonObject(data) && typeof data.status === 'string' ? data.status : null;
1028
+ if (startStatus === 'failed') {
1029
+ throw buildSearchJourneyRunFailure(data);
1030
+ }
1031
+ if (startStatus === 'completed') {
1032
+ emitJsonOutput(data, opts.pretty);
1033
+ return;
1034
+ }
1035
+
1036
+ const runId = isJsonObject(data) && typeof data.runId === 'string' ? data.runId : null;
1037
+ if (!runId) {
1038
+ throw new CliError('Async search-journey-run start response did not include a runId.', {
1039
+ code: 'INVALID_START_RESPONSE',
1040
+ exitCode: EXIT_CODES.SERVER,
1041
+ details: truncateDetails(data),
1042
+ });
1043
+ }
1044
+
1045
+ emitInfo(opts, `search-journey-run async run started: ${runId}`);
1046
+ const finalData = await pollSearchJourneyRun({
1047
+ apiBase: useGateway ? resolvedApiBase : legacyUrl,
1048
+ apiKey,
1049
+ path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
1050
+ workspaceId: isJsonObject(payload) && typeof payload.workspaceId === 'string'
1051
+ ? payload.workspaceId
1052
+ : resolvedWorkspaceId,
1053
+ timeoutMs,
1054
+ pollIntervalMs: resolvePollIntervalMs(opts),
1055
+ runId,
1056
+ opts,
1057
+ });
1058
+
1059
+ emitJsonOutput(finalData, opts.pretty);
749
1060
  return;
750
1061
  }
751
1062
 
@@ -787,6 +1098,7 @@ async function handleDataExportTracking(opts) {
787
1098
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
788
1099
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
789
1100
  const timeoutMs = resolveTimeoutMs(opts, config);
1101
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
790
1102
 
791
1103
  if (!opts.groupId && !opts.itemId) {
792
1104
  throw new CliError('Provide --group-id or --item-id.', {
@@ -807,7 +1119,7 @@ async function handleDataExportTracking(opts) {
807
1119
  path: useGateway ? '/cli/tools/export_tracking_data' : '/functions/v1/export_tracking_data',
808
1120
  method: 'POST',
809
1121
  body: payload,
810
- workspaceId: opts.workspaceId,
1122
+ workspaceId: resolvedWorkspaceId,
811
1123
  timeoutMs,
812
1124
  });
813
1125
 
@@ -838,6 +1150,7 @@ async function handleDataExportReport(opts) {
838
1150
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
839
1151
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
840
1152
  const timeoutMs = resolveTimeoutMs(opts, config);
1153
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
841
1154
 
842
1155
  const payload = ensureJsonObject(parseJsonInput(opts.payload, { label: 'payload' }), 'payload');
843
1156
 
@@ -851,7 +1164,7 @@ async function handleDataExportReport(opts) {
851
1164
  format: opts.format,
852
1165
  payload,
853
1166
  },
854
- workspaceId: opts.workspaceId,
1167
+ workspaceId: resolvedWorkspaceId,
855
1168
  timeoutMs,
856
1169
  });
857
1170
 
@@ -893,6 +1206,150 @@ async function handleDataExportReport(opts) {
893
1206
  process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
894
1207
  }
895
1208
 
1209
+ async function handleWorkspaceList(opts) {
1210
+ const config = loadConfig();
1211
+ const apiKey = requireApiKey(opts, config);
1212
+ const apiBase = resolveApiBase(opts, config);
1213
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
1214
+ const timeoutMs = resolveTimeoutMs(opts, config);
1215
+ const directory = await fetchWorkspaceDirectory({
1216
+ apiBase: resolvedApiBase,
1217
+ apiKey,
1218
+ timeoutMs,
1219
+ });
1220
+ const selection = resolveWorkspaceSelection({}, config);
1221
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
1222
+ const payload = {
1223
+ ...directory,
1224
+ effectiveWorkspaceId: selection.workspaceId,
1225
+ effectiveWorkspaceSource: selection.source,
1226
+ };
1227
+
1228
+ if (opts.json) {
1229
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
1230
+ return;
1231
+ }
1232
+
1233
+ process.stdout.write('[socialseal] Available workspaces\n');
1234
+ if (workspaces.length === 0) {
1235
+ process.stdout.write('[socialseal] No accessible workspaces were returned for this key.\n');
1236
+ return;
1237
+ }
1238
+
1239
+ for (const workspace of workspaces) {
1240
+ const isEffective = selection.workspaceId === workspace.id;
1241
+ const isSuggested = !selection.workspaceId && directory.defaultWorkspaceId === workspace.id;
1242
+ process.stdout.write(`${formatWorkspaceLine(workspace, { isEffective, source: selection.source, isSuggested })}\n`);
1243
+ }
1244
+
1245
+ if (!selection.workspaceId && directory.defaultWorkspaceId) {
1246
+ process.stdout.write('\n[socialseal] No local default is configured. Set one with: socialseal workspace use <id>\n');
1247
+ }
1248
+ }
1249
+
1250
+ async function handleWorkspaceCurrent(opts) {
1251
+ const config = loadConfig();
1252
+ const apiKey = requireApiKey(opts, config);
1253
+ const apiBase = resolveApiBase(opts, config);
1254
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
1255
+ const timeoutMs = resolveTimeoutMs(opts, config);
1256
+ const directory = await fetchWorkspaceDirectory({
1257
+ apiBase: resolvedApiBase,
1258
+ apiKey,
1259
+ timeoutMs,
1260
+ });
1261
+ const selection = resolveWorkspaceSelection({}, config);
1262
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
1263
+ const effectiveWorkspace = selection.workspaceId
1264
+ ? workspaces.find((workspace) => workspace.id === selection.workspaceId) || null
1265
+ : null;
1266
+
1267
+ if (selection.workspaceId && !effectiveWorkspace) {
1268
+ throw new CliError(`Configured workspace "${selection.workspaceId}" is not accessible with this CLI key.`, {
1269
+ code: 'WORKSPACE_NOT_ACCESSIBLE',
1270
+ exitCode: EXIT_CODES.NOT_FOUND,
1271
+ hint: 'Run `socialseal workspace list` to pick a valid workspace or `socialseal workspace clear` to unset the default.',
1272
+ });
1273
+ }
1274
+
1275
+ const payload = {
1276
+ effectiveWorkspaceId: selection.workspaceId,
1277
+ effectiveWorkspaceSource: selection.source,
1278
+ workspace: effectiveWorkspace,
1279
+ defaultWorkspaceId: directory.defaultWorkspaceId || null,
1280
+ personalWorkspaceId: directory.personalWorkspaceId || null,
1281
+ };
1282
+ if (opts.json) {
1283
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
1284
+ return;
1285
+ }
1286
+
1287
+ if (effectiveWorkspace) {
1288
+ process.stdout.write(`[socialseal] Effective workspace: ${effectiveWorkspace.name} (${effectiveWorkspace.id}) via ${selection.source}\n`);
1289
+ return;
1290
+ }
1291
+
1292
+ if (directory.defaultWorkspaceId) {
1293
+ const suggestedWorkspace = workspaces.find((workspace) => workspace.id === directory.defaultWorkspaceId) || null;
1294
+ if (suggestedWorkspace) {
1295
+ process.stdout.write(`[socialseal] No local default workspace is configured. Suggested workspace: ${suggestedWorkspace.name} (${suggestedWorkspace.id})\n`);
1296
+ return;
1297
+ }
1298
+ }
1299
+
1300
+ process.stdout.write('[socialseal] No default workspace is configured and no accessible workspace suggestion is available.\n');
1301
+ }
1302
+
1303
+ async function handleWorkspaceUse(opts) {
1304
+ const config = loadConfig();
1305
+ const apiKey = requireApiKey(opts, config);
1306
+ const apiBase = resolveApiBase(opts, config);
1307
+ const { resolvedApiBase } = resolveApiTarget({ apiBase, legacyUrl: null });
1308
+ const timeoutMs = resolveTimeoutMs(opts, config);
1309
+ const directory = await fetchWorkspaceDirectory({
1310
+ apiBase: resolvedApiBase,
1311
+ apiKey,
1312
+ timeoutMs,
1313
+ });
1314
+ const workspaces = Array.isArray(directory.workspaces) ? directory.workspaces : [];
1315
+ const workspace = matchWorkspaceIdentifier(workspaces, opts.identifier);
1316
+ saveConfig({
1317
+ ...config,
1318
+ workspaceId: workspace.id,
1319
+ });
1320
+
1321
+ const payload = {
1322
+ success: true,
1323
+ workspaceId: workspace.id,
1324
+ workspace,
1325
+ configPath: getConfigPath(),
1326
+ };
1327
+ if (opts.json) {
1328
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
1329
+ return;
1330
+ }
1331
+
1332
+ process.stdout.write(`[socialseal] Default workspace set to ${workspace.name} (${workspace.id})\n`);
1333
+ }
1334
+
1335
+ function handleWorkspaceClear(opts) {
1336
+ const config = loadConfig();
1337
+ const nextConfig = { ...config };
1338
+ delete nextConfig.workspaceId;
1339
+ saveConfig(nextConfig);
1340
+
1341
+ const payload = {
1342
+ success: true,
1343
+ configPath: getConfigPath(),
1344
+ };
1345
+ if (opts.json) {
1346
+ process.stdout.write(JSON.stringify(payload, null, opts.pretty ? 2 : 0) + '\n');
1347
+ return;
1348
+ }
1349
+
1350
+ process.stdout.write('[socialseal] Default workspace cleared.\n');
1351
+ }
1352
+
896
1353
  const program = new Command();
897
1354
  program
898
1355
  .name('socialseal')
@@ -905,7 +1362,7 @@ if (typeof program.showHelpAfterError === 'function') {
905
1362
  if (typeof program.showSuggestionAfterError === 'function') {
906
1363
  program.showSuggestionAfterError(true);
907
1364
  }
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`);
1365
+ 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
1366
 
910
1367
  program
911
1368
  .command('agent')
@@ -916,6 +1373,7 @@ program
916
1373
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
917
1374
  .option('--api-key <key>', 'CLI API key')
918
1375
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
1376
+ .option('--continue <token>', 'Continuation token from a previous agent run')
919
1377
  .option('--conversation-id <id>', 'Conversation id to resume')
920
1378
  .option('--create-new', 'Create a new conversation')
921
1379
  .option('--json', 'Emit NDJSON events')
@@ -924,6 +1382,49 @@ program
924
1382
  .option('--verbose', 'Show error details')
925
1383
  .action((opts) => runCommand(handleAgentRun, opts));
926
1384
 
1385
+ const workspace = program.command('workspace').description('Discover and manage the default workspace');
1386
+
1387
+ workspace
1388
+ .command('list')
1389
+ .description('List accessible workspaces for this CLI key')
1390
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
1391
+ .option('--api-key <key>', 'CLI API key')
1392
+ .option('--json', 'Emit machine-readable output')
1393
+ .option('--pretty', 'Pretty-print JSON')
1394
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
1395
+ .option('--verbose', 'Show error details')
1396
+ .action((opts) => runCommand(handleWorkspaceList, opts));
1397
+
1398
+ workspace
1399
+ .command('current')
1400
+ .description('Show the effective default workspace')
1401
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
1402
+ .option('--api-key <key>', 'CLI API key')
1403
+ .option('--json', 'Emit machine-readable output')
1404
+ .option('--pretty', 'Pretty-print JSON')
1405
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
1406
+ .option('--verbose', 'Show error details')
1407
+ .action((opts) => runCommand(handleWorkspaceCurrent, opts));
1408
+
1409
+ workspace
1410
+ .command('use <identifier>')
1411
+ .description('Persist a default workspace by id, slug, or exact name')
1412
+ .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
1413
+ .option('--api-key <key>', 'CLI API key')
1414
+ .option('--json', 'Emit machine-readable output')
1415
+ .option('--pretty', 'Pretty-print JSON')
1416
+ .option('--timeout <ms>', 'Request timeout in milliseconds')
1417
+ .option('--verbose', 'Show error details')
1418
+ .action((identifier, opts) => runCommand(handleWorkspaceUse, { ...opts, identifier }));
1419
+
1420
+ workspace
1421
+ .command('clear')
1422
+ .description('Clear the locally configured default workspace')
1423
+ .option('--json', 'Emit machine-readable output')
1424
+ .option('--pretty', 'Pretty-print JSON')
1425
+ .option('--verbose', 'Show error details')
1426
+ .action((opts) => runCommand(handleWorkspaceClear, opts));
1427
+
927
1428
  const tools = program.command('tools').description('Call edge functions directly (tool backends)');
928
1429
 
929
1430
  tools
@@ -939,6 +1440,9 @@ tools
939
1440
  .requiredOption('--function <name>', 'Tool name (see official docs)')
940
1441
  .option('--method <method>', 'HTTP method', 'POST')
941
1442
  .option('--body <jsonOrFile>', 'JSON body or @file.json')
1443
+ .option('--async', 'Request async execution for supported tool backends')
1444
+ .option('--no-poll', 'Return immediately after async start instead of polling to completion')
1445
+ .option('--poll-interval <ms>', 'Polling interval in milliseconds for supported async tool calls')
942
1446
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
943
1447
  .option('--api-key <key>', 'CLI API key')
944
1448
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')