diff --git a/.gitignore b/.gitignore index eb79dd5..ce9af41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .idea +logs \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..d918387 --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "cctxSymbol": "ETH/USDT:USDT", + "symbol": "ETHUSDT", + "startBalance": 10000, + "orderSizeQuote": 100, + "interval": 60, + "ohlcvInterval": "1h", + "leverage": 5, + "feeTaker": 0.055, + "pendingCancelTime": 1200000, + "logPath": "./tradeLog.json" +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..b15b0e5 --- /dev/null +++ b/index.js @@ -0,0 +1,239 @@ +import ccxt from 'ccxt'; +import WebSocket from 'ws'; +import fs from 'fs'; +import { aiSignal } from './strategy.js'; +import logger from './logger.js'; + +const cfg = JSON.parse(fs.readFileSync('./config.json', 'utf8')); +const ex = new ccxt.bybit({ + apiKey: '', + secret: '', + options: { defaultType: 'swap' } +}); + +let bars = []; +let balance = cfg.startBalance; +let position = null; +let trades = []; +let curPrice = null; + +async function bootstrap() { + try { + const ohlcv = await ex.fetchOHLCV(cfg.cctxSymbol, cfg.ohlcvInterval, undefined, 200); + bars = ohlcv.map(c => ({ + t: c[0], + o: c[1], + h: c[2], + l: c[3], + c: c[4], + v: c[5] + })); + + logger.info(`Bootstrapped ${bars.length} bars | Balance: ${balance}`); + } catch (error) { + logger.critical('초기화 중 심각한 오류 발생', error, { + stage: 'bootstrap' + }); + process.exit(1); + } +} + + +function initWs() { + try { + const ws = new WebSocket(`wss://stream.bybit.com/v5/public/linear`); + + ws.on('open', () => { + logger.info('WebSocket 연결 성공'); + ws.send(JSON.stringify({ + op: 'subscribe', + args: [ + `kline.${cfg.interval}.${cfg.symbol}`, + `tickers.${cfg.symbol}`, + ] + })); + }); + + ws.on('message', (msg) => { + try { + const m = JSON.parse(msg); + if (m.op !== 'subscribe') { + if (m.topic?.startsWith('tickers')) { + const t = m.data; + if (t) { + if(t.lastPrice) { + curPrice = +t.lastPrice; + // logger.debug('현재 가격 업데이트', { curPrice }); + } + } + } + + if (m.topic?.startsWith('kline')) { + const k = m.data[0]; + if (!k || !k.confirm) return; + + const bar = { + t: +k.start, + o: +k.open, + h: +k.high, + l: +k.low, + c: +k.close, + v: +k.volume + }; + + bars.push(bar); + if (bars.length > 300) bars.shift(); + + logger.debug('새로운 분봉 수신', { + timestamp: new Date(bar.t).toISOString(), + open: bar.o, + high: bar.h, + low: bar.l, + close: bar.c, + volume: bar.v + }); + + onNewBar(bar); + } + } + } catch (e) { + logger.error('WebSocket 메시지 처리 중 오류', e, { + rawMessage: msg.toString().substr(0, 100) + '...' + }); + } + }); + + ws.on('error', (error) => { + logger.error('WebSocket 연결 오류', error); + }); + + ws.on('close', (code, reason) => { + logger.warn('WebSocket 연결 종료', { + code, + reason: reason.toString() + }); + // 재연결 시도 로직 추가 가능 + }); + } catch (error) { + logger.critical('WebSocket 초기화 실패', error); + setTimeout(initWs, 5000); // 5초 후 재시도 + } +} + +async function onNewBar(bar) { + logger.debug(`새로운 ${cfg.interval}분 봉 수신`, { + timestamp: new Date(bar.t).toISOString(), + open: bar.o, + high: bar.h, + low: bar.l, + close: bar.c, + volume: bar.v + }); + + + if (position) { + logger.debug(`진입 보류: 이미 포지션 존재`, { + position: { + side: position.side, + entry: position.entry, + qty: position.qty + } + }); + return; + } + + const sig = await aiSignal(bars); + if(sig.side !== 'HOLD') enterMarket(sig); +} + +function enterMarket(sig) { + + const qty = ((balance * cfg.leverage) / curPrice).toFixed(4); + + const notional = curPrice * +qty; + const fee = notional * cfg.feeTaker / 100; + balance -= fee; + + position = { + id: `m${Date.now()}`, + side: sig.side, + entry: curPrice, + qty: +qty, + sl: sig.sl, + tp: sig.tp, + openTime: Date.now(), + fee, + initialBalance: balance + fee + }; + + logger.info(`[FILLED] ${sig.side} 시장가 @${curPrice} qty=${qty}`, { position }); +} + + +function checkExit() { + if (!position || curPrice === null) return; + + const { side, entry, qty, sl, tp } = position; + let exit = null; + + if (side === 'LONG') { + if (curPrice >= tp) exit = 'TP'; + if (curPrice <= sl) exit = 'SL'; + } else { + if (curPrice <= tp) exit = 'TP'; + if (curPrice >= sl) exit = 'SL'; + } + + if (!exit) return; + + const pnl = (side === 'LONG' ? curPrice - entry : entry - curPrice) * qty; + const fee = qty * curPrice * (cfg.feeTaker / 100); + const netPnl = pnl - fee; + balance += netPnl; + + // 음수 잔고 방지를 위한 체크 + if (balance < 0) { + logger.critical('음수 잔고 발생! 시스템 중지 필요', { + balanceBefore: balance - netPnl, + pnl, + fee, + netPnl, + position + }); + balance = 0; // 안전장치 + } + + const tradeRecord = { + ...position, + closed: true, + exitPrice: curPrice, + exitReason: exit, + pnl: netPnl, + closeTime: Date.now(), + finalBalance: balance, + leverage: cfg.leverage + }; + + trades.push(tradeRecord); + fs.writeFileSync(cfg.logPath, JSON.stringify(trades, null, 2)); + + logger.info(`[EXIT] ${exit} @${curPrice.toFixed(4)} | PnL=${netPnl.toFixed(2)} | Balance=${balance.toFixed(2)}`, { + trade: tradeRecord, + market: { currentPrice: curPrice } + }); + + // 종료된 포지션 로깅 + logger.logPosition(tradeRecord, `포지션 종료 (${exit})`); + + position = null; +} + +// 시스템 시작 +try { + await bootstrap(); + initWs(); + setInterval(checkExit, 1000); +} catch (error) { + logger.critical('시스템 시작 중 심각한 오류', error); + process.exit(1); +} \ No newline at end of file diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..4711609 --- /dev/null +++ b/logger.js @@ -0,0 +1,180 @@ +import fs from 'fs'; +import path from 'path'; + +// 로그 디렉토리 생성 +const logDir = './logs'; +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +// 로그 레벨 정의 +const logLevels = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + CRITICAL: 4 +}; + +const logLevelNames = Object.keys(logLevels); +const currentLogLevel = logLevels.info; // 설정에 따라 변경 가능 + +// 타임스탬프 생성 +const getTimestamp = () => new Date().toISOString(); + +// 호출자 정보 추출 (ESM 호환) +const getCallerInfo = () => { + const err = new Error(); + const stack = err.stack.split('\n'); + + // 스택에서 호출자 위치 찾기 + let callerIndex = 2; // 기본값 (0: Error, 1: getCallerInfo, 2: logger function) + + // 'at '로 시작하는 라인 찾기 + for (let i = 2; i < stack.length; i++) { + if (stack[i].includes('at ')) { + callerIndex = i; + break; + } + } + + if (stack.length > callerIndex) { + const callerLine = stack[callerIndex].trim(); + + // 파일 경로 추출 (Windows와 Unix 경로 모두 처리) + const fileMatch = callerLine.match(/\((.*?):\d+:\d+\)/) || + callerLine.match(/(.*?):\d+:\d+$/); + + if (fileMatch && fileMatch[1]) { + const filePath = fileMatch[1]; + const fileName = path.basename(filePath); + return fileName; + } + } + + return 'unknown'; +}; + +// 로그 포맷팅 +const formatLog = (level, message, metadata = {}) => { + const timestamp = getTimestamp(); + const callerInfo = getCallerInfo(); + const levelName = logLevelNames[level]; + + let logMessage = `[${levelName}] ${timestamp} [${callerInfo}] ${message}`; + + if (Object.keys(metadata).length > 0) { + try { + logMessage += `\n Meta: ${JSON.stringify(metadata, null, 2)}`; + } catch (e) { + logMessage += `\n Meta: [Circular or invalid object]`; + } + } + + return logMessage; +}; + +// 로그 파일 쓰기 +const writeLog = (logMessage, level) => { + if (level < currentLogLevel) return; + + // 콘솔 출력 + if (level >= logLevels.ERROR) { + console.error(logMessage); + } else { + console.log(logMessage); + } + + // 파일 로깅 + try { + const date = new Date().toISOString().split('T')[0]; + const logFile = path.join(logDir, `trading-${date}.log`); + fs.appendFileSync(logFile, logMessage + '\n'); + } catch (e) { + // 파일 로깅 실패 시 콘솔에 경고 + console.error(`[LOGGER] Failed to write log file: ${e.message}`); + } +}; + +// 로거 인스턴스 생성 +const createLogger = () => { + return { + debug: (message, metadata = {}) => { + const logMessage = formatLog(logLevels.DEBUG, message, metadata); + writeLog(logMessage, logLevels.DEBUG); + }, + + info: (message, metadata = {}) => { + const logMessage = formatLog(logLevels.INFO, message, metadata); + writeLog(logMessage, logLevels.INFO); + }, + + warn: (message, metadata = {}) => { + const logMessage = formatLog(logLevels.WARN, message, metadata); + writeLog(logMessage, logLevels.WARN); + }, + + error: (message, error = null, metadata = {}) => { + const errorMetadata = { ...metadata }; + + if (error) { + errorMetadata.error = { + message: error.message || 'Unknown error', + stack: error.stack ? + error.stack.split('\n').slice(0, 5).join('\n') : + 'No stack trace' + }; + } + + const logMessage = formatLog(logLevels.ERROR, message, errorMetadata); + writeLog(logMessage, logLevels.ERROR); + }, + + critical: (message, error = null, metadata = {}) => { + const errorMetadata = { ...metadata }; + + if (error) { + errorMetadata.error = { + message: error.message || 'Unknown critical error', + stack: error.stack || 'No stack trace' + }; + } + + const logMessage = formatLog(logLevels.CRITICAL, message, errorMetadata); + writeLog(logMessage, logLevels.CRITICAL); + }, + + logPosition: (position, message) => { + if (!position) return; + + const metadata = { + position: { + id: position.id, + side: position.side, + entry: position.entry, + qty: position.qty, + sl: position.sl, + tp: position.tp, + openTime: position.openTime ? + new Date(position.openTime).toISOString() : 'N/A' + } + }; + + if (position.closed) { + metadata.position.exitPrice = position.exitPrice; + metadata.position.exitReason = position.exitReason; + metadata.position.pnl = position.pnl; + metadata.position.closeTime = position.closeTime ? + new Date(position.closeTime).toISOString() : 'N/A'; + } + + this.info(message, metadata); + } + }; +}; + +// 싱글톤 인스턴스 생성 +const logger = createLogger(); + +// 모듈 내보내기 +export default logger; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3dd091c --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "bot", + "version": "1.0.0", + "description": "Auto Trade Bot", + "main": "index.mjs", + "type": "module", + "scripts": { + "dev": "yarn " + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/node": "^24.3.0", + "axios": "^1.11.0", + "ccxt": "^4.5.2", + "fs-extra": "^11.3.1", + "openai": "^5.16.0", + "regression": "^2.0.1", + "ws": "^8.18.3" + } +} diff --git a/strategy.js b/strategy.js new file mode 100644 index 0000000..9590431 --- /dev/null +++ b/strategy.js @@ -0,0 +1,41 @@ +// strategy.js +import logger from './logger.js'; +import {OpenAI} from "openai"; + + + +const openai = new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: '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', +}); + +export async function aiSignal(bars) { + if (bars.length < 20) return null; + // const time = '15분' + const time = '1시간' + console.log(Date.now()) + const completion = await openai.chat.completions.create({ + model: 'qwen/qwen3-235b-a22b-2507', + messages: [ + { + role: 'user', + content: `당신은 코인 매매 전문 트레이너 입니다. + 매매 방법은 스윙매매 입니다. 1시간 봉으로 매매 하는 만큼 큰 수익을 얻어야 합니다. 너무 무리 하지 않는 선에서 제어가 필요합니다. + 아래에 제공 되는 json ${time}봉 데이터를 보고 여러지표(macd, rsi, ema, 다이버전스, ovm, vwap) 등 을 대입하여 포지션, 타점, 스톱로스, 테이크피로핏을 정의 해줘야합니다 + 모든 대답은 간결하게 {side: 포지션, price: 진입가, sl: 스탑로스, tp: 테이크프로핏, resaon: 간단한이유} json 형태로 대답하세요. + 포지션은 SHORT, LONG, HOLD 포지션이 LONG, SHORT 일때는 전체적인 추세를 보고 진입가 손절 익절 금액을 채워주세요. + reason 에는 포지션을 지지하는 이유를 요약 하여 채워주세요. + + t: 시간 o: 시작가 h: 최고가 l: 최저가 c: 종료가 v: 볼륨 + ${JSON.stringify(bars)} + `, + }, + ], + }); + console.log(Date.now()) + const msg = completion.choices[0].message.content + logger.info(msg); + const sig = JSON.parse(msg) + + return sig; +} \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..a8d77f3 --- /dev/null +++ b/test.js @@ -0,0 +1,55 @@ +import { aiSignal } from './strategy.js'; +import ccxt from "ccxt"; +import fs from "fs"; +import logger from "./logger.js"; + +const cfg = JSON.parse(fs.readFileSync('./config.json', 'utf8')) + +const ex = new ccxt.bybit({ + apiKey: '', + secret: '', + options: { defaultType: 'swap' } +}); + +const ohlcv = await ex.fetchOHLCV(cfg.cctxSymbol, cfg.ohlcvInterval, undefined, 200); +const bars = ohlcv.map(c => ({ + t: c[0], + o: c[1], + h: c[2], + l: c[3], + c: c[4], + v: c[5] +})); +const sig = await aiSignal(bars); +// const sig = JSON.parse(msg) +console.log(sig) +console.log(sig.side) +console.log(sig.sl) +console.log(sig.tp) +console.log(sig.reason) +let position= {} +let balance = 10000 +const curPrice = sig.price +if(sig.side !== 'HOLD') enterMarket(sig); +function enterMarket(sig) { + + const qty = ((balance * cfg.leverage) / curPrice).toFixed(4); + + const notional = curPrice * +qty; + const fee = notional * cfg.feeTaker / 100; + balance -= fee; + + position = { + id: `m${Date.now()}`, + side: sig.side, + entry: curPrice, + qty: +qty, + sl: sig.sl, + tp: sig.tp, + openTime: Date.now(), + fee, + initialBalance: balance + fee + }; + + logger.info(`[FILLED] ${sig.side} 시장가 @${curPrice} qty=${qty}`, { position }); +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..5ae4ab4 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,226 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" + integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== + dependencies: + undici-types "~7.10.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6" + integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +ccxt@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/ccxt/-/ccxt-4.5.2.tgz#a777d64f8122040229c37ad7adae3b08c75c2a84" + integrity sha512-IY21AVv8E+qJJPQFt2NNGiEFOygtl7dAOwojIh0Lxyz/+L7/q0m54Swjk/ALHCuvGQeBWQBgYrn+U6b45Zx9BQ== + dependencies: + ws "^8.8.1" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +follow-redirects@^1.15.6: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== + +form-data@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + +fs-extra@^11.3.1: + version "11.3.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.1.tgz#ba7a1f97a85f94c6db2e52ff69570db3671d5a74" + integrity sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +jsonfile@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +openai@^5.16.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-5.16.0.tgz#a302d4ca92954598c79c72dd199c58994708130d" + integrity sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +regression@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regression/-/regression-2.0.1.tgz#8d29c3e8224a10850c35e337e85a8b2fac3b0c87" + integrity sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ== + +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +ws@^8.18.3, ws@^8.8.1: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==