@socialseal/cli 0.1.0 → 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
@@ -1,8 +1,17 @@
1
1
  # Changelog
2
2
 
3
3
  ## Unreleased
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
+
10
+ ## 0.1.1 - 2026-03-13
4
11
  - Document public base URL and CLI error output.
5
12
  - Add request timeouts, verbose error output, and OSS-safe tool discovery behavior.
13
+ - Ship a stable built-in tool registry for `tools list` instead of the hard-disabled discovery message.
14
+ - Fail fast on agent WebSocket `error` events and surface session/tool progress diagnostics in `--verbose` mode.
6
15
 
7
16
  ## 0.1.0
8
17
  - Initial CLI with agent streaming, tools calls, and provisional data exports.
package/README.md CHANGED
@@ -9,7 +9,9 @@
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)
14
+ - `SOCIALSEAL_AGENT_IDLE_TIMEOUT_MS` (optional agent WebSocket inactivity timeout override; default 300000)
13
15
 
14
16
  Optional config file:
15
17
  - `~/.config/socialseal/config.json`
@@ -18,22 +20,34 @@ Optional config file:
18
20
  {
19
21
  "apiKey": "ss_cli_...",
20
22
  "apiBase": "https://api.socialseal.co",
21
- "timeoutMs": 30000
23
+ "workspaceId": "00000000-0000-0000-0000-000000000000",
24
+ "timeoutMs": 30000,
25
+ "agentIdleTimeoutMs": 300000
22
26
  }
23
27
  ```
24
28
 
25
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
+
26
36
  - Agent (non-interactive, streaming):
27
37
  - `socialseal agent run --message "..." --api-base https://api.socialseal.co --api-key <key> [--workspace-id <uuid>]`
38
+ - `socialseal agent run --message "..." --continue <token>`
28
39
  - `socialseal agent run --message "..." --timeout 60000`
40
+ - `socialseal agent run --message "..." --idle-timeout 300000 --verbose`
29
41
 
30
- - Tools list (limited):
42
+ - Tools list (built-in registry):
31
43
  - `socialseal tools list`
32
44
  - `socialseal tools list --json`
33
45
 
34
46
  - Tools (direct edge function call):
35
47
  - `socialseal tools call --function <tool> --body @payload.json --api-base https://api.socialseal.co --api-key <key>`
