@eightstate/escli 0.5.0

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.
Files changed (54) hide show
  1. package/CONVENTIONS.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +106 -0
  4. package/RELEASE-NOTES-0.5.0.md +34 -0
  5. package/dist/base-command.js +166 -0
  6. package/dist/commands/audio/get.js +39 -0
  7. package/dist/commands/audio/index.js +18 -0
  8. package/dist/commands/audio/list.js +39 -0
  9. package/dist/commands/audio/status.js +34 -0
  10. package/dist/commands/audio/transcribe.js +99 -0
  11. package/dist/commands/auth/index.js +18 -0
  12. package/dist/commands/auth/login.js +38 -0
  13. package/dist/commands/auth/logout.js +27 -0
  14. package/dist/commands/auth/profiles.js +31 -0
  15. package/dist/commands/auth/status.js +27 -0
  16. package/dist/commands/auth/switch.js +24 -0
  17. package/dist/commands/docs/fetch.js +37 -0
  18. package/dist/commands/docs/get.js +47 -0
  19. package/dist/commands/docs/index.js +18 -0
  20. package/dist/commands/docs/search.js +55 -0
  21. package/dist/commands/fetch.js +55 -0
  22. package/dist/commands/image/edit.js +59 -0
  23. package/dist/commands/image/generate.js +67 -0
  24. package/dist/commands/image/index.js +18 -0
  25. package/dist/commands/models.js +27 -0
  26. package/dist/commands/research.js +92 -0
  27. package/dist/commands/search.js +54 -0
  28. package/dist/commands/social.js +69 -0
  29. package/dist/commands/usage.js +51 -0
  30. package/dist/commands/version.js +22 -0
  31. package/dist/entry.js +120 -0
  32. package/dist/io/io.js +322 -0
  33. package/dist/lib/build-flags.js +2 -0
  34. package/dist/lib/command-metadata.js +8 -0
  35. package/dist/lib/envelope.js +28 -0
  36. package/dist/lib/escli-error.js +20 -0
  37. package/dist/lib/global-flags.js +29 -0
  38. package/dist/lib/globals.js +2 -0
  39. package/dist/lib/manifest.js +67 -0
  40. package/dist/lib/oclif-manifest-check.js +11 -0
  41. package/dist/lib/registry.js +228 -0
  42. package/dist/services/audio.js +454 -0
  43. package/dist/services/auth.js +329 -0
  44. package/dist/services/credentials.js +137 -0
  45. package/dist/services/docs.js +303 -0
  46. package/dist/services/fetch.js +197 -0
  47. package/dist/services/image.js +297 -0
  48. package/dist/services/models.js +131 -0
  49. package/dist/services/research.js +504 -0
  50. package/dist/services/search.js +195 -0
  51. package/dist/services/social.js +224 -0
  52. package/dist/services/usage.js +165 -0
  53. package/oclif.manifest.json +3377 -0
  54. package/package.json +57 -0
