krx-cli 1.3.0 → 1.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/dist/cli.js CHANGED
@@ -12,9 +12,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  if (typeof require !== "undefined") return require.apply(this, arguments);
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
- var __esm = (fn, res) => function __init() {
16
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
- };
18
15
  var __commonJS = (cb, mod) => function __require2() {
19
16
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
20
17
  };
@@ -1202,8 +1199,8 @@ var require_command = __commonJS({
1202
1199
  "node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/command.js"(exports) {
1203
1200
  var EventEmitter = __require("node:events").EventEmitter;
1204
1201
  var childProcess = __require("node:child_process");
1205
- var path3 = __require("node:path");
1206
- var fs3 = __require("node:fs");
1202
+ var path6 = __require("node:path");
1203
+ var fs6 = __require("node:fs");
1207
1204
  var process3 = __require("node:process");
1208
1205
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1209
1206
  var { CommanderError: CommanderError2 } = require_error();
@@ -2197,7 +2194,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2197
2194
  * @param {string} subcommandName
2198
2195
  */
2199
2196
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2200
- if (fs3.existsSync(executableFile)) return;
2197
+ if (fs6.existsSync(executableFile)) return;
2201
2198
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2202
2199
  const executableMissing = `'${executableFile}' does not exist
2203
2200
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2215,11 +2212,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2215
2212
  let launchWithNode = false;
2216
2213
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2217
2214
  function findFile(baseDir, baseName) {
2218
- const localBin = path3.resolve(baseDir, baseName);
2219
- if (fs3.existsSync(localBin)) return localBin;
2220
- if (sourceExt.includes(path3.extname(baseName))) return void 0;
2215
+ const localBin = path6.resolve(baseDir, baseName);
2216
+ if (fs6.existsSync(localBin)) return localBin;
2217
+ if (sourceExt.includes(path6.extname(baseName))) return void 0;
2221
2218
  const foundExt = sourceExt.find(
2222
- (ext) => fs3.existsSync(`${localBin}${ext}`)
2219
+ (ext) => fs6.existsSync(`${localBin}${ext}`)
2223
2220
  );
2224
2221
  if (foundExt) return `${localBin}${foundExt}`;
2225
2222
  return void 0;
@@ -2231,21 +2228,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2231
2228
  if (this._scriptPath) {
2232
2229
  let resolvedScriptPath;
2233
2230
  try {
2234
- resolvedScriptPath = fs3.realpathSync(this._scriptPath);
2231
+ resolvedScriptPath = fs6.realpathSync(this._scriptPath);
2235
2232
  } catch {
2236
2233
  resolvedScriptPath = this._scriptPath;
2237
2234
  }
2238
- executableDir = path3.resolve(
2239
- path3.dirname(resolvedScriptPath),
2235
+ executableDir = path6.resolve(
2236
+ path6.dirname(resolvedScriptPath),
2240
2237
  executableDir
2241
2238
  );
2242
2239
  }
2243
2240
  if (executableDir) {
2244
2241
  let localFile = findFile(executableDir, executableFile);
2245
2242
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2246
- const legacyName = path3.basename(
2243
+ const legacyName = path6.basename(
2247
2244
  this._scriptPath,
2248
- path3.extname(this._scriptPath)
2245
+ path6.extname(this._scriptPath)
2249
2246
  );
2250
2247
  if (legacyName !== this._name) {
2251
2248
  localFile = findFile(
@@ -2256,7 +2253,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2256
2253
  }
2257
2254
  executableFile = localFile || executableFile;
2258
2255
  }
2259
- launchWithNode = sourceExt.includes(path3.extname(executableFile));
2256
+ launchWithNode = sourceExt.includes(path6.extname(executableFile));
2260
2257
  let proc;
2261
2258
  if (process3.platform !== "win32") {
2262
2259
  if (launchWithNode) {
@@ -3171,7 +3168,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3171
3168
  * @return {Command}
3172
3169
  */
3173
3170
  nameFromFilename(filename) {
3174
- this._name = path3.basename(filename, path3.extname(filename));
3171
+ this._name = path6.basename(filename, path6.extname(filename));
3175
3172
  return this;
3176
3173
  }
3177
3174
  /**
@@ -3185,9 +3182,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3185
3182
  * @param {string} [path]
3186
3183
  * @return {(string|null|Command)}
3187
3184
  */
3188
- executableDir(path4) {
3189
- if (path4 === void 0) return this._executableDir;
3190
- this._executableDir = path4;
3185
+ executableDir(path7) {
3186
+ if (path7 === void 0) return this._executableDir;
3187
+ this._executableDir = path7;
3191
3188
  return this;
3192
3189
  }
3193
3190
  /**
@@ -3465,72 +3462,6 @@ var require_commander = __commonJS({
3465
3462
  }
3466
3463
  });
3467
3464
 
3468
- // src/client/rate-limit.ts
3469
- var rate_limit_exports = {};
3470
- __export(rate_limit_exports, {
3471
- checkRateLimit: () => checkRateLimit,
3472
- getRateLimitStatus: () => getRateLimitStatus,
3473
- incrementCallCount: () => incrementCallCount
3474
- });
3475
- import * as fs from "node:fs";
3476
- import * as path from "node:path";
3477
- import * as os from "node:os";
3478
- function today() {
3479
- const now = /* @__PURE__ */ new Date();
3480
- const yyyy = now.getFullYear().toString();
3481
- const mm = (now.getMonth() + 1).toString().padStart(2, "0");
3482
- const dd = now.getDate().toString().padStart(2, "0");
3483
- return `${yyyy}${mm}${dd}`;
3484
- }
3485
- function readRateData() {
3486
- try {
3487
- const raw = fs.readFileSync(RATE_FILE, "utf-8");
3488
- return JSON.parse(raw);
3489
- } catch {
3490
- return { date: today(), count: 0 };
3491
- }
3492
- }
3493
- function writeRateData(data) {
3494
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
3495
- fs.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
3496
- }
3497
- function incrementCallCount() {
3498
- const data = readRateData();
3499
- const currentDate = today();
3500
- const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
3501
- writeRateData(newData);
3502
- return { count: newData.count, limit: DAILY_LIMIT };
3503
- }
3504
- function checkRateLimit() {
3505
- const data = readRateData();
3506
- const currentDate = today();
3507
- const count = data.date === currentDate ? data.count : 0;
3508
- const allowed = count < DAILY_LIMIT;
3509
- const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
3510
- return { allowed, count, limit: DAILY_LIMIT, warning };
3511
- }
3512
- function getRateLimitStatus() {
3513
- const data = readRateData();
3514
- const currentDate = today();
3515
- const count = data.date === currentDate ? data.count : 0;
3516
- return {
3517
- date: currentDate,
3518
- count,
3519
- limit: DAILY_LIMIT,
3520
- remaining: DAILY_LIMIT - count
3521
- };
3522
- }
3523
- var CONFIG_DIR, RATE_FILE, DAILY_LIMIT, WARNING_THRESHOLD;
3524
- var init_rate_limit = __esm({
3525
- "src/client/rate-limit.ts"() {
3526
- "use strict";
3527
- CONFIG_DIR = path.join(os.homedir(), ".krx-cli");
3528
- RATE_FILE = path.join(CONFIG_DIR, "rate-limit.json");
3529
- DAILY_LIMIT = 1e4;
3530
- WARNING_THRESHOLD = 0.8;
3531
- }
3532
- });
3533
-
3534
3465
  // node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
3535
3466
  var import_index = __toESM(require_commander(), 1);
3536
3467
  var {
@@ -3549,9 +3480,9 @@ var {
3549
3480
  } = import_index.default;
3550
3481
 
3551
3482
  // src/client/auth.ts
3552
- import * as fs2 from "node:fs";
3553
- import * as path2 from "node:path";
3554
- import * as os2 from "node:os";
3483
+ import * as fs3 from "node:fs";
3484
+ import * as path3 from "node:path";
3485
+ import * as os3 from "node:os";
3555
3486
 
3556
3487
  // src/client/response-fields.ts
3557
3488
  var INDEX_COMMON_FIELDS = [
@@ -3942,13 +3873,13 @@ var CATEGORIES = [
3942
3873
  probeEndpoint: "/svc/apis/esg/esg_index_info"
3943
3874
  }
3944
3875
  ];
3945
- function ep(path3, description, descriptionKo, category) {
3876
+ function ep(path6, description, descriptionKo, category) {
3946
3877
  return {
3947
- path: path3,
3878
+ path: path6,
3948
3879
  description,
3949
3880
  descriptionKo,
3950
3881
  category,
3951
- responseFields: RESPONSE_FIELDS[path3] ?? []
3882
+ responseFields: RESPONSE_FIELDS[path6] ?? []
3952
3883
  };
3953
3884
  }
3954
3885
  var ENDPOINTS = [
@@ -4137,11 +4068,251 @@ var ENDPOINTS = [
4137
4068
  ep("/svc/apis/esg/esg_index_info", "ESG index info", "ESG \uC9C0\uC218 \uC815\uBCF4", "esg")
4138
4069
  ];
4139
4070
 
4071
+ // src/cache/store.ts
4072
+ import * as fs from "node:fs";
4073
+ import * as path from "node:path";
4074
+ import * as os from "node:os";
4075
+ import * as crypto from "node:crypto";
4076
+
4077
+ // src/utils/date.ts
4078
+ function formatDateToYYYYMMDD(date5) {
4079
+ const yyyy = date5.getFullYear().toString();
4080
+ const mm = (date5.getMonth() + 1).toString().padStart(2, "0");
4081
+ const dd = date5.getDate().toString().padStart(2, "0");
4082
+ return `${yyyy}${mm}${dd}`;
4083
+ }
4084
+ function isWeekend(date5) {
4085
+ const day = date5.getDay();
4086
+ return day === 0 || day === 6;
4087
+ }
4088
+ var KST_OFFSET_MS = 9 * 60 * 60 * 1e3;
4089
+ function getRecentTradingDate() {
4090
+ const nowKst = new Date(Date.now() + KST_OFFSET_MS);
4091
+ const day = nowKst.getUTCDay();
4092
+ const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
4093
+ const targetKst = new Date(nowKst.getTime() - daysBack * 24 * 60 * 60 * 1e3);
4094
+ const yyyy = targetKst.getUTCFullYear().toString();
4095
+ const mm = (targetKst.getUTCMonth() + 1).toString().padStart(2, "0");
4096
+ const dd = targetKst.getUTCDate().toString().padStart(2, "0");
4097
+ return `${yyyy}${mm}${dd}`;
4098
+ }
4099
+ function parseYYYYMMDD(str) {
4100
+ const year = parseInt(str.substring(0, 4));
4101
+ const month = parseInt(str.substring(4, 6)) - 1;
4102
+ const day = parseInt(str.substring(6, 8));
4103
+ return new Date(year, month, day);
4104
+ }
4105
+ function getTradingDays(from, to) {
4106
+ const fromDate = parseYYYYMMDD(from);
4107
+ const toDate = parseYYYYMMDD(to);
4108
+ const days = [];
4109
+ let current = new Date(fromDate);
4110
+ while (current <= toDate) {
4111
+ if (!isWeekend(current)) {
4112
+ days.push(formatDateToYYYYMMDD(current));
4113
+ }
4114
+ current = new Date(current.getTime() + 24 * 60 * 60 * 1e3);
4115
+ }
4116
+ return days;
4117
+ }
4118
+
4119
+ // src/cache/store.ts
4120
+ var CACHE_DIR = path.join(os.homedir(), ".krx-cli", "cache");
4121
+ var DATE_FORMAT = /^\d{8}$/;
4122
+ function isValidCacheDate(date5) {
4123
+ return DATE_FORMAT.test(date5);
4124
+ }
4125
+ function getCacheKey(endpoint, params) {
4126
+ const sortedEntries = Object.entries(params).sort(
4127
+ ([a], [b]) => a.localeCompare(b)
4128
+ );
4129
+ const raw = `${endpoint}:${JSON.stringify(sortedEntries)}`;
4130
+ return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
4131
+ }
4132
+ function getCachePath(date5, key) {
4133
+ return path.join(CACHE_DIR, date5, `${key}.json`);
4134
+ }
4135
+ function isToday(dateStr) {
4136
+ const today2 = formatDateToYYYYMMDD(/* @__PURE__ */ new Date());
4137
+ return dateStr === today2;
4138
+ }
4139
+ function getCached(endpoint, params) {
4140
+ const date5 = params["basDd"];
4141
+ if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
4142
+ return null;
4143
+ }
4144
+ const key = getCacheKey(endpoint, params);
4145
+ const cachePath = getCachePath(date5, key);
4146
+ try {
4147
+ const raw = fs.readFileSync(cachePath, "utf-8");
4148
+ return JSON.parse(raw);
4149
+ } catch (err) {
4150
+ if (err instanceof Error && err.message.includes("ENOENT")) {
4151
+ return null;
4152
+ }
4153
+ process.stderr.write(`[krx-cli] cache read failed: ${String(err)}
4154
+ `);
4155
+ return null;
4156
+ }
4157
+ }
4158
+ function setCached(endpoint, params, data) {
4159
+ const date5 = params["basDd"];
4160
+ if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
4161
+ return;
4162
+ }
4163
+ const key = getCacheKey(endpoint, params);
4164
+ const cachePath = getCachePath(date5, key);
4165
+ const dir = path.dirname(cachePath);
4166
+ try {
4167
+ fs.mkdirSync(dir, { recursive: true });
4168
+ fs.writeFileSync(cachePath, JSON.stringify(data), "utf-8");
4169
+ } catch (err) {
4170
+ process.stderr.write(`[krx-cli] cache write failed: ${String(err)}
4171
+ `);
4172
+ }
4173
+ }
4174
+ function clearCache() {
4175
+ let files = 0;
4176
+ let directories = 0;
4177
+ try {
4178
+ if (!fs.existsSync(CACHE_DIR)) {
4179
+ return { files, directories };
4180
+ }
4181
+ const dateDirs = fs.readdirSync(CACHE_DIR);
4182
+ for (const dateDir of dateDirs) {
4183
+ const dirPath = path.join(CACHE_DIR, dateDir);
4184
+ const stat = fs.statSync(dirPath);
4185
+ if (stat.isDirectory()) {
4186
+ const cacheFiles = fs.readdirSync(dirPath);
4187
+ files += cacheFiles.length;
4188
+ fs.rmSync(dirPath, { recursive: true, force: true });
4189
+ directories += 1;
4190
+ }
4191
+ }
4192
+ } catch (err) {
4193
+ process.stderr.write(`[krx-cli] cache clear failed: ${String(err)}
4194
+ `);
4195
+ }
4196
+ return { files, directories };
4197
+ }
4198
+ function getCacheStatus() {
4199
+ let totalFiles = 0;
4200
+ let totalSize = 0;
4201
+ let dates = 0;
4202
+ try {
4203
+ if (!fs.existsSync(CACHE_DIR)) {
4204
+ return { totalFiles, totalSize, dates };
4205
+ }
4206
+ const dateDirs = fs.readdirSync(CACHE_DIR);
4207
+ for (const dateDir of dateDirs) {
4208
+ const dirPath = path.join(CACHE_DIR, dateDir);
4209
+ const stat = fs.statSync(dirPath);
4210
+ if (stat.isDirectory()) {
4211
+ dates += 1;
4212
+ const cacheFiles = fs.readdirSync(dirPath);
4213
+ for (const file2 of cacheFiles) {
4214
+ const fileStat = fs.statSync(path.join(dirPath, file2));
4215
+ totalFiles += 1;
4216
+ totalSize += fileStat.size;
4217
+ }
4218
+ }
4219
+ }
4220
+ } catch (err) {
4221
+ process.stderr.write(`[krx-cli] cache status failed: ${String(err)}
4222
+ `);
4223
+ }
4224
+ return { totalFiles, totalSize, dates };
4225
+ }
4226
+
4227
+ // src/client/rate-limit.ts
4228
+ import * as fs2 from "node:fs";
4229
+ import * as path2 from "node:path";
4230
+ import * as os2 from "node:os";
4231
+ var CONFIG_DIR = path2.join(os2.homedir(), ".krx-cli");
4232
+ var RATE_FILE = path2.join(CONFIG_DIR, "rate-limit.json");
4233
+ var DAILY_LIMIT = 1e4;
4234
+ var WARNING_THRESHOLD = 0.8;
4235
+ function today() {
4236
+ const now = /* @__PURE__ */ new Date();
4237
+ const yyyy = now.getFullYear().toString();
4238
+ const mm = (now.getMonth() + 1).toString().padStart(2, "0");
4239
+ const dd = now.getDate().toString().padStart(2, "0");
4240
+ return `${yyyy}${mm}${dd}`;
4241
+ }
4242
+ function readRateData() {
4243
+ try {
4244
+ const raw = fs2.readFileSync(RATE_FILE, "utf-8");
4245
+ return JSON.parse(raw);
4246
+ } catch {
4247
+ return { date: today(), count: 0 };
4248
+ }
4249
+ }
4250
+ function writeRateData(data) {
4251
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
4252
+ fs2.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
4253
+ }
4254
+ function incrementCallCount() {
4255
+ const data = readRateData();
4256
+ const currentDate = today();
4257
+ const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
4258
+ writeRateData(newData);
4259
+ return { count: newData.count, limit: DAILY_LIMIT };
4260
+ }
4261
+ function checkRateLimit() {
4262
+ const data = readRateData();
4263
+ const currentDate = today();
4264
+ const count = data.date === currentDate ? data.count : 0;
4265
+ const allowed = count < DAILY_LIMIT;
4266
+ const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
4267
+ return { allowed, count, limit: DAILY_LIMIT, warning };
4268
+ }
4269
+
4270
+ // src/client/retry.ts
4271
+ var DEFAULT_MAX_RETRIES = 3;
4272
+ var DEFAULT_BASE_DELAY = 1e3;
4273
+ function isNetworkError(error48) {
4274
+ if (error48 instanceof TypeError) {
4275
+ return true;
4276
+ }
4277
+ if (error48 instanceof Error) {
4278
+ const msg = error48.message.toLowerCase();
4279
+ return msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("network");
4280
+ }
4281
+ return false;
4282
+ }
4283
+ function delay(ms) {
4284
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4285
+ }
4286
+ async function withRetry(fn, options) {
4287
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
4288
+ const baseDelay = options?.baseDelay ?? DEFAULT_BASE_DELAY;
4289
+ let lastError;
4290
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
4291
+ try {
4292
+ return await fn();
4293
+ } catch (err) {
4294
+ lastError = err;
4295
+ if (!isNetworkError(err) || attempt === maxRetries) {
4296
+ throw err;
4297
+ }
4298
+ const waitMs = baseDelay * Math.pow(2, attempt);
4299
+ await delay(waitMs);
4300
+ }
4301
+ }
4302
+ throw lastError;
4303
+ }
4304
+
4140
4305
  // src/client/client.ts
4141
4306
  var BASE_URL = "https://data-dbg.krx.co.kr";
4142
4307
  async function krxFetch(options) {
4143
- const { checkRateLimit: checkRateLimit2, incrementCallCount: incrementCallCount2 } = await Promise.resolve().then(() => (init_rate_limit(), rate_limit_exports));
4144
- const rateStatus = checkRateLimit2();
4308
+ const useCache = options.cache !== false;
4309
+ if (useCache) {
4310
+ const cached2 = getCached(options.endpoint, options.params);
4311
+ if (cached2) {
4312
+ return { success: true, data: cached2 };
4313
+ }
4314
+ }
4315
+ const rateStatus = checkRateLimit();
4145
4316
  if (!rateStatus.allowed) {
4146
4317
  return {
4147
4318
  success: false,
@@ -4157,15 +4328,18 @@ async function krxFetch(options) {
4157
4328
  );
4158
4329
  }
4159
4330
  const url2 = `${BASE_URL}${options.endpoint}`;
4160
- const response = await fetch(url2, {
4161
- method: "POST",
4162
- headers: {
4163
- AUTH_KEY: options.apiKey,
4164
- "Content-Type": "application/json; charset=utf-8"
4165
- },
4166
- body: JSON.stringify(options.params)
4167
- });
4168
- incrementCallCount2();
4331
+ const response = await withRetry(
4332
+ () => fetch(url2, {
4333
+ method: "POST",
4334
+ headers: {
4335
+ AUTH_KEY: options.apiKey,
4336
+ "Content-Type": "application/json; charset=utf-8"
4337
+ },
4338
+ body: JSON.stringify(options.params)
4339
+ }),
4340
+ { maxRetries: options.retries ?? 3 }
4341
+ );
4342
+ incrementCallCount();
4169
4343
  if (!response.ok) {
4170
4344
  let errorMsg = `HTTP ${response.status}: ${response.statusText}`;
4171
4345
  let errorCode;
@@ -4193,6 +4367,9 @@ async function krxFetch(options) {
4193
4367
  error: "Unexpected response format: missing OutBlock_1"
4194
4368
  };
4195
4369
  }
4370
+ if (useCache) {
4371
+ setCached(options.endpoint, options.params, outBlock);
4372
+ }
4196
4373
  return {
4197
4374
  success: true,
4198
4375
  data: outBlock
@@ -4200,19 +4377,19 @@ async function krxFetch(options) {
4200
4377
  }
4201
4378
 
4202
4379
  // src/client/auth.ts
4203
- var CONFIG_DIR2 = path2.join(os2.homedir(), ".krx-cli");
4204
- var CONFIG_FILE = path2.join(CONFIG_DIR2, "config.json");
4380
+ var CONFIG_DIR2 = path3.join(os3.homedir(), ".krx-cli");
4381
+ var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
4205
4382
  function readConfig() {
4206
4383
  try {
4207
- const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
4384
+ const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
4208
4385
  return JSON.parse(raw);
4209
4386
  } catch {
4210
4387
  return {};
4211
4388
  }
4212
4389
  }
4213
4390
  function writeConfig(config2) {
4214
- fs2.mkdirSync(CONFIG_DIR2, { recursive: true });
4215
- fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), "utf-8");
4391
+ fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
4392
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), "utf-8");
4216
4393
  }
4217
4394
  function getApiKey() {
4218
4395
  return process.env["KRX_API_KEY"] ?? readConfig().apiKey;
@@ -4221,16 +4398,6 @@ function saveApiKey(apiKey) {
4221
4398
  const config2 = readConfig();
4222
4399
  writeConfig({ ...config2, apiKey });
4223
4400
  }
4224
- function getRecentTradingDate() {
4225
- const now = /* @__PURE__ */ new Date();
4226
- const day = now.getDay();
4227
- const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
4228
- const target = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
4229
- const yyyy = target.getFullYear().toString();
4230
- const mm = (target.getMonth() + 1).toString().padStart(2, "0");
4231
- const dd = target.getDate().toString().padStart(2, "0");
4232
- return `${yyyy}${mm}${dd}`;
4233
- }
4234
4401
  async function checkCategoryApproval(apiKey, categoryId) {
4235
4402
  const category = CATEGORIES.find((c) => c.id === categoryId);
4236
4403
  if (!category) {
@@ -4282,6 +4449,8 @@ function formatOutput(data, format, fields) {
4282
4449
  return filtered.map((row) => JSON.stringify(row)).join("\n");
4283
4450
  case "table":
4284
4451
  return formatTable(filtered);
4452
+ case "csv":
4453
+ return formatCsv(filtered);
4285
4454
  }
4286
4455
  }
4287
4456
  function filterFields(data, fields) {
@@ -4311,6 +4480,23 @@ function formatTable(data) {
4311
4480
  );
4312
4481
  return [header, separator, ...rows].join("\n");
4313
4482
  }
4483
+ function escapeCsvField(value) {
4484
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
4485
+ return `"${value.replace(/"/g, '""')}"`;
4486
+ }
4487
+ return value;
4488
+ }
4489
+ function formatCsv(data) {
4490
+ if (data.length === 0) return "";
4491
+ const firstRow = data[0];
4492
+ if (!firstRow) return "";
4493
+ const keys = Object.keys(firstRow);
4494
+ const header = keys.join(",");
4495
+ const rows = data.map(
4496
+ (row) => keys.map((k) => escapeCsvField(String(row[k] ?? ""))).join(",")
4497
+ );
4498
+ return [header, ...rows].join("\n");
4499
+ }
4314
4500
  function writeOutput(output) {
4315
4501
  process.stdout.write(output + "\n");
4316
4502
  }