36
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>`
37
51
 
38
52
  - Data exports (provisional):
39
53
  - `socialseal data export-tracking --group-id 123 --time-period 30d --out out.csv`
@@ -41,13 +55,29 @@ Optional config file:
41
55
 
42
56
  ## Notes
43
57
  - `export-report` and `export_tracking_data` are provisional until CLI export specs are finalized.
44
- - `tools list` does not enumerate internal endpoints in the OSS build. Refer to official docs for supported tool names.
45
- - Use `--timeout <ms>` to override the default 30s timeout for network calls.
58
+ - `tools list` ships a stable built-in registry of supported direct-call function targets. It is not live backend enumeration.
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.
46
66
 
47
67
  ## Errors and exit codes
48
68
  - Exit codes: `2` (usage), `3` (auth), `4` (not found), `5` (server), `1` (unknown)
49
69
  - Add `--json` to `tools call` or `data` commands to emit machine-readable errors.
50
- - Add `--verbose` to print error details (suppressed by default).
70
+ - Add `--verbose` to print error details plus agent session/tool progress diagnostics.
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.
51
81
 
52
82
  ## Smoke Test (manual)
53
83
  1. `SOCIALSEAL_API_KEY=... socialseal agent run --message "ping"`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socialseal/cli",
3
- "version": "0.1.0",
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
@@ -11,7 +11,9 @@ const DEFAULT_API_BASE = 'https://api.socialseal.co';
11
11
  const CLI_KEY_HEADER = 'X-CLI-Key';
12
12
  const WORKSPACE_HEADER = 'X-Workspace-Id';
13
13
  const DEFAULT_TIMEOUT_MS = 30000;
14
- const MAX_TIMEOUT_MS = 300000;
14
+ const DEFAULT_AGENT_IDLE_TIMEOUT_MS = 300000;
15
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
16
+ const MAX_TIMEOUT_MS = 900000;
15
17
  const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
16
18
  const EXIT_CODES = {
17
19
  OK: 0,
@@ -22,10 +24,47 @@ const EXIT_CODES = {
22
24
  SERVER: 5,
23
25
  };
24
26
  const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
25
- const KNOWN_TOOLS = [];
27
+ const KNOWN_TOOLS = [
28
+ { name: 'agent-tool-jobs', category: 'agent', description: 'Poll queued agent-backed tool jobs and fetch their results.' },
29
+ { name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
30
+ { name: 'workspace-notes', category: 'agent', description: 'Search, create, update, and pin workspace note memory.' },
31
+ { name: 'workspace-onboarding', category: 'agent', description: 'Read or update workspace onboarding metadata used by the agent.' },
32
+ { name: 'brand-group-management', category: 'brand', description: 'Manage brand groups, aliases, competitors, and rule configuration.' },
33
+ { name: 'enqueue-brand-metrics-backfill', category: 'brand', description: 'Queue backfill jobs for brand metrics refreshes.' },
34
+ { name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
35
+ { name: 'export_tracking_data', category: 'export', description: 'Stream tracking exports as CSV for a group or tracking item.' },
36
+ { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
37
+ { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
38
+ { name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
39
+ { name: 'tiktok-geo-api', category: 'search', description: 'Query TikTok search and geo data.' },
40
+ { name: 'xhs-geo-api', category: 'search', description: 'Query Xiaohongshu search and geo data.' },
41
+ { name: 'youtube-geo-api', category: 'search', description: 'Query YouTube search and geo data.' },
42
+ { name: 'group-management', category: 'tracking', description: 'Create, update, list, and delete tracking groups and memberships.' },
43
+ { name: 'tracking', category: 'tracking', description: 'Create, update, list, refresh, and delete tracking items.' },
44
+ { name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
45
+ { name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
46
+ { name: 'search-journey-run', category: 'vnext', description: 'Run a search journey for a subject across supported platforms.' },
47
+ { name: 'vnext-blueprints-create', category: 'vnext', description: 'Create a vNext blueprint from grounded evidence.' },
48
+ { name: 'vnext-blueprints-generate', category: 'vnext', description: 'Generate a vNext blueprint from workspace opportunity data.' },
49
+ { name: 'vnext-blueprints-read', category: 'vnext', description: 'Read vNext blueprint history and specific versions.' },
50
+ { name: 'vnext-briefs-create', category: 'vnext', description: 'Create a vNext brief record.' },
51
+ { name: 'vnext-briefs-generate', category: 'vnext', description: 'Generate a vNext brief from a blueprint or opportunity.' },
52
+ { name: 'vnext-briefs-read', category: 'vnext', description: 'Read generated vNext briefs and version history.' },
53
+ { name: 'vnext-intents', category: 'vnext', description: 'List, create, update, or delete vNext intents.' },
54
+ { name: 'vnext-journeys', category: 'vnext', description: 'List journey runs and inspect their latest outputs.' },
55
+ { name: 'vnext-keywords', category: 'vnext', description: 'List, create, update, or delete vNext keywords.' },
56
+ { name: 'vnext-personas', category: 'vnext', description: 'List, create, update, retire, or reactivate vNext personas.' },
57
+ { name: 'vnext-pillars', category: 'vnext', description: 'List, create, update, or delete vNext content pillars.' },
58
+ { name: 'vnext-topics', category: 'vnext', description: 'Manage topics, assignments, queues, and topic suggestions.' },
59
+ { name: 'vnext-topics-auto-tag', category: 'vnext', description: 'Auto-tag keyword and topic assignments with Gemini-assisted review.' },
60
+ ];
61
+
62
+ function getConfigPath() {
63
+ return process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
64
+ }
26
65
 
27
66
  function loadConfig() {
28
- const configPath = process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
67
+ const configPath = getConfigPath();
29
68
  try {
30
69
  if (!fs.existsSync(configPath)) return {};
31
70
  const raw = fs.readFileSync(configPath, 'utf8');
@@ -36,6 +75,15 @@ function loadConfig() {
36
75
  }
37
76
  }
38
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
+
39
87
  function resolveApiKey(opts, config) {
40
88
  return opts.apiKey || process.env.SOCIALSEAL_API_KEY || config.apiKey;
41
89
  }
@@ -52,6 +100,19 @@ function resolveSupabaseUrl(opts, config) {
52
100
  return opts.supabaseUrl || process.env.SOCIALSEAL_SUPABASE_URL || config.supabaseUrl;
53
101
  }
54
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
+
55
116
  class CliError extends Error {
56
117
  constructor(message, { code = 'CLI_ERROR', exitCode = EXIT_CODES.UNKNOWN, status, hint, details } = {}) {
57
118
  super(message);
@@ -76,12 +137,11 @@ function normalizeMethod(method) {
76
137
  return normalized;
77
138
  }
78
139
 
79
- function resolveTimeoutMs(opts, config) {
80
- const raw = opts.timeout ?? process.env.SOCIALSEAL_TIMEOUT_MS ?? config.timeoutMs;
81
- if (raw == null || raw === '') return DEFAULT_TIMEOUT_MS;
140
+ function parseTimeoutMs(raw, { defaultValue = DEFAULT_TIMEOUT_MS, label = 'timeout' } = {}) {
141
+ if (raw == null || raw === '') return defaultValue;
82
142
  const parsed = Number(raw);
83
143
  if (!Number.isFinite(parsed) || parsed <= 0) {
84
- throw new CliError('Invalid timeout value. Use a positive number of milliseconds.', {
144
+ throw new CliError(`Invalid ${label} value. Use a positive number of milliseconds.`, {
85
145
  code: 'INVALID_TIMEOUT',
86
146
  exitCode: EXIT_CODES.USAGE,
87
147
  });
@@ -89,6 +149,31 @@ function resolveTimeoutMs(opts, config) {
89
149
  return Math.min(parsed, MAX_TIMEOUT_MS);
90
150
  }
91
151
 
152
+ function resolveTimeoutMs(opts, config) {
153
+ const raw = opts.timeout ?? process.env.SOCIALSEAL_TIMEOUT_MS ?? config.timeoutMs;
154
+ return parseTimeoutMs(raw, { defaultValue: DEFAULT_TIMEOUT_MS, label: 'timeout' });
155
+ }
156
+
157
+ function resolveAgentIdleTimeoutMs(opts, config, fallbackTimeoutMs) {
158
+ const explicitIdleTimeout =
159
+ opts.idleTimeout
160
+ ?? process.env.SOCIALSEAL_AGENT_IDLE_TIMEOUT_MS
161
+ ?? config.agentIdleTimeoutMs;
162
+ if (explicitIdleTimeout != null && explicitIdleTimeout !== '') {
163
+ return parseTimeoutMs(explicitIdleTimeout, {
164
+ defaultValue: DEFAULT_AGENT_IDLE_TIMEOUT_MS,
165
+ label: 'idle timeout',
166
+ });
167
+ }
168
+
169
+ const explicitTimeout = opts.timeout ?? process.env.SOCIALSEAL_TIMEOUT_MS ?? config.timeoutMs;
170
+ if (explicitTimeout != null && explicitTimeout !== '') {
171
+ return fallbackTimeoutMs;
172
+ }
173
+
174
+ return DEFAULT_AGENT_IDLE_TIMEOUT_MS;
175
+ }
176
+
92
177
  function resolveLegacyUrl(value, label) {
93
178
  if (!value) return null;
94
179
  if (!LEGACY_ENABLED) {
@@ -101,6 +186,52 @@ function resolveLegacyUrl(value, label) {
101
186
  return value;
102
187
  }
103
188
 
189
+ function emitInfo(opts, message) {
190
+ if (opts?.verbose) {
191
+ process.stderr.write(`[socialseal] ${message}\n`);
192
+ }
193
+ }
194
+
195
+ function formatCloseReason(reason) {
196
+ if (reason == null) return '';
197
+ if (Buffer.isBuffer(reason)) return reason.toString('utf8');
198
+ if (typeof reason === 'string') return reason;
199
+ return String(reason);
200
+ }
201
+
202
+ async function readNodeResponseBody(response, limit = 2000) {
203
+ if (!response) return null;
204
+
205
+ return await new Promise((resolve) => {
206
+ const chunks = [];
207
+ let bufferedBytes = 0;
208
+ let totalBytes = 0;
209
+ let settled = false;
210
+
211
+ const finish = (value) => {
212
+ if (settled) return;
213
+ settled = true;
214
+ resolve(value);
215
+ };
216
+
217
+ response.on('data', (chunk) => {
218
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
219
+ totalBytes += buffer.length;
220
+ if (bufferedBytes >= limit) return;
221
+
222
+ const remaining = limit - bufferedBytes;
223
+ const slice = buffer.subarray(0, remaining);
224
+ chunks.push(slice);
225
+ bufferedBytes += slice.length;
226
+ });
227
+ response.on('end', () => {
228
+ const text = chunks.length > 0 ? Buffer.concat(chunks).toString('utf8') : '';
229
+ finish(totalBytes > limit ? `${text}…` : text);
230
+ });
231
+ response.on('error', () => finish(null));
232
+ });
233
+ }
234
+
104
235
  function parseJsonInput(value, { label = 'payload', allowString = false } = {}) {
105
236
  if (!value) return null;
106
237
  if (value.startsWith('@')) {
@@ -148,6 +279,150 @@ function ensureJsonObject(value, label) {
148
279
  return value;
149
280
  }
150
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
+
151
426
  function mapStatusToExitCode(status) {
152
427
  if (status === 401 || status === 403) return EXIT_CODES.AUTH;
153
428
  if (status === 404) return EXIT_CODES.NOT_FOUND;
@@ -231,7 +506,11 @@ function emitError(err, opts = {}) {
231
506
  process.stderr.write(`[socialseal] ${payload.error.hint}\n`);
232
507
  }
233
508
  if (showDetails && payload.error.details) {
234
- process.stderr.write(`[socialseal] Details: ${payload.error.details}\n`);
509
+ const detailsText =
510
+ typeof payload.error.details === 'string'
511
+ ? payload.error.details
512
+ : JSON.stringify(payload.error.details);
513
+ process.stderr.write(`[socialseal] Details: ${detailsText}\n`);
235
514
  } else if (!showDetails && err.details) {
236
515
  process.stderr.write('[socialseal] Use --verbose to see error details.\n');
237
516
  }
@@ -329,6 +608,68 @@ async function callApi({ apiBase, apiKey, path, method = 'POST', body, workspace
329
608
  return res;
330
609
  }
331
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
+
332
673
  async function handleAgentRun(opts) {
333
674
  const config = loadConfig();
334
675
  const apiKey = requireApiKey(opts, config);
@@ -336,12 +677,34 @@ async function handleAgentRun(opts) {
336
677
  const agentUrl = resolveLegacyUrl(resolveAgentUrl(opts, config), 'SOCIALSEAL_AGENT_URL');
337
678
  const { resolvedApiBase, legacyUrl } = resolveApiTarget({ apiBase, legacyUrl: agentUrl });
338
679
  const timeoutMs = resolveTimeoutMs(opts, config);
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
+ }
339
702
 
340
703
  const headers = {
341
704
  'Content-Type': 'application/json',
342
705
  [CLI_KEY_HEADER]: apiKey,
343
706
  };
344
- if (opts.workspaceId) headers[WORKSPACE_HEADER] = opts.workspaceId;
707
+ if (resolvedWorkspaceIdInput) headers[WORKSPACE_HEADER] = resolvedWorkspaceIdInput;
345
708
 
346
709
  const sessionUrl = resolvedApiBase
347
710
  ? `${resolvedApiBase.replace(/\/$/, '')}/cli/agent/session`
@@ -351,8 +714,9 @@ async function handleAgentRun(opts) {
351
714
  method: 'POST',
352
715
  headers,
353
716
  body: JSON.stringify({
354
- conversationId: opts.conversationId || undefined,
355
- createNew: !!opts.createNew,
717
+ continuationToken: continuationToken || undefined,
718
+ conversationId: continuationToken ? undefined : (opts.conversationId || undefined),
719
+ createNew: continuationToken || opts.conversationId ? undefined : true,
356
720
  }),
357
721
  }, timeoutMs);
358
722
 
@@ -361,6 +725,10 @@ async function handleAgentRun(opts) {
361
725
  }
362
726
 
363
727
  const sessionData = await sessionRes.json();
728
+ const sessionId = sessionData?.data?.sessionId || null;
729
+ const initialConversationId = sessionData?.data?.activeConversationId || opts.conversationId || null;
730
+ const resolvedWorkspaceId = sessionData?.data?.workspaceId || resolvedWorkspaceIdInput || null;
731
+ const nextContinuationToken = sessionData?.data?.continuationToken || null;
364
732
  const wsUrl = sessionData?.data?.websocketUrl;
365
733
  if (!wsUrl) {
366
734
  throw new CliError('Missing websocketUrl in session response.', {
@@ -368,6 +736,23 @@ async function handleAgentRun(opts) {
368
736
  exitCode: EXIT_CODES.SERVER,
369
737
  });
370
738
  }
739
+ emitInfo(
740
+ opts,
741
+ `Agent session created${sessionId ? ` (session ${sessionId})` : ''}${initialConversationId ? ` for conversation ${initialConversationId}` : ''}.`,
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
+ }
371
756
 
372
757
  const context = parseJsonInput(opts.context, { label: 'context', allowString: true });
373
758
  const message = opts.message;
@@ -375,10 +760,36 @@ async function handleAgentRun(opts) {
375
760
  await new Promise((resolve, reject) => {
376
761
  const ws = new WebSocket(wsUrl);
377
762
  let finished = false;
763
+ let settled = false;
378
764
  let inactivityTimer = null;
765
+ let sawAssistantChunk = false;
766
+ let sawToolCall = false;
767
+ let sawThinking = false;
768
+ let lastMessageType = 'none';
769
+ let activeConversationId = initialConversationId;
770
+ const toolProgressStatus = new Map();
771
+
772
+ const settleResolve = () => {
773
+ if (settled) return;
774
+ settled = true;
775
+ if (inactivityTimer) clearTimeout(inactivityTimer);
776
+ resolve();
777
+ };
778
+
779
+ const settleReject = (error) => {
780
+ if (settled) return;
781
+ settled = true;
782
+ if (inactivityTimer) clearTimeout(inactivityTimer);
783
+ try {
784
+ ws.terminate();
785
+ } catch {
786
+ // ignore
787
+ }
788
+ reject(error);
789
+ };
379
790
 
380
791
  const resetInactivity = () => {
381
- if (!timeoutMs) return;
792
+ if (!idleTimeoutMs) return;
382
793
  if (inactivityTimer) clearTimeout(inactivityTimer);
383
794
  inactivityTimer = setTimeout(() => {
384
795
  try {
@@ -386,38 +797,74 @@ async function handleAgentRun(opts) {
386
797
  } catch {
387
798
  // ignore
388
799
  }
389
- reject(new CliError('WebSocket timed out waiting for agent response.', {
800
+ settleReject(new CliError('WebSocket timed out waiting for agent response.', {
390
801
  code: 'WEBSOCKET_TIMEOUT',
391
802
  exitCode: EXIT_CODES.SERVER,
392
- hint: 'Increase the timeout with --timeout <ms>.',
803
+ hint: 'Increase the timeout with --idle-timeout <ms> or --timeout <ms>.',
804
+ details: truncateDetails({
805
+ sessionId,
806
+ activeConversationId,
807
+ lastMessageType,
808
+ sawAssistantChunk,
809
+ sawToolCall,
810
+ sawThinking,
811
+ idleTimeoutMs,
812
+ }),
393
813
  }));
394
- }, timeoutMs);
814
+ }, idleTimeoutMs);
395
815
  };
396
816
 
397
817
  ws.on('open', () => {
398
818
  resetInactivity();
819
+ emitInfo(opts, 'Connected to agent WebSocket.');
399
820
  const payload = {
400
821
  type: 'user_message',
401
822
  payload: { content: message, context: context || undefined },
402
823
  timestamp: Date.now(),
403
824
  };
404
825
  ws.send(JSON.stringify(payload));
826
+ emitInfo(opts, 'User message sent to agent.');
405
827
  });
406
828
 
407
829
  ws.on('message', (data) => {
408
830
  try {
409
831
  resetInactivity();
410
832
  const msg = JSON.parse(data.toString());
833
+ lastMessageType = msg.type || 'unknown';
834
+
835
+ if (msg.type === 'session_state' && msg.payload?.activeConversationId) {
836
+ activeConversationId = msg.payload.activeConversationId;
837
+ emitInfo(
838
+ opts,
839
+ `Session state received${sessionId ? ` for session ${sessionId}` : ''}${activeConversationId ? ` (conversation ${activeConversationId})` : ''}.`,
840
+ );
841
+ }
842
+
411
843
  if (opts.json) {
412
844
  process.stdout.write(JSON.stringify(msg) + '\n');
413
845
  if (msg.type === 'assistant_chunk' && msg.payload?.done) {
414
846
  finished = true;
415
847
  ws.close(1000, 'done');
416
848
  }
849
+ if (msg.type === 'error') {
850
+ const payload = msg.payload || {};
851
+ settleReject(new CliError(`Agent error: ${payload.message || 'unknown'}`, {
852
+ code: payload.code || 'AGENT_ERROR',
853
+ exitCode: EXIT_CODES.SERVER,
854
+ hint: payload.retryable ? 'Retry the request or inspect backend status.' : null,
855
+ details: truncateDetails({
856
+ ...payload,
857
+ sessionId,
858
+ activeConversationId,
859
+ lastMessageType,
860
+ }),
861
+ }));
862
+ }
417
863
  return;
418
864
  }
419
865
  if (msg.type === 'assistant_chunk') {
420
866
  const chunk = msg.payload?.chunk ?? '';
867
+ sawAssistantChunk = sawAssistantChunk || chunk.length > 0 || !!msg.payload?.done;
421
868
  if (chunk) process.stdout.write(chunk);
422
869
  if (msg.payload?.done) {
423
870
  finished = true;
@@ -425,28 +872,114 @@ async function handleAgentRun(opts) {
425
872
  ws.close(1000, 'done');
426
873
  }
427
874
  } else if (msg.type === 'error') {
428
- process.stderr.write(`\n[socialseal] Agent error: ${msg.payload?.message || 'unknown'}\n`);
875
+ const payload = msg.payload || {};
876
+ settleReject(new CliError(`Agent error: ${payload.message || 'unknown'}`, {
877
+ code: payload.code || 'AGENT_ERROR',
878
+ exitCode: EXIT_CODES.SERVER,
879
+ hint: payload.retryable ? 'Retry the request or inspect backend status.' : null,
880
+ details: truncateDetails({
881
+ ...payload,
882
+ sessionId,
883
+ activeConversationId,
884
+ lastMessageType,
885
+ }),
886
+ }));
887
+ } else if (msg.type === 'thinking_chunk') {
888
+ sawThinking = true;
889
+ emitInfo(opts, 'Agent is thinking.');
890
+ } else if (msg.type === 'assistant_status') {
891
+ const code = msg.payload?.code || 'unknown';
892
+ const statusMessage = msg.payload?.message || 'Agent reported a status update.';
893
+ emitInfo(opts, `Agent status [${code}]: ${statusMessage}`);
894
+ } else if (msg.type === 'tool_call_start') {
895
+ sawToolCall = true;
896
+ emitInfo(opts, `Tool start: ${msg.payload?.name || 'unknown'}`);
897
+ } else if (msg.type === 'tool_call_progress') {
898
+ const toolCallId = msg.payload?.toolCallId || '';
899
+ const progressStatus = msg.payload?.status || 'running';
900
+ if (toolProgressStatus.get(toolCallId) !== progressStatus) {
901
+ toolProgressStatus.set(toolCallId, progressStatus);
902
+ emitInfo(opts, `Tool progress: ${progressStatus}`);
903
+ }
904
+ } else if (msg.type === 'tool_call_complete') {
905
+ const error = msg.payload?.error;
906
+ const duration = typeof msg.payload?.duration_ms === 'number'
907
+ ? `${msg.payload.duration_ms}ms`
908
+ : 'unknown duration';
909
+ if (error) {
910
+ emitInfo(opts, `Tool failed after ${duration}: ${error}`);
911
+ } else {
912
+ emitInfo(opts, `Tool completed in ${duration}.`);
913
+ }
429
914
  }
430
915
  } catch (err) {
431
- process.stderr.write(`\n[socialseal] Failed to parse agent message: ${err.message || err}\n`);
916
+ settleReject(new CliError(`Failed to parse agent message: ${err.message || err}`, {
917
+ code: 'INVALID_AGENT_MESSAGE',
918
+ exitCode: EXIT_CODES.SERVER,
919
+ details: data.toString(),
920
+ }));
432
921
  }
433
922
  });
434
923
 
435
- ws.on('close', () => {
436
- if (inactivityTimer) clearTimeout(inactivityTimer);
924
+ ws.on('unexpected-response', async (_req, response) => {
925
+ const statusText = response.statusCode
926
+ ? `${response.statusCode}${response.statusMessage ? ` ${response.statusMessage}` : ''}`
927
+ : 'unknown';
928
+ const details = await readNodeResponseBody(response);
929
+ settleReject(new CliError(`WebSocket upgrade failed: ${statusText}`.trim(), {
930
+ code: 'WEBSOCKET_UPGRADE_FAILED',
931
+ exitCode:
932
+ response.statusCode === 401 || response.statusCode === 403
933
+ ? EXIT_CODES.AUTH
934
+ : EXIT_CODES.SERVER,
935
+ hint:
936
+ response.statusCode === 401 || response.statusCode === 403
937
+ ? 'Check your CLI key, workspace scope, and session endpoint auth.'
938
+ : 'Retry with --verbose to inspect gateway or backend behavior.',
939
+ details: truncateDetails({
940
+ sessionId,
941
+ activeConversationId,
942
+ responseBody: details,
943
+ }),
944
+ }));
945
+ });
946
+
947
+ ws.on('close', (code, reason) => {
948
+ const closeReason = formatCloseReason(reason);
437
949
  if (!finished) {
438
- reject(new CliError('WebSocket closed before completion.', {
439
- code: 'WEBSOCKET_CLOSED',
440
- exitCode: EXIT_CODES.SERVER,
441
- }));
950
+ settleReject(new CliError(
951
+ `WebSocket closed before completion (code ${code}${closeReason ? `: ${closeReason}` : ''}).`,
952
+ {
953
+ code: 'WEBSOCKET_CLOSED',
954
+ exitCode: EXIT_CODES.SERVER,
955
+ hint: sawAssistantChunk
956
+ ? 'The agent disconnected mid-response. Retry the request.'
957
+ : 'The agent closed the connection before completing. Retry with --verbose for more diagnostics.',
958
+ details: truncateDetails({
959
+ sessionId,
960
+ activeConversationId,
961
+ lastMessageType,
962
+ sawAssistantChunk,
963
+ sawToolCall,
964
+ sawThinking,
965
+ }),
966
+ },
967
+ ));
442
968
  } else {
443
- resolve();
969
+ settleResolve();
444
970
  }
445
971
  });
446
972
 
447
973
  ws.on('error', (err) => {
448
- if (inactivityTimer) clearTimeout(inactivityTimer);
449
- reject(err);
974
+ settleReject(new CliError(`WebSocket error: ${err.message || err}`, {
975
+ code: 'WEBSOCKET_ERROR',
976
+ exitCode: EXIT_CODES.SERVER,
977
+ details: truncateDetails({
978
+ sessionId,
979
+ activeConversationId,
980
+ lastMessageType,
981
+ }),
982
+ }));
450
983
  });
451
984
  });
452
985
  }
@@ -458,16 +991,19 @@ async function handleToolsCall(opts) {
458
991
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
459
992
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
460
993
  const timeoutMs = resolveTimeoutMs(opts, config);
994
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
461
995
 
462
- const payload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
996
+ const parsedPayload = parseJsonInput(opts.body, { label: 'body' }) ?? {};
997
+ const mergedPayload = mergeWorkspaceIdIntoPayload(parsedPayload, resolvedWorkspaceId);
463
998
  const method = normalizeMethod(opts.method);
999
+ const payload = applySearchJourneyRunAsyncStart(mergedPayload, { ...opts, method });
464
1000
  const res = await callApi({
465
1001
  apiBase: useGateway ? resolvedApiBase : legacyUrl,
466
1002
  apiKey,
467
1003
  path: useGateway ? `/cli/tools/${opts.function}` : `/functions/v1/${opts.function}`,
468
1004
  method,
469
1005
  body: payload,
470
- workspaceId: opts.workspaceId,
1006
+ workspaceId: resolvedWorkspaceId,
471
1007
  timeoutMs,
472
1008
  });
473
1009
 
@@ -482,7 +1018,45 @@ async function handleToolsCall(opts) {
482
1018
  const contentType = res.headers.get('content-type') || '';
483
1019
  if (contentType.includes('application/json')) {
484
1020
  const data = await res.json();
485
- 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);
486
1060
  return;
487
1061
  }
488
1062
 
@@ -492,8 +1066,9 @@ async function handleToolsCall(opts) {
492
1066
 
493
1067
  function handleToolsList(opts) {
494
1068
  const payload = {
1069
+ discovery: 'built_in_registry',
495
1070
  tools: KNOWN_TOOLS,
496
- note: 'Tool discovery is disabled in the OSS CLI. Refer to official docs for supported tool names.',
1071
+ note: 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration.',
497
1072
  };
498
1073
 
499
1074
  if (opts.json) {
@@ -501,8 +1076,19 @@ function handleToolsList(opts) {
501
1076
  return;
502
1077
  }
503
1078
 
504
- process.stdout.write('[socialseal] Tool discovery is disabled in the OSS CLI.\n');
1079
+ process.stdout.write('[socialseal] Built-in tool registry\n');
505
1080
  process.stdout.write(`[socialseal] ${payload.note}\n`);
1081
+
1082
+ let currentCategory = null;
1083
+ for (const tool of KNOWN_TOOLS) {
1084
+ if (tool.category !== currentCategory) {
1085
+ currentCategory = tool.category;
1086
+ process.stdout.write(`\n${currentCategory}\n`);
1087
+ }
1088
+ process.stdout.write(`- ${tool.name}: ${tool.description}\n`);
1089
+ }
1090
+
1091
+ process.stdout.write('\n[socialseal] Call a tool with: socialseal tools call --function <name> --body @payload.json\n');
506
1092
  }
507
1093
 
508
1094
  async function handleDataExportTracking(opts) {
@@ -512,6 +1098,7 @@ async function handleDataExportTracking(opts) {
512
1098
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
513
1099
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
514
1100
  const timeoutMs = resolveTimeoutMs(opts, config);
1101
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
515
1102
 
516
1103
  if (!opts.groupId && !opts.itemId) {
517
1104
  throw new CliError('Provide --group-id or --item-id.', {
@@ -532,7 +1119,7 @@ async function handleDataExportTracking(opts) {
532
1119
  path: useGateway ? '/cli/tools/export_tracking_data' : '/functions/v1/export_tracking_data',
533
1120
  method: 'POST',
534
1121
  body: payload,
535
- workspaceId: opts.workspaceId,
1122
+ workspaceId: resolvedWorkspaceId,
536
1123
  timeoutMs,
537
1124
  });
538
1125
 
@@ -540,6 +1127,13 @@ async function handleDataExportTracking(opts) {
540
1127
  throw await buildHttpError(res, { label: 'Tracking export' });
541
1128
  }
542
1129
 
1130
+ if (!res.body) {
1131
+ throw new CliError('Export response contained no body.', {
1132
+ code: 'EMPTY_RESPONSE',
1133
+ exitCode: EXIT_CODES.SERVER,
1134
+ });
1135
+ }
1136
+
543
1137
  const outPath = opts.stdout ? null : (opts.out || 'tracking_export.csv');
544
1138
  if (outPath) {
545
1139
  await pipeline(res.body, fs.createWriteStream(outPath));
@@ -556,6 +1150,7 @@ async function handleDataExportReport(opts) {
556
1150
  const supabaseUrl = resolveLegacyUrl(resolveSupabaseUrl(opts, config), 'SOCIALSEAL_SUPABASE_URL');
557
1151
  const { resolvedApiBase, legacyUrl, useGateway } = resolveApiTarget({ apiBase, legacyUrl: supabaseUrl });
558
1152
  const timeoutMs = resolveTimeoutMs(opts, config);
1153
+ const { workspaceId: resolvedWorkspaceId } = resolveWorkspaceSelection(opts, config);
559
1154
 
560
1155
  const payload = ensureJsonObject(parseJsonInput(opts.payload, { label: 'payload' }), 'payload');
561
1156
 
@@ -569,7 +1164,7 @@ async function handleDataExportReport(opts) {
569
1164
  format: opts.format,
570
1165
  payload,
571
1166
  },
572
- workspaceId: opts.workspaceId,
1167
+ workspaceId: resolvedWorkspaceId,
573
1168
  timeoutMs,
574
1169
  });
575
1170
 
@@ -611,11 +1206,155 @@ async function handleDataExportReport(opts) {
611
1206
  process.stdout.write(JSON.stringify(json, null, opts.pretty ? 2 : 0) + '\n');
612
1207
  }
613
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
+
614
1353
  const program = new Command();
615
1354
  program
616
1355
  .name('socialseal')
617
1356
  .description('SocialSeal CLI (non-interactive)')
618
- .version('0.1.0');
1357
+ .version('0.1.1');
619
1358
 
620
1359
  if (typeof program.showHelpAfterError === 'function') {
621
1360
  program.showHelpAfterError(true);
@@ -623,7 +1362,7 @@ if (typeof program.showHelpAfterError === 'function') {
623
1362
  if (typeof program.showSuggestionAfterError === 'function') {
624
1363
  program.showSuggestionAfterError(true);
625
1364
  }
626
- 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`);
627
1366
 
