@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.
- package/CONVENTIONS.md +59 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/RELEASE-NOTES-0.5.0.md +34 -0
- package/dist/base-command.js +166 -0
- package/dist/commands/audio/get.js +39 -0
- package/dist/commands/audio/index.js +18 -0
- package/dist/commands/audio/list.js +39 -0
- package/dist/commands/audio/status.js +34 -0
- package/dist/commands/audio/transcribe.js +99 -0
- package/dist/commands/auth/index.js +18 -0
- package/dist/commands/auth/login.js +38 -0
- package/dist/commands/auth/logout.js +27 -0
- package/dist/commands/auth/profiles.js +31 -0
- package/dist/commands/auth/status.js +27 -0
- package/dist/commands/auth/switch.js +24 -0
- package/dist/commands/docs/fetch.js +37 -0
- package/dist/commands/docs/get.js +47 -0
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/docs/search.js +55 -0
- package/dist/commands/fetch.js +55 -0
- package/dist/commands/image/edit.js +59 -0
- package/dist/commands/image/generate.js +67 -0
- package/dist/commands/image/index.js +18 -0
- package/dist/commands/models.js +27 -0
- package/dist/commands/research.js +92 -0
- package/dist/commands/search.js +54 -0
- package/dist/commands/social.js +69 -0
- package/dist/commands/usage.js +51 -0
- package/dist/commands/version.js +22 -0
- package/dist/entry.js +120 -0
- package/dist/io/io.js +322 -0
- package/dist/lib/build-flags.js +2 -0
- package/dist/lib/command-metadata.js +8 -0
- package/dist/lib/envelope.js +28 -0
- package/dist/lib/escli-error.js +20 -0
- package/dist/lib/global-flags.js +29 -0
- package/dist/lib/globals.js +2 -0
- package/dist/lib/manifest.js +67 -0
- package/dist/lib/oclif-manifest-check.js +11 -0
- package/dist/lib/registry.js +228 -0
- package/dist/services/audio.js +454 -0
- package/dist/services/auth.js +329 -0
- package/dist/services/credentials.js +137 -0
- package/dist/services/docs.js +303 -0
- package/dist/services/fetch.js +197 -0
- package/dist/services/image.js +297 -0
- package/dist/services/models.js +131 -0
- package/dist/services/research.js +504 -0
- package/dist/services/search.js +195 -0
- package/dist/services/social.js +224 -0
- package/dist/services/usage.js +165 -0
- package/oclif.manifest.json +3377 -0
- package/package.json +57 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
7
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
8
|
+
import { EscliError } from '../lib/escli-error.js';
|
|
9
|
+
import { callWithRetry } from './credentials.js';
|
|
10
|
+
const CONTEXT7_API = 'https://context7.com/api';
|
|
11
|
+
const SEARCH_TTL_SECONDS = 86_400;
|
|
12
|
+
const RESOLVE_TTL_SECONDS = 604_800;
|
|
13
|
+
const DOCS_TTL_SECONDS = 86_400;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
export async function searchDocs(options) {
|
|
16
|
+
const name = options.libraryName;
|
|
17
|
+
const query = options.query || name;
|
|
18
|
+
const limit = options.limit || 10;
|
|
19
|
+
const cacheKey = `search:${name.toLowerCase()}:${query.toLowerCase()}`;
|
|
20
|
+
if (!options.refresh) {
|
|
21
|
+
const cached = await cacheRead('search', cacheKey);
|
|
22
|
+
if (cached)
|
|
23
|
+
return searchData(cached, limit, true);
|
|
24
|
+
}
|
|
25
|
+
const payload = await docsRequestJson(searchUrl(name, query));
|
|
26
|
+
await cacheWrite('search', cacheKey, payload, SEARCH_TTL_SECONDS);
|
|
27
|
+
return searchData(payload, limit, false);
|
|
28
|
+
}
|
|
29
|
+
export async function getDocs(options) {
|
|
30
|
+
const resolveKey = `resolve:${options.libraryName.toLowerCase()}:${options.query.toLowerCase()}`;
|
|
31
|
+
let libraryId;
|
|
32
|
+
if (!options.refresh) {
|
|
33
|
+
const cached = await cacheRead('resolve', resolveKey);
|
|
34
|
+
libraryId = cached?.id;
|
|
35
|
+
}
|
|
36
|
+
if (!libraryId) {
|
|
37
|
+
const payload = await docsRequestJson(searchUrl(options.libraryName, options.query));
|
|
38
|
+
const results = payload.results ?? [];
|
|
39
|
+
const pick = results.find((result) => result.state === 'finalized') ?? results[0];
|
|
40
|
+
if (!pick?.id) {
|
|
41
|
+
throw new EscliError(`no results for: ${options.libraryName}`, {
|
|
42
|
+
code: ErrorCode.DocsNoResults,
|
|
43
|
+
exitCode: ExitCodes.NotFound,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
libraryId = pick.id;
|
|
47
|
+
await cacheWrite('resolve', resolveKey, { id: libraryId, name: options.libraryName }, RESOLVE_TTL_SECONDS);
|
|
48
|
+
}
|
|
49
|
+
const docs = await fetchDocs({ ...options, libraryId });
|
|
50
|
+
return { ...docs, operation: 'get' };
|
|
51
|
+
}
|
|
52
|
+
export async function fetchDocs(options) {
|
|
53
|
+
const cacheKey = `docs:${options.libraryId}:${options.query}:${options.tokens ?? 'undefined'}:${options.page ?? 'undefined'}:${options.topic ?? 'undefined'}`;
|
|
54
|
+
if (!options.refresh) {
|
|
55
|
+
const cached = await cacheRead('docs', cacheKey);
|
|
56
|
+
if (cached)
|
|
57
|
+
return docsData(cached.body ?? '', options.libraryId, options.query, true, 'fetch');
|
|
58
|
+
}
|
|
59
|
+
const response = await docsRequestText(contextUrl(options));
|
|
60
|
+
await cacheWrite('docs', cacheKey, { body: response }, DOCS_TTL_SECONDS);
|
|
61
|
+
return docsData(response, options.libraryId, options.query, false, 'fetch');
|
|
62
|
+
}
|
|
63
|
+
function searchData(payload, limit, cached) {
|
|
64
|
+
const results = (payload.results ?? []).slice(0, limit);
|
|
65
|
+
return { operation: 'search', cached, results, count: results.length };
|
|
66
|
+
}
|
|
67
|
+
function docsData(body, libraryId, query, cached, operation) {
|
|
68
|
+
return { operation, cached, library_id: libraryId, query, content: body };
|
|
69
|
+
}
|
|
70
|
+
async function docsRequestJson(url) {
|
|
71
|
+
const text = await docsRequestText(url);
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new EscliError(`invalid JSON response: ${error instanceof Error ? error.message : String(error)}`, {
|
|
77
|
+
code: ErrorCode.GateInvalidResponse,
|
|
78
|
+
exitCode: ExitCodes.Error,
|
|
79
|
+
details: text,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function docsRequestText(url) {
|
|
84
|
+
const explicitKey = process.env.CONTEXT7_API_KEY;
|
|
85
|
+
if (explicitKey)
|
|
86
|
+
return requestText(url, { apiKey: explicitKey, fingerprint: explicitKey.slice(-8) }, timeoutMs());
|
|
87
|
+
try {
|
|
88
|
+
const response = await callWithRetry('context7', (key) => requestText(url, key, timeoutMs()));
|
|
89
|
+
if (response !== null)
|
|
90
|
+
return response;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!(error instanceof Error && error.name === 'ServiceCallError'))
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return requestText(url, undefined, timeoutMs());
|
|
97
|
+
}
|
|
98
|
+
async function requestText(url, key, requestTimeoutMs) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
method: 'GET',
|
|
104
|
+
headers: requestHeaders(key?.apiKey),
|
|
105
|
+
redirect: 'follow',
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
if (key?.apiKey)
|
|
109
|
+
await reportContext7Key(key, response.status, response.headers);
|
|
110
|
+
const text = await response.text();
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw httpStatusError(response.status, formatHttpError(response.status, text, url), text);
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (isHttpStatusError(error))
|
|
117
|
+
throw mapHttpStatusError(error);
|
|
118
|
+
throw mapNetworkError(error);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function requestHeaders(apiKey) {
|
|
125
|
+
const headers = { 'user-agent': 'escli-docs/1.0', 'x-context7-source': 'cli' };
|
|
126
|
+
if (apiKey)
|
|
127
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
128
|
+
return headers;
|
|
129
|
+
}
|
|
130
|
+
async function reportContext7Key(key, status, headers) {
|
|
131
|
+
const token = resolveCliTokenForReport();
|
|
132
|
+
if (!token)
|
|
133
|
+
return;
|
|
134
|
+
const body = { service: 'context7', fingerprint: key.fingerprint, status };
|
|
135
|
+
const remaining = numericHeader(headers, 'ratelimit-remaining');
|
|
136
|
+
const limit = numericHeader(headers, 'ratelimit-limit');
|
|
137
|
+
const reset = headers.get('ratelimit-reset');
|
|
138
|
+
const tier = headers.get('context7-quota-tier');
|
|
139
|
+
if (remaining !== undefined)
|
|
140
|
+
body.remaining = remaining;
|
|
141
|
+
if (limit !== undefined)
|
|
142
|
+
body.limit = limit;
|
|
143
|
+
if (reset)
|
|
144
|
+
body.reset = reset;
|
|
145
|
+
if (tier)
|
|
146
|
+
body.tier = tier;
|
|
147
|
+
try {
|
|
148
|
+
await fetch(`${gateUrl()}/api/keys/report`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' },
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// reporting failures are intentionally swallowed; the Context7
|
|
156
|
+
// result is the user-facing payload and remains authoritative.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function numericHeader(headers, name) {
|
|
160
|
+
const value = headers.get(name);
|
|
161
|
+
if (!value)
|
|
162
|
+
return undefined;
|
|
163
|
+
const number = Number(value);
|
|
164
|
+
return Number.isFinite(number) ? number : undefined;
|
|
165
|
+
}
|
|
166
|
+
function resolveCliTokenForReport() {
|
|
167
|
+
try {
|
|
168
|
+
const config = JSON.parse(readFileSync(join(configDir(), 'config.json'), 'utf8'));
|
|
169
|
+
const activeProfile = stringValue(config.active_profile) ?? 'default';
|
|
170
|
+
const profiles = recordValue(config.profiles);
|
|
171
|
+
return stringValue(recordValue(profiles?.[activeProfile])?.cli_token);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function searchUrl(libraryName, query) {
|
|
178
|
+
const url = new URL(`${context7BaseUrl()}/v2/libs/search`);
|
|
179
|
+
url.searchParams.set('libraryName', libraryName);
|
|
180
|
+
url.searchParams.set('query', query);
|
|
181
|
+
return url.toString();
|
|
182
|
+
}
|
|
183
|
+
function contextUrl(options) {
|
|
184
|
+
const url = new URL(`${context7BaseUrl()}/v2/context`);
|
|
185
|
+
url.searchParams.set('libraryId', options.libraryId);
|
|
186
|
+
url.searchParams.set('query', options.query);
|
|
187
|
+
url.searchParams.set('type', 'txt');
|
|
188
|
+
if (options.tokens)
|
|
189
|
+
url.searchParams.set('tokens', String(options.tokens));
|
|
190
|
+
if (options.page)
|
|
191
|
+
url.searchParams.set('page', String(options.page));
|
|
192
|
+
if (options.topic)
|
|
193
|
+
url.searchParams.set('topic', options.topic);
|
|
194
|
+
return url.toString();
|
|
195
|
+
}
|
|
196
|
+
function context7BaseUrl() {
|
|
197
|
+
if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_CONTEXT7_URL)
|
|
198
|
+
return process.env.ESCLI_TEST_CONTEXT7_URL;
|
|
199
|
+
return CONTEXT7_API;
|
|
200
|
+
}
|
|
201
|
+
function timeoutMs() {
|
|
202
|
+
if (typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) {
|
|
203
|
+
const value = Number(process.env.ESCLI_DOCS_TIMEOUT_MS);
|
|
204
|
+
if (Number.isFinite(value) && value > 0)
|
|
205
|
+
return Math.trunc(value);
|
|
206
|
+
}
|
|
207
|
+
return DEFAULT_TIMEOUT_MS;
|
|
208
|
+
}
|
|
209
|
+
async function cacheRead(layer, key) {
|
|
210
|
+
const path = cachePath(layer, key);
|
|
211
|
+
try {
|
|
212
|
+
const data = JSON.parse(await readFile(path, 'utf8'));
|
|
213
|
+
const expiresAt = typeof data.expires_at === 'number' ? data.expires_at : 0;
|
|
214
|
+
if (Date.now() / 1000 > expiresAt) {
|
|
215
|
+
await unlink(path).catch(() => undefined);
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
return data.payload;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
await unlink(path).catch(() => undefined);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function cacheWrite(layer, key, payload, ttlSeconds) {
|
|
226
|
+
const path = cachePath(layer, key);
|
|
227
|
+
const createdAt = Date.now() / 1000;
|
|
228
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
229
|
+
await writeFile(path, JSON.stringify({ created_at: createdAt, expires_at: createdAt + ttlSeconds, payload }));
|
|
230
|
+
}
|
|
231
|
+
function cachePath(layer, key) {
|
|
232
|
+
const hash = createHash('sha256').update(key).digest('hex');
|
|
233
|
+
return join(cacheDir(), layer, `${hash}.json`);
|
|
234
|
+
}
|
|
235
|
+
function cacheDir() {
|
|
236
|
+
return process.env.ESCLI_CACHE_DIR ?? join(homedir(), '.escli', 'cache');
|
|
237
|
+
}
|
|
238
|
+
function configDir() {
|
|
239
|
+
return process.env.ESCLI_CONFIG_DIR ?? join(homedir(), '.escli');
|
|
240
|
+
}
|
|
241
|
+
function gateUrl() {
|
|
242
|
+
return process.env.ESCLI_GATE_URL ?? 'https://internal.eightstate.co';
|
|
243
|
+
}
|
|
244
|
+
function httpStatusError(status, message, details) {
|
|
245
|
+
const error = new Error(message);
|
|
246
|
+
error.status = status;
|
|
247
|
+
error.details = details;
|
|
248
|
+
return error;
|
|
249
|
+
}
|
|
250
|
+
function isHttpStatusError(error) {
|
|
251
|
+
return error instanceof Error && typeof error.status === 'number';
|
|
252
|
+
}
|
|
253
|
+
function mapHttpStatusError(error) {
|
|
254
|
+
const status = error.status;
|
|
255
|
+
const message = error.message;
|
|
256
|
+
if (status === 401)
|
|
257
|
+
return new EscliError(message, { code: ErrorCode.ApiUnauthorized, exitCode: ExitCodes.Auth, details: error.details });
|
|
258
|
+
if (status === 403)
|
|
259
|
+
return new EscliError(message, { code: ErrorCode.ApiForbidden, exitCode: ExitCodes.Auth, details: error.details });
|
|
260
|
+
if (status === 404)
|
|
261
|
+
return new EscliError(message.startsWith('library not found:') ? message : 'library not found', { code: ErrorCode.DocsLibraryNotFound, exitCode: ExitCodes.NotFound, details: error.details });
|
|
262
|
+
if (status === 429)
|
|
263
|
+
return new EscliError(message, { code: ErrorCode.ApiRateLimited, exitCode: ExitCodes.Transient, details: error.details });
|
|
264
|
+
if (status >= 500)
|
|
265
|
+
return new EscliError(message, { code: ErrorCode.ServiceUnavailable, exitCode: ExitCodes.Transient, details: error.details });
|
|
266
|
+
return new EscliError(message, { code: ErrorCode.DocsFetchFailed, details: error.details });
|
|
267
|
+
}
|
|
268
|
+
function mapNetworkError(error) {
|
|
269
|
+
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
270
|
+
return new EscliError(isAbort ? 'network error: request timed out' : `network error: ${error instanceof Error ? error.message : String(error)}`, {
|
|
271
|
+
code: isAbort ? ErrorCode.NetworkTimeout : ErrorCode.NetworkError,
|
|
272
|
+
exitCode: ExitCodes.Transient,
|
|
273
|
+
details: error instanceof Error ? error.message : error,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function formatHttpError(status, body, url) {
|
|
277
|
+
if (status === 404 && url.includes('/v2/context')) {
|
|
278
|
+
const parsed = new URL(url);
|
|
279
|
+
return `library not found: ${parsed.searchParams.get('libraryId') ?? ''}`;
|
|
280
|
+
}
|
|
281
|
+
const parsedBody = parseMaybeJson(body);
|
|
282
|
+
const extracted = extractErrorMessage(parsedBody) ?? (typeof parsedBody === 'string' ? parsedBody : undefined);
|
|
283
|
+
return extracted ? `API error (${status}): ${extracted}` : `API error (${status})`;
|
|
284
|
+
}
|
|
285
|
+
function parseMaybeJson(value) {
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(value);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function extractErrorMessage(body) {
|
|
294
|
+
const root = recordValue(body);
|
|
295
|
+
return stringValue(root?.error) ?? stringValue(root?.message) ?? stringValue(recordValue(root?.error)?.message);
|
|
296
|
+
}
|
|
297
|
+
function recordValue(value) {
|
|
298
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
299
|
+
}
|
|
300
|
+
function stringValue(value) {
|
|
301
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=docs.js.map
|
|
@@ -0,0 +1,197 @@
|
|
|
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 fetchUrls(options) {
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
const payload = await fetchPayload(options);
|
|
12
|
+
const results = normalizeResults(recordValue(payload)?.results);
|
|
13
|
+
return {
|
|
14
|
+
elapsed_seconds: roundElapsed((Date.now() - started) / 1000),
|
|
15
|
+
results,
|
|
16
|
+
count: results.length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function fetchPayload(options) {
|
|
20
|
+
const explicitKey = process.env.PARALLEL_API_KEY;
|
|
21
|
+
if (explicitKey)
|
|
22
|
+
return restExtract(options, { apiKey: explicitKey, fingerprint: 'env' });
|
|
23
|
+
try {
|
|
24
|
+
const response = await callWithRetry('parallel', (key) => restExtract(options, key));
|
|
25
|
+
if (response !== null)
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (!(error instanceof Error && error.name === 'ServiceCallError'))
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return mcpFetch(options);
|
|
33
|
+
}
|
|
34
|
+
async function restExtract(options, key) {
|
|
35
|
+
const body = { urls: options.urls };
|
|
36
|
+
if (options.objective)
|
|
37
|
+
body.objective = options.objective;
|
|
38
|
+
if (options.full)
|
|
39
|
+
body.full_content = true;
|
|
40
|
+
const response = await requestText(`${parallelBaseUrl()}/v1/extract`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json', 'x-api-key': key.apiKey },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
timeoutMs: timeoutMs(options.timeoutSeconds),
|
|
45
|
+
});
|
|
46
|
+
return parseJsonResponse(response);
|
|
47
|
+
}
|
|
48
|
+
async function mcpFetch(options) {
|
|
49
|
+
const args = { urls: options.urls, session_id: randomUUID().replaceAll('-', '') };
|
|
50
|
+
if (options.objective)
|
|
51
|
+
args.objective = options.objective;
|
|
52
|
+
if (options.full)
|
|
53
|
+
args.full_content = true;
|
|
54
|
+
const response = await requestText(parallelMcpUrl(), {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
|
|
57
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'web_fetch', arguments: args } }),
|
|
58
|
+
timeoutMs: timeoutMs(options.timeoutSeconds),
|
|
59
|
+
});
|
|
60
|
+
const data = recordValue(parseJsonResponse(response));
|
|
61
|
+
const error = recordValue(data?.error);
|
|
62
|
+
if (error)
|
|
63
|
+
throw new EscliError(stringValue(error.message) ?? 'MCP error', { code: ErrorCode.FetchFailed });
|
|
64
|
+
const result = recordValue(data?.result);
|
|
65
|
+
const content = arrayValue(result?.content);
|
|
66
|
+
for (const item of content) {
|
|
67
|
+
const record = recordValue(item);
|
|
68
|
+
if (record?.type === 'text' && typeof record.text === 'string')
|
|
69
|
+
return parseJsonResponse(record.text);
|
|
70
|
+
}
|
|
71
|
+
return result ?? data;
|
|
72
|
+
}
|
|
73
|
+
async function requestText(url, options) {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(url, {
|
|
78
|
+
method: options.method,
|
|
79
|
+
headers: options.headers,
|
|
80
|
+
body: options.body,
|
|
81
|
+
redirect: 'follow',
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
const text = await response.text();
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
throw httpStatusError(response.status, formatHttpError(response.status, text), text);
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (isHttpStatusError(error))
|
|
91
|
+
throw mapHttpStatusError(error);
|
|
92
|
+
throw mapNetworkError(error);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function parseJsonResponse(text) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new EscliError(`invalid JSON response: ${error instanceof Error ? error.message : String(error)}`, {
|
|
104
|
+
code: ErrorCode.JsonInvalid,
|
|
105
|
+
exitCode: ExitCodes.Error,
|
|
106
|
+
details: text,
|
|
107
|
+
});
|
|
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
|
+
...(typeof item.title === 'string' || item.title === null ? { title: item.title } : {}),
|
|
118
|
+
url: String(item.url ?? ''),
|
|
119
|
+
excerpts: Array.isArray(item.excerpts) ? item.excerpts.map(String) : [],
|
|
120
|
+
...(item.full_content === null || typeof item.full_content === 'string' ? { full_content: item.full_content } : {}),
|
|
121
|
+
...(item.publish_date === null || typeof item.publish_date === 'string' ? { publish_date: item.publish_date } : {}),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
function parallelBaseUrl() {
|
|
125
|
+
if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_PARALLEL_URL)
|
|
126
|
+
return process.env.ESCLI_TEST_PARALLEL_URL;
|
|
127
|
+
return REST_ENDPOINT.replace(/\/v1$/u, '');
|
|
128
|
+
}
|
|
129
|
+
function parallelMcpUrl() {
|
|
130
|
+
if ((typeof __ESCLI_TEST__ === 'undefined' || __ESCLI_TEST__) && process.env.ESCLI_TEST_PARALLEL_URL)
|
|
131
|
+
return `${process.env.ESCLI_TEST_PARALLEL_URL}/mcp`;
|
|
132
|
+
return MCP_ENDPOINT;
|
|
133
|
+
}
|
|
134
|
+
function timeoutMs(timeoutSeconds) {
|
|
135
|
+
const seconds = timeoutSeconds ?? DEFAULT_TIMEOUT_MS / 1000;
|
|
136
|
+
return Math.max(1, seconds) * 1000;
|
|
137
|
+
}
|
|
138
|
+
function roundElapsed(seconds) {
|
|
139
|
+
return Math.round(seconds * 10) / 10;
|
|
140
|
+
}
|
|
141
|
+
function httpStatusError(status, message, details) {
|
|
142
|
+
const error = new Error(message);
|
|
143
|
+
error.status = status;
|
|
144
|
+
error.details = details;
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
function isHttpStatusError(error) {
|
|
148
|
+
return error instanceof Error && typeof error.status === 'number';
|
|
149
|
+
}
|
|
150
|
+
function mapHttpStatusError(error) {
|
|
151
|
+
const status = error.status;
|
|
152
|
+
if (status === 401)
|
|
153
|
+
return new EscliError(error.message, { code: ErrorCode.ApiUnauthorized, details: error.details });
|
|
154
|
+
if (status === 403)
|
|
155
|
+
return new EscliError(error.message, { code: ErrorCode.ApiForbidden, details: error.details });
|
|
156
|
+
if (status === 404)
|
|
157
|
+
return new EscliError(error.message, { code: ErrorCode.FileNotFound, details: error.details });
|
|
158
|
+
if (status === 429)
|
|
159
|
+
return new EscliError(error.message, { code: ErrorCode.ApiRateLimited, details: error.details });
|
|
160
|
+
if (status >= 500)
|
|
161
|
+
return new EscliError(error.message, { code: ErrorCode.ServiceUnavailable, details: error.details });
|
|
162
|
+
return new EscliError(error.message, { code: ErrorCode.FetchFailed, details: error.details });
|
|
163
|
+
}
|
|
164
|
+
function mapNetworkError(error) {
|
|
165
|
+
const isAbort = error instanceof Error && error.name === 'AbortError';
|
|
166
|
+
return new EscliError(isAbort ? 'network error: request timed out' : `network error: ${error instanceof Error ? error.message : String(error)}`, {
|
|
167
|
+
code: isAbort ? ErrorCode.NetworkTimeout : ErrorCode.NetworkError,
|
|
168
|
+
details: error instanceof Error ? error.message : error,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function formatHttpError(status, body) {
|
|
172
|
+
const parsed = parseMaybeJson(body);
|
|
173
|
+
const extracted = extractErrorMessage(parsed) ?? (typeof parsed === 'string' ? parsed : undefined);
|
|
174
|
+
return extracted ? `API error (${status}): ${extracted}` : `API error (${status})`;
|
|
175
|
+
}
|
|
176
|
+
function parseMaybeJson(value) {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(value);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function extractErrorMessage(body) {
|
|
185
|
+
const root = recordValue(body);
|
|
186
|
+
return stringValue(root?.error) ?? stringValue(root?.message) ?? stringValue(recordValue(root?.error)?.message);
|
|
187
|
+
}
|
|
188
|
+
function arrayValue(value) {
|
|
189
|
+
return Array.isArray(value) ? value : [];
|
|
190
|
+
}
|
|
191
|
+
function recordValue(value) {
|
|
192
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
193
|
+
}
|
|
194
|
+
function stringValue(value) {
|
|
195
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=fetch.js.map
|