@@ -0,0 +1,504 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { ErrorCode } from '@eightstate/contracts/errors';
6
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
7
+ import { EscliError } from '../lib/escli-error.js';
8
+ import { resolveCliToken } from './credentials.js';
9
+ const API_BASE = 'https://api.parallel.ai';
10
+ const DEFAULT_MAX_POLL_MS = 7_200_000;
11
+ const DEFAULT_POLL_INTERVAL_MS = 10_000;
12
+ const DEFAULT_TIMEOUT_MS = 60_000;
13
+ const STICKY_VEND_ATTEMPTS = 6;
14
+ export const PROCESSORS = [
15
+ 'lite', 'base', 'core', 'core2x', 'pro', 'ultra', 'ultra2x', 'ultra4x', 'ultra8x',
16
+ 'lite-fast', 'base-fast', 'core-fast', 'core2x-fast', 'pro-fast', 'ultra-fast', 'ultra2x-fast', 'ultra4x-fast', 'ultra8x-fast',
17
+ ];
18
+ export function listResearchProcessors() {
19
+ const processors = PROCESSORS.map((name) => ({ name }));
20
+ return { processors, count: processors.length };
21
+ }
22
+ export async function runResearch(options) {
23
+ const body = await buildTaskRequest(options);
24
+ const captured = {};
25
+ const task = await submitTask(body, captured, options.timeoutSeconds);
26
+ const runId = task.run_id;
27
+ if (!runId)
28
+ throw new EscliError('task creation response missing run_id', { code: ErrorCode.GateInvalidResponse });
29
+ if (captured.key)
30
+ await rememberRunOwner(runId, captured.key.fingerprint);
31
+ const apiKey = captured.key?.apiKey ?? await getResearchApiKeyForRun(runId);
32
+ const output = await pollUntilComplete(apiKey, runId, options.timeoutSeconds);
33
+ const data = { run_id: runId, processor: options.processor, output };
34
+ if (options.output) {
35
+ data.path = resolve(options.output);
36
+ await writeResearchOutput(data.path, options.query, data.processor, runId, output, task.created_at ?? '', options.noBasis);
37
+ }
38
+ return data;
39
+ }
40
+ export async function getResearchStatus(options) {
41
+ const apiKey = await getResearchApiKeyForRun(options.runId);
42
+ const result = await apiRequest('GET', `/v1/tasks/runs/${options.runId}`, apiKey, undefined, undefined, options.timeoutSeconds);
43
+ return {
44
+ run_id: stringValue(result.run_id) ?? options.runId,
45
+ status: stringValue(result.status) ?? 'unknown',
46
+ error: result.error,
47
+ warnings: Array.isArray(result.warnings) ? result.warnings : undefined,
48
+ };
49
+ }
50
+ export async function getResearchResult(options) {
51
+ const apiKey = await getResearchApiKeyForRun(options.runId);
52
+ const run = await apiRequest('GET', `/v1/tasks/runs/${options.runId}`, apiKey, undefined, undefined, options.timeoutSeconds);
53
+ if (run.status !== 'completed') {
54
+ throw new EscliError(`task not complete: ${String(run.status ?? 'unknown')}`, {
55
+ code: ErrorCode.ResearchTaskFailed,
56
+ details: { status: run.status, error: run.error },
57
+ });
58
+ }
59
+ const result = await apiRequest('GET', `/v1/tasks/runs/${options.runId}/result`, apiKey, undefined, undefined, options.timeoutSeconds);
60
+ const output = normalizeOutput(result.output);
61
+ const data = { run_id: options.runId, output };
62
+ if (options.output) {
63
+ data.path = resolve(options.output);
64
+ await writeResearchOutput(data.path, '(retrieved)', stringValue(run.processor) ?? '', options.runId, output, stringValue(run.created_at) ?? '', options.noBasis);
65
+ }
66
+ return data;
67
+ }
68
+ async function buildTaskRequest(options) {
69
+ const body = {
70
+ processor: options.processor,
71
+ input: options.query,
72
+ };
73
+ if (options.inputJson)
74
+ body.input = parseJsonInput(options.inputJson, '--input-json must be valid JSON');
75
+ if (options.inputFile)
76
+ body.input = await readJsonFile(options.inputFile, 'input file not found');
77
+ if (options.schema) {
78
+ body.task_spec = { output_schema: { type: 'json', json_schema: await readJsonFile(options.schema, 'schema file not found') } };
79
+ }
80
+ else if (options.outputSchema) {
81
+ body.task_spec = { output_schema: options.outputSchema };
82
+ }
83
+ else if (options.text) {
84
+ body.task_spec = { output_schema: { type: 'text' } };
85
+ }
86
+ const sourcePolicy = {};
87
+ if (options.includeDomains)
88
+ sourcePolicy.include_domains = splitCsv(options.includeDomains);
89
+ if (options.excludeDomains)
90
+ sourcePolicy.exclude_domains = splitCsv(options.excludeDomains);
91
+ if (options.afterDate)
92
+ sourcePolicy.after_date = options.afterDate;
93
+ if (Object.keys(sourcePolicy).length > 0)
94
+ body.source_policy = sourcePolicy;
95
+ if (options.location)
96
+ body.advanced_settings = { location: options.location };
97
+ if (options.metadata)
98
+ body.metadata = parseMetadata(options.metadata);
99
+ if (options.followUp)
100
+ body.previous_interaction_id = options.followUp;
101
+ return body;
102
+ }
103
+ async function submitTask(body, captured, timeoutSeconds) {
104
+ const explicitKey = process.env.PARALLEL_API_KEY;
105
+ if (explicitKey) {
106
+ captured.key = { apiKey: explicitKey, fingerprint: 'env' };
107
+ return apiRequest('POST', '/v1/tasks/runs', explicitKey, body, undefined, timeoutSeconds);
108
+ }
109
+ let lastRetryableError;
110
+ for (let attempt = 0; attempt < 3; attempt += 1) {
111
+ const key = await vendParallelKey();
112
+ if (!key)
113
+ break;
114
+ captured.key = key;
115
+ try {
116
+ return await apiRequest('POST', '/v1/tasks/runs', key.apiKey, body, undefined, timeoutSeconds);
117
+ }
118
+ catch (error) {
119
+ if (error instanceof EscliError && (error.code === ErrorCode.ApiRateLimited || error.code === ErrorCode.ServiceUnavailable)) {
120
+ await reportParallelKey(key.fingerprint, error.code === ErrorCode.ApiRateLimited ? 429 : 402);
121
+ lastRetryableError = error;
122
+ continue;
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+ if (lastRetryableError)
128
+ throw lastRetryableError;
129
+ throw new EscliError('no Parallel API key. Set PARALLEL_API_KEY or add one via the dashboard.', { code: ErrorCode.ResearchApiKeyMissing });
130
+ }
131
+ async function getResearchApiKeyForRun(runId) {
132
+ if (process.env.PARALLEL_API_KEY)
133
+ return process.env.PARALLEL_API_KEY;
134
+ const target = recallRunOwner(runId);
135
+ let lastKey;
136
+ for (let attempt = 0; attempt < STICKY_VEND_ATTEMPTS; attempt += 1) {
137
+ const key = await vendParallelKey();
138
+ if (!key)
139
+ break;
140
+ lastKey = key.apiKey;
141
+ if (!target || key.fingerprint === target)
142
+ return lastKey;
143
+ }
144
+ if (lastKey)
145
+ return lastKey;
146
+ throw new EscliError('no Parallel API key. Set PARALLEL_API_KEY or add one via the dashboard.', { code: ErrorCode.ResearchApiKeyMissing });
147
+ }
148
+ async function reportParallelKey(fingerprint, status) {
149
+ const token = resolveCliToken();
150
+ if (!token)
151
+ return;
152
+ try {
153
+ await fetch(`${gateUrl()}/api/keys/report`, {
154
+ method: 'POST',
155
+ headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' },
156
+ body: JSON.stringify({ service: 'parallel', fingerprint, status }),
157
+ });
158
+ }
159
+ catch {
160
+ // Gate reporting is best-effort.
161
+ }
162
+ }
163
+ async function vendParallelKey() {
164
+ const token = resolveCliToken();
165
+ if (!token)
166
+ return null;
167
+ try {
168
+ const response = await fetch(`${gateUrl()}/api/keys/vend`, {
169
+ method: 'POST',
170
+ headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' },
171
+ body: JSON.stringify({ service: 'parallel' }),
172
+ });
173
+ if (response.status !== 200)
174
+ return null;
175
+ const data = recordValue(await response.json());
176
+ if (data?.success !== true)
177
+ return null;
178
+ const apiKey = stringValue(data.api_key);
179
+ const fingerprint = stringValue(data.fingerprint);
180
+ if (!apiKey || !fingerprint)
181
+ return null;
182
+ return { apiKey, fingerprint, baseUrl: stringValue(data.base_url) };
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ async function apiRequest(method, path, apiKey, body, extraHeaders, timeoutSeconds) {
189
+ const responseText = await requestText(`${parallelBaseUrl()}${path}`, {
190
+ method,
191
+ headers: { 'content-type': 'application/json', 'x-api-key': apiKey, ...(extraHeaders ?? {}) },
192
+ body: body === undefined ? undefined : JSON.stringify(body),
193
+ timeoutMs: timeoutMs(timeoutSeconds),
194
+ });
195
+ return parseJsonResponse(responseText);
196
+ }
197
+ async function requestText(url, options) {
198
+ const controller = new AbortController();
199
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
200
+ try {
201
+ const response = await fetch(url, {
202
+ method: options.method,
203
+ headers: options.headers,
204
+ body: options.body,
205
+ redirect: 'follow',
206
+ signal: controller.signal,
207
+ });
208
+ const text = await response.text();
209
+ if (!response.ok)
210
+ throw httpStatusError(response.status, formatHttpError(response.status, text), text);
211
+ return text;
212
+ }
213
+ catch (error) {
214
+ if (isHttpStatusError(error))
215
+ throw mapHttpStatusError(error);
216
+ throw mapNetworkError(error);
217
+ }
218
+ finally {
219
+ clearTimeout(timeout);
220
+ }
221
+ }
222
+ async function pollUntilComplete(apiKey, runId, timeoutSeconds) {
223
+ const started = Date.now();
224
+ const maxMs = pollMaxMs(timeoutSeconds);
225
+ while (Date.now() - started < maxMs) {
226
+ const result = await apiRequest('GET', `/v1/tasks/runs/${runId}`, apiKey, undefined, undefined, timeoutSeconds);
227
+ const status = result.status;
228
+ if (status === 'completed') {
229
+ const output = await apiRequest('GET', `/v1/tasks/runs/${runId}/result`, apiKey, undefined, undefined, timeoutSeconds);
230
+ return normalizeOutput(output.output);
231
+ }
232
+ if (status === 'failed' || status === 'cancelled') {
233
+ throw new EscliError(`task ${String(status)}: ${String(result.error ?? '')}`, { code: ErrorCode.ResearchTaskFailed, details: result.error });
234
+ }
235
+ await sleep(pollIntervalMs());
236
+ }
237
+ throw new EscliError('research task timed out', { code: ErrorCode.ResearchTimeout });
238
+ }
239
+ async function writeResearchOutput(path, query, processor, runId, output, createdAt, noBasis) {
240
+ try {
241
+ await mkdir(dirname(path), { recursive: true });
242
+ await writeFile(path, formatMarkdown(query, processor, runId, output, createdAt, !noBasis), 'utf8');
243
+ }
244
+ catch (error) {
245
+ throw new EscliError(`failed to write output file: ${error instanceof Error ? error.message : String(error)}`, { code: ErrorCode.FileWriteFailed });
246
+ }
247
+ }
248
+ export function formatMarkdown(query, processor, runId, output, createdAt, includeBasis = true) {
249
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/u, 'Z');
250
+ const lines = [
251
+ '---', `query: "${query}"`, `processor: ${processor}`, `run_id: ${runId}`, `created_at: ${createdAt}`, `retrieved_at: ${now}`, '---', '', `# Research: ${query}`, '',
252
+ ];
253
+ if (output === null) {
254
+ lines.push('*No output returned.*');
255
+ return lines.join('\n');
256
+ }
257
+ const content = output.content;
258
+ const basis = Array.isArray(output.basis) ? output.basis : [];
259
+ if (typeof content === 'string') {
260
+ const parsed = parseMaybeJson(content);
261
+ if (recordValue(parsed))
262
+ renderDict(lines, parsed);
263
+ else
264
+ lines.push(content);
265
+ }
266
+ else if (recordValue(content)) {
267
+ renderDict(lines, content);
268
+ }
269
+ else if (content !== undefined && content !== null) {
270
+ lines.push(String(content));
271
+ }
272
+ if (includeBasis && basis.length > 0) {
273
+ lines.push('', '## Research Basis', '');
274
+ for (const item of basis) {
275
+ const entry = recordValue(item) ?? {};
276
+ const field = stringValue(entry.field) ?? 'unknown';
277
+ const reasoning = stringValue(entry.reasoning) ?? '';
278
+ const confidence = stringValue(entry.confidence) ?? '';
279
+ const citations = Array.isArray(entry.citations) ? entry.citations : [];
280
+ lines.push(`### ${field}`);
281
+ if (confidence)
282
+ lines.push(`**Confidence:** ${confidence}`);
283
+ if (reasoning)
284
+ lines.push('', reasoning);
285
+ if (citations.length > 0)
286
+ lines.push('');
287
+ for (const citation of citations) {
288
+ const cite = recordValue(citation) ?? {};
289
+ const url = stringValue(cite.url) ?? '';
290
+ const title = stringValue(cite.title) ?? url;
291
+ lines.push(`- [${title}](${url})`);
292
+ const excerpts = Array.isArray(cite.excerpts) ? cite.excerpts : [];
293
+ for (const excerpt of excerpts)
294
+ lines.push(` > ${String(excerpt)}`);
295
+ }
296
+ lines.push('');
297
+ }
298
+ }
299
+ return lines.join('\n');
300
+ }
301
+ function renderDict(lines, data) {
302
+ for (const [key, value] of Object.entries(data)) {
303
+ lines.push(`## ${titleCase(key)}`, '');
304
+ if (typeof value === 'string')
305
+ lines.push(value);
306
+ else if (Array.isArray(value)) {
307
+ if (value.length > 0 && value.every((item) => Boolean(recordValue(item))))
308
+ renderTable(lines, value);
309
+ else
310
+ for (const item of value)
311
+ lines.push(`- ${String(item)}`);
312
+ }
313
+ else if (recordValue(value)) {
314
+ for (const [nestedKey, nestedValue] of Object.entries(value))
315
+ lines.push(`**${titleCase(nestedKey)}:** ${String(nestedValue)}`);
316
+ }
317
+ else {
318
+ lines.push(String(value));
319
+ }
320
+ lines.push('');
321
+ }
322
+ }
323
+ function renderTable(lines, items) {
324
+ const keys = [...new Set(items.flatMap((item) => Object.keys(item)))];
325
+ lines.push(`| ${keys.map(titleCase).join(' | ')} |`);
326
+ lines.push(`| ${keys.map(() => '---').join(' | ')} |`);
327
+ for (const item of items)
328
+ lines.push(`| ${keys.map((key) => String(item[key] ?? '').replace(/\n/gu, ' ').replace(/\|/gu, '\\|')).join(' | ')} |`);
329
+ }
330
+ function titleCase(value) {
331
+ return value.replaceAll('_', ' ').replace(/\b\w/gu, (char) => char.toUpperCase());
332
+ }
333
+ function parseMetadata(value) {
334
+ const parsed = parseMaybeJson(value);
335
+ if (recordValue(parsed))
336
+ return parsed;
337
+ const metadata = {};
338
+ for (const pair of value.split(',')) {
339
+ const index = pair.indexOf('=');
340
+ if (index > 0)
341
+ metadata[pair.slice(0, index).trim()] = pair.slice(index + 1).trim();
342
+ }
343
+ return metadata;
344
+ }
345
+ function parseJsonInput(value, message) {
346
+ try {
347
+ return JSON.parse(value);
348
+ }
349
+ catch (error) {
350
+ throw new EscliError(message, { code: ErrorCode.ResearchInputInvalid, details: error instanceof Error ? error.message : String(error) });
351
+ }
352
+ }
353
+ async function readJsonFile(path, missingMessage) {
354
+ let text;
355
+ try {
356
+ text = await readFile(path, 'utf8');
357
+ }
358
+ catch (error) {
359
+ const code = error.code === 'ENOENT' ? ErrorCode.FileNotFound : ErrorCode.FileReadFailed;
360
+ const exitCode = code === ErrorCode.FileNotFound ? ExitCodes.Usage : undefined;
361
+ throw new EscliError(`${missingMessage}: ${path}`, { code, exitCode, details: error instanceof Error ? error.message : String(error) });
362
+ }
363
+ return parseJsonInput(text, `${path} must contain valid JSON`);
364
+ }
365
+ function parseJsonResponse(text) {
366
+ try {
367
+ return JSON.parse(text);
368
+ }
369
+ catch (error) {
370
+ throw new EscliError(`invalid JSON response: ${error instanceof Error ? error.message : String(error)}`, {
371
+ code: ErrorCode.GateInvalidResponse,
372
+ exitCode: ExitCodes.Error,
373
+ details: text,
374
+ });
375
+ }
376
+ }
377
+ function normalizeOutput(value) {
378
+ if (value === null)
379
+ return null;
380
+ const record = recordValue(value);
381
+ if (!record)
382
+ return { content: value };
383
+ return { content: record.content, basis: Array.isArray(record.basis) ? record.basis : undefined };
384
+ }
385
+ function splitCsv(value) {
386
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
387
+ }
388
+ function parallelBaseUrl() {
389
+ if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_PARALLEL_URL)
390
+ return process.env.ESCLI_TEST_PARALLEL_URL;
391
+ return API_BASE;
392
+ }
393
+ function gateUrl() {
394
+ return process.env.ESCLI_GATE_URL ?? 'https://internal.eightstate.co';
395
+ }
396
+ function configDir() {
397
+ return process.env.ESCLI_CONFIG_DIR ?? join(homedir(), '.escli');
398
+ }
399
+ function ownersPath() {
400
+ return join(configDir(), 'parallel-run-owners.json');
401
+ }
402
+ function recallRunOwner(runId) {
403
+ try {
404
+ const owners = recordValue(JSON.parse(readFileSync(ownersPath(), 'utf8')));
405
+ return stringValue(owners?.[runId]);
406
+ }
407
+ catch {
408
+ return undefined;
409
+ }
410
+ }
411
+ async function rememberRunOwner(runId, fingerprint) {
412
+ if (!runId || !fingerprint || fingerprint === 'env')
413
+ return;
414
+ try {
415
+ await mkdir(configDir(), { recursive: true });
416
+ let owners = {};
417
+ try {
418
+ owners = recordValue(JSON.parse(readFileSync(ownersPath(), 'utf8'))) ?? {};
419
+ }
420
+ catch {
421
+ owners = {};
422
+ }
423
+ owners[runId] = fingerprint;
424
+ const entries = Object.entries(owners).slice(-500);
425
+ await writeFile(ownersPath(), JSON.stringify(Object.fromEntries(entries)), 'utf8');
426
+ }
427
+ catch {
428
+ // Owner cache is a best-effort parity affordance; task success must not depend on it.
429
+ }
430
+ }
431
+ function pollIntervalMs() {
432
+ return Math.max(1, Number(process.env.ESCLI_RESEARCH_POLL_INTERVAL_MS ?? DEFAULT_POLL_INTERVAL_MS));
433
+ }
434
+ function pollMaxMs(timeoutSeconds) {
435
+ if (typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) {
436
+ const value = Number(process.env.ESCLI_RESEARCH_MAX_POLL_MS);
437
+ if (Number.isFinite(value) && value > 0)
438
+ return Math.max(1, value);
439
+ }
440
+ return timeoutSeconds ? Math.max(1, timeoutSeconds * 1000) : DEFAULT_MAX_POLL_MS;
441
+ }
442
+ function timeoutMs(timeoutSeconds, defaultMs = DEFAULT_TIMEOUT_MS) {
443
+ return Math.max(1, timeoutSeconds ? timeoutSeconds * 1000 : defaultMs);
444
+ }
445
+ function sleep(ms) {
446
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
447
+ }
448
+ function httpStatusError(status, message, details) {
449
+ const error = new Error(message);
450
+ error.status = status;
451
+ error.details = details;
452
+ return error;
453
+ }
454
+ function isHttpStatusError(error) {
455
+ return error instanceof Error && typeof error.status === 'number';
456
+ }
457
+ function mapHttpStatusError(error) {
458
+ const status = error.status;
459
+ if (status === 401)
460
+ return new EscliError(error.message, { code: ErrorCode.ApiUnauthorized, details: error.details });
461
+ if (status === 403)
462
+ return new EscliError(error.message, { code: ErrorCode.ApiForbidden, details: error.details });
463
+ if (status === 404)
464
+ return new EscliError(error.message, { code: ErrorCode.ResearchRunNotFound, details: error.details });
465
+ if (status === 429)
466
+ return new EscliError(error.message, { code: ErrorCode.ApiRateLimited, details: error.details });
467
+ if (status >= 500)
468
+ return new EscliError(error.message, { code: ErrorCode.ServiceUnavailable, details: error.details });
469
+ return new EscliError(error.message, { code: ErrorCode.ResearchTaskFailed, details: error.details });
470
+ }
471
+ function mapNetworkError(error) {
472
+ const isAbort = error instanceof Error && error.name === 'AbortError';
473
+ return new EscliError(isAbort ? 'network error: request timed out' : `network error: ${error instanceof Error ? error.message : String(error)}`, {
474
+ code: isAbort ? ErrorCode.NetworkTimeout : ErrorCode.NetworkError,
475
+ details: error instanceof Error ? error.message : error,
476
+ });
477
+ }
478
+ function formatHttpError(status, body) {
479
+ const parsed = parseMaybeJson(body);
480
+ const extracted = extractErrorMessage(parsed) ?? (typeof parsed === 'string' ? parsed : undefined);
481
+ return extracted ? `API error (${status}): ${extracted}` : `API error (${status})`;
482
+ }
483
+ function parseMaybeJson(value) {
484
+ if (typeof value !== 'string')
485
+ return value;
486
+ try {
487
+ return JSON.parse(value);
488
+ }
489
+ catch {
490
+ return value;
491
+ }
492
+ }
493
+ function extractErrorMessage(body) {
494
+ const root = recordValue(body);
495
+ const error = recordValue(root?.error);
496
+ return stringValue(root?.error) ?? stringValue(root?.message) ?? stringValue(error?.message) ?? stringValue(error?.detail);
497
+ }
498
+ function recordValue(value) {
499
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
500
+ }
501
+ function stringValue(value) {
502
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
503
+ }
504
+ //# sourceMappingURL=research.js.map
@@ -0,0 +1,195 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ErrorCode } from '@eightstate/contracts/errors';
3
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
4
+ import { EscliError } from '../lib/escli-error.js';
5
+ import { callWithRetry } from './credentials.js';
6
+ const REST_ENDPOINT = 'https://api.parallel.ai/v1';
7
+ const MCP_ENDPOINT = 'https://search.parallel.ai/mcp';
8
+ const DEFAULT_TIMEOUT_MS = 60_000;
9
+ export async function searchWeb(options) {
10
+ const started = Date.now();
11
+ const queries = options.queries && options.queries.length > 0 ? options.queries : [options.objective];
12
+ const payload = await searchPayload({ ...options, queries });
13
+ const results = normalizeResults(recordValue(payload)?.results);
14
+ return {
15
+ elapsed_seconds: roundElapsed((Date.now() - started) / 1000),
16
+ results,
17
+ count: results.length,
18
+ };
19
+ }
20
+ async function searchPayload(options) {
21
+ const explicitKey = process.env.PARALLEL_API_KEY;
22
+ if (explicitKey)
23
+ return restSearch(options, { apiKey: explicitKey, fingerprint: 'env' });
24
+ try {
25
+ const response = await callWithRetry('parallel', (key) => restSearch(options, key));
26
+ if (response !== null)
27
+ return response;
28
+ }
29
+ catch (error) {
30
+ if (!(error instanceof Error && error.name === 'ServiceCallError'))
31
+ throw error;
32
+ }
33
+ return mcpSearch(options);
34
+ }
35
+ async function restSearch(options, key) {
36
+ const response = await requestText(`${parallelBaseUrl()}/v1/search`, {
37
+ method: 'POST',
38
+ headers: { 'content-type': 'application/json', 'x-api-key': key.apiKey },
39
+ body: JSON.stringify({ objective: options.objective, search_queries: options.queries }),
40
+ timeoutMs: timeoutMs(options.timeoutSeconds),
41
+ });
42
+ return parseJsonResponse(response);
43
+ }
44
+ async function mcpSearch(options) {
45
+ const args = { objective: options.objective, search_queries: options.queries, session_id: randomUUID().replaceAll('-', '') };
46
+ const response = await requestText(parallelMcpUrl(), {
47
+ method: 'POST',
48
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
49
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'web_search', arguments: args } }),
50
+ timeoutMs: timeoutMs(options.timeoutSeconds),
51
+ });
52
+ const data = recordValue(parseJsonResponse(response));
53
+ const error = recordValue(data?.error);
54
+ if (error)
55
+ throw new EscliError(stringValue(error.message) ?? 'MCP error', { code: ErrorCode.SearchFailed });
56
+ const result = recordValue(data?.result);
57
+ const content = arrayValue(result?.content);
58
+ for (const item of content) {
59
+ const record = recordValue(item);
60
+ if (record?.type === 'text' && typeof record.text === 'string')
61
+ return parseMcpTextContent(record.text);
62
+ }
63
+ return result ?? data;
64
+ }
65
+ async function requestText(url, options) {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
68
+ try {
69
+ const response = await fetch(url, {
70
+ method: options.method,
71
+ headers: options.headers,
72
+ body: options.body,
73
+ redirect: 'follow',
74
+ signal: controller.signal,
75
+ });
76
+ const text = await response.text();
77
+ if (!response.ok)
78
+ throw httpStatusError(response.status, formatHttpError(response.status, text), text);
79
+ return text;
80
+ }
81
+ catch (error) {
82
+ if (isHttpStatusError(error))
83
+ throw mapHttpStatusError(error);
84
+ throw mapNetworkError(error);
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ function parseJsonResponse(text) {
91
+ try {
92
+ return JSON.parse(text);
93
+ }
94
+ catch (error) {
95
+ throw new EscliError(`invalid JSON response: ${error instanceof Error ? error.message : String(error)}`, {
96
+ code: ErrorCode.GateInvalidResponse,
97
+ exitCode: ExitCodes.Error,
98
+ details: text,
99
+ });
100
+ }
101
+ }
102
+ function parseMcpTextContent(text) {
103
+ try {
104
+ return JSON.parse(text);
105
+ }
106
+ catch {
107
+ return { text };
108
+ }
109
+ }
110
+ function normalizeResults(results) {
111
+ if (!Array.isArray(results))
112
+ return [];
113
+ return results
114
+ .map((item) => recordValue(item))
115
+ .filter((item) => Boolean(item))
116
+ .map((item) => ({
117
+ title: String(item.title ?? ''),
118
+ url: String(item.url ?? ''),
119
+ excerpts: Array.isArray(item.excerpts) ? item.excerpts.map(String) : [],
120
+ }));
121
+ }
122
+ function parallelBaseUrl() {
123
+ if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_PARALLEL_URL)
124
+ return process.env.ESCLI_TEST_PARALLEL_URL;
125
+ return REST_ENDPOINT.replace(/\/v1$/u, '');
126
+ }
127
+ function parallelMcpUrl() {
128
+ if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_PARALLEL_URL)
129
+ return `${process.env.ESCLI_TEST_PARALLEL_URL}/mcp`;
130
+ return MCP_ENDPOINT;
131
+ }
132
+ function timeoutMs(timeoutSeconds) {
133
+ const seconds = timeoutSeconds ?? DEFAULT_TIMEOUT_MS / 1000;
134
+ return Math.max(1, seconds) * 1000;
135
+ }
136
+ function roundElapsed(seconds) {
137
+ return Math.round(seconds * 10) / 10;
138
+ }
139
+ function httpStatusError(status, message, details) {
140
+ const error = new Error(message);
141
+ error.status = status;
142
+ error.details = details;
143
+ return error;
144
+ }
145
+ function isHttpStatusError(error) {
146
+ return error instanceof Error && typeof error.status === 'number';
147
+ }
148
+ function mapHttpStatusError(error) {
149
+ const status = error.status;
150
+ if (status === 401)
151
+ return new EscliError(error.message, { code: ErrorCode.ApiUnauthorized, details: error.details });
152
+ if (status === 403)
153
+ return new EscliError(error.message, { code: ErrorCode.ApiForbidden, details: error.details });
154
+ if (status === 404)
155
+ return new EscliError(error.message, { code: ErrorCode.ApiError, exitCode: ExitCodes.NotFound, details: error.details });
156
+ if (status === 429)
157
+ return new EscliError(error.message, { code: ErrorCode.ApiRateLimited, details: error.details });
158
+ if (status >= 500)
159
+ return new EscliError(error.message, { code: ErrorCode.ServiceUnavailable, details: error.details });
160
+ return new EscliError(error.message, { code: ErrorCode.SearchFailed, details: error.details });
161
+ }
162
+ function mapNetworkError(error) {
163
+ const isAbort = error instanceof Error && error.name === 'AbortError';
164
+ return new EscliError(isAbort ? 'network error: request timed out' : `network error: ${error instanceof Error ? error.message : String(error)}`, {
165
+ code: isAbort ? ErrorCode.NetworkTimeout : ErrorCode.NetworkError,
166
+ details: error instanceof Error ? error.message : error,
167
+ });
168
+ }
169
+ function formatHttpError(status, body) {
170
+ const parsed = parseMaybeJson(body);
171
+ const extracted = extractErrorMessage(parsed) ?? (typeof parsed === 'string' ? parsed : undefined);
172
+ return extracted ? `API error (${status}): ${extracted}` : `API error (${status})`;
173
+ }
174
+ function parseMaybeJson(value) {
175
+ try {
176
+ return JSON.parse(value);
177
+ }
178
+ catch {
179
+ return value;
180
+ }
181
+ }
182
+ function extractErrorMessage(body) {
183
+ const root = recordValue(body);
184
+ return stringValue(root?.error) ?? stringValue(root?.message) ?? stringValue(recordValue(root?.error)?.message);
185
+ }
186
+ function arrayValue(value) {
187
+ return Array.isArray(value) ? value : [];
188
+ }
189
+ function recordValue(value) {
190
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
191
+ }
192
+ function stringValue(value) {
193
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
194
+ }
195
+ //# sourceMappingURL=search.js.map