628
1367
  program
629
1368
  .command('agent')
@@ -634,18 +1373,63 @@ program
634
1373
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
635
1374
  .option('--api-key <key>', 'CLI API key')
636
1375
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')
1376
+ .option('--continue <token>', 'Continuation token from a previous agent run')
637
1377
  .option('--conversation-id <id>', 'Conversation id to resume')
638
1378
  .option('--create-new', 'Create a new conversation')
639
1379
  .option('--json', 'Emit NDJSON events')
640
1380
  .option('--timeout <ms>', 'Request timeout in milliseconds')
1381
+ .option('--idle-timeout <ms>', 'WebSocket inactivity timeout in milliseconds')
641
1382
  .option('--verbose', 'Show error details')
642
1383
  .action((opts) => runCommand(handleAgentRun, opts));
643
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
+
644
1428
  const tools = program.command('tools').description('Call edge functions directly (tool backends)');
645
1429
 
646
1430
  tools
647
1431
  .command('list')
648
- .description('List tools (discovery disabled in OSS build)')
1432
+ .description('List built-in tool registry entries')
649
1433
  .option('--json', 'Emit machine-readable output')
650
1434
  .option('--pretty', 'Pretty-print JSON')
651
1435
  .option('--verbose', 'Show error details')
@@ -656,6 +1440,9 @@ tools
656
1440
  .requiredOption('--function <name>', 'Tool name (see official docs)')
657
1441
  .option('--method <method>', 'HTTP method', 'POST')
658
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')
659
1446
  .option('--api-base <url>', 'API base URL (default https://api.socialseal.co)')
660
1447
  .option('--api-key <key>', 'CLI API key')
661
1448
  .option('--workspace-id <id>', 'Workspace id (for scoped keys)')