@socialseal/cli 0.1.0 → 0.1.1

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,12 @@
1
1
  # Changelog
2
2
 
3
3
  ## Unreleased
4
+
5
+ ## 0.1.1 - 2026-03-13
4
6
  - Document public base URL and CLI error output.
5
7
  - Add request timeouts, verbose error output, and OSS-safe tool discovery behavior.
8
+ - Ship a stable built-in tool registry for `tools list` instead of the hard-disabled discovery message.
9
+ - Fail fast on agent WebSocket `error` events and surface session/tool progress diagnostics in `--verbose` mode.
6
10
 
7
11
  ## 0.1.0
8
12
  - Initial CLI with agent streaming, tools calls, and provisional data exports.
package/README.md CHANGED
@@ -10,6 +10,7 @@ Environment variables:
10
10
  - `SOCIALSEAL_API_KEY`
11
11
  - `SOCIALSEAL_API_BASE` (default `https://api.socialseal.co`)
12
12
  - `SOCIALSEAL_TIMEOUT_MS` (optional request timeout override)
13
+ - `SOCIALSEAL_AGENT_IDLE_TIMEOUT_MS` (optional agent WebSocket inactivity timeout override; default 300000)
13
14
 
14
15
  Optional config file:
15
16
  - `~/.config/socialseal/config.json`
@@ -18,7 +19,8 @@ Optional config file:
18
19
  {
19
20
  "apiKey": "ss_cli_...",
20
21
  "apiBase": "https://api.socialseal.co",
21
- "timeoutMs": 30000
22
+ "timeoutMs": 30000,
23
+ "agentIdleTimeoutMs": 300000
22
24
  }
23
25
  ```
24
26
 
@@ -26,8 +28,9 @@ Optional config file:
26
28
  - Agent (non-interactive, streaming):
27
29
  - `socialseal agent run --message "..." --api-base https://api.socialseal.co --api-key <key> [--workspace-id <uuid>]`
28
30
  - `socialseal agent run --message "..." --timeout 60000`
31
+ - `socialseal agent run --message "..." --idle-timeout 300000 --verbose`
29
32
 
30
- - Tools list (limited):
33
+ - Tools list (built-in registry):
31
34
  - `socialseal tools list`
32
35
  - `socialseal tools list --json`
33
36
 
@@ -41,13 +44,13 @@ Optional config file:
41
44
 
42
45
  ## Notes
43
46
  - `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.
47
+ - `tools list` ships a stable built-in registry of supported direct-call function targets. It is not live backend enumeration.
48
+ - `--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).
46
49
 
47
50
  ## Errors and exit codes
48
51
  - Exit codes: `2` (usage), `3` (auth), `4` (not found), `5` (server), `1` (unknown)
49
52
  - Add `--json` to `tools call` or `data` commands to emit machine-readable errors.
50
- - Add `--verbose` to print error details (suppressed by default).
53
+ - Add `--verbose` to print error details plus agent session/tool progress diagnostics.
51
54
 
52
55
  ## Smoke Test (manual)
53
56
  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.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SocialSeal CLI (non-interactive)",
package/src/index.js CHANGED
@@ -11,7 +11,8 @@ 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 MAX_TIMEOUT_MS = 900000;
15
16
  const LEGACY_ENABLED = process.env.SOCIALSEAL_ENABLE_LEGACY === '1';
