@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,329 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { writeStderr } from '../io/io.js';
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 { DEFAULT_ENDPOINT } from './credentials.js';
10
+ const DEFAULT_GATE_URL = 'https://internal.eightstate.co';
11
+ const DEVICE_TIMEOUT_NON_TTY_MS = 250;
12
+ const DEVICE_TIMEOUT_TTY_MS = 900_000;
13
+ export async function authStatus() {
14
+ const config = await loadConfig();
15
+ const profileName = activeProfileName(config);
16
+ const stored = config.profiles?.[profileName] ?? {};
17
+ const apiKey = nonEmpty(stored.api_key) ?? nonEmpty(process.env.ESCLI_API_KEY);
18
+ const cliToken = nonEmpty(stored.cli_token);
19
+ const endpoint = nonEmpty(stored.endpoint) ?? nonEmpty(process.env.ESCLI_BASE_URL) ?? DEFAULT_ENDPOINT;
20
+ if (!apiKey && !cliToken) {
21
+ throw new EscliError('not authenticated', {
22
+ code: ErrorCode.AuthRequired,
23
+ exitCode: ExitCodes.Auth,
24
+ remediation: { command: 'escli auth login' },
25
+ });
26
+ }
27
+ return {
28
+ authenticated: true,
29
+ profile: profileName,
30
+ auth_type: cliToken ? 'gate' : 'api_key',
31
+ endpoint,
32
+ config_path: configPath(),
33
+ ...(apiKey ? { key: maskKey(apiKey) } : {}),
34
+ };
35
+ }
36
+ export async function authProfiles() {
37
+ const config = await loadConfig();
38
+ const active = activeProfileName(config);
39
+ const profiles = Object.entries(config.profiles ?? {}).map(([name, profile]) => {
40
+ const apiKey = nonEmpty(profile.api_key);
41
+ const cliToken = nonEmpty(profile.cli_token);
42
+ return {
43
+ name,
44
+ label: profile.label ?? name,
45
+ key: apiKey ? maskKey(apiKey) : cliToken ? 'gate' : '***',
46
+ auth_type: cliToken ? 'gate' : 'api_key',
47
+ endpoint: profile.endpoint ?? DEFAULT_ENDPOINT,
48
+ active: name === active,
49
+ created: profile.created ?? '',
50
+ };
51
+ });
52
+ return { profiles, count: profiles.length };
53
+ }
54
+ export async function authSwitch(profileName) {
55
+ const config = await loadConfig();
56
+ if (!config.profiles?.[profileName])
57
+ throw profileNotFound(profileName);
58
+ config.active_profile = profileName;
59
+ await saveConfig(config);
60
+ return { active_profile: profileName };
61
+ }
62
+ export async function authLogout(options) {
63
+ if (options.all) {
64
+ await rm(configPath(), { force: true });
65
+ return { message: 'all profiles removed' };
66
+ }
67
+ const config = await loadConfig();
68
+ const profileName = options.profile ?? activeProfileName(config);
69
+ if (!config.profiles?.[profileName])
70
+ return { profile: profileName, deleted: false };
71
+ delete config.profiles[profileName];
72
+ if (config.active_profile === profileName) {
73
+ config.active_profile = Object.keys(config.profiles)[0] ?? '';
74
+ }
75
+ await saveConfig(config);
76
+ return { profile: profileName, deleted: true };
77
+ }
78
+ export async function authLogin(options) {
79
+ const stdinKey = options.key ? undefined : await readPipedKey();
80
+ const apiKey = options.key ?? stdinKey;
81
+ if (apiKey)
82
+ return keyLogin({ ...options, key: apiKey });
83
+ return deviceLogin(options);
84
+ }
85
+ async function keyLogin(options) {
86
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
87
+ const profileName = options.profile ?? 'default';
88
+ const label = options.label ?? profileName;
89
+ const models = await validateKey(options.key, endpoint, (options.timeoutSeconds ?? 15) * 1000);
90
+ const config = await loadConfig();
91
+ config.profiles = config.profiles ?? {};
92
+ config.profiles[profileName] = {
93
+ api_key: options.key,
94
+ endpoint,
95
+ label,
96
+ created: createdTimestamp(),
97
+ };
98
+ config.active_profile = profileName;
99
+ await saveConfig(config);
100
+ return {
101
+ profile: profileName,
102
+ auth_type: 'api_key',
103
+ key: maskKey(options.key),
104
+ endpoint,
105
+ models: `${models} models`,
106
+ config_path: configPath(),
107
+ };
108
+ }
109
+ async function deviceLogin(options) {
110
+ const gate = gateUrl();
111
+ const device = await gateJson(`${gate}/api/auth/device`, undefined, ErrorCode.GateUnavailable);
112
+ const deviceCode = stringValue(device.device_code);
113
+ const userCode = stringValue(device.user_code);
114
+ const verificationUrl = stringValue(device.verification_url);
115
+ const interval = numberValue(device.interval) ?? 5;
116
+ if (!deviceCode || !verificationUrl)
117
+ throw new EscliError('invalid gate device response', { code: ErrorCode.GateInvalidResponse, exitCode: ExitCodes.Transient, details: device });
118
+ if (browserDisabled())
119
+ await writeManualDeviceInstructions(verificationUrl, userCode);
120
+ else
121
+ openBrowser(verificationUrl);
122
+ const deadline = Date.now() + (process.stdin.isTTY ? DEVICE_TIMEOUT_TTY_MS : DEVICE_TIMEOUT_NON_TTY_MS);
123
+ while (Date.now() < deadline) {
124
+ await sleep(Math.max(0, interval) * 1000);
125
+ const poll = await gateJson(`${gate}/api/auth/poll`, { device_code: deviceCode }, ErrorCode.GateUnavailable);
126
+ const status = stringValue(poll.status);
127
+ if (status === 'authorized') {
128
+ const token = stringValue(poll.token);
129
+ if (!token)
130
+ throw new EscliError('invalid gate poll response', { code: ErrorCode.GateInvalidResponse, exitCode: ExitCodes.Transient, details: poll });
131
+ const profileName = options.profile ?? 'default';
132
+ const config = await loadConfig();
133
+ config.profiles = config.profiles ?? {};
134
+ config.profiles[profileName] = {
135
+ cli_token: token,
136
+ endpoint: gate,
137
+ label: options.label ?? profileName,
138
+ created: createdTimestamp(),
139
+ };
140
+ config.active_profile = profileName;
141
+ await saveConfig(config);
142
+ return { profile: profileName, auth_type: 'gate', gate };
143
+ }
144
+ if (status === 'expired')
145
+ throw new EscliError('session expired', { code: ErrorCode.AuthSessionExpired, exitCode: ExitCodes.Auth });
146
+ }
147
+ throw new EscliError('timed out waiting for auth', {
148
+ code: ErrorCode.AuthTimeout,
149
+ exitCode: ExitCodes.Transient,
150
+ remediation: { command: 'escli auth login --key <key>' },
151
+ });
152
+ }
153
+ async function validateKey(apiKey, endpoint, timeoutMs) {
154
+ const controller = new AbortController();
155
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
156
+ try {
157
+ const response = await fetch(`${endpoint.replace(/\/+$/u, '')}/models`, {
158
+ method: 'GET',
159
+ headers: { 'authorization': `Bearer ${apiKey}`, 'accept': 'application/json' },
160
+ signal: controller.signal,
161
+ });
162
+ const body = await parseBody(response);
163
+ if (response.status === 401)
164
+ throw new EscliError('invalid key', { code: ErrorCode.AuthInvalid, exitCode: ExitCodes.Auth, details: body });
165
+ if (response.status === 403)
166
+ throw new EscliError('not authorized', { code: ErrorCode.AuthForbidden, exitCode: ExitCodes.Auth, details: body });
167
+ if (!response.ok)
168
+ throw new EscliError(`key validation failed (${response.status})`, { code: ErrorCode.AuthLoginFailed, details: body });
169
+ const data = recordValue(body)?.data;
170
+ return Array.isArray(data) ? data.length : 0;
171
+ }
172
+ catch (error) {
173
+ if (error instanceof EscliError)
174
+ throw error;
175
+ const isAbort = error instanceof Error && error.name === 'AbortError';
176
+ throw new EscliError(isAbort ? 'key validation timed out' : 'key validation failed', {
177
+ code: isAbort ? ErrorCode.NetworkTimeout : ErrorCode.NetworkError,
178
+ exitCode: ExitCodes.Transient,
179
+ details: error instanceof Error ? error.message : error,
180
+ });
181
+ }
182
+ finally {
183
+ clearTimeout(timeout);
184
+ }
185
+ }
186
+ async function gateJson(url, body, code) {
187
+ try {
188
+ const response = await fetch(url, {
189
+ method: 'POST',
190
+ headers: { 'content-type': 'application/json', 'accept': 'application/json' },
191
+ body: body === undefined ? undefined : JSON.stringify(body),
192
+ });
193
+ const data = await parseBody(response);
194
+ if (!response.ok)
195
+ throw new EscliError(`gate request failed (${response.status})`, { code, exitCode: ExitCodes.Transient, details: data });
196
+ const record = recordValue(data);
197
+ if (!record)
198
+ throw new EscliError('invalid gate response', { code: ErrorCode.GateInvalidResponse, exitCode: ExitCodes.Transient, details: data });
199
+ return record;
200
+ }
201
+ catch (error) {
202
+ if (error instanceof EscliError)
203
+ throw error;
204
+ throw new EscliError('gate unavailable', { code, exitCode: ExitCodes.Transient, details: error instanceof Error ? error.message : error });
205
+ }
206
+ }
207
+ async function parseBody(response) {
208
+ const text = await response.text();
209
+ if (!text)
210
+ return {};
211
+ try {
212
+ return JSON.parse(text);
213
+ }
214
+ catch {
215
+ return text;
216
+ }
217
+ }
218
+ async function loadConfig() {
219
+ const path = configPath();
220
+ let raw;
221
+ try {
222
+ raw = await readFile(path, 'utf8');
223
+ }
224
+ catch (error) {
225
+ if (isNodeError(error) && error.code === 'ENOENT')
226
+ return {};
227
+ throw new EscliError('failed to read config', { code: ErrorCode.Internal, details: path });
228
+ }
229
+ try {
230
+ const parsed = JSON.parse(raw);
231
+ return recordValue(parsed) ?? {};
232
+ }
233
+ catch {
234
+ throw new EscliError('invalid config file', { code: ErrorCode.ConfigInvalid, details: path });
235
+ }
236
+ }
237
+ async function saveConfig(config) {
238
+ await mkdir(configDir(), { recursive: true });
239
+ await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`);
240
+ await chmod(configPath(), 0o600);
241
+ }
242
+ async function readPipedKey() {
243
+ if (process.stdin.isTTY)
244
+ return undefined;
245
+ const chunks = [];
246
+ return new Promise((resolve) => {
247
+ const timer = setTimeout(() => {
248
+ cleanup();
249
+ resolve(nonEmpty(chunks.join('').trim()));
250
+ }, 25);
251
+ const cleanup = () => {
252
+ clearTimeout(timer);
253
+ process.stdin.off('data', onData);
254
+ process.stdin.off('end', onEnd);
255
+ process.stdin.pause();
256
+ };
257
+ const onData = (chunk) => chunks.push(String(chunk));
258
+ const onEnd = () => {
259
+ cleanup();
260
+ resolve(nonEmpty(chunks.join('').trim()));
261
+ };
262
+ process.stdin.on('data', onData);
263
+ process.stdin.once('end', onEnd);
264
+ process.stdin.resume();
265
+ });
266
+ }
267
+ function openBrowser(url) {
268
+ const command = process.env.BROWSER ?? (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open');
269
+ const args = process.env.BROWSER ? [url] : process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
270
+ try {
271
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
272
+ child.unref();
273
+ }
274
+ catch {
275
+ // Browser launch is best-effort; the device flow can still complete if the user opens the URL manually.
276
+ }
277
+ }
278
+ function browserDisabled() {
279
+ return envFlagSet(process.env.ESCLI_NO_BROWSER) || envFlagSet(process.env.ESCLI_DISABLE_BROWSER);
280
+ }
281
+ function envFlagSet(value) {
282
+ if (value === undefined)
283
+ return false;
284
+ return !['', '0', 'false', 'no'].includes(value.toLowerCase());
285
+ }
286
+ async function writeManualDeviceInstructions(verificationUrl, userCode) {
287
+ const codeLine = userCode ? `\nUser code: ${userCode}` : '';
288
+ await writeStderr(`Open this URL to authenticate:${codeLine}\n${verificationUrl}\n`);
289
+ }
290
+ function activeProfileName(config) {
291
+ return config.active_profile || 'default';
292
+ }
293
+ function profileNotFound(profileName) {
294
+ return new EscliError(`profile '${profileName}' not found`, { code: ErrorCode.AuthProfileNotFound, exitCode: ExitCodes.NotFound });
295
+ }
296
+ function maskKey(key) {
297
+ return key.length > 12 ? `${key.slice(0, 8)}...${key.slice(-4)}` : '***';
298
+ }
299
+ function createdTimestamp() {
300
+ return new Date().toISOString().replace(/Z$/u, '');
301
+ }
302
+ function sleep(ms) {
303
+ return new Promise((resolve) => setTimeout(resolve, ms));
304
+ }
305
+ function configDir() {
306
+ return process.env.ESCLI_CONFIG_DIR ?? join(homedir(), '.escli');
307
+ }
308
+ function configPath() {
309
+ return join(configDir(), 'config.json');
310
+ }
311
+ function gateUrl() {
312
+ return process.env.ESCLI_GATE_URL ?? DEFAULT_GATE_URL;
313
+ }
314
+ function recordValue(value) {
315
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
316
+ }
317
+ function isNodeError(error) {
318
+ return error instanceof Error && 'code' in error;
319
+ }
320
+ function stringValue(value) {
321
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
322
+ }
323
+ function numberValue(value) {
324
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
325
+ }
326
+ function nonEmpty(value) {
327
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
328
+ }
329
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ export const DEFAULT_ENDPOINT = 'https://ai.eightstate.co/v1';
5
+ const DEFAULT_GATE_URL = 'https://internal.eightstate.co';
6
+ const RETRYABLE_STATUSES = new Set([402, 429]);
7
+ export class ServiceCallError extends Error {
8
+ statusCode;
9
+ constructor(statusCode, message = '') {
10
+ super(`keys exhausted (last status ${statusCode}): ${message}`);
11
+ this.name = 'ServiceCallError';
12
+ this.statusCode = statusCode;
13
+ }
14
+ }
15
+ export function resolveApiKey(apiKeyOverride) {
16
+ if (apiKeyOverride)
17
+ return apiKeyOverride;
18
+ if (process.env.ESCLI_API_KEY)
19
+ return process.env.ESCLI_API_KEY;
20
+ return getActiveProfile().apiKey ?? '';
21
+ }
22
+ export function resolveEndpoint(baseUrlOverride) {
23
+ if (baseUrlOverride)
24
+ return baseUrlOverride;
25
+ if (process.env.ESCLI_BASE_URL)
26
+ return process.env.ESCLI_BASE_URL;
27
+ return getActiveProfile().endpoint ?? DEFAULT_ENDPOINT;
28
+ }
29
+ export async function callWithRetry(service, fn, maxAttempts = 3) {
30
+ let lastError;
31
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
32
+ const key = await vendKey(service);
33
+ if (!key)
34
+ break;
35
+ try {
36
+ return await fn(key);
37
+ }
38
+ catch (error) {
39
+ const status = extractStatus(error);
40
+ if (status !== undefined && RETRYABLE_STATUSES.has(status)) {
41
+ await reportKey(service, key.fingerprint, status);
42
+ lastError = new ServiceCallError(status, error instanceof Error ? error.message.slice(0, 200) : '');
43
+ continue;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ if (lastError)
49
+ throw lastError;
50
+ return null;
51
+ }
52
+ export async function vendKeyForService(service) {
53
+ return vendKey(service);
54
+ }
55
+ function getActiveProfile() {
56
+ const config = loadConfig();
57
+ const activeProfile = stringValue(config.active_profile) ?? 'default';
58
+ const profiles = recordValue(config.profiles);
59
+ const profile = profiles ? recordValue(profiles[activeProfile]) : undefined;
60
+ return {
61
+ apiKey: stringValue(profile?.api_key),
62
+ cliToken: stringValue(profile?.cli_token),
63
+ endpoint: stringValue(profile?.endpoint),
64
+ };
65
+ }
66
+ export function resolveCliToken() {
67
+ return getActiveProfile().cliToken;
68
+ }
69
+ async function vendKey(service) {
70
+ const token = resolveCliToken();
71
+ if (!token)
72
+ return null;
73
+ try {
74
+ const response = await fetch(`${gateUrl()}/api/keys/vend`, {
75
+ method: 'POST',
76
+ headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' },
77
+ body: JSON.stringify({ service }),
78
+ });
79
+ if (response.status !== 200)
80
+ return null;
81
+ const data = recordValue(await response.json());
82
+ if (!data || data['success'] !== true)
83
+ return null;
84
+ const apiKey = stringValue(data['api_key']);
85
+ const fingerprint = stringValue(data.fingerprint);
86
+ if (!apiKey || !fingerprint)
87
+ return null;
88
+ return { apiKey, fingerprint, baseUrl: stringValue(data['base_url']) };
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ async function reportKey(service, fingerprint, status) {
95
+ const token = resolveCliToken();
96
+ if (!token)
97
+ return;
98
+ try {
99
+ await fetch(`${gateUrl()}/api/keys/report`, {
100
+ method: 'POST',
101
+ headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' },
102
+ body: JSON.stringify({ service, fingerprint, status }),
103
+ });
104
+ }
105
+ catch {
106
+ // gate-reporting failures are intentionally swallowed: the
107
+ // credential vend was the user-facing operation and already
108
+ // completed; reporting is best-effort telemetry.
109
+ }
110
+ }
111
+ function extractStatus(error) {
112
+ const maybe = recordValue(error);
113
+ const status = maybe ? maybe.status ?? maybe.statusCode ?? maybe.status_code : undefined;
114
+ return typeof status === 'number' ? status : undefined;
115
+ }
116
+ function loadConfig() {
117
+ try {
118
+ const raw = readFileSync(join(configDir(), 'config.json'), 'utf8');
119
+ return recordValue(JSON.parse(raw)) ?? {};
120
+ }
121
+ catch {
122
+ return {};
123
+ }
124
+ }
125
+ function configDir() {
126
+ return process.env.ESCLI_CONFIG_DIR ?? join(homedir(), '.escli');
127
+ }
128
+ function gateUrl() {
129
+ return process.env.ESCLI_GATE_URL ?? DEFAULT_GATE_URL;
130
+ }
131
+ function recordValue(value) {
132
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
133
+ }
134
+ function stringValue(value) {
135
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
136
+ }
137
+ //# sourceMappingURL=credentials.js.map