@steipete/oracle 0.8.4 → 0.8.5
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/README.md +7 -0
- package/dist/bin/oracle-cli.js +102 -9
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +13 -5
- package/dist/src/browser/chromeLifecycle.js +62 -9
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/index.js +55 -2
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/engine.js +17 -1
- package/dist/src/cli/options.js +14 -0
- package/dist/src/cli/runOptions.js +4 -0
- package/dist/src/mcp/tools/consult.js +80 -15
- package/dist/src/mcp/tools/sessions.js +15 -6
- package/dist/src/mcp/types.js +4 -0
- package/dist/src/mcp/utils.js +12 -2
- package/dist/src/oracle/background.js +1 -2
- package/dist/src/oracle/client.js +5 -2
- package/dist/src/oracle/files.js +2 -2
- package/dist/src/oracle/run.js +1 -0
- package/dist/src/remote/client.js +6 -5
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +28 -1
- package/dist/src/sessionManager.js +63 -5
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +13 -13
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { parseHostPort } from '../bridge/connection.js';
|
|
4
|
+
export async function checkTcpConnection(host, timeoutMs = 2000) {
|
|
5
|
+
const { hostname, port } = parseHostPort(host);
|
|
6
|
+
return await new Promise((resolve) => {
|
|
7
|
+
const socket = net.createConnection({ host: hostname, port });
|
|
8
|
+
const onError = (err) => {
|
|
9
|
+
cleanup();
|
|
10
|
+
resolve({ ok: false, error: err.message });
|
|
11
|
+
};
|
|
12
|
+
const onConnect = () => {
|
|
13
|
+
cleanup();
|
|
14
|
+
resolve({ ok: true });
|
|
15
|
+
};
|
|
16
|
+
const onTimeout = () => {
|
|
17
|
+
cleanup();
|
|
18
|
+
resolve({ ok: false, error: `timeout after ${timeoutMs}ms` });
|
|
19
|
+
};
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
socket.removeAllListeners();
|
|
22
|
+
socket.end();
|
|
23
|
+
socket.destroy();
|
|
24
|
+
socket.unref();
|
|
25
|
+
};
|
|
26
|
+
socket.setTimeout(timeoutMs);
|
|
27
|
+
socket.once('error', onError);
|
|
28
|
+
socket.once('connect', onConnect);
|
|
29
|
+
socket.once('timeout', onTimeout);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export async function checkRemoteHealth({ host, token, timeoutMs = 5000, }) {
|
|
33
|
+
const { hostname, port } = parseHostPort(host);
|
|
34
|
+
const headers = { accept: 'application/json' };
|
|
35
|
+
if (token) {
|
|
36
|
+
headers.authorization = `Bearer ${token}`;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const response = await requestJson({
|
|
40
|
+
hostname,
|
|
41
|
+
port,
|
|
42
|
+
path: '/health',
|
|
43
|
+
headers,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
});
|
|
46
|
+
if (response.statusCode === 200 && typeof response.json === 'object' && response.json) {
|
|
47
|
+
const ok = response.json.ok === true;
|
|
48
|
+
const version = response.json.version;
|
|
49
|
+
const uptimeSeconds = response.json.uptimeSeconds;
|
|
50
|
+
return {
|
|
51
|
+
ok,
|
|
52
|
+
statusCode: response.statusCode,
|
|
53
|
+
version: typeof version === 'string' ? version : undefined,
|
|
54
|
+
uptimeSeconds: typeof uptimeSeconds === 'number' ? uptimeSeconds : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (response.statusCode === 404) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
statusCode: response.statusCode,
|
|
61
|
+
error: 'remote host does not expose /health (upgrade oracle on the host and retry)',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const error = extractErrorMessage(response.json, response.bodyText) ?? `HTTP ${response.statusCode}`;
|
|
65
|
+
return { ok: false, statusCode: response.statusCode, error };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function extractErrorMessage(json, bodyText) {
|
|
72
|
+
if (json && typeof json === 'object') {
|
|
73
|
+
const err = json.error;
|
|
74
|
+
if (typeof err === 'string' && err.trim().length > 0) {
|
|
75
|
+
return err.trim();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const trimmed = bodyText.trim();
|
|
79
|
+
return trimmed.length ? trimmed : null;
|
|
80
|
+
}
|
|
81
|
+
async function requestJson({ hostname, port, path, headers, timeoutMs, }) {
|
|
82
|
+
return await new Promise((resolve, reject) => {
|
|
83
|
+
const req = http.request({
|
|
84
|
+
hostname,
|
|
85
|
+
port,
|
|
86
|
+
path,
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers,
|
|
89
|
+
}, (res) => {
|
|
90
|
+
res.setEncoding('utf8');
|
|
91
|
+
let body = '';
|
|
92
|
+
res.on('data', (chunk) => {
|
|
93
|
+
body += chunk;
|
|
94
|
+
});
|
|
95
|
+
res.on('end', () => {
|
|
96
|
+
const statusCode = res.statusCode ?? 0;
|
|
97
|
+
let json = null;
|
|
98
|
+
try {
|
|
99
|
+
json = body.length ? JSON.parse(body) : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
json = null;
|
|
103
|
+
}
|
|
104
|
+
resolve({ statusCode, json, bodyText: body });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
req.setTimeout(timeoutMs, () => {
|
|
108
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms`));
|
|
109
|
+
});
|
|
110
|
+
req.on('error', reject);
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function normalizeString(value) {
|
|
2
|
+
if (typeof value !== 'string')
|
|
3
|
+
return undefined;
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
return trimmed.length ? trimmed : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function resolveRemoteServiceConfig({ cliHost, cliToken, userConfig, env = process.env, }) {
|
|
8
|
+
const configBrowserHost = normalizeString(userConfig?.browser?.remoteHost);
|
|
9
|
+
const configBrowserToken = normalizeString(userConfig?.browser?.remoteToken);
|
|
10
|
+
const envHost = normalizeString(env.ORACLE_REMOTE_HOST);
|
|
11
|
+
const envToken = normalizeString(env.ORACLE_REMOTE_TOKEN);
|
|
12
|
+
const cliHostValue = normalizeString(cliHost);
|
|
13
|
+
const cliTokenValue = normalizeString(cliToken);
|
|
14
|
+
const host = cliHostValue ?? configBrowserHost ?? envHost;
|
|
15
|
+
const token = cliTokenValue ?? configBrowserToken ?? envToken;
|
|
16
|
+
const hostSource = cliHostValue
|
|
17
|
+
? 'cli'
|
|
18
|
+
: configBrowserHost
|
|
19
|
+
? 'config.browser'
|
|
20
|
+
: envHost
|
|
21
|
+
? 'env'
|
|
22
|
+
: 'unset';
|
|
23
|
+
const tokenSource = cliTokenValue
|
|
24
|
+
? 'cli'
|
|
25
|
+
: configBrowserToken
|
|
26
|
+
? 'config.browser'
|
|
27
|
+
: envToken
|
|
28
|
+
? 'env'
|
|
29
|
+
: 'unset';
|
|
30
|
+
return { host, token, sources: { host: hostSource, token: tokenSource } };
|
|
31
|
+
}
|
|
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { runBrowserMode } from '../browserMode.js';
|
|
10
10
|
import { getCookies } from '@steipete/sweet-cookie';
|
|
11
11
|
import { CHATGPT_URL } from '../browser/constants.js';
|
|
12
|
+
import { getCliVersion } from '../version.js';
|
|
12
13
|
import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from '../browser/profileState.js';
|
|
13
14
|
import { normalizeChatgptUrl } from '../browser/utils.js';
|
|
14
15
|
async function findAvailablePort() {
|
|
@@ -32,6 +33,7 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
32
33
|
const server = http.createServer();
|
|
33
34
|
const logger = options.logger ?? console.log;
|
|
34
35
|
const authToken = options.token ?? randomBytes(16).toString('hex');
|
|
36
|
+
const startedAt = Date.now();
|
|
35
37
|
const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
|
|
36
38
|
const color = process.stdout.isTTY
|
|
37
39
|
? (formatter, msg) => formatter(msg)
|
|
@@ -50,6 +52,24 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
50
52
|
res.end(JSON.stringify({ ok: true }));
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
55
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
56
|
+
const authHeader = req.headers.authorization ?? '';
|
|
57
|
+
if (authHeader !== `Bearer ${authToken}`) {
|
|
58
|
+
if (verbose) {
|
|
59
|
+
logger(`[serve] Unauthorized /health attempt from ${formatSocket(req)} (missing/invalid token)`);
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
62
|
+
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
66
|
+
res.end(JSON.stringify({
|
|
67
|
+
ok: true,
|
|
68
|
+
version: getCliVersion(),
|
|
69
|
+
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
70
|
+
}));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
53
73
|
if (req.method !== 'POST' || req.url !== '/runs') {
|
|
54
74
|
res.statusCode = 404;
|
|
55
75
|
res.end();
|
|
@@ -352,7 +372,14 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
|
|
|
352
372
|
}
|
|
353
373
|
catch (error) {
|
|
354
374
|
const message = error instanceof Error ? error.message : String(error);
|
|
355
|
-
|
|
375
|
+
const missingDbMatch = message.match(/Unable to locate Chrome cookie DB at (.+?)(?:\.|$)/);
|
|
376
|
+
if (missingDbMatch) {
|
|
377
|
+
const lookedPath = missingDbMatch[1];
|
|
378
|
+
logger(`Chrome cookies not found at ${lookedPath}. Set --browser-cookie-path to your Chrome profile or log in manually.`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
|
|
382
|
+
}
|
|
356
383
|
if (process.platform === 'linux' && isWsl()) {
|
|
357
384
|
logger('WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.');
|
|
358
385
|
}
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import { createWriteStream } from 'node:fs';
|
|
4
4
|
import net from 'node:net';
|
|
5
|
-
import { DEFAULT_MODEL } from './oracle.js';
|
|
5
|
+
import { DEFAULT_MODEL, formatElapsed } from './oracle.js';
|
|
6
6
|
import { safeModelSlug } from './oracle/modelResolver.js';
|
|
7
7
|
import { getOracleHomeDir } from './oracleHome.js';
|
|
8
8
|
export function getSessionsDir() {
|
|
@@ -201,6 +201,10 @@ export async function initializeSession(options, cwd, notifications) {
|
|
|
201
201
|
search: options.search,
|
|
202
202
|
baseUrl: options.baseUrl,
|
|
203
203
|
azure: options.azure,
|
|
204
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
205
|
+
httpTimeoutMs: options.httpTimeoutMs,
|
|
206
|
+
zombieTimeoutMs: options.zombieTimeoutMs,
|
|
207
|
+
zombieUseLastActivity: options.zombieUseLastActivity,
|
|
204
208
|
writeOutputPath: options.writeOutputPath,
|
|
205
209
|
},
|
|
206
210
|
};
|
|
@@ -442,7 +446,7 @@ export async function getSessionPaths(sessionId) {
|
|
|
442
446
|
return { dir, metadata, log, request };
|
|
443
447
|
}
|
|
444
448
|
async function markZombie(meta, { persist }) {
|
|
445
|
-
if (!isZombie(meta)) {
|
|
449
|
+
if (!(await isZombie(meta))) {
|
|
446
450
|
return meta;
|
|
447
451
|
}
|
|
448
452
|
if (meta.mode === 'browser') {
|
|
@@ -461,10 +465,11 @@ async function markZombie(meta, { persist }) {
|
|
|
461
465
|
}
|
|
462
466
|
}
|
|
463
467
|
}
|
|
468
|
+
const maxAgeMs = resolveZombieMaxAgeMs(meta);
|
|
464
469
|
const updated = {
|
|
465
470
|
...meta,
|
|
466
471
|
status: 'error',
|
|
467
|
-
errorMessage:
|
|
472
|
+
errorMessage: `Session marked as zombie (> ${formatElapsed(maxAgeMs)} stale)`,
|
|
468
473
|
completedAt: new Date().toISOString(),
|
|
469
474
|
};
|
|
470
475
|
if (persist) {
|
|
@@ -510,7 +515,7 @@ async function markDeadBrowser(meta, { persist }) {
|
|
|
510
515
|
}
|
|
511
516
|
return updated;
|
|
512
517
|
}
|
|
513
|
-
function isZombie(meta) {
|
|
518
|
+
async function isZombie(meta) {
|
|
514
519
|
if (meta.status !== 'running') {
|
|
515
520
|
return false;
|
|
516
521
|
}
|
|
@@ -522,7 +527,60 @@ function isZombie(meta) {
|
|
|
522
527
|
if (Number.isNaN(startedMs)) {
|
|
523
528
|
return false;
|
|
524
529
|
}
|
|
525
|
-
|
|
530
|
+
const useLastActivity = meta.options?.zombieUseLastActivity === true;
|
|
531
|
+
const lastActivityMs = useLastActivity ? await getLastActivityMs(meta) : null;
|
|
532
|
+
const anchorMs = lastActivityMs ?? startedMs;
|
|
533
|
+
const maxAgeMs = resolveZombieMaxAgeMs(meta);
|
|
534
|
+
return Date.now() - anchorMs > maxAgeMs;
|
|
535
|
+
}
|
|
536
|
+
function resolveZombieMaxAgeMs(meta) {
|
|
537
|
+
const explicit = meta.options?.zombieTimeoutMs;
|
|
538
|
+
const hasExplicit = typeof explicit === 'number' && Number.isFinite(explicit) && explicit > 0;
|
|
539
|
+
let maxAgeMs = hasExplicit ? explicit : ZOMBIE_MAX_AGE_MS;
|
|
540
|
+
if (!hasExplicit) {
|
|
541
|
+
const timeoutSeconds = meta.options?.timeoutSeconds;
|
|
542
|
+
if (typeof timeoutSeconds === 'number' && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) {
|
|
543
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
544
|
+
if (timeoutMs > maxAgeMs) {
|
|
545
|
+
maxAgeMs = timeoutMs;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return maxAgeMs;
|
|
550
|
+
}
|
|
551
|
+
async function getLastActivityMs(meta) {
|
|
552
|
+
const candidates = new Set();
|
|
553
|
+
candidates.add(logPath(meta.id));
|
|
554
|
+
const modelNames = new Set();
|
|
555
|
+
if (typeof meta.model === 'string' && meta.model.length > 0) {
|
|
556
|
+
modelNames.add(meta.model);
|
|
557
|
+
}
|
|
558
|
+
if (Array.isArray(meta.models)) {
|
|
559
|
+
for (const entry of meta.models) {
|
|
560
|
+
if (entry?.model) {
|
|
561
|
+
modelNames.add(entry.model);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const modelName of modelNames) {
|
|
566
|
+
candidates.add(modelLogPath(meta.id, modelName));
|
|
567
|
+
}
|
|
568
|
+
let latest = 0;
|
|
569
|
+
let sawStat = false;
|
|
570
|
+
for (const candidate of candidates) {
|
|
571
|
+
try {
|
|
572
|
+
const stats = await fs.stat(candidate);
|
|
573
|
+
const mtimeMs = Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : stats.mtime.getTime();
|
|
574
|
+
if (Number.isFinite(mtimeMs)) {
|
|
575
|
+
latest = Math.max(latest, mtimeMs);
|
|
576
|
+
sawStat = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
// ignore missing logs; fallback to startedAt
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return sawStat ? latest : null;
|
|
526
584
|
}
|
|
527
585
|
function isProcessAlive(pid) {
|
|
528
586
|
if (!pid)
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"homepage": "https://github.com/steipete/oracle#readme",
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
65
|
-
"@google/genai": "^1.
|
|
65
|
+
"@google/genai": "^1.35.0",
|
|
66
66
|
"@google/generative-ai": "^0.24.1",
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.25.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
68
68
|
"@steipete/sweet-cookie": "^0.1.0",
|
|
69
69
|
"chalk": "^5.6.2",
|
|
70
70
|
"chrome-launcher": "^1.2.1",
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
"dotenv": "^17.2.3",
|
|
75
75
|
"fast-glob": "^3.3.3",
|
|
76
76
|
"gpt-tokenizer": "^3.4.0",
|
|
77
|
-
"inquirer": "13.
|
|
77
|
+
"inquirer": "13.2.0",
|
|
78
78
|
"json5": "^2.2.3",
|
|
79
79
|
"kleur": "^4.1.5",
|
|
80
|
-
"markdansi": "0.2.
|
|
81
|
-
"openai": "^6.
|
|
80
|
+
"markdansi": "0.2.1",
|
|
81
|
+
"openai": "^6.16.0",
|
|
82
82
|
"osc-progress": "^0.2.0",
|
|
83
83
|
"qs": "^6.14.1",
|
|
84
|
-
"shiki": "^3.
|
|
84
|
+
"shiki": "^3.21.0",
|
|
85
85
|
"toasted-notifier": "^10.1.0",
|
|
86
86
|
"tokentally": "^0.1.1",
|
|
87
87
|
"zod": "^4.3.5"
|
|
@@ -92,19 +92,19 @@
|
|
|
92
92
|
"@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
|
|
93
93
|
"@types/chrome-remote-interface": "^0.33.0",
|
|
94
94
|
"@types/inquirer": "^9.0.9",
|
|
95
|
-
"@types/node": "^25.0.
|
|
96
|
-
"@vitest/coverage-v8": "4.0.
|
|
97
|
-
"devtools-protocol": "0.0.
|
|
95
|
+
"@types/node": "^25.0.6",
|
|
96
|
+
"@vitest/coverage-v8": "4.0.17",
|
|
97
|
+
"devtools-protocol": "0.0.1568893",
|
|
98
98
|
"es-toolkit": "^1.43.0",
|
|
99
99
|
"esbuild": "^0.27.2",
|
|
100
|
-
"puppeteer-core": "^24.
|
|
100
|
+
"puppeteer-core": "^24.35.0",
|
|
101
101
|
"tsx": "^4.21.0",
|
|
102
102
|
"typescript": "^5.9.3",
|
|
103
|
-
"vitest": "^4.0.
|
|
103
|
+
"vitest": "^4.0.17"
|
|
104
104
|
},
|
|
105
105
|
"pnpm": {
|
|
106
106
|
"overrides": {
|
|
107
|
-
"devtools-protocol": "0.0.
|
|
107
|
+
"devtools-protocol": "0.0.1568893"
|
|
108
108
|
},
|
|
109
109
|
"onlyBuiltDependencies": [
|
|
110
110
|
"@cdktf/node-pty-prebuilt-multiarch",
|
|
Binary file
|
|
Binary file
|