16
17
  const EXIT_CODES = {
17
18
  OK: 0,
@@ -22,7 +23,40 @@ const EXIT_CODES = {
22
23
  SERVER: 5,
23
24
  };
24
25
  const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
25
- const KNOWN_TOOLS = [];
26
+ const KNOWN_TOOLS = [
27
+ { name: 'agent-tool-jobs', category: 'agent', description: 'Poll queued agent-backed tool jobs and fetch their results.' },
28
+ { name: 'deep-exploration-runs', category: 'agent', description: 'Read or persist deep exploration render runs.' },
29
+ { name: 'workspace-notes', category: 'agent', description: 'Search, create, update, and pin workspace note memory.' },
30
+ { name: 'workspace-onboarding', category: 'agent', description: 'Read or update workspace onboarding metadata used by the agent.' },
31
+ { name: 'brand-group-management', category: 'brand', description: 'Manage brand groups, aliases, competitors, and rule configuration.' },
32
+ { name: 'enqueue-brand-metrics-backfill', category: 'brand', description: 'Queue backfill jobs for brand metrics refreshes.' },
33
+ { name: 'export-report', category: 'export', description: 'Generate report exports (csv/json/markdown/html/excel_data).' },
34
+ { name: 'export_tracking_data', category: 'export', description: 'Stream tracking exports as CSV for a group or tracking item.' },
35
+ { name: 'douyin-geo-api', category: 'search', description: 'Query Douyin search and geo data.' },
36
+ { name: 'google-ai-search', category: 'search', description: 'Run Google AI search queries and fetch result snapshots.' },
37
+ { name: 'instagram-geo-api', category: 'search', description: 'Query Instagram search and geo data.' },
38
+ { name: 'tiktok-geo-api', category: 'search', description: 'Query TikTok search and geo data.' },
39
+ { name: 'xhs-geo-api', category: 'search', description: 'Query Xiaohongshu search and geo data.' },
40
+ { name: 'youtube-geo-api', category: 'search', description: 'Query YouTube search and geo data.' },
41
+ { name: 'group-management', category: 'tracking', description: 'Create, update, list, and delete tracking groups and memberships.' },
42
+ { name: 'tracking', category: 'tracking', description: 'Create, update, list, refresh, and delete tracking items.' },
43
+ { name: 'journey-feedback', category: 'vnext', description: 'Record acceptance or rejection feedback for opportunity bundles.' },
44
+ { name: 'opportunity-bundle-approve', category: 'vnext', description: 'Approve an opportunity bundle and create tracking coverage.' },
45
+ { name: 'search-journey-run', category: 'vnext', description: 'Run a search journey for a subject across supported platforms.' },
46
+ { name: 'vnext-blueprints-create', category: 'vnext', description: 'Create a vNext blueprint from grounded evidence.' },
47
+ { name: 'vnext-blueprints-generate', category: 'vnext', description: 'Generate a vNext blueprint from workspace opportunity data.' },
48
+ { name: 'vnext-blueprints-read', category: 'vnext', description: 'Read vNext blueprint history and specific versions.' },
49
+ { name: 'vnext-briefs-create', category: 'vnext', description: 'Create a vNext brief record.' },
50
+ { name: 'vnext-briefs-generate', category: 'vnext', description: 'Generate a vNext brief from a blueprint or opportunity.' },
51
+ { name: 'vnext-briefs-read', category: 'vnext', description: 'Read generated vNext briefs and version history.' },
52
+ { name: 'vnext-intents', category: 'vnext', description: 'List, create, update, or delete vNext intents.' },
53
+ { name: 'vnext-journeys', category: 'vnext', description: 'List journey runs and inspect their latest outputs.' },
54
+ { name: 'vnext-keywords', category: 'vnext', description: 'List, create, update, or delete vNext keywords.' },
55
+ { name: 'vnext-personas', category: 'vnext', description: 'List, create, update, retire, or reactivate vNext personas.' },
56
+ { name: 'vnext-pillars', category: 'vnext', description: 'List, create, update, or delete vNext content pillars.' },
57
+ { name: 'vnext-topics', category: 'vnext', description: 'Manage topics, assignments, queues, and topic suggestions.' },
58
+ { name: 'vnext-topics-auto-tag', category: 'vnext', description: 'Auto-tag keyword and topic assignments with Gemini-assisted review.' },
59
+ ];
26
60
 
27
61
  function loadConfig() {
28
62
  const configPath = process.env.SOCIALSEAL_CONFIG || DEFAULT_CONFIG_PATH;
@@ -76,12 +110,11 @@ function normalizeMethod(method) {
76
110
  return normalized;
77
111
  }
78
112
 
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;
113
+ function parseTimeoutMs(raw, { defaultValue = DEFAULT_TIMEOUT_MS, label = 'timeout' } = {}) {
114
+ if (raw == null || raw === '') return defaultValue;
82
115
  const parsed = Number(raw);
83
116
  if (!Number.isFinite(parsed) || parsed <= 0) {
84
- throw new CliError('Invalid timeout value. Use a positive number of milliseconds.', {
117
+ throw new CliError(`Invalid ${label} value. Use a positive number of milliseconds.`, {
85
118
  code: 'INVALID_TIMEOUT',
86
119
  exitCode: EXIT_CODES.USAGE,
87
120
  });
@@ -89,6 +122,31 @@ function resolveTimeoutMs(opts, config) {
89
122
  return Math.min(parsed, MAX_TIMEOUT_MS);
90
123
  }
91
124
 
125
+ function resolveTimeoutMs(opts, config) {
126
+ const raw = opts.timeout ?? process.env.SOCIALSEAL_TIMEOUT_MS ?? config.timeoutMs;
127
+ return parseTimeoutMs(raw, { defaultValue: DEFAULT_TIMEOUT_MS, label: 'timeout' });
128
+ }
129
+
130
+ function resolveAgentIdleTimeoutMs(opts, config, fallbackTimeoutMs) {
131
+ const explicitIdleTimeout =
132
+ opts.idleTimeout
133
+ ?? process.env.SOCIALSEAL_AGENT_IDLE_TIMEOUT_MS
134
+ ?? config.agentIdleTimeoutMs;
135
+ if (explicitIdleTimeout != null && explicitIdleTimeout !== '') {
136
+ return parseTimeoutMs(explicitIdleTimeout, {
137
+ defaultValue: DEFAULT_AGENT_IDLE_TIMEOUT_MS,
138
+ label: 'idle timeout',
139
+ });
140
+ }
141
+
142
+ const explicitTimeout = opts.timeout ?? process.env.SOCIALSEAL_TIMEOUT_MS ?? config.timeoutMs;
143
+ if (explicitTimeout != null && explicitTimeout !== '') {
144
+ return fallbackTimeoutMs;
145
+ }
146
+
147
+ return DEFAULT_AGENT_IDLE_TIMEOUT_MS;
148
+ }
149
+
92
150
  function resolveLegacyUrl(value, label) {
93
151
  if (!value) return null;
94
152
  if (!LEGACY_ENABLED) {
@@ -101,6 +159,52 @@ function resolveLegacyUrl(value, label) {
101
159
  return value;
102
160
  }
103
161
 
162
+ function emitInfo(opts, message) {
163
+ if (opts?.verbose) {
164
+ process.stderr.write(`[socialseal] ${message}\n`);
165
+ }
166
+ }
167
+
168
+ function formatCloseReason(reason) {
169
+ if (reason == null) return '';
170
+ if (Buffer.isBuffer(reason)) return reason.toString('utf8');
171
+ if (typeof reason === 'string') return reason;
172
+ return String(reason);
173
+ }
174
+
175
+ async function readNodeResponseBody(response, limit = 2000) {
176
+ if (!response) return null;
177
+
178
+ return await new Promise((resolve) => {
179
+ const chunks = [];
180
+ let bufferedBytes = 0;
181
+ let totalBytes = 0;
182
+ let settled = false;
183
+
184
+ const finish = (value) => {
185
+ if (settled) return;
186
+ settled = true;
187
+ resolve(value);
188
+ };
189
+
190
+ response.on('data', (chunk) => {
191
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
192
+ totalBytes += buffer.length;
193
+ if (bufferedBytes >= limit) return;
194
+
195
+ const remaining = limit - bufferedBytes;
196
+ const slice = buffer.subarray(0, remaining);
197
+ chunks.push(slice);
198
+ bufferedBytes += slice.length;
199
+ });
200
+ response.on('end', () => {
201
+ const text = chunks.length > 0 ? Buffer.concat(chunks).toString('utf8') : '';
202
+ finish(totalBytes > limit ? `${text}…` : text);
203
+ });
204
+ response.on('error', () => finish(null));
205
+ });
206
+ }
207
+
104
208
  function parseJsonInput(value, { label = 'payload', allowString = false } = {}) {
105
209
  if (!value) return null;
106
210
  if (value.startsWith('@')) {
@@ -231,7 +335,11 @@ function emitError(err, opts = {}) {
231
335
  process.stderr.write(`[socialseal] ${payload.error.hint}\n`);
232
336
  }
233
337
  if (showDetails && payload.error.details) {
234
- process.stderr.write(`[socialseal] Details: ${payload.error.details}\n`);
338
+ const detailsText =
339
+ typeof payload.error.details === 'string'
340
+ ? payload.error.details
341
+ : JSON.stringify(payload.error.details);
342
+ process.stderr.write(`[socialseal] Details: ${detailsText}\n`);
235
343
  } else if (!showDetails && err.details) {
236
344
  process.stderr.write('[socialseal] Use --verbose to see error details.\n');
237
345
  }
@@ -336,6 +444,7 @@ async function handleAgentRun(opts) {
336
444
  const agentUrl = resolveLegacyUrl(resolveAgentUrl(opts, config), 'SOCIALSEAL_AGENT_URL');
337
445
  const { resolvedApiBase, legacyUrl } = resolveApiTarget({ apiBase, legacyUrl: agentUrl });
338
446
  const timeoutMs = resolveTimeoutMs(opts, config);
447
+ const idleTimeoutMs = resolveAgentIdleTimeoutMs(opts, config, timeoutMs);
339
448
 
340
449
  const headers = {
341
450
  'Content-Type': 'application/json',
@@ -361,6 +470,8 @@ async function handleAgentRun(opts) {
361
470
  }
362
471
 
363
472
  const sessionData = await sessionRes.json();
473
+ const sessionId = sessionData?.data?.sessionId || null;
474
+ const initialConversationId = sessionData?.data?.activeConversationId || opts.conversationId || null;
364
475
  const wsUrl = sessionData?.data?.websocketUrl;
365
476
  if (!wsUrl) {
366
477
  throw new CliError('Missing websocketUrl in session response.', {
@@ -368,6 +479,10 @@ async function handleAgentRun(opts) {
368
479
  exitCode: EXIT_CODES.SERVER,
369
480
  });
370
481
  }
482
+ emitInfo(
483
+ opts,
484
+ `Agent session created${sessionId ? ` (session ${sessionId})` : ''}${initialConversationId ? ` for conversation ${initialConversationId}` : ''}.`,
485
+ );
371
486
 
372
487
  const context = parseJsonInput(opts.context, { label: 'context', allowString: true });
373
488
  const message = opts.message;
@@ -375,10 +490,36 @@ async function handleAgentRun(opts) {
375
490
  await new Promise((resolve, reject) => {
376
491
  const ws = new WebSocket(wsUrl);
377
492
  let finished = false;
493
+ let settled = false;
378
494
  let inactivityTimer = null;
495
+ let sawAssistantChunk = false;
496
+ let sawToolCall = false;
497
+ let sawThinking = false;
498
+ let lastMessageType = 'none';
499
+ let activeConversationId = initialConversationId;
500
+ const toolProgressStatus = new Map();
501
+
502
+ const settleResolve = () => {
503
+ if (settled) return;
504
+ settled = true;
505
+ if (inactivityTimer) clearTimeout(inactivityTimer);
506
+ resolve();
507
+ };
508
+
509
+ const settleReject = (error) => {
510
+ if (settled) return;
511
+ settled = true;
512
+ if (inactivityTimer) clearTimeout(inactivityTimer);
513
+ try {
514
+ ws.terminate();
515
+ } catch {
516
+ // ignore
517
+ }
518
+ reject(error);
519
+ };
379
520
 
380
521
  const resetInactivity = () => {
381
- if (!timeoutMs) return;
522
+ if (!idleTimeoutMs) return;
382
523
  if (inactivityTimer) clearTimeout(inactivityTimer);
383
524
  inactivityTimer = setTimeout(() => {
384
525
  try {
@@ -386,38 +527,74 @@ async function handleAgentRun(opts) {
386
527
  } catch {
387
528
  // ignore
388
529
  }
389
- reject(new CliError('WebSocket timed out waiting for agent response.', {
530
+ settleReject(new CliError('WebSocket timed out waiting for agent response.', {
390
531
  code: 'WEBSOCKET_TIMEOUT',
391
532
  exitCode: EXIT_CODES.SERVER,
392
- hint: 'Increase the timeout with --timeout <ms>.',
533
+ hint: 'Increase the timeout with --idle-timeout <ms> or --timeout <ms>.',
534
+ details: truncateDetails({
535
+ sessionId,
536
+ activeConversationId,
537
+ lastMessageType,
538
+ sawAssistantChunk,
539
+ sawToolCall,
540
+ sawThinking,
541
+ idleTimeoutMs,
542
+ }),
393
543
  }));
394
- }, timeoutMs);
544
+ }, idleTimeoutMs);
395
545
  };
396
546
 
397
547
  ws.on('open', () => {
398
548
  resetInactivity();
549
+ emitInfo(opts, 'Connected to agent WebSocket.');
399
550
  const payload = {
400
551
  type: 'user_message',
401
552
  payload: { content: message, context: context || undefined },
402
553
  timestamp: Date.now(),
403
554
  };
404
555
  ws.send(JSON.stringify(payload));
556
+ emitInfo(opts, 'User message sent to agent.');
405
557
  });
406
558
 
407
559
  ws.on('message', (data) => {
408
560
  try {
409
561
  resetInactivity();
410
562
  const msg = JSON.parse(data.toString());
563
+ lastMessageType = msg.type || 'unknown';
564
+
565
+ if (msg.type === 'session_state' && msg.payload?.activeConversationId) {
566
+ activeConversationId = msg.payload.activeConversationId;
567
+ emitInfo(
568
+ opts,
569
+ `Session state received${sessionId ? ` for session ${sessionId}` : ''}${activeConversationId ? ` (conversation ${activeConversationId})` : ''}.`,
570
+ );
571
+ }
572
+
411
573
  if (opts.json) {
412
574
  process.stdout.write(JSON.stringify(msg) + '\n');
413
575
  if (msg.type === 'assistant_chunk' && msg.payload?.done) {
414
576
  finished = true;
415
577
  ws.close(1000, 'done');
416
578
  }
579
+ if (msg.type === 'error') {
580
+ const payload = msg.payload || {};
581
+ settleReject(new CliError(`Agent error: ${payload.message || 'unknown'}`, {
582
+ code: payload.code || 'AGENT_ERROR',
583
+ exitCode: EXIT_CODES.SERVER,
584
+ hint: payload.retryable ? 'Retry the request or inspect backend status.' : null,
585
+ details: truncateDetails({
586
+ ...payload,
587
+ sessionId,
588
+ activeConversationId,
589
+ lastMessageType,
590
+ }),
591
+ }));
592
+ }
417
593
  return;
418
594
  }
419
595
  if (msg.type === 'assistant_chunk') {
420
596
  const chunk = msg.payload?.chunk ?? '';
597
+ sawAssistantChunk = sawAssistantChunk || chunk.length > 0 || !!msg.payload?.done;
421
598
  if (chunk) process.stdout.write(chunk);
422
599
  if (msg.payload?.done) {
423
600
  finished = true;
@@ -425,28 +602,114 @@ async function handleAgentRun(opts) {
425
602
  ws.close(1000, 'done');
426
603
  }
427
604
  } else if (msg.type === 'error') {
428
- process.stderr.write(`\n[socialseal] Agent error: ${msg.payload?.message || 'unknown'}\n`);
605
+ const payload = msg.payload || {};
606
+ settleReject(new CliError(`Agent error: ${payload.message || 'unknown'}`, {
607
+ code: payload.code || 'AGENT_ERROR',
608
+ exitCode: EXIT_CODES.SERVER,
609
+ hint: payload.retryable ? 'Retry the request or inspect backend status.' : null,
610
+ details: truncateDetails({
611
+ ...payload,
612
+ sessionId,
613
+ activeConversationId,
614
+ lastMessageType,
615
+ }),
616
+ }));
617
+ } else if (msg.type === 'thinking_chunk') {
618
+ sawThinking = true;
619
+ emitInfo(opts, 'Agent is thinking.');
620
+ } else if (msg.type === 'assistant_status') {
621
+ const code = msg.payload?.code || 'unknown';
622
+ const statusMessage = msg.payload?.message || 'Agent reported a status update.';
623
+ emitInfo(opts, `Agent status [${code}]: ${statusMessage}`);
624
+ } else if (msg.type === 'tool_call_start') {
625
+ sawToolCall = true;
626
+ emitInfo(opts, `Tool start: ${msg.payload?.name || 'unknown'}`);
627
+ } else if (msg.type === 'tool_call_progress') {
628
+ const toolCallId = msg.payload?.toolCallId || '';
629
+ const progressStatus = msg.payload?.status || 'running';
630
+ if (toolProgressStatus.get(toolCallId) !== progressStatus) {
631
+ toolProgressStatus.set(toolCallId, progressStatus);
632
+ emitInfo(opts, `Tool progress: ${progressStatus}`);
633
+ }
634
+ } else if (msg.type === 'tool_call_complete') {
635
+ const error = msg.payload?.error;
636
+ const duration = typeof msg.payload?.duration_ms === 'number'
637
+ ? `${msg.payload.duration_ms}ms`
638
+ : 'unknown duration';
639
+ if (error) {
640
+ emitInfo(opts, `Tool failed after ${duration}: ${error}`);
641
+ } else {
642
+ emitInfo(opts, `Tool completed in ${duration}.`);
643
+ }
429
644
  }
430
645
  } catch (err) {
431
- process.stderr.write(`\n[socialseal] Failed to parse agent message: ${err.message || err}\n`);
646
+ settleReject(new CliError(`Failed to parse agent message: ${err.message || err}`, {
647
+ code: 'INVALID_AGENT_MESSAGE',
648
+ exitCode: EXIT_CODES.SERVER,
649
+ details: data.toString(),
650
+ }));
432
651
  }
433
652
  });
434
653
 
435
- ws.on('close', () => {
436
- if (inactivityTimer) clearTimeout(inactivityTimer);
654
+ ws.on('unexpected-response', async (_req, response) => {
655
+ const statusText = response.statusCode
656
+ ? `${response.statusCode}${response.statusMessage ? ` ${response.statusMessage}` : ''}`
657
+ : 'unknown';
658
+ const details = await readNodeResponseBody(response);
659
+ settleReject(new CliError(`WebSocket upgrade failed: ${statusText}`.trim(), {
660
+ code: 'WEBSOCKET_UPGRADE_FAILED',
661
+ exitCode:
662
+ response.statusCode === 401 || response.statusCode === 403
663
+ ? EXIT_CODES.AUTH
664
+ : EXIT_CODES.SERVER,
665
+ hint:
666
+ response.statusCode === 401 || response.statusCode === 403
667
+ ? 'Check your CLI key, workspace scope, and session endpoint auth.'
668
+ : 'Retry with --verbose to inspect gateway or backend behavior.',
669
+ details: truncateDetails({
670
+ sessionId,
671
+ activeConversationId,
672
+ responseBody: details,
673
+ }),
674
+ }));
675
+ });
676
+
677
+ ws.on('close', (code, reason) => {
678
+ const closeReason = formatCloseReason(reason);
437
679
  if (!finished) {
438
- reject(new CliError('WebSocket closed before completion.', {
439
- code: 'WEBSOCKET_CLOSED',
440
- exitCode: EXIT_CODES.SERVER,
441
- }));
680
+ settleReject(new CliError(
681
+ `WebSocket closed before completion (code ${code}${closeReason ? `: ${closeReason}` : ''}).`,
682
+ {
683
+ code: 'WEBSOCKET_CLOSED',
684
+ exitCode: EXIT_CODES.SERVER,
685
+ hint: sawAssistantChunk
686
+ ? 'The agent disconnected mid-response. Retry the request.'
687
+ : 'The agent closed the connection before completing. Retry with --verbose for more diagnostics.',
688
+ details: truncateDetails({
689
+ sessionId,
690
+ activeConversationId,
691
+ lastMessageType,
692
+ sawAssistantChunk,
693
+ sawToolCall,
694
+ sawThinking,
695
+ }),
696
+ },
697
+ ));
442
698
  } else {
443
- resolve();
699
+ settleResolve();
444
700
  }
445
701
  });
446
702
 
447
703
  ws.on('error', (err) => {
448
- if (inactivityTimer) clearTimeout(inactivityTimer);
449
- reject(err);
704
+ settleReject(new CliError(`WebSocket error: ${err.message || err}`, {
705
+ code: 'WEBSOCKET_ERROR',
706
+ exitCode: EXIT_CODES.SERVER,
707
+ details: truncateDetails({
708
+ sessionId,
709
+ activeConversationId,
710
+ lastMessageType,
711
+ }),
712
+ }));
450
713
  });
451
714
  });
452
715
  }
@@ -492,8 +755,9 @@ async function handleToolsCall(opts) {
492
755
 
493
756
  function handleToolsList(opts) {
494
757
  const payload = {
758
+ discovery: 'built_in_registry',
495
759
  tools: KNOWN_TOOLS,
496
- note: 'Tool discovery is disabled in the OSS CLI. Refer to official docs for supported tool names.',
760
+ note: 'This registry is shipped with the CLI for stable discovery. It is not live backend enumeration.',
497
761
  };
498
762
 
499
763
  if (opts.json) {
@@ -501,8 +765,19 @@ function handleToolsList(opts) {
501
765
  return;
502
766
  }
503
767
 
504
- process.stdout.write('[socialseal] Tool discovery is disabled in the OSS CLI.\n');
768
+ process.stdout.write('[socialseal] Built-in tool registry\n');
505
769
  process.stdout.write(`[socialseal] ${payload.note}\n`);
770
+
771
+ let currentCategory = null;
772
+ for (const tool of KNOWN_TOOLS) {
773
+ if (tool.category !== currentCategory) {
774
+ currentCategory = tool.category;
775
+ process.stdout.write(`\n${currentCategory}\n`);
776
+ }
777
+ process.stdout.write(`- ${tool.name}: ${tool.description}\n`);
778
+ }
779
+
780
+ process.stdout.write('\n[socialseal] Call a tool with: socialseal tools call --function <name> --body @payload.json\n');
506
781
  }
507
782
 
508
783
  async function handleDataExportTracking(opts) {
@@ -540,6 +815,13 @@ async function handleDataExportTracking(opts) {
540
815
  throw await buildHttpError(res, { label: 'Tracking export' });
541
816
  }
542
817
 
818
+ if (!res.body) {
819
+ throw new CliError('Export response contained no body.', {
820
+ code: 'EMPTY_RESPONSE',
821
+ exitCode: EXIT_CODES.SERVER,
822
+ });
823
+ }
824
+
543
825
  const outPath = opts.stdout ? null : (opts.out || 'tracking_export.csv');
544
826
  if (outPath) {
545
827
  await pipeline(res.body, fs.createWriteStream(outPath));
@@ -615,7 +897,7 @@ const program = new Command();
615
897
  program
616
898
  .name('socialseal')
617
899
  .description('SocialSeal CLI (non-interactive)')
618
- .version('0.1.0');
900
+ .version('0.1.1');
619
901
 
620
902
  if (typeof program.showHelpAfterError === 'function') {
621
903
  program.showHelpAfterError(true);
@@ -638,6 +920,7 @@ program
638
920
  .option('--create-new', 'Create a new conversation')
639
921
  .option('--json', 'Emit NDJSON events')
640
922
  .option('--timeout <ms>', 'Request timeout in milliseconds')
923
+ .option('--idle-timeout <ms>', 'WebSocket inactivity timeout in milliseconds')
641
924
  .option('--verbose', 'Show error details')
642
925
  .action((opts) => runCommand(handleAgentRun, opts));
643
926
 
@@ -645,7 +928,7 @@ const tools = program.command('tools').description('Call edge functions directly
645
928
 
646
929
  tools
647
930
  .command('list')
648
- .description('List tools (discovery disabled in OSS build)')
931
+ .description('List built-in tool registry entries')
649
932
  .option('--json', 'Emit machine-readable output')
650
933
  .option('--pretty', 'Pretty-print JSON')
651
934
  .option('--verbose', 'Show error details')