@@ -5153,10 +5339,10 @@ function mergeDefs(...defs) {
5153
5339
  function cloneDef(schema) {
5154
5340
  return mergeDefs(schema._zod.def);
5155
5341
  }
5156
- function getElementAtPath(obj, path3) {
5157
- if (!path3)
5342
+ function getElementAtPath(obj, path6) {
5343
+ if (!path6)
5158
5344
  return obj;
5159
- return path3.reduce((acc, key) => acc?.[key], obj);
5345
+ return path6.reduce((acc, key) => acc?.[key], obj);
5160
5346
  }
5161
5347
  function promiseAllObject(promisesObj) {
5162
5348
  const keys = Object.keys(promisesObj);
@@ -5539,11 +5725,11 @@ function aborted(x, startIndex = 0) {
5539
5725
  }
5540
5726
  return false;
5541
5727
  }
5542
- function prefixIssues(path3, issues) {
5728
+ function prefixIssues(path6, issues) {
5543
5729
  return issues.map((iss) => {
5544
5730
  var _a2;
5545
5731
  (_a2 = iss).path ?? (_a2.path = []);
5546
- iss.path.unshift(path3);
5732
+ iss.path.unshift(path6);
5547
5733
  return iss;
5548
5734
  });
5549
5735
  }
@@ -5726,7 +5912,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
5726
5912
  }
5727
5913
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
5728
5914
  const result = { errors: [] };
5729
- const processError = (error49, path3 = []) => {
5915
+ const processError = (error49, path6 = []) => {
5730
5916
  var _a2, _b;
5731
5917
  for (const issue2 of error49.issues) {
5732
5918
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -5736,7 +5922,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
5736
5922
  } else if (issue2.code === "invalid_element") {
5737
5923
  processError({ issues: issue2.issues }, issue2.path);
5738
5924
  } else {
5739
- const fullpath = [...path3, ...issue2.path];
5925
+ const fullpath = [...path6, ...issue2.path];
5740
5926
  if (fullpath.length === 0) {
5741
5927
  result.errors.push(mapper(issue2));
5742
5928
  continue;
@@ -5768,8 +5954,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
5768
5954
  }
5769
5955
  function toDotPath(_path) {
5770
5956
  const segs = [];
5771
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5772
- for (const seg of path3) {
5957
+ const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5958
+ for (const seg of path6) {
5773
5959
  if (typeof seg === "number")
5774
5960
  segs.push(`[${seg}]`);
5775
5961
  else if (typeof seg === "symbol")
@@ -17746,13 +17932,13 @@ function resolveRef(ref, ctx) {
17746
17932
  if (!ref.startsWith("#")) {
17747
17933
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17748
17934
  }
17749
- const path3 = ref.slice(1).split("/").filter(Boolean);
17750
- if (path3.length === 0) {
17935
+ const path6 = ref.slice(1).split("/").filter(Boolean);
17936
+ if (path6.length === 0) {
17751
17937
  return ctx.rootSchema;
17752
17938
  }
17753
17939
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17754
- if (path3[0] === defsKey) {
17755
- const key = path3[1];
17940
+ if (path6[0] === defsKey) {
17941
+ const key = path6[1];
17756
17942
  if (!key || !ctx.defs[key]) {
17757
17943
  throw new Error(`Reference not found: ${ref}`);
17758
17944
  }
@@ -18170,6 +18356,17 @@ var dateSchema = external_exports.string().regex(DATE_PATTERN, "Date must be YYY
18170
18356
  { message: "Invalid date. Must be a valid date from 2010 onwards." }
18171
18357
  );
18172
18358
  var marketSchema = external_exports.enum(["kospi", "kosdaq", "konex", "krx"]);
18359
+ var CONTROL_CHAR_PATTERN = /[\x00-\x1f\x7f]/;
18360
+ var PATH_TRAVERSAL_PATTERN = /\.\.[/\\]/;
18361
+ function validateNoInjection(input) {
18362
+ if (CONTROL_CHAR_PATTERN.test(input)) {
18363
+ return "Input contains control characters";
18364
+ }
18365
+ if (PATH_TRAVERSAL_PATTERN.test(input)) {
18366
+ return "Input contains path traversal";
18367
+ }
18368
+ return null;
18369
+ }
18173
18370
  function validateDate(value) {
18174
18371
  const result = dateSchema.safeParse(value);
18175
18372
  if (!result.success) {
@@ -18187,6 +18384,92 @@ function validateMarket(value) {
18187
18384
  return result.data;
18188
18385
  }
18189
18386
 
18387
+ // src/cli/command-helper.ts
18388
+ import * as fs4 from "node:fs";
18389
+ import * as path4 from "node:path";
18390
+
18391
+ // src/client/range-fetch.ts
18392
+ var DEFAULT_CONCURRENCY = 5;
18393
+ async function fetchWithConcurrency(tasks, concurrency) {
18394
+ const slots = new Array(tasks.length);
18395
+ let index = 0;
18396
+ async function runNext() {
18397
+ while (index < tasks.length) {
18398
+ const currentIndex = index;
18399
+ index += 1;
18400
+ const task = tasks[currentIndex];
18401
+ if (task) {
18402
+ const result = await task();
18403
+ slots[currentIndex] = Promise.resolve(result);
18404
+ }
18405
+ }
18406
+ }
18407
+ const workers = Array.from(
18408
+ { length: Math.min(concurrency, tasks.length) },
18409
+ () => runNext()
18410
+ );
18411
+ await Promise.all(workers);
18412
+ return Promise.all(slots);
18413
+ }
18414
+ async function fetchDateRange(options) {
18415
+ const { endpoint, from, to, apiKey, cache, extraParams } = options;
18416
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
18417
+ if (from > to) {
18418
+ return {
18419
+ success: false,
18420
+ data: [],
18421
+ error: `'from' date (${from}) must not be after 'to' date (${to})`,
18422
+ fetchedDays: 0,
18423
+ failedDays: 0
18424
+ };
18425
+ }
18426
+ const tradingDays = getTradingDays(from, to);
18427
+ if (tradingDays.length === 0) {
18428
+ return { success: true, data: [], fetchedDays: 0, failedDays: 0 };
18429
+ }
18430
+ const tasks = tradingDays.map((day) => async () => {
18431
+ const params = {
18432
+ basDd: day,
18433
+ ...extraParams
18434
+ };
18435
+ return krxFetch({
18436
+ endpoint,
18437
+ params,
18438
+ apiKey,
18439
+ cache
18440
+ });
18441
+ });
18442
+ const results = await fetchWithConcurrency(tasks, concurrency);
18443
+ const failedDays = results.filter((r) => !r.success).length;
18444
+ const mergedData = results.filter((r) => r.success).flatMap((r) => [...r.data]);
18445
+ if (mergedData.length === 0 && failedDays > 0) {
18446
+ return {
18447
+ success: false,
18448
+ data: [],
18449
+ error: `All ${failedDays} trading day(s) failed to fetch`,
18450
+ fetchedDays: 0,
18451
+ failedDays
18452
+ };
18453
+ }
18454
+ return {
18455
+ success: true,
18456
+ data: mergedData,
18457
+ fetchedDays: tradingDays.length - failedDays,
18458
+ failedDays
18459
+ };
18460
+ }
18461
+
18462
+ // src/cli/exit-codes.ts
18463
+ var EXIT_CODES = {
18464
+ SUCCESS: 0,
18465
+ GENERAL_ERROR: 1,
18466
+ USAGE_ERROR: 2,
18467
+ NO_DATA: 3,
18468
+ AUTH_FAILURE: 4,
18469
+ RATE_LIMIT: 5,
18470
+ SERVICE_NOT_APPROVED: 6
18471
+ };
18472
+
18190
18473
  // src/cli/error-handler.ts
18191
18474
  function handleKrxError(result) {
18192
18475
  const exitCode = result.errorCode === "RATE_LIMIT" ? EXIT_CODES.RATE_LIMIT : result.errorCode === "401" ? EXIT_CODES.SERVICE_NOT_APPROVED : EXIT_CODES.GENERAL_ERROR;
@@ -18194,209 +18477,368 @@ function handleKrxError(result) {
18194
18477
  process.exit(exitCode);
18195
18478
  }
18196
18479
 
18197
- // src/cli/commands/index-cmd.ts
18198
- var MARKET_ENDPOINTS = {
18199
- kospi: "/svc/apis/idx/kospi_dd_trd",
18200
- kosdaq: "/svc/apis/idx/kosdaq_dd_trd",
18201
- krx: "/svc/apis/idx/krx_dd_trd",
18202
- bond: "/svc/apis/idx/bon_dd_trd",
18203
- derivative: "/svc/apis/idx/drvprod_dd_trd"
18204
- };
18205
- function registerIndexCommand(program3) {
18206
- const index = program3.command("index").description("Query KRX index data");
18207
- index.command("list").description("List index daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
18208
- "--market <market>",
18209
- "market: kospi, kosdaq, krx, bond, derivative",
18210
- "kospi"
18211
- ).action(async (opts) => {
18212
- try {
18213
- const date5 = validateDate(opts.date);
18214
- const market = opts.market.toLowerCase();
18215
- const endpoint = MARKET_ENDPOINTS[market];
18216
- if (!endpoint) {
18217
- writeError(
18218
- `Invalid market: ${market}. Must be one of: ${Object.keys(MARKET_ENDPOINTS).join(", ")}`
18219
- );
18220
- process.exit(EXIT_CODES.USAGE_ERROR);
18221
- }
18222
- const apiKey = getApiKey();
18223
- if (!apiKey) {
18224
- writeError(
18225
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18226
- );
18227
- process.exit(EXIT_CODES.AUTH_FAILURE);
18228
- }
18229
- const parentOpts = program3.opts();
18230
- if (parentOpts.dryRun) {
18231
- writeOutput(
18232
- JSON.stringify(
18233
- {
18234
- method: "POST",
18235
- endpoint,
18236
- params: { basDd: date5 },
18237
- headers: { AUTH_KEY: "***" }
18238
- },
18239
- null,
18240
- 2
18241
- )
18242
- );
18243
- return;
18244
- }
18245
- const result = await krxFetch({
18246
- endpoint,
18247
- params: { basDd: date5 },
18248
- apiKey
18249
- });
18250
- if (!result.success) {
18251
- handleKrxError(result);
18252
- }
18253
- if (result.data.length === 0) {
18254
- writeError(`No data for date ${date5}`);
18255
- process.exit(EXIT_CODES.NO_DATA);
18256
- }
18257
- const format = detectOutputFormat(parentOpts.output);
18258
- const fields = parentOpts.fields?.split(",");
18259
- writeOutput(
18260
- formatOutput(
18261
- result.data,
18262
- format,
18263
- fields
18264
- )
18265
- );
18266
- } catch (err) {
18267
- writeError(err instanceof Error ? err.message : String(err));
18268
- process.exit(EXIT_CODES.USAGE_ERROR);
18269
- }
18270
- });
18480
+ // src/utils/krx-number.ts
18481
+ function parseKrxNumber(value) {
18482
+ const cleaned = value.replace(/,/g, "");
18483
+ const num = parseFloat(cleaned);
18484
+ return isNaN(num) ? 0 : num;
18485
+ }
18486
+ function isKrxNumericString(value) {
18487
+ return /^-?[\d,]+\.?\d*$/.test(value.trim());
18271
18488
  }
18272
18489
 
18273
- // src/cli/commands/stock.ts
18274
- var TRADING_ENDPOINTS = {
18275
- kospi: "/svc/apis/sto/stk_bydd_trd",
18276
- kosdaq: "/svc/apis/sto/ksq_bydd_trd",
18277
- konex: "/svc/apis/sto/knx_bydd_trd"
18278
- };
18279
- var INFO_ENDPOINTS = {
18280
- kospi: "/svc/apis/sto/stk_isu_base_info",
18281
- kosdaq: "/svc/apis/sto/ksq_isu_base_info",
18282
- konex: "/svc/apis/sto/knx_isu_base_info"
18283
- };
18284
- function registerStockCommand(program3) {
18285
- const stock = program3.command("stock").description("Query KRX stock data");
18286
- stock.command("list").description("List stock daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
18490
+ // src/utils/filter.ts
18491
+ var OPERATOR_PATTERN = /^(\S+)\s+(>=|<=|==|!=|>|<)\s+(.+)$/;
18492
+ function parseFilterExpression(expression) {
18493
+ const trimmed = expression.trim().replace(/\s+/g, " ");
18494
+ const match = OPERATOR_PATTERN.exec(trimmed);
18495
+ if (!match || !match[1] || !match[2] || !match[3]) {
18496
+ return null;
18497
+ }
18498
+ return {
18499
+ field: match[1],
18500
+ operator: match[2],
18501
+ value: match[3].trim()
18502
+ };
18503
+ }
18504
+ function compareValues(fieldValue, operator, filterValue) {
18505
+ const isNumeric = isKrxNumericString(fieldValue) && isKrxNumericString(filterValue);
18506
+ if (isNumeric) {
18507
+ const a = parseKrxNumber(fieldValue);
18508
+ const b = parseKrxNumber(filterValue);
18509
+ switch (operator) {
18510
+ case ">":
18511
+ return a > b;
18512
+ case "<":
18513
+ return a < b;
18514
+ case ">=":
18515
+ return a >= b;
18516
+ case "<=":
18517
+ return a <= b;
18518
+ case "==":
18519
+ return a === b;
18520
+ case "!=":
18521
+ return a !== b;
18522
+ }
18523
+ }
18524
+ switch (operator) {
18525
+ case "==":
18526
+ return fieldValue === filterValue;
18527
+ case "!=":
18528
+ return fieldValue !== filterValue;
18529
+ case ">":
18530
+ return fieldValue > filterValue;
18531
+ case "<":
18532
+ return fieldValue < filterValue;
18533
+ case ">=":
18534
+ return fieldValue >= filterValue;
18535
+ case "<=":
18536
+ return fieldValue <= filterValue;
18537
+ default: {
18538
+ const _exhaustive = operator;
18539
+ void _exhaustive;
18540
+ return false;
18541
+ }
18542
+ }
18543
+ }
18544
+ function filterData(data, expression) {
18545
+ const parsed = parseFilterExpression(expression);
18546
+ if (!parsed) {
18547
+ return data;
18548
+ }
18549
+ return data.filter((row) => {
18550
+ const fieldValue = row[parsed.field];
18551
+ if (fieldValue === void 0 || fieldValue === null) {
18552
+ return false;
18553
+ }
18554
+ return compareValues(String(fieldValue), parsed.operator, parsed.value);
18555
+ });
18556
+ }
18557
+
18558
+ // src/utils/data-pipeline.ts
18559
+ function sortData(data, field, direction = "desc") {
18560
+ const sorted = [...data].sort((a, b) => {
18561
+ const aVal = String(a[field] ?? "");
18562
+ const bVal = String(b[field] ?? "");
18563
+ if (isKrxNumericString(aVal) && isKrxNumericString(bVal)) {
18564
+ return parseKrxNumber(aVal) - parseKrxNumber(bVal);
18565
+ }
18566
+ return aVal.localeCompare(bVal, "ko");
18567
+ });
18568
+ return direction === "desc" ? sorted.reverse() : sorted;
18569
+ }
18570
+ function limitData(data, limit) {
18571
+ return data.slice(0, limit);
18572
+ }
18573
+ function applyPipeline(data, options) {
18574
+ let result = data;
18575
+ if (options.filter) {
18576
+ result = filterData(result, options.filter);
18577
+ }
18578
+ if (options.sort) {
18579
+ result = sortData(result, options.sort, options.direction ?? "desc");
18580
+ }
18581
+ if (options.limit && options.limit > 0) {
18582
+ result = limitData(result, options.limit);
18583
+ }
18584
+ return result;
18585
+ }
18586
+
18587
+ // src/cli/command-helper.ts
18588
+ async function executeCommand(options) {
18589
+ const { endpoint, params, program: program3, noDataMessage } = options;
18590
+ const apiKey = getApiKey();
18591
+ if (!apiKey) {
18592
+ writeError(
18593
+ "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18594
+ );
18595
+ process.exit(EXIT_CODES.AUTH_FAILURE);
18596
+ }
18597
+ const parentOpts = program3.opts();
18598
+ const finalParams = parentOpts.code ? { ...params, isuCd: parentOpts.code } : params;
18599
+ if (parentOpts.dryRun) {
18600
+ writeOutput(
18601
+ JSON.stringify(
18602
+ {
18603
+ method: "POST",
18604
+ endpoint,
18605
+ params: finalParams,
18606
+ headers: { AUTH_KEY: "***" }
18607
+ },
18608
+ null,
18609
+ 2
18610
+ )
18611
+ );
18612
+ return;
18613
+ }
18614
+ const fromDate = parentOpts.from;
18615
+ const toDate = parentOpts.to;
18616
+ if (fromDate && !toDate || !fromDate && toDate) {
18617
+ writeError("Both --from and --to must be provided together");
18618
+ process.exit(EXIT_CODES.USAGE_ERROR);
18619
+ }
18620
+ if (fromDate) {
18287
18621
  try {
18288
- const date5 = validateDate(opts.date);
18289
- const market = validateMarket(opts.market);
18290
- const endpoint = TRADING_ENDPOINTS[market];
18291
- if (!endpoint) {
18292
- writeError(`Invalid market for stock list: ${market}`);
18293
- process.exit(EXIT_CODES.USAGE_ERROR);
18294
- }
18295
- const apiKey = getApiKey();
18296
- if (!apiKey) {
18297
- writeError(
18298
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18299
- );
18300
- process.exit(EXIT_CODES.AUTH_FAILURE);
18301
- }
18302
- const parentOpts = program3.opts();
18303
- if (parentOpts.dryRun) {
18304
- writeOutput(
18305
- JSON.stringify(
18306
- {
18307
- method: "POST",
18308
- endpoint,
18309
- params: { basDd: date5 },
18310
- headers: { AUTH_KEY: "***" }
18311
- },
18312
- null,
18313
- 2
18314
- )
18315
- );
18316
- return;
18317
- }
18318
- const result = await krxFetch({
18319
- endpoint,
18320
- params: { basDd: date5 },
18321
- apiKey
18322
- });
18323
- if (!result.success) {
18324
- handleKrxError(result);
18325
- }
18326
- if (result.data.length === 0) {
18327
- writeError(`No data for date ${date5}`);
18328
- process.exit(EXIT_CODES.NO_DATA);
18329
- }
18330
- const format = detectOutputFormat(parentOpts.output);
18331
- const fields = parentOpts.fields?.split(",");
18332
- writeOutput(
18333
- formatOutput(
18334
- result.data,
18335
- format,
18336
- fields
18337
- )
18622
+ validateDate(fromDate);
18623
+ } catch (err) {
18624
+ writeError(
18625
+ `Invalid --from date: ${err instanceof Error ? err.message : String(err)}`
18338
18626
  );
18627
+ process.exit(EXIT_CODES.USAGE_ERROR);
18628
+ }
18629
+ }
18630
+ if (toDate) {
18631
+ try {
18632
+ validateDate(toDate);
18339
18633
  } catch (err) {
18340
- writeError(err instanceof Error ? err.message : String(err));
18634
+ writeError(
18635
+ `Invalid --to date: ${err instanceof Error ? err.message : String(err)}`
18636
+ );
18341
18637
  process.exit(EXIT_CODES.USAGE_ERROR);
18342
18638
  }
18639
+ }
18640
+ const isDateRange = fromDate && toDate;
18641
+ let data;
18642
+ if (isDateRange) {
18643
+ const restParams = Object.fromEntries(
18644
+ Object.entries(finalParams).filter(([k]) => k !== "basDd")
18645
+ );
18646
+ const rangeResult = await fetchDateRange({
18647
+ endpoint,
18648
+ from: fromDate,
18649
+ to: toDate,
18650
+ apiKey,
18651
+ cache: parentOpts.cache,
18652
+ extraParams: restParams
18653
+ });
18654
+ if (!rangeResult.success) {
18655
+ writeError(rangeResult.error ?? "Date range fetch failed");
18656
+ process.exit(EXIT_CODES.GENERAL_ERROR);
18657
+ }
18658
+ if (rangeResult.failedDays > 0) {
18659
+ writeError(`Warning: ${rangeResult.failedDays} day(s) failed to fetch`);
18660
+ }
18661
+ data = rangeResult.data;
18662
+ } else {
18663
+ const result = await krxFetch({
18664
+ endpoint,
18665
+ params: finalParams,
18666
+ apiKey,
18667
+ cache: parentOpts.cache,
18668
+ retries: parentOpts.retries
18669
+ });
18670
+ if (!result.success) {
18671
+ handleKrxError(result);
18672
+ }
18673
+ data = result.data;
18674
+ }
18675
+ if (data.length === 0) {
18676
+ writeError(noDataMessage ?? "No data");
18677
+ process.exit(EXIT_CODES.NO_DATA);
18678
+ }
18679
+ data = applyPipeline(data, {
18680
+ filter: parentOpts.filter,
18681
+ sort: parentOpts.sort,
18682
+ direction: parentOpts.asc ? "asc" : "desc",
18683
+ limit: parentOpts.limit
18343
18684
  });
18344
- stock.command("info").description("List stock base information").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
18685
+ if (data.length === 0) {
18686
+ writeError(
18687
+ parentOpts.filter ? `No results matched filter: ${parentOpts.filter}` : noDataMessage ?? "No data"
18688
+ );
18689
+ process.exit(EXIT_CODES.NO_DATA);
18690
+ }
18691
+ const format = detectOutputFormat(parentOpts.output);
18692
+ const fields = parentOpts.fields?.split(",");
18693
+ const output = formatOutput(data, format, fields);
18694
+ const savePath = parentOpts.save;
18695
+ if (savePath) {
18345
18696
  try {
18346
- const market = validateMarket(opts.market);
18347
- const endpoint = INFO_ENDPOINTS[market];
18348
- if (!endpoint) {
18349
- writeError(`Invalid market for stock info: ${market}`);
18350
- process.exit(EXIT_CODES.USAGE_ERROR);
18351
- }
18352
- const apiKey = getApiKey();
18353
- if (!apiKey) {
18354
- writeError(
18355
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18356
- );
18357
- process.exit(EXIT_CODES.AUTH_FAILURE);
18358
- }
18359
- const parentOpts = program3.opts();
18360
- if (parentOpts.dryRun) {
18361
- writeOutput(
18362
- JSON.stringify(
18363
- {
18364
- method: "POST",
18365
- endpoint,
18366
- params: {},
18367
- headers: { AUTH_KEY: "***" }
18368
- },
18369
- null,
18370
- 2
18371
- )
18372
- );
18373
- return;
18374
- }
18697
+ const dir = path4.dirname(savePath);
18698
+ fs4.mkdirSync(dir, { recursive: true });
18699
+ fs4.writeFileSync(savePath, output + "\n", "utf-8");
18700
+ writeOutput(JSON.stringify({ saved: savePath, records: data.length }));
18701
+ } catch (err) {
18702
+ writeError(
18703
+ `Failed to save file: ${err instanceof Error ? err.message : String(err)}`
18704
+ );
18705
+ process.exit(EXIT_CODES.GENERAL_ERROR);
18706
+ }
18707
+ } else {
18708
+ writeOutput(output);
18709
+ }
18710
+ }
18711
+ function resolveEndpoint(endpoints, key, label) {
18712
+ const endpoint = endpoints[key.toLowerCase()];
18713
+ if (!endpoint) {
18714
+ writeError(
18715
+ `Invalid ${label}: ${key}. Must be one of: ${Object.keys(endpoints).join(", ")}`
18716
+ );
18717
+ process.exit(EXIT_CODES.USAGE_ERROR);
18718
+ }
18719
+ return endpoint;
18720
+ }
18721
+
18722
+ // src/cli/commands/index-cmd.ts
18723
+ var MARKET_ENDPOINTS = {
18724
+ kospi: "/svc/apis/idx/kospi_dd_trd",
18725
+ kosdaq: "/svc/apis/idx/kosdaq_dd_trd",
18726
+ krx: "/svc/apis/idx/krx_dd_trd",
18727
+ bond: "/svc/apis/idx/bon_dd_trd",
18728
+ derivative: "/svc/apis/idx/drvprod_dd_trd"
18729
+ };
18730
+ function registerIndexCommand(program3) {
18731
+ const index = program3.command("index").description("Query KRX index data");
18732
+ index.command("list").description("List index daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
18733
+ "--market <market>",
18734
+ "market: kospi, kosdaq, krx, bond, derivative",
18735
+ "kospi"
18736
+ ).action(async (opts) => {
18737
+ const date5 = validateDate(opts.date);
18738
+ const endpoint = resolveEndpoint(MARKET_ENDPOINTS, opts.market, "market");
18739
+ await executeCommand({
18740
+ endpoint,
18741
+ params: { basDd: date5 },
18742
+ program: program3,
18743
+ noDataMessage: `No data for date ${date5}`
18744
+ });
18745
+ });
18746
+ }
18747
+
18748
+ // src/client/search.ts
18749
+ var BASE_INFO_ENDPOINTS = [
18750
+ { endpoint: "/svc/apis/sto/stk_isu_base_info", market: "KOSPI" },
18751
+ { endpoint: "/svc/apis/sto/ksq_isu_base_info", market: "KOSDAQ" }
18752
+ ];
18753
+ async function searchStock(apiKey, query) {
18754
+ const basDd = getRecentTradingDate();
18755
+ const lowerQuery = query.toLowerCase();
18756
+ const results = await Promise.all(
18757
+ BASE_INFO_ENDPOINTS.map(async ({ endpoint, market }) => {
18375
18758
  const result = await krxFetch({
18376
18759
  endpoint,
18377
- params: {},
18760
+ params: { basDd },
18378
18761
  apiKey
18379
18762
  });
18380
18763
  if (!result.success) {
18381
- handleKrxError(result);
18382
- }
18383
- if (result.data.length === 0) {
18384
- writeError("No data");
18385
- process.exit(EXIT_CODES.NO_DATA);
18764
+ return [];
18386
18765
  }
18387
- const format = detectOutputFormat(parentOpts.output);
18388
- const fields = parentOpts.fields?.split(",");
18389
- writeOutput(
18390
- formatOutput(
18391
- result.data,
18392
- format,
18393
- fields
18394
- )
18766
+ return result.data.filter((row) => {
18767
+ const name = row["ISU_NM"] ?? "";
18768
+ const shortName = row["ISU_ABBRV"] ?? "";
18769
+ return name.toLowerCase().includes(lowerQuery) || shortName.toLowerCase().includes(lowerQuery);
18770
+ }).map((row) => ({
18771
+ ISU_CD: row["ISU_CD"] ?? "",
18772
+ ISU_SRT_CD: row["ISU_SRT_CD"] ?? "",
18773
+ ISU_NM: row["ISU_NM"] ?? row["ISU_ABBRV"] ?? "",
18774
+ MKT_NM: market
18775
+ }));
18776
+ })
18777
+ );
18778
+ return results.flat();
18779
+ }
18780
+
18781
+ // src/cli/commands/stock.ts
18782
+ var TRADING_ENDPOINTS = {
18783
+ kospi: "/svc/apis/sto/stk_bydd_trd",
18784
+ kosdaq: "/svc/apis/sto/ksq_bydd_trd",
18785
+ konex: "/svc/apis/sto/knx_bydd_trd"
18786
+ };
18787
+ var INFO_ENDPOINTS = {
18788
+ kospi: "/svc/apis/sto/stk_isu_base_info",
18789
+ kosdaq: "/svc/apis/sto/ksq_isu_base_info",
18790
+ konex: "/svc/apis/sto/knx_isu_base_info"
18791
+ };
18792
+ function registerStockCommand(program3) {
18793
+ const stock = program3.command("stock").description("Query KRX stock data");
18794
+ stock.command("list").description("List stock daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
18795
+ const date5 = validateDate(opts.date);
18796
+ validateMarket(opts.market);
18797
+ const endpoint = resolveEndpoint(
18798
+ TRADING_ENDPOINTS,
18799
+ opts.market,
18800
+ "market"
18801
+ );
18802
+ await executeCommand({
18803
+ endpoint,
18804
+ params: { basDd: date5 },
18805
+ program: program3,
18806
+ noDataMessage: `No data for date ${date5}`
18807
+ });
18808
+ });
18809
+ stock.command("info").description("List stock base information").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
18810
+ validateMarket(opts.market);
18811
+ const endpoint = resolveEndpoint(INFO_ENDPOINTS, opts.market, "market");
18812
+ await executeCommand({
18813
+ endpoint,
18814
+ params: {},
18815
+ program: program3
18816
+ });
18817
+ });
18818
+ stock.command("search <query>").description("Search stocks by name").action(async (query) => {
18819
+ validateNoInjection(query);
18820
+ const apiKey = getApiKey();
18821
+ if (!apiKey) {
18822
+ writeError(
18823
+ "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18395
18824
  );
18396
- } catch (err) {
18397
- writeError(err instanceof Error ? err.message : String(err));
18398
- process.exit(EXIT_CODES.USAGE_ERROR);
18825
+ process.exit(EXIT_CODES.AUTH_FAILURE);
18399
18826
  }
18827
+ const results = await searchStock(apiKey, query);
18828
+ if (results.length === 0) {
18829
+ writeError(`No stocks found matching "${query}"`);
18830
+ process.exit(EXIT_CODES.NO_DATA);
18831
+ }
18832
+ const parentOpts = program3.opts();
18833
+ const format = detectOutputFormat(parentOpts.output);
18834
+ const fields = parentOpts.fields?.split(",");
18835
+ writeOutput(
18836
+ formatOutput(
18837
+ results,
18838
+ format,
18839
+ fields
18840
+ )
18841
+ );
18400
18842
  });
18401
18843
  }
18402
18844
 
@@ -18409,64 +18851,14 @@ var TYPE_ENDPOINTS = {
18409
18851
  function registerEtpCommand(program3) {
18410
18852
  const etp = program3.command("etp").description("Query KRX ETP data (ETF/ETN/ELW)");
18411
18853
  etp.command("list").description("List ETP daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: etf, etn, elw", "etf").action(async (opts) => {
18412
- try {
18413
- const date5 = validateDate(opts.date);
18414
- const type = opts.type.toLowerCase();
18415
- const endpoint = TYPE_ENDPOINTS[type];
18416
- if (!endpoint) {
18417
- writeError(
18418
- `Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS).join(", ")}`
18419
- );
18420
- process.exit(EXIT_CODES.USAGE_ERROR);
18421
- }
18422
- const apiKey = getApiKey();
18423
- if (!apiKey) {
18424
- writeError(
18425
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18426
- );
18427
- process.exit(EXIT_CODES.AUTH_FAILURE);
18428
- }
18429
- const parentOpts = program3.opts();
18430
- if (parentOpts.dryRun) {
18431
- writeOutput(
18432
- JSON.stringify(
18433
- {
18434
- method: "POST",
18435
- endpoint,
18436
- params: { basDd: date5 },
18437
- headers: { AUTH_KEY: "***" }
18438
- },
18439
- null,
18440
- 2
18441
- )
18442
- );
18443
- return;
18444
- }
18445
- const result = await krxFetch({
18446
- endpoint,
18447
- params: { basDd: date5 },
18448
- apiKey
18449
- });
18450
- if (!result.success) {
18451
- handleKrxError(result);
18452
- }
18453
- if (result.data.length === 0) {
18454
- writeError(`No data for date ${date5}`);
18455
- process.exit(EXIT_CODES.NO_DATA);
18456
- }
18457
- const format = detectOutputFormat(parentOpts.output);
18458
- const fields = parentOpts.fields?.split(",");
18459
- writeOutput(
18460
- formatOutput(
18461
- result.data,
18462
- format,
18463
- fields
18464
- )
18465
- );
18466
- } catch (err) {
18467
- writeError(err instanceof Error ? err.message : String(err));
18468
- process.exit(EXIT_CODES.USAGE_ERROR);
18469
- }
18854
+ const date5 = validateDate(opts.date);
18855
+ const endpoint = resolveEndpoint(TYPE_ENDPOINTS, opts.type, "type");
18856
+ await executeCommand({
18857
+ endpoint,
18858
+ params: { basDd: date5 },
18859
+ program: program3,
18860
+ noDataMessage: `No data for date ${date5}`
18861
+ });
18470
18862
  });
18471
18863
  }
18472
18864
 
@@ -18479,64 +18871,14 @@ var MARKET_ENDPOINTS2 = {
18479
18871
  function registerBondCommand(program3) {
18480
18872
  const bond = program3.command("bond").description("Query KRX bond data");
18481
18873
  bond.command("list").description("List bond daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--market <market>", "market: kts, general, small", "general").action(async (opts) => {
18482
- try {
18483
- const date5 = validateDate(opts.date);
18484
- const market = opts.market.toLowerCase();
18485
- const endpoint = MARKET_ENDPOINTS2[market];
18486
- if (!endpoint) {
18487
- writeError(
18488
- `Invalid market: ${market}. Must be one of: ${Object.keys(MARKET_ENDPOINTS2).join(", ")}`
18489
- );
18490
- process.exit(EXIT_CODES.USAGE_ERROR);
18491
- }
18492
- const apiKey = getApiKey();
18493
- if (!apiKey) {
18494
- writeError(
18495
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18496
- );
18497
- process.exit(EXIT_CODES.AUTH_FAILURE);
18498
- }
18499
- const parentOpts = program3.opts();
18500
- if (parentOpts.dryRun) {
18501
- writeOutput(
18502
- JSON.stringify(
18503
- {
18504
- method: "POST",
18505
- endpoint,
18506
- params: { basDd: date5 },
18507
- headers: { AUTH_KEY: "***" }
18508
- },
18509
- null,
18510
- 2
18511
- )
18512
- );
18513
- return;
18514
- }
18515
- const result = await krxFetch({
18516
- endpoint,
18517
- params: { basDd: date5 },
18518
- apiKey
18519
- });
18520
- if (!result.success) {
18521
- handleKrxError(result);
18522
- }
18523
- if (result.data.length === 0) {
18524
- writeError(`No data for date ${date5}`);
18525
- process.exit(EXIT_CODES.NO_DATA);
18526
- }
18527
- const format = detectOutputFormat(parentOpts.output);
18528
- const fields = parentOpts.fields?.split(",");
18529
- writeOutput(
18530
- formatOutput(
18531
- result.data,
18532
- format,
18533
- fields
18534
- )
18535
- );
18536
- } catch (err) {
18537
- writeError(err instanceof Error ? err.message : String(err));
18538
- process.exit(EXIT_CODES.USAGE_ERROR);
18539
- }
18874
+ const date5 = validateDate(opts.date);
18875
+ const endpoint = resolveEndpoint(MARKET_ENDPOINTS2, opts.market, "market");
18876
+ await executeCommand({
18877
+ endpoint,
18878
+ params: { basDd: date5 },
18879
+ program: program3,
18880
+ noDataMessage: `No data for date ${date5}`
18881
+ });
18540
18882
  });
18541
18883
  }
18542
18884
 
@@ -18556,64 +18898,14 @@ function registerDerivativeCommand(program3) {
18556
18898
  "type: futures, futures-kospi, futures-kosdaq, options, options-kospi, options-kosdaq",
18557
18899
  "futures"
18558
18900
  ).action(async (opts) => {
18559
- try {
18560
- const date5 = validateDate(opts.date);
18561
- const type = opts.type.toLowerCase();
18562
- const endpoint = TYPE_ENDPOINTS2[type];
18563
- if (!endpoint) {
18564
- writeError(
18565
- `Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS2).join(", ")}`
18566
- );
18567
- process.exit(EXIT_CODES.USAGE_ERROR);
18568
- }
18569
- const apiKey = getApiKey();
18570
- if (!apiKey) {
18571
- writeError(
18572
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18573
- );
18574
- process.exit(EXIT_CODES.AUTH_FAILURE);
18575
- }
18576
- const parentOpts = program3.opts();
18577
- if (parentOpts.dryRun) {
18578
- writeOutput(
18579
- JSON.stringify(
18580
- {
18581
- method: "POST",
18582
- endpoint,
18583
- params: { basDd: date5 },
18584
- headers: { AUTH_KEY: "***" }
18585
- },
18586
- null,
18587
- 2
18588
- )
18589
- );
18590
- return;
18591
- }
18592
- const result = await krxFetch({
18593
- endpoint,
18594
- params: { basDd: date5 },
18595
- apiKey
18596
- });
18597
- if (!result.success) {
18598
- handleKrxError(result);
18599
- }
18600
- if (result.data.length === 0) {
18601
- writeError(`No data for date ${date5}`);
18602
- process.exit(EXIT_CODES.NO_DATA);
18603
- }
18604
- const format = detectOutputFormat(parentOpts.output);
18605
- const fields = parentOpts.fields?.split(",");
18606
- writeOutput(
18607
- formatOutput(
18608
- result.data,
18609
- format,
18610
- fields
18611
- )
18612
- );
18613
- } catch (err) {
18614
- writeError(err instanceof Error ? err.message : String(err));
18615
- process.exit(EXIT_CODES.USAGE_ERROR);
18616
- }
18901
+ const date5 = validateDate(opts.date);
18902
+ const endpoint = resolveEndpoint(TYPE_ENDPOINTS2, opts.type, "type");
18903
+ await executeCommand({
18904
+ endpoint,
18905
+ params: { basDd: date5 },
18906
+ program: program3,
18907
+ noDataMessage: `No data for date ${date5}`
18908
+ });
18617
18909
  });
18618
18910
  }
18619
18911
 
@@ -18626,64 +18918,14 @@ var TYPE_ENDPOINTS3 = {
18626
18918
  function registerCommodityCommand(program3) {
18627
18919
  const commodity = program3.command("commodity").description("Query KRX commodity data (oil/gold/emission)");
18628
18920
  commodity.command("list").description("List commodity daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: oil, gold, emission", "gold").action(async (opts) => {
18629
- try {
18630
- const date5 = validateDate(opts.date);
18631
- const type = opts.type.toLowerCase();
18632
- const endpoint = TYPE_ENDPOINTS3[type];
18633
- if (!endpoint) {
18634
- writeError(
18635
- `Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS3).join(", ")}`
18636
- );
18637
- process.exit(EXIT_CODES.USAGE_ERROR);
18638
- }
18639
- const apiKey = getApiKey();
18640
- if (!apiKey) {
18641
- writeError(
18642
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18643
- );
18644
- process.exit(EXIT_CODES.AUTH_FAILURE);
18645
- }
18646
- const parentOpts = program3.opts();
18647
- if (parentOpts.dryRun) {
18648
- writeOutput(
18649
- JSON.stringify(
18650
- {
18651
- method: "POST",
18652
- endpoint,
18653
- params: { basDd: date5 },
18654
- headers: { AUTH_KEY: "***" }
18655
- },
18656
- null,
18657
- 2
18658
- )
18659
- );
18660
- return;
18661
- }
18662
- const result = await krxFetch({
18663
- endpoint,
18664
- params: { basDd: date5 },
18665
- apiKey
18666
- });
18667
- if (!result.success) {
18668
- handleKrxError(result);
18669
- }
18670
- if (result.data.length === 0) {
18671
- writeError(`No data for date ${date5}`);
18672
- process.exit(EXIT_CODES.NO_DATA);
18673
- }
18674
- const format = detectOutputFormat(parentOpts.output);
18675
- const fields = parentOpts.fields?.split(",");
18676
- writeOutput(
18677
- formatOutput(
18678
- result.data,
18679
- format,
18680
- fields
18681
- )
18682
- );
18683
- } catch (err) {
18684
- writeError(err instanceof Error ? err.message : String(err));
18685
- process.exit(EXIT_CODES.USAGE_ERROR);
18686
- }
18921
+ const date5 = validateDate(opts.date);
18922
+ const endpoint = resolveEndpoint(TYPE_ENDPOINTS3, opts.type, "type");
18923
+ await executeCommand({
18924
+ endpoint,
18925
+ params: { basDd: date5 },
18926
+ program: program3,
18927
+ noDataMessage: `No data for date ${date5}`
18928
+ });
18687
18929
  });
18688
18930
  }
18689
18931
 
@@ -18696,64 +18938,14 @@ var TYPE_ENDPOINTS4 = {
18696
18938
  function registerEsgCommand(program3) {
18697
18939
  const esg = program3.command("esg").description("Query KRX ESG data");
18698
18940
  esg.command("list").description("List ESG data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: sri-bond, etp, index", "index").action(async (opts) => {
18699
- try {
18700
- const date5 = validateDate(opts.date);
18701
- const type = opts.type.toLowerCase();
18702
- const endpoint = TYPE_ENDPOINTS4[type];
18703
- if (!endpoint) {
18704
- writeError(
18705
- `Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS4).join(", ")}`
18706
- );
18707
- process.exit(EXIT_CODES.USAGE_ERROR);
18708
- }
18709
- const apiKey = getApiKey();
18710
- if (!apiKey) {
18711
- writeError(
18712
- "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
18713
- );
18714
- process.exit(EXIT_CODES.AUTH_FAILURE);
18715
- }
18716
- const parentOpts = program3.opts();
18717
- if (parentOpts.dryRun) {
18718
- writeOutput(
18719
- JSON.stringify(
18720
- {
18721
- method: "POST",
18722
- endpoint,
18723
- params: { basDd: date5 },
18724
- headers: { AUTH_KEY: "***" }
18725
- },
18726
- null,
18727
- 2
18728
- )
18729
- );
18730
- return;
18731
- }
18732
- const result = await krxFetch({
18733
- endpoint,
18734
- params: { basDd: date5 },
18735
- apiKey
18736
- });
18737
- if (!result.success) {
18738
- handleKrxError(result);
18739
- }
18740
- if (result.data.length === 0) {
18741
- writeError(`No data for date ${date5}`);
18742
- process.exit(EXIT_CODES.NO_DATA);
18743
- }
18744
- const format = detectOutputFormat(parentOpts.output);
18745
- const fields = parentOpts.fields?.split(",");
18746
- writeOutput(
18747
- formatOutput(
18748
- result.data,
18749
- format,
18750
- fields
18751
- )
18752
- );
18753
- } catch (err) {
18754
- writeError(err instanceof Error ? err.message : String(err));
18755
- process.exit(EXIT_CODES.USAGE_ERROR);
18756
- }
18941
+ const date5 = validateDate(opts.date);
18942
+ const endpoint = resolveEndpoint(TYPE_ENDPOINTS4, opts.type, "type");
18943
+ await executeCommand({
18944
+ endpoint,
18945
+ params: { basDd: date5 },
18946
+ program: program3,
18947
+ noDataMessage: `No data for date ${date5}`
18948
+ });
18757
18949
  });
18758
18950
  }
18759
18951
 
@@ -18805,20 +18997,402 @@ function registerSchemaCommand(program3) {
18805
18997
  });
18806
18998
  }
18807
18999
 
19000
+ // src/cli/commands/cache.ts
19001
+ function registerCacheCommand(program3) {
19002
+ const cache = program3.command("cache").description("Manage API response cache");
19003
+ cache.command("status").description("Show cache status").action(() => {
19004
+ const status = getCacheStatus();
19005
+ writeOutput(
19006
+ JSON.stringify(
19007
+ {
19008
+ dates: status.dates,
19009
+ files: status.totalFiles,
19010
+ sizeBytes: status.totalSize,
19011
+ sizeMB: Math.round(status.totalSize / 1024 / 1024 * 100) / 100
19012
+ },
19013
+ null,
19014
+ 2
19015
+ )
19016
+ );
19017
+ });
19018
+ cache.command("clear").description("Clear all cached data").action(() => {
19019
+ const result = clearCache();
19020
+ writeOutput(
19021
+ JSON.stringify(
19022
+ {
19023
+ cleared: true,
19024
+ files: result.files,
19025
+ directories: result.directories
19026
+ },
19027
+ null,
19028
+ 2
19029
+ )
19030
+ );
19031
+ });
19032
+ }
19033
+
19034
+ // src/client/market-summary.ts
19035
+ var KOSPI_INDEX_ENDPOINT = "/svc/apis/idx/kospi_dd_trd";
19036
+ var KOSDAQ_INDEX_ENDPOINT = "/svc/apis/idx/kosdaq_dd_trd";
19037
+ var KOSPI_STOCK_ENDPOINT = "/svc/apis/sto/stk_bydd_trd";
19038
+ var KOSDAQ_STOCK_ENDPOINT = "/svc/apis/sto/ksq_bydd_trd";
19039
+ var TOP_N = 5;
19040
+ function computeStockStats(stocks) {
19041
+ return stocks.reduce(
19042
+ (acc, stock) => {
19043
+ const rate = parseKrxNumber(stock["FLUC_RT"] ?? "0");
19044
+ return {
19045
+ advancing: acc.advancing + (rate > 0 ? 1 : 0),
19046
+ declining: acc.declining + (rate < 0 ? 1 : 0),
19047
+ unchanged: acc.unchanged + (rate === 0 ? 1 : 0),
19048
+ totalVolume: acc.totalVolume + parseKrxNumber(stock["ACC_TRDVOL"] ?? "0"),
19049
+ totalValue: acc.totalValue + parseKrxNumber(stock["ACC_TRDVAL"] ?? "0")
19050
+ };
19051
+ },
19052
+ {
19053
+ advancing: 0,
19054
+ declining: 0,
19055
+ unchanged: 0,
19056
+ totalVolume: 0,
19057
+ totalValue: 0
19058
+ }
19059
+ );
19060
+ }
19061
+ function computeTopMovers(stocks) {
19062
+ const sorted = [...stocks].sort((a, b) => {
19063
+ const rateA = parseKrxNumber(a["FLUC_RT"] ?? "0");
19064
+ const rateB = parseKrxNumber(b["FLUC_RT"] ?? "0");
19065
+ return rateB - rateA;
19066
+ });
19067
+ const topGainers = sorted.slice(0, TOP_N);
19068
+ const topLosers = sorted.slice(-TOP_N).reverse();
19069
+ return { topGainers, topLosers };
19070
+ }
19071
+ function safeFetch(...args) {
19072
+ return krxFetch(...args).catch(
19073
+ (err) => ({
19074
+ success: false,
19075
+ data: [],
19076
+ error: err instanceof Error ? err.message : "Network error"
19077
+ })
19078
+ );
19079
+ }
19080
+ async function fetchMarketSummary(options) {
19081
+ const { apiKey, date: date5, cache } = options;
19082
+ const params = { basDd: date5 };
19083
+ const [kospiIdx, kosdaqIdx, kospiStk, kosdaqStk] = await Promise.all([
19084
+ safeFetch({ endpoint: KOSPI_INDEX_ENDPOINT, params, apiKey, cache }),
19085
+ safeFetch({ endpoint: KOSDAQ_INDEX_ENDPOINT, params, apiKey, cache }),
19086
+ safeFetch({ endpoint: KOSPI_STOCK_ENDPOINT, params, apiKey, cache }),
19087
+ safeFetch({ endpoint: KOSDAQ_STOCK_ENDPOINT, params, apiKey, cache })
19088
+ ]);
19089
+ const allFailed = !kospiIdx.success && !kosdaqIdx.success && !kospiStk.success && !kosdaqStk.success;
19090
+ if (allFailed) {
19091
+ return {
19092
+ success: false,
19093
+ error: "All market data fetches failed"
19094
+ };
19095
+ }
19096
+ const kospiIndexData = kospiIdx.success ? kospiIdx.data : [];
19097
+ const kosdaqIndexData = kosdaqIdx.success ? kosdaqIdx.data : [];
19098
+ const kospiStockData = kospiStk.success ? kospiStk.data : [];
19099
+ const kosdaqStockData = kosdaqStk.success ? kosdaqStk.data : [];
19100
+ const allStocks = [...kospiStockData, ...kosdaqStockData];
19101
+ const stockStats = computeStockStats(allStocks);
19102
+ const { topGainers, topLosers } = computeTopMovers(allStocks);
19103
+ return {
19104
+ success: true,
19105
+ data: {
19106
+ date: date5,
19107
+ kospiIndex: kospiIndexData,
19108
+ kosdaqIndex: kosdaqIndexData,
19109
+ stockStats,
19110
+ topGainers,
19111
+ topLosers
19112
+ }
19113
+ };
19114
+ }
19115
+
19116
+ // src/cli/commands/market.ts
19117
+ function registerMarketCommand(program3) {
19118
+ const market = program3.command("market").description("Market overview commands");
19119
+ market.command("summary").description("Market summary: indices, top movers, volume").option("-d, --date <date>", "trading date (YYYYMMDD)").action(async (opts) => {
19120
+ const apiKey = getApiKey();
19121
+ if (!apiKey) {
19122
+ writeError(
19123
+ "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
19124
+ );
19125
+ process.exit(EXIT_CODES.AUTH_FAILURE);
19126
+ }
19127
+ const date5 = opts.date ?? getRecentTradingDate();
19128
+ try {
19129
+ validateDate(date5);
19130
+ } catch (err) {
19131
+ writeError(
19132
+ `Invalid date: ${err instanceof Error ? err.message : String(err)}`
19133
+ );
19134
+ process.exit(EXIT_CODES.USAGE_ERROR);
19135
+ }
19136
+ const parentOpts = program3.opts();
19137
+ const result = await fetchMarketSummary({
19138
+ apiKey,
19139
+ date: date5,
19140
+ cache: parentOpts.cache
19141
+ });
19142
+ if (!result.success) {
19143
+ writeError(result.error ?? "Failed to fetch market summary");
19144
+ process.exit(EXIT_CODES.GENERAL_ERROR);
19145
+ }
19146
+ writeOutput(JSON.stringify(result.data, null, 2));
19147
+ });
19148
+ }
19149
+
19150
+ // src/watchlist/store.ts
19151
+ import * as fs5 from "node:fs";
19152
+ import * as path5 from "node:path";
19153
+ import * as os4 from "node:os";
19154
+ function getWatchlistPath() {
19155
+ return path5.join(os4.homedir(), ".krx-cli", "watchlist.json");
19156
+ }
19157
+ function getWatchlist() {
19158
+ const filePath = getWatchlistPath();
19159
+ if (!fs5.existsSync(filePath)) {
19160
+ return [];
19161
+ }
19162
+ try {
19163
+ const raw = fs5.readFileSync(filePath, "utf-8");
19164
+ return JSON.parse(raw);
19165
+ } catch (err) {
19166
+ process.stderr.write(`[krx-cli] watchlist read failed: ${String(err)}
19167
+ `);
19168
+ return [];
19169
+ }
19170
+ }
19171
+ function saveWatchlist(entries) {
19172
+ const filePath = getWatchlistPath();
19173
+ const dir = path5.dirname(filePath);
19174
+ try {
19175
+ fs5.mkdirSync(dir, { recursive: true });
19176
+ fs5.writeFileSync(filePath, JSON.stringify(entries, null, 2), "utf-8");
19177
+ return true;
19178
+ } catch (err) {
19179
+ process.stderr.write(`[krx-cli] watchlist write failed: ${String(err)}
19180
+ `);
19181
+ return false;
19182
+ }
19183
+ }
19184
+ function addToWatchlist(entry) {
19185
+ const current = getWatchlist();
19186
+ const isDuplicate = current.some((e) => e.isuCd === entry.isuCd);
19187
+ if (isDuplicate) {
19188
+ return { added: false, reason: "duplicate" };
19189
+ }
19190
+ const saved = saveWatchlist([...current, entry]);
19191
+ return saved ? { added: true } : { added: false, reason: "write_error" };
19192
+ }
19193
+ function removeFromWatchlist(nameOrCode) {
19194
+ const current = getWatchlist();
19195
+ const filtered = current.filter(
19196
+ (e) => e.isuCd !== nameOrCode && e.name !== nameOrCode
19197
+ );
19198
+ if (filtered.length === current.length) {
19199
+ return { removed: false, reason: "not_found" };
19200
+ }
19201
+ const saved = saveWatchlist(filtered);
19202
+ return saved ? { removed: true } : { removed: false, reason: "write_error" };
19203
+ }
19204
+
19205
+ // src/cli/commands/watchlist.ts
19206
+ var STOCK_ENDPOINTS = {
19207
+ KOSPI: "/svc/apis/sto/stk_bydd_trd",
19208
+ KOSDAQ: "/svc/apis/sto/ksq_bydd_trd"
19209
+ };
19210
+ function registerWatchlistCommand(program3) {
19211
+ const watchlist = program3.command("watchlist").description("Manage watchlist: add, remove, list, show");
19212
+ watchlist.command("add <name>").description("Add stock to watchlist by name (searches first)").action(async (name) => {
19213
+ const apiKey = getApiKey();
19214
+ if (!apiKey) {
19215
+ writeError(
19216
+ "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
19217
+ );
19218
+ process.exit(EXIT_CODES.AUTH_FAILURE);
19219
+ }
19220
+ const injectionError = validateNoInjection(name);
19221
+ if (injectionError) {
19222
+ writeError(`Invalid input: ${injectionError}`);
19223
+ process.exit(EXIT_CODES.USAGE_ERROR);
19224
+ }
19225
+ let results;
19226
+ try {
19227
+ results = await searchStock(apiKey, name);
19228
+ } catch (err) {
19229
+ writeError(
19230
+ `Search failed: ${err instanceof Error ? err.message : String(err)}`
19231
+ );
19232
+ process.exit(EXIT_CODES.GENERAL_ERROR);
19233
+ }
19234
+ if (results.length === 0) {
19235
+ writeError(`No stock found matching '${name}'`);
19236
+ process.exit(EXIT_CODES.NO_DATA);
19237
+ }
19238
+ if (results.length > 1) {
19239
+ writeOutput(
19240
+ JSON.stringify(
19241
+ {
19242
+ message: `Multiple matches found for '${name}'. Please be more specific.`,
19243
+ matches: results
19244
+ },
19245
+ null,
19246
+ 2
19247
+ )
19248
+ );
19249
+ process.exit(EXIT_CODES.USAGE_ERROR);
19250
+ }
19251
+ const stock = results[0];
19252
+ const result = addToWatchlist({
19253
+ isuCd: stock.ISU_CD,
19254
+ isuSrtCd: stock.ISU_SRT_CD,
19255
+ name: stock.ISU_NM,
19256
+ market: stock.MKT_NM
19257
+ });
19258
+ if (!result.added) {
19259
+ const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${stock.ISU_NM}' is already in watchlist`;
19260
+ writeError(message);
19261
+ if (result.reason === "write_error") {
19262
+ process.exit(EXIT_CODES.GENERAL_ERROR);
19263
+ }
19264
+ return;
19265
+ }
19266
+ writeOutput(
19267
+ JSON.stringify({
19268
+ message: `Added '${stock.ISU_NM}' (${stock.ISU_CD}) to watchlist`,
19269
+ entry: {
19270
+ isuCd: stock.ISU_CD,
19271
+ isuSrtCd: stock.ISU_SRT_CD,
19272
+ name: stock.ISU_NM,
19273
+ market: stock.MKT_NM
19274
+ }
19275
+ })
19276
+ );
19277
+ });
19278
+ watchlist.command("remove <name>").description("Remove stock from watchlist by name or code").action((name) => {
19279
+ const injectionError = validateNoInjection(name);
19280
+ if (injectionError) {
19281
+ writeError(`Invalid input: ${injectionError}`);
19282
+ process.exit(EXIT_CODES.USAGE_ERROR);
19283
+ }
19284
+ const result = removeFromWatchlist(name);
19285
+ if (!result.removed) {
19286
+ const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${name}' not found in watchlist`;
19287
+ writeError(message);
19288
+ process.exit(
19289
+ result.reason === "write_error" ? EXIT_CODES.GENERAL_ERROR : EXIT_CODES.NO_DATA
19290
+ );
19291
+ }
19292
+ writeOutput(
19293
+ JSON.stringify({ message: `Removed '${name}' from watchlist` })
19294
+ );
19295
+ });
19296
+ watchlist.command("list").description("List all stocks in watchlist").action(() => {
19297
+ const entries = getWatchlist();
19298
+ if (entries.length === 0) {
19299
+ writeOutput(
19300
+ JSON.stringify({ message: "Watchlist is empty", entries: [] })
19301
+ );
19302
+ return;
19303
+ }
19304
+ writeOutput(JSON.stringify(entries, null, 2));
19305
+ });
19306
+ watchlist.command("show").description("Show current prices for watchlist stocks").option("-d, --date <date>", "trading date (YYYYMMDD)").action(async (opts) => {
19307
+ const apiKey = getApiKey();
19308
+ if (!apiKey) {
19309
+ writeError(
19310
+ "No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
19311
+ );
19312
+ process.exit(EXIT_CODES.AUTH_FAILURE);
19313
+ }
19314
+ const entries = getWatchlist();
19315
+ if (entries.length === 0) {
19316
+ writeOutput(
19317
+ JSON.stringify({ message: "Watchlist is empty", entries: [] })
19318
+ );
19319
+ return;
19320
+ }
19321
+ const date5 = opts.date ?? getRecentTradingDate();
19322
+ try {
19323
+ validateDate(date5);
19324
+ } catch (err) {
19325
+ writeError(
19326
+ `Invalid date: ${err instanceof Error ? err.message : String(err)}`
19327
+ );
19328
+ process.exit(EXIT_CODES.USAGE_ERROR);
19329
+ }
19330
+ const parentOpts = program3.opts();
19331
+ const isuCds = new Set(entries.map((e) => e.isuCd));
19332
+ const errors = [];
19333
+ const [kospiResult, kosdaqResult] = await Promise.all([
19334
+ krxFetch({
19335
+ endpoint: STOCK_ENDPOINTS.KOSPI,
19336
+ params: { basDd: date5 },
19337
+ apiKey,
19338
+ cache: parentOpts.cache
19339
+ }).catch((err) => {
19340
+ errors.push(
19341
+ `KOSPI: ${err instanceof Error ? err.message : String(err)}`
19342
+ );
19343
+ return {
19344
+ success: false,
19345
+ data: []
19346
+ };
19347
+ }),
19348
+ krxFetch({
19349
+ endpoint: STOCK_ENDPOINTS.KOSDAQ,
19350
+ params: { basDd: date5 },
19351
+ apiKey,
19352
+ cache: parentOpts.cache
19353
+ }).catch((err) => {
19354
+ errors.push(
19355
+ `KOSDAQ: ${err instanceof Error ? err.message : String(err)}`
19356
+ );
19357
+ return {
19358
+ success: false,
19359
+ data: []
19360
+ };
19361
+ })
19362
+ ]);
19363
+ if (!kospiResult.success && !kosdaqResult.success) {
19364
+ writeError(`All market data fetches failed: ${errors.join("; ")}`);
19365
+ process.exit(EXIT_CODES.GENERAL_ERROR);
19366
+ }
19367
+ const allStocks = [
19368
+ ...kospiResult.success ? kospiResult.data : [],
19369
+ ...kosdaqResult.success ? kosdaqResult.data : []
19370
+ ];
19371
+ const watchlistData = allStocks.filter(
19372
+ (s) => isuCds.has(s["ISU_CD"] ?? "")
19373
+ );
19374
+ const output = { date: date5, stocks: watchlistData };
19375
+ if (errors.length > 0) {
19376
+ output["warnings"] = errors;
19377
+ }
19378
+ writeOutput(JSON.stringify(output, null, 2));
19379
+ });
19380
+ }
19381
+
18808
19382
  // src/cli/commands/version.ts
18809
- import { readFileSync as readFileSync3 } from "node:fs";
19383
+ import { readFileSync as readFileSync5 } from "node:fs";
18810
19384
  import { fileURLToPath } from "node:url";
18811
- import { dirname, resolve } from "node:path";
19385
+ import { dirname as dirname4, resolve } from "node:path";
18812
19386
  function getLocalVersion() {
18813
19387
  const __filename = fileURLToPath(import.meta.url);
18814
- const __dirname = dirname(__filename);
19388
+ const __dirname = dirname4(__filename);
18815
19389
  const candidates = [
18816
19390
  resolve(__dirname, "..", "package.json"),
18817
19391
  resolve(__dirname, "..", "..", "..", "package.json")
18818
19392
  ];
18819
19393
  for (const candidate of candidates) {
18820
19394
  try {
18821
- const raw = readFileSync3(candidate, "utf-8");
19395
+ const raw = readFileSync5(candidate, "utf-8");
18822
19396
  const pkg = JSON.parse(raw);
18823
19397
  return pkg.version;
18824
19398
  } catch {
@@ -18900,17 +19474,12 @@ function registerUpdateCommand(program3) {
18900
19474
  }
18901
19475
 
18902
19476
  // src/cli/index.ts
18903
- var EXIT_CODES = {
18904
- SUCCESS: 0,
18905
- GENERAL_ERROR: 1,
18906
- USAGE_ERROR: 2,
18907
- NO_DATA: 3,
18908
- AUTH_FAILURE: 4,
18909
- RATE_LIMIT: 5,
18910
- SERVICE_NOT_APPROVED: 6
18911
- };
18912
19477
  var program2 = new Command();
18913
- program2.name("krx").description("Agent-native CLI for KRX (Korea Exchange) Open API").version(getVersion()).option("-o, --output <format>", "output format: json, table, ndjson").option("-f, --fields <fields>", "comma-separated fields to include").option("--dry-run", "show request without calling API").option("-v, --verbose", "verbose output to stderr");
19478
+ program2.name("krx").description("Agent-native CLI for KRX (Korea Exchange) Open API").version(getVersion()).option("-o, --output <format>", "output format: json, table, ndjson, csv").option("-f, --fields <fields>", "comma-separated fields to include").option("--dry-run", "show request without calling API").option("-v, --verbose", "verbose output to stderr").option("--code <isuCd>", "filter by stock code (ISU_CD)").option("--sort <field>", "sort results by field name").option("--asc", "sort ascending (default: descending)").option("--limit <n>", "limit number of results", parseInt).option("--no-cache", "bypass cache and fetch fresh data").option("--from <date>", "start date for range query (YYYYMMDD)").option("--to <date>", "end date for range query (YYYYMMDD)").option("--filter <expression>", 'filter results (e.g. "FLUC_RT > 5")').option("--save <path>", "save output to file instead of stdout").option(
19479
+ "--retries <n>",
19480
+ "max retries on network error (default: 3)",
19481
+ parseInt
19482
+ );
18914
19483
  registerAuthCommand(program2);
18915
19484
  registerIndexCommand(program2);
18916
19485
  registerStockCommand(program2);
@@ -18920,6 +19489,9 @@ registerDerivativeCommand(program2);
18920
19489
  registerCommodityCommand(program2);
18921
19490
  registerEsgCommand(program2);
18922
19491
  registerSchemaCommand(program2);
19492
+ registerCacheCommand(program2);
19493
+ registerMarketCommand(program2);
19494
+ registerWatchlistCommand(program2);
18923
19495
  registerVersionCommand(program2);
18924
19496
  registerUpdateCommand(program2);
18925
19497
  program2.exitOverride();